[PUI] Implement "build outputs" table (#7115)

* Update build line allocation table

- Allow display of "tracked" items in main allocation table

* Add resolveItem function for finding nested items

* Update BuildLineTable

* Allow BuildLineList to be ordered by 'trackable' field

* Bump API version

* Building out columns

* Table tweaks

* Fetch list of required test templates

* Tweaks

* Add placeholders for table actions

* Add typing

* Add placeholder buttons

* Update columns

* Add button to duplicate build order

* Add placeholder cancel action

* Refactoring

* Edit company from table

* Change "create" to "add"

* Change more from Create to Add
This commit is contained in:
Oliver 2024-04-25 10:11:44 +10:00 committed by GitHub
parent 5f54aef79a
commit d30ab932ca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 581 additions and 147 deletions

View File

@ -19,9 +19,11 @@ import {
SubmitHandler,
useForm
} from 'react-hook-form';
import { useNavigate } from 'react-router-dom';
import { api, queryClient } from '../../App';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import {
NestedDict,
constructField,
@ -30,6 +32,7 @@ import {
mapFields
} from '../../functions/forms';
import { invalidResponse } from '../../functions/notifications';
import { getDetailUrl } from '../../functions/urls';
import { PathParams } from '../../states/ApiState';
import {
ApiFormField,
@ -59,6 +62,8 @@ export interface ApiFormAction {
* @param successMessage : Optional message to display on successful form submission
* @param onFormSuccess : A callback function to call when the form is submitted successfully.
* @param onFormError : A callback function to call when the form is submitted with errors.
* @param modelType : Define a model type for this form
* @param follow : Boolean, follow the result of the form (if possible)
*/
export interface ApiFormProps {
url: ApiEndpoints | string;
@ -79,6 +84,8 @@ export interface ApiFormProps {
successMessage?: string;
onFormSuccess?: (data: any) => void;
onFormError?: () => void;
modelType?: ModelType;
follow?: boolean;
actions?: ApiFormAction[];
timeout?: number;
}
@ -183,6 +190,8 @@ export function ApiForm({
props: ApiFormProps;
optionsLoading: boolean;
}) {
const navigate = useNavigate();
const fields: ApiFormFieldSet = useMemo(() => {
return props.fields ?? {};
}, [props.fields]);
@ -384,6 +393,12 @@ export function ApiForm({
props.onFormSuccess(response.data);
}
if (props.follow) {
if (props.modelType && response.data?.pk) {
navigate(getDetailUrl(props.modelType, response.data?.pk));
}
}
// Optionally show a success message
if (props.successMessage) {
notifications.hide('form-success');

View File

@ -14,8 +14,10 @@ import {
IconTrash,
IconUnlink
} from '@tabler/icons-react';
import { color } from '@uiw/react-codemirror';
import { ReactNode, useMemo } from 'react';
import { InvenTreeIcon } from '../../functions/icons';
import { notYetImplemented } from '../../functions/notifications';
export type ActionDropdownItem = {
@ -203,6 +205,24 @@ export function DeleteItemAction({
};
}
export function CancelItemAction({
hidden = false,
tooltip,
onClick
}: {
hidden?: boolean;
tooltip?: string;
onClick?: () => void;
}): ActionDropdownItem {
return {
icon: <InvenTreeIcon icon="cancel" iconProps={{ color: 'red' }} />,
name: t`Cancel`,
tooltip: tooltip ?? t`Cancel`,
onClick: onClick,
hidden: hidden
};
}
// Common action button for duplicating an item
export function DuplicateItemAction({
hidden = false,

View File

@ -268,8 +268,6 @@ function StockOperationsRow({
}) {
const item = input.item;
console.log('rec', record);
const [value, setValue] = useState<StockItemQuantity>(
add ? 0 : item.quantity ?? 0
);

View File

@ -12,8 +12,10 @@ import {
IconCalendarStats,
IconCategory,
IconCheck,
IconCircleCheck,
IconCircleMinus,
IconCirclePlus,
IconCircleX,
IconClipboardList,
IconClipboardText,
IconCopy,
@ -59,6 +61,7 @@ import {
IconTool,
IconTools,
IconTransfer,
IconTransitionRight,
IconTrash,
IconTruck,
IconTruckDelivery,
@ -130,6 +133,10 @@ const icons = {
delete: IconTrash,
packaging: IconPackage,
packages: IconPackages,
install: IconTransitionRight,
plus: IconCirclePlus,
minus: IconCircleMinus,
cancel: IconCircleX,
// Part Icons
active: IconCheck,
@ -186,7 +193,8 @@ const icons = {
batch_code: IconClipboardText,
destination: IconFlag,
repeat_destination: IconFlagShare,
unlink: IconUnlink
unlink: IconUnlink,
success: IconCircleCheck
};
export type InvenTreeIconType = keyof typeof icons;

View File

@ -1,5 +1,5 @@
import { randomId, useLocalStorage } from '@mantine/hooks';
import { useCallback, useState } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { TableFilter } from '../tables/Filter';
@ -22,6 +22,7 @@ export type TableState = {
expandedRecords: any[];
setExpandedRecords: (records: any[]) => void;
selectedRecords: any[];
hasSelectedRecords: boolean;
setSelectedRecords: (records: any[]) => void;
clearSelectedRecords: () => void;
hiddenColumns: string[];
@ -78,6 +79,11 @@ export function useTable(tableName: string): TableState {
setSelectedRecords([]);
}, []);
const hasSelectedRecords = useMemo(
() => selectedRecords.length > 0,
[selectedRecords]
);
// Total record count
const [recordCount, setRecordCount] = useState<number>(0);
@ -126,6 +132,7 @@ export function useTable(tableName: string): TableState {
selectedRecords,
setSelectedRecords,
clearSelectedRecords,
hasSelectedRecords,
hiddenColumns,
setHiddenColumns,
searchTerm,

View File

@ -22,6 +22,7 @@ import { DetailsImage } from '../../components/details/DetailsImage';
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
import {
ActionDropdown,
CancelItemAction,
DuplicateItemAction,
EditItemAction,
LinkBarcodeAction,
@ -36,12 +37,16 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles';
import { useBuildOrderFields } from '../../forms/BuildForms';
import { useEditApiFormModal } from '../../hooks/UseForm';
import {
useCreateApiFormModal,
useEditApiFormModal
} from '../../hooks/UseForm';
import { useInstance } from '../../hooks/UseInstance';
import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';
import BuildLineTable from '../../tables/build/BuildLineTable';
import { BuildOrderTable } from '../../tables/build/BuildOrderTable';
import BuildOutputTable from '../../tables/build/BuildOutputTable';
import { AttachmentTable } from '../../tables/general/AttachmentTable';
import { StockItemTable } from '../../tables/stock/StockItemTable';
@ -213,7 +218,12 @@ export default function BuildDetail() {
{
name: 'incomplete-outputs',
label: t`Incomplete Outputs`,
icon: <IconClipboardList />
icon: <IconClipboardList />,
content: build.pk ? (
<BuildOutputTable buildId={build.pk} partId={build.part} />
) : (
<Skeleton />
)
// TODO: Hide if build is complete
},
{
@ -290,6 +300,18 @@ export default function BuildDetail() {
}
});
const duplicateBuild = useCreateApiFormModal({
url: ApiEndpoints.build_order_list,
title: t`Add Build Order`,
fields: buildOrderFields,
initialData: {
...build,
reference: undefined
},
follow: true,
modelType: ModelType.build
});
const buildActions = useMemo(() => {
// TODO: Disable certain actions based on user permissions
return [
@ -328,7 +350,13 @@ export default function BuildDetail() {
onClick: () => editBuild.open(),
hidden: !user.hasChangeRole(UserRoles.build)
}),
DuplicateItemAction({})
CancelItemAction({
tooltip: t`Cancel order`
}),
DuplicateItemAction({
onClick: () => duplicateBuild.open(),
hidden: !user.hasAddRole(UserRoles.build)
})
]}
/>
];
@ -349,6 +377,7 @@ export default function BuildDetail() {
return (
<>
{editBuild.modal}
{duplicateBuild.modal}
<Stack spacing="xs">
<LoadingOverlay visible={instanceQuery.isFetching} />
<PageDetail

View File

@ -26,7 +26,10 @@ import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles';
import { useManufacturerPartFields } from '../../forms/CompanyForms';
import { getDetailUrl } from '../../functions/urls';
import { useEditApiFormModal } from '../../hooks/UseForm';
import {
useCreateApiFormModal,
useEditApiFormModal
} from '../../hooks/UseForm';
import { useInstance } from '../../hooks/UseInstance';
import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';
@ -189,6 +192,17 @@ export default function ManufacturerPartDetail() {
onFormSuccess: refreshInstance
});
const duplicateManufacturerPart = useCreateApiFormModal({
url: ApiEndpoints.manufacturer_part_list,
title: t`Add Manufacturer Part`,
fields: editManufacturerPartFields,
initialData: {
...manufacturerPart
},
follow: true,
modelType: ModelType.manufacturerpart
});
const manufacturerPartActions = useMemo(() => {
return [
<ActionDropdown
@ -197,7 +211,8 @@ export default function ManufacturerPartDetail() {
icon={<IconDots />}
actions={[
DuplicateItemAction({
hidden: !user.hasAddRole(UserRoles.purchase_order)
hidden: !user.hasAddRole(UserRoles.purchase_order),
onClick: () => duplicateManufacturerPart.open()
}),
EditItemAction({
hidden: !user.hasChangeRole(UserRoles.purchase_order),
@ -227,6 +242,7 @@ export default function ManufacturerPartDetail() {
return (
<>
{editManufacturerPart.modal}
{duplicateManufacturerPart.modal}
<Stack spacing="xs">
<LoadingOverlay visible={instanceQuery.isFetching} />
<PageDetail

View File

@ -8,7 +8,7 @@ import {
IconShoppingCart
} from '@tabler/icons-react';
import { ReactNode, useMemo } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useParams } from 'react-router-dom';
import { DetailsField, DetailsTable } from '../../components/details/Details';
import DetailsBadge from '../../components/details/DetailsBadge';
@ -26,7 +26,6 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles';
import { useSupplierPartFields } from '../../forms/CompanyForms';
import { getDetailUrl } from '../../functions/urls';
import {
useCreateApiFormModal,
useEditApiFormModal
@ -43,8 +42,6 @@ export default function SupplierPartDetail() {
const user = useUserState();
const navigate = useNavigate();
const {
instance: supplierPart,
instanceQuery,
@ -284,11 +281,8 @@ export default function SupplierPartDetail() {
initialData: {
...supplierPart
},
onFormSuccess: (response: any) => {
if (response.pk) {
navigate(getDetailUrl(ModelType.supplierpart, response.pk));
}
}
follow: true,
modelType: ModelType.supplierpart
});
const breadcrumbs = useMemo(() => {

View File

@ -24,7 +24,7 @@ import {
} from '@tabler/icons-react';
import { useSuspenseQuery } from '@tanstack/react-query';
import { ReactNode, useMemo, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useParams } from 'react-router-dom';
import { api } from '../../App';
import { DetailsField, DetailsTable } from '../../components/details/Details';
@ -86,7 +86,6 @@ export default function PartDetail() {
const { id } = useParams();
const user = useUserState();
const navigate = useNavigate();
const [treeOpen, setTreeOpen] = useState(false);
@ -680,11 +679,8 @@ export default function PartDetail() {
initialData: {
...part
},
onFormSuccess: (response: any) => {
if (response.pk) {
navigate(getDetailUrl(ModelType.part, response.pk));
}
}
follow: true,
modelType: ModelType.part
});
const stockActionProps: StockOperationProps = useMemo(() => {

View File

@ -9,7 +9,7 @@ import {
IconPaperclip
} from '@tabler/icons-react';
import { ReactNode, useMemo } from 'react';
import { useParams } from 'react-router-dom';
import { useNavigate, useParams } from 'react-router-dom';
import { DetailsField, DetailsTable } from '../../components/details/Details';
import { DetailsImage } from '../../components/details/DetailsImage';
@ -17,7 +17,8 @@ import { ItemDetailsGrid } from '../../components/details/ItemDetails';
import {
ActionDropdown,
BarcodeActionDropdown,
DeleteItemAction,
CancelItemAction,
DuplicateItemAction,
EditItemAction,
LinkBarcodeAction,
UnlinkBarcodeAction,
@ -31,7 +32,11 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles';
import { usePurchaseOrderFields } from '../../forms/PurchaseOrderForms';
import { useEditApiFormModal } from '../../hooks/UseForm';
import { getDetailUrl } from '../../functions/urls';
import {
useCreateApiFormModal,
useEditApiFormModal
} from '../../hooks/UseForm';
import { useInstance } from '../../hooks/UseInstance';
import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';
@ -46,6 +51,7 @@ export default function PurchaseOrderDetail() {
const { id } = useParams();
const user = useUserState();
const navigate = useNavigate();
const {
instance: order,
@ -72,6 +78,18 @@ export default function PurchaseOrderDetail() {
}
});
const duplicatePurchaseOrder = useCreateApiFormModal({
url: ApiEndpoints.purchase_order_list,
title: t`Add Purchase Order`,
fields: purchaseOrderFields,
initialData: {
...order,
reference: undefined
},
follow: true,
modelType: ModelType.purchaseorder
});
const detailsPanel = useMemo(() => {
if (instanceQuery.isFetching) {
return <Skeleton />;
@ -299,8 +317,12 @@ export default function PurchaseOrderDetail() {
editPurchaseOrder.open();
}
}),
DeleteItemAction({
hidden: !user.hasDeleteRole(UserRoles.purchase_order)
CancelItemAction({
tooltip: t`Cancel order`
}),
DuplicateItemAction({
hidden: !user.hasAddRole(UserRoles.purchase_order),
onClick: () => duplicatePurchaseOrder.open()
})
]}
/>
@ -322,6 +344,7 @@ export default function PurchaseOrderDetail() {
return (
<>
{editPurchaseOrder.modal}
{duplicatePurchaseOrder.modal}
<Stack spacing="xs">
<LoadingOverlay visible={instanceQuery.isFetching} />
<PageDetail

View File

@ -8,14 +8,16 @@ import {
IconPaperclip
} from '@tabler/icons-react';
import { ReactNode, useMemo } from 'react';
import { useParams } from 'react-router-dom';
import { useNavigate, useParams } from 'react-router-dom';
import { DetailsField, DetailsTable } from '../../components/details/Details';
import { DetailsImage } from '../../components/details/DetailsImage';
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
import {
ActionDropdown,
CancelItemAction,
DeleteItemAction,
DuplicateItemAction,
EditItemAction
} from '../../components/items/ActionDropdown';
import { PageDetail } from '../../components/nav/PageDetail';
@ -26,7 +28,10 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles';
import { useReturnOrderFields } from '../../forms/SalesOrderForms';
import { useEditApiFormModal } from '../../hooks/UseForm';
import {
useCreateApiFormModal,
useEditApiFormModal
} from '../../hooks/UseForm';
import { useInstance } from '../../hooks/UseInstance';
import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';
@ -39,6 +44,7 @@ export default function ReturnOrderDetail() {
const { id } = useParams();
const user = useUserState();
const navigate = useNavigate();
const {
instance: order,
@ -260,6 +266,18 @@ export default function ReturnOrderDetail() {
}
});
const duplicateReturnOrder = useCreateApiFormModal({
url: ApiEndpoints.return_order_list,
title: t`Add Return Order`,
fields: returnOrderFields,
initialData: {
...order,
reference: undefined
},
modelType: ModelType.returnorder,
follow: true
});
const orderActions = useMemo(() => {
return [
<ActionDropdown
@ -273,9 +291,12 @@ export default function ReturnOrderDetail() {
editReturnOrder.open();
}
}),
DeleteItemAction({
hidden: !user.hasDeleteRole(UserRoles.return_order)
// TODO: Delete?
CancelItemAction({
tooltip: t`Cancel order`
}),
DuplicateItemAction({
hidden: !user.hasChangeRole(UserRoles.return_order),
onClick: () => duplicateReturnOrder.open()
})
]}
/>
@ -285,6 +306,7 @@ export default function ReturnOrderDetail() {
return (
<>
{editReturnOrder.modal}
{duplicateReturnOrder.modal}
<Stack spacing="xs">
<LoadingOverlay visible={instanceQuery.isFetching} />
<PageDetail

View File

@ -11,14 +11,16 @@ import {
IconTruckLoading
} from '@tabler/icons-react';
import { ReactNode, useMemo } from 'react';
import { useParams } from 'react-router-dom';
import { useNavigate, useParams } from 'react-router-dom';
import { DetailsField, DetailsTable } from '../../components/details/Details';
import { DetailsImage } from '../../components/details/DetailsImage';
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
import {
ActionDropdown,
CancelItemAction,
DeleteItemAction,
DuplicateItemAction,
EditItemAction
} from '../../components/items/ActionDropdown';
import { PageDetail } from '../../components/nav/PageDetail';
@ -29,7 +31,11 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles';
import { useSalesOrderFields } from '../../forms/SalesOrderForms';
import { useEditApiFormModal } from '../../hooks/UseForm';
import { getDetailUrl } from '../../functions/urls';
import {
useCreateApiFormModal,
useEditApiFormModal
} from '../../hooks/UseForm';
import { useInstance } from '../../hooks/UseInstance';
import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';
@ -43,6 +49,7 @@ export default function SalesOrderDetail() {
const { id } = useParams();
const user = useUserState();
const navigate = useNavigate();
const {
instance: order,
@ -212,6 +219,18 @@ export default function SalesOrderDetail() {
}
});
const duplicateSalesOrder = useCreateApiFormModal({
url: ApiEndpoints.sales_order_list,
title: t`Add Sales Order`,
fields: salesOrderFields,
initialData: {
...order,
reference: undefined
},
follow: true,
modelType: ModelType.salesorder
});
const orderPanels: PanelType[] = useMemo(() => {
return [
{
@ -281,13 +300,14 @@ export default function SalesOrderDetail() {
actions={[
EditItemAction({
hidden: !user.hasChangeRole(UserRoles.sales_order),
onClick: () => {
editSalesOrder.open();
}
onClick: () => editSalesOrder.open()
}),
DeleteItemAction({
hidden: !user.hasDeleteRole(UserRoles.sales_order)
// TODO: Delete?
CancelItemAction({
tooltip: t`Cancel order`
}),
DuplicateItemAction({
hidden: !user.hasAddRole(UserRoles.sales_order),
onClick: () => duplicateSalesOrder.open()
})
]}
/>
@ -309,6 +329,7 @@ export default function SalesOrderDetail() {
return (
<>
{editSalesOrder.modal}
{duplicateSalesOrder.modal}
<Stack spacing="xs">
<LoadingOverlay visible={instanceQuery.isFetching} />
<PageDetail

View File

@ -1,19 +1,9 @@
import { t } from '@lingui/macro';
import {
Alert,
Badge,
Grid,
Group,
LoadingOverlay,
Skeleton,
Stack,
Text
} from '@mantine/core';
import { Grid, LoadingOverlay, Skeleton, Stack } from '@mantine/core';
import {
IconBookmark,
IconBoxPadding,
IconChecklist,
IconCopy,
IconDots,
IconHistory,
IconInfoCircle,
@ -23,7 +13,7 @@ import {
IconSitemap
} from '@tabler/icons-react';
import { ReactNode, useMemo, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useParams } from 'react-router-dom';
import { DetailsField, DetailsTable } from '../../components/details/Details';
import DetailsBadge from '../../components/details/DetailsBadge';
@ -74,8 +64,6 @@ export default function StockDetail() {
const user = useUserState();
const navigate = useNavigate();
const [treeOpen, setTreeOpen] = useState(false);
const {
@ -375,11 +363,8 @@ export default function StockDetail() {
initialData: {
...stockitem
},
onFormSuccess: (response: any) => {
if (response.pk) {
navigate(getDetailUrl(ModelType.stockitem, response.pk));
}
}
follow: true,
modelType: ModelType.stockitem
});
const stockActionProps: StockOperationProps = useMemo(() => {
@ -479,6 +464,11 @@ export default function StockDetail() {
return instanceQuery.isLoading
? []
: [
<DetailsBadge
color="yellow"
label={t`In Production`}
visible={stockitem.is_building}
/>,
<DetailsBadge
color="blue"
label={t`Serial Number` + `: ${stockitem.serial}`}

View File

@ -2,8 +2,7 @@
* Common rendering functions for table column data.
*/
import { t } from '@lingui/macro';
import { Anchor } from '@mantine/core';
import { access } from 'fs';
import { Anchor, Text } from '@mantine/core';
import { YesNoButton } from '../components/buttons/YesNoButton';
import { Thumbnail } from '../components/images/Thumbnail';
@ -27,6 +26,34 @@ export function PartColumn(part: any, full_name?: boolean) {
);
}
export function LocationColumn({
accessor,
title,
sortable,
ordering
}: {
accessor: string;
title?: string;
sortable?: boolean;
ordering?: string;
}): TableColumn {
return {
accessor: accessor,
title: title ?? t`Location`,
sortable: sortable ?? true,
ordering: ordering ?? 'location',
render: (record: any) => {
let location = resolveItem(record, accessor);
if (!location) {
return <Text italic>{t`No location set`}</Text>;
}
return <Text>{location.name}</Text>;
}
};
}
export function BooleanColumn({
accessor,
title,

View File

@ -27,7 +27,7 @@ export function TableHoverCard({
}
return (
<HoverCard withinPortal={true}>
<HoverCard withinPortal={true} closeDelay={20} openDelay={250}>
<HoverCard.Target>
<Group spacing="xs" position="apart" noWrap={true}>
{value}

View File

@ -298,7 +298,7 @@ export function BomTable({
const newBomItem = useCreateApiFormModal({
url: ApiEndpoints.bom_list,
title: t`Create BOM Item`,
title: t`Add BOM Item`,
fields: bomItemFields(),
initialData: {
part: partId

View File

@ -1,6 +1,5 @@
import { t } from '@lingui/macro';
import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { AddItemButton } from '../../components/buttons/AddItemButton';
import { PartHoverCard } from '../../components/images/Thumbnail';
@ -11,7 +10,6 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles';
import { useBuildOrderFields } from '../../forms/BuildForms';
import { getDetailUrl } from '../../functions/urls';
import { useCreateApiFormModal } from '../../hooks/UseForm';
import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState';
@ -130,7 +128,6 @@ export function BuildOrderTable({
];
}, []);
const navigate = useNavigate();
const user = useUserState();
const table = useTable('buildorder');
@ -146,11 +143,8 @@ export function BuildOrderTable({
sales_order: salesOrderId,
parent: parentBuildId
},
onFormSuccess: (data: any) => {
if (data.pk) {
navigate(getDetailUrl(ModelType.build, data.pk));
}
}
follow: true,
modelType: ModelType.build
});
const tableActions = useMemo(() => {

View File

@ -0,0 +1,284 @@
import { t } from '@lingui/macro';
import { Group, Text } from '@mantine/core';
import {
IconCircleCheck,
IconCircleX,
IconExclamationCircle
} from '@tabler/icons-react';
import { useQuery } from '@tanstack/react-query';
import { useCallback, useMemo } from 'react';
import { api } from '../../App';
import { ActionButton } from '../../components/buttons/ActionButton';
import { AddItemButton } from '../../components/buttons/AddItemButton';
import { ProgressBar } from '../../components/items/ProgressBar';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles';
import { InvenTreeIcon } from '../../functions/icons';
import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';
import { TableColumn } from '../Column';
import { LocationColumn, PartColumn } from '../ColumnRenderers';
import { InvenTreeTable } from '../InvenTreeTable';
import { RowAction } from '../RowActions';
import { TableHoverCard } from '../TableHoverCard';
type TestResultOverview = {
name: string;
result: boolean;
};
export default function BuildOutputTable({
buildId,
partId
}: {
buildId: number;
partId: number;
}) {
const user = useUserState();
const table = useTable('build-outputs');
// Fetch the test templates associated with the partId
const { data: testTemplates } = useQuery({
queryKey: ['buildoutputtests', partId],
queryFn: async () => {
if (!partId) {
return [];
}
return api
.get(apiUrl(ApiEndpoints.part_test_template_list), {
params: {
part: partId,
include_inherited: true,
enabled: true,
required: true
}
})
.then((response) => response.data)
.catch(() => []);
}
});
const hasRequiredTests: boolean = useMemo(() => {
return (testTemplates?.length ?? 0) > 0;
}, [partId, testTemplates]);
// Format table records
const formatRecords = useCallback(
(records: any[]): any[] => {
records?.forEach((record: any, index: number) => {
let results: TestResultOverview[] = [];
let passCount: number = 0;
// Iterate through each
testTemplates?.forEach((template: any) => {
// Find the "newest" result for this template in the returned data
let result = record.tests
?.filter((test: any) => test.template == template.pk)
.sort((a: any, b: any) => {
return a.pk < b.pk ? 1 : -1;
})
.shift();
if (template?.required && result?.result) {
passCount += 1;
}
results.push({
name: template.test_name,
result: result?.result ?? false
});
});
records[index].passCount = passCount;
records[index].results = results;
});
return records;
},
[partId, testTemplates]
);
const tableActions = useMemo(() => {
// TODO: Button to create new build output
// TODO: Button to complete output(s)
// TODO: Button to cancel output(s)
// TODO: Button to scrap output(s)
return [
<AddItemButton
tooltip={t`Add Build Output`}
hidden={!user.hasAddRole(UserRoles.build)}
/>,
<ActionButton
tooltip={t`Complete selected outputs`}
icon={<InvenTreeIcon icon="success" />}
color="green"
disabled={!table.hasSelectedRecords}
/>,
<ActionButton
tooltip={t`Scrap selected outputs`}
icon={<InvenTreeIcon icon="cancel" />}
color="red"
disabled={!table.hasSelectedRecords}
/>,
<ActionButton
tooltip={t`Cancel selected outputs`}
icon={<InvenTreeIcon icon="delete" />}
color="red"
disabled={!table.hasSelectedRecords}
/>
];
}, [user, partId, buildId, table.hasSelectedRecords]);
const rowActions = useCallback(
(record: any) => {
let actions: RowAction[] = [
{
title: t`Allocate`,
tooltip: t`Allocate stock to build output`,
color: 'blue',
icon: <InvenTreeIcon icon="plus" />
},
{
title: t`Deallocate`,
tooltip: t`Deallocate stock from build output`,
color: 'red',
icon: <InvenTreeIcon icon="minus" />
},
{
title: t`Complete`,
tooltip: t`Complete build output`,
color: 'green',
icon: <InvenTreeIcon icon="success" />
},
{
title: t`Scrap`,
tooltip: t`Scrap build output`,
color: 'red',
icon: <InvenTreeIcon icon="cancel" />
},
{
title: t`Delete`,
tooltip: t`Delete build output`,
color: 'red',
icon: <InvenTreeIcon icon="delete" />
}
];
return actions;
},
[user, partId, buildId]
);
const tableColumns: TableColumn[] = useMemo(() => {
return [
{
accessor: 'part',
sortable: true,
render: (record: any) => PartColumn(record?.part_detail)
},
{
accessor: 'quantity',
ordering: 'stock',
sortable: true,
switchable: false,
title: t`Build Output`,
render: (record: any) => {
let text = record.quantity;
if (record.serial) {
text = `# ${record.serial}`;
}
return (
<Group position="left" noWrap>
<Text>{text}</Text>
{record.batch && (
<Text italic size="sm">
{t`Batch`}: {record.batch}
</Text>
)}
</Group>
);
}
},
LocationColumn({
accessor: 'location_detail'
}),
{
accessor: 'allocations',
sortable: false,
switchable: false,
title: t`Allocated Items`,
render: (record: any) => {
// TODO: Implement this!
return '-';
}
},
{
accessor: 'tests',
sortable: false,
switchable: false,
title: t`Required Tests`,
hidden: !hasRequiredTests,
render: (record: any) => {
const extra =
record.results?.map((result: TestResultOverview) => {
return (
result && (
<Group position="left" key={result.name} noWrap>
{result.result ? (
<IconCircleCheck color="green" />
) : (
<IconCircleX color="red" />
)}
<Text>{result.name}</Text>
</Group>
)
);
}) ?? [];
return (
<TableHoverCard
value={
<ProgressBar
progressLabel
value={record.passCount ?? 0}
maximum={testTemplates?.length ?? 0}
/>
}
extra={extra}
title={t`Test Results`}
/>
);
}
}
];
}, [buildId, partId]);
return (
<>
<InvenTreeTable
tableState={table}
url={apiUrl(ApiEndpoints.stock_item_list)}
columns={tableColumns}
props={{
params: {
part_detail: true,
tests: true,
is_building: true,
build: buildId
},
modelType: ModelType.stockitem,
dataFormatter: formatRecords,
tableActions: tableActions,
rowActions: rowActions,
enableSelection: true
}}
/>
</>
);
}

View File

@ -1,7 +1,7 @@
import { t } from '@lingui/macro';
import { Group, Text } from '@mantine/core';
import { access } from 'fs';
import { useMemo } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { AddItemButton } from '../../components/buttons/AddItemButton';
@ -10,13 +10,17 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles';
import { companyFields } from '../../forms/CompanyForms';
import { useCreateApiFormModal } from '../../hooks/UseForm';
import {
useCreateApiFormModal,
useEditApiFormModal
} from '../../hooks/UseForm';
import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';
import { BooleanColumn, DescriptionColumn } from '../ColumnRenderers';
import { TableFilter } from '../Filter';
import { InvenTreeTable } from '../InvenTreeTable';
import { RowEditAction } from '../RowActions';
/**
* A table which displays a list of company records,
@ -68,17 +72,21 @@ export function CompanyTable({
const newCompany = useCreateApiFormModal({
url: ApiEndpoints.company_list,
title: t`New Company`,
title: t`Add Company`,
fields: companyFields(),
initialData: params,
onFormSuccess: (response) => {
if (response.pk) {
let base = path ?? 'company';
navigate(`/${base}/${response.pk}`);
} else {
table.refreshTable();
}
}
follow: true,
modelType: ModelType.company
});
const [selectedCompany, setSelectedCompany] = useState<number>(0);
const editCompany = useEditApiFormModal({
url: ApiEndpoints.company_list,
pk: selectedCompany,
title: t`Edit Company`,
fields: companyFields(),
onFormSuccess: (record: any) => table.updateRecord(record)
});
const tableFilters: TableFilter[] = useMemo(() => {
@ -120,9 +128,27 @@ export function CompanyTable({
];
}, [user]);
const rowActions = useCallback(
(record: any) => {
return [
RowEditAction({
hidden:
!user.hasChangeRole(UserRoles.purchase_order) &&
!user.hasChangeRole(UserRoles.sales_order),
onClick: () => {
setSelectedCompany(record.pk);
editCompany.open();
}
})
];
},
[user]
);
return (
<>
{newCompany.modal}
{editCompany.modal}
<InvenTreeTable
url={apiUrl(ApiEndpoints.company_list)}
tableState={table}
@ -133,6 +159,7 @@ export function CompanyTable({
},
tableFilters: tableFilters,
tableActions: tableActions,
rowActions: rowActions,
onRowClick: (row: any) => {
if (row.pk) {
let base = path ?? 'company';

View File

@ -491,7 +491,7 @@ export function MachineListTable({
}, [machineDrivers, createFormMachineType]);
const createMachineForm = useCreateApiFormModal({
title: t`Create machine`,
title: t`Add machine`,
url: ApiEndpoints.machine_list,
fields: {
name: {},

View File

@ -278,11 +278,8 @@ export function PartListTable({ props }: { props: InvenTreeTableProps }) {
initialData: {
...(props.params ?? {})
},
onFormSuccess: (data: any) => {
if (data.pk) {
navigate(getDetailUrl(ModelType.part, data.pk));
}
}
follow: true,
modelType: ModelType.part
});
const tableActions = useMemo(() => {

View File

@ -60,7 +60,7 @@ export function ManufacturerPartTable({ params }: { params: any }): ReactNode {
const createManufacturerPart = useCreateApiFormModal({
url: ApiEndpoints.manufacturer_part_list,
title: t`Create Manufacturer Part`,
title: t`Add Manufacturer Part`,
fields: useManufacturerPartFields(),
onFormSuccess: table.refreshTable,
initialData: {

View File

@ -1,6 +1,5 @@
import { t } from '@lingui/macro';
import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { AddItemButton } from '../../components/buttons/AddItemButton';
import { Thumbnail } from '../../components/images/Thumbnail';
@ -9,7 +8,6 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles';
import { usePurchaseOrderFields } from '../../forms/PurchaseOrderForms';
import { getDetailUrl } from '../../functions/urls';
import { useCreateApiFormModal } from '../../hooks/UseForm';
import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState';
@ -43,8 +41,6 @@ export function PurchaseOrderTable({
supplierId?: number;
supplierPartId?: number;
}) {
const navigate = useNavigate();
const table = useTable('purchase-order');
const user = useUserState();
@ -115,13 +111,8 @@ export function PurchaseOrderTable({
initialData: {
supplier: supplierId
},
onFormSuccess: (response) => {
if (response.pk) {
navigate(getDetailUrl(ModelType.purchaseorder, response.pk));
} else {
table.refreshTable();
}
}
follow: true,
modelType: ModelType.purchaseorder
});
const tableActions = useMemo(() => {

View File

@ -1,6 +1,5 @@
import { t } from '@lingui/macro';
import { useCallback, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { useMemo } from 'react';
import { AddItemButton } from '../../components/buttons/AddItemButton';
import { Thumbnail } from '../../components/images/Thumbnail';
@ -9,8 +8,6 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles';
import { useReturnOrderFields } from '../../forms/SalesOrderForms';
import { notYetImplemented } from '../../functions/notifications';
import { getDetailUrl } from '../../functions/urls';
import { useCreateApiFormModal } from '../../hooks/UseForm';
import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState';
@ -37,7 +34,6 @@ import { InvenTreeTable } from '../InvenTreeTable';
export function ReturnOrderTable({ params }: { params?: any }) {
const table = useTable('return-orders');
const user = useUserState();
const navigate = useNavigate();
const tableFilters: TableFilter[] = useMemo(() => {
return [
@ -101,13 +97,8 @@ export function ReturnOrderTable({ params }: { params?: any }) {
url: ApiEndpoints.return_order_list,
title: t`Add Return Order`,
fields: returnOrderFields,
onFormSuccess: (response) => {
if (response.pk) {
navigate(getDetailUrl(ModelType.returnorder, response.pk));
} else {
table.refreshTable();
}
}
follow: true,
modelType: ModelType.returnorder
});
const tableActions = useMemo(() => {

View File

@ -1,6 +1,5 @@
import { t } from '@lingui/macro';
import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { AddItemButton } from '../../components/buttons/AddItemButton';
import { Thumbnail } from '../../components/images/Thumbnail';
@ -9,7 +8,6 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles';
import { useSalesOrderFields } from '../../forms/SalesOrderForms';
import { getDetailUrl } from '../../functions/urls';
import { useCreateApiFormModal } from '../../hooks/UseForm';
import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState';
@ -43,8 +41,6 @@ export function SalesOrderTable({
const table = useTable('sales-order');
const user = useUserState();
const navigate = useNavigate();
const tableFilters: TableFilter[] = useMemo(() => {
return [
{
@ -70,13 +66,8 @@ export function SalesOrderTable({
initialData: {
customer: customerId
},
onFormSuccess: (response) => {
if (response.pk) {
navigate(getDetailUrl(ModelType.salesorder, response.pk));
} else {
table.refreshTable();
}
}
follow: true,
modelType: ModelType.salesorder
});
const tableActions = useMemo(() => {

View File

@ -240,7 +240,7 @@ export function TemplateTable({
const newTemplate = useCreateApiFormModal({
url: apiEndpoint,
pathParams: { variant },
title: t`Create new` + ' ' + templateTypeTranslation,
title: t`Add new` + ' ' + templateTypeTranslation,
fields: {
name: {},
description: {},

View File

@ -1,7 +1,6 @@
import { t } from '@lingui/macro';
import { Group, Text } from '@mantine/core';
import { ReactNode, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { AddItemButton } from '../../components/buttons/AddItemButton';
import { ActionDropdown } from '../../components/items/ActionDropdown';
@ -22,14 +21,15 @@ import {
useTransferStockItem
} from '../../forms/StockForms';
import { InvenTreeIcon } from '../../functions/icons';
import { getDetailUrl } from '../../functions/urls';
import { useCreateApiFormModal } from '../../hooks/UseForm';
import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';
import { TableColumn } from '../Column';
import {
DateColumn,
DescriptionColumn,
LocationColumn,
PartColumn,
StatusColumn
} from '../ColumnRenderers';
@ -55,7 +55,7 @@ function stockItemTableColumns(): TableColumn[] {
ordering: 'stock',
sortable: true,
title: t`Stock`,
render: (record) => {
render: (record: any) => {
// TODO: Push this out into a custom renderer
let quantity = record?.quantity ?? 0;
let allocated = record?.allocated ?? 0;
@ -198,16 +198,14 @@ function stockItemTableColumns(): TableColumn[] {
accessor: 'batch',
sortable: true
},
{
accessor: 'location',
sortable: true,
render: function (record: any) {
// TODO: Custom renderer for location
// TODO: Note, if not "In stock" we don't want to display the actual location here
return record?.location_detail?.pathstring ?? record.location ?? '-';
}
},
// TODO: stocktake column
LocationColumn({
accessor: 'location_detail'
}),
DateColumn({
accessor: 'stocktake_date',
title: t`Stocktake`,
sortable: true
}),
{
accessor: 'expiry_date',
sortable: true,
@ -357,8 +355,6 @@ export function StockItemTable({
const table = useTable(tableName);
const user = useUserState();
const navigate = useNavigate();
const tableActionParams: StockOperationProps = useMemo(() => {
return {
items: table.selectedRecords,
@ -377,11 +373,8 @@ export function StockItemTable({
part: params.part,
location: params.location
},
onFormSuccess: (data: any) => {
if (data.pk) {
navigate(getDetailUrl(ModelType.stockitem, data.pk));
}
}
follow: true,
modelType: ModelType.stockitem
});
const transferStock = useTransferStockItem(tableActionParams);