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:
Oliver 2023-10-27 14:39:17 +11:00 committed by GitHub
parent 22e9b14743
commit 0acfaced83
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 470 additions and 76 deletions

View File

@ -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>
);

View File

@ -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();

View File

@ -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

View File

@ -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}

View File

@ -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');

View File

@ -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 />

View File

@ -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} />

View File

@ -67,9 +67,11 @@ export function CompanyTable({
...params
},
onRowClick: (row: any) => {
if (row.pk) {
let base = path ?? 'company';
navigate(`/${base}/${row.pk}`);
}
}
}}
/>
);

View File

@ -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}`);
}
}
}}
/>

View File

@ -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}/`);
}
}
}}
/>

View File

@ -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}/`);
}
}
}}
/>

View File

@ -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
}
}}
/>

View File

@ -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;
}

View File

@ -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>
</>

View File

@ -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>
]}

View File

@ -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} />

View File

@ -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}

View 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>
</>
);
}

View 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>
</>
);
}

View 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>
</>
);
}

View File

@ -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 />} />

View File

@ -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:

View File

@ -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: {