mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-17 12:35:46 +00:00
[WIP] Test result table (#6430)
* Add basic table for stock item test results * Improve custom data formatter callback * Custom data formatter for returned results * Update YesNoButton functionality - Add PassFailButton with custom text * Enhancements for stock item test result table - Render all data * Add placeholder row actions * Fix table link * Add option to filter parttesttemplate table by "inherited" * Navigate through to parent part * Update PartTestTemplate model - Save 'key' value to database - Update whenever model is saved - Custom data migration * Custom migration step in tasks.py - Add custom management command - Wraps migration step in maintenance mode * Improve uniqueness validation for PartTestTemplate * Add 'template' field to StockItemTestResult - Links to a PartTestTemplate instance - Add migrations to link existing PartTestTemplates * Add "results" count to PartTestTemplate API - Include in rendered tables * Add 'results' column to test result table - Allow filtering too * Update serializer for StockItemTestResult - Include template information - Update CUI and PUI tables * Control template_detail field with query params * Update ref in api_version.py * Update data migration - Ensure new template is created for top level assembly * Fix admin integration * Update StockItemTestResult table - Remove 'test' field - Make 'template' field non-nullable - Previous data migrations should have accounted for this * Implement "legacy" API support - Create test result by providing test name - Lookup existing template * PUI: Cleanup table * Update tasks.py - Exclude temporary settings when exporting data * Fix unique validation check * Remove duplicate code * CUI: Fix data rendering * More refactoring of PUI table * More fixes for PUI table * Get row expansion working (kinda) * Improve rendering of subtable * More PUI updates: - Edit existing results - Add new results * allow delete of test result * Fix typo * Updates for admin integration * Unit tests for stock migrations * Added migration test for PartTestTemplate * Fix for AttachmentTable - Rebuild actions when permissions are recalculated * Update test fixtures * Add ModelType information * Fix TableState * Fix dataFormatter type def * Improve table rendering * Correctly filter "edit" and "delete" buttons * Loosen requirements for dataFormatter * Fixtures for report tests * Better API filtering for StocokItemTestResult list - Add Filter class - Add option for filtering against legacy "name" data * Cleanup API filter * Fix unit tests * Further unit test fixes * Include test results for installed stock items * Improve rendering of test result table * Fix filtering for getTestResults * More unit test fixes * Fix more unit tests * FIx part unit test * More fixes * More unit test fixes * Rebuild stock item trees when merging * Helper function for adding a test result to a stock item * Set init fix * Code cleanup * Cleanup unused variables * Add docs and more unit tests * Update build unit test
This commit is contained in:
@ -21,7 +21,7 @@ export function setApiDefaults() {
|
||||
const token = useSessionState.getState().token;
|
||||
|
||||
api.defaults.baseURL = host;
|
||||
api.defaults.timeout = 1000;
|
||||
api.defaults.timeout = 2500;
|
||||
|
||||
if (!!token) {
|
||||
api.defaults.headers.common['Authorization'] = `Token ${token}`;
|
||||
|
@ -3,8 +3,18 @@ import { Badge } from '@mantine/core';
|
||||
|
||||
import { isTrue } from '../../functions/conversion';
|
||||
|
||||
export function YesNoButton({ value }: { value: any }) {
|
||||
export function PassFailButton({
|
||||
value,
|
||||
passText,
|
||||
failText
|
||||
}: {
|
||||
value: any;
|
||||
passText?: string;
|
||||
failText?: string;
|
||||
}) {
|
||||
const v = isTrue(value);
|
||||
const pass = passText || t`Pass`;
|
||||
const fail = failText || t`Fail`;
|
||||
|
||||
return (
|
||||
<Badge
|
||||
@ -13,7 +23,11 @@ export function YesNoButton({ value }: { value: any }) {
|
||||
radius="lg"
|
||||
size="sm"
|
||||
>
|
||||
{v ? t`Yes` : t`No`}
|
||||
{v ? pass : fail}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
export function YesNoButton({ value }: { value: any }) {
|
||||
return <PassFailButton value={value} passText={t`Yes`} failText={t`No`} />;
|
||||
}
|
||||
|
@ -23,7 +23,8 @@ import {
|
||||
import {
|
||||
RenderPart,
|
||||
RenderPartCategory,
|
||||
RenderPartParameterTemplate
|
||||
RenderPartParameterTemplate,
|
||||
RenderPartTestTemplate
|
||||
} from './Part';
|
||||
import { RenderStockItem, RenderStockLocation } from './Stock';
|
||||
import { RenderOwner, RenderUser } from './User';
|
||||
@ -48,6 +49,7 @@ const RendererLookup: EnumDictionary<
|
||||
[ModelType.part]: RenderPart,
|
||||
[ModelType.partcategory]: RenderPartCategory,
|
||||
[ModelType.partparametertemplate]: RenderPartParameterTemplate,
|
||||
[ModelType.parttesttemplate]: RenderPartTestTemplate,
|
||||
[ModelType.projectcode]: RenderProjectCode,
|
||||
[ModelType.purchaseorder]: RenderPurchaseOrder,
|
||||
[ModelType.purchaseorderline]: RenderPurchaseOrder,
|
||||
|
@ -32,6 +32,13 @@ export const ModelInformationDict: ModelDict = {
|
||||
url_detail: '/partparametertemplate/:pk/',
|
||||
api_endpoint: ApiEndpoints.part_parameter_template_list
|
||||
},
|
||||
parttesttemplate: {
|
||||
label: t`Part Test Template`,
|
||||
label_multiple: t`Part Test Templates`,
|
||||
url_overview: '/parttesttemplate',
|
||||
url_detail: '/parttesttemplate/:pk/',
|
||||
api_endpoint: ApiEndpoints.part_test_template_list
|
||||
},
|
||||
supplierpart: {
|
||||
label: t`Supplier Part`,
|
||||
label_multiple: t`Supplier Parts`,
|
||||
|
@ -51,3 +51,16 @@ export function RenderPartParameterTemplate({
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function RenderPartTestTemplate({
|
||||
instance
|
||||
}: {
|
||||
instance: any;
|
||||
}): ReactNode {
|
||||
return (
|
||||
<RenderInlineModel
|
||||
primary={instance.test_name}
|
||||
secondary={instance.description}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -80,6 +80,7 @@ export enum ApiEndpoints {
|
||||
stock_location_list = 'stock/location/',
|
||||
stock_location_tree = 'stock/location/tree/',
|
||||
stock_attachment_list = 'stock/attachment/',
|
||||
stock_test_result_list = 'stock/test/',
|
||||
|
||||
// Order API endpoints
|
||||
purchase_order_list = 'order/po/',
|
||||
|
@ -7,6 +7,7 @@ export enum ModelType {
|
||||
manufacturerpart = 'manufacturerpart',
|
||||
partcategory = 'partcategory',
|
||||
partparametertemplate = 'partparametertemplate',
|
||||
parttesttemplate = 'parttesttemplate',
|
||||
projectcode = 'projectcode',
|
||||
stockitem = 'stockitem',
|
||||
stocklocation = 'stocklocation',
|
||||
|
@ -51,6 +51,7 @@ export function useInstance<T = any>({
|
||||
|
||||
return api
|
||||
.get(url, {
|
||||
timeout: 10000,
|
||||
params: params
|
||||
})
|
||||
.then((response) => {
|
||||
|
@ -19,6 +19,8 @@ export type TableState = {
|
||||
activeFilters: TableFilter[];
|
||||
setActiveFilters: (filters: TableFilter[]) => void;
|
||||
clearActiveFilters: () => void;
|
||||
expandedRecords: any[];
|
||||
setExpandedRecords: (records: any[]) => void;
|
||||
selectedRecords: any[];
|
||||
setSelectedRecords: (records: any[]) => void;
|
||||
clearSelectedRecords: () => void;
|
||||
@ -59,6 +61,9 @@ export function useTable(tableName: string): TableState {
|
||||
setActiveFilters([]);
|
||||
}, []);
|
||||
|
||||
// Array of expanded records
|
||||
const [expandedRecords, setExpandedRecords] = useState<any[]>([]);
|
||||
|
||||
// Array of selected records
|
||||
const [selectedRecords, setSelectedRecords] = useState<any[]>([]);
|
||||
|
||||
@ -81,6 +86,8 @@ export function useTable(tableName: string): TableState {
|
||||
activeFilters,
|
||||
setActiveFilters,
|
||||
clearActiveFilters,
|
||||
expandedRecords,
|
||||
setExpandedRecords,
|
||||
selectedRecords,
|
||||
setSelectedRecords,
|
||||
clearSelectedRecords,
|
||||
|
@ -41,6 +41,7 @@ import { apiUrl } from '../../states/ApiState';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import { AttachmentTable } from '../../tables/general/AttachmentTable';
|
||||
import { StockItemTable } from '../../tables/stock/StockItemTable';
|
||||
import StockItemTestResultTable from '../../tables/stock/StockItemTestResultTable';
|
||||
|
||||
export default function StockDetail() {
|
||||
const { id } = useParams();
|
||||
@ -89,7 +90,15 @@ export default function StockDetail() {
|
||||
name: 'testdata',
|
||||
label: t`Test Data`,
|
||||
icon: <IconChecklist />,
|
||||
hidden: !stockitem?.part_detail?.trackable
|
||||
hidden: !stockitem?.part_detail?.trackable,
|
||||
content: stockitem?.pk ? (
|
||||
<StockItemTestResultTable
|
||||
itemId={stockitem.pk}
|
||||
partId={stockitem.part}
|
||||
/>
|
||||
) : (
|
||||
<Skeleton />
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'installed_items',
|
||||
|
@ -74,7 +74,9 @@ export function ReferenceColumn(): TableColumn {
|
||||
export function NoteColumn(): TableColumn {
|
||||
return {
|
||||
accessor: 'note',
|
||||
sortable: false
|
||||
sortable: false,
|
||||
title: t`Note`,
|
||||
render: (record: any) => record.note ?? record.notes
|
||||
};
|
||||
}
|
||||
|
||||
@ -119,6 +121,15 @@ export function ResponsibleColumn(): TableColumn {
|
||||
};
|
||||
}
|
||||
|
||||
export function DateColumn(): TableColumn {
|
||||
return {
|
||||
accessor: 'date',
|
||||
sortable: true,
|
||||
title: t`Date`,
|
||||
render: (record: any) => renderDate(record.date)
|
||||
};
|
||||
}
|
||||
|
||||
export function TargetDateColumn(): TableColumn {
|
||||
return {
|
||||
accessor: 'target_date',
|
||||
|
@ -74,8 +74,9 @@ export type InvenTreeTableProps<T = any> = {
|
||||
tableFilters?: TableFilter[];
|
||||
tableActions?: React.ReactNode[];
|
||||
printingActions?: any[];
|
||||
rowExpansion?: any;
|
||||
idAccessor?: string;
|
||||
dataFormatter?: (data: T) => any;
|
||||
dataFormatter?: (data: any) => any;
|
||||
rowActions?: (record: T) => RowAction[];
|
||||
onRowClick?: (record: T, index: number, event: any) => void;
|
||||
};
|
||||
@ -223,14 +224,12 @@ export function InvenTreeTable<T = any>({
|
||||
hidden: false,
|
||||
switchable: false,
|
||||
width: 50,
|
||||
render: function (record: any) {
|
||||
return (
|
||||
<RowActions
|
||||
actions={tableProps.rowActions?.(record) ?? []}
|
||||
disabled={tableState.selectedRecords.length > 0}
|
||||
/>
|
||||
);
|
||||
}
|
||||
render: (record: any) => (
|
||||
<RowActions
|
||||
actions={tableProps.rowActions?.(record) ?? []}
|
||||
disabled={tableState.selectedRecords.length > 0}
|
||||
/>
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
@ -373,14 +372,11 @@ export function InvenTreeTable<T = any>({
|
||||
tableProps.noRecordsText ?? t`No records found`
|
||||
);
|
||||
|
||||
let results = [];
|
||||
let results = response.data?.results ?? response.data ?? [];
|
||||
|
||||
if (props.dataFormatter) {
|
||||
// Custom data formatter provided
|
||||
results = props.dataFormatter(response.data);
|
||||
} else {
|
||||
// Extract returned data (accounting for pagination) and ensure it is a list
|
||||
results = response.data?.results ?? response.data ?? [];
|
||||
results = props.dataFormatter(results);
|
||||
}
|
||||
|
||||
if (!Array.isArray(results)) {
|
||||
@ -611,6 +607,7 @@ export function InvenTreeTable<T = any>({
|
||||
onSelectedRecordsChange={
|
||||
tableProps.enableSelection ? onSelectedRecordsChange : undefined
|
||||
}
|
||||
rowExpansion={tableProps.rowExpansion}
|
||||
fetching={isFetching}
|
||||
noRecordsText={missingRecordsText}
|
||||
records={data}
|
||||
|
@ -1,10 +1,14 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { Trans, t } from '@lingui/macro';
|
||||
import { Alert, Badge, Text } from '@mantine/core';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { AddItemButton } from '../../components/buttons/AddItemButton';
|
||||
import { ApiFormFieldSet } from '../../components/forms/fields/ApiFormField';
|
||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||
import { ModelType } from '../../enums/ModelType';
|
||||
import { UserRoles } from '../../enums/Roles';
|
||||
import { getDetailUrl } from '../../functions/urls';
|
||||
import {
|
||||
useCreateApiFormModal,
|
||||
useDeleteApiFormModal,
|
||||
@ -22,6 +26,7 @@ import { RowDeleteAction, RowEditAction } from '../RowActions';
|
||||
export default function PartTestTemplateTable({ partId }: { partId: number }) {
|
||||
const table = useTable('part-test-template');
|
||||
const user = useUserState();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const tableColumns: TableColumn[] = useMemo(() => {
|
||||
return [
|
||||
@ -30,6 +35,15 @@ export default function PartTestTemplateTable({ partId }: { partId: number }) {
|
||||
switchable: false,
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
accessor: 'results',
|
||||
switchable: true,
|
||||
sortable: true,
|
||||
title: t`Results`,
|
||||
render: (record: any) => {
|
||||
return record.results || <Badge color="blue">{t`No Results`}</Badge>;
|
||||
}
|
||||
},
|
||||
DescriptionColumn({
|
||||
switchable: false
|
||||
}),
|
||||
@ -43,7 +57,7 @@ export default function PartTestTemplateTable({ partId }: { partId: number }) {
|
||||
accessor: 'requires_attachment'
|
||||
})
|
||||
];
|
||||
}, []);
|
||||
}, [partId]);
|
||||
|
||||
const tableFilters: TableFilter[] = useMemo(() => {
|
||||
return [
|
||||
@ -58,6 +72,11 @@ export default function PartTestTemplateTable({ partId }: { partId: number }) {
|
||||
{
|
||||
name: 'requires_attachment',
|
||||
description: t`Show tests that require an attachment`
|
||||
},
|
||||
{
|
||||
name: 'include_inherited',
|
||||
label: t`Include Inherited`,
|
||||
description: t`Show tests from inherited templates`
|
||||
}
|
||||
];
|
||||
}, []);
|
||||
@ -99,13 +118,27 @@ export default function PartTestTemplateTable({ partId }: { partId: number }) {
|
||||
url: ApiEndpoints.part_test_template_list,
|
||||
pk: selectedTest,
|
||||
title: t`Delete Test Template`,
|
||||
preFormContent: (
|
||||
<Alert color="red" title={t`This action cannot be reversed`}>
|
||||
<Text>
|
||||
<Trans>
|
||||
Any tests results associated with this template will be deleted
|
||||
</Trans>
|
||||
</Text>
|
||||
</Alert>
|
||||
),
|
||||
onFormSuccess: table.refreshTable
|
||||
});
|
||||
|
||||
const rowActions = useCallback(
|
||||
(record: any) => {
|
||||
let can_edit = user.hasChangeRole(UserRoles.part);
|
||||
let can_delete = user.hasDeleteRole(UserRoles.part);
|
||||
const can_edit = user.hasChangeRole(UserRoles.part);
|
||||
const can_delete = user.hasDeleteRole(UserRoles.part);
|
||||
|
||||
if (record.part != partId) {
|
||||
// No actions, as this test is defined for a parent part
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
RowEditAction({
|
||||
@ -124,7 +157,7 @@ export default function PartTestTemplateTable({ partId }: { partId: number }) {
|
||||
})
|
||||
];
|
||||
},
|
||||
[user]
|
||||
[user, partId]
|
||||
);
|
||||
|
||||
const tableActions = useMemo(() => {
|
||||
@ -150,11 +183,18 @@ export default function PartTestTemplateTable({ partId }: { partId: number }) {
|
||||
columns={tableColumns}
|
||||
props={{
|
||||
params: {
|
||||
part: partId
|
||||
part: partId,
|
||||
part_detail: true
|
||||
},
|
||||
tableFilters: tableFilters,
|
||||
tableActions: tableActions,
|
||||
rowActions: rowActions
|
||||
rowActions: rowActions,
|
||||
onRowClick: (row) => {
|
||||
if (row.part && row.part != partId) {
|
||||
// This test is defined for a different part
|
||||
navigate(getDetailUrl(ModelType.part, row.part));
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
@ -67,8 +67,8 @@ export default function CurrencyTable() {
|
||||
columns={columns}
|
||||
props={{
|
||||
tableActions: tableActions,
|
||||
dataFormatter: (data) => {
|
||||
let rates = data?.exchange_rates ?? {};
|
||||
dataFormatter: (data: any) => {
|
||||
let rates = data.exchange_rates ?? {};
|
||||
|
||||
return Object.entries(rates).map(([currency, rate]) => {
|
||||
return {
|
||||
|
@ -338,7 +338,7 @@ export function StockItemTable({ params = {} }: { params?: any }) {
|
||||
columns={tableColumns}
|
||||
props={{
|
||||
enableDownload: true,
|
||||
enableSelection: true,
|
||||
enableSelection: false,
|
||||
tableFilters: tableFilters,
|
||||
onRowClick: (record) =>
|
||||
navigate(getDetailUrl(ModelType.stockitem, record.pk)),
|
||||
|
428
src/frontend/src/tables/stock/StockItemTestResultTable.tsx
Normal file
428
src/frontend/src/tables/stock/StockItemTestResultTable.tsx
Normal file
@ -0,0 +1,428 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { Badge, Group, Text, Tooltip } from '@mantine/core';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import {
|
||||
IconCircleCheck,
|
||||
IconCirclePlus,
|
||||
IconInfoCircle
|
||||
} from '@tabler/icons-react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { DataTable } from 'mantine-datatable';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { api } from '../../App';
|
||||
import { AddItemButton } from '../../components/buttons/AddItemButton';
|
||||
import { ApiFormFieldSet } from '../../components/forms/fields/ApiFormField';
|
||||
import { AttachmentLink } from '../../components/items/AttachmentLink';
|
||||
import { PassFailButton } from '../../components/items/YesNoButton';
|
||||
import { RenderUser } from '../../components/render/User';
|
||||
import { renderDate } from '../../defaults/formatters';
|
||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||
import { UserRoles } from '../../enums/Roles';
|
||||
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 { DescriptionColumn, NoteColumn } from '../ColumnRenderers';
|
||||
import { TableFilter } from '../Filter';
|
||||
import { InvenTreeTable } from '../InvenTreeTable';
|
||||
import { RowActions, RowDeleteAction, RowEditAction } from '../RowActions';
|
||||
|
||||
export default function StockItemTestResultTable({
|
||||
partId,
|
||||
itemId
|
||||
}: {
|
||||
partId: number;
|
||||
itemId: number;
|
||||
}) {
|
||||
const user = useUserState();
|
||||
const table = useTable('stocktests');
|
||||
|
||||
// Fetch the test templates required for this stock item
|
||||
const { data: testTemplates } = useQuery({
|
||||
queryKey: ['stocktesttemplates', partId, itemId],
|
||||
queryFn: async () => {
|
||||
if (!partId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return api
|
||||
.get(apiUrl(ApiEndpoints.part_test_template_list), {
|
||||
params: {
|
||||
part: partId,
|
||||
include_inherited: true
|
||||
}
|
||||
})
|
||||
.then((response) => response.data)
|
||||
.catch((_error) => []);
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
table.refreshTable();
|
||||
}, [testTemplates]);
|
||||
|
||||
// Format the test results based on the returned data
|
||||
const formatRecords = useCallback(
|
||||
(records: any[]): any[] => {
|
||||
// Construct a list of test templates
|
||||
let results = testTemplates.map((template: any) => {
|
||||
return {
|
||||
...template,
|
||||
templateId: template.pk,
|
||||
results: []
|
||||
};
|
||||
});
|
||||
|
||||
// If any of the tests results point to templates which we do not have, add them in
|
||||
records.forEach((record) => {
|
||||
if (!results.find((r: any) => r.templateId == record.template)) {
|
||||
results.push({
|
||||
...record.template_detail,
|
||||
templateId: record.template,
|
||||
results: []
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Iterate through the returned records
|
||||
// Note that the results are sorted by oldest first,
|
||||
// to ensure that the most recent result is displayed "on top"
|
||||
records
|
||||
.sort((a: any, b: any) => {
|
||||
return a.pk > b.pk ? 1 : -1;
|
||||
})
|
||||
.forEach((record) => {
|
||||
// Find matching template
|
||||
let idx = results.findIndex(
|
||||
(r: any) => r.templateId == record.template
|
||||
);
|
||||
if (idx >= 0) {
|
||||
results[idx] = {
|
||||
...results[idx],
|
||||
...record
|
||||
};
|
||||
|
||||
results[idx].results.push(record);
|
||||
}
|
||||
});
|
||||
|
||||
return results;
|
||||
},
|
||||
[partId, itemId, testTemplates]
|
||||
);
|
||||
|
||||
const tableColumns: TableColumn[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
accessor: 'test',
|
||||
title: t`Test`,
|
||||
switchable: false,
|
||||
sortable: true,
|
||||
render: (record: any) => {
|
||||
let required = record.required ?? record.template_detail?.required;
|
||||
let installed =
|
||||
record.stock_item != undefined && record.stock_item != itemId;
|
||||
|
||||
return (
|
||||
<Group position="apart">
|
||||
<Text italic={installed} fw={required && 700}>
|
||||
{!record.templateId && '- '}
|
||||
{record.test_name ?? record.template_detail?.test_name}
|
||||
</Text>
|
||||
<Group position="right">
|
||||
{record.results && record.results.length > 1 && (
|
||||
<Tooltip label={t`Test Results`}>
|
||||
<Badge color="lightblue" variant="filled">
|
||||
{record.results.length}
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
)}
|
||||
{installed && (
|
||||
<Tooltip label={t`Test result for installed stock item`}>
|
||||
<IconInfoCircle size={16} color="blue" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</Group>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessor: 'result',
|
||||
title: t`Result`,
|
||||
switchable: false,
|
||||
sortable: true,
|
||||
render: (record: any) => {
|
||||
if (record.result === undefined) {
|
||||
return (
|
||||
<Badge color="lightblue" variant="filled">{t`No Result`}</Badge>
|
||||
);
|
||||
} else {
|
||||
return <PassFailButton value={record.result} />;
|
||||
}
|
||||
}
|
||||
},
|
||||
DescriptionColumn({
|
||||
accessor: 'description'
|
||||
}),
|
||||
{
|
||||
accessor: 'value',
|
||||
title: t`Value`
|
||||
},
|
||||
{
|
||||
accessor: 'attachment',
|
||||
title: t`Attachment`,
|
||||
render: (record: any) =>
|
||||
record.attachment && <AttachmentLink attachment={record.attachment} />
|
||||
},
|
||||
NoteColumn(),
|
||||
{
|
||||
accessor: 'date',
|
||||
sortable: true,
|
||||
title: t`Date`,
|
||||
render: (record: any) => {
|
||||
return (
|
||||
<Group position="apart">
|
||||
{renderDate(record.date)}
|
||||
{record.user_detail && (
|
||||
<RenderUser instance={record.user_detail} />
|
||||
)}
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
}
|
||||
];
|
||||
}, [itemId]);
|
||||
|
||||
const resultFields: ApiFormFieldSet = useMemo(() => {
|
||||
return {
|
||||
template: {
|
||||
filters: {
|
||||
include_inherited: true,
|
||||
part: partId
|
||||
}
|
||||
},
|
||||
result: {},
|
||||
value: {},
|
||||
attachment: {},
|
||||
notes: {},
|
||||
stock_item: {
|
||||
value: itemId,
|
||||
hidden: true
|
||||
}
|
||||
};
|
||||
}, [partId, itemId]);
|
||||
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<number | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
const newTestModal = useCreateApiFormModal({
|
||||
url: ApiEndpoints.stock_test_result_list,
|
||||
fields: resultFields,
|
||||
initialData: {
|
||||
template: selectedTemplate,
|
||||
result: true
|
||||
},
|
||||
title: t`Add Test Result`,
|
||||
onFormSuccess: () => table.refreshTable(),
|
||||
successMessage: t`Test result added`
|
||||
});
|
||||
|
||||
const [selectedTest, setSelectedTest] = useState<number | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
const editTestModal = useEditApiFormModal({
|
||||
url: ApiEndpoints.stock_test_result_list,
|
||||
pk: selectedTest,
|
||||
fields: resultFields,
|
||||
title: t`Edit Test Result`,
|
||||
onFormSuccess: () => table.refreshTable(),
|
||||
successMessage: t`Test result updated`
|
||||
});
|
||||
|
||||
const deleteTestModal = useDeleteApiFormModal({
|
||||
url: ApiEndpoints.stock_test_result_list,
|
||||
pk: selectedTest,
|
||||
title: t`Delete Test Result`,
|
||||
onFormSuccess: () => table.refreshTable(),
|
||||
successMessage: t`Test result deleted`
|
||||
});
|
||||
|
||||
const passTest = useCallback(
|
||||
(templateId: number) => {
|
||||
api
|
||||
.post(apiUrl(ApiEndpoints.stock_test_result_list), {
|
||||
template: templateId,
|
||||
stock_item: itemId,
|
||||
result: true
|
||||
})
|
||||
.then(() => {
|
||||
table.refreshTable();
|
||||
showNotification({
|
||||
title: t`Test Passed`,
|
||||
message: t`Test result has been recorded`,
|
||||
color: 'green'
|
||||
});
|
||||
});
|
||||
},
|
||||
[itemId]
|
||||
);
|
||||
|
||||
const rowActions = useCallback(
|
||||
(record: any) => {
|
||||
if (record.stock_item != undefined && record.stock_item != itemId) {
|
||||
// Test results for other stock items cannot be edited
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
title: t`Pass Test`,
|
||||
color: 'green',
|
||||
icon: <IconCircleCheck />,
|
||||
hidden:
|
||||
!record.templateId ||
|
||||
record?.requires_attachment ||
|
||||
record?.requires_value ||
|
||||
record.result,
|
||||
onClick: () => passTest(record.templateId)
|
||||
},
|
||||
{
|
||||
title: t`Add`,
|
||||
tooltip: t`Add Test Result`,
|
||||
color: 'green',
|
||||
icon: <IconCirclePlus />,
|
||||
hidden: !user.hasAddRole(UserRoles.stock) || !record.templateId,
|
||||
onClick: () => {
|
||||
setSelectedTemplate(record.templateId);
|
||||
newTestModal.open();
|
||||
}
|
||||
},
|
||||
RowEditAction({
|
||||
tooltip: t`Edit Test Result`,
|
||||
hidden:
|
||||
!user.hasChangeRole(UserRoles.stock) || !record.template_detail,
|
||||
onClick: () => {
|
||||
setSelectedTest(record.pk);
|
||||
editTestModal.open();
|
||||
}
|
||||
}),
|
||||
RowDeleteAction({
|
||||
tooltip: t`Delete Test Result`,
|
||||
hidden:
|
||||
!user.hasDeleteRole(UserRoles.stock) || !record.template_detail,
|
||||
onClick: () => {
|
||||
setSelectedTest(record.pk);
|
||||
deleteTestModal.open();
|
||||
}
|
||||
})
|
||||
];
|
||||
},
|
||||
[user, itemId]
|
||||
);
|
||||
|
||||
const tableFilters: TableFilter[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
name: 'required',
|
||||
label: t`Required`,
|
||||
description: t`Show results for required tests`
|
||||
},
|
||||
{
|
||||
name: 'include_installed',
|
||||
label: t`Include Installed`,
|
||||
description: t`Show results for installed stock items`
|
||||
},
|
||||
{
|
||||
name: 'result',
|
||||
label: t`Passed`,
|
||||
description: t`Show only passed tests`
|
||||
}
|
||||
];
|
||||
}, []);
|
||||
|
||||
const tableActions = useMemo(() => {
|
||||
return [
|
||||
<AddItemButton
|
||||
tooltip={t`Add Test Result`}
|
||||
onClick={() => {
|
||||
setSelectedTemplate(undefined);
|
||||
newTestModal.open();
|
||||
}}
|
||||
hidden={!user.hasAddRole(UserRoles.stock)}
|
||||
/>
|
||||
];
|
||||
}, [user]);
|
||||
|
||||
// Row expansion controller
|
||||
const rowExpansion: any = useMemo(() => {
|
||||
const cols: any = [
|
||||
...tableColumns,
|
||||
{
|
||||
accessor: 'actions',
|
||||
title: ' ',
|
||||
hidden: false,
|
||||
switchable: false,
|
||||
width: 50,
|
||||
render: (record: any) => (
|
||||
<RowActions actions={rowActions(record) ?? []} />
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
return {
|
||||
allowMultiple: true,
|
||||
content: ({ record }: { record: any }) => {
|
||||
if (!record || !record.results || record.results.length < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const results = record?.results ?? [];
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
key={record.pk}
|
||||
noHeader
|
||||
columns={cols}
|
||||
records={results.slice(0, -1)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{newTestModal.modal}
|
||||
{editTestModal.modal}
|
||||
{deleteTestModal.modal}
|
||||
<InvenTreeTable
|
||||
url={apiUrl(ApiEndpoints.stock_test_result_list)}
|
||||
tableState={table}
|
||||
columns={tableColumns}
|
||||
props={{
|
||||
dataFormatter: formatRecords,
|
||||
enablePagination: false,
|
||||
tableActions: tableActions,
|
||||
tableFilters: tableFilters,
|
||||
rowActions: rowActions,
|
||||
rowExpansion: rowExpansion,
|
||||
params: {
|
||||
stock_item: itemId,
|
||||
user_detail: true,
|
||||
attachment_detail: true,
|
||||
template_detail: true
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user