From 566fef5309b1635b5f32756a853fdb7458f55118 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 19 Oct 2023 10:11:04 +1100 Subject: [PATCH] [React] Purchasing and Sales (#5743) * Skeleton layout for purchasing and sales - Empty pages - Nav links - Tab panels * Add component - Customers - Suppliers - Manufacturers * Table fxies * Initial implementation of PurchaseOrderTable * More tables - Basic SalesOrderTable - Basic ReturnOrderTable * Improvements to InvenTreeTable component - Handle incorrect return type from server - Use state for recordCount * Formatting fixes --- .../src/components/tables/InvenTreeTable.tsx | 19 +++- .../tables/{ => general}/AttachmentTable.tsx | 16 +-- .../tables/general/CompanyTable.tsx | 63 +++++++++++ .../tables/purchasing/PurchaseOrderTable.tsx | 101 ++++++++++++++++++ .../tables/sales/ReturnOrderTable.tsx | 85 +++++++++++++++ .../tables/sales/SalesOrderTable.tsx | 87 +++++++++++++++ .../tables/settings/ProjectCodeTable.tsx | 2 - src/frontend/src/defaults/links.tsx | 4 +- src/frontend/src/pages/build/BuildDetail.tsx | 2 +- src/frontend/src/pages/part/PartDetail.tsx | 2 +- .../src/pages/purchasing/PurchasingIndex.tsx | 48 +++++++++ src/frontend/src/pages/sales/SalesIndex.tsx | 48 +++++++++ src/frontend/src/pages/stock/StockDetail.tsx | 2 +- src/frontend/src/router.tsx | 14 +++ src/frontend/src/states/ApiState.tsx | 5 + 15 files changed, 481 insertions(+), 17 deletions(-) rename src/frontend/src/components/tables/{ => general}/AttachmentTable.tsx (93%) create mode 100644 src/frontend/src/components/tables/general/CompanyTable.tsx create mode 100644 src/frontend/src/components/tables/purchasing/PurchaseOrderTable.tsx create mode 100644 src/frontend/src/components/tables/sales/ReturnOrderTable.tsx create mode 100644 src/frontend/src/components/tables/sales/SalesOrderTable.tsx create mode 100644 src/frontend/src/pages/purchasing/PurchasingIndex.tsx create mode 100644 src/frontend/src/pages/sales/SalesIndex.tsx diff --git a/src/frontend/src/components/tables/InvenTreeTable.tsx b/src/frontend/src/components/tables/InvenTreeTable.tsx index ba99dd8c27..d04ccae5af 100644 --- a/src/frontend/src/components/tables/InvenTreeTable.tsx +++ b/src/frontend/src/components/tables/InvenTreeTable.tsx @@ -347,7 +347,18 @@ export function InvenTreeTable({ setMissingRecordsText( tableProps.noRecordsText ?? t`No records found` ); - return response.data; + + // Extract returned data (accounting for pagination) and ensure it is a list + let results = response.data?.results ?? response.data ?? []; + + if (!Array.isArray(results)) { + setMissingRecordsText(t`Server returned incorrect data type`); + results = []; + } + + setRecordCount(response.data?.count ?? results.length); + + return results; case 400: setMissingRecordsText(t`Bad request`); break; @@ -389,6 +400,8 @@ export function InvenTreeTable({ refetchOnMount: true }); + const [recordCount, setRecordCount] = useState(0); + /* * Reload the table whenever the refetch changes * this allows us to programmatically refresh the table @@ -487,7 +500,7 @@ export function InvenTreeTable({ loaderVariant="dots" idAccessor={tableProps.idAccessor} minHeight={200} - totalRecords={data?.count ?? data?.length ?? 0} + totalRecords={recordCount} recordsPerPage={tableProps.pageSize ?? defaultPageSize} page={page} onPageChange={setPage} @@ -501,7 +514,7 @@ export function InvenTreeTable({ } fetching={isFetching} noRecordsText={missingRecordsText} - records={data?.results ?? data ?? []} + records={data} columns={dataColumns} onRowClick={tableProps.onRowClick} /> diff --git a/src/frontend/src/components/tables/AttachmentTable.tsx b/src/frontend/src/components/tables/general/AttachmentTable.tsx similarity index 93% rename from src/frontend/src/components/tables/AttachmentTable.tsx rename to src/frontend/src/components/tables/general/AttachmentTable.tsx index f377d43434..86907aa81f 100644 --- a/src/frontend/src/components/tables/AttachmentTable.tsx +++ b/src/frontend/src/components/tables/general/AttachmentTable.tsx @@ -6,18 +6,18 @@ import { notifications } from '@mantine/notifications'; import { IconExternalLink, IconFileUpload } from '@tabler/icons-react'; import { ReactNode, useEffect, useMemo, useState } from 'react'; -import { api } from '../../App'; +import { api } from '../../../App'; import { addAttachment, deleteAttachment, editAttachment -} from '../../functions/forms/AttachmentForms'; -import { useTableRefresh } from '../../hooks/TableRefresh'; -import { ApiPaths, apiUrl } from '../../states/ApiState'; -import { AttachmentLink } from '../items/AttachmentLink'; -import { TableColumn } from './Column'; -import { InvenTreeTable } from './InvenTreeTable'; -import { RowAction } from './RowActions'; +} from '../../../functions/forms/AttachmentForms'; +import { useTableRefresh } from '../../../hooks/TableRefresh'; +import { ApiPaths, apiUrl } from '../../../states/ApiState'; +import { AttachmentLink } from '../../items/AttachmentLink'; +import { TableColumn } from '../Column'; +import { InvenTreeTable } from '../InvenTreeTable'; +import { RowAction } from '../RowActions'; /** * Define set of columns to display for the attachment table diff --git a/src/frontend/src/components/tables/general/CompanyTable.tsx b/src/frontend/src/components/tables/general/CompanyTable.tsx new file mode 100644 index 0000000000..5880c9fe8c --- /dev/null +++ b/src/frontend/src/components/tables/general/CompanyTable.tsx @@ -0,0 +1,63 @@ +import { t } from '@lingui/macro'; +import { Group, Text } from '@mantine/core'; +import { useMemo } from 'react'; + +import { useTableRefresh } from '../../../hooks/TableRefresh'; +import { ApiPaths, apiUrl } from '../../../states/ApiState'; +import { Thumbnail } from '../../images/Thumbnail'; +import { InvenTreeTable } from '../InvenTreeTable'; + +/** + * A table which displays a list of company records, + * based on the provided filter parameters + */ +export function CompanyTable({ params }: { params?: any }) { + const { tableKey } = useTableRefresh('company'); + + const columns = useMemo(() => { + return [ + { + accessor: 'name', + title: t`Company Name`, + sortable: true, + render: (record: any) => { + return ( + + + {record.name} + + ); + } + }, + { + accessor: 'description', + title: t`Description`, + sortable: false, + switchable: true + }, + { + accessor: 'website', + title: t`Website`, + sortable: false, + switchable: true + } + ]; + }, []); + + return ( + + ); +} diff --git a/src/frontend/src/components/tables/purchasing/PurchaseOrderTable.tsx b/src/frontend/src/components/tables/purchasing/PurchaseOrderTable.tsx new file mode 100644 index 0000000000..0ffd43d2f4 --- /dev/null +++ b/src/frontend/src/components/tables/purchasing/PurchaseOrderTable.tsx @@ -0,0 +1,101 @@ +import { t } from '@lingui/macro'; +import { Group, Text } from '@mantine/core'; +import { useMemo } from 'react'; + +import { useTableRefresh } from '../../../hooks/TableRefresh'; +import { ApiPaths, apiUrl } from '../../../states/ApiState'; +import { Thumbnail } from '../../images/Thumbnail'; +import { InvenTreeTable } from '../InvenTreeTable'; + +export function PurchaseOrderTable({ params }: { params?: any }) { + const { tableKey } = useTableRefresh('purchase-order'); + + // TODO: Custom filters + + // TODO: Row actions + + // TODO: Table actions (e.g. create new purchase order) + + const tableColumns = useMemo(() => { + return [ + { + accessor: 'reference', + title: t`Reference`, + sortable: true, + switchable: false + }, + { + accessor: 'description', + title: t`Description`, + switchable: true + }, + { + accessor: 'supplier__name', + title: t`Supplier`, + sortable: true, + render: function (record: any) { + let supplier = record.supplier_detail ?? {}; + + return ( + + + {supplier?.name} + + ); + } + }, + { + accessor: 'supplier_reference', + title: t`Supplier Reference`, + switchable: true + }, + { + accessor: 'project_code', + title: t`Project Code`, + switchable: true + // TODO: Custom formatter + }, + { + accessor: 'status', + title: t`Status`, + sortable: true, + switchable: true + // TODO: Custom formatter + }, + { + accessor: 'creation_date', + title: t`Created`, + switchable: true + // TODO: Custom formatter + }, + { + accessor: 'target_date', + title: t`Target Date`, + switchable: true + // TODO: Custom formatter + }, + { + accessor: 'line_items', + title: t`Line Items`, + sortable: true, + switchable: true + } + // TODO: total_price + // TODO: responsible + ]; + }, []); + + return ( + + ); +} diff --git a/src/frontend/src/components/tables/sales/ReturnOrderTable.tsx b/src/frontend/src/components/tables/sales/ReturnOrderTable.tsx new file mode 100644 index 0000000000..192feeb978 --- /dev/null +++ b/src/frontend/src/components/tables/sales/ReturnOrderTable.tsx @@ -0,0 +1,85 @@ +import { t } from '@lingui/macro'; +import { Group, Text } from '@mantine/core'; +import { useMemo } from 'react'; + +import { useTableRefresh } from '../../../hooks/TableRefresh'; +import { ApiPaths, apiUrl } from '../../../states/ApiState'; +import { Thumbnail } from '../../images/Thumbnail'; +import { InvenTreeTable } from '../InvenTreeTable'; + +export function ReturnOrderTable({ params }: { params?: any }) { + const { tableKey } = useTableRefresh('return-orders'); + + // TODO: Custom filters + + // TODO: Row actions + + // TODO: Table actions (e.g. create new return order) + + const tableColumns = useMemo(() => { + return [ + { + accessor: 'reference', + title: t`Return Order`, + sortable: true + }, + { + accessor: 'description', + title: t`Description`, + switchable: true + }, + { + accessor: 'customer__name', + title: t`Customer`, + sortable: true, + render: function (record: any) { + let customer = record.customer_detail ?? {}; + + return ( + + + {customer?.name} + + ); + } + }, + { + accessor: 'customer_reference', + title: t`Customer Reference`, + switchable: true + }, + { + accessor: 'project_code', + title: t`Project Code`, + switchable: true + // TODO: Custom formatter + }, + { + accessor: 'status', + title: t`Status`, + sortable: true, + switchable: true + // TODO: Custom formatter + } + // TODO: Creation date + // TODO: Target date + // TODO: Line items + // TODO: Responsible + // TODO: Total cost + ]; + }, []); + + return ( + + ); +} diff --git a/src/frontend/src/components/tables/sales/SalesOrderTable.tsx b/src/frontend/src/components/tables/sales/SalesOrderTable.tsx new file mode 100644 index 0000000000..0375db21a0 --- /dev/null +++ b/src/frontend/src/components/tables/sales/SalesOrderTable.tsx @@ -0,0 +1,87 @@ +import { t } from '@lingui/macro'; +import { Group, Text } from '@mantine/core'; +import { useMemo } from 'react'; + +import { useTableRefresh } from '../../../hooks/TableRefresh'; +import { ApiPaths, apiUrl } from '../../../states/ApiState'; +import { Thumbnail } from '../../images/Thumbnail'; +import { InvenTreeTable } from '../InvenTreeTable'; + +export function SalesOrderTable({ params }: { params?: any }) { + const { tableKey } = useTableRefresh('sales-order'); + + // TODO: Custom filters + + // TODO: Row actions + + // TODO: Table actions (e.g. create new sales order) + + const tableColumns = useMemo(() => { + return [ + { + accessor: 'reference', + title: t`Sales Order`, + sortable: true, + switchable: false + }, + { + accessor: 'description', + title: t`Description`, + switchable: true + }, + { + accessor: 'customer__name', + title: t`Customer`, + sortable: true, + render: function (record: any) { + let customer = record.customer_detail ?? {}; + + return ( + + + {customer?.name} + + ); + } + }, + { + accessor: 'customer_reference', + title: t`Customer Reference`, + switchable: true + }, + { + accessor: 'project_code', + title: t`Project Code`, + switchable: true + // TODO: Custom formatter + }, + { + accessor: 'status', + title: t`Status`, + sortable: true, + switchable: true + // TODO: Custom formatter + } + + // TODO: Creation date + // TODO: Target date + // TODO: Shipment date + // TODO: Line items + // TODO: Total price + ]; + }, []); + + return ( + + ); +} diff --git a/src/frontend/src/components/tables/settings/ProjectCodeTable.tsx b/src/frontend/src/components/tables/settings/ProjectCodeTable.tsx index 1766a2948a..8333f60c6e 100644 --- a/src/frontend/src/components/tables/settings/ProjectCodeTable.tsx +++ b/src/frontend/src/components/tables/settings/ProjectCodeTable.tsx @@ -1,6 +1,5 @@ import { t } from '@lingui/macro'; import { ActionIcon, Text, Tooltip } from '@mantine/core'; -import { showNotification } from '@mantine/notifications'; import { IconCirclePlus } from '@tabler/icons-react'; import { useCallback, useMemo } from 'react'; @@ -9,7 +8,6 @@ import { openDeleteApiForm, openEditApiForm } from '../../../functions/forms'; -import { notYetImplemented } from '../../../functions/notifications'; import { useTableRefresh } from '../../../hooks/TableRefresh'; import { ApiPaths, apiUrl } from '../../../states/ApiState'; import { TableColumn } from '../Column'; diff --git a/src/frontend/src/defaults/links.tsx b/src/frontend/src/defaults/links.tsx index efc80bd035..93fc816d76 100644 --- a/src/frontend/src/defaults/links.tsx +++ b/src/frontend/src/defaults/links.tsx @@ -25,7 +25,9 @@ export const navTabs = [ { text: Dashboard, name: 'dashboard' }, { text: Parts, name: 'part' }, { text: Stock, name: 'stock' }, - { text: Build, name: 'build' } + { text: Build, name: 'build' }, + { text: Purchasing, name: 'purchasing' }, + { text: Sales, name: 'sales' } ]; if (IS_DEV_OR_DEMO) { navTabs.push({ text: Playground, name: 'playground' }); diff --git a/src/frontend/src/pages/build/BuildDetail.tsx b/src/frontend/src/pages/build/BuildDetail.tsx index 05924b0dea..b77416aed4 100644 --- a/src/frontend/src/pages/build/BuildDetail.tsx +++ b/src/frontend/src/pages/build/BuildDetail.tsx @@ -19,8 +19,8 @@ import { } from '../../components/items/Placeholder'; import { PageDetail } from '../../components/nav/PageDetail'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; -import { AttachmentTable } from '../../components/tables/AttachmentTable'; import { BuildOrderTable } from '../../components/tables/build/BuildOrderTable'; +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'; diff --git a/src/frontend/src/pages/part/PartDetail.tsx b/src/frontend/src/pages/part/PartDetail.tsx index c199080724..7b722191f4 100644 --- a/src/frontend/src/pages/part/PartDetail.tsx +++ b/src/frontend/src/pages/part/PartDetail.tsx @@ -31,7 +31,7 @@ import { ApiImage } from '../../components/images/ApiImage'; import { PlaceholderPanel } from '../../components/items/Placeholder'; import { PageDetail } from '../../components/nav/PageDetail'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; -import { AttachmentTable } from '../../components/tables/AttachmentTable'; +import { AttachmentTable } from '../../components/tables/general/AttachmentTable'; import { PartParameterTable } from '../../components/tables/part/PartParameterTable'; import { PartVariantTable } from '../../components/tables/part/PartVariantTable'; import { RelatedPartTable } from '../../components/tables/part/RelatedPartTable'; diff --git a/src/frontend/src/pages/purchasing/PurchasingIndex.tsx b/src/frontend/src/pages/purchasing/PurchasingIndex.tsx new file mode 100644 index 0000000000..4e2c7969d0 --- /dev/null +++ b/src/frontend/src/pages/purchasing/PurchasingIndex.tsx @@ -0,0 +1,48 @@ +import { t } from '@lingui/macro'; +import { Stack } from '@mantine/core'; +import { + IconBuildingFactory2, + IconBuildingStore, + IconShoppingCart +} from '@tabler/icons-react'; +import { useMemo } from 'react'; + +import { PageDetail } from '../../components/nav/PageDetail'; +import { PanelGroup } from '../../components/nav/PanelGroup'; +import { CompanyTable } from '../../components/tables/general/CompanyTable'; +import { PurchaseOrderTable } from '../../components/tables/purchasing/PurchaseOrderTable'; + +export default function PurchasingIndex() { + const panels = useMemo(() => { + return [ + { + name: 'purchaseorders', + label: t`Purchase Orders`, + icon: , + content: + // TODO: Add optional "calendar" display here... + }, + { + name: 'suppliers', + label: t`Suppliers`, + icon: , + content: + }, + { + name: 'manufacturer', + label: t`Manufacturers`, + icon: , + content: + } + ]; + }, []); + + return ( + <> + + + + + + ); +} diff --git a/src/frontend/src/pages/sales/SalesIndex.tsx b/src/frontend/src/pages/sales/SalesIndex.tsx new file mode 100644 index 0000000000..9ed47387f7 --- /dev/null +++ b/src/frontend/src/pages/sales/SalesIndex.tsx @@ -0,0 +1,48 @@ +import { t } from '@lingui/macro'; +import { Stack } from '@mantine/core'; +import { + IconBuildingStore, + IconTruckDelivery, + IconTruckReturn +} from '@tabler/icons-react'; +import { useMemo } from 'react'; + +import { PageDetail } from '../../components/nav/PageDetail'; +import { PanelGroup } from '../../components/nav/PanelGroup'; +import { CompanyTable } from '../../components/tables/general/CompanyTable'; +import { ReturnOrderTable } from '../../components/tables/sales/ReturnOrderTable'; +import { SalesOrderTable } from '../../components/tables/sales/SalesOrderTable'; + +export default function PurchasingIndex() { + const panels = useMemo(() => { + return [ + { + name: 'salesorders', + label: t`Sales Orders`, + icon: , + content: + }, + { + name: 'returnorders', + label: t`Return Orders`, + icon: , + content: + }, + { + name: 'suppliers', + label: t`Customers`, + icon: , + content: + } + ]; + }, []); + + return ( + <> + + + + + + ); +} diff --git a/src/frontend/src/pages/stock/StockDetail.tsx b/src/frontend/src/pages/stock/StockDetail.tsx index ecd6cdbb17..7a31b8b07b 100644 --- a/src/frontend/src/pages/stock/StockDetail.tsx +++ b/src/frontend/src/pages/stock/StockDetail.tsx @@ -15,7 +15,7 @@ import { useParams } from 'react-router-dom'; import { PlaceholderPanel } from '../../components/items/Placeholder'; import { PageDetail } from '../../components/nav/PageDetail'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; -import { AttachmentTable } from '../../components/tables/AttachmentTable'; +import { AttachmentTable } from '../../components/tables/general/AttachmentTable'; import { NotesEditor } from '../../components/widgets/MarkdownEditor'; import { useInstance } from '../../hooks/UseInstance'; import { ApiPaths, apiUrl } from '../../states/ApiState'; diff --git a/src/frontend/src/router.tsx b/src/frontend/src/router.tsx index fdbfcf6070..33c1e65119 100644 --- a/src/frontend/src/router.tsx +++ b/src/frontend/src/router.tsx @@ -34,6 +34,14 @@ export const BuildDetail = Loadable( lazy(() => import('./pages/build/BuildDetail')) ); +export const PurchasingIndex = Loadable( + lazy(() => import('./pages/purchasing/PurchasingIndex')) +); + +export const SalesIndex = Loadable( + lazy(() => import('./pages/sales/SalesIndex')) +); + export const Scan = Loadable(lazy(() => import('./pages/Index/Scan'))); export const Dashboard = Loadable( @@ -99,6 +107,12 @@ export const routes = ( } /> } /> + + } /> + + + } /> + } /> }> diff --git a/src/frontend/src/states/ApiState.tsx b/src/frontend/src/states/ApiState.tsx index cc16df07e3..d5069b1d3b 100644 --- a/src/frontend/src/states/ApiState.tsx +++ b/src/frontend/src/states/ApiState.tsx @@ -68,6 +68,9 @@ export enum ApiPaths { // Sales Order URLs sales_order_list = 'api-sales-order-list', + // Return Order URLs + return_order_list = 'api-return-order-list', + // Plugin URLs plugin_list = 'api-plugin-list', @@ -146,6 +149,8 @@ export function apiEndpoint(path: ApiPaths): string { return 'order/po/'; case ApiPaths.sales_order_list: return 'order/so/'; + case ApiPaths.return_order_list: + return 'order/ro/'; case ApiPaths.plugin_list: return 'plugins/'; case ApiPaths.project_code_list: