mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-31 05:05:42 +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:
		| @@ -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: { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user