diff --git a/src/frontend/src/components/images/ApiImage.tsx b/src/frontend/src/components/images/ApiImage.tsx index 1b5ca4dd92..0221d04990 100644 --- a/src/frontend/src/components/images/ApiImage.tsx +++ b/src/frontend/src/components/images/ApiImage.tsx @@ -56,7 +56,7 @@ export function ApiImage(props: ImageProps) { return ( - + {imgQuery.isError && } ); diff --git a/src/frontend/src/components/items/ActionDropdown.tsx b/src/frontend/src/components/items/ActionDropdown.tsx index d10ee66160..530019b47c 100644 --- a/src/frontend/src/components/items/ActionDropdown.tsx +++ b/src/frontend/src/components/items/ActionDropdown.tsx @@ -39,12 +39,12 @@ export function ActionDropdown({ - {actions.map((action, index) => + {actions.map((action) => action.disabled ? null : ( - + { if (action.onClick != undefined) { action.onClick(); diff --git a/src/frontend/src/components/nav/BreadcrumbList.tsx b/src/frontend/src/components/nav/BreadcrumbList.tsx index 4b59c6a00a..7223396b29 100644 --- a/src/frontend/src/components/nav/BreadcrumbList.tsx +++ b/src/frontend/src/components/nav/BreadcrumbList.tsx @@ -14,7 +14,7 @@ export function BreadcrumbList({ breadcrumbs }: { breadcrumbs: Breadcrumb[] }) { return ( - + {breadcrumbs.map((breadcrumb, index) => { return ( - - + + + {imageUrl && ( + + )} - {title && {title}} - {subtitle && {subtitle}} - {detail} + {title && {title}} + {subtitle && ( + + {subtitle} + + )} + {detail} + {actions && ( {actions} diff --git a/src/frontend/src/components/renderers/StatusRenderer.tsx b/src/frontend/src/components/renderers/StatusRenderer.tsx index b0af1e1d76..f3a97ccf5d 100644 --- a/src/frontend/src/components/renderers/StatusRenderer.tsx +++ b/src/frontend/src/components/renderers/StatusRenderer.tsx @@ -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'); diff --git a/src/frontend/src/components/tables/InvenTreeTable.tsx b/src/frontend/src/components/tables/InvenTreeTable.tsx index d04ccae5af..1af28739b7 100644 --- a/src/frontend/src/components/tables/InvenTreeTable.tsx +++ b/src/frontend/src/components/tables/InvenTreeTable.tsx @@ -423,12 +423,13 @@ export function InvenTreeTable({ /> - + {tableProps.customActionGroups?.map( (group: any, idx: number) => group )} {(tableProps.barcodeActions?.length ?? 0 > 0) && ( } label={t`Barcode actions`} tooltip={t`Barcode actions`} @@ -437,6 +438,7 @@ export function InvenTreeTable({ )} {(tableProps.printingActions?.length ?? 0 > 0) && ( } label={t`Print actions`} tooltip={t`Print actions`} @@ -444,7 +446,10 @@ export function InvenTreeTable({ /> )} {tableProps.enableDownload && ( - + )} diff --git a/src/frontend/src/components/tables/general/AttachmentTable.tsx b/src/frontend/src/components/tables/general/AttachmentTable.tsx index 391be18191..ac7b3388ff 100644 --- a/src/frontend/src/components/tables/general/AttachmentTable.tsx +++ b/src/frontend/src/components/tables/general/AttachmentTable.tsx @@ -181,7 +181,7 @@ export function AttachmentTable({ if (allowEdit) { actions.push( - + { @@ -200,7 +200,7 @@ export function AttachmentTable({ ); actions.push( - + { @@ -226,6 +226,7 @@ export function AttachmentTable({ {pk && pk > 0 && ( )} {allowEdit && validPk && ( - + diff --git a/src/frontend/src/components/tables/general/CompanyTable.tsx b/src/frontend/src/components/tables/general/CompanyTable.tsx index 2a7cfe0582..73a40c1b12 100644 --- a/src/frontend/src/components/tables/general/CompanyTable.tsx +++ b/src/frontend/src/components/tables/general/CompanyTable.tsx @@ -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}`); + } } }} /> diff --git a/src/frontend/src/components/tables/purchasing/PurchaseOrderTable.tsx b/src/frontend/src/components/tables/purchasing/PurchaseOrderTable.tsx index 2f04bd4747..18e0783903 100644 --- a/src/frontend/src/components/tables/purchasing/PurchaseOrderTable.tsx +++ b/src/frontend/src/components/tables/purchasing/PurchaseOrderTable.tsx @@ -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}`); + } } }} /> diff --git a/src/frontend/src/components/tables/sales/ReturnOrderTable.tsx b/src/frontend/src/components/tables/sales/ReturnOrderTable.tsx index 94bfcc523b..3c8a94cbe1 100644 --- a/src/frontend/src/components/tables/sales/ReturnOrderTable.tsx +++ b/src/frontend/src/components/tables/sales/ReturnOrderTable.tsx @@ -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}/`); + } } }} /> diff --git a/src/frontend/src/components/tables/sales/SalesOrderTable.tsx b/src/frontend/src/components/tables/sales/SalesOrderTable.tsx index 3a25b75a5d..5f0589a366 100644 --- a/src/frontend/src/components/tables/sales/SalesOrderTable.tsx +++ b/src/frontend/src/components/tables/sales/SalesOrderTable.tsx @@ -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}/`); + } } }} /> diff --git a/src/frontend/src/components/tables/stock/StockItemTable.tsx b/src/frontend/src/components/tables/stock/StockItemTable.tsx index c29e5792ee..776ac66f4c 100644 --- a/src/frontend/src/components/tables/stock/StockItemTable.tsx +++ b/src/frontend/src/components/tables/stock/StockItemTable.tsx @@ -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 } }} /> diff --git a/src/frontend/src/hooks/UseInstance.tsx b/src/frontend/src/hooks/UseInstance.tsx index 7dbf10bfa4..ed1077bd6f 100644 --- a/src/frontend/src/hooks/UseInstance.tsx +++ b/src/frontend/src/hooks/UseInstance.tsx @@ -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; } diff --git a/src/frontend/src/pages/build/BuildDetail.tsx b/src/frontend/src/pages/build/BuildDetail.tsx index 72e60b57eb..c58f3d6e7c 100644 --- a/src/frontend/src/pages/build/BuildDetail.tsx +++ b/src/frontend/src/pages/build/BuildDetail.tsx @@ -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 ( + + + + + + + + + + + + + + + + +
{t`Base Part`}{build.part_detail?.name}
{t`Quantity`}{build.quantity}
{t`Build Status`} + {build.status && ( + + )} +
+
+
+ ); + }, [build]); const buildPanels: PanelType[] = useMemo(() => { return [ { name: 'details', label: t`Build Details`, - icon: , - content: + icon: , + content: buildDetailsPanel }, { name: 'allocate-stock', label: t`Allocate Stock`, - icon: , - content: + icon: // TODO: Hide if build is complete }, { name: 'incomplete-outputs', label: t`Incomplete Outputs`, - icon: , - content: + icon: // TODO: Hide if build is complete }, { name: 'complete-outputs', label: t`Completed Outputs`, - icon: , + icon: , content: ( , + icon: , content: ( , + icon: , content: ( , + icon: , content: ( , + icon: , content: ( } actions={[ @@ -170,6 +199,7 @@ export default function BuildDetail() { ]} />, } actions={[ @@ -181,6 +211,7 @@ export default function BuildDetail() { ]} />, } 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 ( <> + - diff --git a/src/frontend/src/pages/build/BuildIndex.tsx b/src/frontend/src/pages/build/BuildIndex.tsx index 652cc6c160..7e6fafbd44 100644 --- a/src/frontend/src/pages/build/BuildIndex.tsx +++ b/src/frontend/src/pages/build/BuildIndex.tsx @@ -15,7 +15,11 @@ export default function BuildIndex() { notYetImplemented()}> + ]} diff --git a/src/frontend/src/pages/company/CompanyDetail.tsx b/src/frontend/src/pages/company/CompanyDetail.tsx index f080fbb6e2..304e219d6d 100644 --- a/src/frontend/src/pages/company/CompanyDetail.tsx +++ b/src/frontend/src/pages/company/CompanyDetail.tsx @@ -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 ( - - - - - {company.name} - - {company.description} - - - ); - }, [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 [ } actions={[ @@ -216,8 +198,10 @@ export default function CompanyDetail(props: CompanyDetailProps) { diff --git a/src/frontend/src/pages/part/PartDetail.tsx b/src/frontend/src/pages/part/PartDetail.tsx index 3179a65ae7..a154a9f5fa 100644 --- a/src/frontend/src/pages/part/PartDetail.tsx +++ b/src/frontend/src/pages/part/PartDetail.tsx @@ -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 ( - - - {part.full_name} - - {part.description} + Stock: {part.in_stock} ); @@ -223,6 +213,7 @@ export default function PartDetail() { // TODO: Disable actions based on user permissions return [ } actions={[ @@ -246,6 +237,7 @@ export default function PartDetail() { ]} />, } actions={[ @@ -262,6 +254,7 @@ export default function PartDetail() { ]} />, } actions={[ @@ -297,6 +290,9 @@ export default function PartDetail() { { + return [ + { + name: 'detail', + label: t`Order Details`, + icon: + }, + { + name: 'line-items', + label: t`Line Items`, + icon: + }, + { + name: 'received-stock', + label: t`Received Stock`, + icon: , + content: ( + + ) + }, + { + name: 'attachments', + label: t`Attachments`, + icon: , + content: ( + + ) + }, + { + name: 'notes', + label: t`Notes`, + icon: , + content: ( + + ) + } + ]; + }, [order, id]); + + return ( + <> + + + + + + + ); +} diff --git a/src/frontend/src/pages/sales/ReturnOrderDetail.tsx b/src/frontend/src/pages/sales/ReturnOrderDetail.tsx new file mode 100644 index 0000000000..55844b9d16 --- /dev/null +++ b/src/frontend/src/pages/sales/ReturnOrderDetail.tsx @@ -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: + }, + { + name: 'attachments', + label: t`Attachments`, + icon: , + content: ( + + ) + }, + { + name: 'notes', + label: t`Notes`, + icon: , + content: ( + + ) + } + ]; + }, [order, id]); + + return ( + <> + + + + + + + ); +} diff --git a/src/frontend/src/pages/sales/SalesOrderDetail.tsx b/src/frontend/src/pages/sales/SalesOrderDetail.tsx new file mode 100644 index 0000000000..8fc3ca3b7c --- /dev/null +++ b/src/frontend/src/pages/sales/SalesOrderDetail.tsx @@ -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: + }, + { + name: 'line-items', + label: t`Line Items`, + icon: + }, + { + name: 'pending-shipments', + label: t`Pending Shipments`, + icon: + }, + { + name: 'completed-shipments', + label: t`Completed Shipments`, + icon: + }, + { + name: 'build-orders', + label: t`Build Orders`, + icon: + }, + { + name: 'attachments', + label: t`Attachments`, + icon: , + content: ( + + ) + }, + { + name: 'notes', + label: t`Notes`, + icon: , + content: ( + + ) + } + ]; + }, [order, id]); + + return ( + <> + + + + + + + ); +} diff --git a/src/frontend/src/router.tsx b/src/frontend/src/router.tsx index f67a85ece8..d044882ec7 100644 --- a/src/frontend/src/router.tsx +++ b/src/frontend/src/router.tsx @@ -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 = ( } /> + } /> } /> } /> } /> } /> + } /> + } /> } /> } /> diff --git a/src/frontend/src/states/ApiState.tsx b/src/frontend/src/states/ApiState.tsx index bd87b697e6..703e377a0f 100644 --- a/src/frontend/src/states/ApiState.tsx +++ b/src/frontend/src/states/ApiState.tsx @@ -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: diff --git a/src/frontend/vite.config.ts b/src/frontend/vite.config.ts index 10e0bf949c..827d69da81 100644 --- a/src/frontend/vite.config.ts +++ b/src/frontend/vite.config.ts @@ -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: {