Merge cf88f84247 into c56fe7f612
This commit is contained in:
commit
48f698d755
|
|
@ -25,6 +25,7 @@ The *Display Settings* screen shows general display configuration options:
|
|||
{{ usersetting("BARCODE_IN_FORM_FIELDS") }}
|
||||
{{ usersetting("DATE_DISPLAY_FORMAT") }}
|
||||
{{ usersetting("FORMS_CLOSE_USING_ESCAPE") }}
|
||||
{{ usersetting("ENABLE_PREVIEW_PANEL") }}
|
||||
{{ usersetting("DISPLAY_STOCKTAKE_TAB") }}
|
||||
{{ usersetting("SHOW_FULL_CATEGORY_IN_TABLES")}}
|
||||
{{ usersetting("SHOW_BOM_SUBASSEMBLY_LEVELS")}}
|
||||
|
|
|
|||
|
|
@ -223,6 +223,12 @@ USER_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
|
|||
('MMM DD YYYY', 'Feb 22 2022'),
|
||||
],
|
||||
},
|
||||
'ENABLE_PREVIEW_PANEL': {
|
||||
'name': _('Enable Preview Panel'),
|
||||
'description': _('Display a preview panel when selecting items in tables'),
|
||||
'default': True,
|
||||
'validator': bool,
|
||||
},
|
||||
'DISPLAY_STOCKTAKE_TAB': {
|
||||
'name': _('Show Stock History'),
|
||||
'description': _('Display stock history information in the part detail page'),
|
||||
|
|
|
|||
|
|
@ -69,6 +69,12 @@ export type ImporterDrawerContext = {
|
|||
sessionId: () => number | null;
|
||||
};
|
||||
|
||||
export type PreviewDrawerContext = {
|
||||
open: (modelType: ModelType, id?: number, instance?: any) => void;
|
||||
close: () => void;
|
||||
isOpen: () => boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* A set of properties which are passed to a plugin,
|
||||
* for rendering an element in the user interface.
|
||||
|
|
@ -90,6 +96,7 @@ export type ImporterDrawerContext = {
|
|||
* @param forms - A set of functions for opening various API forms (see ../components/Forms.tsx)
|
||||
* @param tables - A set of functions for rendering API tables
|
||||
* @param importer - A set of functions for controlling the global importer drawer (see ../components/importer/GlobalImporterDrawer.tsx)
|
||||
* @param preview - A set of functions for controlling the global preview drawer (see ../components/previews/GlobalPreviewDrawer.tsx)
|
||||
* @param model - The model type associated with the rendered component (if applicable)
|
||||
* @param id - The ID (primary key) of the model instance for the plugin (if applicable)
|
||||
* @param instance - The model instance data (if available)
|
||||
|
|
@ -125,6 +132,7 @@ export type InvenTreePluginContext = {
|
|||
};
|
||||
tables: InvenTreeTablesContext<any>;
|
||||
importer: ImporterDrawerContext;
|
||||
preview: PreviewDrawerContext;
|
||||
model?: ModelType | string;
|
||||
id?: string | number | null;
|
||||
instance?: any;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,11 @@ import { ModelInformationDict } from '@lib/enums/ModelInformation';
|
|||
import type { ModelType } from '@lib/enums/ModelType';
|
||||
import type { UserRoles } from '@lib/enums/Roles';
|
||||
import { apiUrl } from '@lib/functions/Api';
|
||||
import { getDetailUrl, navigateToLink } from '@lib/functions/Navigation';
|
||||
import {
|
||||
eventModified,
|
||||
getDetailUrl,
|
||||
navigateToLink
|
||||
} from '@lib/functions/Navigation';
|
||||
import type { TableFilter } from '@lib/types/Filters';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { ActionIcon, Group, Text } from '@mantine/core';
|
||||
|
|
@ -22,6 +26,7 @@ import { useCallback, useMemo } from 'react';
|
|||
import { useNavigate } from 'react-router-dom';
|
||||
import { api } from '../../App';
|
||||
import useCalendar from '../../hooks/UseCalendar';
|
||||
import { openGlobalPreview } from '../../states/PreviewDrawerState';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import {
|
||||
AssignedToMeFilter,
|
||||
|
|
@ -166,14 +171,18 @@ export default function OrderCalendar({
|
|||
}
|
||||
};
|
||||
|
||||
// Callback when PurchaseOrder is clicked
|
||||
// Callback when an order is clicked - open preview drawer, or navigate on modifier click
|
||||
const onClickOrder = (info: EventClickArg) => {
|
||||
if (!!info.event.id) {
|
||||
if (!info.event.id) return;
|
||||
|
||||
if (eventModified(event as any)) {
|
||||
navigateToLink(
|
||||
getDetailUrl(model, info.event.id),
|
||||
navigate,
|
||||
info.jsEvent
|
||||
);
|
||||
} else {
|
||||
openGlobalPreview(model, Number.parseInt(info.event.id));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -280,7 +280,7 @@ function NumberValue(props: Readonly<FieldProps>) {
|
|||
const value = props?.field_value;
|
||||
|
||||
// Convert to double
|
||||
const numberValue = Number.parseFloat(value.toString());
|
||||
const numberValue = Number.parseFloat(value?.toString() ?? '');
|
||||
|
||||
if (value === null || value === undefined) {
|
||||
return <Text size='sm'>'---'</Text>;
|
||||
|
|
@ -465,10 +465,12 @@ function CopyField({ value }: Readonly<{ value: string }>) {
|
|||
|
||||
export function DetailsTableField({
|
||||
item,
|
||||
field
|
||||
field,
|
||||
showIcons = true
|
||||
}: Readonly<{
|
||||
item: any;
|
||||
field: DetailsField;
|
||||
showIcons?: boolean;
|
||||
}>) {
|
||||
function getFieldType(type: string) {
|
||||
switch (type) {
|
||||
|
|
@ -502,10 +504,12 @@ export function DetailsTableField({
|
|||
<Table.Tr style={{ verticalAlign: 'top' }}>
|
||||
<Table.Td style={{ minWidth: 75, lineBreak: 'auto', flex: 2 }}>
|
||||
<Group gap='xs' wrap='nowrap'>
|
||||
<InvenTreeIcon
|
||||
icon={field.icon ?? (field.name as keyof InvenTreeIconType)}
|
||||
/>
|
||||
<Text style={{ paddingLeft: 10 }}>{field.label}</Text>
|
||||
{showIcons && (
|
||||
<InvenTreeIcon
|
||||
icon={field.icon ?? (field.name as keyof InvenTreeIconType)}
|
||||
/>
|
||||
)}
|
||||
<Text style={{ paddingLeft: showIcons ? 10 : 0 }}>{field.label}</Text>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
<Table.Td
|
||||
|
|
@ -525,15 +529,19 @@ export function DetailsTableField({
|
|||
);
|
||||
}
|
||||
|
||||
export function DetailsTable({
|
||||
item,
|
||||
fields,
|
||||
title
|
||||
}: Readonly<{
|
||||
export interface DetailsTableProps {
|
||||
item: any;
|
||||
fields: DetailsField[];
|
||||
title?: string;
|
||||
}>) {
|
||||
showIcons?: boolean;
|
||||
}
|
||||
|
||||
export function DetailsTable({
|
||||
item,
|
||||
fields,
|
||||
title,
|
||||
showIcons = true
|
||||
}: Readonly<DetailsTableProps>) {
|
||||
const visibleFields = useMemo(() => {
|
||||
return fields.filter((field) => !field.hidden);
|
||||
}, [fields]);
|
||||
|
|
@ -553,7 +561,12 @@ export function DetailsTable({
|
|||
<Table striped verticalSpacing={5} horizontalSpacing='sm'>
|
||||
<Table.Tbody>
|
||||
{visibleFields.map((field: DetailsField, index: number) => (
|
||||
<DetailsTableField field={field} item={item} key={index} />
|
||||
<DetailsTableField
|
||||
field={field}
|
||||
item={item}
|
||||
showIcons={showIcons}
|
||||
key={index}
|
||||
/>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
|
|
|
|||
|
|
@ -406,6 +406,10 @@ export function DetailsImage(props: Readonly<DetailImageProps>) {
|
|||
const { hovered, ref } = useHover();
|
||||
const [img, setImg] = useState<string>(props.src ?? backup_image);
|
||||
|
||||
useEffect(() => {
|
||||
setImg(props.src ?? backup_image);
|
||||
}, [props.src]);
|
||||
|
||||
// Sets a new image, and triggers upstream instance refresh
|
||||
const setAndRefresh = (image: string) => {
|
||||
setImg(image);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,20 @@
|
|||
import { Paper, SimpleGrid } from '@mantine/core';
|
||||
import type React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { DetailsTable, type DetailsTableProps } from './Details';
|
||||
|
||||
export type { DetailsTableProps };
|
||||
|
||||
export function ItemDetailsGrid({
|
||||
children,
|
||||
tables
|
||||
}: React.PropsWithChildren<{ tables?: DetailsTableProps[] }>) {
|
||||
const visibleTables = useMemo(
|
||||
() => tables?.filter((t) => t.fields.some((f) => !f.hidden)) ?? [],
|
||||
[tables]
|
||||
);
|
||||
|
||||
export function ItemDetailsGrid(props: React.PropsWithChildren<{}>) {
|
||||
return (
|
||||
<Paper p='xs'>
|
||||
<SimpleGrid
|
||||
|
|
@ -10,7 +23,10 @@ export function ItemDetailsGrid(props: React.PropsWithChildren<{}>) {
|
|||
spacing='xs'
|
||||
verticalSpacing='xs'
|
||||
>
|
||||
{props.children}
|
||||
{children}
|
||||
{visibleTables.map((props, index) => (
|
||||
<DetailsTable key={index} {...props} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Paper>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||
import type { ModelType } from '@lib/enums/ModelType';
|
||||
import { apiUrl } from '@lib/functions/Api';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useMemo } from 'react';
|
||||
import { useApi } from '../../contexts/ApiContext';
|
||||
import type { DetailsField, DetailsTableProps } from './Details';
|
||||
|
||||
export function useParameterDetailsGrid({
|
||||
model_type,
|
||||
model_id
|
||||
}: Readonly<{
|
||||
model_type: ModelType;
|
||||
model_id: number | string | undefined;
|
||||
}>): DetailsTableProps {
|
||||
const api = useApi();
|
||||
|
||||
const { data: parameters = [] } = useQuery({
|
||||
queryKey: ['parameter-details', model_type, model_id],
|
||||
enabled: !!model_id && !!model_type,
|
||||
queryFn: () =>
|
||||
api
|
||||
.get(apiUrl(ApiEndpoints.parameter_list), {
|
||||
params: { model_type, model_id, limit: 100 }
|
||||
})
|
||||
.then((res) => res.data?.results ?? [])
|
||||
});
|
||||
|
||||
const { fields, item } = useMemo(() => {
|
||||
const item: Record<string, string> = {};
|
||||
const fields: DetailsField[] = parameters.map((param: any) => {
|
||||
const key = `param_${param.pk}`;
|
||||
const value =
|
||||
param.data +
|
||||
(param.template_detail?.units ? ` ${param.template_detail.units}` : '');
|
||||
item[key] = value;
|
||||
return {
|
||||
type: 'string' as const,
|
||||
name: key,
|
||||
label: param.template_detail?.name ?? String(param.pk),
|
||||
copy: true
|
||||
};
|
||||
});
|
||||
return { fields, item };
|
||||
}, [parameters]);
|
||||
|
||||
return {
|
||||
title: t`Parameters`,
|
||||
fields: fields,
|
||||
item: item,
|
||||
showIcons: false
|
||||
};
|
||||
}
|
||||
|
|
@ -29,6 +29,7 @@ import {
|
|||
type PluginUIFeature,
|
||||
PluginUIFeatureType
|
||||
} from '../plugins/PluginUIFeature';
|
||||
import GlobalPreviewDrawer from '../previews/GlobalPreviewDrawer';
|
||||
import { Footer } from './Footer';
|
||||
import { Header } from './Header';
|
||||
|
||||
|
|
@ -148,6 +149,7 @@ export default function LayoutComponent() {
|
|||
)}
|
||||
</Flex>
|
||||
<GlobalImporterDrawer />
|
||||
<GlobalPreviewDrawer />
|
||||
</>
|
||||
</ProtectedRoute>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -46,6 +46,11 @@ import {
|
|||
openGlobalImporter
|
||||
} from '../../states/ImporterState';
|
||||
import { usePluginState } from '../../states/PluginState';
|
||||
import {
|
||||
closeGlobalPreview,
|
||||
getGlobalPreviewState,
|
||||
openGlobalPreview
|
||||
} from '../../states/PreviewDrawerState';
|
||||
import { useServerApiState } from '../../states/ServerApiState';
|
||||
import { InvenTreeTableInternal } from '../../tables/InvenTreeTable';
|
||||
import { EditApiForm } from '../forms/ApiForm';
|
||||
|
|
@ -97,6 +102,12 @@ export const useInvenTreeContext = () => {
|
|||
isOpen: () => getGlobalImporterState().isOpen,
|
||||
sessionId: () => getGlobalImporterState().sessionId
|
||||
},
|
||||
preview: {
|
||||
open: (modelType, id?, instance?) =>
|
||||
openGlobalPreview(modelType, id, instance),
|
||||
close: () => closeGlobalPreview(),
|
||||
isOpen: () => getGlobalPreviewState().isOpen
|
||||
},
|
||||
tables: {
|
||||
renderTable: (props: InvenTreeTableRenderProps<any>) => (
|
||||
<InvenTreeTableInternal
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
import { usePreviewDrawerState } from '../../states/PreviewDrawerState';
|
||||
import { useUserSettingsState } from '../../states/SettingsStates';
|
||||
import PreviewDrawer from './PreviewDrawer';
|
||||
|
||||
export default function GlobalPreviewDrawer() {
|
||||
const enabled = useUserSettingsState((state) =>
|
||||
state.isSet('ENABLE_PREVIEW_PANEL')
|
||||
);
|
||||
const isOpen = usePreviewDrawerState((state) => state.isOpen);
|
||||
const modelType = usePreviewDrawerState((state) => state.modelType);
|
||||
const id = usePreviewDrawerState((state) => state.id);
|
||||
const instance = usePreviewDrawerState((state) => state.instance);
|
||||
const preview = usePreviewDrawerState((state) => state.preview);
|
||||
const closePreview = usePreviewDrawerState((state) => state.closePreview);
|
||||
|
||||
if (!enabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<PreviewDrawer
|
||||
modelType={modelType}
|
||||
id={id}
|
||||
instance={instance}
|
||||
preview={preview}
|
||||
opened={isOpen}
|
||||
onClose={closePreview}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
import {
|
||||
ActionIcon,
|
||||
Anchor,
|
||||
Divider,
|
||||
Drawer,
|
||||
Group,
|
||||
LoadingOverlay,
|
||||
Stack,
|
||||
Tooltip
|
||||
} from '@mantine/core';
|
||||
|
||||
import { StylishText } from '@lib/components/StylishText';
|
||||
import { ModelInformationDict } from '@lib/enums/ModelInformation';
|
||||
import { cancelEvent } from '@lib/functions/Events';
|
||||
import {
|
||||
eventModified,
|
||||
getDetailUrl,
|
||||
navigateToLink
|
||||
} from '@lib/functions/Navigation';
|
||||
import type { ModelType } from '@lib/index';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { IconArrowRight } from '@tabler/icons-react';
|
||||
import type React from 'react';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useInstance } from '../../hooks/UseInstance';
|
||||
import { getModelInfo } from '../render/ModelType';
|
||||
import { type PreviewType, getPreviewComponentForModel } from './PreviewType';
|
||||
import { FallbackPreviewComponent } from './models/Fallback';
|
||||
|
||||
export default function PreviewDrawer({
|
||||
modelType,
|
||||
id,
|
||||
instance: providedInstance,
|
||||
filters,
|
||||
preview: providedPreview,
|
||||
opened,
|
||||
onClose
|
||||
}: Readonly<{
|
||||
modelType?: ModelType;
|
||||
id?: number | string;
|
||||
instance?: any;
|
||||
filters?: Record<string, any>;
|
||||
preview?: PreviewType;
|
||||
opened: boolean;
|
||||
onClose: () => void;
|
||||
}>) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const modelInfo = modelType ? getModelInfo(modelType) : null;
|
||||
const apiEndpoint = modelType
|
||||
? ModelInformationDict[modelType].api_endpoint
|
||||
: undefined;
|
||||
|
||||
const { instance: fetchedInstance, instanceQuery } = useInstance({
|
||||
endpoint: apiEndpoint!,
|
||||
pk: id,
|
||||
hasPrimaryKey: true,
|
||||
defaultValue: {},
|
||||
params: filters,
|
||||
disabled: !!providedInstance || !modelType || !id
|
||||
});
|
||||
|
||||
const instance = useMemo(() => {
|
||||
return providedInstance ?? fetchedInstance;
|
||||
}, [providedInstance, fetchedInstance]);
|
||||
|
||||
const previewComponent: PreviewType | null = useMemo(() => {
|
||||
if (providedPreview) return providedPreview;
|
||||
if (!modelType || !modelInfo || id == null) return null;
|
||||
|
||||
const component: PreviewType | null = getPreviewComponentForModel({
|
||||
modelType,
|
||||
instance,
|
||||
modelId: typeof id === 'string' ? Number(id) : id
|
||||
});
|
||||
|
||||
if (component == null) {
|
||||
return FallbackPreviewComponent({
|
||||
modelInfo,
|
||||
modelType,
|
||||
modelId: id,
|
||||
instance
|
||||
});
|
||||
}
|
||||
|
||||
return component;
|
||||
}, [providedPreview, modelType, id, instance]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!opened) return;
|
||||
const handler = (event: MouseEvent) => {
|
||||
const anchor = (event.target as HTMLElement).closest('a');
|
||||
if (anchor && !eventModified(event as any)) {
|
||||
if (anchor.origin === window.location.origin) {
|
||||
// Same-origin: prevent browser navigation and route internally
|
||||
const href = anchor.pathname + anchor.search + anchor.hash;
|
||||
cancelEvent(event);
|
||||
onClose();
|
||||
navigateToLink(href, navigate, event as any);
|
||||
} else {
|
||||
// External link: let browser open it, just close the drawer
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
};
|
||||
document.addEventListener('click', handler, true);
|
||||
return () => document.removeEventListener('click', handler, true);
|
||||
}, [onClose, opened]);
|
||||
|
||||
const clickTitle = useCallback(
|
||||
(
|
||||
event: Parameters<
|
||||
React.AnchorHTMLAttributes<HTMLAnchorElement>['onClick'] & {}
|
||||
>[0]
|
||||
) => {
|
||||
if (!modelType || !id) return;
|
||||
|
||||
if (!eventModified(event as any)) {
|
||||
onClose();
|
||||
}
|
||||
navigateToLink(getDetailUrl(modelType!, id!), navigate, event as any);
|
||||
},
|
||||
[modelType, id, navigate]
|
||||
);
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
position='right'
|
||||
size='xl'
|
||||
title={
|
||||
previewComponent ? (
|
||||
!!modelType && !!id ? (
|
||||
<Anchor
|
||||
href={getDetailUrl(modelType!, id, true)}
|
||||
onClick={(e) => clickTitle(e)}
|
||||
>
|
||||
<Group gap='xs'>
|
||||
<Tooltip label={t`View Details`} position='left'>
|
||||
<ActionIcon variant='transparent' size='lg'>
|
||||
<IconArrowRight />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<StylishText size='lg'>{previewComponent.title}</StylishText>
|
||||
</Group>
|
||||
</Anchor>
|
||||
) : (
|
||||
<StylishText size='lg'>{previewComponent.title}</StylishText>
|
||||
)
|
||||
) : null
|
||||
}
|
||||
opened={opened}
|
||||
onClose={onClose}
|
||||
withCloseButton
|
||||
transitionProps={{
|
||||
transition: 'slide-left',
|
||||
duration: 300,
|
||||
timingFunction: 'ease'
|
||||
}}
|
||||
>
|
||||
<Stack gap='xs'>
|
||||
{previewComponent && (
|
||||
<>
|
||||
<Divider />
|
||||
<LoadingOverlay visible={instanceQuery.isFetching} />
|
||||
{previewComponent.preview}
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
import { ModelType } from '@lib/enums/ModelType';
|
||||
import type { ReactNode } from 'react';
|
||||
import { BuildOrderPreviewComponent } from './models/BuildOrderPreview';
|
||||
import { CompanyPreviewComponent } from './models/CompanyPreview';
|
||||
import { ManufacturerPartPreviewComponent } from './models/ManufacturerPartPreview';
|
||||
import { PartCategoryPreviewComponent } from './models/PartCategoryPreview';
|
||||
import { PartPreviewComponent } from './models/PartPreview';
|
||||
import { PurchaseOrderPreviewComponent } from './models/PurchaseOrderPreview';
|
||||
import { ReturnOrderPreviewComponent } from './models/ReturnOrderPreview';
|
||||
import { SalesOrderPreviewComponent } from './models/SalesOrderPreview';
|
||||
import { SalesOrderShipmentPreviewComponent } from './models/SalesOrderShipmentPreview';
|
||||
import { StockLocationPreviewComponent } from './models/StockLocationPreview';
|
||||
import { StockPreviewComponent } from './models/StockPreview';
|
||||
import { SupplierPartPreviewComponent } from './models/SupplierPartPreview';
|
||||
import { TransferOrderPreviewComponent } from './models/TransferOrderPreview';
|
||||
|
||||
export interface PreviewType {
|
||||
preview: ReactNode;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export type PreviewComponentProps = {
|
||||
instance: any;
|
||||
};
|
||||
|
||||
export type PreviewComponent = (props: PreviewComponentProps) => PreviewType;
|
||||
|
||||
export function getPreviewComponentForModel({
|
||||
modelType,
|
||||
instance,
|
||||
modelId
|
||||
}: {
|
||||
modelType: ModelType;
|
||||
instance: any;
|
||||
modelId: number;
|
||||
}): PreviewType | null {
|
||||
switch (modelType) {
|
||||
case ModelType.part:
|
||||
return PartPreviewComponent({ instance, modelId });
|
||||
case ModelType.stockitem:
|
||||
return StockPreviewComponent({ instance, modelId });
|
||||
case ModelType.purchaseorder:
|
||||
return PurchaseOrderPreviewComponent({ instance, modelId });
|
||||
case ModelType.salesorder:
|
||||
return SalesOrderPreviewComponent({ instance, modelId });
|
||||
case ModelType.returnorder:
|
||||
return ReturnOrderPreviewComponent({ instance, modelId });
|
||||
case ModelType.supplierpart:
|
||||
return SupplierPartPreviewComponent({ instance, modelId });
|
||||
case ModelType.manufacturerpart:
|
||||
return ManufacturerPartPreviewComponent({ instance, modelId });
|
||||
case ModelType.company:
|
||||
return CompanyPreviewComponent({ instance, modelId });
|
||||
case ModelType.build:
|
||||
return BuildOrderPreviewComponent({ instance, modelId });
|
||||
case ModelType.salesordershipment:
|
||||
return SalesOrderShipmentPreviewComponent({ instance, modelId });
|
||||
case ModelType.transferorder:
|
||||
return TransferOrderPreviewComponent({ instance, modelId });
|
||||
case ModelType.stocklocation:
|
||||
return StockLocationPreviewComponent({ instance, modelId });
|
||||
case ModelType.partcategory:
|
||||
return PartCategoryPreviewComponent({ instance, modelId });
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import { t } from '@lingui/core/macro';
|
||||
import { BuildOrderDetailsPanel } from '../../../pages/build/BuildOrderDetailsPanel';
|
||||
import type { PreviewType } from '../PreviewType';
|
||||
|
||||
export function BuildOrderPreviewComponent({
|
||||
instance,
|
||||
modelId
|
||||
}: Readonly<{
|
||||
instance: any;
|
||||
modelId: number;
|
||||
}>): PreviewType {
|
||||
const part = instance?.part_detail?.full_name ?? instance?.part_detail?.name;
|
||||
const ref = instance?.reference ?? `#${modelId}`;
|
||||
|
||||
let title = `${t`Build Order`} ${ref}`;
|
||||
|
||||
if (part) {
|
||||
title += ` (${part})`;
|
||||
}
|
||||
|
||||
return {
|
||||
title,
|
||||
preview: <BuildOrderDetailsPanel instance={instance} />
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { t } from '@lingui/core/macro';
|
||||
import { CompanyDetailsPanel } from '../../../pages/company/CompanyDetailsPanel';
|
||||
import type { PreviewType } from '../PreviewType';
|
||||
|
||||
export function CompanyPreviewComponent({
|
||||
instance,
|
||||
modelId
|
||||
}: Readonly<{
|
||||
instance: any;
|
||||
modelId: number;
|
||||
}>): PreviewType {
|
||||
const name = instance?.name ?? `#${modelId}`;
|
||||
|
||||
return {
|
||||
title: `${t`Company`} - ${name}`,
|
||||
preview: <CompanyDetailsPanel instance={instance} />
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import type { ModelInformationInterface } from '@lib/enums/ModelInformation';
|
||||
import type { ModelType } from '@lib/enums/ModelType';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Alert, Text } from '@mantine/core';
|
||||
import { IconExclamationCircle } from '@tabler/icons-react';
|
||||
import type { PreviewType } from '../PreviewType';
|
||||
|
||||
export function FallbackPreviewComponent({
|
||||
modelInfo,
|
||||
modelType,
|
||||
modelId,
|
||||
instance
|
||||
}: {
|
||||
modelInfo: ModelInformationInterface;
|
||||
modelType: ModelType;
|
||||
modelId: number | string;
|
||||
instance: any;
|
||||
}): PreviewType {
|
||||
return {
|
||||
title: `${modelInfo.label} #${modelId}`,
|
||||
preview: (
|
||||
<Alert
|
||||
color='red'
|
||||
title={t`No preview available`}
|
||||
icon={<IconExclamationCircle />}
|
||||
>
|
||||
<Text>{t`No preview available for this model.`}</Text>
|
||||
</Alert>
|
||||
)
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import { t } from '@lingui/core/macro';
|
||||
import { ManufacturerPartDetailsPanel } from '../../../pages/company/ManufacturerPartDetailsPanel';
|
||||
import type { PreviewType } from '../PreviewType';
|
||||
|
||||
export function ManufacturerPartPreviewComponent({
|
||||
instance,
|
||||
modelId
|
||||
}: Readonly<{
|
||||
instance: any;
|
||||
modelId: number;
|
||||
}>): PreviewType {
|
||||
const manufacturer =
|
||||
instance?.manufacturer_detail?.name ?? instance?.manufacturer_name;
|
||||
const mpn = instance?.MPN ?? `#${modelId}`;
|
||||
|
||||
let title = `${t`Manufacturer Part`} ${mpn}`;
|
||||
|
||||
if (manufacturer) {
|
||||
title += ` (${manufacturer})`;
|
||||
}
|
||||
|
||||
return {
|
||||
title,
|
||||
preview: <ManufacturerPartDetailsPanel instance={instance} />
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { t } from '@lingui/core/macro';
|
||||
import { PartCategoryDetailsPanel } from '../../../pages/part/PartCategoryDetailsPanel';
|
||||
import type { PreviewType } from '../PreviewType';
|
||||
|
||||
export function PartCategoryPreviewComponent({
|
||||
instance,
|
||||
modelId
|
||||
}: Readonly<{
|
||||
instance: any;
|
||||
modelId: number;
|
||||
}>): PreviewType {
|
||||
const name = `${t`Part Category`} - ${instance?.name ?? `#${modelId}`}`;
|
||||
|
||||
return {
|
||||
title: name,
|
||||
preview: <PartCategoryDetailsPanel instance={instance} />
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import { PartDetailsPanel } from '../../../pages/part/PartDetailsPanel';
|
||||
import type { PreviewType } from '../PreviewType';
|
||||
|
||||
export function PartPreviewComponent({
|
||||
instance,
|
||||
modelId
|
||||
}: Readonly<{
|
||||
instance: any;
|
||||
modelId: number;
|
||||
}>): PreviewType {
|
||||
return {
|
||||
title: instance?.full_name || instance?.name || `Part #${modelId}`,
|
||||
preview: <PartDetailsPanel instance={instance} />
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import { t } from '@lingui/core/macro';
|
||||
import { PurchaseOrderDetailsPanel } from '../../../pages/purchasing/PurchaseOrderDetailsPanel';
|
||||
import type { PreviewType } from '../PreviewType';
|
||||
|
||||
export function PurchaseOrderPreviewComponent({
|
||||
instance,
|
||||
modelId
|
||||
}: Readonly<{
|
||||
instance: any;
|
||||
modelId: number;
|
||||
}>): PreviewType {
|
||||
const supplier = instance?.supplier_detail?.name ?? instance?.supplier_name;
|
||||
const ref = instance?.reference ?? `#${modelId}`;
|
||||
|
||||
let title = `${t`Purchase Order`} ${ref}`;
|
||||
|
||||
if (supplier) {
|
||||
title += ` (${supplier})`;
|
||||
}
|
||||
|
||||
return {
|
||||
title: title,
|
||||
preview: <PurchaseOrderDetailsPanel instance={instance} />
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import { t } from '@lingui/core/macro';
|
||||
import { ReturnOrderDetailsPanel } from '../../../pages/sales/ReturnOrderDetailsPanel';
|
||||
import type { PreviewType } from '../PreviewType';
|
||||
|
||||
export function ReturnOrderPreviewComponent({
|
||||
instance,
|
||||
modelId
|
||||
}: Readonly<{
|
||||
instance: any;
|
||||
modelId: number;
|
||||
}>): PreviewType {
|
||||
const customer = instance?.customer_detail?.name ?? instance?.customer_name;
|
||||
const ref = instance?.reference ?? `#${modelId}`;
|
||||
|
||||
let title = `${t`Return Order`} ${ref}`;
|
||||
|
||||
if (customer) {
|
||||
title += ` (${customer})`;
|
||||
}
|
||||
|
||||
return {
|
||||
title,
|
||||
preview: <ReturnOrderDetailsPanel instance={instance} />
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import { t } from '@lingui/core/macro';
|
||||
import { SalesOrderDetailsPanel } from '../../../pages/sales/SalesOrderDetailsPanel';
|
||||
import type { PreviewType } from '../PreviewType';
|
||||
|
||||
export function SalesOrderPreviewComponent({
|
||||
instance,
|
||||
modelId
|
||||
}: Readonly<{
|
||||
instance: any;
|
||||
modelId: number;
|
||||
}>): PreviewType {
|
||||
const customer = instance?.customer_detail?.name ?? instance?.customer_name;
|
||||
const ref = instance?.reference ?? `#${modelId}`;
|
||||
|
||||
let title = `${t`Sales Order`} ${ref}`;
|
||||
|
||||
if (customer) {
|
||||
title += ` (${customer})`;
|
||||
}
|
||||
|
||||
return {
|
||||
title,
|
||||
preview: <SalesOrderDetailsPanel instance={instance} />
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import { t } from '@lingui/core/macro';
|
||||
import { SalesOrderShipmentDetailsPanel } from '../../../pages/sales/SalesOrderShipmentDetailsPanel';
|
||||
import type { PreviewType } from '../PreviewType';
|
||||
|
||||
export function SalesOrderShipmentPreviewComponent({
|
||||
instance,
|
||||
modelId
|
||||
}: Readonly<{
|
||||
instance: any;
|
||||
modelId: number;
|
||||
}>): PreviewType {
|
||||
const order = instance?.order_detail?.reference;
|
||||
const ref = instance?.reference ?? `#${modelId}`;
|
||||
|
||||
let title = `${t`Shipment`} ${ref}`;
|
||||
|
||||
if (order) {
|
||||
title += ` (${order})`;
|
||||
}
|
||||
|
||||
return {
|
||||
title,
|
||||
preview: <SalesOrderShipmentDetailsPanel instance={instance} />
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { t } from '@lingui/core/macro';
|
||||
import { StockLocationDetailsPanel } from '../../../pages/stock/StockLocationDetailsPanel';
|
||||
import type { PreviewType } from '../PreviewType';
|
||||
|
||||
export function StockLocationPreviewComponent({
|
||||
instance,
|
||||
modelId
|
||||
}: Readonly<{
|
||||
instance: any;
|
||||
modelId: number;
|
||||
}>): PreviewType {
|
||||
const name = `${t`Stock Location`} - ${instance?.name ?? `#${modelId}`}`;
|
||||
|
||||
return {
|
||||
title: name,
|
||||
preview: <StockLocationDetailsPanel instance={instance} />
|
||||
};
|
||||
}
|
||||
|
|
@ -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} />
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import { t } from '@lingui/core/macro';
|
||||
import { SupplierPartDetailsPanel } from '../../../pages/company/SupplierPartDetailsPanel';
|
||||
import type { PreviewType } from '../PreviewType';
|
||||
|
||||
export function SupplierPartPreviewComponent({
|
||||
instance,
|
||||
modelId
|
||||
}: Readonly<{
|
||||
instance: any;
|
||||
modelId: number;
|
||||
}>): PreviewType {
|
||||
const supplier = instance?.supplier_detail?.name ?? instance?.supplier_name;
|
||||
const sku = instance?.SKU ?? `#${modelId}`;
|
||||
|
||||
let title = `${t`Supplier Part`} ${sku}`;
|
||||
|
||||
if (supplier) {
|
||||
title += ` (${supplier})`;
|
||||
}
|
||||
|
||||
return {
|
||||
title,
|
||||
preview: <SupplierPartDetailsPanel instance={instance} />
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { t } from '@lingui/core/macro';
|
||||
import { TransferOrderDetailsPanel } from '../../../pages/stock/TransferOrderDetailsPanel';
|
||||
import type { PreviewType } from '../PreviewType';
|
||||
|
||||
export function TransferOrderPreviewComponent({
|
||||
instance,
|
||||
modelId
|
||||
}: Readonly<{
|
||||
instance: any;
|
||||
modelId: number;
|
||||
}>): PreviewType {
|
||||
const ref = instance?.reference ?? `#${modelId}`;
|
||||
|
||||
return {
|
||||
title: `${t`Transfer Order`} ${ref}`,
|
||||
preview: <TransferOrderDetailsPanel instance={instance} />
|
||||
};
|
||||
}
|
||||
|
|
@ -59,6 +59,7 @@ export default function UserSettings() {
|
|||
'BARCODE_IN_FORM_FIELDS',
|
||||
'DATE_DISPLAY_FORMAT',
|
||||
'FORMS_CLOSE_USING_ESCAPE',
|
||||
'ENABLE_PREVIEW_PANEL',
|
||||
'DISPLAY_STOCKTAKE_TAB',
|
||||
'ENABLE_LAST_BREADCRUMB',
|
||||
'SHOW_EXTRA_MODEL_INFO',
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { t } from '@lingui/core/macro';
|
||||
import { Alert, Grid, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import { Alert, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import {
|
||||
IconChecklist,
|
||||
IconCircleCheck,
|
||||
|
|
@ -21,19 +21,12 @@ 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 { TagsList } from '@lib/index';
|
||||
import type { ApiFormFieldSet } from '@lib/types/Forms';
|
||||
import type { PanelType } from '@lib/types/Panel';
|
||||
import AdminButton from '../../components/buttons/AdminButton';
|
||||
import PrimaryActionButton from '../../components/buttons/PrimaryActionButton';
|
||||
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 {
|
||||
BarcodeActionDropdown,
|
||||
CancelItemAction,
|
||||
|
|
@ -66,6 +59,7 @@ import BuildOutputTable from '../../tables/build/BuildOutputTable';
|
|||
import PartTestResultTable from '../../tables/part/PartTestResultTable';
|
||||
import { PurchaseOrderTable } from '../../tables/purchasing/PurchaseOrderTable';
|
||||
import { StockItemTable } from '../../tables/stock/StockItemTable';
|
||||
import { BuildOrderDetailsPanel } from './BuildOrderDetailsPanel';
|
||||
|
||||
function NoItems() {
|
||||
return (
|
||||
|
|
@ -235,239 +229,19 @@ export default function BuildDetail() {
|
|||
refetchOnMount: true
|
||||
});
|
||||
|
||||
const { instance: partRequirements, instanceQuery: partRequirementsQuery } =
|
||||
useInstance({
|
||||
endpoint: ApiEndpoints.part_requirements,
|
||||
pk: build?.part,
|
||||
hasPrimaryKey: true,
|
||||
defaultValue: {}
|
||||
});
|
||||
|
||||
const detailsPanel = useMemo(() => {
|
||||
if (instanceQuery.isFetching) {
|
||||
return <Skeleton />;
|
||||
}
|
||||
|
||||
const data = {
|
||||
...build,
|
||||
can_build: partRequirements?.can_build ?? 0
|
||||
};
|
||||
|
||||
const tl: DetailsField[] = [
|
||||
{
|
||||
type: 'link',
|
||||
name: 'part',
|
||||
label: t`Part`,
|
||||
model: ModelType.part
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'part_detail.IPN',
|
||||
icon: 'part',
|
||||
label: t`IPN`,
|
||||
hidden: !build.part_detail?.IPN,
|
||||
copy: true
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'part_detail.revision',
|
||||
icon: 'revision',
|
||||
label: t`Revision`,
|
||||
hidden: !build.part_detail?.revision,
|
||||
copy: true
|
||||
},
|
||||
{
|
||||
type: 'status',
|
||||
name: 'status',
|
||||
label: t`Status`,
|
||||
model: ModelType.build
|
||||
},
|
||||
{
|
||||
type: 'status',
|
||||
name: 'status_custom_key',
|
||||
label: t`Custom Status`,
|
||||
model: ModelType.build,
|
||||
icon: 'status',
|
||||
hidden:
|
||||
!build.status_custom_key || build.status_custom_key == build.status
|
||||
},
|
||||
{
|
||||
type: 'boolean',
|
||||
name: 'external',
|
||||
label: t`External`,
|
||||
icon: 'manufacturers',
|
||||
hidden: !build.external
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'reference',
|
||||
label: t`Reference`,
|
||||
copy: true
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'title',
|
||||
label: t`Description`,
|
||||
icon: 'description',
|
||||
copy: true
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'parent',
|
||||
icon: 'builds',
|
||||
label: t`Parent Build`,
|
||||
model_field: 'reference',
|
||||
model: ModelType.build,
|
||||
hidden: !build.parent
|
||||
}
|
||||
];
|
||||
|
||||
const tr: DetailsField[] = [
|
||||
{
|
||||
type: 'number',
|
||||
name: 'quantity',
|
||||
label: t`Build Quantity`
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
name: 'can_build',
|
||||
unit: build.part_detail?.units,
|
||||
label: t`Can Build`,
|
||||
hidden: partRequirements?.can_build === undefined
|
||||
},
|
||||
{
|
||||
type: 'progressbar',
|
||||
name: 'completed',
|
||||
icon: 'progress',
|
||||
total: build.quantity,
|
||||
progress: build.completed,
|
||||
label: t`Completed Outputs`
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'sales_order',
|
||||
label: t`Sales Order`,
|
||||
icon: 'sales_orders',
|
||||
model: ModelType.salesorder,
|
||||
model_field: 'reference',
|
||||
hidden: !build.sales_order
|
||||
}
|
||||
];
|
||||
|
||||
const bl: DetailsField[] = [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'issued_by',
|
||||
label: t`Issued By`,
|
||||
icon: 'user',
|
||||
badge: 'user',
|
||||
hidden: !build.issued_by
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'responsible',
|
||||
label: t`Responsible`,
|
||||
badge: 'owner',
|
||||
hidden: !build.responsible
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'project_code_label',
|
||||
label: t`Project Code`,
|
||||
icon: 'reference',
|
||||
copy: true,
|
||||
hidden: !build.project_code
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'take_from',
|
||||
icon: 'location',
|
||||
model: ModelType.stocklocation,
|
||||
label: t`Source Location`,
|
||||
backup_value: t`Any location`
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'destination',
|
||||
icon: 'location',
|
||||
model: ModelType.stocklocation,
|
||||
label: t`Destination Location`,
|
||||
hidden: !build.destination
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'batch',
|
||||
label: t`Batch Code`,
|
||||
hidden: !build.batch,
|
||||
copy: true
|
||||
}
|
||||
];
|
||||
|
||||
const br: DetailsField[] = [
|
||||
{
|
||||
type: 'date',
|
||||
name: 'creation_date',
|
||||
label: t`Created`,
|
||||
icon: 'calendar',
|
||||
copy: true,
|
||||
hidden: !build.creation_date
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'start_date',
|
||||
label: t`Start Date`,
|
||||
icon: 'calendar',
|
||||
copy: true,
|
||||
hidden: !build.start_date
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'target_date',
|
||||
label: t`Target Date`,
|
||||
icon: 'calendar',
|
||||
copy: true,
|
||||
hidden: !build.target_date
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'completion_date',
|
||||
label: t`Completed`,
|
||||
icon: 'calendar',
|
||||
copy: true,
|
||||
hidden: !build.completion_date
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<ItemDetailsGrid>
|
||||
<Stack gap='xs'>
|
||||
<Grid grow>
|
||||
<DetailsImage
|
||||
appRole={UserRoles.part}
|
||||
apiPath={ApiEndpoints.part_list}
|
||||
src={build.part_detail?.image ?? build.part_detail?.thumbnail}
|
||||
pk={build.part}
|
||||
/>
|
||||
<Grid.Col span={{ base: 12, sm: 8 }}>
|
||||
<DetailsTable fields={tl} item={data} />
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
<TagsList tags={build.tags} />
|
||||
</Stack>
|
||||
<DetailsTable fields={tr} item={data} />
|
||||
<DetailsTable fields={bl} item={data} />
|
||||
<DetailsTable fields={br} item={data} />
|
||||
</ItemDetailsGrid>
|
||||
);
|
||||
}, [build, instanceQuery, partRequirements, partRequirementsQuery]);
|
||||
|
||||
const buildPanels: PanelType[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
name: 'details',
|
||||
label: t`Build Details`,
|
||||
icon: <IconInfoCircle />,
|
||||
content: detailsPanel
|
||||
content: (
|
||||
<BuildOrderDetailsPanel
|
||||
instance={build}
|
||||
allowImageEdit
|
||||
refreshInstance={refreshInstance}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'line-items',
|
||||
|
|
@ -597,7 +371,7 @@ export default function BuildDetail() {
|
|||
build,
|
||||
id,
|
||||
user,
|
||||
partRequirements,
|
||||
|
||||
buildStatus,
|
||||
globalSettings,
|
||||
showChildBuilds,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,262 @@
|
|||
import { t } from '@lingui/core/macro';
|
||||
import { Grid, Skeleton, Stack } from '@mantine/core';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
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 { TagsList } from '@lib/index';
|
||||
|
||||
import {
|
||||
type DetailsField,
|
||||
DetailsTable
|
||||
} from '../../components/details/Details';
|
||||
import { DetailsImage } from '../../components/details/DetailsImage';
|
||||
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
||||
import { useParameterDetailsGrid } from '../../components/details/ParameterDetailsGrid';
|
||||
import { useInstance } from '../../hooks/UseInstance';
|
||||
|
||||
export function BuildOrderDetailsPanel({
|
||||
instance,
|
||||
allowImageEdit = false,
|
||||
refreshInstance
|
||||
}: Readonly<{
|
||||
instance: any;
|
||||
allowImageEdit?: boolean;
|
||||
refreshInstance?: () => void;
|
||||
}>) {
|
||||
const { instance: partRequirements } = useInstance({
|
||||
endpoint: ApiEndpoints.part_requirements,
|
||||
pk: instance?.part,
|
||||
hasPrimaryKey: true,
|
||||
defaultValue: {}
|
||||
});
|
||||
|
||||
const data = useMemo(
|
||||
() => ({ ...instance, can_build: partRequirements?.can_build ?? 0 }),
|
||||
[instance, partRequirements]
|
||||
);
|
||||
|
||||
const tl: DetailsField[] = [
|
||||
{
|
||||
type: 'link',
|
||||
name: 'part',
|
||||
label: t`Part`,
|
||||
model: ModelType.part
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'part_detail.IPN',
|
||||
icon: 'part',
|
||||
label: t`IPN`,
|
||||
hidden: !instance?.part_detail?.IPN,
|
||||
copy: true
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'part_detail.revision',
|
||||
icon: 'revision',
|
||||
label: t`Revision`,
|
||||
hidden: !instance?.part_detail?.revision,
|
||||
copy: true
|
||||
},
|
||||
{
|
||||
type: 'status',
|
||||
name: 'status',
|
||||
label: t`Status`,
|
||||
model: ModelType.build
|
||||
},
|
||||
{
|
||||
type: 'status',
|
||||
name: 'status_custom_key',
|
||||
label: t`Custom Status`,
|
||||
model: ModelType.build,
|
||||
icon: 'status',
|
||||
hidden:
|
||||
!instance?.status_custom_key ||
|
||||
instance?.status_custom_key == instance?.status
|
||||
},
|
||||
{
|
||||
type: 'boolean',
|
||||
name: 'external',
|
||||
label: t`External`,
|
||||
icon: 'manufacturers',
|
||||
hidden: !instance?.external
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'reference',
|
||||
label: t`Reference`,
|
||||
copy: true
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'title',
|
||||
label: t`Description`,
|
||||
icon: 'description',
|
||||
copy: true
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'parent',
|
||||
icon: 'builds',
|
||||
label: t`Parent Build`,
|
||||
model_field: 'reference',
|
||||
model: ModelType.build,
|
||||
hidden: !instance?.parent
|
||||
}
|
||||
];
|
||||
|
||||
const tr: DetailsField[] = [
|
||||
{
|
||||
type: 'number',
|
||||
name: 'quantity',
|
||||
label: t`Build Quantity`
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
name: 'can_build',
|
||||
unit: instance?.part_detail?.units,
|
||||
label: t`Can Build`,
|
||||
hidden: partRequirements?.can_build === undefined
|
||||
},
|
||||
{
|
||||
type: 'progressbar',
|
||||
name: 'completed',
|
||||
icon: 'progress',
|
||||
total: instance?.quantity,
|
||||
progress: instance?.completed,
|
||||
label: t`Completed Outputs`
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'sales_order',
|
||||
label: t`Sales Order`,
|
||||
icon: 'sales_orders',
|
||||
model: ModelType.salesorder,
|
||||
model_field: 'reference',
|
||||
hidden: !instance?.sales_order
|
||||
}
|
||||
];
|
||||
|
||||
const bl: DetailsField[] = [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'issued_by',
|
||||
label: t`Issued By`,
|
||||
icon: 'user',
|
||||
badge: 'user',
|
||||
hidden: !instance?.issued_by
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'responsible',
|
||||
label: t`Responsible`,
|
||||
badge: 'owner',
|
||||
hidden: !instance?.responsible
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'project_code_label',
|
||||
label: t`Project Code`,
|
||||
icon: 'reference',
|
||||
copy: true,
|
||||
hidden: !instance?.project_code
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'take_from',
|
||||
icon: 'location',
|
||||
model: ModelType.stocklocation,
|
||||
label: t`Source Location`,
|
||||
backup_value: t`Any location`
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'destination',
|
||||
icon: 'location',
|
||||
model: ModelType.stocklocation,
|
||||
label: t`Destination Location`,
|
||||
hidden: !instance?.destination
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'batch',
|
||||
label: t`Batch Code`,
|
||||
hidden: !instance?.batch,
|
||||
copy: true
|
||||
}
|
||||
];
|
||||
|
||||
const br: DetailsField[] = [
|
||||
{
|
||||
type: 'date',
|
||||
name: 'creation_date',
|
||||
label: t`Created`,
|
||||
icon: 'calendar',
|
||||
copy: true,
|
||||
hidden: !instance?.creation_date
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'start_date',
|
||||
label: t`Start Date`,
|
||||
icon: 'calendar',
|
||||
copy: true,
|
||||
hidden: !instance?.start_date
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'target_date',
|
||||
label: t`Target Date`,
|
||||
icon: 'calendar',
|
||||
copy: true,
|
||||
hidden: !instance?.target_date
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'completion_date',
|
||||
label: t`Completed`,
|
||||
icon: 'calendar',
|
||||
copy: true,
|
||||
hidden: !instance?.completion_date
|
||||
}
|
||||
];
|
||||
|
||||
const parametersTable = useParameterDetailsGrid({
|
||||
model_type: ModelType.build,
|
||||
model_id: instance?.pk
|
||||
});
|
||||
|
||||
if (!instance?.pk) return <Skeleton />;
|
||||
|
||||
return (
|
||||
<ItemDetailsGrid
|
||||
tables={[
|
||||
{ fields: tr, item: data },
|
||||
{ fields: bl, item: data },
|
||||
{ fields: br, item: data },
|
||||
parametersTable
|
||||
]}
|
||||
>
|
||||
<Stack gap='xs'>
|
||||
<Grid grow>
|
||||
<DetailsImage
|
||||
appRole={allowImageEdit ? UserRoles.part : undefined}
|
||||
apiPath={apiUrl(ApiEndpoints.part_list, instance?.part)}
|
||||
src={
|
||||
instance?.part_detail?.image ?? instance?.part_detail?.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>
|
||||
</ItemDetailsGrid>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { t } from '@lingui/core/macro';
|
||||
import { Grid, Skeleton, Stack } from '@mantine/core';
|
||||
import { Skeleton, Stack } from '@mantine/core';
|
||||
import {
|
||||
IconBuildingWarehouse,
|
||||
IconInfoCircle,
|
||||
|
|
@ -17,18 +17,10 @@ import { useNavigate, useParams } from 'react-router-dom';
|
|||
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 { TagsList } from '@lib/index';
|
||||
import type { PanelType } from '@lib/types/Panel';
|
||||
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 {
|
||||
DeleteItemAction,
|
||||
DuplicateItemAction,
|
||||
|
|
@ -58,6 +50,7 @@ import { SupplierPartTable } from '../../tables/purchasing/SupplierPartTable';
|
|||
import { ReturnOrderTable } from '../../tables/sales/ReturnOrderTable';
|
||||
import { SalesOrderTable } from '../../tables/sales/SalesOrderTable';
|
||||
import { StockItemTable } from '../../tables/stock/StockItemTable';
|
||||
import { CompanyDetailsPanel } from './CompanyDetailsPanel';
|
||||
|
||||
export type CompanyDetailProps = {
|
||||
title: string;
|
||||
|
|
@ -87,101 +80,15 @@ export default function CompanyDetail(props: Readonly<CompanyDetailProps>) {
|
|||
refetchOnMount: true
|
||||
});
|
||||
|
||||
const detailsPanel = useMemo(() => {
|
||||
if (instanceQuery.isFetching) {
|
||||
return <Skeleton />;
|
||||
}
|
||||
|
||||
const tl: DetailsField[] = [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'description',
|
||||
label: t`Description`,
|
||||
copy: true
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'website',
|
||||
label: t`Website`,
|
||||
external: true,
|
||||
copy: true,
|
||||
hidden: !company.website
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'phone',
|
||||
label: t`Phone Number`,
|
||||
copy: true,
|
||||
hidden: !company.phone
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'email',
|
||||
label: t`Email Address`,
|
||||
copy: true,
|
||||
hidden: !company.email
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'tax_id',
|
||||
label: t`Tax ID`,
|
||||
copy: true,
|
||||
hidden: !company.tax_id
|
||||
}
|
||||
];
|
||||
|
||||
const tr: DetailsField[] = [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'currency',
|
||||
label: t`Default Currency`
|
||||
},
|
||||
{
|
||||
type: 'boolean',
|
||||
name: 'is_supplier',
|
||||
label: t`Supplier`,
|
||||
icon: 'suppliers'
|
||||
},
|
||||
{
|
||||
type: 'boolean',
|
||||
name: 'is_manufacturer',
|
||||
label: t`Manufacturer`,
|
||||
icon: 'manufacturers'
|
||||
},
|
||||
{
|
||||
type: 'boolean',
|
||||
name: 'is_customer',
|
||||
label: t`Customer`,
|
||||
icon: 'customers'
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<ItemDetailsGrid>
|
||||
<Stack gap='xs'>
|
||||
<Grid grow>
|
||||
<DetailsImage
|
||||
appRole={UserRoles.purchase_order}
|
||||
apiPath={apiUrl(ApiEndpoints.company_list, company.pk)}
|
||||
src={company.image}
|
||||
pk={company.pk}
|
||||
refresh={refreshInstance}
|
||||
imageActions={{
|
||||
uploadFile: true,
|
||||
downloadImage: true,
|
||||
deleteFile: true
|
||||
}}
|
||||
/>
|
||||
<Grid.Col span={{ base: 12, sm: 8 }}>
|
||||
<DetailsTable item={company} fields={tl} />
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
<TagsList tags={company.tags} />
|
||||
</Stack>
|
||||
<DetailsTable item={company} fields={tr} />
|
||||
</ItemDetailsGrid>
|
||||
);
|
||||
}, [company, instanceQuery]);
|
||||
const detailsPanel = instanceQuery.isFetching ? (
|
||||
<Skeleton />
|
||||
) : (
|
||||
<CompanyDetailsPanel
|
||||
instance={company}
|
||||
allowImageEdit
|
||||
refreshInstance={refreshInstance}
|
||||
/>
|
||||
);
|
||||
|
||||
const companyPanels: PanelType[] = useMemo(() => {
|
||||
return [
|
||||
|
|
|
|||
|
|
@ -0,0 +1,119 @@
|
|||
import { t } from '@lingui/core/macro';
|
||||
import { Grid, Skeleton, Stack } from '@mantine/core';
|
||||
|
||||
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||
import { UserRoles } from '@lib/enums/Roles';
|
||||
import { apiUrl } from '@lib/functions/Api';
|
||||
import { TagsList } from '@lib/index';
|
||||
|
||||
import {
|
||||
type DetailsField,
|
||||
DetailsTable
|
||||
} from '../../components/details/Details';
|
||||
import { DetailsImage } from '../../components/details/DetailsImage';
|
||||
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
||||
|
||||
export function CompanyDetailsPanel({
|
||||
instance,
|
||||
allowImageEdit = false,
|
||||
refreshInstance
|
||||
}: Readonly<{
|
||||
instance: any;
|
||||
allowImageEdit?: boolean;
|
||||
refreshInstance?: () => void;
|
||||
}>) {
|
||||
const tl: DetailsField[] = [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'description',
|
||||
label: t`Description`,
|
||||
copy: true
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'website',
|
||||
label: t`Website`,
|
||||
external: true,
|
||||
copy: true,
|
||||
hidden: !instance?.website
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'phone',
|
||||
label: t`Phone Number`,
|
||||
copy: true,
|
||||
hidden: !instance?.phone
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'email',
|
||||
label: t`Email Address`,
|
||||
copy: true,
|
||||
hidden: !instance?.email
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'tax_id',
|
||||
label: t`Tax ID`,
|
||||
copy: true,
|
||||
hidden: !instance?.tax_id
|
||||
}
|
||||
];
|
||||
|
||||
const tr: DetailsField[] = [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'currency',
|
||||
label: t`Default Currency`
|
||||
},
|
||||
{
|
||||
type: 'boolean',
|
||||
name: 'is_supplier',
|
||||
label: t`Supplier`,
|
||||
icon: 'suppliers'
|
||||
},
|
||||
{
|
||||
type: 'boolean',
|
||||
name: 'is_manufacturer',
|
||||
label: t`Manufacturer`,
|
||||
icon: 'manufacturers'
|
||||
},
|
||||
{
|
||||
type: 'boolean',
|
||||
name: 'is_customer',
|
||||
label: t`Customer`,
|
||||
icon: 'customers'
|
||||
}
|
||||
];
|
||||
|
||||
if (!instance?.pk) return <Skeleton />;
|
||||
|
||||
return (
|
||||
<ItemDetailsGrid tables={[{ item: instance, fields: tr }]}>
|
||||
<Stack gap='xs'>
|
||||
<Grid grow>
|
||||
<DetailsImage
|
||||
appRole={allowImageEdit ? UserRoles.purchase_order : undefined}
|
||||
apiPath={apiUrl(ApiEndpoints.company_list, instance.pk)}
|
||||
src={instance.image}
|
||||
pk={instance.pk}
|
||||
refresh={refreshInstance}
|
||||
imageActions={
|
||||
allowImageEdit
|
||||
? {
|
||||
uploadFile: true,
|
||||
downloadImage: true,
|
||||
deleteFile: true
|
||||
}
|
||||
: {}
|
||||
}
|
||||
/>
|
||||
<Grid.Col span={{ base: 12, sm: 8 }}>
|
||||
<DetailsTable item={instance} fields={tl} />
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
<TagsList tags={instance.tags} />
|
||||
</Stack>
|
||||
</ItemDetailsGrid>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { t } from '@lingui/core/macro';
|
||||
import { Grid, Skeleton, Stack } from '@mantine/core';
|
||||
import { Skeleton, Stack } from '@mantine/core';
|
||||
import {
|
||||
IconBuildingWarehouse,
|
||||
IconInfoCircle,
|
||||
|
|
@ -8,20 +8,12 @@ import {
|
|||
import { useMemo } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
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 { PanelType } from '@lib/types/Panel';
|
||||
import AdminButton from '../../components/buttons/AdminButton';
|
||||
import {
|
||||
type DetailsField,
|
||||
DetailsTable
|
||||
} from '../../components/details/Details';
|
||||
import { DetailsImage } from '../../components/details/DetailsImage';
|
||||
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
||||
import {
|
||||
DeleteItemAction,
|
||||
DuplicateItemAction,
|
||||
|
|
@ -44,6 +36,7 @@ import { useInstance } from '../../hooks/UseInstance';
|
|||
import { useUserState } from '../../states/UserState';
|
||||
import { SupplierPartTable } from '../../tables/purchasing/SupplierPartTable';
|
||||
import { StockItemTable } from '../../tables/stock/StockItemTable';
|
||||
import { ManufacturerPartDetailsPanel } from './ManufacturerPartDetailsPanel';
|
||||
|
||||
export default function ManufacturerPartDetail() {
|
||||
const { id } = useParams();
|
||||
|
|
@ -65,105 +58,19 @@ export default function ManufacturerPartDetail() {
|
|||
}
|
||||
});
|
||||
|
||||
const detailsPanel = useMemo(() => {
|
||||
if (instanceQuery.isFetching) {
|
||||
return <Skeleton />;
|
||||
}
|
||||
|
||||
const data = manufacturerPart ?? {};
|
||||
|
||||
const tl: DetailsField[] = [
|
||||
{
|
||||
type: 'link',
|
||||
name: 'part',
|
||||
label: t`Internal Part`,
|
||||
model: ModelType.part,
|
||||
hidden: !manufacturerPart.part
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'part_detail.IPN',
|
||||
label: t`IPN`,
|
||||
copy: true,
|
||||
icon: 'serial',
|
||||
hidden: !data.part_detail?.IPN
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'part_detail.description',
|
||||
label: t`Description`,
|
||||
copy: true,
|
||||
icon: 'info',
|
||||
hidden: !manufacturerPart.description
|
||||
}
|
||||
];
|
||||
|
||||
const tr: DetailsField[] = [
|
||||
{
|
||||
type: 'link',
|
||||
name: 'manufacturer',
|
||||
label: t`Manufacturer`,
|
||||
icon: 'manufacturers',
|
||||
model: ModelType.company,
|
||||
hidden: !manufacturerPart.manufacturer
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'MPN',
|
||||
label: t`Manufacturer Part Number`,
|
||||
copy: true,
|
||||
hidden: !manufacturerPart.MPN,
|
||||
icon: 'reference'
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'description',
|
||||
label: t`Description`,
|
||||
copy: true,
|
||||
hidden: !manufacturerPart.description,
|
||||
icon: 'info'
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
external: true,
|
||||
name: 'link',
|
||||
label: t`External Link`,
|
||||
copy: true,
|
||||
hidden: !manufacturerPart.link
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<ItemDetailsGrid>
|
||||
<Stack gap='xs'>
|
||||
<Grid grow>
|
||||
<DetailsImage
|
||||
appRole={UserRoles.part}
|
||||
src={manufacturerPart?.part_detail?.image}
|
||||
apiPath={apiUrl(
|
||||
ApiEndpoints.part_list,
|
||||
manufacturerPart?.part_detail?.pk
|
||||
)}
|
||||
pk={manufacturerPart?.part_detail?.pk}
|
||||
/>
|
||||
<Grid.Col span={{ base: 12, sm: 8 }}>
|
||||
<DetailsTable title={t`Part Details`} fields={tl} item={data} />
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
<TagsList tags={manufacturerPart.tags} />
|
||||
</Stack>
|
||||
<DetailsTable title={t`Manufacturer Details`} fields={tr} item={data} />
|
||||
</ItemDetailsGrid>
|
||||
);
|
||||
}, [manufacturerPart, instanceQuery]);
|
||||
|
||||
const panels: PanelType[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
name: 'details',
|
||||
label: t`Manufacturer Part Details`,
|
||||
icon: <IconInfoCircle />,
|
||||
content: detailsPanel
|
||||
content: (
|
||||
<ManufacturerPartDetailsPanel
|
||||
instance={manufacturerPart}
|
||||
allowImageEdit
|
||||
refreshInstance={refreshInstance}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'stock',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,119 @@
|
|||
import { t } from '@lingui/core/macro';
|
||||
import { Grid, Skeleton, Stack } from '@mantine/core';
|
||||
|
||||
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 {
|
||||
type DetailsField,
|
||||
DetailsTable
|
||||
} from '../../components/details/Details';
|
||||
import { DetailsImage } from '../../components/details/DetailsImage';
|
||||
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
||||
import { useParameterDetailsGrid } from '../../components/details/ParameterDetailsGrid';
|
||||
|
||||
export function ManufacturerPartDetailsPanel({
|
||||
instance,
|
||||
allowImageEdit = false,
|
||||
refreshInstance
|
||||
}: Readonly<{
|
||||
instance: any;
|
||||
allowImageEdit?: boolean;
|
||||
refreshInstance?: () => void;
|
||||
}>) {
|
||||
const tl: DetailsField[] = [
|
||||
{
|
||||
type: 'link',
|
||||
name: 'part',
|
||||
label: t`Internal Part`,
|
||||
model: ModelType.part,
|
||||
hidden: !instance?.part
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'part_detail.IPN',
|
||||
label: t`IPN`,
|
||||
copy: true,
|
||||
icon: 'serial',
|
||||
hidden: !instance?.part_detail?.IPN
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'part_detail.description',
|
||||
label: t`Description`,
|
||||
copy: true,
|
||||
icon: 'info',
|
||||
hidden: !instance?.description
|
||||
}
|
||||
];
|
||||
|
||||
const tr: DetailsField[] = [
|
||||
{
|
||||
type: 'link',
|
||||
name: 'manufacturer',
|
||||
label: t`Manufacturer`,
|
||||
icon: 'manufacturers',
|
||||
model: ModelType.company,
|
||||
hidden: !instance?.manufacturer
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'MPN',
|
||||
label: t`Manufacturer Part Number`,
|
||||
copy: true,
|
||||
hidden: !instance?.MPN,
|
||||
icon: 'reference'
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'description',
|
||||
label: t`Description`,
|
||||
copy: true,
|
||||
hidden: !instance?.description,
|
||||
icon: 'info'
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
external: true,
|
||||
name: 'link',
|
||||
label: t`External Link`,
|
||||
copy: true,
|
||||
hidden: !instance?.link
|
||||
}
|
||||
];
|
||||
|
||||
const parametersTable = useParameterDetailsGrid({
|
||||
model_type: ModelType.manufacturerpart,
|
||||
model_id: instance?.pk
|
||||
});
|
||||
|
||||
if (!instance?.pk) return <Skeleton />;
|
||||
|
||||
return (
|
||||
<ItemDetailsGrid
|
||||
tables={[
|
||||
{ title: t`Manufacturer Details`, fields: tr, item: instance },
|
||||
parametersTable
|
||||
]}
|
||||
>
|
||||
<Stack gap='xs'>
|
||||
<Grid grow>
|
||||
<DetailsImage
|
||||
appRole={allowImageEdit ? UserRoles.part : undefined}
|
||||
src={instance?.part_detail?.image}
|
||||
apiPath={apiUrl(ApiEndpoints.part_list, instance?.part_detail?.pk)}
|
||||
pk={instance?.part_detail?.pk}
|
||||
refresh={refreshInstance}
|
||||
/>
|
||||
<Grid.Col span={{ base: 12, sm: 8 }}>
|
||||
<DetailsTable title={t`Part Details`} fields={tl} item={instance} />
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
<TagsList tags={instance?.tags} />
|
||||
</Stack>
|
||||
</ItemDetailsGrid>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { t } from '@lingui/core/macro';
|
||||
import { Grid, Skeleton, Stack } from '@mantine/core';
|
||||
import { Skeleton, Stack } from '@mantine/core';
|
||||
import {
|
||||
IconCurrencyDollar,
|
||||
IconInfoCircle,
|
||||
|
|
@ -9,22 +9,14 @@ import {
|
|||
import { type ReactNode, useMemo } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
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 { formatDecimal } from '@lib/functions/Formatting';
|
||||
import { getDetailUrl } from '@lib/functions/Navigation';
|
||||
import type { PanelType } from '@lib/types/Panel';
|
||||
import AdminButton from '../../components/buttons/AdminButton';
|
||||
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 {
|
||||
BarcodeActionDropdown,
|
||||
DeleteItemAction,
|
||||
|
|
@ -49,6 +41,7 @@ import { useUserState } from '../../states/UserState';
|
|||
import { PurchaseOrderTable } from '../../tables/purchasing/PurchaseOrderTable';
|
||||
import SupplierPriceBreakTable from '../../tables/purchasing/SupplierPriceBreakTable';
|
||||
import { StockItemTable } from '../../tables/stock/StockItemTable';
|
||||
import { SupplierPartDetailsPanel } from './SupplierPartDetailsPanel';
|
||||
|
||||
export default function SupplierPartDetail() {
|
||||
const { id } = useParams();
|
||||
|
|
@ -73,187 +66,19 @@ export default function SupplierPartDetail() {
|
|||
}
|
||||
});
|
||||
|
||||
const detailsPanel = useMemo(() => {
|
||||
if (instanceQuery.isFetching) {
|
||||
return <Skeleton />;
|
||||
}
|
||||
|
||||
const data = supplierPart ?? {};
|
||||
|
||||
// Access nested data
|
||||
data.manufacturer =
|
||||
supplierPart.manufacturer || data.manufacturer_detail?.pk;
|
||||
data.MPN = supplierPart.MPN || data.manufacturer_part_detail?.MPN;
|
||||
data.manufacturer_part =
|
||||
supplierPart.manufacturer_part || data.manufacturer_part_detail?.pk;
|
||||
|
||||
const tl: DetailsField[] = [
|
||||
{
|
||||
type: 'link',
|
||||
name: 'part',
|
||||
label: t`Internal Part`,
|
||||
model: ModelType.part,
|
||||
hidden: !supplierPart.part
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'part_detail.IPN',
|
||||
label: t`IPN`,
|
||||
copy: true,
|
||||
hidden: !data.part_detail?.IPN,
|
||||
icon: 'serial'
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'part_detail.description',
|
||||
label: t`Part Description`,
|
||||
copy: true,
|
||||
icon: 'info',
|
||||
hidden: !data.part_detail?.description
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
external: true,
|
||||
name: 'link',
|
||||
label: t`External Link`,
|
||||
copy: true,
|
||||
hidden: !supplierPart.link
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'note',
|
||||
label: t`Note`,
|
||||
copy: true,
|
||||
hidden: !supplierPart.note
|
||||
}
|
||||
];
|
||||
|
||||
const bl: DetailsField[] = [
|
||||
{
|
||||
type: 'link',
|
||||
name: 'supplier',
|
||||
label: t`Supplier`,
|
||||
model: ModelType.company,
|
||||
icon: 'suppliers',
|
||||
hidden: !supplierPart.supplier
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'SKU',
|
||||
label: t`SKU`,
|
||||
copy: true,
|
||||
icon: 'reference'
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'description',
|
||||
label: t`Description`,
|
||||
copy: true,
|
||||
hidden: !data.description
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'manufacturer',
|
||||
label: t`Manufacturer`,
|
||||
model: ModelType.company,
|
||||
icon: 'manufacturers',
|
||||
hidden: !data.manufacturer
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'manufacturer_part',
|
||||
model_field: 'MPN',
|
||||
label: t`Manufacturer Part`,
|
||||
model: ModelType.manufacturerpart,
|
||||
icon: 'reference',
|
||||
hidden: !data.manufacturer_part
|
||||
}
|
||||
];
|
||||
|
||||
const br: DetailsField[] = [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'packaging',
|
||||
label: t`Packaging`,
|
||||
copy: true,
|
||||
hidden: !data.packaging
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'pack_quantity',
|
||||
label: t`Pack Quantity`,
|
||||
copy: true,
|
||||
hidden: !data.pack_quantity,
|
||||
icon: 'packages'
|
||||
}
|
||||
];
|
||||
|
||||
const tr: DetailsField[] = [
|
||||
{
|
||||
type: 'number',
|
||||
name: 'in_stock',
|
||||
label: t`In Stock`,
|
||||
copy: true,
|
||||
icon: 'stock'
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
name: 'on_order',
|
||||
label: t`On Order`,
|
||||
copy: true,
|
||||
icon: 'purchase_orders'
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
name: 'available',
|
||||
label: t`Supplier Availability`,
|
||||
hidden: !data.availability_updated,
|
||||
copy: true,
|
||||
icon: 'packages'
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'availability_updated',
|
||||
label: t`Availability Updated`,
|
||||
copy: true,
|
||||
hidden: !data.availability_updated,
|
||||
icon: 'calendar'
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<ItemDetailsGrid>
|
||||
<Stack gap='xs'>
|
||||
<Grid grow>
|
||||
<DetailsImage
|
||||
appRole={UserRoles.part}
|
||||
src={supplierPart?.part_detail?.image}
|
||||
apiPath={apiUrl(
|
||||
ApiEndpoints.part_list,
|
||||
supplierPart?.part_detail?.pk
|
||||
)}
|
||||
pk={supplierPart?.part_detail?.pk}
|
||||
/>
|
||||
<Grid.Col span={8}>
|
||||
<DetailsTable title={t`Part Details`} fields={tl} item={data} />
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
<TagsList tags={supplierPart.tags} />
|
||||
</Stack>
|
||||
<DetailsTable title={t`Supplier`} fields={bl} item={data} />
|
||||
<DetailsTable title={t`Packaging`} fields={br} item={data} />
|
||||
<DetailsTable title={t`Availability`} fields={tr} item={data} />
|
||||
</ItemDetailsGrid>
|
||||
);
|
||||
}, [supplierPart, instanceQuery.isFetching]);
|
||||
|
||||
const panels: PanelType[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
name: 'details',
|
||||
label: t`Supplier Part Details`,
|
||||
icon: <IconInfoCircle />,
|
||||
content: detailsPanel
|
||||
content: (
|
||||
<SupplierPartDetailsPanel
|
||||
instance={supplierPart}
|
||||
allowImageEdit
|
||||
refreshInstance={refreshInstance}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'stock',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,206 @@
|
|||
import { t } from '@lingui/core/macro';
|
||||
import { Grid, Skeleton, Stack } from '@mantine/core';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
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 {
|
||||
type DetailsField,
|
||||
DetailsTable
|
||||
} from '../../components/details/Details';
|
||||
import { DetailsImage } from '../../components/details/DetailsImage';
|
||||
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
||||
import { useParameterDetailsGrid } from '../../components/details/ParameterDetailsGrid';
|
||||
|
||||
export function SupplierPartDetailsPanel({
|
||||
instance,
|
||||
allowImageEdit = false,
|
||||
refreshInstance
|
||||
}: Readonly<{
|
||||
instance: any;
|
||||
allowImageEdit?: boolean;
|
||||
refreshInstance?: () => void;
|
||||
}>) {
|
||||
const data = useMemo(() => {
|
||||
if (!instance) return {};
|
||||
return {
|
||||
...instance,
|
||||
manufacturer: instance.manufacturer || instance.manufacturer_detail?.pk,
|
||||
MPN: instance.MPN || instance.manufacturer_part_detail?.MPN,
|
||||
manufacturer_part:
|
||||
instance.manufacturer_part || instance.manufacturer_part_detail?.pk
|
||||
};
|
||||
}, [instance]);
|
||||
|
||||
const tl: DetailsField[] = [
|
||||
{
|
||||
type: 'link',
|
||||
name: 'part',
|
||||
label: t`Internal Part`,
|
||||
model: ModelType.part,
|
||||
hidden: !instance?.part
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'part_detail.IPN',
|
||||
label: t`IPN`,
|
||||
copy: true,
|
||||
hidden: !data.part_detail?.IPN,
|
||||
icon: 'serial'
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'part_detail.description',
|
||||
label: t`Part Description`,
|
||||
copy: true,
|
||||
icon: 'info',
|
||||
hidden: !data.part_detail?.description
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
external: true,
|
||||
name: 'link',
|
||||
label: t`External Link`,
|
||||
copy: true,
|
||||
hidden: !instance?.link
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'note',
|
||||
label: t`Note`,
|
||||
copy: true,
|
||||
hidden: !instance?.note
|
||||
}
|
||||
];
|
||||
|
||||
const bl: DetailsField[] = [
|
||||
{
|
||||
type: 'link',
|
||||
name: 'supplier',
|
||||
label: t`Supplier`,
|
||||
model: ModelType.company,
|
||||
icon: 'suppliers',
|
||||
hidden: !instance?.supplier
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'SKU',
|
||||
label: t`SKU`,
|
||||
copy: true,
|
||||
icon: 'reference'
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'description',
|
||||
label: t`Description`,
|
||||
copy: true,
|
||||
hidden: !data.description
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'manufacturer',
|
||||
label: t`Manufacturer`,
|
||||
model: ModelType.company,
|
||||
icon: 'manufacturers',
|
||||
hidden: !data.manufacturer
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'manufacturer_part',
|
||||
model_field: 'MPN',
|
||||
label: t`Manufacturer Part`,
|
||||
model: ModelType.manufacturerpart,
|
||||
icon: 'reference',
|
||||
hidden: !data.manufacturer_part
|
||||
}
|
||||
];
|
||||
|
||||
const br: DetailsField[] = [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'packaging',
|
||||
label: t`Packaging`,
|
||||
copy: true,
|
||||
hidden: !data.packaging
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'pack_quantity',
|
||||
label: t`Pack Quantity`,
|
||||
copy: true,
|
||||
hidden: !data.pack_quantity,
|
||||
icon: 'packages'
|
||||
}
|
||||
];
|
||||
|
||||
const tr: DetailsField[] = [
|
||||
{
|
||||
type: 'number',
|
||||
name: 'in_stock',
|
||||
label: t`In Stock`,
|
||||
copy: true,
|
||||
icon: 'stock'
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
name: 'on_order',
|
||||
label: t`On Order`,
|
||||
copy: true,
|
||||
icon: 'purchase_orders'
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
name: 'available',
|
||||
label: t`Supplier Availability`,
|
||||
hidden: !data.availability_updated,
|
||||
copy: true,
|
||||
icon: 'packages'
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'availability_updated',
|
||||
label: t`Availability Updated`,
|
||||
copy: true,
|
||||
hidden: !data.availability_updated,
|
||||
icon: 'calendar'
|
||||
}
|
||||
];
|
||||
|
||||
const parametersTable = useParameterDetailsGrid({
|
||||
model_type: ModelType.supplierpart,
|
||||
model_id: instance?.pk
|
||||
});
|
||||
|
||||
if (!instance?.pk) return <Skeleton />;
|
||||
|
||||
return (
|
||||
<ItemDetailsGrid
|
||||
tables={[
|
||||
{ title: t`Supplier`, fields: bl, item: data },
|
||||
{ title: t`Packaging`, fields: br, item: data },
|
||||
{ title: t`Availability`, fields: tr, item: data },
|
||||
parametersTable
|
||||
]}
|
||||
>
|
||||
<Stack gap='xs'>
|
||||
<Grid grow>
|
||||
<DetailsImage
|
||||
appRole={allowImageEdit ? UserRoles.part : undefined}
|
||||
src={instance?.part_detail?.image}
|
||||
apiPath={apiUrl(ApiEndpoints.part_list, instance?.part_detail?.pk)}
|
||||
pk={instance?.part_detail?.pk}
|
||||
refresh={refreshInstance}
|
||||
/>
|
||||
<Grid.Col span={8}>
|
||||
<DetailsTable title={t`Part Details`} fields={tl} item={data} />
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
<TagsList tags={instance?.tags} />
|
||||
</Stack>
|
||||
</ItemDetailsGrid>
|
||||
);
|
||||
}
|
||||
|
|
@ -7,10 +7,7 @@ import { Paper, Skeleton, Stack } from '@mantine/core';
|
|||
import { IconInfoCircle } from '@tabler/icons-react';
|
||||
import { useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import {
|
||||
type DetailsField,
|
||||
DetailsTable
|
||||
} from '../../components/details/Details';
|
||||
import type { DetailsField } from '../../components/details/Details';
|
||||
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
||||
import {} from '../../components/items/ActionDropdown';
|
||||
import { RoleTable, type RuleSet } from '../../components/items/RoleTable';
|
||||
|
|
@ -48,8 +45,9 @@ export default function GroupDetail() {
|
|||
];
|
||||
|
||||
return (
|
||||
<ItemDetailsGrid>
|
||||
<DetailsTable fields={tl} item={instance} title={t`Group Details`} />
|
||||
<ItemDetailsGrid
|
||||
tables={[{ fields: tl, item: instance, title: t`Group Details` }]}
|
||||
>
|
||||
<Paper p='xs' withBorder>
|
||||
<Stack gap='xs'>
|
||||
<StylishText size='lg'>{t`Group Roles`}</StylishText>
|
||||
|
|
|
|||
|
|
@ -6,10 +6,7 @@ import { Badge, Group, Skeleton, Stack } from '@mantine/core';
|
|||
import { IconInfoCircle } from '@tabler/icons-react';
|
||||
import { type ReactNode, useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import {
|
||||
type DetailsField,
|
||||
DetailsTable
|
||||
} from '../../components/details/Details';
|
||||
import type { DetailsField } from '../../components/details/Details';
|
||||
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
||||
import {} from '../../components/items/ActionDropdown';
|
||||
import InstanceDetail from '../../components/nav/InstanceDetail';
|
||||
|
|
@ -171,13 +168,15 @@ export default function UserDetail() {
|
|||
instance.location;
|
||||
|
||||
return (
|
||||
<ItemDetailsGrid>
|
||||
<DetailsTable fields={tl} item={instance} title={t`User Information`} />
|
||||
<DetailsTable fields={tr} item={instance} title={t`User Permissions`} />
|
||||
{hasProfile && settings.isSet('DISPLAY_PROFILE_INFO') && (
|
||||
<DetailsTable fields={br} item={instance} title={t`User Profile`} />
|
||||
)}
|
||||
</ItemDetailsGrid>
|
||||
<ItemDetailsGrid
|
||||
tables={[
|
||||
{ fields: tl, item: instance, title: t`User Information` },
|
||||
{ fields: tr, item: instance, title: t`User Permissions` },
|
||||
...(hasProfile && settings.isSet('DISPLAY_PROFILE_INFO')
|
||||
? [{ fields: br, item: instance, title: t`User Profile` }]
|
||||
: [])
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}, [instance, userGroups, instanceQuery]);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { t } from '@lingui/core/macro';
|
||||
import { Group, LoadingOverlay, Skeleton, Stack } from '@mantine/core';
|
||||
import { LoadingOverlay, Stack } from '@mantine/core';
|
||||
import {
|
||||
IconCategory,
|
||||
IconInfoCircle,
|
||||
|
|
@ -21,11 +21,6 @@ import type { PanelType } from '@lib/types/Panel';
|
|||
import { useLocalStorage } from '@mantine/hooks';
|
||||
import AdminButton from '../../components/buttons/AdminButton';
|
||||
import StarredToggleButton from '../../components/buttons/StarredToggleButton';
|
||||
import {
|
||||
type DetailsField,
|
||||
DetailsTable
|
||||
} from '../../components/details/Details';
|
||||
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
||||
import {
|
||||
DeleteItemAction,
|
||||
EditItemAction,
|
||||
|
|
@ -52,6 +47,7 @@ import { PartCategoryTable } from '../../tables/part/PartCategoryTable';
|
|||
import PartCategoryTemplateTable from '../../tables/part/PartCategoryTemplateTable';
|
||||
import { PartListTable } from '../../tables/part/PartTable';
|
||||
import { StockItemTable } from '../../tables/stock/StockItemTable';
|
||||
import { PartCategoryDetailsPanel } from './PartCategoryDetailsPanel';
|
||||
|
||||
/**
|
||||
* Detail view for a single PartCategory instance.
|
||||
|
|
@ -106,100 +102,6 @@ export default function CategoryDetail() {
|
|||
assign: false
|
||||
});
|
||||
|
||||
const detailsPanel = useMemo(() => {
|
||||
if (id && instanceQuery.isFetching) {
|
||||
return <Skeleton />;
|
||||
}
|
||||
|
||||
const left: DetailsField[] = [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
label: t`Name`,
|
||||
copy: true,
|
||||
value_formatter: () => (
|
||||
<Group gap='xs'>
|
||||
{category.icon && <ApiIcon name={category.icon} />}
|
||||
{category.name}
|
||||
</Group>
|
||||
)
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'pathstring',
|
||||
label: t`Path`,
|
||||
icon: 'sitemap',
|
||||
copy: true,
|
||||
hidden: !id
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'description',
|
||||
label: t`Description`,
|
||||
copy: true
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'parent',
|
||||
model_field: 'name',
|
||||
icon: 'location',
|
||||
label: t`Parent Category`,
|
||||
model: ModelType.partcategory,
|
||||
hidden: !category?.parent
|
||||
},
|
||||
{
|
||||
type: 'boolean',
|
||||
name: 'starred',
|
||||
icon: 'notification',
|
||||
label: t`Subscribed`
|
||||
}
|
||||
];
|
||||
|
||||
const right: DetailsField[] = [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'part_count',
|
||||
label: t`Parts`,
|
||||
icon: 'part',
|
||||
value_formatter: () => category?.part_count || '0'
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'subcategories',
|
||||
label: t`Subcategories`,
|
||||
icon: 'sitemap',
|
||||
hidden: !category?.subcategories
|
||||
},
|
||||
{
|
||||
type: 'boolean',
|
||||
name: 'structural',
|
||||
label: t`Structural`,
|
||||
icon: 'sitemap'
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'parent_default_location',
|
||||
label: t`Parent default location`,
|
||||
model: ModelType.stocklocation,
|
||||
hidden: !category.parent_default_location || category.default_location
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'default_location',
|
||||
label: t`Default location`,
|
||||
model: ModelType.stocklocation,
|
||||
hidden: !category.default_location
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<ItemDetailsGrid>
|
||||
{id && category?.pk && <DetailsTable item={category} fields={left} />}
|
||||
{id && category?.pk && <DetailsTable item={category} fields={right} />}
|
||||
</ItemDetailsGrid>
|
||||
);
|
||||
}, [category, instanceQuery]);
|
||||
|
||||
const editCategory = useEditApiFormModal({
|
||||
url: ApiEndpoints.category_list,
|
||||
pk: id,
|
||||
|
|
@ -296,7 +198,7 @@ export default function CategoryDetail() {
|
|||
name: 'details',
|
||||
label: t`Category Details`,
|
||||
icon: <IconInfoCircle />,
|
||||
content: detailsPanel,
|
||||
content: <PartCategoryDetailsPanel instance={category} />,
|
||||
hidden: !id || !category?.pk
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,121 @@
|
|||
import { t } from '@lingui/core/macro';
|
||||
import { Group, Skeleton } from '@mantine/core';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { ModelType } from '@lib/enums/ModelType';
|
||||
|
||||
import type { DetailsField } from '../../components/details/Details';
|
||||
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
||||
import { useParameterDetailsGrid } from '../../components/details/ParameterDetailsGrid';
|
||||
import { ApiIcon } from '../../components/items/ApiIcon';
|
||||
|
||||
export function PartCategoryDetailsPanel({
|
||||
instance
|
||||
}: Readonly<{
|
||||
instance: any;
|
||||
}>) {
|
||||
const left: DetailsField[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
label: t`Name`,
|
||||
copy: true,
|
||||
value_formatter: () => (
|
||||
<Group gap='xs'>
|
||||
{instance?.icon && <ApiIcon name={instance.icon} />}
|
||||
{instance?.name}
|
||||
</Group>
|
||||
)
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'pathstring',
|
||||
label: t`Path`,
|
||||
icon: 'sitemap',
|
||||
copy: true,
|
||||
hidden: !instance?.pathstring
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'description',
|
||||
label: t`Description`,
|
||||
copy: true
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'parent',
|
||||
model_field: 'name',
|
||||
icon: 'location',
|
||||
label: t`Parent Category`,
|
||||
model: ModelType.partcategory,
|
||||
hidden: !instance?.parent
|
||||
},
|
||||
{
|
||||
type: 'boolean',
|
||||
name: 'starred',
|
||||
icon: 'notification',
|
||||
label: t`Subscribed`
|
||||
}
|
||||
],
|
||||
[instance]
|
||||
);
|
||||
|
||||
const right: DetailsField[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'part_count',
|
||||
label: t`Parts`,
|
||||
icon: 'part',
|
||||
value_formatter: () => instance?.part_count || '0'
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'subcategories',
|
||||
label: t`Subcategories`,
|
||||
icon: 'sitemap',
|
||||
hidden: !instance?.subcategories
|
||||
},
|
||||
{
|
||||
type: 'boolean',
|
||||
name: 'structural',
|
||||
label: t`Structural`,
|
||||
icon: 'sitemap'
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'parent_default_location',
|
||||
label: t`Parent default location`,
|
||||
model: ModelType.stocklocation,
|
||||
hidden:
|
||||
!instance?.parent_default_location || !!instance?.default_location
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'default_location',
|
||||
label: t`Default location`,
|
||||
model: ModelType.stocklocation,
|
||||
hidden: !instance?.default_location
|
||||
}
|
||||
],
|
||||
[instance]
|
||||
);
|
||||
|
||||
const parametersTable = useParameterDetailsGrid({
|
||||
model_type: ModelType.partcategory,
|
||||
model_id: instance?.pk
|
||||
});
|
||||
|
||||
if (!instance?.pk) return <Skeleton />;
|
||||
|
||||
return (
|
||||
<ItemDetailsGrid
|
||||
tables={[
|
||||
{ item: instance, fields: left },
|
||||
{ item: instance, fields: right },
|
||||
parametersTable
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -3,7 +3,6 @@ import {
|
|||
ActionIcon,
|
||||
Alert,
|
||||
Center,
|
||||
Grid,
|
||||
Group,
|
||||
Loader,
|
||||
Paper,
|
||||
|
|
@ -41,7 +40,6 @@ import { type ReactNode, useMemo, useState } from 'react';
|
|||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import Select from 'react-select';
|
||||
|
||||
import TagsList from '@lib/components/TagsList';
|
||||
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||
import { ModelType } from '@lib/enums/ModelType';
|
||||
import { UserRoles } from '@lib/enums/Roles';
|
||||
|
|
@ -52,13 +50,7 @@ import type { PanelType } from '@lib/types/Panel';
|
|||
import AdminButton from '../../components/buttons/AdminButton';
|
||||
import { PrintingActions } from '../../components/buttons/PrintingActions';
|
||||
import StarredToggleButton from '../../components/buttons/StarredToggleButton';
|
||||
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 { Thumbnail } from '../../components/images/Thumbnail';
|
||||
import {
|
||||
ActionDropdown,
|
||||
|
|
@ -77,7 +69,7 @@ import { PanelGroup } from '../../components/panels/PanelGroup';
|
|||
import { RenderPart } from '../../components/render/Part';
|
||||
import OrderPartsWizard from '../../components/wizards/OrderPartsWizard';
|
||||
import { useApi } from '../../contexts/ApiContext';
|
||||
import { formatDecimal, formatPriceRange } from '../../defaults/formatters';
|
||||
import { formatDecimal } from '../../defaults/formatters';
|
||||
import { usePartFields } from '../../forms/PartForms';
|
||||
import { useFindSerialNumberForm } from '../../forms/StockForms';
|
||||
import {
|
||||
|
|
@ -106,6 +98,7 @@ import { SalesOrderTable } from '../../tables/sales/SalesOrderTable';
|
|||
import { StockItemTable } from '../../tables/stock/StockItemTable';
|
||||
import { TransferOrderTable } from '../../tables/stock/TransferOrderTable';
|
||||
import PartAllocationPanel from './PartAllocationPanel';
|
||||
import { PartDetailsPanel } from './PartDetailsPanel';
|
||||
import PartPricingPanel from './PartPricingPanel';
|
||||
import PartStockHistoryDetail from './PartStockHistoryDetail';
|
||||
import PartSupplierDetail from './PartSupplierDetail';
|
||||
|
|
@ -175,14 +168,6 @@ export default function PartDetail() {
|
|||
refetchOnMount: true
|
||||
});
|
||||
|
||||
const { instance: serials } = useInstance({
|
||||
endpoint: ApiEndpoints.part_serial_numbers,
|
||||
pk: id,
|
||||
hasPrimaryKey: true,
|
||||
refetchOnMount: false,
|
||||
defaultValue: {}
|
||||
});
|
||||
|
||||
const {
|
||||
instance: part,
|
||||
refreshInstance,
|
||||
|
|
@ -275,398 +260,22 @@ export default function PartDetail() {
|
|||
return partRevisionOptions.length > 0 && revisionsEnabled;
|
||||
}, [partRevisionOptions, revisionsEnabled]);
|
||||
|
||||
const detailsPanel = useMemo(() => {
|
||||
if (instanceQuery.isFetching) {
|
||||
return <Skeleton />;
|
||||
}
|
||||
|
||||
const data = { ...part };
|
||||
|
||||
const fetching =
|
||||
partRequirementsQuery.isFetching || instanceQuery.isFetching;
|
||||
|
||||
// Copy part requirements data into the main part data
|
||||
data.total_in_stock =
|
||||
partRequirements?.total_stock ?? part?.total_in_stock ?? 0;
|
||||
data.unallocated =
|
||||
partRequirements?.unallocated_stock ?? part?.unallocated_stock ?? 0;
|
||||
data.ordering = partRequirements?.ordering ?? part?.ordering ?? 0;
|
||||
|
||||
data.required =
|
||||
(partRequirements?.required_for_build_orders ??
|
||||
part?.required_for_build_orders ??
|
||||
0) +
|
||||
(partRequirements?.required_for_sales_orders ??
|
||||
part?.required_for_sales_orders ??
|
||||
0);
|
||||
|
||||
data.allocated =
|
||||
(partRequirements?.allocated_to_build_orders ??
|
||||
part?.allocated_to_build_orders ??
|
||||
0) +
|
||||
(partRequirements?.allocated_to_sales_orders ??
|
||||
part?.allocated_to_sales_orders ??
|
||||
0);
|
||||
|
||||
// Extract requirements data
|
||||
data.can_build = partRequirements?.can_build ?? 0;
|
||||
|
||||
// Provide latest serial number info
|
||||
if (!!serials.latest) {
|
||||
data.latest_serial_number = serials.latest;
|
||||
}
|
||||
|
||||
// Top left - core part information
|
||||
const tl: DetailsField[] = [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'name',
|
||||
label: t`Name`,
|
||||
icon: 'part',
|
||||
copy: true
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'IPN',
|
||||
label: t`IPN`,
|
||||
copy: true,
|
||||
hidden: !part.IPN
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'description',
|
||||
label: t`Description`,
|
||||
copy: true
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'variant_of',
|
||||
label: t`Variant of`,
|
||||
model: ModelType.part,
|
||||
model_field: 'full_name',
|
||||
hidden: !part.variant_of
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'revision_of',
|
||||
label: t`Revision of`,
|
||||
model: ModelType.part,
|
||||
model_field: 'full_name',
|
||||
hidden: !part.revision_of
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'revision',
|
||||
label: t`Revision`,
|
||||
hidden: !part.revision,
|
||||
copy: true
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'category',
|
||||
label: t`Category`,
|
||||
model: ModelType.partcategory
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'default_location',
|
||||
label: t`Default Location`,
|
||||
model: ModelType.stocklocation,
|
||||
hidden: !part.default_location
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'category_default_location',
|
||||
label: t`Category Default Location`,
|
||||
model: ModelType.stocklocation,
|
||||
hidden: part.default_location || !part.category_default_location
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'units',
|
||||
label: t`Units`,
|
||||
copy: true,
|
||||
hidden: !part.units
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'keywords',
|
||||
label: t`Keywords`,
|
||||
copy: true,
|
||||
hidden: !part.keywords
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'link',
|
||||
label: t`Link`,
|
||||
external: true,
|
||||
copy: true,
|
||||
hidden: !part.link
|
||||
}
|
||||
];
|
||||
|
||||
// Top right - stock availability information
|
||||
const tr: DetailsField[] = [
|
||||
{
|
||||
type: 'number',
|
||||
name: 'total_in_stock',
|
||||
unit: part.units,
|
||||
label: t`In Stock`,
|
||||
hidden: part.virtual
|
||||
},
|
||||
{
|
||||
type: 'progressbar',
|
||||
name: 'unallocated_stock',
|
||||
total: data.total_in_stock,
|
||||
progress: data.unallocated,
|
||||
label: t`Available Stock`,
|
||||
hidden: part.virtual || data.total_in_stock == data.unallocated
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
name: 'ordering',
|
||||
label: t`On order`,
|
||||
unit: part.units,
|
||||
hidden: !part.purchaseable || part.ordering <= 0
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
name: 'required',
|
||||
label: t`Required for Orders`,
|
||||
unit: part.units,
|
||||
hidden: data.required <= 0,
|
||||
icon: 'stocktake'
|
||||
},
|
||||
{
|
||||
type: 'progressbar',
|
||||
name: 'allocated_to_build_orders',
|
||||
icon: 'manufacturers',
|
||||
total: partRequirements.required_for_build_orders,
|
||||
progress: partRequirements.allocated_to_build_orders,
|
||||
label: t`Allocated to Build Orders`,
|
||||
hidden:
|
||||
fetching ||
|
||||
(partRequirements.required_for_build_orders <= 0 &&
|
||||
partRequirements.allocated_to_build_orders <= 0)
|
||||
},
|
||||
{
|
||||
type: 'progressbar',
|
||||
icon: 'sales_orders',
|
||||
name: 'allocated_to_sales_orders',
|
||||
total: partRequirements.required_for_sales_orders,
|
||||
progress: partRequirements.allocated_to_sales_orders,
|
||||
label: t`Allocated to Sales Orders`,
|
||||
hidden:
|
||||
fetching ||
|
||||
(partRequirements.required_for_sales_orders <= 0 &&
|
||||
partRequirements.allocated_to_sales_orders <= 0)
|
||||
},
|
||||
{
|
||||
type: 'progressbar',
|
||||
name: 'building',
|
||||
label: t`In Production`,
|
||||
progress: partRequirements.building,
|
||||
total: partRequirements.scheduled_to_build,
|
||||
hidden:
|
||||
fetching ||
|
||||
(!partRequirements.building && !partRequirements.scheduled_to_build)
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
name: 'can_build',
|
||||
unit: part.units,
|
||||
label: t`Can Build`,
|
||||
hidden: !part.assembly || fetching
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
name: 'minimum_stock',
|
||||
unit: part.units,
|
||||
label: t`Minimum Stock`,
|
||||
hidden: part.minimum_stock <= 0
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
name: 'maximum_stock',
|
||||
unit: part.units,
|
||||
label: t`Maximum Stock`,
|
||||
hidden: part.maximum_stock <= 0
|
||||
}
|
||||
];
|
||||
|
||||
// Bottom left - part attributes
|
||||
const bl: DetailsField[] = [
|
||||
{
|
||||
type: 'boolean',
|
||||
name: 'active',
|
||||
label: t`Active`
|
||||
},
|
||||
{
|
||||
type: 'boolean',
|
||||
name: 'locked',
|
||||
label: t`Locked`
|
||||
},
|
||||
{
|
||||
type: 'boolean',
|
||||
icon: 'template',
|
||||
name: 'is_template',
|
||||
label: t`Template Part`
|
||||
},
|
||||
{
|
||||
type: 'boolean',
|
||||
name: 'assembly',
|
||||
label: t`Assembled Part`
|
||||
},
|
||||
{
|
||||
type: 'boolean',
|
||||
name: 'component',
|
||||
label: t`Component Part`
|
||||
},
|
||||
{
|
||||
type: 'boolean',
|
||||
name: 'testable',
|
||||
label: t`Testable Part`,
|
||||
icon: 'test'
|
||||
},
|
||||
{
|
||||
type: 'boolean',
|
||||
name: 'trackable',
|
||||
label: t`Trackable Part`
|
||||
},
|
||||
{
|
||||
type: 'boolean',
|
||||
name: 'purchaseable',
|
||||
label: t`Purchaseable Part`
|
||||
},
|
||||
{
|
||||
type: 'boolean',
|
||||
name: 'salable',
|
||||
icon: 'saleable',
|
||||
label: t`Saleable Part`
|
||||
},
|
||||
{
|
||||
type: 'boolean',
|
||||
name: 'virtual',
|
||||
label: t`Virtual Part`
|
||||
},
|
||||
{
|
||||
type: 'boolean',
|
||||
name: 'starred',
|
||||
label: t`Subscribed`,
|
||||
icon: 'bell'
|
||||
}
|
||||
];
|
||||
|
||||
// Bottom right - other part information
|
||||
const br: DetailsField[] = [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'creation_date',
|
||||
label: t`Creation Date`
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'creation_user',
|
||||
label: t`Created By`,
|
||||
badge: 'user',
|
||||
icon: 'user',
|
||||
hidden: !part.creation_user
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'responsible',
|
||||
label: t`Responsible`,
|
||||
badge: 'owner',
|
||||
hidden: !part.responsible
|
||||
},
|
||||
{
|
||||
name: 'default_expiry',
|
||||
label: t`Default Expiry`,
|
||||
hidden: !part.default_expiry,
|
||||
icon: 'calendar',
|
||||
type: 'string',
|
||||
value_formatter: () => {
|
||||
return `${part.default_expiry} ${t`days`}`;
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// Add in price range data
|
||||
if (part.pricing_min || part.pricing_max) {
|
||||
br.push({
|
||||
type: 'string',
|
||||
name: 'pricing',
|
||||
label: t`Price Range`,
|
||||
value_formatter: () => {
|
||||
return formatPriceRange(part.pricing_min, part.pricing_max);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
br.push({
|
||||
type: 'string',
|
||||
name: 'latest_serial_number',
|
||||
label: t`Latest Serial Number`,
|
||||
hidden: !part.trackable || !data.latest_serial_number,
|
||||
icon: 'serial'
|
||||
});
|
||||
|
||||
return part ? (
|
||||
<ItemDetailsGrid>
|
||||
const revisionSelector = useMemo(() => {
|
||||
if (!enableRevisionSelection) return null;
|
||||
return (
|
||||
<Paper p='sm' withBorder>
|
||||
<Stack gap='xs'>
|
||||
<Grid grow>
|
||||
<DetailsImage
|
||||
appRole={UserRoles.part}
|
||||
imageActions={{
|
||||
selectExisting: true,
|
||||
downloadImage: true,
|
||||
uploadFile: true,
|
||||
deleteFile: true
|
||||
}}
|
||||
src={part.image}
|
||||
thumbnail={part.thumbnail}
|
||||
apiPath={apiUrl(ApiEndpoints.part_list, part.pk)}
|
||||
refresh={refreshInstance}
|
||||
pk={part.pk}
|
||||
/>
|
||||
<Grid.Col span={{ base: 12, sm: 8 }}>
|
||||
<DetailsTable fields={tl} item={data} />
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
<TagsList tags={part.tags} />
|
||||
{enableRevisionSelection && (
|
||||
<Paper p='sm' withBorder>
|
||||
<Stack gap='xs'>
|
||||
<Group gap='xs'>
|
||||
<ActionIcon variant='transparent'>
|
||||
<IconVersions />
|
||||
</ActionIcon>
|
||||
<Text>{t`Select Part Revision`}</Text>
|
||||
</Group>
|
||||
<RevisionSelector part={part} options={partRevisionOptions} />
|
||||
</Stack>
|
||||
</Paper>
|
||||
)}
|
||||
<Group gap='xs'>
|
||||
<ActionIcon variant='transparent'>
|
||||
<IconVersions />
|
||||
</ActionIcon>
|
||||
<Text>{t`Select Part Revision`}</Text>
|
||||
</Group>
|
||||
<RevisionSelector part={part} options={partRevisionOptions} />
|
||||
</Stack>
|
||||
<DetailsTable fields={tr} item={data} />
|
||||
<DetailsTable fields={bl} item={data} />
|
||||
<DetailsTable fields={br} item={data} />
|
||||
</ItemDetailsGrid>
|
||||
) : (
|
||||
<Skeleton />
|
||||
</Paper>
|
||||
);
|
||||
}, [
|
||||
globalSettings,
|
||||
part,
|
||||
id,
|
||||
serials,
|
||||
instanceQuery.isFetching,
|
||||
instanceQuery.data,
|
||||
enableRevisionSelection,
|
||||
partRevisionOptions,
|
||||
partRequirementsQuery.isFetching,
|
||||
partRequirements
|
||||
]);
|
||||
}, [enableRevisionSelection, part, partRevisionOptions]);
|
||||
|
||||
// Part data panels (recalculate when part data changes)
|
||||
const partPanels: PanelType[] = useMemo(() => {
|
||||
|
|
@ -675,7 +284,14 @@ export default function PartDetail() {
|
|||
name: 'details',
|
||||
label: t`Part Details`,
|
||||
icon: <IconInfoCircle />,
|
||||
content: detailsPanel
|
||||
content: (
|
||||
<PartDetailsPanel
|
||||
instance={part}
|
||||
allowImageEdit
|
||||
refreshInstance={refreshInstance}
|
||||
additionalContent={revisionSelector}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'stock',
|
||||
|
|
@ -903,8 +519,9 @@ export default function PartDetail() {
|
|||
user,
|
||||
globalSettings,
|
||||
userSettings,
|
||||
detailsPanel,
|
||||
bomInformation
|
||||
bomInformation,
|
||||
revisionSelector,
|
||||
refreshInstance
|
||||
]);
|
||||
|
||||
const breadcrumbs = useMemo(() => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,370 @@
|
|||
import { t } from '@lingui/core/macro';
|
||||
import { Grid, Skeleton, Stack } from '@mantine/core';
|
||||
import { type ReactNode, useMemo } from 'react';
|
||||
|
||||
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 {
|
||||
type DetailsField,
|
||||
DetailsTable
|
||||
} from '../../components/details/Details';
|
||||
import { DetailsImage } from '../../components/details/DetailsImage';
|
||||
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
||||
import { useParameterDetailsGrid } from '../../components/details/ParameterDetailsGrid';
|
||||
import { formatPriceRange } from '../../defaults/formatters';
|
||||
import { useInstance } from '../../hooks/UseInstance';
|
||||
|
||||
export function PartDetailsPanel({
|
||||
instance,
|
||||
allowImageEdit = false,
|
||||
refreshInstance,
|
||||
additionalContent
|
||||
}: Readonly<{
|
||||
instance: any;
|
||||
allowImageEdit?: boolean;
|
||||
refreshInstance?: () => void;
|
||||
additionalContent?: ReactNode;
|
||||
}>) {
|
||||
const { instance: requirements, instanceQuery: requirementsQuery } =
|
||||
useInstance({
|
||||
endpoint: ApiEndpoints.part_requirements,
|
||||
pk: instance?.pk,
|
||||
hasPrimaryKey: true,
|
||||
disabled: !instance?.pk,
|
||||
defaultValue: {}
|
||||
});
|
||||
|
||||
const { instance: serials } = useInstance({
|
||||
endpoint: ApiEndpoints.part_serial_numbers,
|
||||
pk: instance?.pk,
|
||||
hasPrimaryKey: true,
|
||||
disabled: !instance?.pk,
|
||||
defaultValue: {}
|
||||
});
|
||||
|
||||
const data = useMemo(() => {
|
||||
if (!instance) return {};
|
||||
const d = { ...instance };
|
||||
d.total_in_stock =
|
||||
requirements?.total_stock ?? instance?.total_in_stock ?? 0;
|
||||
d.unallocated =
|
||||
requirements?.unallocated_stock ?? instance?.unallocated_stock ?? 0;
|
||||
d.ordering = requirements?.ordering ?? instance?.ordering ?? 0;
|
||||
d.required =
|
||||
(requirements?.required_for_build_orders ??
|
||||
instance?.required_for_build_orders ??
|
||||
0) +
|
||||
(requirements?.required_for_sales_orders ??
|
||||
instance?.required_for_sales_orders ??
|
||||
0);
|
||||
d.allocated =
|
||||
(requirements?.allocated_to_build_orders ??
|
||||
instance?.allocated_to_build_orders ??
|
||||
0) +
|
||||
(requirements?.allocated_to_sales_orders ??
|
||||
instance?.allocated_to_sales_orders ??
|
||||
0);
|
||||
d.can_build = requirements?.can_build ?? 0;
|
||||
if (serials?.latest) d.latest_serial_number = serials.latest;
|
||||
return d;
|
||||
}, [instance, requirements, serials]);
|
||||
|
||||
const fetching = requirementsQuery?.isFetching ?? false;
|
||||
|
||||
const tl: DetailsField[] = [
|
||||
{ type: 'string', name: 'name', label: t`Name`, icon: 'part', copy: true },
|
||||
{
|
||||
type: 'string',
|
||||
name: 'IPN',
|
||||
label: t`IPN`,
|
||||
copy: true,
|
||||
hidden: !instance?.IPN
|
||||
},
|
||||
{ type: 'string', name: 'description', label: t`Description`, copy: true },
|
||||
{
|
||||
type: 'link',
|
||||
name: 'variant_of',
|
||||
label: t`Variant of`,
|
||||
model: ModelType.part,
|
||||
model_field: 'full_name',
|
||||
hidden: !instance?.variant_of
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'revision_of',
|
||||
label: t`Revision of`,
|
||||
model: ModelType.part,
|
||||
model_field: 'full_name',
|
||||
hidden: !instance?.revision_of
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'revision',
|
||||
label: t`Revision`,
|
||||
hidden: !instance?.revision,
|
||||
copy: true
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'category',
|
||||
label: t`Category`,
|
||||
model: ModelType.partcategory
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'default_location',
|
||||
label: t`Default Location`,
|
||||
model: ModelType.stocklocation,
|
||||
hidden: !instance?.default_location
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'category_default_location',
|
||||
label: t`Category Default Location`,
|
||||
model: ModelType.stocklocation,
|
||||
hidden: instance?.default_location || !instance?.category_default_location
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'units',
|
||||
label: t`Units`,
|
||||
copy: true,
|
||||
hidden: !instance?.units
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'keywords',
|
||||
label: t`Keywords`,
|
||||
copy: true,
|
||||
hidden: !instance?.keywords
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'link',
|
||||
label: t`Link`,
|
||||
external: true,
|
||||
copy: true,
|
||||
hidden: !instance?.link
|
||||
}
|
||||
];
|
||||
|
||||
const tr: DetailsField[] = [
|
||||
{
|
||||
type: 'number',
|
||||
name: 'total_in_stock',
|
||||
unit: instance?.units,
|
||||
label: t`In Stock`,
|
||||
hidden: instance?.virtual
|
||||
},
|
||||
{
|
||||
type: 'progressbar',
|
||||
name: 'unallocated_stock',
|
||||
total: data.total_in_stock,
|
||||
progress: data.unallocated,
|
||||
label: t`Available Stock`,
|
||||
hidden: instance?.virtual || data.total_in_stock == data.unallocated
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
name: 'ordering',
|
||||
label: t`On Order`,
|
||||
unit: instance?.units,
|
||||
hidden: !instance?.purchaseable || instance?.ordering <= 0
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
name: 'required',
|
||||
label: t`Required for Orders`,
|
||||
unit: instance?.units,
|
||||
hidden: data.required <= 0,
|
||||
icon: 'stocktake'
|
||||
},
|
||||
{
|
||||
type: 'progressbar',
|
||||
name: 'allocated_to_build_orders',
|
||||
icon: 'manufacturers',
|
||||
total: requirements?.required_for_build_orders,
|
||||
progress: requirements?.allocated_to_build_orders,
|
||||
label: t`Allocated to Build Orders`,
|
||||
hidden:
|
||||
fetching ||
|
||||
(requirements?.required_for_build_orders <= 0 &&
|
||||
requirements?.allocated_to_build_orders <= 0)
|
||||
},
|
||||
{
|
||||
type: 'progressbar',
|
||||
icon: 'sales_orders',
|
||||
name: 'allocated_to_sales_orders',
|
||||
total: requirements?.required_for_sales_orders,
|
||||
progress: requirements?.allocated_to_sales_orders,
|
||||
label: t`Allocated to Sales Orders`,
|
||||
hidden:
|
||||
fetching ||
|
||||
(requirements?.required_for_sales_orders <= 0 &&
|
||||
requirements?.allocated_to_sales_orders <= 0)
|
||||
},
|
||||
{
|
||||
type: 'progressbar',
|
||||
name: 'building',
|
||||
label: t`In Production`,
|
||||
progress: requirements?.building,
|
||||
total: requirements?.scheduled_to_build,
|
||||
hidden:
|
||||
fetching ||
|
||||
(!requirements?.building && !requirements?.scheduled_to_build)
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
name: 'can_build',
|
||||
unit: instance?.units,
|
||||
label: t`Can Build`,
|
||||
hidden: !instance?.assembly || fetching
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
name: 'minimum_stock',
|
||||
unit: instance?.units,
|
||||
label: t`Minimum Stock`,
|
||||
hidden: instance?.minimum_stock <= 0
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
name: 'maximum_stock',
|
||||
unit: instance?.units,
|
||||
label: t`Maximum Stock`,
|
||||
hidden: instance?.maximum_stock <= 0
|
||||
}
|
||||
];
|
||||
|
||||
const bl: DetailsField[] = [
|
||||
{ type: 'boolean', name: 'active', label: t`Active` },
|
||||
{ type: 'boolean', name: 'locked', label: t`Locked` },
|
||||
{
|
||||
type: 'boolean',
|
||||
icon: 'template',
|
||||
name: 'is_template',
|
||||
label: t`Template Part`
|
||||
},
|
||||
{ type: 'boolean', name: 'assembly', label: t`Assembled Part` },
|
||||
{ type: 'boolean', name: 'component', label: t`Component Part` },
|
||||
{
|
||||
type: 'boolean',
|
||||
name: 'testable',
|
||||
label: t`Testable Part`,
|
||||
icon: 'test'
|
||||
},
|
||||
{ type: 'boolean', name: 'trackable', label: t`Trackable Part` },
|
||||
{ type: 'boolean', name: 'purchaseable', label: t`Purchaseable Part` },
|
||||
{
|
||||
type: 'boolean',
|
||||
name: 'salable',
|
||||
icon: 'saleable',
|
||||
label: t`Saleable Part`
|
||||
},
|
||||
{ type: 'boolean', name: 'virtual', label: t`Virtual Part` },
|
||||
{ type: 'boolean', name: 'starred', label: t`Subscribed`, icon: 'bell' }
|
||||
];
|
||||
|
||||
const br: DetailsField[] = useMemo(() => {
|
||||
const fields: DetailsField[] = [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'creation_date',
|
||||
label: t`Creation Date`
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'creation_user',
|
||||
label: t`Created By`,
|
||||
badge: 'user',
|
||||
icon: 'user',
|
||||
hidden: !instance?.creation_user
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'responsible',
|
||||
label: t`Responsible`,
|
||||
badge: 'owner',
|
||||
hidden: !instance?.responsible
|
||||
},
|
||||
{
|
||||
name: 'default_expiry',
|
||||
label: t`Default Expiry`,
|
||||
hidden: !instance?.default_expiry,
|
||||
icon: 'calendar',
|
||||
type: 'string',
|
||||
value_formatter: () => `${instance?.default_expiry} ${t`days`}`
|
||||
}
|
||||
];
|
||||
|
||||
if (instance?.pricing_min || instance?.pricing_max) {
|
||||
fields.push({
|
||||
type: 'string',
|
||||
name: 'pricing',
|
||||
label: t`Price Range`,
|
||||
value_formatter: () =>
|
||||
formatPriceRange(instance?.pricing_min, instance?.pricing_max)
|
||||
});
|
||||
}
|
||||
|
||||
fields.push({
|
||||
type: 'string',
|
||||
name: 'latest_serial_number',
|
||||
label: t`Latest Serial Number`,
|
||||
hidden: !instance?.trackable || !data.latest_serial_number,
|
||||
icon: 'serial'
|
||||
});
|
||||
|
||||
return fields;
|
||||
}, [instance, data.latest_serial_number]);
|
||||
|
||||
const parametersTable = useParameterDetailsGrid({
|
||||
model_type: ModelType.part,
|
||||
model_id: instance?.pk
|
||||
});
|
||||
|
||||
if (!instance?.pk) return <Skeleton />;
|
||||
|
||||
return (
|
||||
<ItemDetailsGrid
|
||||
tables={[
|
||||
{ fields: tr, item: data },
|
||||
{ fields: bl, item: data },
|
||||
{ fields: br, item: data },
|
||||
parametersTable
|
||||
]}
|
||||
>
|
||||
<Stack gap='xs'>
|
||||
<Grid grow>
|
||||
<DetailsImage
|
||||
appRole={allowImageEdit ? UserRoles.part : undefined}
|
||||
imageActions={
|
||||
allowImageEdit
|
||||
? {
|
||||
selectExisting: true,
|
||||
downloadImage: true,
|
||||
uploadFile: true,
|
||||
deleteFile: true
|
||||
}
|
||||
: {}
|
||||
}
|
||||
src={instance.image}
|
||||
thumbnail={instance.thumbnail}
|
||||
apiPath={apiUrl(ApiEndpoints.part_list, instance.pk)}
|
||||
refresh={refreshInstance}
|
||||
pk={instance.pk}
|
||||
/>
|
||||
<Grid.Col span={{ base: 12, sm: 8 }}>
|
||||
<DetailsTable fields={tl} item={data} />
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
<TagsList tags={instance.tags} />
|
||||
{additionalContent}
|
||||
</Stack>
|
||||
</ItemDetailsGrid>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { t } from '@lingui/core/macro';
|
||||
import { Accordion, Grid, Skeleton, Stack } from '@mantine/core';
|
||||
import { Accordion, Stack } from '@mantine/core';
|
||||
import { IconInfoCircle, IconList, IconPackages } from '@tabler/icons-react';
|
||||
import { type ReactNode, useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
|
@ -9,17 +9,10 @@ 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 { TagsList } from '@lib/index';
|
||||
import type { PanelType } from '@lib/types/Panel';
|
||||
import AdminButton from '../../components/buttons/AdminButton';
|
||||
import PrimaryActionButton from '../../components/buttons/PrimaryActionButton';
|
||||
import { PrintingActions } from '../../components/buttons/PrintingActions';
|
||||
import {
|
||||
type DetailsField,
|
||||
DetailsTable
|
||||
} from '../../components/details/Details';
|
||||
import { DetailsImage } from '../../components/details/DetailsImage';
|
||||
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
||||
import {
|
||||
BarcodeActionDropdown,
|
||||
CancelItemAction,
|
||||
|
|
@ -35,7 +28,6 @@ import NotesPanel from '../../components/panels/NotesPanel';
|
|||
import { PanelGroup } from '../../components/panels/PanelGroup';
|
||||
import ParametersPanel from '../../components/panels/ParametersPanel';
|
||||
import { StatusRenderer } from '../../components/render/StatusRenderer';
|
||||
import { formatCurrency } from '../../defaults/formatters';
|
||||
import { usePurchaseOrderFields } from '../../forms/PurchaseOrderForms';
|
||||
import {
|
||||
useCreateApiFormModal,
|
||||
|
|
@ -48,6 +40,7 @@ import { useUserState } from '../../states/UserState';
|
|||
import ExtraLineItemTable from '../../tables/general/ExtraLineItemTable';
|
||||
import { PurchaseOrderLineItemTable } from '../../tables/purchasing/PurchaseOrderLineItemTable';
|
||||
import { StockItemTable } from '../../tables/stock/StockItemTable';
|
||||
import { PurchaseOrderDetailsPanel } from './PurchaseOrderDetailsPanel';
|
||||
|
||||
/**
|
||||
* Detail page for a single PurchaseOrder
|
||||
|
|
@ -72,13 +65,13 @@ export default function PurchaseOrderDetail() {
|
|||
refetchOnMount: true
|
||||
});
|
||||
|
||||
const orderCurrency = useMemo(() => {
|
||||
return (
|
||||
const orderCurrency = useMemo(
|
||||
() =>
|
||||
order.order_currency ||
|
||||
order.supplier_detail?.currency ||
|
||||
globalSettings.getSetting('INVENTREE_DEFAULT_CURRENCY')
|
||||
);
|
||||
}, [order, globalSettings]);
|
||||
globalSettings.getSetting('INVENTREE_DEFAULT_CURRENCY'),
|
||||
[order, globalSettings]
|
||||
);
|
||||
|
||||
const purchaseOrderFields = usePurchaseOrderFields({});
|
||||
|
||||
|
|
@ -133,222 +126,19 @@ export default function PurchaseOrderDetail() {
|
|||
modelType: ModelType.purchaseorder
|
||||
});
|
||||
|
||||
const detailsPanel = useMemo(() => {
|
||||
if (instanceQuery.isFetching) {
|
||||
return <Skeleton />;
|
||||
}
|
||||
|
||||
const tl: DetailsField[] = [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'reference',
|
||||
label: t`Reference`,
|
||||
copy: true
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'supplier_reference',
|
||||
label: t`Supplier Reference`,
|
||||
icon: 'reference',
|
||||
hidden: !order.supplier_reference,
|
||||
copy: true
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'supplier',
|
||||
icon: 'suppliers',
|
||||
label: t`Supplier`,
|
||||
model: ModelType.company
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'description',
|
||||
label: t`Description`,
|
||||
copy: true
|
||||
},
|
||||
{
|
||||
type: 'status',
|
||||
name: 'status',
|
||||
label: t`Status`,
|
||||
model: ModelType.purchaseorder
|
||||
},
|
||||
{
|
||||
type: 'status',
|
||||
name: 'status_custom_key',
|
||||
label: t`Custom Status`,
|
||||
model: ModelType.purchaseorder,
|
||||
icon: 'status',
|
||||
hidden:
|
||||
!order.status_custom_key || order.status_custom_key == order.status
|
||||
}
|
||||
];
|
||||
|
||||
const tr: DetailsField[] = [
|
||||
{
|
||||
type: 'progressbar',
|
||||
name: 'completed',
|
||||
icon: 'progress',
|
||||
label: t`Completed Line Items`,
|
||||
total: order.line_items,
|
||||
progress: order.completed_lines
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
model: ModelType.stocklocation,
|
||||
link: true,
|
||||
name: 'destination',
|
||||
label: t`Destination`,
|
||||
hidden: !order.destination
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'currency',
|
||||
label: t`Order Currency`,
|
||||
value_formatter: () => orderCurrency
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'total_price',
|
||||
label: t`Total Cost`,
|
||||
value_formatter: () => {
|
||||
return formatCurrency(order?.total_price, {
|
||||
currency: order?.order_currency || order?.supplier_detail?.currency
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const bl: DetailsField[] = [
|
||||
{
|
||||
type: 'link',
|
||||
external: true,
|
||||
name: 'link',
|
||||
label: t`Link`,
|
||||
copy: true,
|
||||
hidden: !order.link
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'contact_detail.name',
|
||||
label: t`Contact`,
|
||||
icon: 'user',
|
||||
copy: true,
|
||||
hidden: !order.contact
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'contact_detail.email',
|
||||
label: t`Contact Email`,
|
||||
icon: 'email',
|
||||
copy: true,
|
||||
hidden: !order.contact_detail?.email
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'contact_detail.phone',
|
||||
label: t`Contact Phone`,
|
||||
icon: 'phone',
|
||||
copy: true,
|
||||
hidden: !order.contact_detail?.phone
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'project_code_label',
|
||||
label: t`Project Code`,
|
||||
icon: 'reference',
|
||||
copy: true,
|
||||
hidden: !order.project_code
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'responsible',
|
||||
label: t`Responsible`,
|
||||
badge: 'owner',
|
||||
hidden: !order.responsible
|
||||
}
|
||||
];
|
||||
|
||||
const br: DetailsField[] = [
|
||||
{
|
||||
type: 'date',
|
||||
name: 'creation_date',
|
||||
label: t`Creation Date`,
|
||||
copy: true,
|
||||
icon: 'calendar'
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'issue_date',
|
||||
label: t`Issue Date`,
|
||||
icon: 'calendar',
|
||||
copy: true,
|
||||
hidden: !order.issue_date
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'start_date',
|
||||
label: t`Start Date`,
|
||||
icon: 'calendar',
|
||||
copy: true,
|
||||
hidden: !order.start_date
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'target_date',
|
||||
label: t`Target Date`,
|
||||
icon: 'calendar',
|
||||
copy: true,
|
||||
hidden: !order.target_date
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'complete_date',
|
||||
icon: 'calendar_check',
|
||||
label: t`Completion Date`,
|
||||
copy: true,
|
||||
hidden: !order.complete_date
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'updated_at',
|
||||
label: t`Last Updated`,
|
||||
icon: 'calendar',
|
||||
copy: true,
|
||||
showTime: true,
|
||||
hidden: !order.updated_at
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<ItemDetailsGrid>
|
||||
<Stack gap='xs'>
|
||||
<Grid grow>
|
||||
<DetailsImage
|
||||
appRole={UserRoles.purchase_order}
|
||||
apiPath={ApiEndpoints.company_list}
|
||||
src={order.supplier_detail?.image}
|
||||
pk={order.supplier}
|
||||
/>
|
||||
<Grid.Col span={{ base: 12, sm: 8 }}>
|
||||
<DetailsTable fields={tl} item={order} />
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
<TagsList tags={order.tags} />
|
||||
</Stack>
|
||||
<DetailsTable fields={tr} item={order} />
|
||||
<DetailsTable fields={bl} item={order} />
|
||||
<DetailsTable fields={br} item={order} />
|
||||
</ItemDetailsGrid>
|
||||
);
|
||||
}, [order, orderCurrency, instanceQuery]);
|
||||
|
||||
const orderPanels: PanelType[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
name: 'detail',
|
||||
label: t`Order Details`,
|
||||
icon: <IconInfoCircle />,
|
||||
content: detailsPanel
|
||||
content: (
|
||||
<PurchaseOrderDetailsPanel
|
||||
instance={order}
|
||||
allowImageEdit
|
||||
refreshInstance={refreshInstance}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'line-items',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,258 @@
|
|||
import { t } from '@lingui/core/macro';
|
||||
import { Grid, Skeleton, Stack } from '@mantine/core';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
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 {
|
||||
type DetailsField,
|
||||
DetailsTable
|
||||
} from '../../components/details/Details';
|
||||
import { DetailsImage } from '../../components/details/DetailsImage';
|
||||
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
||||
import { useParameterDetailsGrid } from '../../components/details/ParameterDetailsGrid';
|
||||
import { formatCurrency } from '../../defaults/formatters';
|
||||
import { useGlobalSettingsState } from '../../states/SettingsStates';
|
||||
|
||||
export function PurchaseOrderDetailsPanel({
|
||||
instance,
|
||||
allowImageEdit = false,
|
||||
refreshInstance
|
||||
}: Readonly<{
|
||||
instance: any;
|
||||
allowImageEdit?: boolean;
|
||||
refreshInstance?: () => void;
|
||||
}>) {
|
||||
const globalSettings = useGlobalSettingsState();
|
||||
|
||||
const orderCurrency = useMemo(
|
||||
() =>
|
||||
instance?.order_currency ||
|
||||
instance?.supplier_detail?.currency ||
|
||||
globalSettings.getSetting('INVENTREE_DEFAULT_CURRENCY'),
|
||||
[instance, globalSettings]
|
||||
);
|
||||
|
||||
const tl: DetailsField[] = [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'reference',
|
||||
label: t`Reference`,
|
||||
copy: true
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'supplier_reference',
|
||||
label: t`Supplier Reference`,
|
||||
icon: 'reference',
|
||||
hidden: !instance?.supplier_reference,
|
||||
copy: true
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'supplier',
|
||||
icon: 'suppliers',
|
||||
label: t`Supplier`,
|
||||
model: ModelType.company
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'description',
|
||||
label: t`Description`,
|
||||
copy: true
|
||||
},
|
||||
{
|
||||
type: 'status',
|
||||
name: 'status',
|
||||
label: t`Status`,
|
||||
model: ModelType.purchaseorder
|
||||
},
|
||||
{
|
||||
type: 'status',
|
||||
name: 'status_custom_key',
|
||||
label: t`Custom Status`,
|
||||
model: ModelType.purchaseorder,
|
||||
icon: 'status',
|
||||
hidden:
|
||||
!instance?.status_custom_key ||
|
||||
instance?.status_custom_key == instance?.status
|
||||
}
|
||||
];
|
||||
|
||||
const tr: DetailsField[] = [
|
||||
{
|
||||
type: 'progressbar',
|
||||
name: 'completed',
|
||||
icon: 'progress',
|
||||
label: t`Completed Line Items`,
|
||||
total: instance?.line_items,
|
||||
progress: instance?.completed_lines
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
model: ModelType.stocklocation,
|
||||
link: true,
|
||||
name: 'destination',
|
||||
label: t`Destination`,
|
||||
hidden: !instance?.destination
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'currency',
|
||||
label: t`Order Currency`,
|
||||
value_formatter: () => orderCurrency
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'total_price',
|
||||
label: t`Total Cost`,
|
||||
value_formatter: () =>
|
||||
formatCurrency(instance?.total_price, {
|
||||
currency:
|
||||
instance?.order_currency || instance?.supplier_detail?.currency
|
||||
})
|
||||
}
|
||||
];
|
||||
|
||||
const bl: DetailsField[] = [
|
||||
{
|
||||
type: 'link',
|
||||
external: true,
|
||||
name: 'link',
|
||||
label: t`Link`,
|
||||
copy: true,
|
||||
hidden: !instance?.link
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'contact_detail.name',
|
||||
label: t`Contact`,
|
||||
icon: 'user',
|
||||
copy: true,
|
||||
hidden: !instance?.contact
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'contact_detail.email',
|
||||
label: t`Contact Email`,
|
||||
icon: 'email',
|
||||
copy: true,
|
||||
hidden: !instance?.contact_detail?.email
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'contact_detail.phone',
|
||||
label: t`Contact Phone`,
|
||||
icon: 'phone',
|
||||
copy: true,
|
||||
hidden: !instance?.contact_detail?.phone
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'project_code_label',
|
||||
label: t`Project Code`,
|
||||
icon: 'reference',
|
||||
copy: true,
|
||||
hidden: !instance?.project_code
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'responsible',
|
||||
label: t`Responsible`,
|
||||
badge: 'owner',
|
||||
hidden: !instance?.responsible
|
||||
}
|
||||
];
|
||||
|
||||
const br: DetailsField[] = [
|
||||
{
|
||||
type: 'date',
|
||||
name: 'creation_date',
|
||||
label: t`Creation Date`,
|
||||
copy: true,
|
||||
icon: 'calendar'
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'issue_date',
|
||||
label: t`Issue Date`,
|
||||
icon: 'calendar',
|
||||
copy: true,
|
||||
hidden: !instance?.issue_date
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'start_date',
|
||||
label: t`Start Date`,
|
||||
icon: 'calendar',
|
||||
copy: true,
|
||||
hidden: !instance?.start_date
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'target_date',
|
||||
label: t`Target Date`,
|
||||
icon: 'calendar',
|
||||
copy: true,
|
||||
hidden: !instance?.target_date
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'complete_date',
|
||||
icon: 'calendar_check',
|
||||
label: t`Completion Date`,
|
||||
copy: true,
|
||||
hidden: !instance?.complete_date
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'updated_at',
|
||||
label: t`Last Updated`,
|
||||
icon: 'calendar',
|
||||
copy: true,
|
||||
showTime: true,
|
||||
hidden: !instance?.updated_at
|
||||
}
|
||||
];
|
||||
|
||||
const parametersTable = useParameterDetailsGrid({
|
||||
model_type: ModelType.purchaseorder,
|
||||
model_id: instance?.pk
|
||||
});
|
||||
|
||||
if (!instance?.pk) return <Skeleton />;
|
||||
|
||||
return (
|
||||
<ItemDetailsGrid
|
||||
tables={[
|
||||
{ fields: tr, item: instance },
|
||||
{ fields: bl, item: instance },
|
||||
{ fields: br, item: instance },
|
||||
parametersTable
|
||||
]}
|
||||
>
|
||||
<Stack gap='xs'>
|
||||
<Grid grow>
|
||||
<DetailsImage
|
||||
appRole={allowImageEdit ? UserRoles.purchase_order : undefined}
|
||||
imageActions={
|
||||
allowImageEdit ? { uploadFile: true, deleteFile: true } : {}
|
||||
}
|
||||
apiPath={apiUrl(ApiEndpoints.company_list, instance?.supplier)}
|
||||
src={instance?.supplier_detail?.image}
|
||||
pk={instance?.supplier}
|
||||
refresh={refreshInstance}
|
||||
/>
|
||||
<Grid.Col span={{ base: 12, sm: 8 }}>
|
||||
<DetailsTable fields={tl} item={instance} />
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
<TagsList tags={instance?.tags} />
|
||||
</Stack>
|
||||
</ItemDetailsGrid>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { t } from '@lingui/core/macro';
|
||||
import { Accordion, Grid, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import { Accordion, Stack } from '@mantine/core';
|
||||
import { IconInfoCircle, IconList } from '@tabler/icons-react';
|
||||
import { type ReactNode, useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
|
@ -9,17 +9,10 @@ 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 { TagsList } from '@lib/index';
|
||||
import type { PanelType } from '@lib/types/Panel';
|
||||
import AdminButton from '../../components/buttons/AdminButton';
|
||||
import PrimaryActionButton from '../../components/buttons/PrimaryActionButton';
|
||||
import { PrintingActions } from '../../components/buttons/PrintingActions';
|
||||
import {
|
||||
type DetailsField,
|
||||
DetailsTable
|
||||
} from '../../components/details/Details';
|
||||
import { DetailsImage } from '../../components/details/DetailsImage';
|
||||
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
||||
import {
|
||||
BarcodeActionDropdown,
|
||||
CancelItemAction,
|
||||
|
|
@ -34,9 +27,7 @@ import AttachmentPanel from '../../components/panels/AttachmentPanel';
|
|||
import NotesPanel from '../../components/panels/NotesPanel';
|
||||
import { PanelGroup } from '../../components/panels/PanelGroup';
|
||||
import ParametersPanel from '../../components/panels/ParametersPanel';
|
||||
import { RenderAddress } from '../../components/render/Company';
|
||||
import { StatusRenderer } from '../../components/render/StatusRenderer';
|
||||
import { formatCurrency } from '../../defaults/formatters';
|
||||
import { useReturnOrderFields } from '../../forms/ReturnOrderForms';
|
||||
import {
|
||||
useCreateApiFormModal,
|
||||
|
|
@ -48,6 +39,7 @@ import { useGlobalSettingsState } from '../../states/SettingsStates';
|
|||
import { useUserState } from '../../states/UserState';
|
||||
import ExtraLineItemTable from '../../tables/general/ExtraLineItemTable';
|
||||
import ReturnOrderLineItemTable from '../../tables/sales/ReturnOrderLineItemTable';
|
||||
import { ReturnOrderDetailsPanel } from './ReturnOrderDetailsPanel';
|
||||
|
||||
/**
|
||||
* Detail page for a single ReturnOrder
|
||||
|
|
@ -99,233 +91,19 @@ export default function ReturnOrderDetail() {
|
|||
);
|
||||
}, [order, globalSettings]);
|
||||
|
||||
const detailsPanel = useMemo(() => {
|
||||
if (instanceQuery.isFetching) {
|
||||
return <Skeleton />;
|
||||
}
|
||||
|
||||
const tl: DetailsField[] = [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'reference',
|
||||
label: t`Reference`,
|
||||
copy: true
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'customer_reference',
|
||||
label: t`Customer Reference`,
|
||||
icon: 'customer',
|
||||
copy: true,
|
||||
hidden: !order.customer_reference
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'customer',
|
||||
icon: 'customers',
|
||||
label: t`Customer`,
|
||||
model: ModelType.company
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'description',
|
||||
label: t`Description`,
|
||||
copy: true
|
||||
},
|
||||
{
|
||||
type: 'status',
|
||||
name: 'status',
|
||||
label: t`Status`,
|
||||
model: ModelType.returnorder
|
||||
},
|
||||
{
|
||||
type: 'status',
|
||||
name: 'status_custom_key',
|
||||
label: t`Custom Status`,
|
||||
model: ModelType.returnorder,
|
||||
icon: 'status',
|
||||
hidden:
|
||||
!order.status_custom_key || order.status_custom_key == order.status
|
||||
}
|
||||
];
|
||||
|
||||
const tr: DetailsField[] = [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'line_items',
|
||||
label: t`Line Items`,
|
||||
icon: 'list'
|
||||
},
|
||||
{
|
||||
type: 'progressbar',
|
||||
name: 'completed',
|
||||
icon: 'progress',
|
||||
label: t`Completed Line Items`,
|
||||
total: order.line_items,
|
||||
progress: order.completed_lines
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'currency',
|
||||
label: t`Order Currency`,
|
||||
value_formatter: () =>
|
||||
order?.order_currency ?? order?.customer_detail?.currency
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'total_price',
|
||||
label: t`Total Cost`,
|
||||
value_formatter: () => {
|
||||
return formatCurrency(order?.total_price, {
|
||||
currency: order?.order_currency || order?.customer_detail?.currency
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const bl: DetailsField[] = [
|
||||
{
|
||||
type: 'link',
|
||||
external: true,
|
||||
name: 'link',
|
||||
label: t`Link`,
|
||||
copy: true,
|
||||
hidden: !order.link
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'address',
|
||||
label: t`Return Address`,
|
||||
icon: 'address',
|
||||
value_formatter: () =>
|
||||
order.address_detail ? (
|
||||
<RenderAddress instance={order.address_detail} />
|
||||
) : (
|
||||
<Text size='sm' c='red'>{t`Not specified`}</Text>
|
||||
)
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'contact_detail.name',
|
||||
label: t`Contact`,
|
||||
icon: 'user',
|
||||
copy: true,
|
||||
hidden: !order.contact
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'contact_detail.email',
|
||||
label: t`Contact Email`,
|
||||
icon: 'email',
|
||||
copy: true,
|
||||
hidden: !order.contact_detail?.email
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'contact_detail.phone',
|
||||
label: t`Contact Phone`,
|
||||
icon: 'phone',
|
||||
copy: true,
|
||||
hidden: !order.contact_detail?.phone
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'project_code_label',
|
||||
label: t`Project Code`,
|
||||
icon: 'reference',
|
||||
copy: true,
|
||||
hidden: !order.project_code
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'responsible',
|
||||
label: t`Responsible`,
|
||||
badge: 'owner',
|
||||
hidden: !order.responsible
|
||||
}
|
||||
];
|
||||
|
||||
const br: DetailsField[] = [
|
||||
{
|
||||
type: 'date',
|
||||
name: 'creation_date',
|
||||
label: t`Creation Date`,
|
||||
icon: 'calendar',
|
||||
copy: true,
|
||||
hidden: !order.creation_date
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'issue_date',
|
||||
label: t`Issue Date`,
|
||||
icon: 'calendar',
|
||||
copy: true,
|
||||
hidden: !order.issue_date
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'start_date',
|
||||
label: t`Start Date`,
|
||||
icon: 'calendar',
|
||||
copy: true,
|
||||
hidden: !order.start_date
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'target_date',
|
||||
label: t`Target Date`,
|
||||
copy: true,
|
||||
hidden: !order.target_date
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'complete_date',
|
||||
icon: 'calendar_check',
|
||||
label: t`Completion Date`,
|
||||
copy: true,
|
||||
hidden: !order.complete_date
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'updated_at',
|
||||
label: t`Last Updated`,
|
||||
icon: 'calendar',
|
||||
copy: true,
|
||||
showTime: true,
|
||||
hidden: !order.updated_at
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<ItemDetailsGrid>
|
||||
<Stack gap='xs'>
|
||||
<Grid grow>
|
||||
<DetailsImage
|
||||
appRole={UserRoles.purchase_order}
|
||||
apiPath={ApiEndpoints.company_list}
|
||||
src={order.customer_detail?.image}
|
||||
pk={order.customer}
|
||||
/>
|
||||
<Grid.Col span={{ base: 12, sm: 8 }}>
|
||||
<DetailsTable fields={tl} item={order} />
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
<TagsList tags={order.tags} />
|
||||
</Stack>
|
||||
<DetailsTable fields={tr} item={order} />
|
||||
<DetailsTable fields={bl} item={order} />
|
||||
<DetailsTable fields={br} item={order} />
|
||||
</ItemDetailsGrid>
|
||||
);
|
||||
}, [order, instanceQuery]);
|
||||
|
||||
const orderPanels: PanelType[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
name: 'detail',
|
||||
label: t`Order Details`,
|
||||
icon: <IconInfoCircle />,
|
||||
content: detailsPanel
|
||||
content: (
|
||||
<ReturnOrderDetailsPanel
|
||||
instance={order}
|
||||
allowImageEdit
|
||||
refreshInstance={refreshInstance}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'line-items',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,270 @@
|
|||
import { t } from '@lingui/core/macro';
|
||||
import { Grid, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
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 {
|
||||
type DetailsField,
|
||||
DetailsTable
|
||||
} from '../../components/details/Details';
|
||||
import { DetailsImage } from '../../components/details/DetailsImage';
|
||||
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
||||
import { useParameterDetailsGrid } from '../../components/details/ParameterDetailsGrid';
|
||||
import { RenderAddress } from '../../components/render/Company';
|
||||
import { formatCurrency } from '../../defaults/formatters';
|
||||
import { useGlobalSettingsState } from '../../states/SettingsStates';
|
||||
|
||||
export function ReturnOrderDetailsPanel({
|
||||
instance,
|
||||
allowImageEdit = false,
|
||||
refreshInstance
|
||||
}: Readonly<{
|
||||
instance: any;
|
||||
allowImageEdit?: boolean;
|
||||
refreshInstance?: () => void;
|
||||
}>) {
|
||||
const globalSettings = useGlobalSettingsState();
|
||||
|
||||
const orderCurrency = useMemo(
|
||||
() =>
|
||||
instance?.order_currency ||
|
||||
instance?.customer_detail?.currency ||
|
||||
globalSettings.getSetting('INVENTREE_DEFAULT_CURRENCY'),
|
||||
[instance, globalSettings]
|
||||
);
|
||||
|
||||
const tl: DetailsField[] = [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'reference',
|
||||
label: t`Reference`,
|
||||
copy: true
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'customer_reference',
|
||||
label: t`Customer Reference`,
|
||||
icon: 'customer',
|
||||
copy: true,
|
||||
hidden: !instance?.customer_reference
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'customer',
|
||||
icon: 'customers',
|
||||
label: t`Customer`,
|
||||
model: ModelType.company
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'description',
|
||||
label: t`Description`,
|
||||
copy: true
|
||||
},
|
||||
{
|
||||
type: 'status',
|
||||
name: 'status',
|
||||
label: t`Status`,
|
||||
model: ModelType.returnorder
|
||||
},
|
||||
{
|
||||
type: 'status',
|
||||
name: 'status_custom_key',
|
||||
label: t`Custom Status`,
|
||||
model: ModelType.returnorder,
|
||||
icon: 'status',
|
||||
hidden:
|
||||
!instance?.status_custom_key ||
|
||||
instance?.status_custom_key == instance?.status
|
||||
}
|
||||
];
|
||||
|
||||
const tr: DetailsField[] = [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'line_items',
|
||||
label: t`Line Items`,
|
||||
icon: 'list'
|
||||
},
|
||||
{
|
||||
type: 'progressbar',
|
||||
name: 'completed',
|
||||
icon: 'progress',
|
||||
label: t`Completed Line Items`,
|
||||
total: instance?.line_items,
|
||||
progress: instance?.completed_lines
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'currency',
|
||||
label: t`Order Currency`,
|
||||
value_formatter: () =>
|
||||
instance?.order_currency ?? instance?.customer_detail?.currency
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'total_price',
|
||||
label: t`Total Cost`,
|
||||
value_formatter: () =>
|
||||
formatCurrency(instance?.total_price, {
|
||||
currency:
|
||||
instance?.order_currency || instance?.customer_detail?.currency
|
||||
})
|
||||
}
|
||||
];
|
||||
|
||||
const bl: DetailsField[] = [
|
||||
{
|
||||
type: 'link',
|
||||
external: true,
|
||||
name: 'link',
|
||||
label: t`Link`,
|
||||
copy: true,
|
||||
hidden: !instance?.link
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'address',
|
||||
label: t`Return Address`,
|
||||
icon: 'address',
|
||||
value_formatter: () =>
|
||||
instance?.address_detail ? (
|
||||
<RenderAddress instance={instance.address_detail} />
|
||||
) : (
|
||||
<Text size='sm' c='red'>{t`Not specified`}</Text>
|
||||
)
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'contact_detail.name',
|
||||
label: t`Contact`,
|
||||
icon: 'user',
|
||||
copy: true,
|
||||
hidden: !instance?.contact
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'contact_detail.email',
|
||||
label: t`Contact Email`,
|
||||
icon: 'email',
|
||||
copy: true,
|
||||
hidden: !instance?.contact_detail?.email
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'contact_detail.phone',
|
||||
label: t`Contact Phone`,
|
||||
icon: 'phone',
|
||||
copy: true,
|
||||
hidden: !instance?.contact_detail?.phone
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'project_code_label',
|
||||
label: t`Project Code`,
|
||||
icon: 'reference',
|
||||
copy: true,
|
||||
hidden: !instance?.project_code
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'responsible',
|
||||
label: t`Responsible`,
|
||||
badge: 'owner',
|
||||
hidden: !instance?.responsible
|
||||
}
|
||||
];
|
||||
|
||||
const br: DetailsField[] = [
|
||||
{
|
||||
type: 'date',
|
||||
name: 'creation_date',
|
||||
label: t`Creation Date`,
|
||||
icon: 'calendar',
|
||||
copy: true,
|
||||
hidden: !instance?.creation_date
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'issue_date',
|
||||
label: t`Issue Date`,
|
||||
icon: 'calendar',
|
||||
copy: true,
|
||||
hidden: !instance?.issue_date
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'start_date',
|
||||
label: t`Start Date`,
|
||||
icon: 'calendar',
|
||||
copy: true,
|
||||
hidden: !instance?.start_date
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'target_date',
|
||||
label: t`Target Date`,
|
||||
copy: true,
|
||||
hidden: !instance?.target_date
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'complete_date',
|
||||
icon: 'calendar_check',
|
||||
label: t`Completion Date`,
|
||||
copy: true,
|
||||
hidden: !instance?.complete_date
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'updated_at',
|
||||
label: t`Last Updated`,
|
||||
icon: 'calendar',
|
||||
copy: true,
|
||||
showTime: true,
|
||||
hidden: !instance?.updated_at
|
||||
}
|
||||
];
|
||||
|
||||
const parametersTable = useParameterDetailsGrid({
|
||||
model_type: ModelType.returnorder,
|
||||
model_id: instance?.pk
|
||||
});
|
||||
|
||||
if (!instance?.pk) return <Skeleton />;
|
||||
|
||||
return (
|
||||
<ItemDetailsGrid
|
||||
tables={[
|
||||
{ fields: tr, item: instance },
|
||||
{ fields: bl, item: instance },
|
||||
{ fields: br, item: instance },
|
||||
parametersTable
|
||||
]}
|
||||
>
|
||||
<Stack gap='xs'>
|
||||
<Grid grow>
|
||||
<DetailsImage
|
||||
appRole={allowImageEdit ? UserRoles.return_order : undefined}
|
||||
imageActions={
|
||||
allowImageEdit ? { uploadFile: true, deleteFile: true } : {}
|
||||
}
|
||||
apiPath={apiUrl(ApiEndpoints.company_list, instance?.customer)}
|
||||
src={instance?.customer_detail?.image}
|
||||
pk={instance?.customer}
|
||||
refresh={refreshInstance}
|
||||
/>
|
||||
<Grid.Col span={{ base: 12, sm: 8 }}>
|
||||
<DetailsTable fields={tl} item={instance} />
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
<TagsList tags={instance?.tags} />
|
||||
</Stack>
|
||||
</ItemDetailsGrid>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { t } from '@lingui/core/macro';
|
||||
import { Accordion, Grid, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import { Accordion, Skeleton, Stack } from '@mantine/core';
|
||||
import {
|
||||
IconBookmark,
|
||||
IconCubeSend,
|
||||
|
|
@ -11,7 +11,6 @@ import { type ReactNode, useMemo } from 'react';
|
|||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { StylishText } from '@lib/components/StylishText';
|
||||
import TagsList from '@lib/components/TagsList';
|
||||
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||
import { ModelType } from '@lib/enums/ModelType';
|
||||
import { UserRoles } from '@lib/enums/Roles';
|
||||
|
|
@ -20,12 +19,6 @@ import type { PanelType } from '@lib/types/Panel';
|
|||
import AdminButton from '../../components/buttons/AdminButton';
|
||||
import PrimaryActionButton from '../../components/buttons/PrimaryActionButton';
|
||||
import { PrintingActions } from '../../components/buttons/PrintingActions';
|
||||
import {
|
||||
type DetailsField,
|
||||
DetailsTable
|
||||
} from '../../components/details/Details';
|
||||
import { DetailsImage } from '../../components/details/DetailsImage';
|
||||
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
||||
import {
|
||||
BarcodeActionDropdown,
|
||||
CancelItemAction,
|
||||
|
|
@ -40,9 +33,7 @@ import AttachmentPanel from '../../components/panels/AttachmentPanel';
|
|||
import NotesPanel from '../../components/panels/NotesPanel';
|
||||
import { PanelGroup } from '../../components/panels/PanelGroup';
|
||||
import ParametersPanel from '../../components/panels/ParametersPanel';
|
||||
import { RenderAddress } from '../../components/render/Company';
|
||||
import { StatusRenderer } from '../../components/render/StatusRenderer';
|
||||
import { formatCurrency } from '../../defaults/formatters';
|
||||
import { useSalesOrderFields } from '../../forms/SalesOrderForms';
|
||||
import {
|
||||
useCreateApiFormModal,
|
||||
|
|
@ -57,6 +48,7 @@ import ExtraLineItemTable from '../../tables/general/ExtraLineItemTable';
|
|||
import SalesOrderAllocationTable from '../../tables/sales/SalesOrderAllocationTable';
|
||||
import SalesOrderLineItemTable from '../../tables/sales/SalesOrderLineItemTable';
|
||||
import SalesOrderShipmentTable from '../../tables/sales/SalesOrderShipmentTable';
|
||||
import { SalesOrderDetailsPanel } from './SalesOrderDetailsPanel';
|
||||
|
||||
/**
|
||||
* Detail page for a single SalesOrder
|
||||
|
|
@ -89,227 +81,6 @@ export default function SalesOrderDetail() {
|
|||
);
|
||||
}, [order, globalSettings]);
|
||||
|
||||
const detailsPanel = useMemo(() => {
|
||||
if (instanceQuery.isFetching) {
|
||||
return <Skeleton />;
|
||||
}
|
||||
|
||||
const tl: DetailsField[] = [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'reference',
|
||||
label: t`Reference`,
|
||||
copy: true
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'customer_reference',
|
||||
label: t`Customer Reference`,
|
||||
copy: true,
|
||||
icon: 'reference',
|
||||
hidden: !order.customer_reference
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'customer',
|
||||
icon: 'customers',
|
||||
label: t`Customer`,
|
||||
model: ModelType.company
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'description',
|
||||
label: t`Description`,
|
||||
copy: true
|
||||
},
|
||||
{
|
||||
type: 'status',
|
||||
name: 'status',
|
||||
label: t`Status`,
|
||||
model: ModelType.salesorder
|
||||
},
|
||||
{
|
||||
type: 'status',
|
||||
name: 'status_custom_key',
|
||||
label: t`Custom Status`,
|
||||
model: ModelType.salesorder,
|
||||
icon: 'status',
|
||||
hidden:
|
||||
!order.status_custom_key || order.status_custom_key == order.status
|
||||
}
|
||||
];
|
||||
|
||||
const tr: DetailsField[] = [
|
||||
{
|
||||
type: 'progressbar',
|
||||
name: 'completed',
|
||||
icon: 'progress',
|
||||
label: t`Completed Line Items`,
|
||||
total: order.line_items,
|
||||
progress: order.completed_lines,
|
||||
hidden: !order.line_items
|
||||
},
|
||||
{
|
||||
type: 'progressbar',
|
||||
name: 'shipments',
|
||||
icon: 'shipment',
|
||||
label: t`Completed Shipments`,
|
||||
total: order.shipments_count,
|
||||
progress: order.completed_shipments_count,
|
||||
hidden: !order.shipments_count
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'currency',
|
||||
label: t`Order Currency`,
|
||||
value_formatter: () => orderCurrency
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'total_price',
|
||||
label: t`Total Cost`,
|
||||
value_formatter: () => {
|
||||
return formatCurrency(order?.total_price, {
|
||||
currency: orderCurrency
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const bl: DetailsField[] = [
|
||||
{
|
||||
type: 'link',
|
||||
external: true,
|
||||
name: 'link',
|
||||
label: t`Link`,
|
||||
copy: true,
|
||||
hidden: !order.link
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'address',
|
||||
label: t`Shipping Address`,
|
||||
icon: 'address',
|
||||
value_formatter: () =>
|
||||
order.address_detail ? (
|
||||
<RenderAddress instance={order.address_detail} />
|
||||
) : (
|
||||
<Text size='sm' c='red'>{t`Not specified`}</Text>
|
||||
)
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'contact_detail.name',
|
||||
label: t`Contact`,
|
||||
icon: 'user',
|
||||
copy: true,
|
||||
hidden: !order.contact
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'contact_detail.email',
|
||||
label: t`Contact Email`,
|
||||
icon: 'email',
|
||||
copy: true,
|
||||
hidden: !order.contact_detail?.email
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'contact_detail.phone',
|
||||
label: t`Contact Phone`,
|
||||
icon: 'phone',
|
||||
copy: true,
|
||||
hidden: !order.contact_detail?.phone
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'project_code_label',
|
||||
label: t`Project Code`,
|
||||
icon: 'reference',
|
||||
copy: true,
|
||||
hidden: !order.project_code
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'responsible',
|
||||
label: t`Responsible`,
|
||||
badge: 'owner',
|
||||
hidden: !order.responsible
|
||||
}
|
||||
];
|
||||
|
||||
const br: DetailsField[] = [
|
||||
{
|
||||
type: 'date',
|
||||
name: 'creation_date',
|
||||
label: t`Creation Date`,
|
||||
copy: true,
|
||||
hidden: !order.creation_date
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'issue_date',
|
||||
label: t`Issue Date`,
|
||||
icon: 'calendar',
|
||||
copy: true,
|
||||
hidden: !order.issue_date
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'start_date',
|
||||
label: t`Start Date`,
|
||||
icon: 'calendar',
|
||||
hidden: !order.start_date,
|
||||
copy: true
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'target_date',
|
||||
label: t`Target Date`,
|
||||
hidden: !order.target_date,
|
||||
copy: true
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'shipment_date',
|
||||
label: t`Completion Date`,
|
||||
hidden: !order.shipment_date,
|
||||
copy: true
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'updated_at',
|
||||
label: t`Last Updated`,
|
||||
icon: 'calendar',
|
||||
copy: true,
|
||||
showTime: true,
|
||||
hidden: !order.updated_at
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<ItemDetailsGrid>
|
||||
<Stack gap='xs'>
|
||||
<Grid grow>
|
||||
<DetailsImage
|
||||
appRole={UserRoles.purchase_order}
|
||||
apiPath={ApiEndpoints.company_list}
|
||||
src={order.customer_detail?.image}
|
||||
pk={order.customer}
|
||||
/>
|
||||
<Grid.Col span={{ base: 12, sm: 8 }}>
|
||||
<DetailsTable fields={tl} item={order} />
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
<TagsList tags={order.tags} />
|
||||
</Stack>
|
||||
<DetailsTable fields={tr} item={order} />
|
||||
<DetailsTable fields={bl} item={order} />
|
||||
<DetailsTable fields={br} item={order} />
|
||||
</ItemDetailsGrid>
|
||||
);
|
||||
}, [order, orderCurrency, instanceQuery]);
|
||||
|
||||
const soStatus = useStatusCodes({ modelType: ModelType.salesorder });
|
||||
|
||||
const lineItemsEditable: boolean = useMemo(() => {
|
||||
|
|
@ -364,7 +135,13 @@ export default function SalesOrderDetail() {
|
|||
name: 'detail',
|
||||
label: t`Order Details`,
|
||||
icon: <IconInfoCircle />,
|
||||
content: detailsPanel
|
||||
content: (
|
||||
<SalesOrderDetailsPanel
|
||||
instance={order}
|
||||
allowImageEdit
|
||||
refreshInstance={refreshInstance}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'line-items',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,270 @@
|
|||
import { t } from '@lingui/core/macro';
|
||||
import { Grid, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
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 {
|
||||
type DetailsField,
|
||||
DetailsTable
|
||||
} from '../../components/details/Details';
|
||||
import { DetailsImage } from '../../components/details/DetailsImage';
|
||||
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
||||
import { useParameterDetailsGrid } from '../../components/details/ParameterDetailsGrid';
|
||||
import { RenderAddress } from '../../components/render/Company';
|
||||
import { formatCurrency } from '../../defaults/formatters';
|
||||
import { useGlobalSettingsState } from '../../states/SettingsStates';
|
||||
|
||||
export function SalesOrderDetailsPanel({
|
||||
instance,
|
||||
allowImageEdit = false,
|
||||
refreshInstance
|
||||
}: Readonly<{
|
||||
instance: any;
|
||||
allowImageEdit?: boolean;
|
||||
refreshInstance?: () => void;
|
||||
}>) {
|
||||
const globalSettings = useGlobalSettingsState();
|
||||
|
||||
const orderCurrency = useMemo(
|
||||
() =>
|
||||
instance?.order_currency ||
|
||||
instance?.customer_detail?.currency ||
|
||||
globalSettings.getSetting('INVENTREE_DEFAULT_CURRENCY'),
|
||||
[instance, globalSettings]
|
||||
);
|
||||
|
||||
const tl: DetailsField[] = [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'reference',
|
||||
label: t`Reference`,
|
||||
copy: true
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'customer_reference',
|
||||
label: t`Customer Reference`,
|
||||
copy: true,
|
||||
icon: 'reference',
|
||||
hidden: !instance?.customer_reference
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'customer',
|
||||
icon: 'customers',
|
||||
label: t`Customer`,
|
||||
model: ModelType.company
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'description',
|
||||
label: t`Description`,
|
||||
copy: true
|
||||
},
|
||||
{
|
||||
type: 'status',
|
||||
name: 'status',
|
||||
label: t`Status`,
|
||||
model: ModelType.salesorder
|
||||
},
|
||||
{
|
||||
type: 'status',
|
||||
name: 'status_custom_key',
|
||||
label: t`Custom Status`,
|
||||
model: ModelType.salesorder,
|
||||
icon: 'status',
|
||||
hidden:
|
||||
!instance?.status_custom_key ||
|
||||
instance?.status_custom_key == instance?.status
|
||||
}
|
||||
];
|
||||
|
||||
const tr: DetailsField[] = [
|
||||
{
|
||||
type: 'progressbar',
|
||||
name: 'completed',
|
||||
icon: 'progress',
|
||||
label: t`Completed Line Items`,
|
||||
total: instance?.line_items,
|
||||
progress: instance?.completed_lines,
|
||||
hidden: !instance?.line_items
|
||||
},
|
||||
{
|
||||
type: 'progressbar',
|
||||
name: 'shipments',
|
||||
icon: 'shipment',
|
||||
label: t`Completed Shipments`,
|
||||
total: instance?.shipments_count,
|
||||
progress: instance?.completed_shipments_count,
|
||||
hidden: !instance?.shipments_count
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'currency',
|
||||
label: t`Order Currency`,
|
||||
value_formatter: () => orderCurrency
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'total_price',
|
||||
label: t`Total Cost`,
|
||||
value_formatter: () =>
|
||||
formatCurrency(instance?.total_price, {
|
||||
currency: orderCurrency
|
||||
})
|
||||
}
|
||||
];
|
||||
|
||||
const bl: DetailsField[] = [
|
||||
{
|
||||
type: 'link',
|
||||
external: true,
|
||||
name: 'link',
|
||||
label: t`Link`,
|
||||
copy: true,
|
||||
hidden: !instance?.link
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'address',
|
||||
label: t`Shipping Address`,
|
||||
icon: 'address',
|
||||
value_formatter: () =>
|
||||
instance?.address_detail ? (
|
||||
<RenderAddress instance={instance.address_detail} />
|
||||
) : (
|
||||
<Text size='sm' c='red'>{t`Not specified`}</Text>
|
||||
)
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'contact_detail.name',
|
||||
label: t`Contact`,
|
||||
icon: 'user',
|
||||
copy: true,
|
||||
hidden: !instance?.contact_detail?.name
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'contact_detail.email',
|
||||
label: t`Contact Email`,
|
||||
icon: 'email',
|
||||
copy: true,
|
||||
hidden: !instance?.contact_detail?.email
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'contact_detail.phone',
|
||||
label: t`Contact Phone`,
|
||||
icon: 'phone',
|
||||
copy: true,
|
||||
hidden: !instance?.contact_detail?.phone
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'project_code_label',
|
||||
label: t`Project Code`,
|
||||
icon: 'reference',
|
||||
copy: true,
|
||||
hidden: !instance?.project_code_label
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'responsible',
|
||||
label: t`Responsible`,
|
||||
badge: 'owner',
|
||||
hidden: !instance?.responsible
|
||||
}
|
||||
];
|
||||
|
||||
const br: DetailsField[] = [
|
||||
{
|
||||
type: 'date',
|
||||
name: 'creation_date',
|
||||
label: t`Creation Date`,
|
||||
copy: true,
|
||||
hidden: !instance?.creation_date
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'issue_date',
|
||||
label: t`Issue Date`,
|
||||
icon: 'calendar',
|
||||
copy: true,
|
||||
hidden: !instance?.issue_date
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'start_date',
|
||||
label: t`Start Date`,
|
||||
icon: 'calendar',
|
||||
hidden: !instance?.start_date,
|
||||
copy: true
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'target_date',
|
||||
label: t`Target Date`,
|
||||
hidden: !instance?.target_date,
|
||||
copy: true
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'shipment_date',
|
||||
label: t`Completion Date`,
|
||||
hidden: !instance?.shipment_date,
|
||||
copy: true
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'updated_at',
|
||||
label: t`Last Updated`,
|
||||
icon: 'calendar',
|
||||
copy: true,
|
||||
showTime: true,
|
||||
hidden: !instance?.updated_at
|
||||
}
|
||||
];
|
||||
|
||||
const parametersTable = useParameterDetailsGrid({
|
||||
model_type: ModelType.salesorder,
|
||||
model_id: instance?.pk
|
||||
});
|
||||
|
||||
if (!instance?.pk) return <Skeleton />;
|
||||
|
||||
return (
|
||||
<ItemDetailsGrid
|
||||
tables={[
|
||||
{ fields: tr, item: instance },
|
||||
{ fields: bl, item: instance },
|
||||
{ fields: br, item: instance },
|
||||
parametersTable
|
||||
]}
|
||||
>
|
||||
<Stack gap='xs'>
|
||||
<Grid grow>
|
||||
<DetailsImage
|
||||
appRole={allowImageEdit ? UserRoles.sales_order : undefined}
|
||||
imageActions={
|
||||
allowImageEdit ? { uploadFile: true, deleteFile: true } : {}
|
||||
}
|
||||
apiPath={apiUrl(ApiEndpoints.company_list, instance?.customer)}
|
||||
src={instance?.customer_detail?.image}
|
||||
pk={instance?.customer}
|
||||
refresh={refreshInstance}
|
||||
/>
|
||||
<Grid.Col span={{ base: 12, sm: 8 }}>
|
||||
<DetailsTable fields={tl} item={instance} />
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
<TagsList tags={instance?.tags} />
|
||||
</Stack>
|
||||
</ItemDetailsGrid>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { t } from '@lingui/core/macro';
|
||||
import { Grid, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import { Stack } from '@mantine/core';
|
||||
import {
|
||||
IconBookmark,
|
||||
IconCircleCheck,
|
||||
|
|
@ -13,18 +13,11 @@ import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
|||
import { ModelType } from '@lib/enums/ModelType';
|
||||
import { UserRoles } from '@lib/enums/Roles';
|
||||
import { getDetailUrl } from '@lib/functions/Navigation';
|
||||
import { TagsList } from '@lib/index';
|
||||
import type { PanelType } from '@lib/types/Panel';
|
||||
import AdminButton from '../../components/buttons/AdminButton';
|
||||
import PrimaryActionButton from '../../components/buttons/PrimaryActionButton';
|
||||
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 {
|
||||
BarcodeActionDropdown,
|
||||
CancelItemAction,
|
||||
|
|
@ -37,9 +30,6 @@ import AttachmentPanel from '../../components/panels/AttachmentPanel';
|
|||
import NotesPanel from '../../components/panels/NotesPanel';
|
||||
import { PanelGroup } from '../../components/panels/PanelGroup';
|
||||
import ParametersPanel from '../../components/panels/ParametersPanel';
|
||||
import { RenderAddress } from '../../components/render/Company';
|
||||
import { RenderUser } from '../../components/render/User';
|
||||
import { formatDate } from '../../defaults/formatters';
|
||||
import {
|
||||
useCheckShipmentForm,
|
||||
useCompleteShipmentForm,
|
||||
|
|
@ -53,6 +43,7 @@ import {
|
|||
import { useInstance } from '../../hooks/UseInstance';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import SalesOrderAllocationTable from '../../tables/sales/SalesOrderAllocationTable';
|
||||
import { SalesOrderShipmentDetailsPanel } from './SalesOrderShipmentDetailsPanel';
|
||||
|
||||
export default function SalesOrderShipmentDetail() {
|
||||
const { id } = useParams();
|
||||
|
|
@ -74,190 +65,21 @@ export default function SalesOrderShipmentDetail() {
|
|||
}
|
||||
});
|
||||
|
||||
const { instance: customer, instanceQuery: customerQuery } = useInstance({
|
||||
endpoint: ApiEndpoints.company_list,
|
||||
pk: shipment.order_detail?.customer,
|
||||
hasPrimaryKey: true
|
||||
});
|
||||
|
||||
const isPending = useMemo(() => !shipment.shipment_date, [shipment]);
|
||||
|
||||
const isChecked = useMemo(() => !!shipment.checked_by, [shipment]);
|
||||
|
||||
const detailsPanel = useMemo(() => {
|
||||
if (shipmentQuery.isFetching || customerQuery.isFetching) {
|
||||
return <Skeleton />;
|
||||
}
|
||||
|
||||
const data: any = {
|
||||
...shipment,
|
||||
customer: customer?.pk,
|
||||
customer_name: customer?.name,
|
||||
customer_reference: shipment.order_detail?.customer_reference
|
||||
};
|
||||
|
||||
// Top Left: Order / customer information
|
||||
const tl: DetailsField[] = [
|
||||
{
|
||||
type: 'link',
|
||||
model: ModelType.salesorder,
|
||||
name: 'order',
|
||||
label: t`Sales Order`,
|
||||
icon: 'sales_orders',
|
||||
model_field: 'reference'
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'customer',
|
||||
icon: 'customers',
|
||||
label: t`Customer`,
|
||||
model: ModelType.company,
|
||||
model_field: 'name',
|
||||
hidden: !data.customer
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
external: true,
|
||||
name: 'link',
|
||||
label: t`Link`,
|
||||
copy: true,
|
||||
hidden: !shipment.link
|
||||
}
|
||||
];
|
||||
|
||||
// Top right: Shipment information
|
||||
const tr: DetailsField[] = [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'customer_reference',
|
||||
icon: 'serial',
|
||||
label: t`Customer Reference`,
|
||||
hidden: !data.customer_reference,
|
||||
copy: true
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'reference',
|
||||
icon: 'serial',
|
||||
label: t`Shipment Reference`,
|
||||
copy: true
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'tracking_number',
|
||||
label: t`Tracking Number`,
|
||||
icon: 'trackable',
|
||||
value_formatter: () => shipment.tracking_number || '---',
|
||||
copy: !!shipment.tracking_number
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'invoice_number',
|
||||
label: t`Invoice Number`,
|
||||
icon: 'serial',
|
||||
value_formatter: () => shipment.invoice_number || '---',
|
||||
copy: !!shipment.invoice_number
|
||||
}
|
||||
];
|
||||
|
||||
const address: any =
|
||||
shipment.shipment_address_detail || shipment.order_detail?.address_detail;
|
||||
|
||||
const bl: DetailsField[] = [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'address',
|
||||
label: t`Shipping Address`,
|
||||
icon: 'address',
|
||||
value_formatter: () =>
|
||||
address ? (
|
||||
<RenderAddress
|
||||
instance={
|
||||
shipment.shipment_address_detail ||
|
||||
shipment.order_detail?.address_detail
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<Text size='sm' c='red'>{t`Not specified`}</Text>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
const br: DetailsField[] = [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'allocated_items',
|
||||
icon: 'packages',
|
||||
label: t`Allocated Items`
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'checked_by',
|
||||
label: t`Checked By`,
|
||||
icon: 'check',
|
||||
value_formatter: () =>
|
||||
shipment.checked_by_detail ? (
|
||||
<RenderUser instance={shipment.checked_by_detail} />
|
||||
) : (
|
||||
<Text size='sm' c='red'>{t`Not checked`}</Text>
|
||||
)
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'shipment_date',
|
||||
label: t`Shipment Date`,
|
||||
icon: 'calendar',
|
||||
value_formatter: () => formatDate(shipment.shipment_date),
|
||||
hidden: !shipment.shipment_date
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'delivery_date',
|
||||
label: t`Delivery Date`,
|
||||
icon: 'calendar',
|
||||
value_formatter: () => formatDate(shipment.delivery_date),
|
||||
hidden: !shipment.delivery_date
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<ItemDetailsGrid>
|
||||
<Stack gap='xs'>
|
||||
<Grid grow>
|
||||
<DetailsImage
|
||||
appRole={UserRoles.sales_order}
|
||||
apiPath={ApiEndpoints.company_list}
|
||||
src={customer?.image}
|
||||
pk={customer?.pk}
|
||||
imageActions={{
|
||||
selectExisting: false,
|
||||
downloadImage: false,
|
||||
uploadFile: false,
|
||||
deleteFile: false
|
||||
}}
|
||||
/>
|
||||
<Grid.Col span={{ base: 12, sm: 8 }}>
|
||||
<DetailsTable fields={tl} item={data} />
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
<TagsList tags={shipment.tags} />
|
||||
</Stack>
|
||||
<DetailsTable fields={tr} item={data} />
|
||||
<DetailsTable fields={bl} item={data} />
|
||||
<DetailsTable fields={br} item={data} />
|
||||
</ItemDetailsGrid>
|
||||
</>
|
||||
);
|
||||
}, [shipment, shipmentQuery, customer, customerQuery]);
|
||||
|
||||
const shipmentPanels: PanelType[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
name: 'detail',
|
||||
label: t`Shipment Details`,
|
||||
icon: <IconInfoCircle />,
|
||||
content: detailsPanel
|
||||
content: (
|
||||
<SalesOrderShipmentDetailsPanel
|
||||
instance={shipment}
|
||||
refreshInstance={refreshShipment}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'items',
|
||||
|
|
@ -288,7 +110,7 @@ export default function SalesOrderShipmentDetail() {
|
|||
has_note: !!shipment.notes
|
||||
})
|
||||
];
|
||||
}, [isPending, shipment, detailsPanel]);
|
||||
}, [isPending, shipment]);
|
||||
|
||||
const editShipmentFields = useSalesOrderShipmentFields({
|
||||
pending: isPending,
|
||||
|
|
@ -454,7 +276,7 @@ export default function SalesOrderShipmentDetail() {
|
|||
}
|
||||
]}
|
||||
badges={shipmentBadges}
|
||||
imageUrl={customer?.image}
|
||||
imageUrl={shipment.order_detail?.customer_detail?.image}
|
||||
editAction={editShipment.open}
|
||||
editEnabled={user.hasChangePermission(ModelType.salesordershipment)}
|
||||
actions={shipmentActions}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,205 @@
|
|||
import { t } from '@lingui/core/macro';
|
||||
import { Grid, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
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 { TagsList } from '@lib/index';
|
||||
|
||||
import {
|
||||
type DetailsField,
|
||||
DetailsTable
|
||||
} from '../../components/details/Details';
|
||||
import { DetailsImage } from '../../components/details/DetailsImage';
|
||||
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
||||
import { useParameterDetailsGrid } from '../../components/details/ParameterDetailsGrid';
|
||||
import { RenderAddress } from '../../components/render/Company';
|
||||
import { RenderUser } from '../../components/render/User';
|
||||
import { formatDate } from '../../defaults/formatters';
|
||||
import { useInstance } from '../../hooks/UseInstance';
|
||||
|
||||
export function SalesOrderShipmentDetailsPanel({
|
||||
instance,
|
||||
refreshInstance
|
||||
}: Readonly<{
|
||||
instance: any;
|
||||
refreshInstance?: () => void;
|
||||
}>) {
|
||||
const { instance: customer } = useInstance({
|
||||
endpoint: ApiEndpoints.company_list,
|
||||
pk: instance?.order_detail?.customer,
|
||||
hasPrimaryKey: true
|
||||
});
|
||||
|
||||
const data = useMemo(
|
||||
() => ({
|
||||
...instance,
|
||||
customer: customer?.pk,
|
||||
customer_name: customer?.name,
|
||||
customer_reference: instance?.order_detail?.customer_reference
|
||||
}),
|
||||
[instance, customer]
|
||||
);
|
||||
|
||||
const address = useMemo(
|
||||
() =>
|
||||
instance?.shipment_address_detail ||
|
||||
instance?.order_detail?.address_detail,
|
||||
[instance]
|
||||
);
|
||||
|
||||
const tl: DetailsField[] = [
|
||||
{
|
||||
type: 'link',
|
||||
model: ModelType.salesorder,
|
||||
name: 'order',
|
||||
label: t`Sales Order`,
|
||||
icon: 'sales_orders',
|
||||
model_field: 'reference'
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'customer',
|
||||
icon: 'customers',
|
||||
label: t`Customer`,
|
||||
model: ModelType.company,
|
||||
model_field: 'name',
|
||||
hidden: !data.customer
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
external: true,
|
||||
name: 'link',
|
||||
label: t`Link`,
|
||||
copy: true,
|
||||
hidden: !instance?.link
|
||||
}
|
||||
];
|
||||
|
||||
const tr: DetailsField[] = [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'customer_reference',
|
||||
icon: 'serial',
|
||||
label: t`Customer Reference`,
|
||||
hidden: !data.customer_reference,
|
||||
copy: true
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'reference',
|
||||
icon: 'serial',
|
||||
label: t`Shipment Reference`,
|
||||
copy: true
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'tracking_number',
|
||||
label: t`Tracking Number`,
|
||||
icon: 'trackable',
|
||||
value_formatter: () => instance?.tracking_number || '---',
|
||||
copy: !!instance?.tracking_number
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'invoice_number',
|
||||
label: t`Invoice Number`,
|
||||
icon: 'serial',
|
||||
value_formatter: () => instance?.invoice_number || '---',
|
||||
copy: !!instance?.invoice_number
|
||||
}
|
||||
];
|
||||
|
||||
const bl: DetailsField[] = [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'address',
|
||||
label: t`Shipping Address`,
|
||||
icon: 'address',
|
||||
value_formatter: () =>
|
||||
address ? (
|
||||
<RenderAddress instance={address} />
|
||||
) : (
|
||||
<Text size='sm' c='red'>{t`Not specified`}</Text>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
const br: DetailsField[] = [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'allocated_items',
|
||||
icon: 'packages',
|
||||
label: t`Allocated Items`
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'checked_by',
|
||||
label: t`Checked By`,
|
||||
icon: 'check',
|
||||
value_formatter: () =>
|
||||
instance?.checked_by_detail ? (
|
||||
<RenderUser instance={instance.checked_by_detail} />
|
||||
) : (
|
||||
<Text size='sm' c='red'>{t`Not checked`}</Text>
|
||||
)
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'shipment_date',
|
||||
label: t`Shipment Date`,
|
||||
icon: 'calendar',
|
||||
value_formatter: () => formatDate(instance?.shipment_date),
|
||||
hidden: !instance?.shipment_date
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'delivery_date',
|
||||
label: t`Delivery Date`,
|
||||
icon: 'calendar',
|
||||
value_formatter: () => formatDate(instance?.delivery_date),
|
||||
hidden: !instance?.delivery_date
|
||||
}
|
||||
];
|
||||
|
||||
const parametersTable = useParameterDetailsGrid({
|
||||
model_type: ModelType.salesordershipment,
|
||||
model_id: instance?.pk
|
||||
});
|
||||
|
||||
if (!instance?.pk) return <Skeleton />;
|
||||
|
||||
return (
|
||||
<ItemDetailsGrid
|
||||
tables={[
|
||||
{ fields: tr, item: data },
|
||||
{ fields: bl, item: data },
|
||||
{ fields: br, item: data },
|
||||
parametersTable
|
||||
]}
|
||||
>
|
||||
<Stack gap='xs'>
|
||||
<Grid grow>
|
||||
<DetailsImage
|
||||
appRole={UserRoles.sales_order}
|
||||
apiPath={apiUrl(ApiEndpoints.company_list, customer?.pk)}
|
||||
src={customer?.image}
|
||||
pk={customer?.pk}
|
||||
imageActions={{
|
||||
selectExisting: false,
|
||||
downloadImage: false,
|
||||
uploadFile: false,
|
||||
deleteFile: false
|
||||
}}
|
||||
/>
|
||||
<Grid.Col span={{ base: 12, sm: 8 }}>
|
||||
<DetailsTable fields={tl} item={data} />
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
<TagsList tags={instance?.tags} />
|
||||
</Stack>
|
||||
</ItemDetailsGrid>
|
||||
);
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@ import type { TableFilter } from '@lib/index';
|
|||
import type { StockOperationProps } from '@lib/types/Forms';
|
||||
import type { PanelType } from '@lib/types/Panel';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Group, Skeleton, Stack } from '@mantine/core';
|
||||
import { Skeleton, Stack } from '@mantine/core';
|
||||
import {
|
||||
IconCalendar,
|
||||
IconInfoCircle,
|
||||
|
|
@ -24,11 +24,6 @@ import { useBarcodeScanDialog } from '../../components/barcodes/BarcodeScanDialo
|
|||
import AdminButton from '../../components/buttons/AdminButton';
|
||||
import { PrintingActions } from '../../components/buttons/PrintingActions';
|
||||
import OrderCalendar from '../../components/calendar/OrderCalendar';
|
||||
import {
|
||||
type DetailsField,
|
||||
DetailsTable
|
||||
} from '../../components/details/Details';
|
||||
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
||||
import {
|
||||
BarcodeActionDropdown,
|
||||
DeleteItemAction,
|
||||
|
|
@ -60,6 +55,7 @@ import StockLocationParametricTable from '../../tables/stock/StockLocationParame
|
|||
import { StockLocationTable } from '../../tables/stock/StockLocationTable';
|
||||
import TransferOrderParametricTable from '../../tables/stock/TransferOrderParametricTable';
|
||||
import { TransferOrderTable } from '../../tables/stock/TransferOrderTable';
|
||||
import { StockLocationDetailsPanel } from './StockLocationDetailsPanel';
|
||||
|
||||
function TransferOrderCalendar() {
|
||||
const calendarFilters: TableFilter[] = useMemo(() => {
|
||||
|
|
@ -105,92 +101,12 @@ export default function Stock() {
|
|||
}
|
||||
});
|
||||
|
||||
const detailsPanel = useMemo(() => {
|
||||
if (id && instanceQuery.isFetching) {
|
||||
return <Skeleton />;
|
||||
}
|
||||
|
||||
const left: DetailsField[] = [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
label: t`Name`,
|
||||
copy: true,
|
||||
value_formatter: () => (
|
||||
<Group gap='xs'>
|
||||
{location.icon && <ApiIcon name={location.icon} />}
|
||||
{location.name}
|
||||
</Group>
|
||||
)
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'pathstring',
|
||||
label: t`Path`,
|
||||
icon: 'sitemap',
|
||||
copy: true,
|
||||
hidden: !id
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'description',
|
||||
label: t`Description`,
|
||||
copy: true
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'parent',
|
||||
model_field: 'name',
|
||||
icon: 'location',
|
||||
label: t`Parent Location`,
|
||||
model: ModelType.stocklocation,
|
||||
hidden: !location?.parent
|
||||
}
|
||||
];
|
||||
|
||||
const right: DetailsField[] = [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'items',
|
||||
icon: 'stock',
|
||||
label: t`Stock Items`,
|
||||
value_formatter: () => location?.items || '0'
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'sublocations',
|
||||
icon: 'location',
|
||||
label: t`Sublocations`,
|
||||
hidden: !location?.sublocations
|
||||
},
|
||||
{
|
||||
type: 'boolean',
|
||||
name: 'structural',
|
||||
label: t`Structural`,
|
||||
icon: 'sitemap'
|
||||
},
|
||||
{
|
||||
type: 'boolean',
|
||||
name: 'external',
|
||||
label: t`External`
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
// TODO: render location type icon here (ref: #7237)
|
||||
name: 'location_type_detail.name',
|
||||
label: t`Location Type`,
|
||||
hidden: !location?.location_type,
|
||||
icon: 'packages'
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<ItemDetailsGrid>
|
||||
{id && location?.pk && <DetailsTable item={location} fields={left} />}
|
||||
{id && location?.pk && <DetailsTable item={location} fields={right} />}
|
||||
</ItemDetailsGrid>
|
||||
const detailsPanel =
|
||||
id && instanceQuery.isFetching ? (
|
||||
<Skeleton />
|
||||
) : (
|
||||
<StockLocationDetailsPanel instance={id ? location : undefined} />
|
||||
);
|
||||
}, [location, instanceQuery]);
|
||||
|
||||
const [sublocationView, setSublocationView] = useState<string>('table');
|
||||
const [transferOrderView, setTransferOrderView] = useState<string>('table');
|
||||
|
|
|
|||
|
|
@ -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,367 +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',
|
||||
hidden: stockitem.in_stock == false
|
||||
},
|
||||
{
|
||||
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 (
|
||||
|
|
@ -555,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',
|
||||
|
|
@ -686,8 +318,6 @@ export default function StockDetail() {
|
|||
showBuildAllocations,
|
||||
showInstalledItems,
|
||||
stockitem,
|
||||
serialNumbers,
|
||||
serialNumbersQuery,
|
||||
id,
|
||||
user
|
||||
]);
|
||||
|
|
@ -1063,7 +693,6 @@ export default function StockDetail() {
|
|||
|
||||
return (
|
||||
<>
|
||||
{findBySerialNumber.modal}
|
||||
{scanIntoLocation.dialog}
|
||||
<InstanceDetail
|
||||
query={instanceQuery}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,413 @@
|
|||
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
|
||||
tables={[
|
||||
{ fields: tr, item: data },
|
||||
{ fields: bl, item: data },
|
||||
{ fields: br, item: data }
|
||||
]}
|
||||
>
|
||||
<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>
|
||||
</ItemDetailsGrid>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
import { t } from '@lingui/core/macro';
|
||||
import { Group, Skeleton } from '@mantine/core';
|
||||
|
||||
import { ModelType } from '@lib/enums/ModelType';
|
||||
|
||||
import type { DetailsField } from '../../components/details/Details';
|
||||
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
||||
import { useParameterDetailsGrid } from '../../components/details/ParameterDetailsGrid';
|
||||
import { ApiIcon } from '../../components/items/ApiIcon';
|
||||
|
||||
export function StockLocationDetailsPanel({
|
||||
instance
|
||||
}: Readonly<{
|
||||
instance: any;
|
||||
}>) {
|
||||
const left: DetailsField[] = [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
label: t`Name`,
|
||||
copy: true,
|
||||
value_formatter: () => (
|
||||
<Group gap='xs'>
|
||||
{instance?.icon && <ApiIcon name={instance.icon} />}
|
||||
{instance?.name}
|
||||
</Group>
|
||||
)
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'pathstring',
|
||||
label: t`Path`,
|
||||
icon: 'sitemap',
|
||||
copy: true,
|
||||
hidden: !instance?.pathstring
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'description',
|
||||
label: t`Description`,
|
||||
copy: true,
|
||||
hidden: !instance?.description
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'parent',
|
||||
model_field: 'name',
|
||||
icon: 'location',
|
||||
label: t`Parent Location`,
|
||||
model: ModelType.stocklocation,
|
||||
hidden: !instance?.parent
|
||||
}
|
||||
];
|
||||
|
||||
const right: DetailsField[] = [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'items',
|
||||
icon: 'stock',
|
||||
label: t`Stock Items`,
|
||||
value_formatter: () => instance?.items || '0'
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'sublocations',
|
||||
icon: 'location',
|
||||
label: t`Sublocations`,
|
||||
hidden: !instance?.sublocations
|
||||
},
|
||||
{
|
||||
type: 'boolean',
|
||||
name: 'structural',
|
||||
label: t`Structural`,
|
||||
icon: 'sitemap'
|
||||
},
|
||||
{
|
||||
type: 'boolean',
|
||||
name: 'external',
|
||||
label: t`External`
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'location_type_detail.name',
|
||||
label: t`Location Type`,
|
||||
hidden: !instance?.location_type,
|
||||
icon: 'packages'
|
||||
}
|
||||
];
|
||||
|
||||
const parametersTable = useParameterDetailsGrid({
|
||||
model_type: ModelType.stocklocation,
|
||||
model_id: instance?.pk
|
||||
});
|
||||
|
||||
if (!instance?.pk) return <Skeleton />;
|
||||
|
||||
return (
|
||||
<ItemDetailsGrid
|
||||
tables={[
|
||||
{ item: instance, fields: left },
|
||||
{ item: instance, fields: right },
|
||||
parametersTable
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
import { t } from '@lingui/core/macro';
|
||||
import { Grid, Skeleton, Stack } from '@mantine/core';
|
||||
import { Skeleton, Stack } from '@mantine/core';
|
||||
import { type ReactNode, useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||
import { ModelType } from '@lib/enums/ModelType';
|
||||
import { UserRoles } from '@lib/enums/Roles';
|
||||
import { type PanelType, TagsList, apiUrl } from '@lib/index';
|
||||
import { type PanelType, apiUrl } from '@lib/index';
|
||||
import {
|
||||
IconBookmark,
|
||||
IconInfoCircle,
|
||||
|
|
@ -16,11 +16,6 @@ import {
|
|||
import AdminButton from '../../components/buttons/AdminButton';
|
||||
import PrimaryActionButton from '../../components/buttons/PrimaryActionButton';
|
||||
import { PrintingActions } from '../../components/buttons/PrintingActions';
|
||||
import {
|
||||
type DetailsField,
|
||||
DetailsTable
|
||||
} from '../../components/details/Details';
|
||||
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
||||
import {
|
||||
BarcodeActionDropdown,
|
||||
CancelItemAction,
|
||||
|
|
@ -47,6 +42,7 @@ import { useGlobalSettingsState } from '../../states/SettingsStates';
|
|||
import { useUserState } from '../../states/UserState';
|
||||
import TransferOrderAllocationTable from '../../tables/stock/TransferOrderAllocationTable';
|
||||
import TransferOrderLineItemTable from '../../tables/stock/TransferOrderLineItemTable';
|
||||
import { TransferOrderDetailsPanel } from './TransferOrderDetailsPanel';
|
||||
|
||||
export default function TransferOrderDetail() {
|
||||
const { id } = useParams();
|
||||
|
|
@ -93,169 +89,11 @@ export default function TransferOrderDetail() {
|
|||
);
|
||||
}, [order, toStatus]);
|
||||
|
||||
const detailsPanel = useMemo(() => {
|
||||
if (instanceQuery.isFetching) {
|
||||
return <Skeleton />;
|
||||
}
|
||||
|
||||
const tl: DetailsField[] = [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'reference',
|
||||
label: t`Reference`,
|
||||
copy: true
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'take_from',
|
||||
icon: 'location',
|
||||
label: t`Source Location`,
|
||||
model: ModelType.stocklocation
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'destination',
|
||||
icon: 'location',
|
||||
label: t`Destination Location`,
|
||||
model: ModelType.stocklocation
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'description',
|
||||
label: t`Description`,
|
||||
copy: true
|
||||
},
|
||||
{
|
||||
type: 'status',
|
||||
name: 'status',
|
||||
label: t`Status`,
|
||||
model: ModelType.transferorder
|
||||
},
|
||||
{
|
||||
type: 'status',
|
||||
name: 'status_custom_key',
|
||||
label: t`Custom Status`,
|
||||
model: ModelType.transferorder,
|
||||
icon: 'status',
|
||||
hidden:
|
||||
!order.status_custom_key || order.status_custom_key == order.status
|
||||
}
|
||||
];
|
||||
|
||||
const tr: DetailsField[] = [
|
||||
{
|
||||
type: 'boolean',
|
||||
name: 'consume',
|
||||
icon: 'consume',
|
||||
label: t`Consume Stock`
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'line_items',
|
||||
label: t`Line Items`,
|
||||
icon: 'list'
|
||||
},
|
||||
{
|
||||
type: 'progressbar',
|
||||
name: 'completed',
|
||||
icon: 'progress',
|
||||
label: t`Completed Line Items`,
|
||||
total: order.line_items,
|
||||
progress: order.completed_lines
|
||||
}
|
||||
];
|
||||
|
||||
const bl: DetailsField[] = [
|
||||
{
|
||||
type: 'link',
|
||||
external: true,
|
||||
name: 'link',
|
||||
label: t`Link`,
|
||||
copy: true,
|
||||
hidden: !order.link
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'project_code_label',
|
||||
label: t`Project Code`,
|
||||
icon: 'reference',
|
||||
copy: true,
|
||||
hidden: !order.project_code
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'responsible',
|
||||
label: t`Responsible`,
|
||||
badge: 'owner',
|
||||
hidden: !order.responsible
|
||||
}
|
||||
];
|
||||
|
||||
const br: DetailsField[] = [
|
||||
{
|
||||
type: 'date',
|
||||
name: 'creation_date',
|
||||
label: t`Creation Date`,
|
||||
icon: 'calendar',
|
||||
copy: true,
|
||||
hidden: !order.creation_date
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'issue_date',
|
||||
label: t`Issue Date`,
|
||||
icon: 'calendar',
|
||||
copy: true,
|
||||
hidden: !order.issue_date
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'start_date',
|
||||
label: t`Start Date`,
|
||||
icon: 'calendar',
|
||||
copy: true,
|
||||
hidden: !order.start_date
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'target_date',
|
||||
label: t`Target Date`,
|
||||
copy: true,
|
||||
hidden: !order.target_date
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'complete_date',
|
||||
icon: 'calendar_check',
|
||||
label: t`Completion Date`,
|
||||
copy: true,
|
||||
hidden: !order.complete_date
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<ItemDetailsGrid>
|
||||
<Stack gap='xs'>
|
||||
<Grid grow>
|
||||
{/* TODO: what image do we show for a Transfer Order? */}
|
||||
{/* <DetailsImage
|
||||
appRole={UserRoles.transfer_order}
|
||||
apiPath={ApiEndpoints.transfer_order_list}
|
||||
src="/static/img/blank_image.png"
|
||||
pk={order.pk}
|
||||
/> */}
|
||||
<Grid.Col span={{ base: 12, sm: 8 }}>
|
||||
<DetailsTable fields={tl} item={order} />
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
<TagsList tags={order.tags} />
|
||||
</Stack>
|
||||
<DetailsTable fields={tr} item={order} />
|
||||
<DetailsTable fields={bl} item={order} />
|
||||
<DetailsTable fields={br} item={order} />
|
||||
</ItemDetailsGrid>
|
||||
);
|
||||
}, [order, instanceQuery]);
|
||||
const detailsPanel = instanceQuery.isFetching ? (
|
||||
<Skeleton />
|
||||
) : (
|
||||
<TransferOrderDetailsPanel instance={order} />
|
||||
);
|
||||
|
||||
const orderPanels: PanelType[] = useMemo(() => {
|
||||
return [
|
||||
|
|
|
|||
|
|
@ -0,0 +1,181 @@
|
|||
import { t } from '@lingui/core/macro';
|
||||
import { Grid, Skeleton, Stack } from '@mantine/core';
|
||||
|
||||
import { ModelType } from '@lib/enums/ModelType';
|
||||
import { TagsList } from '@lib/index';
|
||||
|
||||
import {
|
||||
type DetailsField,
|
||||
DetailsTable
|
||||
} from '../../components/details/Details';
|
||||
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
||||
import { useParameterDetailsGrid } from '../../components/details/ParameterDetailsGrid';
|
||||
|
||||
export function TransferOrderDetailsPanel({
|
||||
instance
|
||||
}: Readonly<{
|
||||
instance: any;
|
||||
}>) {
|
||||
const tl: DetailsField[] = [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'reference',
|
||||
label: t`Reference`,
|
||||
copy: true
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'take_from',
|
||||
icon: 'location',
|
||||
label: t`Source Location`,
|
||||
model: ModelType.stocklocation
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
name: 'destination',
|
||||
icon: 'location',
|
||||
label: t`Destination Location`,
|
||||
model: ModelType.stocklocation
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'description',
|
||||
label: t`Description`,
|
||||
copy: true
|
||||
},
|
||||
{
|
||||
type: 'status',
|
||||
name: 'status',
|
||||
label: t`Status`,
|
||||
model: ModelType.transferorder
|
||||
},
|
||||
{
|
||||
type: 'status',
|
||||
name: 'status_custom_key',
|
||||
label: t`Custom Status`,
|
||||
model: ModelType.transferorder,
|
||||
icon: 'status',
|
||||
hidden:
|
||||
!instance?.status_custom_key ||
|
||||
instance?.status_custom_key == instance?.status
|
||||
}
|
||||
];
|
||||
|
||||
const tr: DetailsField[] = [
|
||||
{
|
||||
type: 'boolean',
|
||||
name: 'consume',
|
||||
icon: 'consume',
|
||||
label: t`Consume Stock`
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'line_items',
|
||||
label: t`Line Items`,
|
||||
icon: 'list'
|
||||
},
|
||||
{
|
||||
type: 'progressbar',
|
||||
name: 'completed',
|
||||
icon: 'progress',
|
||||
label: t`Completed Line Items`,
|
||||
total: instance?.line_items,
|
||||
progress: instance?.completed_lines
|
||||
}
|
||||
];
|
||||
|
||||
const bl: DetailsField[] = [
|
||||
{
|
||||
type: 'link',
|
||||
external: true,
|
||||
name: 'link',
|
||||
label: t`Link`,
|
||||
copy: true,
|
||||
hidden: !instance?.link
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'project_code_label',
|
||||
label: t`Project Code`,
|
||||
icon: 'reference',
|
||||
copy: true,
|
||||
hidden: !instance?.project_code
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'responsible',
|
||||
label: t`Responsible`,
|
||||
badge: 'owner',
|
||||
hidden: !instance?.responsible
|
||||
}
|
||||
];
|
||||
|
||||
const br: DetailsField[] = [
|
||||
{
|
||||
type: 'date',
|
||||
name: 'creation_date',
|
||||
label: t`Creation Date`,
|
||||
icon: 'calendar',
|
||||
copy: true,
|
||||
hidden: !instance?.creation_date
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'issue_date',
|
||||
label: t`Issue Date`,
|
||||
icon: 'calendar',
|
||||
copy: true,
|
||||
hidden: !instance?.issue_date
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'start_date',
|
||||
label: t`Start Date`,
|
||||
icon: 'calendar',
|
||||
copy: true,
|
||||
hidden: !instance?.start_date
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'target_date',
|
||||
label: t`Target Date`,
|
||||
copy: true,
|
||||
hidden: !instance?.target_date
|
||||
},
|
||||
{
|
||||
type: 'date',
|
||||
name: 'complete_date',
|
||||
icon: 'calendar_check',
|
||||
label: t`Completion Date`,
|
||||
copy: true,
|
||||
hidden: !instance?.complete_date
|
||||
}
|
||||
];
|
||||
|
||||
const parametersTable = useParameterDetailsGrid({
|
||||
model_type: ModelType.transferorder,
|
||||
model_id: instance?.pk
|
||||
});
|
||||
|
||||
if (!instance?.pk) return <Skeleton />;
|
||||
|
||||
return (
|
||||
<ItemDetailsGrid
|
||||
tables={[
|
||||
{ fields: tr, item: instance },
|
||||
{ fields: bl, item: instance },
|
||||
{ fields: br, item: instance },
|
||||
parametersTable
|
||||
]}
|
||||
>
|
||||
<Stack gap='xs'>
|
||||
<Grid grow>
|
||||
<Grid.Col span={{ base: 12, sm: 8 }}>
|
||||
<DetailsTable fields={tl} item={instance} />
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
<TagsList tags={instance.tags} />
|
||||
</Stack>
|
||||
</ItemDetailsGrid>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
import type { ModelType } from '@lib/enums/ModelType';
|
||||
import { create } from 'zustand';
|
||||
import type { PreviewType } from '../components/previews/PreviewType';
|
||||
|
||||
interface PreviewDrawerStateProps {
|
||||
isOpen: boolean;
|
||||
modelType?: ModelType;
|
||||
id?: number | string;
|
||||
instance?: any;
|
||||
preview?: PreviewType;
|
||||
openPreview: (
|
||||
modelType: ModelType,
|
||||
id: number | string | undefined,
|
||||
instance?: any,
|
||||
preview?: PreviewType
|
||||
) => void;
|
||||
closePreview: () => void;
|
||||
}
|
||||
|
||||
export const usePreviewDrawerState = create<PreviewDrawerStateProps>()(
|
||||
(set) => ({
|
||||
isOpen: false,
|
||||
modelType: undefined,
|
||||
id: undefined,
|
||||
instance: undefined,
|
||||
preview: undefined,
|
||||
|
||||
openPreview: (
|
||||
modelType: ModelType,
|
||||
id: number | string | undefined,
|
||||
instance?: any,
|
||||
preview?: PreviewType
|
||||
) => {
|
||||
set({ modelType, id, instance, preview, isOpen: true });
|
||||
},
|
||||
|
||||
closePreview: () => {
|
||||
set({
|
||||
isOpen: false,
|
||||
instance: undefined,
|
||||
id: undefined,
|
||||
preview: undefined
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
export function openGlobalPreview(
|
||||
modelType: ModelType,
|
||||
id?: number | string | undefined,
|
||||
instance?: any,
|
||||
preview?: PreviewType
|
||||
) {
|
||||
usePreviewDrawerState
|
||||
.getState()
|
||||
.openPreview(modelType, id, instance, preview);
|
||||
}
|
||||
|
||||
export function closeGlobalPreview() {
|
||||
usePreviewDrawerState.getState().closePreview();
|
||||
}
|
||||
|
||||
export function getGlobalPreviewState() {
|
||||
return usePreviewDrawerState.getState();
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ import { ModelInformationDict } from '@lib/enums/ModelInformation';
|
|||
import { resolveItem } from '@lib/functions/Conversion';
|
||||
import { cancelEvent } from '@lib/functions/Events';
|
||||
import { mapFields } from '@lib/functions/Forms';
|
||||
import { getDetailUrl } from '@lib/functions/Navigation';
|
||||
import { eventModified, getDetailUrl } from '@lib/functions/Navigation';
|
||||
import { navigateToLink } from '@lib/functions/Navigation';
|
||||
import { useStoredTableState } from '@lib/states/StoredTableState';
|
||||
import type { TableFilter } from '@lib/types/Filters';
|
||||
|
|
@ -37,6 +37,7 @@ import { useApi } from '../contexts/ApiContext';
|
|||
import { extractAvailableFields } from '../functions/forms';
|
||||
import { showApiErrorMessage } from '../functions/notifications';
|
||||
import { useLocalState } from '../states/LocalState';
|
||||
import { usePreviewDrawerState } from '../states/PreviewDrawerState';
|
||||
import { useUserSettingsState } from '../states/SettingsStates';
|
||||
import { ColumnFilterPopover } from './FilterSelectDrawer';
|
||||
import InvenTreeTableHeader from './InvenTreeTableHeader';
|
||||
|
|
@ -700,6 +701,18 @@ export function InvenTreeTableInternal<T extends Record<string, any>>({
|
|||
tableState.setRecords(tableData ?? apiData ?? []);
|
||||
}, [tableData, apiData]);
|
||||
|
||||
const previewDrawer = usePreviewDrawerState();
|
||||
|
||||
// Callback to display "preview" view for a row (if available)
|
||||
const showRowPreview = useCallback(
|
||||
(pk: string | number) => {
|
||||
if (tableProps.modelType && pk) {
|
||||
previewDrawer.openPreview(tableProps.modelType, Number(pk));
|
||||
}
|
||||
},
|
||||
[tableProps.modelType]
|
||||
);
|
||||
|
||||
// Callback when a cell is clicked
|
||||
const handleCellClick = useCallback(
|
||||
({
|
||||
|
|
@ -732,7 +745,12 @@ export function InvenTreeTableInternal<T extends Record<string, any>>({
|
|||
cancelEvent(event);
|
||||
// If a model type is provided, navigate to the detail view for that model
|
||||
const url = getDetailUrl(tableProps.modelType, pk);
|
||||
navigateToLink(url, navigate, event);
|
||||
|
||||
if (eventModified(event as any)) {
|
||||
navigateToLink(url, navigate, event);
|
||||
} else {
|
||||
showRowPreview(pk);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -796,7 +814,11 @@ export function InvenTreeTableInternal<T extends Record<string, any>>({
|
|||
icon: <IconArrowRight />,
|
||||
onClick: (event: any) => {
|
||||
cancelEvent(event);
|
||||
navigateToLink(url, navigate, event);
|
||||
if (eventModified(event as any)) {
|
||||
navigateToLink(url, navigate, event);
|
||||
} else {
|
||||
showRowPreview(pk);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ 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 { navigateToLink } from '@lib/functions/Navigation';
|
||||
import { eventModified, navigateToLink } from '@lib/functions/Navigation';
|
||||
import useTable from '@lib/hooks/UseTable';
|
||||
import type { TableFilter } from '@lib/types/Filters';
|
||||
import { companyFields } from '../../forms/CompanyForms';
|
||||
|
|
@ -16,6 +16,7 @@ import {
|
|||
useCreateApiFormModal,
|
||||
useEditApiFormModal
|
||||
} from '../../hooks/UseForm';
|
||||
import { usePreviewDrawerState } from '../../states/PreviewDrawerState';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import {
|
||||
BooleanColumn,
|
||||
|
|
@ -49,6 +50,7 @@ export function CompanyTable({
|
|||
|
||||
const navigate = useNavigate();
|
||||
const user = useUserState();
|
||||
const previewDrawer = usePreviewDrawerState();
|
||||
|
||||
const columns = useMemo(() => {
|
||||
return [
|
||||
|
|
@ -166,10 +168,16 @@ export function CompanyTable({
|
|||
params: {
|
||||
...params
|
||||
},
|
||||
onRowClick: (record: any, index: number, event: any) => {
|
||||
if (record.pk) {
|
||||
const base = path ?? 'company';
|
||||
navigateToLink(`/${base}/${record.pk}`, navigate, event);
|
||||
onRowClick: (record: any, _index: number, event: any) => {
|
||||
if (!record.pk) return;
|
||||
if (eventModified(event as any)) {
|
||||
navigateToLink(
|
||||
`/${path ?? 'company'}/${record.pk}`,
|
||||
navigate,
|
||||
event
|
||||
);
|
||||
} else {
|
||||
previewDrawer.openPreview(ModelType.company, Number(record.pk));
|
||||
}
|
||||
},
|
||||
modelType: ModelType.company,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import { cancelEvent } from '@lib/functions/Events';
|
||||
import { eventModified } from '@lib/functions/Navigation';
|
||||
import { getDetailUrl, navigateToLink } from '@lib/functions/Navigation';
|
||||
import useTable from '@lib/hooks/UseTable';
|
||||
import {
|
||||
ApiEndpoints,
|
||||
|
|
@ -7,9 +9,7 @@ import {
|
|||
UserRoles,
|
||||
YesNoButton,
|
||||
apiUrl,
|
||||
formatDecimal,
|
||||
getDetailUrl,
|
||||
navigateToLink
|
||||
formatDecimal
|
||||
} from '@lib/index';
|
||||
import type { TableFilter } from '@lib/types/Filters';
|
||||
import type { TableColumn } from '@lib/types/Tables';
|
||||
|
|
@ -27,6 +27,7 @@ import {
|
|||
useCreateApiFormModal,
|
||||
useEditApiFormModal
|
||||
} from '../../hooks/UseForm';
|
||||
import { openGlobalPreview } from '../../states/PreviewDrawerState';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import { InvenTreeTable } from '../InvenTreeTable';
|
||||
import { TableHoverCard } from '../TableHoverCard';
|
||||
|
|
@ -472,9 +473,12 @@ export default function ParametricDataTable({
|
|||
const col = column as any;
|
||||
onParameterClick(col.extra.template, record);
|
||||
} else if (record?.pk) {
|
||||
// Navigate through to the detail page
|
||||
const url = getDetailUrl(modelType, record.pk);
|
||||
navigateToLink(url, navigate, event);
|
||||
if (eventModified(event as any)) {
|
||||
navigateToLink(url, navigate, event);
|
||||
} else {
|
||||
openGlobalPreview(modelType, record.pk);
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
|
|
|
|||
Loading…
Reference in New Issue