[PUI] Table Updates (#7783)

* Refactor part display in tables

- Remove legacy code

* Remove unused function

* Refactoring for PurchaseOrderLineItemTable

* Implement sales order line item table

* Add placeholders for row actions

* Implement table actions

* Add placeholder action to allocate stock
This commit is contained in:
Oliver 2024-08-02 09:13:50 +10:00 committed by GitHub
parent 21511c74ff
commit e5fabc6788
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 327 additions and 65 deletions

View File

@ -50,49 +50,3 @@ export function Thumbnail({
</Group>
);
}
export function ThumbnailHoverCard({
src,
text,
link = '',
alt = t`Thumbnail`,
size = 20
}: {
src: string;
text: string;
link?: string;
alt?: string;
size?: number;
}) {
const card = useMemo(() => {
return (
<Group justify="left" gap={10} wrap="nowrap">
<Thumbnail src={src} alt={alt} size={size} />
<Text>{text}</Text>
</Group>
);
}, [src, text, alt, size]);
if (link) {
return (
<Anchor href={link} style={{ textDecoration: 'none' }}>
{card}
</Anchor>
);
}
return <div>{card}</div>;
}
export function PartHoverCard({ part }: { part: any }) {
return part ? (
<ThumbnailHoverCard
src={part.thumbnail || part.image}
text={part.full_name}
alt={part.description}
link=""
/>
) : (
<Skeleton />
);
}

View File

@ -47,6 +47,43 @@ export function useSalesOrderFields(): ApiFormFieldSet {
}, []);
}
export function useSalesOrderLineItemFields({
customerId,
orderId,
create
}: {
customerId?: number;
orderId?: number;
create?: boolean;
}): ApiFormFieldSet {
const fields = useMemo(() => {
return {
order: {
filters: {
customer_detail: true
},
disabled: true,
value: create ? orderId : undefined
},
part: {
filters: {
active: true,
salable: true
}
},
reference: {},
quantity: {},
sale_price: {},
sale_price_currency: {},
target_date: {},
notes: {},
link: {}
};
}, []);
return fields;
}
export function useReturnOrderFields(): ApiFormFieldSet {
return useMemo(() => {
return {

View File

@ -47,6 +47,7 @@ import { useInstance } from '../../hooks/UseInstance';
import { useUserState } from '../../states/UserState';
import { BuildOrderTable } from '../../tables/build/BuildOrderTable';
import { AttachmentTable } from '../../tables/general/AttachmentTable';
import SalesOrderLineItemTable from '../../tables/sales/SalesOrderLineItemTable';
/**
* Detail page for a single SalesOrder
@ -249,7 +250,12 @@ export default function SalesOrderDetail() {
name: 'line-items',
label: t`Line Items`,
icon: <IconList />,
content: <PlaceholderPanel />
content: (
<SalesOrderLineItemTable
orderId={order.pk}
customerId={order.customer}
/>
)
},
{
name: 'pending-shipments',

View File

@ -2,14 +2,13 @@ import { t } from '@lingui/macro';
import { Group, Text } from '@mantine/core';
import { useMemo } from 'react';
import { PartHoverCard } from '../../components/images/Thumbnail';
import { formatDecimal } from '../../defaults/formatters';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState';
import { TableColumn } from '../Column';
import { ReferenceColumn } from '../ColumnRenderers';
import { PartColumn, ReferenceColumn } from '../ColumnRenderers';
import { TableFilter } from '../Filter';
import { InvenTreeTable } from '../InvenTreeTable';
@ -31,12 +30,14 @@ export function UsedInTable({
accessor: 'part',
switchable: false,
sortable: true,
render: (record: any) => <PartHoverCard part={record.part_detail} />
title: t`Assembly`,
render: (record: any) => PartColumn(record.part_detail)
},
{
accessor: 'sub_part',
sortable: true,
render: (record: any) => <PartHoverCard part={record.sub_part_detail} />
title: t`Component`,
render: (record: any) => PartColumn(record.sub_part_detail)
},
{
accessor: 'quantity',

View File

@ -7,7 +7,6 @@ import {
} from '@tabler/icons-react';
import { useCallback, useMemo } from 'react';
import { PartHoverCard } from '../../components/images/Thumbnail';
import { ProgressBar } from '../../components/items/ProgressBar';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
@ -15,7 +14,7 @@ import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';
import { TableColumn } from '../Column';
import { BooleanColumn } from '../ColumnRenderers';
import { BooleanColumn, PartColumn } from '../ColumnRenderers';
import { TableFilter } from '../Filter';
import { InvenTreeTable } from '../InvenTreeTable';
import { TableHoverCard } from '../TableHoverCard';
@ -131,7 +130,7 @@ export default function BuildLineTable({ params = {} }: { params?: any }) {
ordering: 'part',
sortable: true,
switchable: false,
render: (record: any) => <PartHoverCard part={record.part_detail} />
render: (record: any) => PartColumn(record.part_detail)
},
{
accessor: 'bom_item_detail.reference',

View File

@ -2,7 +2,6 @@ import { t } from '@lingui/macro';
import { useMemo } from 'react';
import { AddItemButton } from '../../components/buttons/AddItemButton';
import { PartHoverCard } from '../../components/images/Thumbnail';
import { ProgressBar } from '../../components/items/ProgressBar';
import { RenderUser } from '../../components/render/User';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
@ -22,6 +21,7 @@ import { TableColumn } from '../Column';
import {
CreationDateColumn,
DateColumn,
PartColumn,
ProjectCodeColumn,
ReferenceColumn,
ResponsibleColumn,
@ -41,7 +41,7 @@ function buildOrderTableColumns(): TableColumn[] {
accessor: 'part',
sortable: true,
switchable: false,
render: (record: any) => <PartHoverCard part={record.part_detail} />
render: (record: any) => PartColumn(record.part_detail)
},
{
accessor: 'title',

View File

@ -5,7 +5,6 @@ import { useCallback, useMemo, useState } from 'react';
import { ActionButton } from '../../components/buttons/ActionButton';
import { AddItemButton } from '../../components/buttons/AddItemButton';
import { Thumbnail } from '../../components/images/Thumbnail';
import ImporterDrawer from '../../components/importer/ImporterDrawer';
import { ProgressBar } from '../../components/items/ProgressBar';
import { RenderStockLocation } from '../../components/render/Stock';
@ -30,6 +29,7 @@ import {
CurrencyColumn,
LinkColumn,
NoteColumn,
PartColumn,
ReferenceColumn,
TargetDateColumn,
TotalPriceColumn
@ -124,14 +124,7 @@ export function PurchaseOrderLineItemTable({
title: t`Internal Part`,
sortable: true,
switchable: false,
render: (record: any) => {
return (
<Thumbnail
text={record?.part_detail?.name}
src={record?.part_detail?.thumbnail ?? record?.part_detail?.image}
/>
);
}
render: (record: any) => PartColumn(record.part_detail)
},
{
accessor: 'description',

View File

@ -0,0 +1,272 @@
import { t } from '@lingui/macro';
import { Text } from '@mantine/core';
import { IconSquareArrowRight } from '@tabler/icons-react';
import { ReactNode, useCallback, useMemo, useState } from 'react';
import { AddItemButton } from '../../components/buttons/AddItemButton';
import { ProgressBar } from '../../components/items/ProgressBar';
import { formatCurrency } from '../../defaults/formatters';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles';
import { useSalesOrderLineItemFields } from '../../forms/SalesOrderForms';
import {
useCreateApiFormModal,
useDeleteApiFormModal,
useEditApiFormModal
} from '../../hooks/UseForm';
import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';
import { TableColumn } from '../Column';
import { DateColumn, LinkColumn, PartColumn } from '../ColumnRenderers';
import { InvenTreeTable } from '../InvenTreeTable';
import {
RowDeleteAction,
RowDuplicateAction,
RowEditAction
} from '../RowActions';
import { TableHoverCard } from '../TableHoverCard';
export default function SalesOrderLineItemTable({
orderId,
customerId
}: {
orderId: number;
customerId: number;
}) {
const user = useUserState();
const table = useTable('sales-order-line-item');
const tableColumns: TableColumn[] = useMemo(() => {
return [
{
accessor: 'part',
sortable: true,
switchable: false,
render: (record: any) => PartColumn(record?.part_detail)
},
{
accessor: 'part_detail.IPN',
title: t`IPN`,
switchable: true
},
{
accessor: 'part_detail.description',
title: t`Description`,
sortable: false,
switchable: true
},
{
accessor: 'reference',
sortable: false,
switchable: true
},
{
accessor: 'quantity',
sortable: true
},
{
accessor: 'sale_price',
render: (record: any) =>
formatCurrency(record.sale_price, {
currency: record.sale_price_currency
})
},
{
accessor: 'total_price',
title: t`Total Price`,
render: (record: any) =>
formatCurrency(record.sale_price, {
currency: record.sale_price_currency,
multiplier: record.quantity
})
},
DateColumn({
accessor: 'target_date',
sortable: true,
title: t`Target Date`
}),
{
accessor: 'stock',
title: t`Available Stock`,
render: (record: any) => {
let part_stock = record?.available_stock ?? 0;
let variant_stock = record?.available_variant_stock ?? 0;
let available = part_stock + variant_stock;
let required = Math.max(
record.quantity - record.allocated - record.shipped,
0
);
let color: string | undefined = undefined;
let text: string = `${available}`;
let extra: ReactNode[] = [];
if (available <= 0) {
color = 'red';
text = t`No stock available`;
} else if (available < required) {
color = 'orange';
}
if (variant_stock > 0) {
extra.push(<Text size="sm">{t`Includes variant stock`}</Text>);
}
return (
<TableHoverCard
value={<Text color={color}>{text}</Text>}
extra={extra}
title={t`Stock Information`}
/>
);
}
},
{
accessor: 'allocated',
render: (record: any) => (
<ProgressBar
progressLabel={true}
value={record.allocated}
maximum={record.quantity}
/>
)
},
{
accessor: 'shipped',
render: (record: any) => (
<ProgressBar
progressLabel={true}
value={record.shipped}
maximum={record.quantity}
/>
)
},
{
accessor: 'notes'
},
LinkColumn({
accessor: 'link'
})
];
}, []);
const [selectedLine, setSelectedLine] = useState<number>(0);
const [initialData, setInitialData] = useState({});
const createLineFields = useSalesOrderLineItemFields({
orderId: orderId,
customerId: customerId,
create: true
});
const newLine = useCreateApiFormModal({
url: ApiEndpoints.sales_order_line_list,
title: t`Add Line Item`,
fields: createLineFields,
initialData: initialData,
table: table
});
const editLineFields = useSalesOrderLineItemFields({
orderId: orderId,
customerId: customerId,
create: false
});
const editLine = useEditApiFormModal({
url: ApiEndpoints.sales_order_line_list,
pk: selectedLine,
title: t`Edit Line Item`,
fields: editLineFields,
table: table
});
const deleteLine = useDeleteApiFormModal({
url: ApiEndpoints.sales_order_line_list,
pk: selectedLine,
title: t`Delete Line Item`,
table: table
});
const tableActions = useMemo(() => {
return [
<AddItemButton
tooltip={t`Add line item`}
onClick={() => {
setInitialData({
order: orderId
});
newLine.open();
}}
hidden={!user.hasAddRole(UserRoles.sales_order)}
/>
];
}, [user]);
const rowActions = useCallback(
(record: any) => {
const allocated = (record?.allocated ?? 0) > (record?.quantity ?? 0);
return [
{
hidden: allocated || !user.hasChangeRole(UserRoles.sales_order),
title: t`Allocate stock`,
icon: <IconSquareArrowRight />,
color: 'green'
},
RowEditAction({
hidden: !user.hasChangeRole(UserRoles.sales_order),
onClick: () => {
setSelectedLine(record.pk);
editLine.open();
}
}),
RowDuplicateAction({
hidden: !user.hasAddRole(UserRoles.sales_order),
onClick: () => {
setInitialData(record);
newLine.open();
}
}),
RowDeleteAction({
hidden: !user.hasDeleteRole(UserRoles.sales_order),
onClick: () => {
setSelectedLine(record.pk);
deleteLine.open();
}
})
];
},
[user]
);
return (
<>
{editLine.modal}
{deleteLine.modal}
{newLine.modal}
<InvenTreeTable
url={apiUrl(ApiEndpoints.sales_order_line_list)}
tableState={table}
columns={tableColumns}
props={{
enableSelection: true,
enableDownload: true,
params: {
order: orderId,
part_detail: true
},
rowActions: rowActions,
tableActions: tableActions,
modelType: ModelType.part,
modelField: 'part'
}}
/>
</>
);
}