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("BARCODE_IN_FORM_FIELDS") }}
|
||||||
{{ usersetting("DATE_DISPLAY_FORMAT") }}
|
{{ usersetting("DATE_DISPLAY_FORMAT") }}
|
||||||
{{ usersetting("FORMS_CLOSE_USING_ESCAPE") }}
|
{{ usersetting("FORMS_CLOSE_USING_ESCAPE") }}
|
||||||
|
{{ usersetting("ENABLE_PREVIEW_PANEL") }}
|
||||||
{{ usersetting("DISPLAY_STOCKTAKE_TAB") }}
|
{{ usersetting("DISPLAY_STOCKTAKE_TAB") }}
|
||||||
{{ usersetting("SHOW_FULL_CATEGORY_IN_TABLES")}}
|
{{ usersetting("SHOW_FULL_CATEGORY_IN_TABLES")}}
|
||||||
{{ usersetting("SHOW_BOM_SUBASSEMBLY_LEVELS")}}
|
{{ usersetting("SHOW_BOM_SUBASSEMBLY_LEVELS")}}
|
||||||
|
|
|
||||||
|
|
@ -223,6 +223,12 @@ USER_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
|
||||||
('MMM DD YYYY', 'Feb 22 2022'),
|
('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': {
|
'DISPLAY_STOCKTAKE_TAB': {
|
||||||
'name': _('Show Stock History'),
|
'name': _('Show Stock History'),
|
||||||
'description': _('Display stock history information in the part detail page'),
|
'description': _('Display stock history information in the part detail page'),
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,12 @@ export type ImporterDrawerContext = {
|
||||||
sessionId: () => number | null;
|
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,
|
* A set of properties which are passed to a plugin,
|
||||||
* for rendering an element in the user interface.
|
* 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 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 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 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 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 id - The ID (primary key) of the model instance for the plugin (if applicable)
|
||||||
* @param instance - The model instance data (if available)
|
* @param instance - The model instance data (if available)
|
||||||
|
|
@ -125,6 +132,7 @@ export type InvenTreePluginContext = {
|
||||||
};
|
};
|
||||||
tables: InvenTreeTablesContext<any>;
|
tables: InvenTreeTablesContext<any>;
|
||||||
importer: ImporterDrawerContext;
|
importer: ImporterDrawerContext;
|
||||||
|
preview: PreviewDrawerContext;
|
||||||
model?: ModelType | string;
|
model?: ModelType | string;
|
||||||
id?: string | number | null;
|
id?: string | number | null;
|
||||||
instance?: any;
|
instance?: any;
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,11 @@ import { ModelInformationDict } from '@lib/enums/ModelInformation';
|
||||||
import type { ModelType } from '@lib/enums/ModelType';
|
import type { ModelType } from '@lib/enums/ModelType';
|
||||||
import type { UserRoles } from '@lib/enums/Roles';
|
import type { UserRoles } from '@lib/enums/Roles';
|
||||||
import { apiUrl } from '@lib/functions/Api';
|
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 type { TableFilter } from '@lib/types/Filters';
|
||||||
import { t } from '@lingui/core/macro';
|
import { t } from '@lingui/core/macro';
|
||||||
import { ActionIcon, Group, Text } from '@mantine/core';
|
import { ActionIcon, Group, Text } from '@mantine/core';
|
||||||
|
|
@ -22,6 +26,7 @@ import { useCallback, useMemo } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { api } from '../../App';
|
import { api } from '../../App';
|
||||||
import useCalendar from '../../hooks/UseCalendar';
|
import useCalendar from '../../hooks/UseCalendar';
|
||||||
|
import { openGlobalPreview } from '../../states/PreviewDrawerState';
|
||||||
import { useUserState } from '../../states/UserState';
|
import { useUserState } from '../../states/UserState';
|
||||||
import {
|
import {
|
||||||
AssignedToMeFilter,
|
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) => {
|
const onClickOrder = (info: EventClickArg) => {
|
||||||
if (!!info.event.id) {
|
if (!info.event.id) return;
|
||||||
|
|
||||||
|
if (eventModified(event as any)) {
|
||||||
navigateToLink(
|
navigateToLink(
|
||||||
getDetailUrl(model, info.event.id),
|
getDetailUrl(model, info.event.id),
|
||||||
navigate,
|
navigate,
|
||||||
info.jsEvent
|
info.jsEvent
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
openGlobalPreview(model, Number.parseInt(info.event.id));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -280,7 +280,7 @@ function NumberValue(props: Readonly<FieldProps>) {
|
||||||
const value = props?.field_value;
|
const value = props?.field_value;
|
||||||
|
|
||||||
// Convert to double
|
// Convert to double
|
||||||
const numberValue = Number.parseFloat(value.toString());
|
const numberValue = Number.parseFloat(value?.toString() ?? '');
|
||||||
|
|
||||||
if (value === null || value === undefined) {
|
if (value === null || value === undefined) {
|
||||||
return <Text size='sm'>'---'</Text>;
|
return <Text size='sm'>'---'</Text>;
|
||||||
|
|
@ -465,10 +465,12 @@ function CopyField({ value }: Readonly<{ value: string }>) {
|
||||||
|
|
||||||
export function DetailsTableField({
|
export function DetailsTableField({
|
||||||
item,
|
item,
|
||||||
field
|
field,
|
||||||
|
showIcons = true
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
item: any;
|
item: any;
|
||||||
field: DetailsField;
|
field: DetailsField;
|
||||||
|
showIcons?: boolean;
|
||||||
}>) {
|
}>) {
|
||||||
function getFieldType(type: string) {
|
function getFieldType(type: string) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
|
@ -502,10 +504,12 @@ export function DetailsTableField({
|
||||||
<Table.Tr style={{ verticalAlign: 'top' }}>
|
<Table.Tr style={{ verticalAlign: 'top' }}>
|
||||||
<Table.Td style={{ minWidth: 75, lineBreak: 'auto', flex: 2 }}>
|
<Table.Td style={{ minWidth: 75, lineBreak: 'auto', flex: 2 }}>
|
||||||
<Group gap='xs' wrap='nowrap'>
|
<Group gap='xs' wrap='nowrap'>
|
||||||
<InvenTreeIcon
|
{showIcons && (
|
||||||
icon={field.icon ?? (field.name as keyof InvenTreeIconType)}
|
<InvenTreeIcon
|
||||||
/>
|
icon={field.icon ?? (field.name as keyof InvenTreeIconType)}
|
||||||
<Text style={{ paddingLeft: 10 }}>{field.label}</Text>
|
/>
|
||||||
|
)}
|
||||||
|
<Text style={{ paddingLeft: showIcons ? 10 : 0 }}>{field.label}</Text>
|
||||||
</Group>
|
</Group>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td
|
<Table.Td
|
||||||
|
|
@ -525,15 +529,19 @@ export function DetailsTableField({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DetailsTable({
|
export interface DetailsTableProps {
|
||||||
item,
|
|
||||||
fields,
|
|
||||||
title
|
|
||||||
}: Readonly<{
|
|
||||||
item: any;
|
item: any;
|
||||||
fields: DetailsField[];
|
fields: DetailsField[];
|
||||||
title?: string;
|
title?: string;
|
||||||
}>) {
|
showIcons?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DetailsTable({
|
||||||
|
item,
|
||||||
|
fields,
|
||||||
|
title,
|
||||||
|
showIcons = true
|
||||||
|
}: Readonly<DetailsTableProps>) {
|
||||||
const visibleFields = useMemo(() => {
|
const visibleFields = useMemo(() => {
|
||||||
return fields.filter((field) => !field.hidden);
|
return fields.filter((field) => !field.hidden);
|
||||||
}, [fields]);
|
}, [fields]);
|
||||||
|
|
@ -553,7 +561,12 @@ export function DetailsTable({
|
||||||
<Table striped verticalSpacing={5} horizontalSpacing='sm'>
|
<Table striped verticalSpacing={5} horizontalSpacing='sm'>
|
||||||
<Table.Tbody>
|
<Table.Tbody>
|
||||||
{visibleFields.map((field: DetailsField, index: number) => (
|
{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.Tbody>
|
||||||
</Table>
|
</Table>
|
||||||
|
|
|
||||||
|
|
@ -406,6 +406,10 @@ export function DetailsImage(props: Readonly<DetailImageProps>) {
|
||||||
const { hovered, ref } = useHover();
|
const { hovered, ref } = useHover();
|
||||||
const [img, setImg] = useState<string>(props.src ?? backup_image);
|
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
|
// Sets a new image, and triggers upstream instance refresh
|
||||||
const setAndRefresh = (image: string) => {
|
const setAndRefresh = (image: string) => {
|
||||||
setImg(image);
|
setImg(image);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,20 @@
|
||||||
import { Paper, SimpleGrid } from '@mantine/core';
|
import { Paper, SimpleGrid } from '@mantine/core';
|
||||||
import type React from 'react';
|
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 (
|
return (
|
||||||
<Paper p='xs'>
|
<Paper p='xs'>
|
||||||
<SimpleGrid
|
<SimpleGrid
|
||||||
|
|
@ -10,7 +23,10 @@ export function ItemDetailsGrid(props: React.PropsWithChildren<{}>) {
|
||||||
spacing='xs'
|
spacing='xs'
|
||||||
verticalSpacing='xs'
|
verticalSpacing='xs'
|
||||||
>
|
>
|
||||||
{props.children}
|
{children}
|
||||||
|
{visibleTables.map((props, index) => (
|
||||||
|
<DetailsTable key={index} {...props} />
|
||||||
|
))}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
</Paper>
|
</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,
|
type PluginUIFeature,
|
||||||
PluginUIFeatureType
|
PluginUIFeatureType
|
||||||
} from '../plugins/PluginUIFeature';
|
} from '../plugins/PluginUIFeature';
|
||||||
|
import GlobalPreviewDrawer from '../previews/GlobalPreviewDrawer';
|
||||||
import { Footer } from './Footer';
|
import { Footer } from './Footer';
|
||||||
import { Header } from './Header';
|
import { Header } from './Header';
|
||||||
|
|
||||||
|
|
@ -148,6 +149,7 @@ export default function LayoutComponent() {
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
<GlobalImporterDrawer />
|
<GlobalImporterDrawer />
|
||||||
|
<GlobalPreviewDrawer />
|
||||||
</>
|
</>
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,11 @@ import {
|
||||||
openGlobalImporter
|
openGlobalImporter
|
||||||
} from '../../states/ImporterState';
|
} from '../../states/ImporterState';
|
||||||
import { usePluginState } from '../../states/PluginState';
|
import { usePluginState } from '../../states/PluginState';
|
||||||
|
import {
|
||||||
|
closeGlobalPreview,
|
||||||
|
getGlobalPreviewState,
|
||||||
|
openGlobalPreview
|
||||||
|
} from '../../states/PreviewDrawerState';
|
||||||
import { useServerApiState } from '../../states/ServerApiState';
|
import { useServerApiState } from '../../states/ServerApiState';
|
||||||
import { InvenTreeTableInternal } from '../../tables/InvenTreeTable';
|
import { InvenTreeTableInternal } from '../../tables/InvenTreeTable';
|
||||||
import { EditApiForm } from '../forms/ApiForm';
|
import { EditApiForm } from '../forms/ApiForm';
|
||||||
|
|
@ -97,6 +102,12 @@ export const useInvenTreeContext = () => {
|
||||||
isOpen: () => getGlobalImporterState().isOpen,
|
isOpen: () => getGlobalImporterState().isOpen,
|
||||||
sessionId: () => getGlobalImporterState().sessionId
|
sessionId: () => getGlobalImporterState().sessionId
|
||||||
},
|
},
|
||||||
|
preview: {
|
||||||
|
open: (modelType, id?, instance?) =>
|
||||||
|
openGlobalPreview(modelType, id, instance),
|
||||||
|
close: () => closeGlobalPreview(),
|
||||||
|
isOpen: () => getGlobalPreviewState().isOpen
|
||||||
|
},
|
||||||
tables: {
|
tables: {
|
||||||
renderTable: (props: InvenTreeTableRenderProps<any>) => (
|
renderTable: (props: InvenTreeTableRenderProps<any>) => (
|
||||||
<InvenTreeTableInternal
|
<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',
|
'BARCODE_IN_FORM_FIELDS',
|
||||||
'DATE_DISPLAY_FORMAT',
|
'DATE_DISPLAY_FORMAT',
|
||||||
'FORMS_CLOSE_USING_ESCAPE',
|
'FORMS_CLOSE_USING_ESCAPE',
|
||||||
|
'ENABLE_PREVIEW_PANEL',
|
||||||
'DISPLAY_STOCKTAKE_TAB',
|
'DISPLAY_STOCKTAKE_TAB',
|
||||||
'ENABLE_LAST_BREADCRUMB',
|
'ENABLE_LAST_BREADCRUMB',
|
||||||
'SHOW_EXTRA_MODEL_INFO',
|
'SHOW_EXTRA_MODEL_INFO',
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { t } from '@lingui/core/macro';
|
import { t } from '@lingui/core/macro';
|
||||||
import { Alert, Grid, Skeleton, Stack, Text } from '@mantine/core';
|
import { Alert, Skeleton, Stack, Text } from '@mantine/core';
|
||||||
import {
|
import {
|
||||||
IconChecklist,
|
IconChecklist,
|
||||||
IconCircleCheck,
|
IconCircleCheck,
|
||||||
|
|
@ -21,19 +21,12 @@ import { ModelType } from '@lib/enums/ModelType';
|
||||||
import { UserRoles } from '@lib/enums/Roles';
|
import { UserRoles } from '@lib/enums/Roles';
|
||||||
import { apiUrl } from '@lib/functions/Api';
|
import { apiUrl } from '@lib/functions/Api';
|
||||||
import { getDetailUrl } from '@lib/functions/Navigation';
|
import { getDetailUrl } from '@lib/functions/Navigation';
|
||||||
import { TagsList } from '@lib/index';
|
|
||||||
import type { ApiFormFieldSet } from '@lib/types/Forms';
|
import type { ApiFormFieldSet } from '@lib/types/Forms';
|
||||||
import type { PanelType } from '@lib/types/Panel';
|
import type { PanelType } from '@lib/types/Panel';
|
||||||
import AdminButton from '../../components/buttons/AdminButton';
|
import AdminButton from '../../components/buttons/AdminButton';
|
||||||
import PrimaryActionButton from '../../components/buttons/PrimaryActionButton';
|
import PrimaryActionButton from '../../components/buttons/PrimaryActionButton';
|
||||||
import { PrintingActions } from '../../components/buttons/PrintingActions';
|
import { PrintingActions } from '../../components/buttons/PrintingActions';
|
||||||
import {
|
|
||||||
type DetailsField,
|
|
||||||
DetailsTable
|
|
||||||
} from '../../components/details/Details';
|
|
||||||
import DetailsBadge from '../../components/details/DetailsBadge';
|
import DetailsBadge from '../../components/details/DetailsBadge';
|
||||||
import { DetailsImage } from '../../components/details/DetailsImage';
|
|
||||||
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
|
||||||
import {
|
import {
|
||||||
BarcodeActionDropdown,
|
BarcodeActionDropdown,
|
||||||
CancelItemAction,
|
CancelItemAction,
|
||||||
|
|
@ -66,6 +59,7 @@ import BuildOutputTable from '../../tables/build/BuildOutputTable';
|
||||||
import PartTestResultTable from '../../tables/part/PartTestResultTable';
|
import PartTestResultTable from '../../tables/part/PartTestResultTable';
|
||||||
import { PurchaseOrderTable } from '../../tables/purchasing/PurchaseOrderTable';
|
import { PurchaseOrderTable } from '../../tables/purchasing/PurchaseOrderTable';
|
||||||
import { StockItemTable } from '../../tables/stock/StockItemTable';
|
import { StockItemTable } from '../../tables/stock/StockItemTable';
|
||||||
|
import { BuildOrderDetailsPanel } from './BuildOrderDetailsPanel';
|
||||||
|
|
||||||
function NoItems() {
|
function NoItems() {
|
||||||
return (
|
return (
|
||||||
|
|
@ -235,239 +229,19 @@ export default function BuildDetail() {
|
||||||
refetchOnMount: true
|
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(() => {
|
const buildPanels: PanelType[] = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: 'details',
|
name: 'details',
|
||||||
label: t`Build Details`,
|
label: t`Build Details`,
|
||||||
icon: <IconInfoCircle />,
|
icon: <IconInfoCircle />,
|
||||||
content: detailsPanel
|
content: (
|
||||||
|
<BuildOrderDetailsPanel
|
||||||
|
instance={build}
|
||||||
|
allowImageEdit
|
||||||
|
refreshInstance={refreshInstance}
|
||||||
|
/>
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'line-items',
|
name: 'line-items',
|
||||||
|
|
@ -597,7 +371,7 @@ export default function BuildDetail() {
|
||||||
build,
|
build,
|
||||||
id,
|
id,
|
||||||
user,
|
user,
|
||||||
partRequirements,
|
|
||||||
buildStatus,
|
buildStatus,
|
||||||
globalSettings,
|
globalSettings,
|
||||||
showChildBuilds,
|
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 { t } from '@lingui/core/macro';
|
||||||
import { Grid, Skeleton, Stack } from '@mantine/core';
|
import { Skeleton, Stack } from '@mantine/core';
|
||||||
import {
|
import {
|
||||||
IconBuildingWarehouse,
|
IconBuildingWarehouse,
|
||||||
IconInfoCircle,
|
IconInfoCircle,
|
||||||
|
|
@ -17,18 +17,10 @@ import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||||
import { ModelType } from '@lib/enums/ModelType';
|
import { ModelType } from '@lib/enums/ModelType';
|
||||||
import { UserRoles } from '@lib/enums/Roles';
|
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 type { PanelType } from '@lib/types/Panel';
|
||||||
import AdminButton from '../../components/buttons/AdminButton';
|
import AdminButton from '../../components/buttons/AdminButton';
|
||||||
import { PrintingActions } from '../../components/buttons/PrintingActions';
|
import { PrintingActions } from '../../components/buttons/PrintingActions';
|
||||||
import {
|
|
||||||
type DetailsField,
|
|
||||||
DetailsTable
|
|
||||||
} from '../../components/details/Details';
|
|
||||||
import DetailsBadge from '../../components/details/DetailsBadge';
|
import DetailsBadge from '../../components/details/DetailsBadge';
|
||||||
import { DetailsImage } from '../../components/details/DetailsImage';
|
|
||||||
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
|
||||||
import {
|
import {
|
||||||
DeleteItemAction,
|
DeleteItemAction,
|
||||||
DuplicateItemAction,
|
DuplicateItemAction,
|
||||||
|
|
@ -58,6 +50,7 @@ import { SupplierPartTable } from '../../tables/purchasing/SupplierPartTable';
|
||||||
import { ReturnOrderTable } from '../../tables/sales/ReturnOrderTable';
|
import { ReturnOrderTable } from '../../tables/sales/ReturnOrderTable';
|
||||||
import { SalesOrderTable } from '../../tables/sales/SalesOrderTable';
|
import { SalesOrderTable } from '../../tables/sales/SalesOrderTable';
|
||||||
import { StockItemTable } from '../../tables/stock/StockItemTable';
|
import { StockItemTable } from '../../tables/stock/StockItemTable';
|
||||||
|
import { CompanyDetailsPanel } from './CompanyDetailsPanel';
|
||||||
|
|
||||||
export type CompanyDetailProps = {
|
export type CompanyDetailProps = {
|
||||||
title: string;
|
title: string;
|
||||||
|
|
@ -87,101 +80,15 @@ export default function CompanyDetail(props: Readonly<CompanyDetailProps>) {
|
||||||
refetchOnMount: true
|
refetchOnMount: true
|
||||||
});
|
});
|
||||||
|
|
||||||
const detailsPanel = useMemo(() => {
|
const detailsPanel = instanceQuery.isFetching ? (
|
||||||
if (instanceQuery.isFetching) {
|
<Skeleton />
|
||||||
return <Skeleton />;
|
) : (
|
||||||
}
|
<CompanyDetailsPanel
|
||||||
|
instance={company}
|
||||||
const tl: DetailsField[] = [
|
allowImageEdit
|
||||||
{
|
refreshInstance={refreshInstance}
|
||||||
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 companyPanels: PanelType[] = useMemo(() => {
|
const companyPanels: PanelType[] = useMemo(() => {
|
||||||
return [
|
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 { t } from '@lingui/core/macro';
|
||||||
import { Grid, Skeleton, Stack } from '@mantine/core';
|
import { Skeleton, Stack } from '@mantine/core';
|
||||||
import {
|
import {
|
||||||
IconBuildingWarehouse,
|
IconBuildingWarehouse,
|
||||||
IconInfoCircle,
|
IconInfoCircle,
|
||||||
|
|
@ -8,20 +8,12 @@ import {
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import TagsList from '@lib/components/TagsList';
|
|
||||||
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||||
import { ModelType } from '@lib/enums/ModelType';
|
import { ModelType } from '@lib/enums/ModelType';
|
||||||
import { UserRoles } from '@lib/enums/Roles';
|
import { UserRoles } from '@lib/enums/Roles';
|
||||||
import { apiUrl } from '@lib/functions/Api';
|
|
||||||
import { getDetailUrl } from '@lib/functions/Navigation';
|
import { getDetailUrl } from '@lib/functions/Navigation';
|
||||||
import type { PanelType } from '@lib/types/Panel';
|
import type { PanelType } from '@lib/types/Panel';
|
||||||
import AdminButton from '../../components/buttons/AdminButton';
|
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 {
|
import {
|
||||||
DeleteItemAction,
|
DeleteItemAction,
|
||||||
DuplicateItemAction,
|
DuplicateItemAction,
|
||||||
|
|
@ -44,6 +36,7 @@ import { useInstance } from '../../hooks/UseInstance';
|
||||||
import { useUserState } from '../../states/UserState';
|
import { useUserState } from '../../states/UserState';
|
||||||
import { SupplierPartTable } from '../../tables/purchasing/SupplierPartTable';
|
import { SupplierPartTable } from '../../tables/purchasing/SupplierPartTable';
|
||||||
import { StockItemTable } from '../../tables/stock/StockItemTable';
|
import { StockItemTable } from '../../tables/stock/StockItemTable';
|
||||||
|
import { ManufacturerPartDetailsPanel } from './ManufacturerPartDetailsPanel';
|
||||||
|
|
||||||
export default function ManufacturerPartDetail() {
|
export default function ManufacturerPartDetail() {
|
||||||
const { id } = useParams();
|
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(() => {
|
const panels: PanelType[] = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: 'details',
|
name: 'details',
|
||||||
label: t`Manufacturer Part Details`,
|
label: t`Manufacturer Part Details`,
|
||||||
icon: <IconInfoCircle />,
|
icon: <IconInfoCircle />,
|
||||||
content: detailsPanel
|
content: (
|
||||||
|
<ManufacturerPartDetailsPanel
|
||||||
|
instance={manufacturerPart}
|
||||||
|
allowImageEdit
|
||||||
|
refreshInstance={refreshInstance}
|
||||||
|
/>
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'stock',
|
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 { t } from '@lingui/core/macro';
|
||||||
import { Grid, Skeleton, Stack } from '@mantine/core';
|
import { Skeleton, Stack } from '@mantine/core';
|
||||||
import {
|
import {
|
||||||
IconCurrencyDollar,
|
IconCurrencyDollar,
|
||||||
IconInfoCircle,
|
IconInfoCircle,
|
||||||
|
|
@ -9,22 +9,14 @@ import {
|
||||||
import { type ReactNode, useMemo } from 'react';
|
import { type ReactNode, useMemo } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import TagsList from '@lib/components/TagsList';
|
|
||||||
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||||
import { ModelType } from '@lib/enums/ModelType';
|
import { ModelType } from '@lib/enums/ModelType';
|
||||||
import { UserRoles } from '@lib/enums/Roles';
|
import { UserRoles } from '@lib/enums/Roles';
|
||||||
import { apiUrl } from '@lib/functions/Api';
|
|
||||||
import { formatDecimal } from '@lib/functions/Formatting';
|
import { formatDecimal } from '@lib/functions/Formatting';
|
||||||
import { getDetailUrl } from '@lib/functions/Navigation';
|
import { getDetailUrl } from '@lib/functions/Navigation';
|
||||||
import type { PanelType } from '@lib/types/Panel';
|
import type { PanelType } from '@lib/types/Panel';
|
||||||
import AdminButton from '../../components/buttons/AdminButton';
|
import AdminButton from '../../components/buttons/AdminButton';
|
||||||
import {
|
|
||||||
type DetailsField,
|
|
||||||
DetailsTable
|
|
||||||
} from '../../components/details/Details';
|
|
||||||
import DetailsBadge from '../../components/details/DetailsBadge';
|
import DetailsBadge from '../../components/details/DetailsBadge';
|
||||||
import { DetailsImage } from '../../components/details/DetailsImage';
|
|
||||||
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
|
||||||
import {
|
import {
|
||||||
BarcodeActionDropdown,
|
BarcodeActionDropdown,
|
||||||
DeleteItemAction,
|
DeleteItemAction,
|
||||||
|
|
@ -49,6 +41,7 @@ import { useUserState } from '../../states/UserState';
|
||||||
import { PurchaseOrderTable } from '../../tables/purchasing/PurchaseOrderTable';
|
import { PurchaseOrderTable } from '../../tables/purchasing/PurchaseOrderTable';
|
||||||
import SupplierPriceBreakTable from '../../tables/purchasing/SupplierPriceBreakTable';
|
import SupplierPriceBreakTable from '../../tables/purchasing/SupplierPriceBreakTable';
|
||||||
import { StockItemTable } from '../../tables/stock/StockItemTable';
|
import { StockItemTable } from '../../tables/stock/StockItemTable';
|
||||||
|
import { SupplierPartDetailsPanel } from './SupplierPartDetailsPanel';
|
||||||
|
|
||||||
export default function SupplierPartDetail() {
|
export default function SupplierPartDetail() {
|
||||||
const { id } = useParams();
|
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(() => {
|
const panels: PanelType[] = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: 'details',
|
name: 'details',
|
||||||
label: t`Supplier Part Details`,
|
label: t`Supplier Part Details`,
|
||||||
icon: <IconInfoCircle />,
|
icon: <IconInfoCircle />,
|
||||||
content: detailsPanel
|
content: (
|
||||||
|
<SupplierPartDetailsPanel
|
||||||
|
instance={supplierPart}
|
||||||
|
allowImageEdit
|
||||||
|
refreshInstance={refreshInstance}
|
||||||
|
/>
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'stock',
|
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 { IconInfoCircle } from '@tabler/icons-react';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import {
|
import type { DetailsField } from '../../components/details/Details';
|
||||||
type DetailsField,
|
|
||||||
DetailsTable
|
|
||||||
} from '../../components/details/Details';
|
|
||||||
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
||||||
import {} from '../../components/items/ActionDropdown';
|
import {} from '../../components/items/ActionDropdown';
|
||||||
import { RoleTable, type RuleSet } from '../../components/items/RoleTable';
|
import { RoleTable, type RuleSet } from '../../components/items/RoleTable';
|
||||||
|
|
@ -48,8 +45,9 @@ export default function GroupDetail() {
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ItemDetailsGrid>
|
<ItemDetailsGrid
|
||||||
<DetailsTable fields={tl} item={instance} title={t`Group Details`} />
|
tables={[{ fields: tl, item: instance, title: t`Group Details` }]}
|
||||||
|
>
|
||||||
<Paper p='xs' withBorder>
|
<Paper p='xs' withBorder>
|
||||||
<Stack gap='xs'>
|
<Stack gap='xs'>
|
||||||
<StylishText size='lg'>{t`Group Roles`}</StylishText>
|
<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 { IconInfoCircle } from '@tabler/icons-react';
|
||||||
import { type ReactNode, useMemo } from 'react';
|
import { type ReactNode, useMemo } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import {
|
import type { DetailsField } from '../../components/details/Details';
|
||||||
type DetailsField,
|
|
||||||
DetailsTable
|
|
||||||
} from '../../components/details/Details';
|
|
||||||
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
||||||
import {} from '../../components/items/ActionDropdown';
|
import {} from '../../components/items/ActionDropdown';
|
||||||
import InstanceDetail from '../../components/nav/InstanceDetail';
|
import InstanceDetail from '../../components/nav/InstanceDetail';
|
||||||
|
|
@ -171,13 +168,15 @@ export default function UserDetail() {
|
||||||
instance.location;
|
instance.location;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ItemDetailsGrid>
|
<ItemDetailsGrid
|
||||||
<DetailsTable fields={tl} item={instance} title={t`User Information`} />
|
tables={[
|
||||||
<DetailsTable fields={tr} item={instance} title={t`User Permissions`} />
|
{ fields: tl, item: instance, title: t`User Information` },
|
||||||
{hasProfile && settings.isSet('DISPLAY_PROFILE_INFO') && (
|
{ fields: tr, item: instance, title: t`User Permissions` },
|
||||||
<DetailsTable fields={br} item={instance} title={t`User Profile`} />
|
...(hasProfile && settings.isSet('DISPLAY_PROFILE_INFO')
|
||||||
)}
|
? [{ fields: br, item: instance, title: t`User Profile` }]
|
||||||
</ItemDetailsGrid>
|
: [])
|
||||||
|
]}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}, [instance, userGroups, instanceQuery]);
|
}, [instance, userGroups, instanceQuery]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { t } from '@lingui/core/macro';
|
import { t } from '@lingui/core/macro';
|
||||||
import { Group, LoadingOverlay, Skeleton, Stack } from '@mantine/core';
|
import { LoadingOverlay, Stack } from '@mantine/core';
|
||||||
import {
|
import {
|
||||||
IconCategory,
|
IconCategory,
|
||||||
IconInfoCircle,
|
IconInfoCircle,
|
||||||
|
|
@ -21,11 +21,6 @@ import type { PanelType } from '@lib/types/Panel';
|
||||||
import { useLocalStorage } from '@mantine/hooks';
|
import { useLocalStorage } from '@mantine/hooks';
|
||||||
import AdminButton from '../../components/buttons/AdminButton';
|
import AdminButton from '../../components/buttons/AdminButton';
|
||||||
import StarredToggleButton from '../../components/buttons/StarredToggleButton';
|
import StarredToggleButton from '../../components/buttons/StarredToggleButton';
|
||||||
import {
|
|
||||||
type DetailsField,
|
|
||||||
DetailsTable
|
|
||||||
} from '../../components/details/Details';
|
|
||||||
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
|
||||||
import {
|
import {
|
||||||
DeleteItemAction,
|
DeleteItemAction,
|
||||||
EditItemAction,
|
EditItemAction,
|
||||||
|
|
@ -52,6 +47,7 @@ import { PartCategoryTable } from '../../tables/part/PartCategoryTable';
|
||||||
import PartCategoryTemplateTable from '../../tables/part/PartCategoryTemplateTable';
|
import PartCategoryTemplateTable from '../../tables/part/PartCategoryTemplateTable';
|
||||||
import { PartListTable } from '../../tables/part/PartTable';
|
import { PartListTable } from '../../tables/part/PartTable';
|
||||||
import { StockItemTable } from '../../tables/stock/StockItemTable';
|
import { StockItemTable } from '../../tables/stock/StockItemTable';
|
||||||
|
import { PartCategoryDetailsPanel } from './PartCategoryDetailsPanel';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detail view for a single PartCategory instance.
|
* Detail view for a single PartCategory instance.
|
||||||
|
|
@ -106,100 +102,6 @@ export default function CategoryDetail() {
|
||||||
assign: false
|
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({
|
const editCategory = useEditApiFormModal({
|
||||||
url: ApiEndpoints.category_list,
|
url: ApiEndpoints.category_list,
|
||||||
pk: id,
|
pk: id,
|
||||||
|
|
@ -296,7 +198,7 @@ export default function CategoryDetail() {
|
||||||
name: 'details',
|
name: 'details',
|
||||||
label: t`Category Details`,
|
label: t`Category Details`,
|
||||||
icon: <IconInfoCircle />,
|
icon: <IconInfoCircle />,
|
||||||
content: detailsPanel,
|
content: <PartCategoryDetailsPanel instance={category} />,
|
||||||
hidden: !id || !category?.pk
|
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,
|
ActionIcon,
|
||||||
Alert,
|
Alert,
|
||||||
Center,
|
Center,
|
||||||
Grid,
|
|
||||||
Group,
|
Group,
|
||||||
Loader,
|
Loader,
|
||||||
Paper,
|
Paper,
|
||||||
|
|
@ -41,7 +40,6 @@ import { type ReactNode, useMemo, useState } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import Select from 'react-select';
|
import Select from 'react-select';
|
||||||
|
|
||||||
import TagsList from '@lib/components/TagsList';
|
|
||||||
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||||
import { ModelType } from '@lib/enums/ModelType';
|
import { ModelType } from '@lib/enums/ModelType';
|
||||||
import { UserRoles } from '@lib/enums/Roles';
|
import { UserRoles } from '@lib/enums/Roles';
|
||||||
|
|
@ -52,13 +50,7 @@ import type { PanelType } from '@lib/types/Panel';
|
||||||
import AdminButton from '../../components/buttons/AdminButton';
|
import AdminButton from '../../components/buttons/AdminButton';
|
||||||
import { PrintingActions } from '../../components/buttons/PrintingActions';
|
import { PrintingActions } from '../../components/buttons/PrintingActions';
|
||||||
import StarredToggleButton from '../../components/buttons/StarredToggleButton';
|
import StarredToggleButton from '../../components/buttons/StarredToggleButton';
|
||||||
import {
|
|
||||||
type DetailsField,
|
|
||||||
DetailsTable
|
|
||||||
} from '../../components/details/Details';
|
|
||||||
import DetailsBadge from '../../components/details/DetailsBadge';
|
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 { Thumbnail } from '../../components/images/Thumbnail';
|
||||||
import {
|
import {
|
||||||
ActionDropdown,
|
ActionDropdown,
|
||||||
|
|
@ -77,7 +69,7 @@ import { PanelGroup } from '../../components/panels/PanelGroup';
|
||||||
import { RenderPart } from '../../components/render/Part';
|
import { RenderPart } from '../../components/render/Part';
|
||||||
import OrderPartsWizard from '../../components/wizards/OrderPartsWizard';
|
import OrderPartsWizard from '../../components/wizards/OrderPartsWizard';
|
||||||
import { useApi } from '../../contexts/ApiContext';
|
import { useApi } from '../../contexts/ApiContext';
|
||||||
import { formatDecimal, formatPriceRange } from '../../defaults/formatters';
|
import { formatDecimal } from '../../defaults/formatters';
|
||||||
import { usePartFields } from '../../forms/PartForms';
|
import { usePartFields } from '../../forms/PartForms';
|
||||||
import { useFindSerialNumberForm } from '../../forms/StockForms';
|
import { useFindSerialNumberForm } from '../../forms/StockForms';
|
||||||
import {
|
import {
|
||||||
|
|
@ -106,6 +98,7 @@ import { SalesOrderTable } from '../../tables/sales/SalesOrderTable';
|
||||||
import { StockItemTable } from '../../tables/stock/StockItemTable';
|
import { StockItemTable } from '../../tables/stock/StockItemTable';
|
||||||
import { TransferOrderTable } from '../../tables/stock/TransferOrderTable';
|
import { TransferOrderTable } from '../../tables/stock/TransferOrderTable';
|
||||||
import PartAllocationPanel from './PartAllocationPanel';
|
import PartAllocationPanel from './PartAllocationPanel';
|
||||||
|
import { PartDetailsPanel } from './PartDetailsPanel';
|
||||||
import PartPricingPanel from './PartPricingPanel';
|
import PartPricingPanel from './PartPricingPanel';
|
||||||
import PartStockHistoryDetail from './PartStockHistoryDetail';
|
import PartStockHistoryDetail from './PartStockHistoryDetail';
|
||||||
import PartSupplierDetail from './PartSupplierDetail';
|
import PartSupplierDetail from './PartSupplierDetail';
|
||||||
|
|
@ -175,14 +168,6 @@ export default function PartDetail() {
|
||||||
refetchOnMount: true
|
refetchOnMount: true
|
||||||
});
|
});
|
||||||
|
|
||||||
const { instance: serials } = useInstance({
|
|
||||||
endpoint: ApiEndpoints.part_serial_numbers,
|
|
||||||
pk: id,
|
|
||||||
hasPrimaryKey: true,
|
|
||||||
refetchOnMount: false,
|
|
||||||
defaultValue: {}
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
instance: part,
|
instance: part,
|
||||||
refreshInstance,
|
refreshInstance,
|
||||||
|
|
@ -275,398 +260,22 @@ export default function PartDetail() {
|
||||||
return partRevisionOptions.length > 0 && revisionsEnabled;
|
return partRevisionOptions.length > 0 && revisionsEnabled;
|
||||||
}, [partRevisionOptions, revisionsEnabled]);
|
}, [partRevisionOptions, revisionsEnabled]);
|
||||||
|
|
||||||
const detailsPanel = useMemo(() => {
|
const revisionSelector = useMemo(() => {
|
||||||
if (instanceQuery.isFetching) {
|
if (!enableRevisionSelection) return null;
|
||||||
return <Skeleton />;
|
return (
|
||||||
}
|
<Paper p='sm' withBorder>
|
||||||
|
|
||||||
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>
|
|
||||||
<Stack gap='xs'>
|
<Stack gap='xs'>
|
||||||
<Grid grow>
|
<Group gap='xs'>
|
||||||
<DetailsImage
|
<ActionIcon variant='transparent'>
|
||||||
appRole={UserRoles.part}
|
<IconVersions />
|
||||||
imageActions={{
|
</ActionIcon>
|
||||||
selectExisting: true,
|
<Text>{t`Select Part Revision`}</Text>
|
||||||
downloadImage: true,
|
</Group>
|
||||||
uploadFile: true,
|
<RevisionSelector part={part} options={partRevisionOptions} />
|
||||||
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>
|
|
||||||
)}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
<DetailsTable fields={tr} item={data} />
|
</Paper>
|
||||||
<DetailsTable fields={bl} item={data} />
|
|
||||||
<DetailsTable fields={br} item={data} />
|
|
||||||
</ItemDetailsGrid>
|
|
||||||
) : (
|
|
||||||
<Skeleton />
|
|
||||||
);
|
);
|
||||||
}, [
|
}, [enableRevisionSelection, part, partRevisionOptions]);
|
||||||
globalSettings,
|
|
||||||
part,
|
|
||||||
id,
|
|
||||||
serials,
|
|
||||||
instanceQuery.isFetching,
|
|
||||||
instanceQuery.data,
|
|
||||||
enableRevisionSelection,
|
|
||||||
partRevisionOptions,
|
|
||||||
partRequirementsQuery.isFetching,
|
|
||||||
partRequirements
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Part data panels (recalculate when part data changes)
|
// Part data panels (recalculate when part data changes)
|
||||||
const partPanels: PanelType[] = useMemo(() => {
|
const partPanels: PanelType[] = useMemo(() => {
|
||||||
|
|
@ -675,7 +284,14 @@ export default function PartDetail() {
|
||||||
name: 'details',
|
name: 'details',
|
||||||
label: t`Part Details`,
|
label: t`Part Details`,
|
||||||
icon: <IconInfoCircle />,
|
icon: <IconInfoCircle />,
|
||||||
content: detailsPanel
|
content: (
|
||||||
|
<PartDetailsPanel
|
||||||
|
instance={part}
|
||||||
|
allowImageEdit
|
||||||
|
refreshInstance={refreshInstance}
|
||||||
|
additionalContent={revisionSelector}
|
||||||
|
/>
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'stock',
|
name: 'stock',
|
||||||
|
|
@ -903,8 +519,9 @@ export default function PartDetail() {
|
||||||
user,
|
user,
|
||||||
globalSettings,
|
globalSettings,
|
||||||
userSettings,
|
userSettings,
|
||||||
detailsPanel,
|
bomInformation,
|
||||||
bomInformation
|
revisionSelector,
|
||||||
|
refreshInstance
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const breadcrumbs = useMemo(() => {
|
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 { 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 { IconInfoCircle, IconList, IconPackages } from '@tabler/icons-react';
|
||||||
import { type ReactNode, useMemo } from 'react';
|
import { type ReactNode, useMemo } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
@ -9,17 +9,10 @@ import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||||
import { ModelType } from '@lib/enums/ModelType';
|
import { ModelType } from '@lib/enums/ModelType';
|
||||||
import { UserRoles } from '@lib/enums/Roles';
|
import { UserRoles } from '@lib/enums/Roles';
|
||||||
import { apiUrl } from '@lib/functions/Api';
|
import { apiUrl } from '@lib/functions/Api';
|
||||||
import { TagsList } from '@lib/index';
|
|
||||||
import type { PanelType } from '@lib/types/Panel';
|
import type { PanelType } from '@lib/types/Panel';
|
||||||
import AdminButton from '../../components/buttons/AdminButton';
|
import AdminButton from '../../components/buttons/AdminButton';
|
||||||
import PrimaryActionButton from '../../components/buttons/PrimaryActionButton';
|
import PrimaryActionButton from '../../components/buttons/PrimaryActionButton';
|
||||||
import { PrintingActions } from '../../components/buttons/PrintingActions';
|
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 {
|
import {
|
||||||
BarcodeActionDropdown,
|
BarcodeActionDropdown,
|
||||||
CancelItemAction,
|
CancelItemAction,
|
||||||
|
|
@ -35,7 +28,6 @@ import NotesPanel from '../../components/panels/NotesPanel';
|
||||||
import { PanelGroup } from '../../components/panels/PanelGroup';
|
import { PanelGroup } from '../../components/panels/PanelGroup';
|
||||||
import ParametersPanel from '../../components/panels/ParametersPanel';
|
import ParametersPanel from '../../components/panels/ParametersPanel';
|
||||||
import { StatusRenderer } from '../../components/render/StatusRenderer';
|
import { StatusRenderer } from '../../components/render/StatusRenderer';
|
||||||
import { formatCurrency } from '../../defaults/formatters';
|
|
||||||
import { usePurchaseOrderFields } from '../../forms/PurchaseOrderForms';
|
import { usePurchaseOrderFields } from '../../forms/PurchaseOrderForms';
|
||||||
import {
|
import {
|
||||||
useCreateApiFormModal,
|
useCreateApiFormModal,
|
||||||
|
|
@ -48,6 +40,7 @@ import { useUserState } from '../../states/UserState';
|
||||||
import ExtraLineItemTable from '../../tables/general/ExtraLineItemTable';
|
import ExtraLineItemTable from '../../tables/general/ExtraLineItemTable';
|
||||||
import { PurchaseOrderLineItemTable } from '../../tables/purchasing/PurchaseOrderLineItemTable';
|
import { PurchaseOrderLineItemTable } from '../../tables/purchasing/PurchaseOrderLineItemTable';
|
||||||
import { StockItemTable } from '../../tables/stock/StockItemTable';
|
import { StockItemTable } from '../../tables/stock/StockItemTable';
|
||||||
|
import { PurchaseOrderDetailsPanel } from './PurchaseOrderDetailsPanel';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detail page for a single PurchaseOrder
|
* Detail page for a single PurchaseOrder
|
||||||
|
|
@ -72,13 +65,13 @@ export default function PurchaseOrderDetail() {
|
||||||
refetchOnMount: true
|
refetchOnMount: true
|
||||||
});
|
});
|
||||||
|
|
||||||
const orderCurrency = useMemo(() => {
|
const orderCurrency = useMemo(
|
||||||
return (
|
() =>
|
||||||
order.order_currency ||
|
order.order_currency ||
|
||||||
order.supplier_detail?.currency ||
|
order.supplier_detail?.currency ||
|
||||||
globalSettings.getSetting('INVENTREE_DEFAULT_CURRENCY')
|
globalSettings.getSetting('INVENTREE_DEFAULT_CURRENCY'),
|
||||||
);
|
[order, globalSettings]
|
||||||
}, [order, globalSettings]);
|
);
|
||||||
|
|
||||||
const purchaseOrderFields = usePurchaseOrderFields({});
|
const purchaseOrderFields = usePurchaseOrderFields({});
|
||||||
|
|
||||||
|
|
@ -133,222 +126,19 @@ export default function PurchaseOrderDetail() {
|
||||||
modelType: ModelType.purchaseorder
|
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(() => {
|
const orderPanels: PanelType[] = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: 'detail',
|
name: 'detail',
|
||||||
label: t`Order Details`,
|
label: t`Order Details`,
|
||||||
icon: <IconInfoCircle />,
|
icon: <IconInfoCircle />,
|
||||||
content: detailsPanel
|
content: (
|
||||||
|
<PurchaseOrderDetailsPanel
|
||||||
|
instance={order}
|
||||||
|
allowImageEdit
|
||||||
|
refreshInstance={refreshInstance}
|
||||||
|
/>
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'line-items',
|
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 { 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 { IconInfoCircle, IconList } from '@tabler/icons-react';
|
||||||
import { type ReactNode, useMemo } from 'react';
|
import { type ReactNode, useMemo } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
@ -9,17 +9,10 @@ import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||||
import { ModelType } from '@lib/enums/ModelType';
|
import { ModelType } from '@lib/enums/ModelType';
|
||||||
import { UserRoles } from '@lib/enums/Roles';
|
import { UserRoles } from '@lib/enums/Roles';
|
||||||
import { apiUrl } from '@lib/functions/Api';
|
import { apiUrl } from '@lib/functions/Api';
|
||||||
import { TagsList } from '@lib/index';
|
|
||||||
import type { PanelType } from '@lib/types/Panel';
|
import type { PanelType } from '@lib/types/Panel';
|
||||||
import AdminButton from '../../components/buttons/AdminButton';
|
import AdminButton from '../../components/buttons/AdminButton';
|
||||||
import PrimaryActionButton from '../../components/buttons/PrimaryActionButton';
|
import PrimaryActionButton from '../../components/buttons/PrimaryActionButton';
|
||||||
import { PrintingActions } from '../../components/buttons/PrintingActions';
|
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 {
|
import {
|
||||||
BarcodeActionDropdown,
|
BarcodeActionDropdown,
|
||||||
CancelItemAction,
|
CancelItemAction,
|
||||||
|
|
@ -34,9 +27,7 @@ import AttachmentPanel from '../../components/panels/AttachmentPanel';
|
||||||
import NotesPanel from '../../components/panels/NotesPanel';
|
import NotesPanel from '../../components/panels/NotesPanel';
|
||||||
import { PanelGroup } from '../../components/panels/PanelGroup';
|
import { PanelGroup } from '../../components/panels/PanelGroup';
|
||||||
import ParametersPanel from '../../components/panels/ParametersPanel';
|
import ParametersPanel from '../../components/panels/ParametersPanel';
|
||||||
import { RenderAddress } from '../../components/render/Company';
|
|
||||||
import { StatusRenderer } from '../../components/render/StatusRenderer';
|
import { StatusRenderer } from '../../components/render/StatusRenderer';
|
||||||
import { formatCurrency } from '../../defaults/formatters';
|
|
||||||
import { useReturnOrderFields } from '../../forms/ReturnOrderForms';
|
import { useReturnOrderFields } from '../../forms/ReturnOrderForms';
|
||||||
import {
|
import {
|
||||||
useCreateApiFormModal,
|
useCreateApiFormModal,
|
||||||
|
|
@ -48,6 +39,7 @@ import { useGlobalSettingsState } from '../../states/SettingsStates';
|
||||||
import { useUserState } from '../../states/UserState';
|
import { useUserState } from '../../states/UserState';
|
||||||
import ExtraLineItemTable from '../../tables/general/ExtraLineItemTable';
|
import ExtraLineItemTable from '../../tables/general/ExtraLineItemTable';
|
||||||
import ReturnOrderLineItemTable from '../../tables/sales/ReturnOrderLineItemTable';
|
import ReturnOrderLineItemTable from '../../tables/sales/ReturnOrderLineItemTable';
|
||||||
|
import { ReturnOrderDetailsPanel } from './ReturnOrderDetailsPanel';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detail page for a single ReturnOrder
|
* Detail page for a single ReturnOrder
|
||||||
|
|
@ -99,233 +91,19 @@ export default function ReturnOrderDetail() {
|
||||||
);
|
);
|
||||||
}, [order, globalSettings]);
|
}, [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(() => {
|
const orderPanels: PanelType[] = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: 'detail',
|
name: 'detail',
|
||||||
label: t`Order Details`,
|
label: t`Order Details`,
|
||||||
icon: <IconInfoCircle />,
|
icon: <IconInfoCircle />,
|
||||||
content: detailsPanel
|
content: (
|
||||||
|
<ReturnOrderDetailsPanel
|
||||||
|
instance={order}
|
||||||
|
allowImageEdit
|
||||||
|
refreshInstance={refreshInstance}
|
||||||
|
/>
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'line-items',
|
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 { t } from '@lingui/core/macro';
|
||||||
import { Accordion, Grid, Skeleton, Stack, Text } from '@mantine/core';
|
import { Accordion, Skeleton, Stack } from '@mantine/core';
|
||||||
import {
|
import {
|
||||||
IconBookmark,
|
IconBookmark,
|
||||||
IconCubeSend,
|
IconCubeSend,
|
||||||
|
|
@ -11,7 +11,6 @@ import { type ReactNode, useMemo } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { StylishText } from '@lib/components/StylishText';
|
import { StylishText } from '@lib/components/StylishText';
|
||||||
import TagsList from '@lib/components/TagsList';
|
|
||||||
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||||
import { ModelType } from '@lib/enums/ModelType';
|
import { ModelType } from '@lib/enums/ModelType';
|
||||||
import { UserRoles } from '@lib/enums/Roles';
|
import { UserRoles } from '@lib/enums/Roles';
|
||||||
|
|
@ -20,12 +19,6 @@ import type { PanelType } from '@lib/types/Panel';
|
||||||
import AdminButton from '../../components/buttons/AdminButton';
|
import AdminButton from '../../components/buttons/AdminButton';
|
||||||
import PrimaryActionButton from '../../components/buttons/PrimaryActionButton';
|
import PrimaryActionButton from '../../components/buttons/PrimaryActionButton';
|
||||||
import { PrintingActions } from '../../components/buttons/PrintingActions';
|
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 {
|
import {
|
||||||
BarcodeActionDropdown,
|
BarcodeActionDropdown,
|
||||||
CancelItemAction,
|
CancelItemAction,
|
||||||
|
|
@ -40,9 +33,7 @@ import AttachmentPanel from '../../components/panels/AttachmentPanel';
|
||||||
import NotesPanel from '../../components/panels/NotesPanel';
|
import NotesPanel from '../../components/panels/NotesPanel';
|
||||||
import { PanelGroup } from '../../components/panels/PanelGroup';
|
import { PanelGroup } from '../../components/panels/PanelGroup';
|
||||||
import ParametersPanel from '../../components/panels/ParametersPanel';
|
import ParametersPanel from '../../components/panels/ParametersPanel';
|
||||||
import { RenderAddress } from '../../components/render/Company';
|
|
||||||
import { StatusRenderer } from '../../components/render/StatusRenderer';
|
import { StatusRenderer } from '../../components/render/StatusRenderer';
|
||||||
import { formatCurrency } from '../../defaults/formatters';
|
|
||||||
import { useSalesOrderFields } from '../../forms/SalesOrderForms';
|
import { useSalesOrderFields } from '../../forms/SalesOrderForms';
|
||||||
import {
|
import {
|
||||||
useCreateApiFormModal,
|
useCreateApiFormModal,
|
||||||
|
|
@ -57,6 +48,7 @@ import ExtraLineItemTable from '../../tables/general/ExtraLineItemTable';
|
||||||
import SalesOrderAllocationTable from '../../tables/sales/SalesOrderAllocationTable';
|
import SalesOrderAllocationTable from '../../tables/sales/SalesOrderAllocationTable';
|
||||||
import SalesOrderLineItemTable from '../../tables/sales/SalesOrderLineItemTable';
|
import SalesOrderLineItemTable from '../../tables/sales/SalesOrderLineItemTable';
|
||||||
import SalesOrderShipmentTable from '../../tables/sales/SalesOrderShipmentTable';
|
import SalesOrderShipmentTable from '../../tables/sales/SalesOrderShipmentTable';
|
||||||
|
import { SalesOrderDetailsPanel } from './SalesOrderDetailsPanel';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detail page for a single SalesOrder
|
* Detail page for a single SalesOrder
|
||||||
|
|
@ -89,227 +81,6 @@ export default function SalesOrderDetail() {
|
||||||
);
|
);
|
||||||
}, [order, globalSettings]);
|
}, [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 soStatus = useStatusCodes({ modelType: ModelType.salesorder });
|
||||||
|
|
||||||
const lineItemsEditable: boolean = useMemo(() => {
|
const lineItemsEditable: boolean = useMemo(() => {
|
||||||
|
|
@ -364,7 +135,13 @@ export default function SalesOrderDetail() {
|
||||||
name: 'detail',
|
name: 'detail',
|
||||||
label: t`Order Details`,
|
label: t`Order Details`,
|
||||||
icon: <IconInfoCircle />,
|
icon: <IconInfoCircle />,
|
||||||
content: detailsPanel
|
content: (
|
||||||
|
<SalesOrderDetailsPanel
|
||||||
|
instance={order}
|
||||||
|
allowImageEdit
|
||||||
|
refreshInstance={refreshInstance}
|
||||||
|
/>
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'line-items',
|
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 { t } from '@lingui/core/macro';
|
||||||
import { Grid, Skeleton, Stack, Text } from '@mantine/core';
|
import { Stack } from '@mantine/core';
|
||||||
import {
|
import {
|
||||||
IconBookmark,
|
IconBookmark,
|
||||||
IconCircleCheck,
|
IconCircleCheck,
|
||||||
|
|
@ -13,18 +13,11 @@ import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||||
import { ModelType } from '@lib/enums/ModelType';
|
import { ModelType } from '@lib/enums/ModelType';
|
||||||
import { UserRoles } from '@lib/enums/Roles';
|
import { UserRoles } from '@lib/enums/Roles';
|
||||||
import { getDetailUrl } from '@lib/functions/Navigation';
|
import { getDetailUrl } from '@lib/functions/Navigation';
|
||||||
import { TagsList } from '@lib/index';
|
|
||||||
import type { PanelType } from '@lib/types/Panel';
|
import type { PanelType } from '@lib/types/Panel';
|
||||||
import AdminButton from '../../components/buttons/AdminButton';
|
import AdminButton from '../../components/buttons/AdminButton';
|
||||||
import PrimaryActionButton from '../../components/buttons/PrimaryActionButton';
|
import PrimaryActionButton from '../../components/buttons/PrimaryActionButton';
|
||||||
import { PrintingActions } from '../../components/buttons/PrintingActions';
|
import { PrintingActions } from '../../components/buttons/PrintingActions';
|
||||||
import {
|
|
||||||
type DetailsField,
|
|
||||||
DetailsTable
|
|
||||||
} from '../../components/details/Details';
|
|
||||||
import DetailsBadge from '../../components/details/DetailsBadge';
|
import DetailsBadge from '../../components/details/DetailsBadge';
|
||||||
import { DetailsImage } from '../../components/details/DetailsImage';
|
|
||||||
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
|
||||||
import {
|
import {
|
||||||
BarcodeActionDropdown,
|
BarcodeActionDropdown,
|
||||||
CancelItemAction,
|
CancelItemAction,
|
||||||
|
|
@ -37,9 +30,6 @@ import AttachmentPanel from '../../components/panels/AttachmentPanel';
|
||||||
import NotesPanel from '../../components/panels/NotesPanel';
|
import NotesPanel from '../../components/panels/NotesPanel';
|
||||||
import { PanelGroup } from '../../components/panels/PanelGroup';
|
import { PanelGroup } from '../../components/panels/PanelGroup';
|
||||||
import ParametersPanel from '../../components/panels/ParametersPanel';
|
import ParametersPanel from '../../components/panels/ParametersPanel';
|
||||||
import { RenderAddress } from '../../components/render/Company';
|
|
||||||
import { RenderUser } from '../../components/render/User';
|
|
||||||
import { formatDate } from '../../defaults/formatters';
|
|
||||||
import {
|
import {
|
||||||
useCheckShipmentForm,
|
useCheckShipmentForm,
|
||||||
useCompleteShipmentForm,
|
useCompleteShipmentForm,
|
||||||
|
|
@ -53,6 +43,7 @@ import {
|
||||||
import { useInstance } from '../../hooks/UseInstance';
|
import { useInstance } from '../../hooks/UseInstance';
|
||||||
import { useUserState } from '../../states/UserState';
|
import { useUserState } from '../../states/UserState';
|
||||||
import SalesOrderAllocationTable from '../../tables/sales/SalesOrderAllocationTable';
|
import SalesOrderAllocationTable from '../../tables/sales/SalesOrderAllocationTable';
|
||||||
|
import { SalesOrderShipmentDetailsPanel } from './SalesOrderShipmentDetailsPanel';
|
||||||
|
|
||||||
export default function SalesOrderShipmentDetail() {
|
export default function SalesOrderShipmentDetail() {
|
||||||
const { id } = useParams();
|
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 isPending = useMemo(() => !shipment.shipment_date, [shipment]);
|
||||||
|
|
||||||
const isChecked = useMemo(() => !!shipment.checked_by, [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(() => {
|
const shipmentPanels: PanelType[] = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: 'detail',
|
name: 'detail',
|
||||||
label: t`Shipment Details`,
|
label: t`Shipment Details`,
|
||||||
icon: <IconInfoCircle />,
|
icon: <IconInfoCircle />,
|
||||||
content: detailsPanel
|
content: (
|
||||||
|
<SalesOrderShipmentDetailsPanel
|
||||||
|
instance={shipment}
|
||||||
|
refreshInstance={refreshShipment}
|
||||||
|
/>
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'items',
|
name: 'items',
|
||||||
|
|
@ -288,7 +110,7 @@ export default function SalesOrderShipmentDetail() {
|
||||||
has_note: !!shipment.notes
|
has_note: !!shipment.notes
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
}, [isPending, shipment, detailsPanel]);
|
}, [isPending, shipment]);
|
||||||
|
|
||||||
const editShipmentFields = useSalesOrderShipmentFields({
|
const editShipmentFields = useSalesOrderShipmentFields({
|
||||||
pending: isPending,
|
pending: isPending,
|
||||||
|
|
@ -454,7 +276,7 @@ export default function SalesOrderShipmentDetail() {
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
badges={shipmentBadges}
|
badges={shipmentBadges}
|
||||||
imageUrl={customer?.image}
|
imageUrl={shipment.order_detail?.customer_detail?.image}
|
||||||
editAction={editShipment.open}
|
editAction={editShipment.open}
|
||||||
editEnabled={user.hasChangePermission(ModelType.salesordershipment)}
|
editEnabled={user.hasChangePermission(ModelType.salesordershipment)}
|
||||||
actions={shipmentActions}
|
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 { StockOperationProps } from '@lib/types/Forms';
|
||||||
import type { PanelType } from '@lib/types/Panel';
|
import type { PanelType } from '@lib/types/Panel';
|
||||||
import { t } from '@lingui/core/macro';
|
import { t } from '@lingui/core/macro';
|
||||||
import { Group, Skeleton, Stack } from '@mantine/core';
|
import { Skeleton, Stack } from '@mantine/core';
|
||||||
import {
|
import {
|
||||||
IconCalendar,
|
IconCalendar,
|
||||||
IconInfoCircle,
|
IconInfoCircle,
|
||||||
|
|
@ -24,11 +24,6 @@ import { useBarcodeScanDialog } from '../../components/barcodes/BarcodeScanDialo
|
||||||
import AdminButton from '../../components/buttons/AdminButton';
|
import AdminButton from '../../components/buttons/AdminButton';
|
||||||
import { PrintingActions } from '../../components/buttons/PrintingActions';
|
import { PrintingActions } from '../../components/buttons/PrintingActions';
|
||||||
import OrderCalendar from '../../components/calendar/OrderCalendar';
|
import OrderCalendar from '../../components/calendar/OrderCalendar';
|
||||||
import {
|
|
||||||
type DetailsField,
|
|
||||||
DetailsTable
|
|
||||||
} from '../../components/details/Details';
|
|
||||||
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
|
||||||
import {
|
import {
|
||||||
BarcodeActionDropdown,
|
BarcodeActionDropdown,
|
||||||
DeleteItemAction,
|
DeleteItemAction,
|
||||||
|
|
@ -60,6 +55,7 @@ import StockLocationParametricTable from '../../tables/stock/StockLocationParame
|
||||||
import { StockLocationTable } from '../../tables/stock/StockLocationTable';
|
import { StockLocationTable } from '../../tables/stock/StockLocationTable';
|
||||||
import TransferOrderParametricTable from '../../tables/stock/TransferOrderParametricTable';
|
import TransferOrderParametricTable from '../../tables/stock/TransferOrderParametricTable';
|
||||||
import { TransferOrderTable } from '../../tables/stock/TransferOrderTable';
|
import { TransferOrderTable } from '../../tables/stock/TransferOrderTable';
|
||||||
|
import { StockLocationDetailsPanel } from './StockLocationDetailsPanel';
|
||||||
|
|
||||||
function TransferOrderCalendar() {
|
function TransferOrderCalendar() {
|
||||||
const calendarFilters: TableFilter[] = useMemo(() => {
|
const calendarFilters: TableFilter[] = useMemo(() => {
|
||||||
|
|
@ -105,92 +101,12 @@ export default function Stock() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const detailsPanel = useMemo(() => {
|
const detailsPanel =
|
||||||
if (id && instanceQuery.isFetching) {
|
id && instanceQuery.isFetching ? (
|
||||||
return <Skeleton />;
|
<Skeleton />
|
||||||
}
|
) : (
|
||||||
|
<StockLocationDetailsPanel instance={id ? location : undefined} />
|
||||||
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>
|
|
||||||
);
|
);
|
||||||
}, [location, instanceQuery]);
|
|
||||||
|
|
||||||
const [sublocationView, setSublocationView] = useState<string>('table');
|
const [sublocationView, setSublocationView] = useState<string>('table');
|
||||||
const [transferOrderView, setTransferOrderView] = useState<string>('table');
|
const [transferOrderView, setTransferOrderView] = useState<string>('table');
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,12 @@
|
||||||
import { t } from '@lingui/core/macro';
|
import { t } from '@lingui/core/macro';
|
||||||
|
import { Accordion, Skeleton, Stack } from '@mantine/core';
|
||||||
import {
|
import {
|
||||||
Accordion,
|
|
||||||
Button,
|
|
||||||
Grid,
|
|
||||||
Group,
|
|
||||||
Skeleton,
|
|
||||||
Space,
|
|
||||||
Stack,
|
|
||||||
Text,
|
|
||||||
Tooltip
|
|
||||||
} from '@mantine/core';
|
|
||||||
import {
|
|
||||||
IconArrowLeft,
|
|
||||||
IconArrowRight,
|
|
||||||
IconBookmark,
|
IconBookmark,
|
||||||
IconBoxPadding,
|
IconBoxPadding,
|
||||||
IconChecklist,
|
IconChecklist,
|
||||||
IconHistory,
|
IconHistory,
|
||||||
IconInfoCircle,
|
IconInfoCircle,
|
||||||
IconPackages,
|
IconPackages,
|
||||||
IconSearch,
|
|
||||||
IconShoppingCart,
|
IconShoppingCart,
|
||||||
IconSitemap,
|
IconSitemap,
|
||||||
IconTransform
|
IconTransform
|
||||||
|
|
@ -28,27 +15,19 @@ import { useQuery } from '@tanstack/react-query';
|
||||||
import { type ReactNode, useMemo, useState } from 'react';
|
import { type ReactNode, useMemo, useState } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { ActionButton } from '@lib/components/ActionButton';
|
|
||||||
import { StylishText } from '@lib/components/StylishText';
|
import { StylishText } from '@lib/components/StylishText';
|
||||||
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||||
import { ModelType } from '@lib/enums/ModelType';
|
import { ModelType } from '@lib/enums/ModelType';
|
||||||
import { UserRoles } from '@lib/enums/Roles';
|
import { UserRoles } from '@lib/enums/Roles';
|
||||||
import { apiUrl } from '@lib/functions/Api';
|
import { apiUrl } from '@lib/functions/Api';
|
||||||
import { getDetailUrl, getOverviewUrl } from '@lib/functions/Navigation';
|
import { getDetailUrl, getOverviewUrl } from '@lib/functions/Navigation';
|
||||||
import { TagsList } from '@lib/index';
|
|
||||||
import type { ApiFormFieldSet, StockOperationProps } from '@lib/types/Forms';
|
import type { ApiFormFieldSet, StockOperationProps } from '@lib/types/Forms';
|
||||||
import type { PanelType } from '@lib/types/Panel';
|
import type { PanelType } from '@lib/types/Panel';
|
||||||
import { notifications } from '@mantine/notifications';
|
import { notifications } from '@mantine/notifications';
|
||||||
import { useBarcodeScanDialog } from '../../components/barcodes/BarcodeScanDialog';
|
import { useBarcodeScanDialog } from '../../components/barcodes/BarcodeScanDialog';
|
||||||
import AdminButton from '../../components/buttons/AdminButton';
|
import AdminButton from '../../components/buttons/AdminButton';
|
||||||
import { PrintingActions } from '../../components/buttons/PrintingActions';
|
import { PrintingActions } from '../../components/buttons/PrintingActions';
|
||||||
import {
|
|
||||||
type DetailsField,
|
|
||||||
DetailsTable
|
|
||||||
} from '../../components/details/Details';
|
|
||||||
import DetailsBadge from '../../components/details/DetailsBadge';
|
import DetailsBadge from '../../components/details/DetailsBadge';
|
||||||
import { DetailsImage } from '../../components/details/DetailsImage';
|
|
||||||
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
|
||||||
import {
|
import {
|
||||||
ActionDropdown,
|
ActionDropdown,
|
||||||
BarcodeActionDropdown,
|
BarcodeActionDropdown,
|
||||||
|
|
@ -67,9 +46,8 @@ import LocateItemButton from '../../components/plugins/LocateItemButton';
|
||||||
import { StatusRenderer } from '../../components/render/StatusRenderer';
|
import { StatusRenderer } from '../../components/render/StatusRenderer';
|
||||||
import OrderPartsWizard from '../../components/wizards/OrderPartsWizard';
|
import OrderPartsWizard from '../../components/wizards/OrderPartsWizard';
|
||||||
import { useApi } from '../../contexts/ApiContext';
|
import { useApi } from '../../contexts/ApiContext';
|
||||||
import { formatCurrency, formatDecimal } from '../../defaults/formatters';
|
import { formatDecimal } from '../../defaults/formatters';
|
||||||
import {
|
import {
|
||||||
useFindSerialNumberForm,
|
|
||||||
useStockFields,
|
useStockFields,
|
||||||
useStockItemSerializeFields
|
useStockItemSerializeFields
|
||||||
} from '../../forms/StockForms';
|
} from '../../forms/StockForms';
|
||||||
|
|
@ -90,6 +68,7 @@ import { StockItemTable } from '../../tables/stock/StockItemTable';
|
||||||
import StockItemTestResultTable from '../../tables/stock/StockItemTestResultTable';
|
import StockItemTestResultTable from '../../tables/stock/StockItemTestResultTable';
|
||||||
import { StockTrackingTable } from '../../tables/stock/StockTrackingTable';
|
import { StockTrackingTable } from '../../tables/stock/StockTrackingTable';
|
||||||
import TransferOrderAllocationTable from '../../tables/stock/TransferOrderAllocationTable';
|
import TransferOrderAllocationTable from '../../tables/stock/TransferOrderAllocationTable';
|
||||||
|
import { StockDetailsPanel } from './StockDetailsPanel';
|
||||||
|
|
||||||
export default function StockDetail() {
|
export default function StockDetail() {
|
||||||
const { id } = useParams();
|
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,
|
endpoint: ApiEndpoints.part_list,
|
||||||
pk: stockitem?.part,
|
pk: stockitem?.part,
|
||||||
hasPrimaryKey: true,
|
hasPrimaryKey: true,
|
||||||
defaultValue: {}
|
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(() => {
|
const showBuildAllocations: boolean = useMemo(() => {
|
||||||
// Determine if "build allocations" should be shown for this stock item
|
// Determine if "build allocations" should be shown for this stock item
|
||||||
return (
|
return (
|
||||||
|
|
@ -555,7 +180,14 @@ export default function StockDetail() {
|
||||||
name: 'details',
|
name: 'details',
|
||||||
label: t`Stock Details`,
|
label: t`Stock Details`,
|
||||||
icon: <IconInfoCircle />,
|
icon: <IconInfoCircle />,
|
||||||
content: detailsPanel
|
content: (
|
||||||
|
<StockDetailsPanel
|
||||||
|
instance={stockitem}
|
||||||
|
allowImageEdit
|
||||||
|
showSerialNav
|
||||||
|
refreshInstance={refreshInstance}
|
||||||
|
/>
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'tracking',
|
name: 'tracking',
|
||||||
|
|
@ -686,8 +318,6 @@ export default function StockDetail() {
|
||||||
showBuildAllocations,
|
showBuildAllocations,
|
||||||
showInstalledItems,
|
showInstalledItems,
|
||||||
stockitem,
|
stockitem,
|
||||||
serialNumbers,
|
|
||||||
serialNumbersQuery,
|
|
||||||
id,
|
id,
|
||||||
user
|
user
|
||||||
]);
|
]);
|
||||||
|
|
@ -1063,7 +693,6 @@ export default function StockDetail() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{findBySerialNumber.modal}
|
|
||||||
{scanIntoLocation.dialog}
|
{scanIntoLocation.dialog}
|
||||||
<InstanceDetail
|
<InstanceDetail
|
||||||
query={instanceQuery}
|
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 { 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 { type ReactNode, useMemo } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||||
import { ModelType } from '@lib/enums/ModelType';
|
import { ModelType } from '@lib/enums/ModelType';
|
||||||
import { UserRoles } from '@lib/enums/Roles';
|
import { UserRoles } from '@lib/enums/Roles';
|
||||||
import { type PanelType, TagsList, apiUrl } from '@lib/index';
|
import { type PanelType, apiUrl } from '@lib/index';
|
||||||
import {
|
import {
|
||||||
IconBookmark,
|
IconBookmark,
|
||||||
IconInfoCircle,
|
IconInfoCircle,
|
||||||
|
|
@ -16,11 +16,6 @@ import {
|
||||||
import AdminButton from '../../components/buttons/AdminButton';
|
import AdminButton from '../../components/buttons/AdminButton';
|
||||||
import PrimaryActionButton from '../../components/buttons/PrimaryActionButton';
|
import PrimaryActionButton from '../../components/buttons/PrimaryActionButton';
|
||||||
import { PrintingActions } from '../../components/buttons/PrintingActions';
|
import { PrintingActions } from '../../components/buttons/PrintingActions';
|
||||||
import {
|
|
||||||
type DetailsField,
|
|
||||||
DetailsTable
|
|
||||||
} from '../../components/details/Details';
|
|
||||||
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
|
||||||
import {
|
import {
|
||||||
BarcodeActionDropdown,
|
BarcodeActionDropdown,
|
||||||
CancelItemAction,
|
CancelItemAction,
|
||||||
|
|
@ -47,6 +42,7 @@ import { useGlobalSettingsState } from '../../states/SettingsStates';
|
||||||
import { useUserState } from '../../states/UserState';
|
import { useUserState } from '../../states/UserState';
|
||||||
import TransferOrderAllocationTable from '../../tables/stock/TransferOrderAllocationTable';
|
import TransferOrderAllocationTable from '../../tables/stock/TransferOrderAllocationTable';
|
||||||
import TransferOrderLineItemTable from '../../tables/stock/TransferOrderLineItemTable';
|
import TransferOrderLineItemTable from '../../tables/stock/TransferOrderLineItemTable';
|
||||||
|
import { TransferOrderDetailsPanel } from './TransferOrderDetailsPanel';
|
||||||
|
|
||||||
export default function TransferOrderDetail() {
|
export default function TransferOrderDetail() {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
|
|
@ -93,169 +89,11 @@ export default function TransferOrderDetail() {
|
||||||
);
|
);
|
||||||
}, [order, toStatus]);
|
}, [order, toStatus]);
|
||||||
|
|
||||||
const detailsPanel = useMemo(() => {
|
const detailsPanel = instanceQuery.isFetching ? (
|
||||||
if (instanceQuery.isFetching) {
|
<Skeleton />
|
||||||
return <Skeleton />;
|
) : (
|
||||||
}
|
<TransferOrderDetailsPanel instance={order} />
|
||||||
|
);
|
||||||
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 orderPanels: PanelType[] = useMemo(() => {
|
const orderPanels: PanelType[] = useMemo(() => {
|
||||||
return [
|
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 { resolveItem } from '@lib/functions/Conversion';
|
||||||
import { cancelEvent } from '@lib/functions/Events';
|
import { cancelEvent } from '@lib/functions/Events';
|
||||||
import { mapFields } from '@lib/functions/Forms';
|
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 { navigateToLink } from '@lib/functions/Navigation';
|
||||||
import { useStoredTableState } from '@lib/states/StoredTableState';
|
import { useStoredTableState } from '@lib/states/StoredTableState';
|
||||||
import type { TableFilter } from '@lib/types/Filters';
|
import type { TableFilter } from '@lib/types/Filters';
|
||||||
|
|
@ -37,6 +37,7 @@ import { useApi } from '../contexts/ApiContext';
|
||||||
import { extractAvailableFields } from '../functions/forms';
|
import { extractAvailableFields } from '../functions/forms';
|
||||||
import { showApiErrorMessage } from '../functions/notifications';
|
import { showApiErrorMessage } from '../functions/notifications';
|
||||||
import { useLocalState } from '../states/LocalState';
|
import { useLocalState } from '../states/LocalState';
|
||||||
|
import { usePreviewDrawerState } from '../states/PreviewDrawerState';
|
||||||
import { useUserSettingsState } from '../states/SettingsStates';
|
import { useUserSettingsState } from '../states/SettingsStates';
|
||||||
import { ColumnFilterPopover } from './FilterSelectDrawer';
|
import { ColumnFilterPopover } from './FilterSelectDrawer';
|
||||||
import InvenTreeTableHeader from './InvenTreeTableHeader';
|
import InvenTreeTableHeader from './InvenTreeTableHeader';
|
||||||
|
|
@ -700,6 +701,18 @@ export function InvenTreeTableInternal<T extends Record<string, any>>({
|
||||||
tableState.setRecords(tableData ?? apiData ?? []);
|
tableState.setRecords(tableData ?? apiData ?? []);
|
||||||
}, [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
|
// Callback when a cell is clicked
|
||||||
const handleCellClick = useCallback(
|
const handleCellClick = useCallback(
|
||||||
({
|
({
|
||||||
|
|
@ -732,7 +745,12 @@ export function InvenTreeTableInternal<T extends Record<string, any>>({
|
||||||
cancelEvent(event);
|
cancelEvent(event);
|
||||||
// If a model type is provided, navigate to the detail view for that model
|
// If a model type is provided, navigate to the detail view for that model
|
||||||
const url = getDetailUrl(tableProps.modelType, pk);
|
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 />,
|
icon: <IconArrowRight />,
|
||||||
onClick: (event: any) => {
|
onClick: (event: any) => {
|
||||||
cancelEvent(event);
|
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 { ModelType } from '@lib/enums/ModelType';
|
||||||
import { UserRoles } from '@lib/enums/Roles';
|
import { UserRoles } from '@lib/enums/Roles';
|
||||||
import { apiUrl } from '@lib/functions/Api';
|
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 useTable from '@lib/hooks/UseTable';
|
||||||
import type { TableFilter } from '@lib/types/Filters';
|
import type { TableFilter } from '@lib/types/Filters';
|
||||||
import { companyFields } from '../../forms/CompanyForms';
|
import { companyFields } from '../../forms/CompanyForms';
|
||||||
|
|
@ -16,6 +16,7 @@ import {
|
||||||
useCreateApiFormModal,
|
useCreateApiFormModal,
|
||||||
useEditApiFormModal
|
useEditApiFormModal
|
||||||
} from '../../hooks/UseForm';
|
} from '../../hooks/UseForm';
|
||||||
|
import { usePreviewDrawerState } from '../../states/PreviewDrawerState';
|
||||||
import { useUserState } from '../../states/UserState';
|
import { useUserState } from '../../states/UserState';
|
||||||
import {
|
import {
|
||||||
BooleanColumn,
|
BooleanColumn,
|
||||||
|
|
@ -49,6 +50,7 @@ export function CompanyTable({
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const user = useUserState();
|
const user = useUserState();
|
||||||
|
const previewDrawer = usePreviewDrawerState();
|
||||||
|
|
||||||
const columns = useMemo(() => {
|
const columns = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
|
|
@ -166,10 +168,16 @@ export function CompanyTable({
|
||||||
params: {
|
params: {
|
||||||
...params
|
...params
|
||||||
},
|
},
|
||||||
onRowClick: (record: any, index: number, event: any) => {
|
onRowClick: (record: any, _index: number, event: any) => {
|
||||||
if (record.pk) {
|
if (!record.pk) return;
|
||||||
const base = path ?? 'company';
|
if (eventModified(event as any)) {
|
||||||
navigateToLink(`/${base}/${record.pk}`, navigate, event);
|
navigateToLink(
|
||||||
|
`/${path ?? 'company'}/${record.pk}`,
|
||||||
|
navigate,
|
||||||
|
event
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
previewDrawer.openPreview(ModelType.company, Number(record.pk));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modelType: ModelType.company,
|
modelType: ModelType.company,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import { cancelEvent } from '@lib/functions/Events';
|
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 useTable from '@lib/hooks/UseTable';
|
||||||
import {
|
import {
|
||||||
ApiEndpoints,
|
ApiEndpoints,
|
||||||
|
|
@ -7,9 +9,7 @@ import {
|
||||||
UserRoles,
|
UserRoles,
|
||||||
YesNoButton,
|
YesNoButton,
|
||||||
apiUrl,
|
apiUrl,
|
||||||
formatDecimal,
|
formatDecimal
|
||||||
getDetailUrl,
|
|
||||||
navigateToLink
|
|
||||||
} from '@lib/index';
|
} from '@lib/index';
|
||||||
import type { TableFilter } from '@lib/types/Filters';
|
import type { TableFilter } from '@lib/types/Filters';
|
||||||
import type { TableColumn } from '@lib/types/Tables';
|
import type { TableColumn } from '@lib/types/Tables';
|
||||||
|
|
@ -27,6 +27,7 @@ import {
|
||||||
useCreateApiFormModal,
|
useCreateApiFormModal,
|
||||||
useEditApiFormModal
|
useEditApiFormModal
|
||||||
} from '../../hooks/UseForm';
|
} from '../../hooks/UseForm';
|
||||||
|
import { openGlobalPreview } from '../../states/PreviewDrawerState';
|
||||||
import { useUserState } from '../../states/UserState';
|
import { useUserState } from '../../states/UserState';
|
||||||
import { InvenTreeTable } from '../InvenTreeTable';
|
import { InvenTreeTable } from '../InvenTreeTable';
|
||||||
import { TableHoverCard } from '../TableHoverCard';
|
import { TableHoverCard } from '../TableHoverCard';
|
||||||
|
|
@ -472,9 +473,12 @@ export default function ParametricDataTable({
|
||||||
const col = column as any;
|
const col = column as any;
|
||||||
onParameterClick(col.extra.template, record);
|
onParameterClick(col.extra.template, record);
|
||||||
} else if (record?.pk) {
|
} else if (record?.pk) {
|
||||||
// Navigate through to the detail page
|
|
||||||
const url = getDetailUrl(modelType, record.pk);
|
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