[React] PO detail pgae (#5847)

* Factor out common barcode actions

* Refactoring more icons

* Add PurchaseOrderLineItemTable component

* Improve renderer for SupplierPart

* Edit line item

* Table action column always visible

* Create <AddItemButton> component (refactoring)

* Table updates

- Improve actions column for table
- Move "download" button to right hand side

* Refactoring button components

* More cleanup

- Refactor <TableHoverCard> a bit
- Add placeholder for "receive items"

* Add ProgresBar component

* Make table columns switchable by default

- set switchable: false to disable

* Add project_code column to build table

* Fix row actions column for tables without actions

* Improve rendering for BuildOrderTable

* Cleanup unused imports

* Further fixes

* Remove another unused import
This commit is contained in:
Oliver 2023-11-04 14:02:13 +11:00 committed by GitHub
parent 19810d0965
commit 361fc097a7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 848 additions and 373 deletions

View File

@ -0,0 +1,44 @@
import { ActionIcon, Group, Tooltip } from '@mantine/core';
import { ReactNode } from 'react';
import { notYetImplemented } from '../../functions/notifications';
export type ActionButtonProps = {
icon?: ReactNode;
text?: string;
color?: string;
tooltip?: string;
variant?: string;
size?: number;
disabled?: boolean;
onClick?: any;
hidden?: boolean;
};
/**
* Construct a simple action button with consistent styling
*/
export function ActionButton(props: ActionButtonProps) {
return (
!props.hidden && (
<Tooltip
key={props.text ?? props.tooltip}
disabled={!props.tooltip && !props.text}
label={props.tooltip ?? props.text}
position="left"
>
<ActionIcon
disabled={props.disabled}
radius="xs"
color={props.color}
size={props.size}
onClick={props.onClick ?? notYetImplemented}
>
<Group spacing="xs" noWrap={true}>
{props.icon}
</Group>
</ActionIcon>
</Tooltip>
)
);
}

View File

@ -0,0 +1,10 @@
import { IconPlus } from '@tabler/icons-react';
import { ActionButton, ActionButtonProps } from './ActionButton';
/**
* A generic icon button which is used to add or create a new item
*/
export function AddItemButton(props: ActionButtonProps) {
return <ActionButton {...props} color="green" icon={<IconPlus />} />;
}

View File

@ -1,35 +0,0 @@
import { ActionIcon, Tooltip } from '@mantine/core';
/**
* Construct a simple action button with consistent styling
*/
export function ActionButton({
icon,
color = 'black',
tooltip = '',
disabled = false,
size = 18,
onClick
}: {
icon: any;
color?: string;
tooltip?: string;
variant?: string;
size?: number;
disabled?: boolean;
onClick?: any;
}) {
return (
<ActionIcon
disabled={disabled}
radius="xs"
color={color}
size={size}
onClick={onClick}
>
<Tooltip disabled={!tooltip} label={tooltip} position="left">
{icon}
</Tooltip>
</ActionIcon>
);
}

View File

@ -1,6 +1,13 @@
import { t } from '@lingui/macro';
import { ActionIcon, Menu, Tooltip } from '@mantine/core';
import { IconQrcode } from '@tabler/icons-react';
import {
IconCopy,
IconEdit,
IconLink,
IconQrcode,
IconTrash,
IconUnlink
} from '@tabler/icons-react';
import { ReactNode, useMemo } from 'react';
import { notYetImplemented } from '../../functions/notifications';
@ -81,3 +88,111 @@ export function BarcodeActionDropdown({
/>
);
}
// Common action button for viewing a barcode
export function ViewBarcodeAction({
disabled = false,
callback
}: {
disabled?: boolean;
callback?: () => void;
}): ActionDropdownItem {
return {
icon: <IconQrcode />,
name: t`View`,
tooltip: t`View barcode`,
onClick: callback,
disabled: disabled
};
}
// Common action button for linking a custom barcode
export function LinkBarcodeAction({
disabled = false,
callback
}: {
disabled?: boolean;
callback?: () => void;
}): ActionDropdownItem {
return {
icon: <IconLink />,
name: t`Link Barcode`,
tooltip: t`Link custom barcode`,
onClick: callback,
disabled: disabled
};
}
// Common action button for un-linking a custom barcode
export function UnlinkBarcodeAction({
disabled = false,
callback
}: {
disabled?: boolean;
callback?: () => void;
}): ActionDropdownItem {
return {
icon: <IconUnlink />,
name: t`Unlink Barcode`,
tooltip: t`Unlink custom barcode`,
onClick: callback,
disabled: disabled
};
}
// Common action button for editing an item
export function EditItemAction({
disabled = false,
tooltip,
callback
}: {
disabled?: boolean;
tooltip?: string;
callback?: () => void;
}): ActionDropdownItem {
return {
icon: <IconEdit color="blue" />,
name: t`Edit`,
tooltip: tooltip ?? `Edit item`,
onClick: callback,
disabled: disabled
};
}
// Common action button for deleting an item
export function DeleteItemAction({
disabled = false,
tooltip,
callback
}: {
disabled?: boolean;
tooltip?: string;
callback?: () => void;
}): ActionDropdownItem {
return {
icon: <IconTrash color="red" />,
name: t`Delete`,
tooltip: tooltip ?? t`Delete item`,
onClick: callback,
disabled: disabled
};
}
// Common action button for duplicating an item
export function DuplicateItemAction({
disabled = false,
tooltip,
callback
}: {
disabled?: boolean;
tooltip?: string;
callback?: () => void;
}): ActionDropdownItem {
return {
icon: <IconCopy color="green" />,
name: t`Duplicate`,
tooltip: tooltip ?? t`Duplicate item`,
onClick: callback,
disabled: disabled
};
}

View File

@ -0,0 +1,39 @@
import { Progress, Stack, Text } from '@mantine/core';
import { useMemo } from 'react';
export type ProgressBarProps = {
value: number;
maximum?: number;
label?: string;
progressLabel?: boolean;
};
/**
* A progress bar element, built on mantine.Progress
* The color of the bar is determined based on the value
*/
export function ProgressBar(props: ProgressBarProps) {
const progress = useMemo(() => {
let maximum = props.maximum ?? 100;
let value = Math.max(props.value, 0);
// Calculate progress as a percentage of the maximum value
return Math.min(100, (value / maximum) * 100);
}, [props]);
return (
<Stack spacing={2}>
{props.progressLabel && (
<Text align="center" size="xs">
{props.value} / {props.maximum}
</Text>
)}
<Progress
value={progress}
color={progress < 100 ? 'orange' : progress > 100 ? 'blue' : 'green'}
size="sm"
radius="xs"
/>
</Stack>
);
}

View File

@ -52,19 +52,19 @@ export function RenderContact({ instance }: { instance: any }): ReactNode {
* Inline rendering of a single SupplierPart instance
*/
export function RenderSupplierPart({ instance }: { instance: any }): ReactNode {
// TODO: Handle image
// TODO: handle URL
let supplier = instance.supplier_detail ?? {};
let part = instance.part_detail ?? {};
let text = instance.SKU;
if (supplier.name) {
text = `${supplier.name} | ${text}`;
}
return <RenderInlineModel primary={text} secondary={part.full_name} />;
return (
<RenderInlineModel
primary={supplier?.name}
secondary={instance.SKU}
image={part?.thumbnail ?? part?.image}
suffix={part.full_name}
/>
);
}
/**

View File

@ -5,14 +5,16 @@ import { RenderInlineModel } from './Instance';
export function RenderOwner({ instance }: { instance: any }): ReactNode {
// TODO: Icon based on user / group status?
return <RenderInlineModel primary={instance.name} />;
return instance && <RenderInlineModel primary={instance.name} />;
}
export function RenderUser({ instance }: { instance: any }): ReactNode {
return (
<RenderInlineModel
primary={instance.username}
secondary={`${instance.first_name} ${instance.last_name}`}
/>
instance && (
<RenderInlineModel
primary={instance.username}
secondary={`${instance.first_name} ${instance.last_name}`}
/>
)
);
}

View File

@ -17,7 +17,7 @@ export const PurchaseOrderRenderer = ({ pk }: { pk: string }) => {
return (
<GeneralRenderer
api_key={ApiPaths.purchase_order_list}
api_ref="pruchaseorder"
api_ref="purchaseorder"
link={`/order/purchase-order/${pk}`}
pk={pk}
renderer={DetailRenderer}

View File

@ -15,4 +15,5 @@ export type TableColumn = {
noWrap?: boolean; // Whether the column should wrap
ellipsis?: boolean; // Whether the column should be ellipsized
textAlignment?: 'left' | 'center' | 'right'; // The text alignment of the column
cellsStyle?: any; // The style of the cells in the column
};

View File

@ -23,7 +23,7 @@ export function TableColumnSelect({
<Menu.Dropdown>
<Menu.Label>{t`Select Columns`}</Menu.Label>
{columns
.filter((col) => col.switchable)
.filter((col) => col.switchable ?? true)
.map((col) => (
<Menu.Item key={col.accessor}>
<Checkbox

View File

@ -9,7 +9,7 @@ import { DataTable, DataTableSortStatus } from 'mantine-datatable';
import { useEffect, useMemo, useState } from 'react';
import { api } from '../../App';
import { ButtonMenu } from '../items/ButtonMenu';
import { ButtonMenu } from '../buttons/ButtonMenu';
import { TableColumn } from './Column';
import { TableColumnSelect } from './ColumnSelect';
import { DownloadAction } from './DownloadAction';
@ -82,7 +82,6 @@ const defaultInvenTreeTableProps: InvenTreeTableProps = {
customFilters: [],
customActionGroups: [],
idAccessor: 'pk',
rowActions: (record: any) => [],
onRowClick: (record: any, index: number, event: any) => {}
};
@ -115,7 +114,7 @@ export function InvenTreeTable({
// Check if any columns are switchable (can be hidden)
const hasSwitchableColumns = columns.some(
(col: TableColumn) => col.switchable
(col: TableColumn) => col.switchable ?? true
);
// A list of hidden columns, saved to local storage
@ -142,7 +141,7 @@ export function InvenTreeTable({
let cols = columns.map((col) => {
let hidden: boolean = col.hidden ?? false;
if (col.switchable) {
if (col.switchable ?? true) {
hidden = hiddenColumns.includes(col.accessor);
}
@ -156,10 +155,19 @@ export function InvenTreeTable({
if (tableProps.rowActions) {
cols.push({
accessor: 'actions',
title: '',
title: ' ',
hidden: false,
switchable: false,
width: 48,
width: 50,
cellsStyle: {
position: 'sticky',
right: 0,
// TODO: Use the theme color to set the background color
backgroundColor: '#FFF',
// TODO: Use the scroll area callbacks to determine if we need to display a "shadow"
borderLeft: '1px solid #DDD',
padding: '3px'
},
render: function (record: any) {
return (
<RowActions
@ -445,12 +453,6 @@ export function InvenTreeTable({
actions={tableProps.printingActions ?? []}
/>
)}
{tableProps.enableDownload && (
<DownloadAction
key="download-action"
downloadCallback={downloadData}
/>
)}
</Group>
<Space />
<Group position="right" spacing={5}>
@ -488,6 +490,12 @@ export function InvenTreeTable({
</ActionIcon>
</Indicator>
)}
{tableProps.enableDownload && (
<DownloadAction
key="download-action"
downloadCallback={downloadData}
/>
)}
</Group>
</Group>
{filtersVisible && (
@ -504,7 +512,7 @@ export function InvenTreeTable({
highlightOnHover
loaderVariant="dots"
idAccessor={tableProps.idAccessor}
minHeight={200}
minHeight={300}
totalRecords={recordCount}
recordsPerPage={tableProps.pageSize ?? defaultPageSize}
page={page}

View File

@ -15,7 +15,23 @@ export type RowAction = {
hidden?: boolean;
};
// Component for ediitng a row in a table
// Component for duplicating a row in a table
export function RowDuplicateAction({
onClick,
hidden
}: {
onClick?: () => void;
hidden?: boolean;
}): RowAction {
return {
title: t`Duplicate`,
color: 'green',
onClick: onClick,
hidden: hidden
};
}
// Component for editing a row in a table
export function RowEditAction({
onClick,
hidden
@ -25,7 +41,7 @@ export function RowEditAction({
}): RowAction {
return {
title: t`Edit`,
color: 'green',
color: 'blue',
onClick: onClick,
hidden: hidden
};

View File

@ -1,5 +1,6 @@
import { Divider, Group, HoverCard, Stack } from '@mantine/core';
import { Divider, Group, HoverCard, Stack, Text } from '@mantine/core';
import { IconInfoCircle } from '@tabler/icons-react';
import { ReactNode } from 'react';
/*
* A custom hovercard element for displaying extra information in a table cell.
@ -12,7 +13,7 @@ export function TableHoverCard({
title // The title of the hovercard
}: {
value: any;
extra?: any;
extra?: ReactNode;
title?: string;
}) {
// If no extra information presented, just return the raw value
@ -32,7 +33,7 @@ export function TableHoverCard({
<Stack spacing="xs">
<Group spacing="xs" position="left">
<IconInfoCircle size="16" color="blue" />
{title}
<Text weight="bold">{title}</Text>
</Group>
<Divider />
{extra}

View File

@ -1,5 +1,5 @@
import { t } from '@lingui/macro';
import { Stack, Text } from '@mantine/core';
import { Text } from '@mantine/core';
import { ReactNode, useCallback, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
@ -51,12 +51,11 @@ export function BomTable({
{
accessor: 'description',
title: t`Description`,
switchable: true,
render: (row) => row?.sub_part_detail?.description
},
{
accessor: 'reference',
switchable: true,
title: t`Reference`
},
{
@ -66,7 +65,7 @@ export function BomTable({
{
accessor: 'substitutes',
title: t`Substitutes`,
switchable: true,
render: (row) => {
let substitutes = row.substitutes ?? [];
@ -80,7 +79,7 @@ export function BomTable({
{
accessor: 'optional',
title: t`Optional`,
switchable: true,
sortable: true,
render: (row) => {
return <YesNoButton value={row.optional} />;
@ -89,7 +88,7 @@ export function BomTable({
{
accessor: 'consumable',
title: t`Consumable`,
switchable: true,
sortable: true,
render: (row) => {
return <YesNoButton value={row.consumable} />;
@ -98,7 +97,7 @@ export function BomTable({
{
accessor: 'allow_variants',
title: t`Allow Variants`,
switchable: true,
sortable: true,
render: (row) => {
return <YesNoButton value={row.allow_variants} />;
@ -107,7 +106,7 @@ export function BomTable({
{
accessor: 'inherited',
title: t`Gets Inherited`,
switchable: true,
sortable: true,
render: (row) => {
// TODO: Update complexity here
@ -117,7 +116,7 @@ export function BomTable({
{
accessor: 'price_range',
title: t`Price Range`,
switchable: true,
sortable: false,
render: (row) => {
let min_price = row.pricing_min || row.pricing_max;
@ -130,7 +129,7 @@ export function BomTable({
{
accessor: 'available_stock',
title: t`Available`,
switchable: true,
render: (row) => {
let extra: ReactNode[] = [];
@ -164,9 +163,7 @@ export function BomTable({
return (
<TableHoverCard
value={available_stock}
extra={
extra.length > 0 ? <Stack spacing="xs">{extra}</Stack> : null
}
extra={extra}
title={t`Available Stock`}
/>
);
@ -175,7 +172,7 @@ export function BomTable({
{
accessor: 'can_build',
title: t`Can Build`,
switchable: true,
sortable: true // TODO: Custom sorting via API
// TODO: Reference bom.js for canBuildQuantity method
},

View File

@ -73,8 +73,7 @@ export function UsedInTable({
{
accessor: 'reference',
title: t`Reference`,
sortable: true,
switchable: true
sortable: true
}
];
}, [partId]);

View File

@ -1,16 +1,19 @@
import { t } from '@lingui/macro';
import { Progress } from '@mantine/core';
import { Text } from '@mantine/core';
import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState';
import { ThumbnailHoverCard } from '../../images/Thumbnail';
import { ProgressBar } from '../../items/ProgressBar';
import { ModelType } from '../../render/ModelType';
import { RenderOwner, RenderUser } from '../../render/User';
import { TableStatusRenderer } from '../../renderers/StatusRenderer';
import { TableColumn } from '../Column';
import { TableFilter } from '../Filter';
import { InvenTreeTable } from '../InvenTreeTable';
import { TableHoverCard } from '../TableHoverCard';
/**
* Construct a list of columns for the build order table
@ -20,12 +23,13 @@ function buildOrderTableColumns(): TableColumn[] {
{
accessor: 'reference',
sortable: true,
switchable: false,
title: t`Reference`
// TODO: Link to the build order detail page
},
{
accessor: 'part',
sortable: true,
switchable: false,
title: t`Part`,
render: (record: any) => {
let part = record.part_detail;
@ -44,86 +48,87 @@ function buildOrderTableColumns(): TableColumn[] {
{
accessor: 'title',
sortable: false,
title: t`Description`,
switchable: true
},
{
accessor: 'project_code',
title: t`Project Code`,
sortable: true,
switchable: false,
hidden: true
// TODO: Hide this if project code is not enabled
// TODO: Custom render function here
title: t`Description`
},
{
accessor: 'quantity',
sortable: true,
title: t`Quantity`,
switchable: true
switchable: false
},
{
accessor: 'completed',
sortable: true,
title: t`Completed`,
render: (record: any) => {
let progress =
record.quantity <= 0 ? 0 : (100 * record.completed) / record.quantity;
return (
<Progress
value={progress}
label={record.completed}
color={progress < 100 ? 'blue' : 'green'}
size="xl"
radius="xl"
/>
);
}
render: (record: any) => (
<ProgressBar
progressLabel={true}
value={record.completed}
maximum={record.quantity}
/>
)
},
{
accessor: 'status',
sortable: true,
title: t`Status`,
switchable: true,
render: TableStatusRenderer(ModelType.build)
},
{
accessor: 'project_code',
title: t`Project Code`,
sortable: true,
// TODO: Hide this if project code is not enabled
render: (record: any) => {
let project = record.project_code_detail;
return project ? (
<TableHoverCard
value={project.code}
title={t`Project Code`}
extra={<Text>{project.description}</Text>}
/>
) : (
'-'
);
}
},
{
accessor: 'priority',
title: t`Priority`,
sortable: true,
switchable: true
sortable: true
},
{
accessor: 'creation_date',
sortable: true,
title: t`Created`,
switchable: true
title: t`Created`
},
{
accessor: 'target_date',
sortable: true,
title: t`Target Date`,
switchable: true
title: t`Target Date`
},
{
accessor: 'completion_date',
sortable: true,
title: t`Completed`,
switchable: true
title: t`Completed`
},
{
accessor: 'issued_by',
sortable: true,
title: t`Issued By`,
switchable: true
// TODO: custom render function
render: (record: any) => (
<RenderUser instance={record?.issued_by_detail} />
)
},
{
accessor: 'responsible',
sortable: true,
title: t`Responsible`,
switchable: true
// TODO: custom render function
render: (record: any) => (
<RenderOwner instance={record?.responsible_detail} />
)
}
];
}

View File

@ -45,7 +45,7 @@ function attachmentTableColumns(): TableColumn[] {
accessor: 'comment',
title: t`Comment`,
sortable: false,
switchable: true,
render: function (record: any) {
return record.comment;
}
@ -54,7 +54,7 @@ function attachmentTableColumns(): TableColumn[] {
accessor: 'uploaded',
title: t`Uploaded`,
sortable: false,
switchable: true,
render: function (record: any) {
return (
<Group position="apart">

View File

@ -45,14 +45,12 @@ export function CompanyTable({
{
accessor: 'description',
title: t`Description`,
sortable: false,
switchable: true
sortable: false
},
{
accessor: 'website',
title: t`Website`,
sortable: false,
switchable: true
sortable: false
}
];
}, []);

View File

@ -26,20 +26,17 @@ export function PartCategoryTable({ params = {} }: { params?: any }) {
{
accessor: 'description',
title: t`Description`,
sortable: false,
switchable: true
sortable: false
},
{
accessor: 'pathstring',
title: t`Path`,
sortable: false,
switchable: true
sortable: false
},
{
accessor: 'part_count',
title: t`Parts`,
sortable: true,
switchable: true
sortable: true
}
];
}, []);

View File

@ -1,6 +1,5 @@
import { t } from '@lingui/macro';
import { ActionIcon, Group, Text, Tooltip } from '@mantine/core';
import { IconTextPlus } from '@tabler/icons-react';
import { Group, Text } from '@mantine/core';
import { useCallback, useMemo } from 'react';
import {
@ -10,6 +9,7 @@ import {
} from '../../../functions/forms';
import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState';
import { AddItemButton } from '../../buttons/AddItemButton';
import { Thumbnail } from '../../images/Thumbnail';
import { YesNoButton } from '../../items/YesNoButton';
import { TableColumn } from '../Column';
@ -27,7 +27,7 @@ export function PartParameterTable({ partId }: { partId: any }) {
{
accessor: 'part',
title: t`Part`,
switchable: true,
sortable: true,
render: function (record: any) {
let part = record?.part_detail ?? {};
@ -59,7 +59,7 @@ export function PartParameterTable({ partId }: { partId: any }) {
accessor: 'description',
title: t`Description`,
sortable: false,
switchable: true,
render: (record) => record.template_detail?.description
},
{
@ -86,7 +86,7 @@ export function PartParameterTable({ partId }: { partId: any }) {
{
accessor: 'units',
title: t`Units`,
switchable: true,
sortable: true,
render: (record) => record.template_detail?.units
}
@ -174,11 +174,7 @@ export function PartParameterTable({ partId }: { partId: any }) {
// TODO: Hide if user does not have permission to edit parts
actions.push(
<Tooltip label={t`Add parameter`}>
<ActionIcon radius="sm" onClick={addParameter}>
<IconTextPlus color="green" />
</ActionIcon>
</Tooltip>
<AddItemButton tooltip="Add parameter" onClick={addParameter} />
);
return actions;

View File

@ -1,5 +1,5 @@
import { t } from '@lingui/macro';
import { Group, Stack, Text } from '@mantine/core';
import { Group, Text } from '@mantine/core';
import { ReactNode, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
@ -35,26 +35,23 @@ function partTableColumns(): TableColumn[] {
{
accessor: 'IPN',
title: t`IPN`,
sortable: true,
switchable: true
sortable: true
},
{
accessor: 'units',
sortable: true,
title: t`Units`,
switchable: true
title: t`Units`
},
{
accessor: 'description',
title: t`Description`,
sortable: true,
switchable: true
sortable: true
},
{
accessor: 'category',
title: t`Category`,
sortable: true,
switchable: true,
render: function (record: any) {
// TODO: Link to the category detail page
return shortenString({
@ -66,7 +63,7 @@ function partTableColumns(): TableColumn[] {
accessor: 'total_in_stock',
title: t`Stock`,
sortable: true,
switchable: true,
render: (record) => {
let extra: ReactNode[] = [];
@ -143,7 +140,7 @@ function partTableColumns(): TableColumn[] {
</Group>
}
title={t`Stock Information`}
extra={extra.length > 0 && <Stack spacing="xs">{extra}</Stack>}
extra={extra}
/>
);
}
@ -152,7 +149,7 @@ function partTableColumns(): TableColumn[] {
accessor: 'price_range',
title: t`Price Range`,
sortable: false,
switchable: true,
render: function (record: any) {
// TODO: Render price range
return '-- price --';
@ -160,8 +157,7 @@ function partTableColumns(): TableColumn[] {
},
{
accessor: 'link',
title: t`Link`,
switchable: true
title: t`Link`
}
];
}

View File

@ -68,7 +68,7 @@ export function PluginListTable({ props }: { props: InvenTreeTableProps }) {
accessor: 'meta.description',
title: t`Description`,
sortable: false,
switchable: true,
render: function (record: any) {
if (record.active) {
return record.meta.description;
@ -80,15 +80,14 @@ export function PluginListTable({ props }: { props: InvenTreeTableProps }) {
{
accessor: 'meta.version',
title: t`Version`,
sortable: false,
switchable: true
sortable: false
// TODO: Display date information if available
},
{
accessor: 'meta.author',
title: 'Author',
sortable: false,
switchable: true
sortable: false
}
],
[]

View File

@ -0,0 +1,266 @@
import { t } from '@lingui/macro';
import { Text } from '@mantine/core';
import { IconSquareArrowRight } from '@tabler/icons-react';
import { useCallback, useMemo } from 'react';
import { ProgressBar } from '../../../components/items/ProgressBar';
import { purchaseOrderLineItemFields } from '../../../forms/PurchaseOrderForms';
import { openCreateApiForm, openEditApiForm } from '../../../functions/forms';
import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState';
import { useUserState } from '../../../states/UserState';
import { ActionButton } from '../../buttons/ActionButton';
import { AddItemButton } from '../../buttons/AddItemButton';
import { Thumbnail } from '../../images/Thumbnail';
import { InvenTreeTable } from '../InvenTreeTable';
import {
RowDeleteAction,
RowDuplicateAction,
RowEditAction
} from '../RowActions';
import { TableHoverCard } from '../TableHoverCard';
/*
* Display a table of purchase order line items, for a specific order
*/
export function PurchaseOrderLineItemTable({
orderId,
params
}: {
orderId: number;
params?: any;
}) {
const { tableKey, refreshTable } = useTableRefresh(
'purchase-order-line-item'
);
const user = useUserState();
const rowActions = useCallback(
(record: any) => {
// TODO: Hide certain actions if user does not have required permissions
let received = (record?.received ?? 0) >= (record?.quantity ?? 0);
return [
{
hidden: received,
title: t`Receive`,
tooltip: t`Receive line item`,
color: 'green'
},
RowEditAction({
onClick: () => {
let supplier = record?.supplier_part_detail?.supplier;
if (!supplier) {
return;
}
let fields = purchaseOrderLineItemFields({
supplierId: supplier
});
openEditApiForm({
url: ApiPaths.purchase_order_line_list,
pk: record.pk,
title: t`Edit Line Item`,
fields: fields,
onFormSuccess: refreshTable,
successMessage: t`Line item updated`
});
}
}),
RowDuplicateAction({}),
RowDeleteAction({})
];
},
[orderId, user]
);
const tableColumns = useMemo(() => {
return [
{
accessor: 'part',
title: t`Part`,
sortable: true,
switchable: false,
render: (record: any) => {
return (
<Thumbnail
text={record?.part_detail?.name}
src={record?.part_detail?.thumbnail ?? record?.part_detail?.image}
/>
);
}
},
{
accessor: 'description',
title: t`Part Description`,
sortable: false,
render: (record: any) => record?.part_detail?.description
},
{
accessor: 'reference',
title: t`Reference`,
sortable: true
},
{
accessor: 'quantity',
title: t`Quantity`,
sortable: true,
switchable: false,
render: (record: any) => {
let part = record?.part_detail;
let supplier_part = record?.supplier_part_detail ?? {};
let extra = [];
if (supplier_part.pack_quantity_native != 1) {
let total = record.quantity * supplier_part.pack_quantity_native;
extra.push(
<Text key="pack-quantity">
{t`Pack Quantity`}: {supplier_part.pack_quantity}
</Text>
);
extra.push(
<Text key="total-quantity">
{t`Total Quantity`}: {total}
{part.units}
</Text>
);
}
return (
<TableHoverCard
value={record.quantity}
extra={extra}
title={t`Quantity`}
/>
);
}
},
{
accessor: 'received',
title: t`Received`,
sortable: false,
render: (record: any) => (
<ProgressBar
progressLabel={true}
value={record.received}
maximum={record.quantity}
/>
)
},
{
accessor: 'pack_quantity',
sortable: false,
title: t`Pack Quantity`,
render: (record: any) => record?.supplier_part_detail?.pack_quantity
},
{
accessor: 'SKU',
title: t`Supplier Code`,
switchable: false,
sortable: true
},
{
accessor: 'supplier_link',
title: t`Supplier Link`,
sortable: false,
render: (record: any) => record?.supplier_part_detail?.link
},
{
accessor: 'MPN',
title: t`Manufacturer Code`,
sortable: true,
render: (record: any) =>
record?.supplier_part_detail?.manufacturer_part_detail?.MPN
},
{
accessor: 'purchase_price',
title: t`Unit Price`,
sortable: true
// TODO: custom renderer
},
{
accessor: 'total_price',
title: t`Total Price`,
sortable: true
// TODO: custom renderer
},
{
accessor: 'target_date',
title: t`Target Date`,
sortable: true
},
{
accessor: 'destination',
title: t`Destination`,
sortable: false
// TODO: Custom renderer
},
{
accessor: 'notes',
title: t`Notes`
},
{
accessor: 'link',
title: t`Link`
// TODO: custom renderer
}
];
}, [orderId, user]);
const addLine = useCallback(() => {
openCreateApiForm({
url: ApiPaths.purchase_order_line_list,
title: t`Add Line Item`,
fields: purchaseOrderLineItemFields({}),
onFormSuccess: refreshTable,
successMessage: t`Line item added`
});
}, []);
// Custom table actions
const tableActions = useMemo(() => {
return [
<AddItemButton
tooltip={t`Add line item`}
onClick={addLine}
hidden={!user?.checkUserRole('purchaseorder', 'add')}
/>,
<ActionButton text={t`Receive items`} icon={<IconSquareArrowRight />} />
];
}, [orderId, user]);
return (
<InvenTreeTable
url={apiUrl(ApiPaths.purchase_order_line_list)}
tableKey={tableKey}
columns={tableColumns}
props={{
enableSelection: true,
enableDownload: true,
params: {
...params,
order: orderId,
part_detail: true
},
rowActions: rowActions,
customActionGroups: tableActions
}}
/>
);
}

View File

@ -33,8 +33,7 @@ export function PurchaseOrderTable({ params }: { params?: any }) {
},
{
accessor: 'description',
title: t`Description`,
switchable: true
title: t`Description`
},
{
accessor: 'supplier__name',
@ -54,20 +53,19 @@ export function PurchaseOrderTable({ params }: { params?: any }) {
},
{
accessor: 'supplier_reference',
title: t`Supplier Reference`,
switchable: true
title: t`Supplier Reference`
},
{
accessor: 'project_code',
title: t`Project Code`,
switchable: true
title: t`Project Code`
// TODO: Custom project code formatter
},
{
accessor: 'status',
title: t`Status`,
sortable: true,
switchable: true,
render: (record: any) =>
StatusRenderer({
status: record.status,
@ -76,34 +74,33 @@ export function PurchaseOrderTable({ params }: { params?: any }) {
},
{
accessor: 'creation_date',
title: t`Created`,
switchable: true
title: t`Created`
// TODO: Custom date formatter
},
{
accessor: 'target_date',
title: t`Target Date`,
switchable: true
title: t`Target Date`
// TODO: Custom date formatter
},
{
accessor: 'line_items',
title: t`Line Items`,
sortable: true,
switchable: true
sortable: true
},
{
accessor: 'total_price',
title: t`Total Price`,
sortable: true,
switchable: true
sortable: true
// TODO: Custom money formatter
},
{
accessor: 'responsible',
title: t`Responsible`,
sortable: true,
switchable: true
sortable: true
// TODO: custom 'owner' formatter
}
];

View File

@ -1,6 +1,5 @@
import { t } from '@lingui/macro';
import { ActionIcon, Stack, Text, Tooltip } from '@mantine/core';
import { IconCirclePlus } from '@tabler/icons-react';
import { Text } from '@mantine/core';
import { ReactNode, useCallback, useMemo } from 'react';
import { supplierPartFields } from '../../../forms/CompanyForms';
@ -12,6 +11,7 @@ import {
import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState';
import { useUserState } from '../../../states/UserState';
import { AddItemButton } from '../../buttons/AddItemButton';
import { Thumbnail } from '../../images/Thumbnail';
import { TableColumn } from '../Column';
import { InvenTreeTable } from '../InvenTreeTable';
@ -66,12 +66,11 @@ export function SupplierPartTable({ params }: { params: any }): ReactNode {
{
accessor: 'description',
title: t`Description`,
sortable: false,
switchable: true
sortable: false
},
{
accessor: 'manufacturer',
switchable: true,
sortable: true,
title: t`Manufacturer`,
render: (record: any) => {
@ -87,7 +86,7 @@ export function SupplierPartTable({ params }: { params: any }): ReactNode {
},
{
accessor: 'MPN',
switchable: true,
sortable: true,
title: t`MPN`,
render: (record: any) => record?.manufacturer_part_detail?.MPN
@ -95,20 +94,18 @@ export function SupplierPartTable({ params }: { params: any }): ReactNode {
{
accessor: 'in_stock',
title: t`In Stock`,
sortable: true,
switchable: true
sortable: true
},
{
accessor: 'packaging',
title: t`Packaging`,
sortable: true,
switchable: true
sortable: true
},
{
accessor: 'pack_quantity',
title: t`Pack Quantity`,
sortable: true,
switchable: true,
render: (record: any) => {
let part = record?.part_detail ?? {};
@ -125,7 +122,7 @@ export function SupplierPartTable({ params }: { params: any }): ReactNode {
return (
<TableHoverCard
value={record.pack_quantity}
extra={extra.length > 0 && <Stack spacing="xs">{extra}</Stack>}
extra={extra}
title={t`Pack Quantity`}
/>
);
@ -134,21 +131,20 @@ export function SupplierPartTable({ params }: { params: any }): ReactNode {
{
accessor: 'link',
title: t`Link`,
sortable: false,
switchable: true
sortable: false
// TODO: custom link renderer?
},
{
accessor: 'note',
title: t`Notes`,
sortable: false,
switchable: true
sortable: false
},
{
accessor: 'available',
title: t`Availability`,
sortable: true,
switchable: true,
render: (record: any) => {
let extra = [];
@ -160,12 +156,7 @@ export function SupplierPartTable({ params }: { params: any }): ReactNode {
);
}
return (
<TableHoverCard
value={record.available}
extra={extra.length > 0 && <Stack spacing="xs">{extra}</Stack>}
/>
);
return <TableHoverCard value={record.available} extra={extra} />;
}
}
];
@ -191,12 +182,7 @@ export function SupplierPartTable({ params }: { params: any }): ReactNode {
// TODO: Hide actions based on user permissions
return [
// TODO: Refactor this component out to something reusable
<Tooltip label={t`Add supplier part`}>
<ActionIcon radius="sm" onClick={addSupplierPart}>
<IconCirclePlus color="green" />
</ActionIcon>
</Tooltip>
<AddItemButton tooltip={t`Add supplier part`} onClick={addSupplierPart} />
];
}, [user]);

View File

@ -29,8 +29,7 @@ export function ReturnOrderTable({ params }: { params?: any }) {
},
{
accessor: 'description',
title: t`Description`,
switchable: true
title: t`Description`
},
{
accessor: 'customer__name',
@ -50,20 +49,19 @@ export function ReturnOrderTable({ params }: { params?: any }) {
},
{
accessor: 'customer_reference',
title: t`Customer Reference`,
switchable: true
title: t`Customer Reference`
},
{
accessor: 'project_code',
title: t`Project Code`,
switchable: true
title: t`Project Code`
// TODO: Custom formatter
},
{
accessor: 'status',
title: t`Status`,
sortable: true,
switchable: true,
render: TableStatusRenderer(ModelType.returnorder)
}
// TODO: Creation date

View File

@ -30,8 +30,7 @@ export function SalesOrderTable({ params }: { params?: any }) {
},
{
accessor: 'description',
title: t`Description`,
switchable: true
title: t`Description`
},
{
accessor: 'customer__name',
@ -51,20 +50,19 @@ export function SalesOrderTable({ params }: { params?: any }) {
},
{
accessor: 'customer_reference',
title: t`Customer Reference`,
switchable: true
title: t`Customer Reference`
},
{
accessor: 'project_code',
title: t`Project Code`,
switchable: true
title: t`Project Code`
// TODO: Custom formatter
},
{
accessor: 'status',
title: t`Status`,
sortable: true,
switchable: true,
render: TableStatusRenderer(ModelType.salesorder)
}

View File

@ -1,6 +1,5 @@
import { t } from '@lingui/macro';
import { ActionIcon, Text, Tooltip } from '@mantine/core';
import { IconCirclePlus } from '@tabler/icons-react';
import { Text } from '@mantine/core';
import { useCallback, useMemo } from 'react';
import {
@ -10,6 +9,7 @@ import {
} from '../../../functions/forms';
import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState';
import { AddItemButton } from '../../buttons/AddItemButton';
import { TableColumn } from '../Column';
import { InvenTreeTable } from '../InvenTreeTable';
import { RowAction, RowDeleteAction, RowEditAction } from '../RowActions';
@ -96,11 +96,8 @@ export function CustomUnitsTable() {
let actions = [];
actions.push(
<Tooltip label={t`Add custom unit`}>
<ActionIcon radius="sm" onClick={addCustomUnit}>
<IconCirclePlus color="green" />
</ActionIcon>
</Tooltip>
// TODO: Adjust actions based on user permissions
<AddItemButton tooltip={t`Add custom unit`} onClick={addCustomUnit} />
);
return actions;

View File

@ -1,6 +1,5 @@
import { t } from '@lingui/macro';
import { ActionIcon, Text, Tooltip } from '@mantine/core';
import { IconCirclePlus } from '@tabler/icons-react';
import { Text } from '@mantine/core';
import { useCallback, useMemo } from 'react';
import {
@ -10,6 +9,7 @@ import {
} from '../../../functions/forms';
import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState';
import { AddItemButton } from '../../buttons/AddItemButton';
import { TableColumn } from '../Column';
import { InvenTreeTable } from '../InvenTreeTable';
import { RowAction, RowDeleteAction, RowEditAction } from '../RowActions';
@ -86,11 +86,7 @@ export function ProjectCodeTable() {
let actions = [];
actions.push(
<Tooltip label={t`Add project code`}>
<ActionIcon radius="sm" onClick={addProjectCode}>
<IconCirclePlus color="green" />
</ActionIcon>
</Tooltip>
<AddItemButton onClick={addProjectCode} tooltip={t`Add project code`} />
);
return actions;

View File

@ -1,5 +1,5 @@
import { t } from '@lingui/macro';
import { Group, Stack, Text } from '@mantine/core';
import { Group, Text } from '@mantine/core';
import { ReactNode, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
@ -40,7 +40,7 @@ function stockItemTableColumns(): TableColumn[] {
{
accessor: 'part_detail.description',
sortable: false,
switchable: true,
title: t`Description`
},
{
@ -145,7 +145,7 @@ function stockItemTableColumns(): TableColumn[] {
</Group>
}
title={t`Stock Information`}
extra={extra.length > 0 && <Stack spacing="xs">{extra}</Stack>}
extra={extra}
/>
);
}
@ -153,7 +153,7 @@ function stockItemTableColumns(): TableColumn[] {
{
accessor: 'status',
sortable: true,
switchable: true,
filter: true,
title: t`Status`,
render: TableStatusRenderer(ModelType.stockitem)
@ -161,13 +161,13 @@ function stockItemTableColumns(): TableColumn[] {
{
accessor: 'batch',
sortable: true,
switchable: true,
title: t`Batch`
},
{
accessor: 'location',
sortable: true,
switchable: true,
title: t`Location`,
render: function (record: any) {
// TODO: Custom renderer for location

View File

@ -25,39 +25,37 @@ export function StockLocationTable({ params = {} }: { params?: any }) {
},
{
accessor: 'description',
title: t`Description`,
switchable: true
title: t`Description`
},
{
accessor: 'pathstring',
title: t`Path`,
sortable: true,
switchable: true
sortable: true
},
{
accessor: 'items',
title: t`Stock Items`,
switchable: true,
sortable: true
},
{
accessor: 'structural',
title: t`Structural`,
switchable: true,
sortable: true,
render: (record: any) => <YesNoButton value={record.structural} />
},
{
accessor: 'external',
title: t`External`,
switchable: true,
sortable: true,
render: (record: any) => <YesNoButton value={record.external} />
},
{
accessor: 'location_type',
title: t`Location Type`,
switchable: true,
sortable: false,
render: (record: any) => record.location_type_detail?.name
}

View File

@ -0,0 +1,65 @@
import {
IconCalendar,
IconCoins,
IconCurrencyDollar,
IconLink,
IconNotes,
IconSitemap
} from '@tabler/icons-react';
import {
ApiFormData,
ApiFormFieldSet
} from '../components/forms/fields/ApiFormField';
/*
* Construct a set of fields for creating / editing a PurchaseOrderLineItem instance
*/
export function purchaseOrderLineItemFields({
supplierId
}: {
supplierId?: number;
}) {
let fields: ApiFormFieldSet = {
order: {
filters: {
supplier_detail: true
}
},
part: {
filters: {
part_detail: true,
supplier_detail: true,
supplier: supplierId
},
adjustFilters: (filters: any, _form: ApiFormData) => {
// TODO: Filter by the supplier associated with the order
return filters;
}
// TODO: Custom onEdit callback (see purchase_order.js)
// TODO: secondary modal (see purchase_order.js)
},
quantity: {},
reference: {},
purchase_price: {
icon: <IconCurrencyDollar />
},
purchase_price_currency: {
icon: <IconCoins />
},
target_date: {
icon: <IconCalendar />
},
destination: {
icon: <IconSitemap />
},
notes: {
icon: <IconNotes />
},
link: {
icon: <IconLink />
}
};
return fields;
}

View File

@ -8,7 +8,6 @@ import {
IconEdit,
IconFileTypePdf,
IconInfoCircle,
IconLink,
IconList,
IconListCheck,
IconNotes,
@ -16,13 +15,20 @@ import {
IconPrinter,
IconQrcode,
IconSitemap,
IconTrash,
IconUnlink
IconTrash
} from '@tabler/icons-react';
import { useMemo } from 'react';
import { useParams } from 'react-router-dom';
import { ActionDropdown } from '../../components/items/ActionDropdown';
import {
ActionDropdown,
DeleteItemAction,
DuplicateItemAction,
EditItemAction,
LinkBarcodeAction,
UnlinkBarcodeAction,
ViewBarcodeAction
} from '../../components/items/ActionDropdown';
import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
import { ModelType } from '../../components/render/ModelType';
@ -179,23 +185,13 @@ export default function BuildDetail() {
tooltip={t`Barcode Actions`}
icon={<IconQrcode />}
actions={[
{
icon: <IconQrcode />,
name: t`View`,
tooltip: t`View part barcode`
},
{
icon: <IconLink />,
name: t`Link Barcode`,
tooltip: t`Link custom barcode to part`,
ViewBarcodeAction({}),
LinkBarcodeAction({
disabled: build?.barcode_hash
},
{
icon: <IconUnlink />,
name: t`Unlink Barcode`,
tooltip: t`Unlink custom barcode from part`,
}),
UnlinkBarcodeAction({
disabled: !build?.barcode_hash
}
})
]}
/>,
<ActionDropdown
@ -215,21 +211,9 @@ export default function BuildDetail() {
tooltip={t`Build Order Actions`}
icon={<IconDots />}
actions={[
{
icon: <IconEdit color="blue" />,
name: t`Edit`,
tooltip: t`Edit build order`
},
{
icon: <IconCopy color="green" />,
name: t`Duplicate`,
tooltip: t`Duplicate build order`
},
{
icon: <IconTrash color="red" />,
name: t`Delete`,
tooltip: t`Delete build order`
}
EditItemAction({}),
DuplicateItemAction({}),
DeleteItemAction({})
]}
/>
];

View File

@ -4,7 +4,6 @@ import {
IconBuildingFactory2,
IconBuildingWarehouse,
IconDots,
IconEdit,
IconInfoCircle,
IconMap2,
IconNotes,
@ -12,7 +11,6 @@ import {
IconPackages,
IconPaperclip,
IconShoppingCart,
IconTrash,
IconTruckDelivery,
IconTruckReturn,
IconUsersGroup
@ -20,7 +18,11 @@ import {
import { useMemo } from 'react';
import { useParams } from 'react-router-dom';
import { ActionDropdown } from '../../components/items/ActionDropdown';
import {
ActionDropdown,
DeleteItemAction,
EditItemAction
} from '../../components/items/ActionDropdown';
import { Breadcrumb } from '../../components/nav/BreadcrumbList';
import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup } from '../../components/nav/PanelGroup';
@ -169,12 +171,9 @@ export default function CompanyDetail(props: CompanyDetailProps) {
tooltip={t`Company Actions`}
icon={<IconDots />}
actions={[
{
icon: <IconEdit color="blue" />,
name: t`Edit`,
tooltip: t`Edit company`,
EditItemAction({
disabled: !canEdit,
onClick: () => {
callback: () => {
if (company?.pk) {
editCompany({
pk: company?.pk,
@ -182,13 +181,10 @@ export default function CompanyDetail(props: CompanyDetailProps) {
});
}
}
},
{
icon: <IconTrash color="red" />,
name: t`Delete`,
tooltip: t`Delete company`,
}),
DeleteItemAction({
disabled: !canDelete
}
})
]}
/>
];

View File

@ -6,27 +6,21 @@ import {
IconBuildingFactory2,
IconCalendarStats,
IconClipboardList,
IconCopy,
IconCurrencyDollar,
IconDots,
IconEdit,
IconInfoCircle,
IconLayersLinked,
IconLink,
IconList,
IconListTree,
IconNotes,
IconPackages,
IconPaperclip,
IconQrcode,
IconShoppingCart,
IconStack2,
IconTestPipe,
IconTools,
IconTransfer,
IconTrash,
IconTruckDelivery,
IconUnlink,
IconVersions
} from '@tabler/icons-react';
import { useMemo, useState } from 'react';
@ -34,7 +28,13 @@ import { useParams } from 'react-router-dom';
import {
ActionDropdown,
BarcodeActionDropdown
BarcodeActionDropdown,
DeleteItemAction,
DuplicateItemAction,
EditItemAction,
LinkBarcodeAction,
UnlinkBarcodeAction,
ViewBarcodeAction
} from '../../components/items/ActionDropdown';
import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
@ -263,23 +263,13 @@ export default function PartDetail() {
return [
<BarcodeActionDropdown
actions={[
{
icon: <IconQrcode />,
name: t`View`,
tooltip: t`View part barcode`
},
{
icon: <IconLink />,
name: t`Link Barcode`,
tooltip: t`Link custom barcode to part`,
ViewBarcodeAction({}),
LinkBarcodeAction({
disabled: part?.barcode_hash
},
{
icon: <IconUnlink />,
name: t`Unlink Barcode`,
tooltip: t`Unlink custom barcode from part`,
}),
UnlinkBarcodeAction({
disabled: !part?.barcode_hash
}
})
]}
/>,
<ActionDropdown
@ -304,28 +294,19 @@ export default function PartDetail() {
tooltip={t`Part Actions`}
icon={<IconDots />}
actions={[
{
icon: <IconEdit color="blue" />,
name: t`Edit`,
tooltip: t`Edit part`,
onClick: () => {
DuplicateItemAction({}),
EditItemAction({
callback: () => {
part.pk &&
editPart({
part_id: part.pk,
callback: refreshInstance
});
}
},
{
icon: <IconCopy color="green" />,
name: t`Duplicate`,
tooltip: t`Duplicate part`
},
{
icon: <IconTrash color="red" />,
name: t`Delete`,
tooltip: t`Delete part`
}
}),
DeleteItemAction({
disabled: part?.active
})
]}
/>
];

View File

@ -1,6 +1,7 @@
import { t } from '@lingui/macro';
import { LoadingOverlay, Stack } from '@mantine/core';
import {
IconDots,
IconInfoCircle,
IconList,
IconNotes,
@ -10,13 +11,24 @@ import {
import { useMemo } from 'react';
import { useParams } from 'react-router-dom';
import {
ActionDropdown,
BarcodeActionDropdown,
DeleteItemAction,
EditItemAction,
LinkBarcodeAction,
UnlinkBarcodeAction,
ViewBarcodeAction
} from '../../components/items/ActionDropdown';
import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
import { AttachmentTable } from '../../components/tables/general/AttachmentTable';
import { PurchaseOrderLineItemTable } from '../../components/tables/purchasing/PurchaseOrderLineItemTable';
import { StockItemTable } from '../../components/tables/stock/StockItemTable';
import { NotesEditor } from '../../components/widgets/MarkdownEditor';
import { useInstance } from '../../hooks/UseInstance';
import { ApiPaths, apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';
/**
* Detail page for a single PurchaseOrder
@ -24,6 +36,8 @@ import { ApiPaths, apiUrl } from '../../states/ApiState';
export default function PurchaseOrderDetail() {
const { id } = useParams();
const user = useUserState();
const { instance: order, instanceQuery } = useInstance({
endpoint: ApiPaths.purchase_order_list,
pk: id,
@ -43,7 +57,8 @@ export default function PurchaseOrderDetail() {
{
name: 'line-items',
label: t`Line Items`,
icon: <IconList />
icon: <IconList />,
content: order?.pk && <PurchaseOrderLineItemTable orderId={order.pk} />
},
{
name: 'received-stock',
@ -84,6 +99,29 @@ export default function PurchaseOrderDetail() {
];
}, [order, id]);
const poActions = useMemo(() => {
// TODO: Disable certain actions based on user permissions
return [
<BarcodeActionDropdown
actions={[
ViewBarcodeAction({}),
LinkBarcodeAction({
disabled: order?.barcode_hash
}),
UnlinkBarcodeAction({
disabled: !order?.barcode_hash
})
]}
/>,
<ActionDropdown
key="order"
tooltip={t`Order Actions`}
icon={<IconDots />}
actions={[EditItemAction({}), DeleteItemAction({})]}
/>
];
}, [id, order, user]);
return (
<>
<Stack spacing="xs">
@ -93,6 +131,7 @@ export default function PurchaseOrderDetail() {
subtitle={order.description}
imageUrl={order.supplier_detail?.image}
breadcrumbs={[{ name: t`Purchasing`, url: '/purchasing/' }]}
actions={poActions}
/>
<PanelGroup pageKey="purchaseorder" panels={orderPanels} />
</Stack>

View File

@ -9,25 +9,25 @@ import {
IconCirclePlus,
IconCopy,
IconDots,
IconEdit,
IconHistory,
IconInfoCircle,
IconLink,
IconNotes,
IconPackages,
IconPaperclip,
IconQrcode,
IconSitemap,
IconTransfer,
IconTrash,
IconUnlink
IconTransfer
} from '@tabler/icons-react';
import { useMemo, useState } from 'react';
import { useParams } from 'react-router-dom';
import {
ActionDropdown,
BarcodeActionDropdown
BarcodeActionDropdown,
DeleteItemAction,
EditItemAction,
LinkBarcodeAction,
UnlinkBarcodeAction,
ViewBarcodeAction
} from '../../components/items/ActionDropdown';
import { PlaceholderPanel } from '../../components/items/Placeholder';
import { PageDetail } from '../../components/nav/PageDetail';
@ -144,23 +144,13 @@ export default function StockDetail() {
() => /* TODO: Disable actions based on user permissions*/ [
<BarcodeActionDropdown
actions={[
{
icon: <IconQrcode />,
name: t`View`,
tooltip: t`View part barcode`
},
{
icon: <IconLink />,
name: t`Link Barcode`,
tooltip: t`Link custom barcode to stock item`,
ViewBarcodeAction({}),
LinkBarcodeAction({
disabled: stockitem?.barcode_hash
},
{
icon: <IconUnlink />,
name: t`Unlink Barcode`,
tooltip: t`Unlink custom barcode from stock item`,
}),
UnlinkBarcodeAction({
disabled: !stockitem?.barcode_hash
}
})
]}
/>,
<ActionDropdown
@ -200,23 +190,16 @@ export default function StockDetail() {
tooltip: t`Duplicate stock item`,
icon: <IconCopy />
},
{
name: t`Edit`,
tooltip: t`Edit stock item`,
icon: <IconEdit color="blue" />,
onClick: () => {
EditItemAction({
callback: () => {
stockitem.pk &&
editStockItem({
item_id: stockitem.pk,
callback: () => refreshInstance
});
}
},
{
name: t`Delete`,
tooltip: t`Delete stock item`,
icon: <IconTrash color="red" />
}
}),
DeleteItemAction({})
]}
/>
],

View File

@ -106,6 +106,7 @@ export enum ApiPaths {
// Purchase Order URLs
purchase_order_list = 'api-purchase-order-list',
purchase_order_line_list = 'api-purchase-order-line-list',
purchase_order_attachment_list = 'api-purchase-order-attachment-list',
// Sales Order URLs
@ -218,6 +219,8 @@ export function apiEndpoint(path: ApiPaths): string {
return 'stock/attachment/';
case ApiPaths.purchase_order_list:
return 'order/po/';
case ApiPaths.purchase_order_line_list:
return 'order/po-line/';
case ApiPaths.purchase_order_attachment_list:
return 'order/po/attachment/';
case ApiPaths.sales_order_list: