Apply same to StockItem
This commit is contained in:
parent
bd0455c940
commit
85e2907f60
|
|
@ -1,6 +1,7 @@
|
|||
import { ModelType } from '@lib/enums/ModelType';
|
||||
import type { ReactNode } from 'react';
|
||||
import { PartPreviewComponent } from './models/PartPreview';
|
||||
import { StockPreviewComponent } from './models/StockPreview';
|
||||
|
||||
export interface PreviewType {
|
||||
preview: ReactNode;
|
||||
|
|
@ -25,6 +26,8 @@ export function getPreviewComponentForModel({
|
|||
switch (modelType) {
|
||||
case ModelType.part:
|
||||
return PartPreviewComponent({ instance, modelId });
|
||||
case ModelType.stockitem:
|
||||
return StockPreviewComponent({ instance, modelId });
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
import { formatDecimal } from '@lib/functions/Formatting';
|
||||
import { StockDetailsPanel } from '../../../pages/stock/StockDetailsPanel';
|
||||
import type { PreviewType } from '../PreviewType';
|
||||
|
||||
export function StockPreviewComponent({
|
||||
instance,
|
||||
modelId
|
||||
}: Readonly<{
|
||||
instance: any;
|
||||
modelId: number;
|
||||
}>): PreviewType {
|
||||
const part = instance?.part_detail;
|
||||
|
||||
let title = `Stock Item #${modelId}`;
|
||||
|
||||
if (part) {
|
||||
title = part?.full_name ?? part?.name;
|
||||
|
||||
if (instance.serial) {
|
||||
title += ` (# ${instance.serial})`;
|
||||
} else {
|
||||
title += ` (x ${formatDecimal(instance.quantity)})`;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
title: title,
|
||||
preview: <StockDetailsPanel instance={instance} />
|
||||
};
|
||||
}
|
||||
|
|
@ -1,25 +1,12 @@
|
|||
import { t } from '@lingui/core/macro';
|
||||
import { Accordion, Skeleton, Stack } from '@mantine/core';
|
||||
import {
|
||||
Accordion,
|
||||
Button,
|
||||
Grid,
|
||||
Group,
|
||||
Skeleton,
|
||||
Space,
|
||||
Stack,
|
||||
Text,
|
||||
Tooltip
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
IconArrowLeft,
|
||||
IconArrowRight,
|
||||
IconBookmark,
|
||||
IconBoxPadding,
|
||||
IconChecklist,
|
||||
IconHistory,
|
||||
IconInfoCircle,
|
||||
IconPackages,
|
||||
IconSearch,
|
||||
IconShoppingCart,
|
||||
IconSitemap,
|
||||
IconTransform
|
||||
|
|
@ -28,27 +15,19 @@ import { useQuery } from '@tanstack/react-query';
|
|||
import { type ReactNode, useMemo, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import { ActionButton } from '@lib/components/ActionButton';
|
||||
import { StylishText } from '@lib/components/StylishText';
|
||||
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||
import { ModelType } from '@lib/enums/ModelType';
|
||||
import { UserRoles } from '@lib/enums/Roles';
|
||||
import { apiUrl } from '@lib/functions/Api';
|
||||
import { getDetailUrl, getOverviewUrl } from '@lib/functions/Navigation';
|
||||
import { TagsList } from '@lib/index';
|
||||
import type { ApiFormFieldSet, StockOperationProps } from '@lib/types/Forms';
|
||||
import type { PanelType } from '@lib/types/Panel';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { useBarcodeScanDialog } from '../../components/barcodes/BarcodeScanDialog';
|
||||
import AdminButton from '../../components/buttons/AdminButton';
|
||||
import { PrintingActions } from '../../components/buttons/PrintingActions';
|
||||
import {
|
||||
type DetailsField,
|
||||
DetailsTable
|
||||
} from '../../components/details/Details';
|
||||
import DetailsBadge from '../../components/details/DetailsBadge';
|
||||
import { DetailsImage } from '../../components/details/DetailsImage';
|
||||
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
||||
import {
|
||||
ActionDropdown,
|
||||
BarcodeActionDropdown,
|
||||
|
|
@ -67,9 +46,8 @@ import LocateItemButton from '../../components/plugins/LocateItemButton';
|
|||
import { StatusRenderer } from '../../components/render/StatusRenderer';
|
||||
import OrderPartsWizard from '../../components/wizards/OrderPartsWizard';
|
||||
import { useApi } from '../../contexts/ApiContext';
|
||||
import { formatCurrency, formatDecimal } from '../../defaults/formatters';
|
||||
import { formatDecimal } from '../../defaults/formatters';
|
||||
import {
|
||||
useFindSerialNumberForm,
|
||||
useStockFields,
|
||||
useStockItemSerializeFields
|
||||
} from '../../forms/StockForms';
|
||||
|
|
@ -90,6 +68,7 @@ import { StockItemTable } from '../../tables/stock/StockItemTable';
|
|||
import StockItemTestResultTable from '../../tables/stock/StockItemTestResultTable';
|
||||
import { StockTrackingTable } from '../../tables/stock/StockTrackingTable';
|
||||
import TransferOrderAllocationTable from '../../tables/stock/TransferOrderAllocationTable';
|
||||
import { StockDetailsPanel } from './StockDetailsPanel';
|
||||
|
||||
export default function StockDetail() {
|
||||
const { id } = useParams();
|
||||
|
|
@ -124,366 +103,13 @@ export default function StockDetail() {
|
|||
}
|
||||
});
|
||||
|
||||
const { instance: part, instanceQuery: partQuery } = useInstance({
|
||||
const { instance: part } = useInstance({
|
||||
endpoint: ApiEndpoints.part_list,
|
||||
pk: stockitem?.part,
|
||||
hasPrimaryKey: true,
|
||||
defaultValue: {}
|
||||
});
|
||||
|
||||
const { instance: serialNumbers, instanceQuery: serialNumbersQuery } =
|
||||
useInstance({
|
||||
endpoint: ApiEndpoints.stock_serial_info,
|
||||
pk: id
|
||||
});
|
||||
|
||||
const findBySerialNumber = useFindSerialNumberForm({
|
||||
partId: stockitem.part
|
||||
});
|
||||
|
||||
const detailsPanel = useMemo(() => {
|
||||
const data = { ...stockitem };
|
||||
const part = stockitem?.part_detail ?? {};
|
||||
|
||||
data.available_stock = Math.max(0, data.quantity - data.allocated);
|
||||
|
||||
if (instanceQuery.isFetching) {
|
||||
return <Skeleton />;
|
||||
}
|
||||
|
||||
// Top left - core part information
|
||||
const tl: DetailsField[] = [
|
||||
{
|
||||
name: 'part',
|
||||
label: t`Base Part`,
|
||||
type: 'link',
|
||||
model: ModelType.part
|
||||
},
|
||||
{
|
||||
name: 'part_detail.IPN',
|
||||
label: t`IPN`,
|
||||
type: 'text',
|
||||
copy: true,
|
||||
icon: 'part',
|
||||
hidden: !part.IPN
|
||||
},
|
||||
{
|
||||
name: 'part_detail.revision',
|
||||
label: t`Revision`,
|
||||
type: 'string',
|
||||
copy: true,
|
||||
icon: 'revision',
|
||||
hidden: !part.revision
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
type: 'status',
|
||||
label: t`Status`,
|
||||
model: ModelType.stockitem
|
||||
},
|
||||
{
|
||||
name: 'status_custom_key',
|
||||
type: 'status',
|
||||
label: t`Custom Status`,
|
||||
model: ModelType.stockitem,
|
||||
icon: 'status',
|
||||
hidden:
|
||||
!stockitem.status_custom_key ||
|
||||
stockitem.status_custom_key == stockitem.status
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'link',
|
||||
label: t`Link`,
|
||||
external: true,
|
||||
copy: true,
|
||||
hidden: !stockitem.link
|
||||
}
|
||||
];
|
||||
|
||||
// Top right - available stock information
|
||||
const tr: DetailsField[] = [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'serial',
|
||||
label: t`Serial Number`,
|
||||
hidden: !stockitem.serial,
|
||||
value_formatter: () => (
|
||||
<Group gap='xs' justify='space-apart'>
|
||||
<Text>{stockitem.serial}</Text>
|
||||
<Space flex={10} />
|
||||
<Group gap={2} justify='right'>
|
||||
{serialNumbers.previous?.pk && (
|
||||
<Tooltip label={t`Previous serial number`} position='top'>
|
||||
<Button
|
||||
p={3}
|
||||
aria-label='previous-serial-number'
|
||||
leftSection={<IconArrowLeft />}
|
||||
variant='transparent'
|
||||
size='sm'
|
||||
onClick={() => {
|
||||
navigate(
|
||||
getDetailUrl(
|
||||
ModelType.stockitem,
|
||||
serialNumbers.previous.pk
|
||||
)
|
||||
);
|
||||
}}
|
||||
>
|
||||
{serialNumbers.previous.serial}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
<ActionButton
|
||||
icon={<IconSearch size={18} />}
|
||||
tooltip={t`Find serial number`}
|
||||
tooltipAlignment='top'
|
||||
variant='transparent'
|
||||
onClick={findBySerialNumber.open}
|
||||
/>
|
||||
{serialNumbers.next?.pk && (
|
||||
<Tooltip label={t`Next serial number`} position='top'>
|
||||
<Button
|
||||
p={3}
|
||||
aria-label='next-serial-number'
|
||||
rightSection={<IconArrowRight />}
|
||||
variant='transparent'
|
||||
size='sm'
|
||||
onClick={() => {
|
||||
navigate(
|
||||
getDetailUrl(ModelType.stockitem, serialNumbers.next.pk)
|
||||
);
|
||||
}}
|
||||
>
|
||||
{serialNumbers.next.serial}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Group>
|
||||
</Group>
|
||||
)
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
name: 'quantity',
|
||||
label: t`Quantity`,
|
||||
unit: part?.units,
|
||||
hidden: !!stockitem.serial && stockitem.quantity == 1
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
name: 'available_stock',
|
||||
label: t`Available`,
|
||||
unit: part?.units,
|
||||
icon: 'stock'
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
name: 'allocated',
|
||||
label: t`Allocated to Orders`,
|
||||
unit: part?.units,
|
||||
icon: 'tick_off',
|
||||
hidden: !stockitem.allocated
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'batch',
|
||||
label: t`Batch Code`,
|
||||
hidden: !stockitem.batch
|
||||
}
|
||||
];
|
||||
|
||||
// Bottom left: location information
|
||||
const bl: DetailsField[] = [
|
||||
{
|
||||
name: 'supplier_part',
|
||||
label: t`Supplier Part`,
|
||||
type: 'link',
|
||||
model_field: 'SKU',
|
||||
model: ModelType.supplierpart,
|
||||
hidden: !stockitem.supplier_part
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'location',
|
||||
label: t`Location`,
|
||||
model: ModelType.stocklocation,
|
||||
hidden: !stockitem.location
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'belongs_to',
|
||||
label: t`Installed In`,
|
||||
model_filters: {
|
||||
part_detail: true
|
||||
},
|
||||
model_formatter: (model: any) => {
|
||||
let text = model?.part_detail?.full_name ?? model?.name;
|
||||
if (model.serial && model.quantity == 1) {
|
||||
text += ` # ${model.serial}`;
|
||||
}
|
||||
|
||||
return text;
|
||||
},
|
||||
icon: 'stock',
|
||||
model: ModelType.stockitem,
|
||||
hidden: !stockitem.belongs_to
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'parent',
|
||||
icon: 'sitemap',
|
||||
label: t`Parent Item`,
|
||||
model: ModelType.stockitem,
|
||||
hidden: !stockitem.parent,
|
||||
model_formatter: (model: any) => {
|
||||
return t`Parent stock item`;
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'consumed_by',
|
||||
label: t`Consumed By`,
|
||||
model: ModelType.build,
|
||||
hidden: !stockitem.consumed_by,
|
||||
icon: 'build',
|
||||
model_field: 'reference'
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'build',
|
||||
label: t`Build Order`,
|
||||
model: ModelType.build,
|
||||
hidden: !stockitem.build,
|
||||
model_field: 'reference'
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'purchase_order',
|
||||
label: t`Purchase Order`,
|
||||
model: ModelType.purchaseorder,
|
||||
hidden: !stockitem.purchase_order,
|
||||
icon: 'purchase_orders',
|
||||
model_field: 'reference'
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'sales_order',
|
||||
label: t`Sales Order`,
|
||||
model: ModelType.salesorder,
|
||||
hidden: !stockitem.sales_order,
|
||||
icon: 'sales_orders',
|
||||
model_field: 'reference'
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'customer',
|
||||
label: t`Customer`,
|
||||
model: ModelType.company,
|
||||
hidden: !stockitem.customer
|
||||
}
|
||||
];
|
||||
|
||||
// Bottom right - any other information
|
||||
const br: DetailsField[] = [
|
||||
// Expiry date
|
||||
{
|
||||
type: 'date',
|
||||
name: 'expiry_date',
|
||||
label: t`Expiry Date`,
|
||||
hidden: !enableExpiry || !stockitem.expiry_date,
|
||||
icon: 'calendar'
|
||||
},
|
||||
// TODO: Ownership
|
||||
{
|
||||
type: 'text',
|
||||
name: 'purchase_price',
|
||||
label: t`Unit Price`,
|
||||
icon: 'currency',
|
||||
hidden: !stockitem.purchase_price,
|
||||
value_formatter: () => {
|
||||
return formatCurrency(stockitem.purchase_price, {
|
||||
currency: stockitem.purchase_price_currency
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'stock_value',
|
||||
label: t`Stock Value`,
|
||||
icon: 'currency',
|
||||
hidden:
|
||||
!stockitem.purchase_price ||
|
||||
stockitem.quantity == 1 ||
|
||||
stockitem.quantity == 0,
|
||||
value_formatter: () => {
|
||||
return formatCurrency(stockitem.purchase_price, {
|
||||
currency: stockitem.purchase_price_currency,
|
||||
multiplier: stockitem.quantity
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'packaging',
|
||||
icon: 'part',
|
||||
label: t`Packaging`,
|
||||
hidden: !stockitem.packaging
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'creation_date',
|
||||
icon: 'calendar',
|
||||
label: t`Created`,
|
||||
hidden: !stockitem.creation_date
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'updated',
|
||||
icon: 'calendar',
|
||||
label: t`Last Updated`
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'stocktake_date',
|
||||
icon: 'calendar',
|
||||
label: t`Last Stocktake`,
|
||||
hidden: !stockitem.stocktake_date
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<ItemDetailsGrid>
|
||||
<Stack gap='xs'>
|
||||
<Grid grow>
|
||||
<DetailsImage
|
||||
appRole={UserRoles.part}
|
||||
apiPath={ApiEndpoints.part_list}
|
||||
src={
|
||||
stockitem.part_detail?.image ??
|
||||
stockitem?.part_detail?.thumbnail
|
||||
}
|
||||
pk={stockitem.part}
|
||||
/>
|
||||
<Grid.Col span={{ base: 12, sm: 8 }}>
|
||||
<DetailsTable fields={tl} item={data} />
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
<TagsList tags={stockitem.tags} />
|
||||
</Stack>
|
||||
<DetailsTable fields={tr} item={data} />
|
||||
<DetailsTable fields={bl} item={data} />
|
||||
<DetailsTable fields={br} item={data} />
|
||||
</ItemDetailsGrid>
|
||||
);
|
||||
}, [
|
||||
stockitem,
|
||||
serialNumbers,
|
||||
serialNumbersQuery.isFetching,
|
||||
instanceQuery.isFetching,
|
||||
enableExpiry
|
||||
]);
|
||||
|
||||
const showBuildAllocations: boolean = useMemo(() => {
|
||||
// Determine if "build allocations" should be shown for this stock item
|
||||
return (
|
||||
|
|
@ -554,7 +180,14 @@ export default function StockDetail() {
|
|||
name: 'details',
|
||||
label: t`Stock Details`,
|
||||
icon: <IconInfoCircle />,
|
||||
content: detailsPanel
|
||||
content: (
|
||||
<StockDetailsPanel
|
||||
instance={stockitem}
|
||||
allowImageEdit
|
||||
showSerialNav
|
||||
refreshInstance={refreshInstance}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'tracking',
|
||||
|
|
@ -685,8 +318,6 @@ export default function StockDetail() {
|
|||
showBuildAllocations,
|
||||
showInstalledItems,
|
||||
stockitem,
|
||||
serialNumbers,
|
||||
serialNumbersQuery,
|
||||
id,
|
||||
user
|
||||
]);
|
||||
|
|
@ -1061,7 +692,6 @@ export default function StockDetail() {
|
|||
|
||||
return (
|
||||
<>
|
||||
{findBySerialNumber.modal}
|
||||
{scanIntoLocation.dialog}
|
||||
<InstanceDetail
|
||||
query={instanceQuery}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,410 @@
|
|||
import { t } from '@lingui/core/macro';
|
||||
import {
|
||||
Button,
|
||||
Grid,
|
||||
Group,
|
||||
Skeleton,
|
||||
Space,
|
||||
Stack,
|
||||
Text,
|
||||
Tooltip
|
||||
} from '@mantine/core';
|
||||
import { IconArrowLeft, IconArrowRight, IconSearch } from '@tabler/icons-react';
|
||||
import { useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { ActionButton } from '@lib/components/ActionButton';
|
||||
import TagsList from '@lib/components/TagsList';
|
||||
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||
import { ModelType } from '@lib/enums/ModelType';
|
||||
import { UserRoles } from '@lib/enums/Roles';
|
||||
import { apiUrl } from '@lib/functions/Api';
|
||||
import { getDetailUrl } from '@lib/functions/Navigation';
|
||||
|
||||
import {
|
||||
type DetailsField,
|
||||
DetailsTable
|
||||
} from '../../components/details/Details';
|
||||
import { DetailsImage } from '../../components/details/DetailsImage';
|
||||
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
||||
import { formatCurrency } from '../../defaults/formatters';
|
||||
import { useFindSerialNumberForm } from '../../forms/StockForms';
|
||||
import { useInstance } from '../../hooks/UseInstance';
|
||||
import { useGlobalSettingsState } from '../../states/SettingsStates';
|
||||
|
||||
export function StockDetailsPanel({
|
||||
instance,
|
||||
allowImageEdit = false,
|
||||
showSerialNav = false,
|
||||
refreshInstance
|
||||
}: Readonly<{
|
||||
instance: any;
|
||||
allowImageEdit?: boolean;
|
||||
showSerialNav?: boolean;
|
||||
refreshInstance?: () => void;
|
||||
}>) {
|
||||
const navigate = useNavigate();
|
||||
const globalSettings = useGlobalSettingsState();
|
||||
|
||||
const enableExpiry = useMemo(
|
||||
() => globalSettings.isSet('STOCK_ENABLE_EXPIRY'),
|
||||
[globalSettings]
|
||||
);
|
||||
|
||||
const { instance: serialNumbers } = useInstance({
|
||||
endpoint: ApiEndpoints.stock_serial_info,
|
||||
pk: instance?.pk,
|
||||
hasPrimaryKey: true,
|
||||
disabled: !instance?.pk,
|
||||
defaultValue: {}
|
||||
});
|
||||
|
||||
const { instance: fetchedPart } = useInstance({
|
||||
endpoint: ApiEndpoints.part_list,
|
||||
pk: instance?.part,
|
||||
hasPrimaryKey: true,
|
||||
disabled: !instance?.part || !!instance?.part_detail,
|
||||
defaultValue: {}
|
||||
});
|
||||
|
||||
const findBySerialNumber = useFindSerialNumberForm({
|
||||
partId: instance?.part
|
||||
});
|
||||
|
||||
const part = instance?.part_detail ?? fetchedPart ?? {};
|
||||
|
||||
const data = useMemo(() => {
|
||||
const d = { ...instance };
|
||||
d.available_stock = Math.max(0, (d.quantity ?? 0) - (d.allocated ?? 0));
|
||||
return d;
|
||||
}, [instance]);
|
||||
|
||||
const tl: DetailsField[] = [
|
||||
{
|
||||
name: 'part',
|
||||
label: t`Base Part`,
|
||||
type: 'link',
|
||||
model: ModelType.part
|
||||
},
|
||||
{
|
||||
name: 'part_detail.IPN',
|
||||
label: t`IPN`,
|
||||
type: 'text',
|
||||
copy: true,
|
||||
icon: 'part',
|
||||
hidden: !part.IPN
|
||||
},
|
||||
{
|
||||
name: 'part_detail.revision',
|
||||
label: t`Revision`,
|
||||
type: 'string',
|
||||
copy: true,
|
||||
icon: 'revision',
|
||||
hidden: !part.revision
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
type: 'status',
|
||||
label: t`Status`,
|
||||
model: ModelType.stockitem
|
||||
},
|
||||
{
|
||||
name: 'status_custom_key',
|
||||
type: 'status',
|
||||
label: t`Custom Status`,
|
||||
model: ModelType.stockitem,
|
||||
icon: 'status',
|
||||
hidden:
|
||||
!instance?.status_custom_key ||
|
||||
instance?.status_custom_key == instance?.status
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'link',
|
||||
label: t`Link`,
|
||||
external: true,
|
||||
copy: true,
|
||||
hidden: !instance?.link
|
||||
}
|
||||
];
|
||||
|
||||
const tr: DetailsField[] = [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'serial',
|
||||
label: t`Serial Number`,
|
||||
hidden: !instance?.serial,
|
||||
...(showSerialNav && {
|
||||
value_formatter: () => (
|
||||
<Group gap='xs' justify='space-apart'>
|
||||
<Text>{instance?.serial}</Text>
|
||||
<Space flex={10} />
|
||||
<Group gap={2} justify='right'>
|
||||
{serialNumbers?.previous?.pk && (
|
||||
<Tooltip label={t`Previous serial number`} position='top'>
|
||||
<Button
|
||||
p={3}
|
||||
aria-label='previous-serial-number'
|
||||
leftSection={<IconArrowLeft />}
|
||||
variant='transparent'
|
||||
size='sm'
|
||||
onClick={() => {
|
||||
navigate(
|
||||
getDetailUrl(
|
||||
ModelType.stockitem,
|
||||
serialNumbers.previous.pk
|
||||
)
|
||||
);
|
||||
}}
|
||||
>
|
||||
{serialNumbers.previous.serial}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
<ActionButton
|
||||
icon={<IconSearch size={18} />}
|
||||
tooltip={t`Find serial number`}
|
||||
tooltipAlignment='top'
|
||||
variant='transparent'
|
||||
onClick={findBySerialNumber.open}
|
||||
/>
|
||||
{serialNumbers?.next?.pk && (
|
||||
<Tooltip label={t`Next serial number`} position='top'>
|
||||
<Button
|
||||
p={3}
|
||||
aria-label='next-serial-number'
|
||||
rightSection={<IconArrowRight />}
|
||||
variant='transparent'
|
||||
size='sm'
|
||||
onClick={() => {
|
||||
navigate(
|
||||
getDetailUrl(ModelType.stockitem, serialNumbers.next.pk)
|
||||
);
|
||||
}}
|
||||
>
|
||||
{serialNumbers.next.serial}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Group>
|
||||
</Group>
|
||||
)
|
||||
})
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
name: 'quantity',
|
||||
label: t`Quantity`,
|
||||
unit: part?.units,
|
||||
hidden: !!instance?.serial && instance?.quantity == 1
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
name: 'available_stock',
|
||||
label: t`Available`,
|
||||
unit: part?.units,
|
||||
icon: 'stock',
|
||||
hidden: !!instance?.serial || instance.in_stock === false
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
name: 'allocated',
|
||||
label: t`Allocated to Orders`,
|
||||
unit: part?.units,
|
||||
icon: 'tick_off',
|
||||
hidden: !instance?.allocated
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'batch',
|
||||
label: t`Batch Code`,
|
||||
hidden: !instance?.batch
|
||||
}
|
||||
];
|
||||
|
||||
const bl: DetailsField[] = [
|
||||
{
|
||||
name: 'supplier_part',
|
||||
label: t`Supplier Part`,
|
||||
type: 'link',
|
||||
model_field: 'SKU',
|
||||
model: ModelType.supplierpart,
|
||||
hidden: !instance?.supplier_part
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'location',
|
||||
label: t`Location`,
|
||||
model: ModelType.stocklocation,
|
||||
hidden: !instance?.location
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'belongs_to',
|
||||
label: t`Installed In`,
|
||||
model_filters: {
|
||||
part_detail: true
|
||||
},
|
||||
model_formatter: (model: any) => {
|
||||
let text = model?.part_detail?.full_name ?? model?.name;
|
||||
if (model.serial && model.quantity == 1) {
|
||||
text += ` # ${model.serial}`;
|
||||
}
|
||||
return text;
|
||||
},
|
||||
icon: 'stock',
|
||||
model: ModelType.stockitem,
|
||||
hidden: !instance?.belongs_to
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'parent',
|
||||
icon: 'sitemap',
|
||||
label: t`Parent Item`,
|
||||
model: ModelType.stockitem,
|
||||
hidden: !instance?.parent,
|
||||
model_formatter: () => t`Parent stock item`
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'consumed_by',
|
||||
label: t`Consumed By`,
|
||||
model: ModelType.build,
|
||||
hidden: !instance?.consumed_by,
|
||||
icon: 'build',
|
||||
model_field: 'reference'
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'build',
|
||||
label: t`Build Order`,
|
||||
model: ModelType.build,
|
||||
hidden: !instance?.build,
|
||||
model_field: 'reference'
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'purchase_order',
|
||||
label: t`Purchase Order`,
|
||||
model: ModelType.purchaseorder,
|
||||
hidden: !instance?.purchase_order,
|
||||
icon: 'purchase_orders',
|
||||
model_field: 'reference'
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'sales_order',
|
||||
label: t`Sales Order`,
|
||||
model: ModelType.salesorder,
|
||||
hidden: !instance?.sales_order,
|
||||
icon: 'sales_orders',
|
||||
model_field: 'reference'
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'customer',
|
||||
label: t`Customer`,
|
||||
model: ModelType.company,
|
||||
hidden: !instance?.customer
|
||||
}
|
||||
];
|
||||
|
||||
const br: DetailsField[] = [
|
||||
{
|
||||
type: 'date',
|
||||
name: 'expiry_date',
|
||||
label: t`Expiry Date`,
|
||||
hidden: !enableExpiry || !instance?.expiry_date,
|
||||
icon: 'calendar'
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'purchase_price',
|
||||
label: t`Unit Price`,
|
||||
icon: 'currency',
|
||||
hidden: !instance?.purchase_price,
|
||||
value_formatter: () =>
|
||||
formatCurrency(instance?.purchase_price, {
|
||||
currency: instance?.purchase_price_currency
|
||||
})
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'stock_value',
|
||||
label: t`Stock Value`,
|
||||
icon: 'currency',
|
||||
hidden:
|
||||
!instance?.purchase_price ||
|
||||
instance?.quantity == 1 ||
|
||||
instance?.quantity == 0,
|
||||
value_formatter: () =>
|
||||
formatCurrency(instance?.purchase_price, {
|
||||
currency: instance?.purchase_price_currency,
|
||||
multiplier: instance?.quantity
|
||||
})
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'packaging',
|
||||
icon: 'part',
|
||||
label: t`Packaging`,
|
||||
hidden: !instance?.packaging
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'creation_date',
|
||||
icon: 'calendar',
|
||||
label: t`Created`,
|
||||
hidden: !instance?.creation_date
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'updated',
|
||||
icon: 'calendar',
|
||||
label: t`Last Updated`
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'stocktake_date',
|
||||
icon: 'calendar',
|
||||
label: t`Last Stocktake`,
|
||||
hidden: !instance?.stocktake_date
|
||||
}
|
||||
];
|
||||
|
||||
if (!instance?.pk) return <Skeleton />;
|
||||
|
||||
return (
|
||||
<>
|
||||
{showSerialNav && findBySerialNumber.modal}
|
||||
<ItemDetailsGrid>
|
||||
<Stack gap='xs'>
|
||||
<Grid grow>
|
||||
<DetailsImage
|
||||
appRole={allowImageEdit ? UserRoles.part : undefined}
|
||||
imageActions={
|
||||
allowImageEdit
|
||||
? {
|
||||
uploadFile: true,
|
||||
deleteFile: true
|
||||
}
|
||||
: {}
|
||||
}
|
||||
apiPath={apiUrl(ApiEndpoints.part_list, instance?.part)}
|
||||
src={part?.image ?? part?.thumbnail}
|
||||
pk={instance?.part}
|
||||
refresh={refreshInstance}
|
||||
/>
|
||||
<Grid.Col span={{ base: 12, sm: 8 }}>
|
||||
<DetailsTable fields={tl} item={data} />
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
<TagsList tags={instance?.tags} />
|
||||
</Stack>
|
||||
<DetailsTable fields={tr} item={data} />
|
||||
<DetailsTable fields={bl} item={data} />
|
||||
<DetailsTable fields={br} item={data} />
|
||||
</ItemDetailsGrid>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue