diff --git a/src/frontend/src/components/forms/ApiForm.tsx b/src/frontend/src/components/forms/ApiForm.tsx index 3c7e526694..d9daf89b51 100644 --- a/src/frontend/src/components/forms/ApiForm.tsx +++ b/src/frontend/src/components/forms/ApiForm.tsx @@ -49,7 +49,7 @@ export interface ApiFormProps { postFormContent?: JSX.Element | (() => JSX.Element); successMessage?: string; onClose?: () => void; - onFormSuccess?: () => void; + onFormSuccess?: (data: any) => void; onFormError?: () => void; } @@ -184,7 +184,7 @@ export function ApiForm({ // Optionally call the onFormSuccess callback if (props.onFormSuccess) { - props.onFormSuccess(); + props.onFormSuccess(response.data); } // Optionally show a success message diff --git a/src/frontend/src/components/items/ActionDropdown.tsx b/src/frontend/src/components/items/ActionDropdown.tsx index b5b9ce17c7..efbe6dee4b 100644 --- a/src/frontend/src/components/items/ActionDropdown.tsx +++ b/src/frontend/src/components/items/ActionDropdown.tsx @@ -92,16 +92,16 @@ export function BarcodeActionDropdown({ // Common action button for viewing a barcode export function ViewBarcodeAction({ disabled = false, - callback + onClick }: { disabled?: boolean; - callback?: () => void; + onClick?: () => void; }): ActionDropdownItem { return { icon: , name: t`View`, tooltip: t`View barcode`, - onClick: callback, + onClick: onClick, disabled: disabled }; } @@ -109,16 +109,16 @@ export function ViewBarcodeAction({ // Common action button for linking a custom barcode export function LinkBarcodeAction({ disabled = false, - callback + onClick }: { disabled?: boolean; - callback?: () => void; + onClick?: () => void; }): ActionDropdownItem { return { icon: , name: t`Link Barcode`, tooltip: t`Link custom barcode`, - onClick: callback, + onClick: onClick, disabled: disabled }; } @@ -126,16 +126,16 @@ export function LinkBarcodeAction({ // Common action button for un-linking a custom barcode export function UnlinkBarcodeAction({ disabled = false, - callback + onClick }: { disabled?: boolean; - callback?: () => void; + onClick?: () => void; }): ActionDropdownItem { return { icon: , name: t`Unlink Barcode`, tooltip: t`Unlink custom barcode`, - onClick: callback, + onClick: onClick, disabled: disabled }; } @@ -144,17 +144,17 @@ export function UnlinkBarcodeAction({ export function EditItemAction({ disabled = false, tooltip, - callback + onClick }: { disabled?: boolean; tooltip?: string; - callback?: () => void; + onClick?: () => void; }): ActionDropdownItem { return { icon: , name: t`Edit`, tooltip: tooltip ?? `Edit item`, - onClick: callback, + onClick: onClick, disabled: disabled }; } @@ -163,17 +163,17 @@ export function EditItemAction({ export function DeleteItemAction({ disabled = false, tooltip, - callback + onClick }: { disabled?: boolean; tooltip?: string; - callback?: () => void; + onClick?: () => void; }): ActionDropdownItem { return { icon: , name: t`Delete`, tooltip: tooltip ?? t`Delete item`, - onClick: callback, + onClick: onClick, disabled: disabled }; } @@ -182,17 +182,17 @@ export function DeleteItemAction({ export function DuplicateItemAction({ disabled = false, tooltip, - callback + onClick }: { disabled?: boolean; tooltip?: string; - callback?: () => void; + onClick?: () => void; }): ActionDropdownItem { return { icon: , name: t`Duplicate`, tooltip: tooltip ?? t`Duplicate item`, - onClick: callback, + onClick: onClick, disabled: disabled }; } diff --git a/src/frontend/src/components/render/Generic.tsx b/src/frontend/src/components/render/Generic.tsx new file mode 100644 index 0000000000..fb616aa692 --- /dev/null +++ b/src/frontend/src/components/render/Generic.tsx @@ -0,0 +1,14 @@ +import { ReactNode } from 'react'; + +import { RenderInlineModel } from './Instance'; + +export function RenderProjectCode({ instance }: { instance: any }): ReactNode { + return ( + instance && ( + + ) + ); +} diff --git a/src/frontend/src/components/render/Instance.tsx b/src/frontend/src/components/render/Instance.tsx index c87e61c394..2ace53f7ac 100644 --- a/src/frontend/src/components/render/Instance.tsx +++ b/src/frontend/src/components/render/Instance.tsx @@ -12,6 +12,7 @@ import { RenderManufacturerPart, RenderSupplierPart } from './Company'; +import { RenderProjectCode } from './Generic'; import { ModelType } from './ModelType'; import { RenderPurchaseOrder, @@ -47,6 +48,7 @@ const RendererLookup: EnumDictionary< [ModelType.part]: RenderPart, [ModelType.partcategory]: RenderPartCategory, [ModelType.partparametertemplate]: RenderPartParameterTemplate, + [ModelType.projectcode]: RenderProjectCode, [ModelType.purchaseorder]: RenderPurchaseOrder, [ModelType.purchaseorderline]: RenderPurchaseOrder, [ModelType.returnorder]: RenderReturnOrder, diff --git a/src/frontend/src/components/render/ModelType.tsx b/src/frontend/src/components/render/ModelType.tsx index 201ad8e39c..1976f95151 100644 --- a/src/frontend/src/components/render/ModelType.tsx +++ b/src/frontend/src/components/render/ModelType.tsx @@ -6,6 +6,7 @@ export enum ModelType { manufacturerpart = 'manufacturerpart', partcategory = 'partcategory', partparametertemplate = 'partparametertemplate', + projectcode = 'projectcode', stockitem = 'stockitem', stocklocation = 'stocklocation', stockhistory = 'stockhistory', @@ -92,6 +93,12 @@ export const ModelInformationDict: ModelDictory = { url_overview: '/company', url_detail: '/company/:pk/' }, + projectcode: { + label: t`Project Code`, + label_multiple: t`Project Codes`, + url_overview: '/project-code', + url_detail: '/project-code/:pk/' + }, purchaseorder: { label: t`Purchase Order`, label_multiple: t`Purchase Orders`, diff --git a/src/frontend/src/components/tables/build/BuildOrderTable.tsx b/src/frontend/src/components/tables/build/BuildOrderTable.tsx index 2de0e78ad3..b39b2a85c3 100644 --- a/src/frontend/src/components/tables/build/BuildOrderTable.tsx +++ b/src/frontend/src/components/tables/build/BuildOrderTable.tsx @@ -1,10 +1,13 @@ import { t } from '@lingui/macro'; import { Text } from '@mantine/core'; -import { useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; +import { buildOrderFields } from '../../../forms/BuildForms'; +import { openCreateApiForm } from '../../../functions/forms'; import { useTableRefresh } from '../../../hooks/TableRefresh'; import { ApiPaths, apiUrl } from '../../../states/ApiState'; +import { AddItemButton } from '../../buttons/AddItemButton'; import { ThumbnailHoverCard } from '../../images/Thumbnail'; import { ProgressBar } from '../../items/ProgressBar'; import { ModelType } from '../../render/ModelType'; @@ -50,16 +53,11 @@ function buildOrderTableColumns(): TableColumn[] { sortable: false, title: t`Description` }, - { - accessor: 'quantity', - sortable: true, - title: t`Quantity`, - switchable: false - }, { accessor: 'completed', sortable: true, - title: t`Completed`, + switchable: false, + title: t`Progress`, render: (record: any) => ( buildOrderTableColumns(), []); - const tableFilters = useMemo(() => buildOrderTableFilters(), []); + + const tableFilters = useMemo(() => { + return [ + { + // TODO: Filter by status code + name: 'active', + type: 'boolean', + label: t`Active` + }, + { + name: 'overdue', + type: 'boolean', + label: t`Overdue` + }, + { + name: 'assigned_to_me', + type: 'boolean', + label: t`Assigned to me` + } + // TODO: 'assigned to' filter + // TODO: 'issued by' filter + // TODO: 'has project code' filter (see table_filters.js) + // TODO: 'project code' filter (see table_filters.js) + ]; + }, []); const navigate = useNavigate(); diff --git a/src/frontend/src/forms/BuildForms.tsx b/src/frontend/src/forms/BuildForms.tsx new file mode 100644 index 0000000000..af44bb70e6 --- /dev/null +++ b/src/frontend/src/forms/BuildForms.tsx @@ -0,0 +1,60 @@ +import { + IconCalendar, + IconLink, + IconList, + IconSitemap, + IconTruckDelivery, + IconUser, + IconUsersGroup +} from '@tabler/icons-react'; + +import { ApiFormFieldSet } from '../components/forms/fields/ApiFormField'; + +/** + * Field set for BuildOrder forms + */ +export function buildOrderFields(): ApiFormFieldSet { + return { + reference: {}, + part: { + filters: { + assembly: true, + virtual: false + } + }, + title: {}, + quantity: {}, + project_code: { + icon: + }, + priority: {}, + parent: { + icon: , + filters: { + part_detail: true + } + }, + sales_order: { + icon: + }, + batch: {}, + target_date: { + icon: + }, + take_from: {}, + destination: { + filters: { + structural: false + } + }, + link: { + icon: + }, + issued_by: { + icon: + }, + responsible: { + icon: + } + }; +} diff --git a/src/frontend/src/pages/build/BuildDetail.tsx b/src/frontend/src/pages/build/BuildDetail.tsx index d8f6f21f72..42d01ffaf5 100644 --- a/src/frontend/src/pages/build/BuildDetail.tsx +++ b/src/frontend/src/pages/build/BuildDetail.tsx @@ -17,7 +17,7 @@ import { IconSitemap, IconTrash } from '@tabler/icons-react'; -import { useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; import { useParams } from 'react-router-dom'; import { @@ -37,6 +37,8 @@ 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 { buildOrderFields } from '../../forms/BuildForms'; +import { openEditApiForm } from '../../functions/forms'; import { useInstance } from '../../hooks/UseInstance'; import { ApiPaths, apiUrl } from '../../states/ApiState'; import { useUserState } from '../../states/UserState'; @@ -177,6 +179,25 @@ export default function BuildDetail() { ]; }, [build]); + const editBuildOrder = useCallback(() => { + let fields = buildOrderFields(); + + // Cannot edit part field after creation + fields['part'].hidden = true; + + build.pk && + openEditApiForm({ + url: ApiPaths.build_order_list, + pk: build.pk, + title: t`Edit Build Order`, + fields: fields, + successMessage: t`Build Order updated`, + onFormSuccess: () => { + refreshInstance(); + } + }); + }, [build]); + const buildActions = useMemo(() => { // TODO: Disable certain actions based on user permissions return [ @@ -211,7 +232,9 @@ export default function BuildDetail() { tooltip={t`Build Order Actions`} icon={} actions={[ - EditItemAction({}), + EditItemAction({ + onClick: editBuildOrder + }), DuplicateItemAction({}), DeleteItemAction({}) ]} diff --git a/src/frontend/src/pages/build/BuildIndex.tsx b/src/frontend/src/pages/build/BuildIndex.tsx index 7e6fafbd44..4e140bd980 100644 --- a/src/frontend/src/pages/build/BuildIndex.tsx +++ b/src/frontend/src/pages/build/BuildIndex.tsx @@ -1,26 +1,42 @@ import { t } from '@lingui/macro'; import { Button, Stack, Text } from '@mantine/core'; +import { useCallback } from 'react'; +import { useNavigate } from 'react-router-dom'; import { PageDetail } from '../../components/nav/PageDetail'; import { BuildOrderTable } from '../../components/tables/build/BuildOrderTable'; -import { notYetImplemented } from '../../functions/notifications'; +import { buildOrderFields } from '../../forms/BuildForms'; +import { openCreateApiForm } from '../../functions/forms'; +import { ApiPaths } from '../../states/ApiState'; /** * Build Order index page */ export default function BuildIndex() { + const navigate = useNavigate(); + + const newBuildOrder = useCallback(() => { + openCreateApiForm({ + url: ApiPaths.build_order_list, + title: t`Add Build Order`, + fields: buildOrderFields(), + successMessage: t`Build order created`, + onFormSuccess: (data: any) => { + if (data.pk) { + navigate(`/build/${data.pk}`); + } + } + }); + }, []); + return ( <> notYetImplemented()} - > - {t`New Build Order`} + ]} /> diff --git a/src/frontend/src/pages/company/CompanyDetail.tsx b/src/frontend/src/pages/company/CompanyDetail.tsx index 2c15fd536e..887681889c 100644 --- a/src/frontend/src/pages/company/CompanyDetail.tsx +++ b/src/frontend/src/pages/company/CompanyDetail.tsx @@ -173,7 +173,7 @@ export default function CompanyDetail(props: CompanyDetailProps) { actions={[ EditItemAction({ disabled: !canEdit, - callback: () => { + onClick: () => { if (company?.pk) { editCompany({ pk: company?.pk, diff --git a/src/frontend/src/pages/part/PartDetail.tsx b/src/frontend/src/pages/part/PartDetail.tsx index bc099996a5..2e26e6cc3d 100644 --- a/src/frontend/src/pages/part/PartDetail.tsx +++ b/src/frontend/src/pages/part/PartDetail.tsx @@ -296,7 +296,7 @@ export default function PartDetail() { actions={[ DuplicateItemAction({}), EditItemAction({ - callback: () => { + onClick: () => { part.pk && editPart({ part_id: part.pk, diff --git a/src/frontend/src/pages/stock/StockDetail.tsx b/src/frontend/src/pages/stock/StockDetail.tsx index f7f0efd188..a13a3adeb6 100644 --- a/src/frontend/src/pages/stock/StockDetail.tsx +++ b/src/frontend/src/pages/stock/StockDetail.tsx @@ -191,7 +191,7 @@ export default function StockDetail() { icon: }, EditItemAction({ - callback: () => { + onClick: () => { stockitem.pk && editStockItem({ item_id: stockitem.pk, diff --git a/src/frontend/src/states/ApiState.tsx b/src/frontend/src/states/ApiState.tsx index 2bbac38c8d..186b55dafe 100644 --- a/src/frontend/src/states/ApiState.tsx +++ b/src/frontend/src/states/ApiState.tsx @@ -100,9 +100,11 @@ export enum ApiPaths { company_list = 'api-company-list', company_attachment_list = 'api-company-attachment-list', supplier_part_list = 'api-supplier-part-list', + manufacturer_part_list = 'api-manufacturer-part-list', // Stock Item URLs stock_item_list = 'api-stock-item-list', + stock_tracking_list = 'api-stock-tracking-list', stock_location_list = 'api-stock-location-list', stock_location_tree = 'api-stock-location-tree', stock_attachment_list = 'api-stock-attachment-list', @@ -216,8 +218,12 @@ export function apiEndpoint(path: ApiPaths): string { return 'company/attachment/'; case ApiPaths.supplier_part_list: return 'company/part/'; + case ApiPaths.manufacturer_part_list: + return 'company/part/manufacturer/'; case ApiPaths.stock_item_list: return 'stock/'; + case ApiPaths.stock_tracking_list: + return 'stock/track/'; case ApiPaths.stock_location_list: return 'stock/location/'; case ApiPaths.stock_location_tree: