diff --git a/src/frontend/src/components/buttons/RemoveRowButton.tsx b/src/frontend/src/components/buttons/RemoveRowButton.tsx index 69bf48130d..c0b549e969 100644 --- a/src/frontend/src/components/buttons/RemoveRowButton.tsx +++ b/src/frontend/src/components/buttons/RemoveRowButton.tsx @@ -1,21 +1,24 @@ import { t } from '@lingui/core/macro'; import { ActionButton } from '@lib/components/ActionButton'; +import type { FloatingPosition } from '@mantine/core'; import { InvenTreeIcon } from '../../functions/icons'; export default function RemoveRowButton({ onClick, - tooltip = t`Remove this row` + tooltip = t`Remove this row`, + tooltipAlignment }: Readonly<{ onClick: () => void; tooltip?: string; + tooltipAlignment?: FloatingPosition; }>) { return ( } tooltip={tooltip} - tooltipAlignment='top-end' + tooltipAlignment={tooltipAlignment ?? 'top-end'} color='red' /> ); diff --git a/src/frontend/src/components/render/Company.tsx b/src/frontend/src/components/render/Company.tsx index ccedfb197e..96dbcd40ce 100644 --- a/src/frontend/src/components/render/Company.tsx +++ b/src/frontend/src/components/render/Company.tsx @@ -80,7 +80,7 @@ export function RenderSupplierPart( const part = instance.part_detail ?? {}; const secondary: string = instance.SKU; - let suffix: string = part.full_name; + let suffix: string = part?.full_name ?? ''; if (instance.pack_quantity) { suffix += ` (${instance.pack_quantity})`; diff --git a/src/frontend/src/components/wizards/OrderPartsWizard.tsx b/src/frontend/src/components/wizards/OrderPartsWizard.tsx index d04f4a302c..ecb53fa739 100644 --- a/src/frontend/src/components/wizards/OrderPartsWizard.tsx +++ b/src/frontend/src/components/wizards/OrderPartsWizard.tsx @@ -3,22 +3,163 @@ import { AddItemButton } from '@lib/components/AddItemButton'; import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; import { ModelType } from '@lib/enums/ModelType'; import { apiUrl } from '@lib/functions/Api'; +import { formatDecimal } from '@lib/functions/Formatting'; import type { ApiFormFieldSet } from '@lib/types/Forms'; import { t } from '@lingui/core/macro'; -import { Alert, Group, Paper, Tooltip } from '@mantine/core'; +import { + ActionIcon, + Alert, + Divider, + Group, + HoverCard, + Loader, + Paper, + Stack, + Text, + Tooltip +} from '@mantine/core'; import { showNotification } from '@mantine/notifications'; -import { IconShoppingCart } from '@tabler/icons-react'; +import { + IconExclamationCircle, + IconInfoCircle, + IconShoppingCart +} from '@tabler/icons-react'; import { DataTable } from 'mantine-datatable'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useSupplierPartFields } from '../../forms/CompanyForms'; import { usePurchaseOrderFields } from '../../forms/PurchaseOrderForms'; import { useCreateApiFormModal } from '../../hooks/UseForm'; +import { useInstance } from '../../hooks/UseInstance'; import useWizard from '../../hooks/UseWizard'; import { RenderPartColumn } from '../../tables/ColumnRenderers'; import RemoveRowButton from '../buttons/RemoveRowButton'; import { StandaloneField } from '../forms/StandaloneField'; import Expand from '../items/Expand'; +/** + * Render the "requirements" info for a part + * This fetches the information dynamically from the API + */ +function PartRequirementsInfo({ + partId, + onQuantityChange +}: { + partId: number | string; + onQuantityChange?: (quantity: number) => void; +}) { + const [requiredQuantity, setRequiredQuantity] = useState(0); + + // Notify parent component of quantity change + useEffect(() => { + onQuantityChange?.(requiredQuantity); + }, [requiredQuantity]); + + const requirements = useInstance({ + endpoint: ApiEndpoints.part_requirements, + pk: partId, + hasPrimaryKey: true, + defaultValue: {} + }); + + const widget = useMemo(() => { + if ( + requirements.instanceQuery.isFetching || + requirements.instanceQuery.isLoading + ) { + return ; + } + + if (requirements.instanceQuery.isError) { + return ( + + + + + + ); + } + + // Calculate the total requirements + const buildRequirements = + requirements.instance?.required_for_build_orders || 0; + const salesRequirements = + requirements.instance?.required_for_sales_orders || 0; + const totalRequirements = buildRequirements + salesRequirements; + + const building = requirements.instance?.building || 0; + const ordering = requirements.instance?.ordering || 0; + const incoming = building + ordering; + + const inStock = requirements.instance?.total_stock || 0; + + const required = Math.max(0, totalRequirements - inStock - incoming); + + setRequiredQuantity(required); + + return ( + + + 0 ? 'blue' : 'green'} + size='sm' + > + + + + + + {t`Requirements`} + + {buildRequirements > 0 && ( + + {t`Build Requirements`} + {formatDecimal(buildRequirements)} + + )} + {salesRequirements > 0 && ( + + {t`Sales Requirements`} + {formatDecimal(salesRequirements)} + + )} + {inStock > 0 && ( + + {t`In Stock`} + {formatDecimal(inStock)} + + )} + {ordering > 0 && ( + + {t`On Order`} + {formatDecimal(ordering)} + + )} + {building > 0 && ( + + {t`In Production`} + {formatDecimal(building)} + + )} + + {t`Required Quantity`} + {formatDecimal(required)} + + + + + ); + }, [ + requirements.instanceQuery.isFetching, + requirements.instanceQuery.isLoading, + requirements.instanceQuery.isError, + requirements.instance, + setRequiredQuantity + ]); + + return widget; +} + /** * Attributes for each selected part * - part: The part instance @@ -123,7 +264,10 @@ function SelectPartsStep({ width: '1%', render: (record: PartOrderRecord) => ( - onRemovePart(record.part)} /> + onRemovePart(record.part)} + /> ) }, @@ -162,6 +306,7 @@ function SelectPartsStep({ filters: { part: record.part.pk, active: true, + part_detail: true, supplier_detail: true } }} @@ -169,7 +314,7 @@ function SelectPartsStep({ { setSelectedRecord(record); newSupplierPart.open(); @@ -207,7 +352,7 @@ function SelectPartsStep({ { setSelectedRecord(record); @@ -220,21 +365,29 @@ function SelectPartsStep({ { accessor: 'quantity', title: t`Quantity`, - width: 125, + width: 150, render: (record: PartOrderRecord) => ( - { - onSelectQuantity(record.part.pk, value); + + { + onSelectQuantity(record.part.pk, value); + } + }} + /> + + onSelectQuantity(record.part.pk, quantity) } - }} - /> + /> + ) }, { @@ -255,7 +408,7 @@ function SelectPartsStep({ } icon={} tooltip={t`Add to selected purchase order`} - tooltipAlignment='top' + tooltipAlignment='top-end' color='blue' /> diff --git a/src/frontend/src/tables/build/BuildLineTable.tsx b/src/frontend/src/tables/build/BuildLineTable.tsx index 25a506c644..4d95f85075 100644 --- a/src/frontend/src/tables/build/BuildLineTable.tsx +++ b/src/frontend/src/tables/build/BuildLineTable.tsx @@ -870,8 +870,6 @@ export default function BuildLineTable({ */ const formatRecords = useCallback( (records: any[]): any[] => { - console.log('format records:', records); - return records.map((record) => { let allocations = [...record.allocations];