2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-18 04:55:44 +00:00
Files
InvenTree/src/frontend/src/pages/stock/LocationDetail.tsx
2024-08-13 09:22:01 +00:00

410 lines
11 KiB
TypeScript

import { t } from '@lingui/macro';
import { Group, Skeleton, Stack, Text } from '@mantine/core';
import {
IconDots,
IconInfoCircle,
IconPackages,
IconSitemap
} from '@tabler/icons-react';
import { useMemo, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { ActionButton } from '../../components/buttons/ActionButton';
import AdminButton from '../../components/buttons/AdminButton';
import { PrintingActions } from '../../components/buttons/PrintingActions';
import { DetailsField, DetailsTable } from '../../components/details/Details';
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
import {
ActionDropdown,
BarcodeActionDropdown,
DeleteItemAction,
EditItemAction,
LinkBarcodeAction,
UnlinkBarcodeAction,
ViewBarcodeAction
} from '../../components/items/ActionDropdown';
import { ApiIcon } from '../../components/items/ApiIcon';
import InstanceDetail from '../../components/nav/InstanceDetail';
import NavigationTree from '../../components/nav/NavigationTree';
import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles';
import {
StockOperationProps,
stockLocationFields,
useCountStockItem,
useTransferStockItem
} from '../../forms/StockForms';
import { InvenTreeIcon } from '../../functions/icons';
import { getDetailUrl } from '../../functions/urls';
import {
useDeleteApiFormModal,
useEditApiFormModal
} from '../../hooks/UseForm';
import { useInstance } from '../../hooks/UseInstance';
import { useUserState } from '../../states/UserState';
import { PartListTable } from '../../tables/part/PartTable';
import { StockItemTable } from '../../tables/stock/StockItemTable';
import { StockLocationTable } from '../../tables/stock/StockLocationTable';
export default function Stock() {
const { id: _id } = useParams();
const id = useMemo(
() => (!isNaN(parseInt(_id || '')) ? _id : undefined),
[_id]
);
const navigate = useNavigate();
const user = useUserState();
const [treeOpen, setTreeOpen] = useState(false);
const {
instance: location,
refreshInstance,
instanceQuery,
requestStatus
} = useInstance({
endpoint: ApiEndpoints.stock_location_list,
hasPrimaryKey: true,
pk: id,
params: {
path_detail: true
}
});
const detailsPanel = useMemo(() => {
if (id && instanceQuery.isFetching) {
return <Skeleton />;
}
let 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
}
];
let 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} />
) : (
<Text>{t`Top level stock location`}</Text>
)}
{id && location?.pk && <DetailsTable item={location} fields={right} />}
</ItemDetailsGrid>
);
}, [location, instanceQuery]);
const locationPanels: PanelType[] = useMemo(() => {
return [
{
name: 'details',
label: t`Location Details`,
icon: <IconInfoCircle />,
content: detailsPanel
},
{
name: 'stock-items',
label: t`Stock Items`,
icon: <IconPackages />,
content: (
<StockItemTable
tableName="location-stock"
allowAdd
params={{
location: id
}}
/>
)
},
{
name: 'sublocations',
label: t`Stock Locations`,
icon: <IconSitemap />,
content: <StockLocationTable parentId={id} />
},
{
name: 'default_parts',
label: t`Default Parts`,
icon: <IconPackages />,
hidden: !location.pk,
content: (
<PartListTable
props={{
params: {
default_location: location.pk
}
}}
/>
)
}
];
}, [location, id]);
const editLocation = useEditApiFormModal({
url: ApiEndpoints.stock_location_list,
pk: id,
title: t`Edit Stock Location`,
fields: stockLocationFields(),
onFormSuccess: refreshInstance
});
const deleteOptions = useMemo(() => {
return [
{
value: 0,
display_name: `Move items to parent location`
},
{
value: 1,
display_name: t`Delete items`
}
];
}, []);
const deleteLocation = useDeleteApiFormModal({
url: ApiEndpoints.stock_location_list,
pk: id,
title: t`Delete Stock Location`,
fields: {
delete_stock_items: {
label: t`Items Action`,
description: t`Action for stock items in this location`,
field_type: 'choice',
choices: deleteOptions
},
delete_sub_location: {
label: t`Child Locations Action`,
description: t`Action for child locations in this location`,
field_type: 'choice',
choices: deleteOptions
}
},
onFormSuccess: () => {
if (location.parent) {
navigate(getDetailUrl(ModelType.stocklocation, location.parent));
} else {
navigate('/stock/');
}
}
});
const stockItemActionProps: StockOperationProps = useMemo(() => {
return {
pk: location.pk,
model: 'location',
refresh: refreshInstance,
filters: {
in_stock: true
}
};
}, [location]);
const transferStockItems = useTransferStockItem(stockItemActionProps);
const countStockItems = useCountStockItem(stockItemActionProps);
const locationActions = useMemo(
() => [
<AdminButton model={ModelType.stocklocation} pk={location.pk} />,
<ActionButton
icon={<InvenTreeIcon icon="stocktake" />}
variant="outline"
size="lg"
/>,
location.pk ? (
<BarcodeActionDropdown
actions={[
ViewBarcodeAction({
model: ModelType.stocklocation,
pk: location.pk
}),
LinkBarcodeAction({}),
UnlinkBarcodeAction({}),
{
name: 'Scan in stock items',
icon: <InvenTreeIcon icon="stock" />,
tooltip: 'Scan items'
},
{
name: 'Scan in container',
icon: <InvenTreeIcon icon="unallocated_stock" />,
tooltip: 'Scan container'
}
]}
/>
) : null,
<PrintingActions
modelType={ModelType.stocklocation}
items={[location.pk ?? 0]}
hidden={!location?.pk}
enableLabels
enableReports
/>,
<ActionDropdown
tooltip={t`Stock Actions`}
icon={<InvenTreeIcon icon="stock" />}
actions={[
{
name: 'Count Stock',
icon: (
<InvenTreeIcon icon="stocktake" iconProps={{ color: 'blue' }} />
),
tooltip: 'Count Stock',
onClick: () => countStockItems.open()
},
{
name: 'Transfer Stock',
icon: (
<InvenTreeIcon icon="transfer" iconProps={{ color: 'blue' }} />
),
tooltip: 'Transfer Stock',
onClick: () => transferStockItems.open()
}
]}
/>,
<ActionDropdown
tooltip={t`Location Actions`}
icon={<IconDots />}
actions={[
EditItemAction({
hidden: !id || !user.hasChangeRole(UserRoles.stock_location),
tooltip: t`Edit Stock Location`,
onClick: () => editLocation.open()
}),
DeleteItemAction({
hidden: !id || !user.hasDeleteRole(UserRoles.stock_location),
tooltip: t`Delete Stock Location`,
onClick: () => deleteLocation.open()
})
]}
/>
],
[location, id, user]
);
const breadcrumbs = useMemo(
() => [
{ name: t`Stock`, url: '/stock' },
...(location.path ?? []).map((l: any) => ({
name: l.name,
url: getDetailUrl(ModelType.stocklocation, l.pk),
icon: l.icon ? <ApiIcon name={l.icon} /> : undefined
}))
],
[location]
);
return (
<>
{editLocation.modal}
{deleteLocation.modal}
<InstanceDetail
status={requestStatus}
loading={id ? instanceQuery.isFetching : false}
>
<Stack>
<NavigationTree
title={t`Stock Locations`}
modelType={ModelType.stocklocation}
endpoint={ApiEndpoints.stock_location_tree}
opened={treeOpen}
onClose={() => setTreeOpen(false)}
selectedId={location?.pk}
/>
<PageDetail
title={t`Stock Items`}
subtitle={location?.name}
icon={location?.icon && <ApiIcon name={location?.icon} />}
actions={locationActions}
breadcrumbs={breadcrumbs}
breadcrumbAction={() => {
setTreeOpen(true);
}}
/>
<PanelGroup
pageKey="stocklocation"
panels={locationPanels}
targetModel={ModelType.stocklocation}
targetId={location.pk}
targetInstance={location}
/>
{transferStockItems.modal}
{countStockItems.modal}
</Stack>
</InstanceDetail>
</>
);
}