mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
React interface updates (#5798)
* Fix for <ActionDropdown> component - Ensure component key is set properly * Update <PageDetail> component - Consolidate and simplify * Update proxy settings for react development * Fixes for StatusRenderer component - Cannot use state hook inside function * Add PurchaseOrderDetail page * Tweak ApiImage component * Add "ReceivedStock" table to PurchaseOrder detail page * Add SalesOrderDetail page * Add ReturnOrderDetail page * Cleanup unused variables * Remove import for unused icon
This commit is contained in:
parent
22e9b14743
commit
0acfaced83
@ -56,7 +56,7 @@ export function ApiImage(props: ImageProps) {
|
||||
return (
|
||||
<Stack>
|
||||
<LoadingOverlay visible={imgQuery.isLoading || imgQuery.isFetching} />
|
||||
<Image {...props} src={image} />
|
||||
<Image {...props} src={image} withPlaceholder fit="contain" />
|
||||
{imgQuery.isError && <Overlay color="#F00" />}
|
||||
</Stack>
|
||||
);
|
||||
|
@ -39,12 +39,12 @@ export function ActionDropdown({
|
||||
</Tooltip>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
{actions.map((action, index) =>
|
||||
{actions.map((action) =>
|
||||
action.disabled ? null : (
|
||||
<Tooltip label={action.tooltip}>
|
||||
<Tooltip label={action.tooltip} key={`tooltip-${action.name}`}>
|
||||
<Menu.Item
|
||||
icon={action.icon}
|
||||
key={index}
|
||||
key={action.name}
|
||||
onClick={() => {
|
||||
if (action.onClick != undefined) {
|
||||
action.onClick();
|
||||
|
@ -14,7 +14,7 @@ export function BreadcrumbList({ breadcrumbs }: { breadcrumbs: Breadcrumb[] }) {
|
||||
|
||||
return (
|
||||
<Paper p="3" radius="xs">
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs separator=">">
|
||||
{breadcrumbs.map((breadcrumb, index) => {
|
||||
return (
|
||||
<Anchor
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Group, Paper, Space, Stack, Text } from '@mantine/core';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { ApiImage } from '../images/ApiImage';
|
||||
import { StylishText } from '../items/StylishText';
|
||||
import { Breadcrumb, BreadcrumbList } from './BreadcrumbList';
|
||||
|
||||
@ -14,11 +15,13 @@ export function PageDetail({
|
||||
title,
|
||||
subtitle,
|
||||
detail,
|
||||
imageUrl,
|
||||
breadcrumbs,
|
||||
actions
|
||||
}: {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
imageUrl?: string;
|
||||
detail?: ReactNode;
|
||||
breadcrumbs?: Breadcrumb[];
|
||||
actions?: ReactNode[];
|
||||
@ -32,15 +35,23 @@ export function PageDetail({
|
||||
)}
|
||||
<Paper p="xs" radius="xs" shadow="xs">
|
||||
<Stack spacing="xs">
|
||||
<Group position="apart">
|
||||
<Group position="left">
|
||||
<Group position="apart" noWrap={true}>
|
||||
<Group position="left" noWrap={true}>
|
||||
{imageUrl && (
|
||||
<ApiImage src={imageUrl} radius="sm" height={64} width={64} />
|
||||
)}
|
||||
<Stack spacing="xs">
|
||||
{title && <StylishText size="xl">{title}</StylishText>}
|
||||
{subtitle && <Text size="lg">{subtitle}</Text>}
|
||||
{detail}
|
||||
{title && <StylishText size="lg">{title}</StylishText>}
|
||||
{subtitle && (
|
||||
<Text size="md" truncate>
|
||||
{subtitle}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</Group>
|
||||
<Space />
|
||||
{detail}
|
||||
<Space />
|
||||
{actions && (
|
||||
<Group spacing={5} position="right">
|
||||
{actions}
|
||||
|
@ -72,11 +72,18 @@ export const StatusRenderer = ({
|
||||
type: ModelType;
|
||||
options?: renderStatusLabelOptionsInterface;
|
||||
}) => {
|
||||
const [statusCodeList] = useServerApiState((state) => [state.status]);
|
||||
const statusCodeList = useServerApiState.getState().status;
|
||||
|
||||
if (status === undefined) {
|
||||
console.log('StatusRenderer: status is undefined');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (statusCodeList === undefined) {
|
||||
console.log('StatusRenderer: statusCodeList is undefined');
|
||||
return null;
|
||||
}
|
||||
|
||||
const statusCodes = statusCodeList[type];
|
||||
if (statusCodes === undefined) {
|
||||
console.log('StatusRenderer: statusCodes is undefined');
|
||||
|
@ -423,12 +423,13 @@ export function InvenTreeTable({
|
||||
/>
|
||||
<Stack>
|
||||
<Group position="apart">
|
||||
<Group position="left" spacing={5}>
|
||||
<Group position="left" key="custom-actions" spacing={5}>
|
||||
{tableProps.customActionGroups?.map(
|
||||
(group: any, idx: number) => group
|
||||
)}
|
||||
{(tableProps.barcodeActions?.length ?? 0 > 0) && (
|
||||
<ButtonMenu
|
||||
key="barcode-actions"
|
||||
icon={<IconBarcode />}
|
||||
label={t`Barcode actions`}
|
||||
tooltip={t`Barcode actions`}
|
||||
@ -437,6 +438,7 @@ export function InvenTreeTable({
|
||||
)}
|
||||
{(tableProps.printingActions?.length ?? 0 > 0) && (
|
||||
<ButtonMenu
|
||||
key="printing-actions"
|
||||
icon={<IconPrinter />}
|
||||
label={t`Print actions`}
|
||||
tooltip={t`Print actions`}
|
||||
@ -444,7 +446,10 @@ export function InvenTreeTable({
|
||||
/>
|
||||
)}
|
||||
{tableProps.enableDownload && (
|
||||
<DownloadAction downloadCallback={downloadData} />
|
||||
<DownloadAction
|
||||
key="download-action"
|
||||
downloadCallback={downloadData}
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
<Space />
|
||||
|
@ -181,7 +181,7 @@ export function AttachmentTable({
|
||||
|
||||
if (allowEdit) {
|
||||
actions.push(
|
||||
<Tooltip label={t`Add attachment`}>
|
||||
<Tooltip label={t`Add attachment`} key="attachment-add">
|
||||
<ActionIcon
|
||||
radius="sm"
|
||||
onClick={() => {
|
||||
@ -200,7 +200,7 @@ export function AttachmentTable({
|
||||
);
|
||||
|
||||
actions.push(
|
||||
<Tooltip label={t`Add external link`}>
|
||||
<Tooltip label={t`Add external link`} key="link-add">
|
||||
<ActionIcon
|
||||
radius="sm"
|
||||
onClick={() => {
|
||||
@ -226,6 +226,7 @@ export function AttachmentTable({
|
||||
<Stack spacing="xs">
|
||||
{pk && pk > 0 && (
|
||||
<InvenTreeTable
|
||||
key="attachment-table"
|
||||
url={url}
|
||||
tableKey={tableKey}
|
||||
columns={tableColumns}
|
||||
@ -241,7 +242,7 @@ export function AttachmentTable({
|
||||
/>
|
||||
)}
|
||||
{allowEdit && validPk && (
|
||||
<Dropzone onDrop={uploadFiles}>
|
||||
<Dropzone onDrop={uploadFiles} key="attachment-dropzone">
|
||||
<Dropzone.Idle>
|
||||
<Group position="center">
|
||||
<IconFileUpload size={24} />
|
||||
|
@ -67,8 +67,10 @@ export function CompanyTable({
|
||||
...params
|
||||
},
|
||||
onRowClick: (row: any) => {
|
||||
let base = path ?? 'company';
|
||||
navigate(`/${base}/${row.pk}`);
|
||||
if (row.pk) {
|
||||
let base = path ?? 'company';
|
||||
navigate(`/${base}/${row.pk}`);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { Group, Text } from '@mantine/core';
|
||||
import { useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useTableRefresh } from '../../../hooks/TableRefresh';
|
||||
import { ApiPaths, apiUrl } from '../../../states/ApiState';
|
||||
@ -9,7 +10,12 @@ import { ModelType } from '../../render/ModelType';
|
||||
import { StatusRenderer } from '../../renderers/StatusRenderer';
|
||||
import { InvenTreeTable } from '../InvenTreeTable';
|
||||
|
||||
/**
|
||||
* Display a table of purchase orders
|
||||
*/
|
||||
export function PurchaseOrderTable({ params }: { params?: any }) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { tableKey } = useTableRefresh('purchase-order');
|
||||
|
||||
// TODO: Custom filters
|
||||
@ -100,6 +106,11 @@ export function PurchaseOrderTable({ params }: { params?: any }) {
|
||||
params: {
|
||||
...params,
|
||||
supplier_detail: true
|
||||
},
|
||||
onRowClick: (row: any) => {
|
||||
if (row.pk) {
|
||||
navigate(`/purchasing/purchase-order/${row.pk}`);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { Group, Text } from '@mantine/core';
|
||||
import { useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useTableRefresh } from '../../../hooks/TableRefresh';
|
||||
import { ApiPaths, apiUrl } from '../../../states/ApiState';
|
||||
@ -12,6 +13,8 @@ import { InvenTreeTable } from '../InvenTreeTable';
|
||||
export function ReturnOrderTable({ params }: { params?: any }) {
|
||||
const { tableKey } = useTableRefresh('return-orders');
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
// TODO: Custom filters
|
||||
|
||||
// TODO: Row actions
|
||||
@ -80,6 +83,11 @@ export function ReturnOrderTable({ params }: { params?: any }) {
|
||||
params: {
|
||||
...params,
|
||||
customer_detail: true
|
||||
},
|
||||
onRowClick: (row: any) => {
|
||||
if (row.pk) {
|
||||
navigate(`/sales/return-order/${row.pk}/`);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { Group, Text } from '@mantine/core';
|
||||
import { useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useTableRefresh } from '../../../hooks/TableRefresh';
|
||||
import { ApiPaths, apiUrl } from '../../../states/ApiState';
|
||||
@ -12,6 +13,8 @@ import { InvenTreeTable } from '../InvenTreeTable';
|
||||
export function SalesOrderTable({ params }: { params?: any }) {
|
||||
const { tableKey } = useTableRefresh('sales-order');
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
// TODO: Custom filters
|
||||
|
||||
// TODO: Row actions
|
||||
@ -82,6 +85,11 @@ export function SalesOrderTable({ params }: { params?: any }) {
|
||||
params: {
|
||||
...params,
|
||||
customer_detail: true
|
||||
},
|
||||
onRowClick: (row: any) => {
|
||||
if (row.pk) {
|
||||
navigate(`/sales/sales-order/${row.pk}/`);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
@ -70,7 +70,8 @@ function stockItemTableColumns(): TableColumn[] {
|
||||
title: t`Location`,
|
||||
render: function (record: any) {
|
||||
// TODO: Custom renderer for location
|
||||
return record.location;
|
||||
// TODO: Note, if not "In stock" we don't want to display the actual location here
|
||||
return record?.location_detail?.pathstring ?? record.location ?? '-';
|
||||
}
|
||||
}
|
||||
// TODO: stocktake column
|
||||
@ -142,7 +143,8 @@ export function StockItemTable({ params = {} }: { params?: any }) {
|
||||
params: {
|
||||
...params,
|
||||
part_detail: true,
|
||||
location_detail: true
|
||||
location_detail: true,
|
||||
supplier_part_detail: true
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
@ -19,7 +19,7 @@ export function useInstance({
|
||||
params = {},
|
||||
defaultValue = {},
|
||||
hasPrimaryKey = true,
|
||||
refetchOnMount = false,
|
||||
refetchOnMount = true,
|
||||
refetchOnWindowFocus = false
|
||||
}: {
|
||||
endpoint: ApiPaths;
|
||||
@ -36,7 +36,7 @@ export function useInstance({
|
||||
queryKey: ['instance', endpoint, pk, params],
|
||||
queryFn: async () => {
|
||||
if (hasPrimaryKey) {
|
||||
if (pk == null || pk == undefined || pk.length == 0) {
|
||||
if (pk == null || pk == undefined || pk.length == 0 || pk == '-1') {
|
||||
setInstance(defaultValue);
|
||||
return null;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { Alert, LoadingOverlay, Stack, Text } from '@mantine/core';
|
||||
import { Group, LoadingOverlay, Stack, Table } from '@mantine/core';
|
||||
import {
|
||||
IconClipboardCheck,
|
||||
IconClipboardList,
|
||||
@ -23,12 +23,10 @@ import { useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { ActionDropdown } from '../../components/items/ActionDropdown';
|
||||
import {
|
||||
PlaceholderPanel,
|
||||
PlaceholderPill
|
||||
} from '../../components/items/Placeholder';
|
||||
import { PageDetail } from '../../components/nav/PageDetail';
|
||||
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
|
||||
import { ModelType } from '../../components/render/ModelType';
|
||||
import { StatusRenderer } from '../../components/renderers/StatusRenderer';
|
||||
import { BuildOrderTable } from '../../components/tables/build/BuildOrderTable';
|
||||
import { AttachmentTable } from '../../components/tables/general/AttachmentTable';
|
||||
import { StockItemTable } from '../../components/tables/stock/StockItemTable';
|
||||
@ -43,6 +41,8 @@ import { useUserState } from '../../states/UserState';
|
||||
export default function BuildDetail() {
|
||||
const { id } = useParams();
|
||||
|
||||
const user = useUserState();
|
||||
|
||||
const {
|
||||
instance: build,
|
||||
refreshInstance,
|
||||
@ -52,37 +52,65 @@ export default function BuildDetail() {
|
||||
pk: id,
|
||||
params: {
|
||||
part_detail: true
|
||||
}
|
||||
},
|
||||
refetchOnMount: true
|
||||
});
|
||||
|
||||
const user = useUserState();
|
||||
const buildDetailsPanel = useMemo(() => {
|
||||
return (
|
||||
<Group position="apart" grow>
|
||||
<Table striped>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{t`Base Part`}</td>
|
||||
<td>{build.part_detail?.name}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{t`Quantity`}</td>
|
||||
<td>{build.quantity}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{t`Build Status`}</td>
|
||||
<td>
|
||||
{build.status && (
|
||||
<StatusRenderer
|
||||
status={build.status}
|
||||
type={ModelType.build}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</Table>
|
||||
<Table></Table>
|
||||
</Group>
|
||||
);
|
||||
}, [build]);
|
||||
|
||||
const buildPanels: PanelType[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
name: 'details',
|
||||
label: t`Build Details`,
|
||||
icon: <IconInfoCircle size="18" />,
|
||||
content: <PlaceholderPanel />
|
||||
icon: <IconInfoCircle />,
|
||||
content: buildDetailsPanel
|
||||
},
|
||||
{
|
||||
name: 'allocate-stock',
|
||||
label: t`Allocate Stock`,
|
||||
icon: <IconListCheck size="18" />,
|
||||
content: <PlaceholderPanel />
|
||||
icon: <IconListCheck />
|
||||
// TODO: Hide if build is complete
|
||||
},
|
||||
{
|
||||
name: 'incomplete-outputs',
|
||||
label: t`Incomplete Outputs`,
|
||||
icon: <IconClipboardList size="18" />,
|
||||
content: <PlaceholderPanel />
|
||||
icon: <IconClipboardList />
|
||||
// TODO: Hide if build is complete
|
||||
},
|
||||
{
|
||||
name: 'complete-outputs',
|
||||
label: t`Completed Outputs`,
|
||||
icon: <IconClipboardCheck size="18" />,
|
||||
icon: <IconClipboardCheck />,
|
||||
content: (
|
||||
<StockItemTable
|
||||
params={{
|
||||
@ -95,7 +123,7 @@ export default function BuildDetail() {
|
||||
{
|
||||
name: 'consumed-stock',
|
||||
label: t`Consumed Stock`,
|
||||
icon: <IconList size="18" />,
|
||||
icon: <IconList />,
|
||||
content: (
|
||||
<StockItemTable
|
||||
params={{
|
||||
@ -107,7 +135,7 @@ export default function BuildDetail() {
|
||||
{
|
||||
name: 'child-orders',
|
||||
label: t`Child Build Orders`,
|
||||
icon: <IconSitemap size="18" />,
|
||||
icon: <IconSitemap />,
|
||||
content: (
|
||||
<BuildOrderTable
|
||||
params={{
|
||||
@ -119,7 +147,7 @@ export default function BuildDetail() {
|
||||
{
|
||||
name: 'attachments',
|
||||
label: t`Attachments`,
|
||||
icon: <IconPaperclip size="18" />,
|
||||
icon: <IconPaperclip />,
|
||||
content: (
|
||||
<AttachmentTable
|
||||
endpoint={ApiPaths.build_order_attachment_list}
|
||||
@ -131,7 +159,7 @@ export default function BuildDetail() {
|
||||
{
|
||||
name: 'notes',
|
||||
label: t`Notes`,
|
||||
icon: <IconNotes size="18" />,
|
||||
icon: <IconNotes />,
|
||||
content: (
|
||||
<NotesEditor
|
||||
url={apiUrl(ApiPaths.build_order_list, build.pk)}
|
||||
@ -147,6 +175,7 @@ export default function BuildDetail() {
|
||||
// TODO: Disable certain actions based on user permissions
|
||||
return [
|
||||
<ActionDropdown
|
||||
key="barcode"
|
||||
tooltip={t`Barcode Actions`}
|
||||
icon={<IconQrcode />}
|
||||
actions={[
|
||||
@ -170,6 +199,7 @@ export default function BuildDetail() {
|
||||
]}
|
||||
/>,
|
||||
<ActionDropdown
|
||||
key="report"
|
||||
tooltip={t`Reporting Actions`}
|
||||
icon={<IconPrinter />}
|
||||
actions={[
|
||||
@ -181,6 +211,7 @@ export default function BuildDetail() {
|
||||
]}
|
||||
/>,
|
||||
<ActionDropdown
|
||||
key="build"
|
||||
tooltip={t`Build Order Actions`}
|
||||
icon={<IconDots />}
|
||||
actions={[
|
||||
@ -204,19 +235,28 @@ export default function BuildDetail() {
|
||||
];
|
||||
}, [id, build, user]);
|
||||
|
||||
const buildDetail = useMemo(() => {
|
||||
return StatusRenderer({
|
||||
status: build.status,
|
||||
type: ModelType.build
|
||||
});
|
||||
}, [build, id]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack spacing="xs">
|
||||
<LoadingOverlay visible={instanceQuery.isFetching} />
|
||||
<PageDetail
|
||||
title={t`Build Order`}
|
||||
subtitle={build.reference}
|
||||
title={build.reference}
|
||||
subtitle={build.title}
|
||||
detail={buildDetail}
|
||||
imageUrl={build.part_detail?.thumbnail}
|
||||
breadcrumbs={[
|
||||
{ name: t`Build Orders`, url: '/build' },
|
||||
{ name: build.reference, url: `/build/${build.pk}` }
|
||||
]}
|
||||
actions={buildActions}
|
||||
/>
|
||||
<LoadingOverlay visible={instanceQuery.isFetching} />
|
||||
<PanelGroup pageKey="build" panels={buildPanels} />
|
||||
</Stack>
|
||||
</>
|
||||
|
@ -15,7 +15,11 @@ export default function BuildIndex() {
|
||||
<PageDetail
|
||||
title={t`Build Orders`}
|
||||
actions={[
|
||||
<Button color="green" onClick={() => notYetImplemented()}>
|
||||
<Button
|
||||
key="new-build"
|
||||
color="green"
|
||||
onClick={() => notYetImplemented()}
|
||||
>
|
||||
<Text>{t`New Build Order`}</Text>
|
||||
</Button>
|
||||
]}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { Group, LoadingOverlay, Stack, Text } from '@mantine/core';
|
||||
import { LoadingOverlay, Stack } from '@mantine/core';
|
||||
import {
|
||||
IconBuildingFactory2,
|
||||
IconBuildingWarehouse,
|
||||
@ -20,7 +20,6 @@ import {
|
||||
import { useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { Thumbnail } from '../../components/images/Thumbnail';
|
||||
import { ActionDropdown } from '../../components/items/ActionDropdown';
|
||||
import { Breadcrumb } from '../../components/nav/BreadcrumbList';
|
||||
import { PageDetail } from '../../components/nav/PageDetail';
|
||||
@ -159,24 +158,6 @@ export default function CompanyDetail(props: CompanyDetailProps) {
|
||||
];
|
||||
}, [id, company]);
|
||||
|
||||
const companyDetail = useMemo(() => {
|
||||
return (
|
||||
<Group spacing="xs" noWrap={true}>
|
||||
<Thumbnail
|
||||
src={String(company.image || '')}
|
||||
size={128}
|
||||
alt={company?.name}
|
||||
/>
|
||||
<Stack spacing="xs">
|
||||
<Text size="lg" weight={500}>
|
||||
{company.name}
|
||||
</Text>
|
||||
<Text size="sm">{company.description}</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
);
|
||||
}, [id, company]);
|
||||
|
||||
const companyActions = useMemo(() => {
|
||||
// TODO: Finer fidelity on these permissions, perhaps?
|
||||
let canEdit = user.checkUserRole('purchase_order', 'change');
|
||||
@ -184,6 +165,7 @@ export default function CompanyDetail(props: CompanyDetailProps) {
|
||||
|
||||
return [
|
||||
<ActionDropdown
|
||||
key="company"
|
||||
tooltip={t`Company Actions`}
|
||||
icon={<IconDots />}
|
||||
actions={[
|
||||
@ -216,8 +198,10 @@ export default function CompanyDetail(props: CompanyDetailProps) {
|
||||
<Stack spacing="xs">
|
||||
<LoadingOverlay visible={instanceQuery.isFetching} />
|
||||
<PageDetail
|
||||
detail={companyDetail}
|
||||
title={t`Company` + `: ${company.name}`}
|
||||
subtitle={company.description}
|
||||
actions={companyActions}
|
||||
imageUrl={company.image}
|
||||
breadcrumbs={props.breadcrumbs}
|
||||
/>
|
||||
<PanelGroup pageKey="company" panels={companyPanels} />
|
||||
|
@ -30,7 +30,6 @@ import {
|
||||
import { useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { ApiImage } from '../../components/images/ApiImage';
|
||||
import { ActionDropdown } from '../../components/items/ActionDropdown';
|
||||
import { PageDetail } from '../../components/nav/PageDetail';
|
||||
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
|
||||
@ -203,17 +202,8 @@ export default function PartDetail() {
|
||||
const partDetail = useMemo(() => {
|
||||
return (
|
||||
<Group spacing="xs" noWrap={true}>
|
||||
<ApiImage
|
||||
src={String(part.image || '')}
|
||||
radius="sm"
|
||||
height={64}
|
||||
width={64}
|
||||
/>
|
||||
<Stack spacing="xs">
|
||||
<Text size="lg" weight={500}>
|
||||
{part.full_name}
|
||||
</Text>
|
||||
<Text size="sm">{part.description}</Text>
|
||||
<Text>Stock: {part.in_stock}</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
);
|
||||
@ -223,6 +213,7 @@ export default function PartDetail() {
|
||||
// TODO: Disable actions based on user permissions
|
||||
return [
|
||||
<ActionDropdown
|
||||
key="barcode"
|
||||
tooltip={t`Barcode Actions`}
|
||||
icon={<IconQrcode />}
|
||||
actions={[
|
||||
@ -246,6 +237,7 @@ export default function PartDetail() {
|
||||
]}
|
||||
/>,
|
||||
<ActionDropdown
|
||||
key="stock"
|
||||
tooltip={t`Stock Actions`}
|
||||
icon={<IconPackages />}
|
||||
actions={[
|
||||
@ -262,6 +254,7 @@ export default function PartDetail() {
|
||||
]}
|
||||
/>,
|
||||
<ActionDropdown
|
||||
key="part"
|
||||
tooltip={t`Part Actions`}
|
||||
icon={<IconDots />}
|
||||
actions={[
|
||||
@ -297,6 +290,9 @@ export default function PartDetail() {
|
||||
<Stack spacing="xs">
|
||||
<LoadingOverlay visible={instanceQuery.isFetching} />
|
||||
<PageDetail
|
||||
title={t`Part` + ': ' + part.full_name}
|
||||
subtitle={part.description}
|
||||
imageUrl={part.image}
|
||||
detail={partDetail}
|
||||
breadcrumbs={breadcrumbs}
|
||||
actions={partActions}
|
||||
|
101
src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx
Normal file
101
src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx
Normal file
@ -0,0 +1,101 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { LoadingOverlay, Stack } from '@mantine/core';
|
||||
import {
|
||||
IconInfoCircle,
|
||||
IconList,
|
||||
IconNotes,
|
||||
IconPackages,
|
||||
IconPaperclip
|
||||
} from '@tabler/icons-react';
|
||||
import { useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { PageDetail } from '../../components/nav/PageDetail';
|
||||
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
|
||||
import { AttachmentTable } from '../../components/tables/general/AttachmentTable';
|
||||
import { StockItemTable } from '../../components/tables/stock/StockItemTable';
|
||||
import { NotesEditor } from '../../components/widgets/MarkdownEditor';
|
||||
import { useInstance } from '../../hooks/UseInstance';
|
||||
import { ApiPaths, apiUrl } from '../../states/ApiState';
|
||||
|
||||
/**
|
||||
* Detail page for a single PurchaseOrder
|
||||
*/
|
||||
export default function PurchaseOrderDetail() {
|
||||
const { id } = useParams();
|
||||
|
||||
const { instance: order, instanceQuery } = useInstance({
|
||||
endpoint: ApiPaths.purchase_order_list,
|
||||
pk: id,
|
||||
params: {
|
||||
supplier_detail: true
|
||||
},
|
||||
refetchOnMount: true
|
||||
});
|
||||
|
||||
const orderPanels: PanelType[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
name: 'detail',
|
||||
label: t`Order Details`,
|
||||
icon: <IconInfoCircle />
|
||||
},
|
||||
{
|
||||
name: 'line-items',
|
||||
label: t`Line Items`,
|
||||
icon: <IconList />
|
||||
},
|
||||
{
|
||||
name: 'received-stock',
|
||||
label: t`Received Stock`,
|
||||
icon: <IconPackages />,
|
||||
content: (
|
||||
<StockItemTable
|
||||
params={{
|
||||
purchase_order: id
|
||||
}}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'attachments',
|
||||
label: t`Attachments`,
|
||||
icon: <IconPaperclip />,
|
||||
content: (
|
||||
<AttachmentTable
|
||||
endpoint={ApiPaths.purchase_order_attachment_list}
|
||||
model="order"
|
||||
pk={order.pk ?? -1}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'notes',
|
||||
label: t`Notes`,
|
||||
icon: <IconNotes />,
|
||||
content: (
|
||||
<NotesEditor
|
||||
url={apiUrl(ApiPaths.purchase_order_list, order.pk)}
|
||||
data={order.notes ?? ''}
|
||||
allowEdit={true}
|
||||
/>
|
||||
)
|
||||
}
|
||||
];
|
||||
}, [order, id]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack spacing="xs">
|
||||
<LoadingOverlay visible={instanceQuery.isFetching} />
|
||||
<PageDetail
|
||||
title={t`Purchase Order` + `: ${order.reference}`}
|
||||
subtitle={order.description}
|
||||
imageUrl={order.supplier_detail?.image}
|
||||
breadcrumbs={[{ name: t`Purchasing`, url: '/purchasing/' }]}
|
||||
/>
|
||||
<PanelGroup pageKey="purchaseorder" panels={orderPanels} />
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
76
src/frontend/src/pages/sales/ReturnOrderDetail.tsx
Normal file
76
src/frontend/src/pages/sales/ReturnOrderDetail.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { LoadingOverlay, Stack } from '@mantine/core';
|
||||
import { IconInfoCircle, IconNotes, IconPaperclip } from '@tabler/icons-react';
|
||||
import { useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { PageDetail } from '../../components/nav/PageDetail';
|
||||
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
|
||||
import { AttachmentTable } from '../../components/tables/general/AttachmentTable';
|
||||
import { NotesEditor } from '../../components/widgets/MarkdownEditor';
|
||||
import { useInstance } from '../../hooks/UseInstance';
|
||||
import { ApiPaths, apiUrl } from '../../states/ApiState';
|
||||
|
||||
/**
|
||||
* Detail page for a single ReturnOrder
|
||||
*/
|
||||
export default function ReturnOrderDetail() {
|
||||
const { id } = useParams();
|
||||
|
||||
const { instance: order, instanceQuery } = useInstance({
|
||||
endpoint: ApiPaths.return_order_list,
|
||||
pk: id,
|
||||
params: {
|
||||
customer_detail: true
|
||||
}
|
||||
});
|
||||
|
||||
const orderPanels: PanelType[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
name: 'detail',
|
||||
label: t`Order Details`,
|
||||
icon: <IconInfoCircle />
|
||||
},
|
||||
{
|
||||
name: 'attachments',
|
||||
label: t`Attachments`,
|
||||
icon: <IconPaperclip />,
|
||||
content: (
|
||||
<AttachmentTable
|
||||
endpoint={ApiPaths.return_order_attachment_list}
|
||||
model="order"
|
||||
pk={order.pk ?? -1}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'notes',
|
||||
label: t`Notes`,
|
||||
icon: <IconNotes />,
|
||||
content: (
|
||||
<NotesEditor
|
||||
url={apiUrl(ApiPaths.return_order_list, order.pk)}
|
||||
data={order.notes ?? ''}
|
||||
allowEdit={true}
|
||||
/>
|
||||
)
|
||||
}
|
||||
];
|
||||
}, [order, id]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack spacing="xs">
|
||||
<LoadingOverlay visible={instanceQuery.isFetching} />
|
||||
<PageDetail
|
||||
title={t`Return Order` + `: ${order.reference}`}
|
||||
subtitle={order.description}
|
||||
imageUrl={order.customer_detail?.image}
|
||||
breadcrumbs={[{ name: t`Sales`, url: '/sales/' }]}
|
||||
/>
|
||||
<PanelGroup pageKey="returnorder" panels={orderPanels} />
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
104
src/frontend/src/pages/sales/SalesOrderDetail.tsx
Normal file
104
src/frontend/src/pages/sales/SalesOrderDetail.tsx
Normal file
@ -0,0 +1,104 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { LoadingOverlay, Stack } from '@mantine/core';
|
||||
import {
|
||||
IconInfoCircle,
|
||||
IconList,
|
||||
IconNotes,
|
||||
IconPaperclip,
|
||||
IconTools,
|
||||
IconTruckDelivery,
|
||||
IconTruckLoading
|
||||
} from '@tabler/icons-react';
|
||||
import { useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { PageDetail } from '../../components/nav/PageDetail';
|
||||
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
|
||||
import { AttachmentTable } from '../../components/tables/general/AttachmentTable';
|
||||
import { NotesEditor } from '../../components/widgets/MarkdownEditor';
|
||||
import { useInstance } from '../../hooks/UseInstance';
|
||||
import { ApiPaths, apiUrl } from '../../states/ApiState';
|
||||
|
||||
/**
|
||||
* Detail page for a single SalesOrder
|
||||
*/
|
||||
export default function SalesOrderDetail() {
|
||||
const { id } = useParams();
|
||||
|
||||
const { instance: order, instanceQuery } = useInstance({
|
||||
endpoint: ApiPaths.sales_order_list,
|
||||
pk: id,
|
||||
params: {
|
||||
customer_detail: true
|
||||
}
|
||||
});
|
||||
|
||||
const orderPanels: PanelType[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
name: 'detail',
|
||||
label: t`Order Details`,
|
||||
icon: <IconInfoCircle />
|
||||
},
|
||||
{
|
||||
name: 'line-items',
|
||||
label: t`Line Items`,
|
||||
icon: <IconList />
|
||||
},
|
||||
{
|
||||
name: 'pending-shipments',
|
||||
label: t`Pending Shipments`,
|
||||
icon: <IconTruckLoading />
|
||||
},
|
||||
{
|
||||
name: 'completed-shipments',
|
||||
label: t`Completed Shipments`,
|
||||
icon: <IconTruckDelivery />
|
||||
},
|
||||
{
|
||||
name: 'build-orders',
|
||||
label: t`Build Orders`,
|
||||
icon: <IconTools />
|
||||
},
|
||||
{
|
||||
name: 'attachments',
|
||||
label: t`Attachments`,
|
||||
icon: <IconPaperclip />,
|
||||
content: (
|
||||
<AttachmentTable
|
||||
endpoint={ApiPaths.sales_order_attachment_list}
|
||||
model="order"
|
||||
pk={order.pk ?? -1}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'notes',
|
||||
label: t`Notes`,
|
||||
icon: <IconNotes />,
|
||||
content: (
|
||||
<NotesEditor
|
||||
url={apiUrl(ApiPaths.sales_order_list, order.pk)}
|
||||
data={order.notes ?? ''}
|
||||
allowEdit={true}
|
||||
/>
|
||||
)
|
||||
}
|
||||
];
|
||||
}, [order, id]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack spacing="xs">
|
||||
<LoadingOverlay visible={instanceQuery.isFetching} />
|
||||
<PageDetail
|
||||
title={t`Sales Order` + `: ${order.reference}`}
|
||||
subtitle={order.description}
|
||||
imageUrl={order.customer_detail?.image}
|
||||
breadcrumbs={[{ name: t`Sales`, url: '/sales/' }]}
|
||||
/>
|
||||
<PanelGroup pageKey="salesorder" panels={orderPanels} />
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
@ -54,10 +54,22 @@ export const PurchasingIndex = Loadable(
|
||||
lazy(() => import('./pages/purchasing/PurchasingIndex'))
|
||||
);
|
||||
|
||||
export const PurchaseOrderDetail = Loadable(
|
||||
lazy(() => import('./pages/purchasing/PurchaseOrderDetail'))
|
||||
);
|
||||
|
||||
export const SalesIndex = Loadable(
|
||||
lazy(() => import('./pages/sales/SalesIndex'))
|
||||
);
|
||||
|
||||
export const SalesOrderDetail = Loadable(
|
||||
lazy(() => import('./pages/sales/SalesOrderDetail'))
|
||||
);
|
||||
|
||||
export const ReturnOrderDetail = Loadable(
|
||||
lazy(() => import('./pages/sales/ReturnOrderDetail'))
|
||||
);
|
||||
|
||||
export const Scan = Loadable(lazy(() => import('./pages/Index/Scan')));
|
||||
|
||||
export const Dashboard = Loadable(
|
||||
@ -125,12 +137,15 @@ export const routes = (
|
||||
</Route>
|
||||
<Route path="purchasing/">
|
||||
<Route index element={<PurchasingIndex />} />
|
||||
<Route path="purchase-order/:id/" element={<PurchaseOrderDetail />} />
|
||||
<Route path="supplier/:id/" element={<SupplierDetail />} />
|
||||
<Route path="manufacturer/:id/" element={<ManufacturerDetail />} />
|
||||
</Route>
|
||||
<Route path="company/:id/" element={<CompanyDetail />} />
|
||||
<Route path="sales/">
|
||||
<Route index element={<SalesIndex />} />
|
||||
<Route path="sales-order/:id/" element={<SalesOrderDetail />} />
|
||||
<Route path="return-order/:id/" element={<ReturnOrderDetail />} />
|
||||
<Route path="customer/:id/" element={<CustomerDetail />} />
|
||||
</Route>
|
||||
<Route path="/profile/:tabValue" element={<Profile />} />
|
||||
|
@ -93,12 +93,15 @@ export enum ApiPaths {
|
||||
|
||||
// Purchase Order URLs
|
||||
purchase_order_list = 'api-purchase-order-list',
|
||||
purchase_order_attachment_list = 'api-purchase-order-attachment-list',
|
||||
|
||||
// Sales Order URLs
|
||||
sales_order_list = 'api-sales-order-list',
|
||||
sales_order_attachment_list = 'api-sales-order-attachment-list',
|
||||
|
||||
// Return Order URLs
|
||||
return_order_list = 'api-return-order-list',
|
||||
return_order_attachment_list = 'api-return-order-attachment-list',
|
||||
|
||||
// Plugin URLs
|
||||
plugin_list = 'api-plugin-list',
|
||||
@ -180,10 +183,16 @@ export function apiEndpoint(path: ApiPaths): string {
|
||||
return 'stock/attachment/';
|
||||
case ApiPaths.purchase_order_list:
|
||||
return 'order/po/';
|
||||
case ApiPaths.purchase_order_attachment_list:
|
||||
return 'order/po/attachment/';
|
||||
case ApiPaths.sales_order_list:
|
||||
return 'order/so/';
|
||||
case ApiPaths.sales_order_attachment_list:
|
||||
return 'order/so/attachment/';
|
||||
case ApiPaths.return_order_list:
|
||||
return 'order/ro/';
|
||||
case ApiPaths.return_order_attachment_list:
|
||||
return 'order/ro/attachment/';
|
||||
case ApiPaths.plugin_list:
|
||||
return 'plugins/';
|
||||
case ApiPaths.project_code_list:
|
||||
|
@ -28,6 +28,16 @@ export default defineConfig({
|
||||
target: 'http://localhost:8000',
|
||||
changeOrigin: true,
|
||||
secure: true
|
||||
},
|
||||
'/media': {
|
||||
target: 'http://localhost:8000',
|
||||
changeOrigin: true,
|
||||
secure: true
|
||||
},
|
||||
'/static': {
|
||||
target: 'http://localhost:8000',
|
||||
changeOrigin: true,
|
||||
secure: true
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
Loading…
Reference in New Issue
Block a user