mirror of
https://github.com/inventree/InvenTree.git
synced 2026-07-04 14:10:52 +00:00
[UI] Table field refactor (#12274)
* Use callback funcs * Don't use idx to identify rows * Add debug function for finding why a component re-rendered * Do not pass 'control' through to each row * Prevent unnecessary re-rendering of table rows * Adjust order of operations for hooks * Keep props hidden * Use lightweight NumberInput * Use NumberInput elsewhere * Add comment * use rowId instead of idx * Generic row memos * Compare errors too * Fix for BomItemSubstituteRow * Adjust more forms * memoize quantity * Memoize build lines * Fix re-rendering issues for build allocation * Fix for useConsumeBuildLinesForm * Fix for transfer order table * Fix useReceiveLineItems * Remove memoized pattern * Fix row keys * Cleanup * Create useStockItems hook for memoizing items * Refactoring * More refactoring * Remove obj reference - preventing shallow comparison from working * Add error message to useWhyDidYouUpdate * Cleanup * Cleanup dead code * Adjust modal width * Change attr name * Remove autoFillFilters prop * Adjustments for serialized stock * Fix typing * Bump frontend version * Adjustments for playwright testing * Fix ref issue * Remove debug entry * Update CHANGELOG.md * Reintroduce index to table header * Refactor common component
This commit is contained in:
@@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
- [#12274](https://github.com/inventree/InvenTree/pull/12274) fixes a bug in rendering the "table field" component in frontend forms. Any plugins which make use of the "table field" component in their forms may be affected by this change, and will need to update their form definitions accordingly.
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
||||||
## 1.4.0 - 2026-06-24
|
## 1.4.0 - 2026-06-24
|
||||||
|
|||||||
@@ -221,8 +221,6 @@ export interface BulkEditApiFormModalProps extends ApiFormModalProps {
|
|||||||
|
|
||||||
export type StockOperationProps = {
|
export type StockOperationProps = {
|
||||||
items?: any[];
|
items?: any[];
|
||||||
pk?: number;
|
|
||||||
filters?: any;
|
filters?: any;
|
||||||
model: ModelType.stockitem | 'location' | ModelType.part;
|
|
||||||
refresh: () => void;
|
refresh: () => void;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@inventreedb/ui",
|
"name": "@inventreedb/ui",
|
||||||
"description": "UI components for the InvenTree project",
|
"description": "UI components for the InvenTree project",
|
||||||
"version": "1.4.6",
|
"version": "1.5.0",
|
||||||
"private": false,
|
"private": false,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -247,7 +247,9 @@ export function ApiFormField({
|
|||||||
<TableField
|
<TableField
|
||||||
definition={fieldDefinition}
|
definition={fieldDefinition}
|
||||||
fieldName={fieldName}
|
fieldName={fieldName}
|
||||||
control={controller}
|
value={value}
|
||||||
|
onChange={field.onChange}
|
||||||
|
error={error}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'tags':
|
case 'tags':
|
||||||
|
|||||||
@@ -419,6 +419,7 @@ export function RelatedModelField({
|
|||||||
...definition,
|
...definition,
|
||||||
addCreateFields: undefined,
|
addCreateFields: undefined,
|
||||||
autoFill: undefined,
|
autoFill: undefined,
|
||||||
|
autoFillFilters: undefined,
|
||||||
modelRenderer: undefined,
|
modelRenderer: undefined,
|
||||||
onValueChange: undefined,
|
onValueChange: undefined,
|
||||||
adjustFilters: undefined,
|
adjustFilters: undefined,
|
||||||
|
|||||||
@@ -1,9 +1,23 @@
|
|||||||
import { t } from '@lingui/core/macro';
|
import { t } from '@lingui/core/macro';
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { Alert, Container, Group, Stack, Table, Text } from '@mantine/core';
|
import {
|
||||||
|
Alert,
|
||||||
|
Container,
|
||||||
|
Group,
|
||||||
|
NumberInput,
|
||||||
|
Stack,
|
||||||
|
Table,
|
||||||
|
Text
|
||||||
|
} from '@mantine/core';
|
||||||
import { IconExclamationCircle } from '@tabler/icons-react';
|
import { IconExclamationCircle } from '@tabler/icons-react';
|
||||||
import { type ReactNode, useCallback, useEffect, useMemo } from 'react';
|
import {
|
||||||
import type { FieldValues, UseControllerReturn } from 'react-hook-form';
|
type ReactNode,
|
||||||
|
memo,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
import { AddItemButton } from '@lib/components/AddItemButton';
|
import { AddItemButton } from '@lib/components/AddItemButton';
|
||||||
import { identifierString } from '@lib/functions/Conversion';
|
import { identifierString } from '@lib/functions/Conversion';
|
||||||
@@ -13,35 +27,34 @@ import { StandaloneField } from '../StandaloneField';
|
|||||||
|
|
||||||
export interface TableFieldRowProps {
|
export interface TableFieldRowProps {
|
||||||
item: any;
|
item: any;
|
||||||
idx: number;
|
rowId: string | number;
|
||||||
rowErrors: any;
|
rowErrors: any;
|
||||||
control: UseControllerReturn<FieldValues, any>;
|
changeFn: (rowId: number | string, key: string, value: any) => void;
|
||||||
changeFn: (idx: number, key: string, value: any) => void;
|
removeFn: (rowId: number | string) => void;
|
||||||
removeFn: (idx: number) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function TableFieldRow({
|
function TableFieldRow({
|
||||||
item,
|
item,
|
||||||
idx,
|
rowId,
|
||||||
errors,
|
rowErrors,
|
||||||
definition,
|
modelRenderer,
|
||||||
control,
|
columnCount,
|
||||||
changeFn,
|
changeFn,
|
||||||
removeFn
|
removeFn
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
item: any;
|
item: any;
|
||||||
idx: number;
|
rowId: string | number;
|
||||||
errors: any;
|
rowErrors: any;
|
||||||
definition: ApiFormFieldType;
|
modelRenderer?: ApiFormFieldType['modelRenderer'];
|
||||||
control: UseControllerReturn<FieldValues, any>;
|
columnCount?: number;
|
||||||
changeFn: (idx: number, key: string, value: any) => void;
|
changeFn: (rowId: number | string, key: string, value: any) => void;
|
||||||
removeFn: (idx: number) => void;
|
removeFn: (rowId: number | string) => void;
|
||||||
}>) {
|
}>) {
|
||||||
// Table fields require render function
|
// Table fields require render function
|
||||||
if (!definition.modelRenderer) {
|
if (!modelRenderer) {
|
||||||
return (
|
return (
|
||||||
<Table.Tr key='table-row-no-renderer'>
|
<Table.Tr key='table-row-no-renderer'>
|
||||||
<Table.Td colSpan={definition.headers?.length}>
|
<Table.Td colSpan={columnCount}>
|
||||||
<Alert color='red' title={t`Error`} icon={<IconExclamationCircle />}>
|
<Alert color='red' title={t`Error`} icon={<IconExclamationCircle />}>
|
||||||
{t`modelRenderer entry required for tables`}
|
{t`modelRenderer entry required for tables`}
|
||||||
</Alert>
|
</Alert>
|
||||||
@@ -50,16 +63,60 @@ function TableFieldRow({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return definition.modelRenderer({
|
return modelRenderer({
|
||||||
item: item,
|
item: item,
|
||||||
idx: idx,
|
rowId: rowId,
|
||||||
rowErrors: errors,
|
rowErrors: rowErrors,
|
||||||
control: control,
|
|
||||||
changeFn: changeFn,
|
changeFn: changeFn,
|
||||||
removeFn: removeFn
|
removeFn: removeFn
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Memoize each table field row, so that we don't re-render the entire table when a single row is updated
|
||||||
|
function areShallowEqual(previousValue: any, nextValue: any): boolean {
|
||||||
|
if (previousValue === nextValue) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!previousValue || !nextValue) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof previousValue !== 'object' || typeof nextValue !== 'object') {
|
||||||
|
return previousValue === nextValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousKeys = Object.keys(previousValue);
|
||||||
|
const nextKeys = Object.keys(nextValue);
|
||||||
|
|
||||||
|
if (previousKeys.length !== nextKeys.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of previousKeys) {
|
||||||
|
if (previousValue[key] !== nextValue[key]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MemoizedTableFieldRow = memo(
|
||||||
|
TableFieldRow,
|
||||||
|
(previousProps, nextProps) => {
|
||||||
|
return (
|
||||||
|
previousProps.rowId === nextProps.rowId &&
|
||||||
|
areShallowEqual(previousProps.item, nextProps.item) &&
|
||||||
|
areShallowEqual(previousProps.rowErrors, nextProps.rowErrors) &&
|
||||||
|
previousProps.modelRenderer === nextProps.modelRenderer &&
|
||||||
|
previousProps.changeFn === nextProps.changeFn &&
|
||||||
|
previousProps.removeFn === nextProps.removeFn &&
|
||||||
|
previousProps.columnCount === nextProps.columnCount
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export function TableFieldErrorWrapper({
|
export function TableFieldErrorWrapper({
|
||||||
props,
|
props,
|
||||||
errorKey,
|
errorKey,
|
||||||
@@ -83,33 +140,131 @@ export function TableFieldErrorWrapper({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TableField({
|
function TableFieldComponent({
|
||||||
definition,
|
definition,
|
||||||
fieldName,
|
fieldName,
|
||||||
control
|
value,
|
||||||
|
onChange,
|
||||||
|
error
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
definition: ApiFormFieldType;
|
definition: ApiFormFieldType;
|
||||||
fieldName: string;
|
fieldName: string;
|
||||||
control: UseControllerReturn<FieldValues, any>;
|
value: any;
|
||||||
|
onChange: (value: any) => void;
|
||||||
|
error?: any;
|
||||||
}>) {
|
}>) {
|
||||||
const {
|
const valueRef = useRef(value);
|
||||||
field,
|
const onChangeRef = useRef(onChange);
|
||||||
fieldState: { error }
|
const rowIndexByIdRef = useRef(new Map<string | number, number>());
|
||||||
} = control;
|
const generatedRowIdsRef = useRef(new WeakMap<object, string>());
|
||||||
const { value } = field;
|
const generatedRowIdCounterRef = useRef(0);
|
||||||
|
|
||||||
const onRowFieldChange = (idx: number, key: string, value: any) => {
|
const getRowIdentifier = useCallback(
|
||||||
const val = field.value;
|
(item: any, idx: number): string | number => {
|
||||||
val[idx][key] = value;
|
if (item && typeof item === 'object') {
|
||||||
|
const intrinsicId = item.pk ?? item.item ?? item.id ?? item.uuid;
|
||||||
|
|
||||||
field.onChange(val);
|
if (intrinsicId !== undefined && intrinsicId !== null) {
|
||||||
};
|
return intrinsicId;
|
||||||
|
}
|
||||||
|
|
||||||
const removeRow = (idx: number) => {
|
const existingGeneratedId = generatedRowIdsRef.current.get(item);
|
||||||
const val = field.value;
|
|
||||||
val.splice(idx, 1);
|
if (existingGeneratedId) {
|
||||||
field.onChange(val);
|
return existingGeneratedId;
|
||||||
};
|
}
|
||||||
|
|
||||||
|
generatedRowIdCounterRef.current += 1;
|
||||||
|
const generatedId = `table-row-generated-${generatedRowIdCounterRef.current}`;
|
||||||
|
generatedRowIdsRef.current.set(item, generatedId);
|
||||||
|
|
||||||
|
return generatedId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return item ?? idx;
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Keep refs in sync with latest values without introducing them as deps
|
||||||
|
valueRef.current = value;
|
||||||
|
onChangeRef.current = onChange;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const nextRowIndexById = new Map<string | number, number>();
|
||||||
|
|
||||||
|
value?.forEach((item: any, idx: number) => {
|
||||||
|
nextRowIndexById.set(getRowIdentifier(item, idx), idx);
|
||||||
|
});
|
||||||
|
|
||||||
|
rowIndexByIdRef.current = nextRowIndexById;
|
||||||
|
}, [value, getRowIdentifier]);
|
||||||
|
|
||||||
|
const resolveRowIndex = useCallback((identifier: number | string) => {
|
||||||
|
const mappedIndex = rowIndexByIdRef.current.get(identifier);
|
||||||
|
|
||||||
|
if (mappedIndex !== undefined) {
|
||||||
|
return mappedIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof identifier === 'number' && identifier >= 0) {
|
||||||
|
return identifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onRowFieldChange = useCallback(
|
||||||
|
(identifier: number | string, key: string, rowValue: any) => {
|
||||||
|
const idx = resolveRowIndex(identifier);
|
||||||
|
|
||||||
|
if (idx === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentValue = valueRef.current;
|
||||||
|
|
||||||
|
if (!Array.isArray(currentValue) || currentValue[idx] === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextValue = [...currentValue];
|
||||||
|
const currentRow = nextValue[idx];
|
||||||
|
|
||||||
|
if (currentRow && typeof currentRow === 'object') {
|
||||||
|
nextValue[idx] = {
|
||||||
|
...currentRow,
|
||||||
|
[key]: rowValue
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
valueRef.current = nextValue;
|
||||||
|
onChangeRef.current(nextValue);
|
||||||
|
},
|
||||||
|
[resolveRowIndex]
|
||||||
|
);
|
||||||
|
|
||||||
|
const removeRow = useCallback(
|
||||||
|
(identifier: number | string) => {
|
||||||
|
const idx = resolveRowIndex(identifier);
|
||||||
|
|
||||||
|
if (idx === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentValue = valueRef.current;
|
||||||
|
|
||||||
|
if (!Array.isArray(currentValue)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextValue = [...currentValue];
|
||||||
|
nextValue.splice(idx, 1);
|
||||||
|
|
||||||
|
onChangeRef.current(nextValue);
|
||||||
|
},
|
||||||
|
[resolveRowIndex]
|
||||||
|
);
|
||||||
|
|
||||||
// Extract errors associated with the current row
|
// Extract errors associated with the current row
|
||||||
const rowErrors: any = useCallback(
|
const rowErrors: any = useCallback(
|
||||||
@@ -125,7 +280,7 @@ export function TableField({
|
|||||||
<Table
|
<Table
|
||||||
highlightOnHover
|
highlightOnHover
|
||||||
striped
|
striped
|
||||||
aria-label={`table-field-${field.name}`}
|
aria-label={`table-field-${fieldName}`}
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
>
|
>
|
||||||
<Table.Thead>
|
<Table.Thead>
|
||||||
@@ -146,14 +301,16 @@ export function TableField({
|
|||||||
<Table.Tbody>
|
<Table.Tbody>
|
||||||
{(value?.length ?? 0) > 0 ? (
|
{(value?.length ?? 0) > 0 ? (
|
||||||
value.map((item: any, idx: number) => {
|
value.map((item: any, idx: number) => {
|
||||||
|
const rowId = getRowIdentifier(item, idx);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableFieldRow
|
<MemoizedTableFieldRow
|
||||||
key={`table-row-${idx}`}
|
key={`table-row-${rowId}`}
|
||||||
item={item}
|
item={item}
|
||||||
idx={idx}
|
rowId={rowId}
|
||||||
errors={rowErrors(idx)}
|
rowErrors={rowErrors(idx)}
|
||||||
control={control}
|
modelRenderer={definition.modelRenderer}
|
||||||
definition={definition}
|
columnCount={definition.headers?.length}
|
||||||
changeFn={onRowFieldChange}
|
changeFn={onRowFieldChange}
|
||||||
removeFn={removeRow}
|
removeFn={removeRow}
|
||||||
/>
|
/>
|
||||||
@@ -189,9 +346,7 @@ export function TableField({
|
|||||||
if (definition.addRow === undefined) return;
|
if (definition.addRow === undefined) return;
|
||||||
const ret = definition.addRow();
|
const ret = definition.addRow();
|
||||||
if (ret) {
|
if (ret) {
|
||||||
const val = field.value;
|
onChange([...(value ?? []), ret]);
|
||||||
val.push(ret);
|
|
||||||
field.onChange(val);
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -203,6 +358,19 @@ export function TableField({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const TableField = memo(
|
||||||
|
TableFieldComponent,
|
||||||
|
(previousProps, nextProps) => {
|
||||||
|
return (
|
||||||
|
previousProps.definition === nextProps.definition &&
|
||||||
|
previousProps.fieldName === nextProps.fieldName &&
|
||||||
|
areShallowEqual(previousProps.value, nextProps.value) &&
|
||||||
|
areShallowEqual(previousProps.error, nextProps.error) &&
|
||||||
|
previousProps.onChange === nextProps.onChange
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Display an "extra" row below the main table row, for additional information.
|
* Display an "extra" row below the main table row, for additional information.
|
||||||
* - Each "row" can display an extra row of information below the main row
|
* - Each "row" can display an extra row of information below the main row
|
||||||
@@ -224,10 +392,16 @@ export function TableFieldExtraRow({
|
|||||||
emptyValue?: any;
|
emptyValue?: any;
|
||||||
onValueChange: (value: any) => void;
|
onValueChange: (value: any) => void;
|
||||||
}) {
|
}) {
|
||||||
|
const hasMounted = useRef(false);
|
||||||
|
|
||||||
// Callback whenever the visibility of the sub-field changes
|
// Callback whenever the visibility of the sub-field changes
|
||||||
|
// Skip the initial mount — the value was never set, nothing to reset
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!hasMounted.current) {
|
||||||
|
hasMounted.current = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!visible) {
|
if (!visible) {
|
||||||
// If the sub-field is hidden, reset the value to the "empty" value
|
|
||||||
onValueChange(emptyValue);
|
onValueChange(emptyValue);
|
||||||
}
|
}
|
||||||
}, [visible]);
|
}, [visible]);
|
||||||
@@ -262,3 +436,37 @@ export function TableFieldExtraRow({
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function TableFieldQuantityInput({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
error,
|
||||||
|
min
|
||||||
|
}: {
|
||||||
|
value: number | '';
|
||||||
|
onChange: (value: number | '') => void;
|
||||||
|
error?: string;
|
||||||
|
min?: number;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<NumberInput
|
||||||
|
radius='sm'
|
||||||
|
aria-label='number-field-quantity'
|
||||||
|
min={min}
|
||||||
|
step={1}
|
||||||
|
decimalScale={10}
|
||||||
|
value={value}
|
||||||
|
onChange={(v: number | string) => {
|
||||||
|
if (typeof v === 'number') {
|
||||||
|
onChange(Number.isFinite(v) ? v : '');
|
||||||
|
} else if (v.trim() !== '') {
|
||||||
|
const parsed = Number.parseFloat(v);
|
||||||
|
onChange(Number.isFinite(parsed) ? parsed : '');
|
||||||
|
} else {
|
||||||
|
onChange('');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
error={error}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -172,15 +172,6 @@ export default function ImporterDataSelector({
|
|||||||
description: fieldDef.help_text,
|
description: fieldDef.help_text,
|
||||||
filters: filters
|
filters: filters
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('Defined Field:', field);
|
|
||||||
console.log({
|
|
||||||
...fieldDef,
|
|
||||||
...customField,
|
|
||||||
field_type: fieldDef.type,
|
|
||||||
description: fieldDef.help_text,
|
|
||||||
filters: filters
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ function BomItemSubstituteRow({
|
|||||||
api
|
api
|
||||||
.delete(apiUrl(ApiEndpoints.bom_substitute_list, record.pk))
|
.delete(apiUrl(ApiEndpoints.bom_substitute_list, record.pk))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
props.removeFn(props.idx);
|
props.removeFn(props.rowId);
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
showApiErrorMessage({
|
showApiErrorMessage({
|
||||||
@@ -116,7 +116,7 @@ export function useEditBomSubstitutesForm(props: BomItemSubstituteFormProps) {
|
|||||||
modelRenderer: (row: TableFieldRowProps) => {
|
modelRenderer: (row: TableFieldRowProps) => {
|
||||||
const record = substitutes.find((r) => r.pk == row.item.pk);
|
const record = substitutes.find((r) => r.pk == row.item.pk);
|
||||||
return record ? (
|
return record ? (
|
||||||
<BomItemSubstituteRow props={row} record={record} />
|
<BomItemSubstituteRow key={row.rowId} props={row} record={record} />
|
||||||
) : null;
|
) : null;
|
||||||
},
|
},
|
||||||
headers: [
|
headers: [
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { apiUrl } from '@lib/functions/Api';
|
|||||||
import type { ApiFormFieldSet, ApiFormFieldType } from '@lib/types/Forms';
|
import type { ApiFormFieldSet, ApiFormFieldType } from '@lib/types/Forms';
|
||||||
import {
|
import {
|
||||||
TableFieldErrorWrapper,
|
TableFieldErrorWrapper,
|
||||||
|
TableFieldQuantityInput,
|
||||||
type TableFieldRowProps
|
type TableFieldRowProps
|
||||||
} from '../components/forms/fields/TableField';
|
} from '../components/forms/fields/TableField';
|
||||||
import { StatusRenderer } from '../components/render/StatusRenderer';
|
import { StatusRenderer } from '../components/render/StatusRenderer';
|
||||||
@@ -272,15 +273,10 @@ function BuildOutputFormRow({
|
|||||||
|
|
||||||
// Non-serialized output - quantity can be changed
|
// Non-serialized output - quantity can be changed
|
||||||
return (
|
return (
|
||||||
<StandaloneField
|
<TableFieldQuantityInput
|
||||||
fieldName='quantity'
|
value={props.item.quantity ?? ''}
|
||||||
fieldDefinition={{
|
onChange={(value) => {
|
||||||
field_type: 'number',
|
props.changeFn(props.rowId, 'quantity', value);
|
||||||
required: true,
|
|
||||||
value: props.item.quantity,
|
|
||||||
onValueChange: (value: any) => {
|
|
||||||
props.changeFn(props.idx, 'quantity', value);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
error={props.rowErrors?.quantity?.message}
|
error={props.rowErrors?.quantity?.message}
|
||||||
/>
|
/>
|
||||||
@@ -310,7 +306,7 @@ function BuildOutputFormRow({
|
|||||||
/>{' '}
|
/>{' '}
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td style={{ width: '1%', whiteSpace: 'nowrap' }}>
|
<Table.Td style={{ width: '1%', whiteSpace: 'nowrap' }}>
|
||||||
<RemoveRowButton onClick={() => props.removeFn(props.idx)} />
|
<RemoveRowButton onClick={() => props.removeFn(props.rowId)} />
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
</>
|
</>
|
||||||
@@ -344,6 +340,7 @@ export function useCompleteBuildOutputsForm({
|
|||||||
const buildOutputs = useMemo(() => {
|
const buildOutputs = useMemo(() => {
|
||||||
return outputs.map((output: any) => {
|
return outputs.map((output: any) => {
|
||||||
return {
|
return {
|
||||||
|
id: output.pk,
|
||||||
output: output.pk,
|
output: output.pk,
|
||||||
quantity: output.quantity
|
quantity: output.quantity
|
||||||
};
|
};
|
||||||
@@ -358,7 +355,7 @@ export function useCompleteBuildOutputsForm({
|
|||||||
modelRenderer: (row: TableFieldRowProps) => {
|
modelRenderer: (row: TableFieldRowProps) => {
|
||||||
const record = outputs.find((output) => output.pk == row.item.output);
|
const record = outputs.find((output) => output.pk == row.item.output);
|
||||||
return (
|
return (
|
||||||
<BuildOutputFormRow props={row} record={record} key={record.pk} />
|
<BuildOutputFormRow props={row} record={record} key={row.rowId} />
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
headers: [
|
headers: [
|
||||||
@@ -426,6 +423,7 @@ export function useScrapBuildOutputsForm({
|
|||||||
const buildOutputs = useMemo(() => {
|
const buildOutputs = useMemo(() => {
|
||||||
return outputs.map((output: any) => {
|
return outputs.map((output: any) => {
|
||||||
return {
|
return {
|
||||||
|
id: output.pk,
|
||||||
output: output.pk,
|
output: output.pk,
|
||||||
quantity: output.quantity
|
quantity: output.quantity
|
||||||
};
|
};
|
||||||
@@ -440,7 +438,7 @@ export function useScrapBuildOutputsForm({
|
|||||||
modelRenderer: (row: TableFieldRowProps) => {
|
modelRenderer: (row: TableFieldRowProps) => {
|
||||||
const record = outputs.find((output) => output.pk == row.item.output);
|
const record = outputs.find((output) => output.pk == row.item.output);
|
||||||
return (
|
return (
|
||||||
<BuildOutputFormRow props={row} record={record} key={record.pk} />
|
<BuildOutputFormRow props={row} record={record} key={row.rowId} />
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
headers: [
|
headers: [
|
||||||
@@ -497,6 +495,7 @@ export function useCancelBuildOutputsForm({
|
|||||||
const buildOutputs = useMemo(() => {
|
const buildOutputs = useMemo(() => {
|
||||||
return outputs.map((output: any) => {
|
return outputs.map((output: any) => {
|
||||||
return {
|
return {
|
||||||
|
id: output.pk,
|
||||||
output: output.pk
|
output: output.pk
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -562,6 +561,8 @@ function BuildAllocateLineRow({
|
|||||||
record: any;
|
record: any;
|
||||||
sourceLocation: number | undefined;
|
sourceLocation: number | undefined;
|
||||||
}>) {
|
}>) {
|
||||||
|
const [quantity, setQuantity] = useState<number>(props.item?.quantity ?? 0);
|
||||||
|
|
||||||
const stockField: ApiFormFieldType = useMemo(() => {
|
const stockField: ApiFormFieldType = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
field_type: 'related field',
|
field_type: 'related field',
|
||||||
@@ -582,7 +583,7 @@ function BuildAllocateLineRow({
|
|||||||
value: props.item.stock_item,
|
value: props.item.stock_item,
|
||||||
name: 'stock_item',
|
name: 'stock_item',
|
||||||
onValueChange: (value: any, instance: any) => {
|
onValueChange: (value: any, instance: any) => {
|
||||||
props.changeFn(props.idx, 'stock_item', value);
|
props.changeFn(props.rowId, 'stock_item', value);
|
||||||
|
|
||||||
// Update the allocated quantity based on the selected stock item
|
// Update the allocated quantity based on the selected stock item
|
||||||
if (instance) {
|
if (instance) {
|
||||||
@@ -590,7 +591,7 @@ function BuildAllocateLineRow({
|
|||||||
|
|
||||||
if (available < props.item.quantity) {
|
if (available < props.item.quantity) {
|
||||||
props.changeFn(
|
props.changeFn(
|
||||||
props.idx,
|
props.rowId,
|
||||||
'quantity',
|
'quantity',
|
||||||
Math.min(props.item.quantity, available)
|
Math.min(props.item.quantity, available)
|
||||||
);
|
);
|
||||||
@@ -600,18 +601,6 @@ function BuildAllocateLineRow({
|
|||||||
};
|
};
|
||||||
}, [record, props]);
|
}, [record, props]);
|
||||||
|
|
||||||
const quantityField: ApiFormFieldType = useMemo(() => {
|
|
||||||
return {
|
|
||||||
field_type: 'number',
|
|
||||||
name: 'quantity',
|
|
||||||
required: true,
|
|
||||||
value: props.item.quantity,
|
|
||||||
onValueChange: (value: any) => {
|
|
||||||
props.changeFn(props.idx, 'quantity', value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [props]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table.Tr key={`table-row-${record.pk}`}>
|
<Table.Tr key={`table-row-${record.pk}`}>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
@@ -636,14 +625,18 @@ function BuildAllocateLineRow({
|
|||||||
/>
|
/>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<StandaloneField
|
<TableFieldQuantityInput
|
||||||
fieldName='quantity'
|
min={0}
|
||||||
fieldDefinition={quantityField}
|
value={quantity}
|
||||||
|
onChange={(value) => {
|
||||||
|
setQuantity(value === '' ? 0 : value);
|
||||||
|
props.changeFn(props.rowId, 'quantity', value);
|
||||||
|
}}
|
||||||
error={props.rowErrors?.quantity?.message}
|
error={props.rowErrors?.quantity?.message}
|
||||||
/>
|
/>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<RemoveRowButton onClick={() => props.removeFn(props.idx)} />
|
<RemoveRowButton onClick={() => props.removeFn(props.rowId)} />
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
);
|
);
|
||||||
@@ -671,6 +664,16 @@ export function useAllocateStockToBuildForm({
|
|||||||
undefined
|
undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Memoize the line items once to avoid re-rendering
|
||||||
|
const buildLines = useMemo(() => {
|
||||||
|
return lineItems.map((item) => {
|
||||||
|
return {
|
||||||
|
id: item.pk,
|
||||||
|
...item
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [lineItems]);
|
||||||
|
|
||||||
const buildAllocateFields: ApiFormFieldSet = useMemo(() => {
|
const buildAllocateFields: ApiFormFieldSet = useMemo(() => {
|
||||||
const fields: ApiFormFieldSet = {
|
const fields: ApiFormFieldSet = {
|
||||||
items: {
|
items: {
|
||||||
@@ -687,10 +690,10 @@ export function useAllocateStockToBuildForm({
|
|||||||
modelRenderer: (row: TableFieldRowProps) => {
|
modelRenderer: (row: TableFieldRowProps) => {
|
||||||
// Find the matching record from the passed 'lineItems'
|
// Find the matching record from the passed 'lineItems'
|
||||||
const record =
|
const record =
|
||||||
lineItems.find((item) => item.pk == row.item.build_line) ?? {};
|
buildLines.find((item) => item.pk == row.item.build_line) ?? {};
|
||||||
return (
|
return (
|
||||||
<BuildAllocateLineRow
|
<BuildAllocateLineRow
|
||||||
key={row.idx}
|
key={row.rowId}
|
||||||
output={output}
|
output={output}
|
||||||
props={row}
|
props={row}
|
||||||
record={record}
|
record={record}
|
||||||
@@ -767,6 +770,7 @@ export function useAllocateStockToBuildForm({
|
|||||||
})
|
})
|
||||||
.map((item) => {
|
.map((item) => {
|
||||||
return {
|
return {
|
||||||
|
id: item.pk,
|
||||||
build_line: item.pk,
|
build_line: item.pk,
|
||||||
stock_item: undefined,
|
stock_item: undefined,
|
||||||
quantity: Math.max(
|
quantity: Math.max(
|
||||||
@@ -788,6 +792,8 @@ function BuildConsumeItemRow({
|
|||||||
props: TableFieldRowProps;
|
props: TableFieldRowProps;
|
||||||
record: any;
|
record: any;
|
||||||
}) {
|
}) {
|
||||||
|
const [quantity, setQuantity] = useState<number>(props.item?.quantity ?? 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table.Tr key={`table-row-${record.pk}`}>
|
<Table.Tr key={`table-row-${record.pk}`}>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
@@ -803,21 +809,18 @@ function BuildConsumeItemRow({
|
|||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td>{record.quantity}</Table.Td>
|
<Table.Td>{record.quantity}</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<StandaloneField
|
<TableFieldQuantityInput
|
||||||
fieldName='quantity'
|
min={0}
|
||||||
fieldDefinition={{
|
value={quantity}
|
||||||
field_type: 'number',
|
onChange={(value) => {
|
||||||
required: true,
|
setQuantity(value === '' ? 0 : value);
|
||||||
value: props.item.quantity,
|
props.changeFn(props.rowId, 'quantity', value);
|
||||||
onValueChange: (value: any) => {
|
|
||||||
props.changeFn(props.idx, 'quantity', value);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
error={props.rowErrors?.quantity?.message}
|
error={props.rowErrors?.quantity?.message}
|
||||||
/>
|
/>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<RemoveRowButton onClick={() => props.removeFn(props.idx)} />
|
<RemoveRowButton onClick={() => props.removeFn(props.rowId)} />
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
);
|
);
|
||||||
@@ -853,7 +856,7 @@ export function useConsumeBuildItemsForm({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BuildConsumeItemRow key={row.idx} props={row} record={record} />
|
<BuildConsumeItemRow key={row.rowId} props={row} record={record} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -872,6 +875,7 @@ export function useConsumeBuildItemsForm({
|
|||||||
initialData: {
|
initialData: {
|
||||||
items: allocatedItems.map((item) => {
|
items: allocatedItems.map((item) => {
|
||||||
return {
|
return {
|
||||||
|
id: item.pk,
|
||||||
build_item: item.pk,
|
build_item: item.pk,
|
||||||
quantity: item.quantity
|
quantity: item.quantity
|
||||||
};
|
};
|
||||||
@@ -916,7 +920,7 @@ function BuildConsumeLineRow({
|
|||||||
/>
|
/>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<RemoveRowButton onClick={() => props.removeFn(props.idx)} />
|
<RemoveRowButton onClick={() => props.removeFn(props.rowId)} />
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
);
|
);
|
||||||
@@ -954,7 +958,7 @@ export function useConsumeBuildLinesForm({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BuildConsumeLineRow key={row.idx} props={row} record={record} />
|
<BuildConsumeLineRow key={row.rowId} props={row} record={record} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -969,9 +973,11 @@ export function useConsumeBuildLinesForm({
|
|||||||
successMessage: null,
|
successMessage: null,
|
||||||
onFormSuccess: onFormSuccess,
|
onFormSuccess: onFormSuccess,
|
||||||
fields: consumeFields,
|
fields: consumeFields,
|
||||||
|
size: '80%',
|
||||||
initialData: {
|
initialData: {
|
||||||
lines: filteredLines.map((item) => {
|
lines: filteredLines.map((item) => {
|
||||||
return {
|
return {
|
||||||
|
id: item.pk,
|
||||||
build_line: item.pk
|
build_line: item.pk
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ import type {
|
|||||||
} from '@lib/types/Forms';
|
} from '@lib/types/Forms';
|
||||||
import {
|
import {
|
||||||
TableFieldExtraRow,
|
TableFieldExtraRow,
|
||||||
|
TableFieldQuantityInput,
|
||||||
type TableFieldRowProps
|
type TableFieldRowProps
|
||||||
} from '../components/forms/fields/TableField';
|
} from '../components/forms/fields/TableField';
|
||||||
import { Thumbnail } from '../components/images/Thumbnail';
|
import { Thumbnail } from '../components/images/Thumbnail';
|
||||||
@@ -348,11 +349,11 @@ function LineItemFormRow({
|
|||||||
}>) {
|
}>) {
|
||||||
// Barcode Modal state
|
// Barcode Modal state
|
||||||
const [opened, { open, close }] = useDisclosure(false, {
|
const [opened, { open, close }] = useDisclosure(false, {
|
||||||
onClose: () => props.changeFn(props.idx, 'barcode', undefined)
|
onClose: () => props.changeFn(props.rowId, 'barcode', undefined)
|
||||||
});
|
});
|
||||||
|
|
||||||
const [locationOpen, locationHandlers] = useDisclosure(false, {
|
const [locationOpen, locationHandlers] = useDisclosure(false, {
|
||||||
onClose: () => props.changeFn(props.idx, 'location', undefined)
|
onClose: () => props.changeFn(props.rowId, 'location', undefined)
|
||||||
});
|
});
|
||||||
|
|
||||||
// Is this a trackable part?
|
// Is this a trackable part?
|
||||||
@@ -365,7 +366,7 @@ function LineItemFormRow({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!!record.destination) {
|
if (!!record.destination) {
|
||||||
props.changeFn(props.idx, 'location', record.destination);
|
props.changeFn(props.rowId, 'location', record.destination);
|
||||||
locationHandlers.open();
|
locationHandlers.open();
|
||||||
}
|
}
|
||||||
}, [record.destination]);
|
}, [record.destination]);
|
||||||
@@ -375,7 +376,7 @@ function LineItemFormRow({
|
|||||||
isEnabled: () => batchOpen,
|
isEnabled: () => batchOpen,
|
||||||
onGenerate: (value: any) => {
|
onGenerate: (value: any) => {
|
||||||
if (value) {
|
if (value) {
|
||||||
props.changeFn(props.idx, 'batch_code', value);
|
props.changeFn(props.rowId, 'batch_code', value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -387,19 +388,19 @@ function LineItemFormRow({
|
|||||||
|
|
||||||
const [packagingOpen, packagingHandlers] = useDisclosure(false, {
|
const [packagingOpen, packagingHandlers] = useDisclosure(false, {
|
||||||
onClose: () => {
|
onClose: () => {
|
||||||
props.changeFn(props.idx, 'packaging', undefined);
|
props.changeFn(props.rowId, 'packaging', undefined);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const [noteOpen, noteHandlers] = useDisclosure(false, {
|
const [noteOpen, noteHandlers] = useDisclosure(false, {
|
||||||
onClose: () => {
|
onClose: () => {
|
||||||
props.changeFn(props.idx, 'note', undefined);
|
props.changeFn(props.rowId, 'note', undefined);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const [batchOpen, batchHandlers] = useDisclosure(false, {
|
const [batchOpen, batchHandlers] = useDisclosure(false, {
|
||||||
onClose: () => {
|
onClose: () => {
|
||||||
props.changeFn(props.idx, 'batch_code', undefined);
|
props.changeFn(props.rowId, 'batch_code', undefined);
|
||||||
},
|
},
|
||||||
onOpen: () => {
|
onOpen: () => {
|
||||||
// Generate a new batch code
|
// Generate a new batch code
|
||||||
@@ -412,7 +413,7 @@ function LineItemFormRow({
|
|||||||
|
|
||||||
const [serialOpen, serialHandlers] = useDisclosure(false, {
|
const [serialOpen, serialHandlers] = useDisclosure(false, {
|
||||||
onClose: () => {
|
onClose: () => {
|
||||||
props.changeFn(props.idx, 'serial_numbers', undefined);
|
props.changeFn(props.rowId, 'serial_numbers', undefined);
|
||||||
},
|
},
|
||||||
onOpen: () => {
|
onOpen: () => {
|
||||||
// Generate new serial numbers
|
// Generate new serial numbers
|
||||||
@@ -422,7 +423,7 @@ function LineItemFormRow({
|
|||||||
quantity: props.item.quantity
|
quantity: props.item.quantity
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
props.changeFn(props.idx, 'serial_numbers', undefined);
|
props.changeFn(props.rowId, 'serial_numbers', undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -433,20 +434,20 @@ function LineItemFormRow({
|
|||||||
const defaultExpiry = record.part_detail?.default_expiry;
|
const defaultExpiry = record.part_detail?.default_expiry;
|
||||||
if (defaultExpiry !== undefined && defaultExpiry > 0) {
|
if (defaultExpiry !== undefined && defaultExpiry > 0) {
|
||||||
props.changeFn(
|
props.changeFn(
|
||||||
props.idx,
|
props.rowId,
|
||||||
'expiry_date',
|
'expiry_date',
|
||||||
dayjs().add(defaultExpiry, 'day').format('YYYY-MM-DD')
|
dayjs().add(defaultExpiry, 'day').format('YYYY-MM-DD')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onClose: () => {
|
onClose: () => {
|
||||||
props.changeFn(props.idx, 'expiry_date', undefined);
|
props.changeFn(props.rowId, 'expiry_date', undefined);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Status value
|
// Status value
|
||||||
const [statusOpen, statusHandlers] = useDisclosure(false, {
|
const [statusOpen, statusHandlers] = useDisclosure(false, {
|
||||||
onClose: () => props.changeFn(props.idx, 'status', undefined)
|
onClose: () => props.changeFn(props.rowId, 'status', undefined)
|
||||||
});
|
});
|
||||||
|
|
||||||
// Barcode value
|
// Barcode value
|
||||||
@@ -455,7 +456,7 @@ function LineItemFormRow({
|
|||||||
|
|
||||||
// Change form value when state is altered
|
// Change form value when state is altered
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
props.changeFn(props.idx, 'barcode', barcode);
|
props.changeFn(props.rowId, 'barcode', barcode);
|
||||||
}, [barcode]);
|
}, [barcode]);
|
||||||
|
|
||||||
// Update location field description on state change
|
// Update location field description on state change
|
||||||
@@ -574,15 +575,14 @@ function LineItemFormRow({
|
|||||||
/>
|
/>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td style={{ whiteSpace: 'nowrap' }}>
|
<Table.Td style={{ whiteSpace: 'nowrap' }}>
|
||||||
<StandaloneField
|
<TableFieldQuantityInput
|
||||||
fieldName='quantity'
|
min={0}
|
||||||
fieldDefinition={{
|
value={props.item.quantity ?? ''}
|
||||||
field_type: 'number',
|
onChange={(value) => {
|
||||||
value: props.item.quantity,
|
props.changeFn(props.rowId, 'quantity', value);
|
||||||
onValueChange: (value) => {
|
serialNumberGenerator.update({
|
||||||
props.changeFn(props.idx, 'quantity', value);
|
quantity: value === '' ? undefined : value
|
||||||
serialNumberGenerator.update({ quantity: value });
|
});
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
error={props.rowErrors?.quantity?.message}
|
error={props.rowErrors?.quantity?.message}
|
||||||
/>
|
/>
|
||||||
@@ -678,7 +678,7 @@ function LineItemFormRow({
|
|||||||
</Flex>
|
</Flex>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<RemoveRowButton onClick={() => props.removeFn(props.idx)} />
|
<RemoveRowButton onClick={() => props.removeFn(props.rowId)} />
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
{locationOpen && (
|
{locationOpen && (
|
||||||
@@ -697,7 +697,7 @@ function LineItemFormRow({
|
|||||||
structural: false
|
structural: false
|
||||||
},
|
},
|
||||||
onValueChange: (value) => {
|
onValueChange: (value) => {
|
||||||
props.changeFn(props.idx, 'location', value);
|
props.changeFn(props.rowId, 'location', value);
|
||||||
},
|
},
|
||||||
description: locationDescription,
|
description: locationDescription,
|
||||||
value: props.item.location,
|
value: props.item.location,
|
||||||
@@ -719,7 +719,7 @@ function LineItemFormRow({
|
|||||||
tooltip={t`Store at default location`}
|
tooltip={t`Store at default location`}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
props.changeFn(
|
props.changeFn(
|
||||||
props.idx,
|
props.rowId,
|
||||||
'location',
|
'location',
|
||||||
record.part_detail?.default_location ??
|
record.part_detail?.default_location ??
|
||||||
record.part_detail?.category_default_location
|
record.part_detail?.category_default_location
|
||||||
@@ -733,7 +733,11 @@ function LineItemFormRow({
|
|||||||
icon={<InvenTreeIcon icon='destination' />}
|
icon={<InvenTreeIcon icon='destination' />}
|
||||||
tooltip={t`Store at line item destination `}
|
tooltip={t`Store at line item destination `}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
props.changeFn(props.idx, 'location', record.destination)
|
props.changeFn(
|
||||||
|
props.rowId,
|
||||||
|
'location',
|
||||||
|
record.destination
|
||||||
|
)
|
||||||
}
|
}
|
||||||
tooltipAlignment='top'
|
tooltipAlignment='top'
|
||||||
/>
|
/>
|
||||||
@@ -746,7 +750,7 @@ function LineItemFormRow({
|
|||||||
tooltip={t`Store with already received stock`}
|
tooltip={t`Store with already received stock`}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
props.changeFn(
|
props.changeFn(
|
||||||
props.idx,
|
props.rowId,
|
||||||
'location',
|
'location',
|
||||||
record.destination_detail.pk
|
record.destination_detail.pk
|
||||||
)
|
)
|
||||||
@@ -762,7 +766,7 @@ function LineItemFormRow({
|
|||||||
<TableFieldExtraRow
|
<TableFieldExtraRow
|
||||||
visible={batchOpen}
|
visible={batchOpen}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
props.changeFn(props.idx, 'batch_code', value);
|
props.changeFn(props.rowId, 'batch_code', value);
|
||||||
}}
|
}}
|
||||||
fieldName='batch_code'
|
fieldName='batch_code'
|
||||||
fieldDefinition={{
|
fieldDefinition={{
|
||||||
@@ -776,7 +780,7 @@ function LineItemFormRow({
|
|||||||
<TableFieldExtraRow
|
<TableFieldExtraRow
|
||||||
visible={serialOpen}
|
visible={serialOpen}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
props.changeFn(props.idx, 'serial_numbers', value)
|
props.changeFn(props.rowId, 'serial_numbers', value)
|
||||||
}
|
}
|
||||||
fieldName='serial_numbers'
|
fieldName='serial_numbers'
|
||||||
fieldDefinition={{
|
fieldDefinition={{
|
||||||
@@ -794,7 +798,7 @@ function LineItemFormRow({
|
|||||||
<TableFieldExtraRow
|
<TableFieldExtraRow
|
||||||
visible={expiryDateOpen}
|
visible={expiryDateOpen}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
props.changeFn(props.idx, 'expiry_date', value)
|
props.changeFn(props.rowId, 'expiry_date', value)
|
||||||
}
|
}
|
||||||
fieldName='expiry_date'
|
fieldName='expiry_date'
|
||||||
fieldDefinition={{
|
fieldDefinition={{
|
||||||
@@ -808,7 +812,9 @@ function LineItemFormRow({
|
|||||||
)}
|
)}
|
||||||
<TableFieldExtraRow
|
<TableFieldExtraRow
|
||||||
visible={packagingOpen}
|
visible={packagingOpen}
|
||||||
onValueChange={(value) => props.changeFn(props.idx, 'packaging', value)}
|
onValueChange={(value) =>
|
||||||
|
props.changeFn(props.rowId, 'packaging', value)
|
||||||
|
}
|
||||||
fieldName='packaging'
|
fieldName='packaging'
|
||||||
fieldDefinition={{
|
fieldDefinition={{
|
||||||
field_type: 'string',
|
field_type: 'string',
|
||||||
@@ -821,7 +827,7 @@ function LineItemFormRow({
|
|||||||
visible={statusOpen}
|
visible={statusOpen}
|
||||||
defaultValue={10}
|
defaultValue={10}
|
||||||
fieldName='status'
|
fieldName='status'
|
||||||
onValueChange={(value) => props.changeFn(props.idx, 'status', value)}
|
onValueChange={(value) => props.changeFn(props.rowId, 'status', value)}
|
||||||
fieldDefinition={{
|
fieldDefinition={{
|
||||||
field_type: 'choice',
|
field_type: 'choice',
|
||||||
api_url: apiUrl(ApiEndpoints.stock_status),
|
api_url: apiUrl(ApiEndpoints.stock_status),
|
||||||
@@ -833,7 +839,7 @@ function LineItemFormRow({
|
|||||||
<TableFieldExtraRow
|
<TableFieldExtraRow
|
||||||
visible={noteOpen}
|
visible={noteOpen}
|
||||||
fieldName='note'
|
fieldName='note'
|
||||||
onValueChange={(value) => props.changeFn(props.idx, 'note', value)}
|
onValueChange={(value) => props.changeFn(props.rowId, 'note', value)}
|
||||||
fieldDefinition={{
|
fieldDefinition={{
|
||||||
field_type: 'string',
|
field_type: 'string',
|
||||||
label: t`Note`
|
label: t`Note`
|
||||||
@@ -862,13 +868,20 @@ export function useReceiveLineItems(props: LineItemsForm) {
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const records = Object.fromEntries(
|
const records = useMemo(() => {
|
||||||
props.items.map((item) => [item.pk, item])
|
return Object.fromEntries(props.items.map((item) => [item.pk, item]));
|
||||||
);
|
}, [props.items]);
|
||||||
|
|
||||||
const filteredItems = props.items.filter(
|
const filteredItems = useMemo(() => {
|
||||||
(elem) => elem.quantity !== elem.received
|
return props.items
|
||||||
);
|
.filter((elem) => elem.quantity !== elem.received)
|
||||||
|
.map((elem) => {
|
||||||
|
return {
|
||||||
|
id: elem.pk,
|
||||||
|
...elem
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [props.items]);
|
||||||
|
|
||||||
const fields: ApiFormFieldSet = useMemo(() => {
|
const fields: ApiFormFieldSet = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
@@ -878,8 +891,9 @@ export function useReceiveLineItems(props: LineItemsForm) {
|
|||||||
},
|
},
|
||||||
items: {
|
items: {
|
||||||
field_type: 'table',
|
field_type: 'table',
|
||||||
value: filteredItems.map((elem, idx) => {
|
value: filteredItems.map((elem) => {
|
||||||
return {
|
return {
|
||||||
|
id: elem.pk,
|
||||||
line_item: elem.pk,
|
line_item: elem.pk,
|
||||||
location: elem.destination ?? elem.destination_detail?.pk ?? null,
|
location: elem.destination ?? elem.destination_detail?.pk ?? null,
|
||||||
quantity: elem.quantity - elem.received,
|
quantity: elem.quantity - elem.received,
|
||||||
@@ -902,7 +916,7 @@ export function useReceiveLineItems(props: LineItemsForm) {
|
|||||||
props={row}
|
props={row}
|
||||||
record={record}
|
record={record}
|
||||||
statuses={stockStatusCodes}
|
statuses={stockStatusCodes}
|
||||||
key={record.pk}
|
key={row.rowId}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -921,7 +935,7 @@ export function useReceiveLineItems(props: LineItemsForm) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [filteredItems, props, stockStatusCodes]);
|
}, [filteredItems, records, props, stockStatusCodes]);
|
||||||
|
|
||||||
return useCreateApiFormModal({
|
return useCreateApiFormModal({
|
||||||
...props.formProps,
|
...props.formProps,
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ function ReturnOrderLineItemFormRow({
|
|||||||
label: t`Status`,
|
label: t`Status`,
|
||||||
choices: statusOptions,
|
choices: statusOptions,
|
||||||
onValueChange: (value) => {
|
onValueChange: (value) => {
|
||||||
props.changeFn(props.idx, 'status', value);
|
props.changeFn(props.rowId, 'status', value);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
defaultValue={record.item_detail?.status}
|
defaultValue={record.item_detail?.status}
|
||||||
@@ -207,7 +207,7 @@ function ReturnOrderLineItemFormRow({
|
|||||||
/>
|
/>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<RemoveRowButton onClick={() => props.removeFn(props.idx)} />
|
<RemoveRowButton onClick={() => props.removeFn(props.rowId)} />
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
</>
|
</>
|
||||||
@@ -226,6 +226,7 @@ export function useReceiveReturnOrderLineItems(
|
|||||||
field_type: 'table',
|
field_type: 'table',
|
||||||
value: props.items.map((item: any) => {
|
value: props.items.map((item: any) => {
|
||||||
return {
|
return {
|
||||||
|
id: item.pk,
|
||||||
item: item.pk
|
item: item.pk
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
@@ -236,7 +237,7 @@ export function useReceiveReturnOrderLineItems(
|
|||||||
<ReturnOrderLineItemFormRow
|
<ReturnOrderLineItemFormRow
|
||||||
props={row}
|
props={row}
|
||||||
record={record}
|
record={record}
|
||||||
key={record.pk}
|
key={row.rowId}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -25,7 +25,10 @@ import type {
|
|||||||
ApiFormFieldType
|
ApiFormFieldType
|
||||||
} from '@lib/types/Forms';
|
} from '@lib/types/Forms';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import type { TableFieldRowProps } from '../components/forms/fields/TableField';
|
import {
|
||||||
|
TableFieldQuantityInput,
|
||||||
|
type TableFieldRowProps
|
||||||
|
} from '../components/forms/fields/TableField';
|
||||||
import useBackgroundTask from '../hooks/UseBackgroundTask';
|
import useBackgroundTask from '../hooks/UseBackgroundTask';
|
||||||
import { useCreateApiFormModal, useEditApiFormModal } from '../hooks/UseForm';
|
import { useCreateApiFormModal, useEditApiFormModal } from '../hooks/UseForm';
|
||||||
import { useGlobalSettingsState } from '../states/SettingsStates';
|
import { useGlobalSettingsState } from '../states/SettingsStates';
|
||||||
@@ -328,41 +331,33 @@ function SalesOrderAllocateLineRow({
|
|||||||
value: props.item.stock_item,
|
value: props.item.stock_item,
|
||||||
name: 'stock_item',
|
name: 'stock_item',
|
||||||
onValueChange: (value: any, instance: any) => {
|
onValueChange: (value: any, instance: any) => {
|
||||||
props.changeFn(props.idx, 'stock_item', value);
|
props.changeFn(props.rowId, 'stock_item', value);
|
||||||
|
|
||||||
// Update the allocated quantity based on the selected stock item
|
// Update the allocated quantity based on the selected stock item
|
||||||
if (instance) {
|
if (instance) {
|
||||||
const available = instance.quantity - instance.allocated;
|
const available = instance.quantity - instance.allocated;
|
||||||
const required = record.quantity - record.allocated;
|
const required = record.quantity - record.allocated;
|
||||||
|
|
||||||
let quantity = props.item?.quantity ?? 0;
|
let q = props.item?.quantity ?? 0;
|
||||||
|
|
||||||
quantity = Math.max(quantity, required);
|
q = Math.max(q, required);
|
||||||
quantity = Math.min(quantity, available);
|
q = Math.min(q, available);
|
||||||
|
|
||||||
if (quantity != props.item.quantity) {
|
if (q != props.item?.quantity) {
|
||||||
props.changeFn(props.idx, 'quantity', quantity);
|
setQuantity(q);
|
||||||
|
props.changeFn(props.rowId, 'quantity', q);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [sourceLocation, record, props]);
|
}, [sourceLocation, record, props]);
|
||||||
|
|
||||||
// Statically defined field for selecting the allocation quantity
|
const [quantity, setQuantity] = useState<number | ''>(
|
||||||
const quantityField: ApiFormFieldType = useMemo(() => {
|
props.item?.quantity ?? ''
|
||||||
return {
|
);
|
||||||
field_type: 'number',
|
|
||||||
name: 'quantity',
|
|
||||||
required: true,
|
|
||||||
value: props.item.quantity,
|
|
||||||
onValueChange: (value: any) => {
|
|
||||||
props.changeFn(props.idx, 'quantity', value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [props]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table.Tr key={`table-row-${props.idx}-${record.pk}`}>
|
<Table.Tr key={`table-row-${props.rowId}-${record.pk}`}>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<RenderPartColumn part={record.part_detail} />
|
<RenderPartColumn part={record.part_detail} />
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
@@ -381,14 +376,18 @@ function SalesOrderAllocateLineRow({
|
|||||||
/>
|
/>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<StandaloneField
|
<TableFieldQuantityInput
|
||||||
fieldName='quantity'
|
min={0}
|
||||||
fieldDefinition={quantityField}
|
value={quantity}
|
||||||
|
onChange={(value) => {
|
||||||
|
setQuantity(value);
|
||||||
|
props.changeFn(props.rowId, 'quantity', value);
|
||||||
|
}}
|
||||||
error={props.rowErrors?.quantity?.message}
|
error={props.rowErrors?.quantity?.message}
|
||||||
/>
|
/>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<RemoveRowButton onClick={() => props.removeFn(props.idx)} />
|
<RemoveRowButton onClick={() => props.removeFn(props.rowId)} />
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
);
|
);
|
||||||
@@ -443,7 +442,7 @@ export function useAllocateToSalesOrderForm({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SalesOrderAllocateLineRow
|
<SalesOrderAllocateLineRow
|
||||||
key={`table-row-${row.idx}-${record.pk}`}
|
key={row.rowId}
|
||||||
props={row}
|
props={row}
|
||||||
record={record}
|
record={record}
|
||||||
sourceLocation={sourceLocation}
|
sourceLocation={sourceLocation}
|
||||||
@@ -474,6 +473,7 @@ export function useAllocateToSalesOrderForm({
|
|||||||
initialData: {
|
initialData: {
|
||||||
items: lineItems.map((item) => {
|
items: lineItems.map((item) => {
|
||||||
return {
|
return {
|
||||||
|
id: item.pk,
|
||||||
line_item: item.pk,
|
line_item: item.pk,
|
||||||
quantity: 0,
|
quantity: 0,
|
||||||
stock_item: null
|
stock_item: null
|
||||||
|
|||||||
@@ -41,15 +41,16 @@ import {
|
|||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
|
useRef,
|
||||||
useState
|
useState
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { useFormContext } from 'react-hook-form';
|
import { useFormContext } from 'react-hook-form';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { api } from '../App';
|
import { api } from '../App';
|
||||||
import RemoveRowButton from '../components/buttons/RemoveRowButton';
|
import RemoveRowButton from '../components/buttons/RemoveRowButton';
|
||||||
import { StandaloneField } from '../components/forms/StandaloneField';
|
|
||||||
import {
|
import {
|
||||||
TableFieldExtraRow,
|
TableFieldExtraRow,
|
||||||
|
TableFieldQuantityInput,
|
||||||
type TableFieldRowProps
|
type TableFieldRowProps
|
||||||
} from '../components/forms/fields/TableField';
|
} from '../components/forms/fields/TableField';
|
||||||
import { Thumbnail } from '../components/images/Thumbnail';
|
import { Thumbnail } from '../components/images/Thumbnail';
|
||||||
@@ -553,19 +554,106 @@ function moveToDefault(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
type StockAdjustmentItemWithRecord = {
|
/*
|
||||||
obj: any;
|
* Memoize a list of stock items for use in a stock operations modal.
|
||||||
} & StockAdjustmentItem;
|
* These items may be provided directly, or fetched from the API
|
||||||
|
*
|
||||||
|
* @param opened - Is the underlying modal opened or closed?
|
||||||
|
* @param items - Optional list of stock items to use directly
|
||||||
|
* @param category - Optional category ID to filter stock items
|
||||||
|
* @param location - Optional location ID to filter stock items
|
||||||
|
* @param part - Optional part ID to filter stock items
|
||||||
|
* @param filters - Optional additional filters to apply to the stock item query
|
||||||
|
*/
|
||||||
|
function useStockItems({
|
||||||
|
opened,
|
||||||
|
items,
|
||||||
|
filters
|
||||||
|
}: Readonly<{
|
||||||
|
opened: boolean;
|
||||||
|
items?: any[] | any;
|
||||||
|
filters?: { [key: string]: any };
|
||||||
|
}>) {
|
||||||
|
const query = useQuery({
|
||||||
|
enabled: opened,
|
||||||
|
queryKey: ['stockItems', filters],
|
||||||
|
queryFn: async () => {
|
||||||
|
if (items !== undefined) {
|
||||||
|
return Array.isArray(items) ? items : [items];
|
||||||
|
}
|
||||||
|
|
||||||
type TableFieldRefreshFn = (idx: number) => void;
|
if (!opened) {
|
||||||
type TableFieldChangeFn = (idx: number, key: string, value: any) => void;
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
type StockRow = {
|
// Fetch via the API
|
||||||
item: StockAdjustmentItemWithRecord;
|
const url = apiUrl(ApiEndpoints.stock_item_list);
|
||||||
idx: number;
|
|
||||||
changeFn: TableFieldChangeFn;
|
return api
|
||||||
removeFn: TableFieldRefreshFn;
|
.get(url, {
|
||||||
};
|
params: {
|
||||||
|
...filters,
|
||||||
|
part_detail: true,
|
||||||
|
location_detail: true,
|
||||||
|
cascade: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((response) => response.data ?? []);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
if (!opened) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return query.data ?? [];
|
||||||
|
}, [opened, query.data]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ReturnStockMoveButton({
|
||||||
|
record,
|
||||||
|
quantity,
|
||||||
|
onRemove,
|
||||||
|
returnStock
|
||||||
|
}: {
|
||||||
|
record: any;
|
||||||
|
quantity: StockItemQuantity;
|
||||||
|
onRemove: () => void;
|
||||||
|
returnStock: boolean;
|
||||||
|
}) {
|
||||||
|
const form = useFormContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActionButton
|
||||||
|
onClick={() =>
|
||||||
|
moveToDefault(
|
||||||
|
record,
|
||||||
|
quantity,
|
||||||
|
onRemove,
|
||||||
|
returnStock
|
||||||
|
? {
|
||||||
|
title: t`Confirm Stock Return`,
|
||||||
|
onConfirm: (location: number) => {
|
||||||
|
form.setValue('location', location, {
|
||||||
|
shouldDirty: true,
|
||||||
|
shouldValidate: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
)
|
||||||
|
}
|
||||||
|
icon={<InvenTreeIcon icon='default_location' />}
|
||||||
|
tooltip={t`Move to default location`}
|
||||||
|
tooltipAlignment='top'
|
||||||
|
disabled={
|
||||||
|
!record.part_detail?.default_location &&
|
||||||
|
!record.part_detail?.category_default_location
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function StockOperationsRow({
|
function StockOperationsRow({
|
||||||
props,
|
props,
|
||||||
@@ -588,7 +676,7 @@ function StockOperationsRow({
|
|||||||
returnStock?: boolean;
|
returnStock?: boolean;
|
||||||
record?: any;
|
record?: any;
|
||||||
}) {
|
}) {
|
||||||
const form = useFormContext();
|
const rowId = props.rowId;
|
||||||
|
|
||||||
const statusOptions: ApiFormFieldChoice[] = useMemo(() => {
|
const statusOptions: ApiFormFieldChoice[] = useMemo(() => {
|
||||||
return (
|
return (
|
||||||
@@ -607,37 +695,54 @@ function StockOperationsRow({
|
|||||||
|
|
||||||
const [status, setStatus] = useState<number | undefined>(undefined);
|
const [status, setStatus] = useState<number | undefined>(undefined);
|
||||||
|
|
||||||
const removeAndRefresh = () => {
|
const [packagingOpen, packagingHandlers] = useDisclosure(false);
|
||||||
props.removeFn(props.idx);
|
const [statusOpen, statusHandlers] = useDisclosure(false);
|
||||||
};
|
const hasMountedPackagingRef = useRef(false);
|
||||||
|
const hasMountedStatusRef = useRef(false);
|
||||||
|
|
||||||
const callChangeFn = (idx: number, key: string, value: any) => {
|
useEffect(() => {
|
||||||
setTimeout(() => props.changeFn(idx, key, value), 0);
|
if (!transfer) {
|
||||||
};
|
return;
|
||||||
|
|
||||||
const [packagingOpen, packagingHandlers] = useDisclosure(false, {
|
|
||||||
onOpen: () => {
|
|
||||||
if (transfer) {
|
|
||||||
callChangeFn(props.idx, 'packaging', record?.packaging || undefined);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onClose: () => {
|
|
||||||
if (transfer) {
|
|
||||||
callChangeFn(props.idx, 'packaging', undefined);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
const [statusOpen, statusHandlers] = useDisclosure(false, {
|
if (!hasMountedPackagingRef.current) {
|
||||||
onOpen: () => {
|
hasMountedPackagingRef.current = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
props.changeFn(
|
||||||
|
rowId,
|
||||||
|
'packaging',
|
||||||
|
packagingOpen ? record?.packaging || undefined : undefined
|
||||||
|
);
|
||||||
|
}, [transfer, packagingOpen, rowId, record?.packaging, props.changeFn]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!changeStatus) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasMountedStatusRef.current) {
|
||||||
|
hasMountedStatusRef.current = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusOpen) {
|
||||||
setStatus(record?.status_custom_key || record?.status || undefined);
|
setStatus(record?.status_custom_key || record?.status || undefined);
|
||||||
props.changeFn(props.idx, 'status', record?.status || undefined);
|
props.changeFn(rowId, 'status', record?.status || undefined);
|
||||||
},
|
return;
|
||||||
onClose: () => {
|
|
||||||
setStatus(undefined);
|
|
||||||
callChangeFn(props.idx, 'status', undefined);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
setStatus(undefined);
|
||||||
|
props.changeFn(rowId, 'status', undefined);
|
||||||
|
}, [
|
||||||
|
changeStatus,
|
||||||
|
statusOpen,
|
||||||
|
rowId,
|
||||||
|
record?.status,
|
||||||
|
record?.status_custom_key,
|
||||||
|
props.changeFn
|
||||||
|
]);
|
||||||
|
|
||||||
const stockString: string = useMemo(() => {
|
const stockString: string = useMemo(() => {
|
||||||
if (!record) {
|
if (!record) {
|
||||||
@@ -652,10 +757,15 @@ function StockOperationsRow({
|
|||||||
}, [record]);
|
}, [record]);
|
||||||
|
|
||||||
return !record ? (
|
return !record ? (
|
||||||
<div>{t`Loading...`}</div>
|
<Table.Tr>
|
||||||
|
<Table.Td colSpan={6}>{t`Loading...`}</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Table.Tr>
|
<Table.Tr
|
||||||
|
aria-label={`stock-op-row-${rowId}`}
|
||||||
|
key={`stock-op-row-${rowId}`}
|
||||||
|
>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<Stack gap='xs'>
|
<Stack gap='xs'>
|
||||||
<Flex gap='sm' align='center'>
|
<Flex gap='sm' align='center'>
|
||||||
@@ -689,15 +799,12 @@ function StockOperationsRow({
|
|||||||
</Table.Td>
|
</Table.Td>
|
||||||
{!merge && (
|
{!merge && (
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<StandaloneField
|
<TableFieldQuantityInput
|
||||||
fieldName='quantity'
|
min={0}
|
||||||
fieldDefinition={{
|
value={quantity ?? ''}
|
||||||
field_type: 'number',
|
onChange={(value) => {
|
||||||
value: quantity,
|
setQuantity(value);
|
||||||
onValueChange: (value: any) => {
|
props.changeFn(rowId, 'quantity', value);
|
||||||
setQuantity(value);
|
|
||||||
props.changeFn(props.idx, 'quantity', value);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
error={props.rowErrors?.quantity?.message}
|
error={props.rowErrors?.quantity?.message}
|
||||||
/>
|
/>
|
||||||
@@ -705,35 +812,30 @@ function StockOperationsRow({
|
|||||||
)}
|
)}
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<Flex gap='3px'>
|
<Flex gap='3px'>
|
||||||
{transfer && (
|
{transfer &&
|
||||||
<ActionButton
|
(returnStock ? (
|
||||||
onClick={() =>
|
<ReturnStockMoveButton
|
||||||
moveToDefault(
|
record={record}
|
||||||
record,
|
quantity={props.item.quantity}
|
||||||
props.item.quantity,
|
onRemove={() => props.removeFn(rowId)}
|
||||||
removeAndRefresh,
|
returnStock={returnStock}
|
||||||
returnStock
|
/>
|
||||||
? {
|
) : (
|
||||||
title: t`Confirm Stock Return`,
|
<ActionButton
|
||||||
onConfirm: (location: number) => {
|
onClick={() =>
|
||||||
form.setValue('location', location, {
|
moveToDefault(record, props.item.quantity, () =>
|
||||||
shouldDirty: true,
|
props.removeFn(rowId)
|
||||||
shouldValidate: true
|
)
|
||||||
});
|
}
|
||||||
}
|
icon={<InvenTreeIcon icon='default_location' />}
|
||||||
}
|
tooltip={t`Move to default location`}
|
||||||
: undefined
|
tooltipAlignment='top'
|
||||||
)
|
disabled={
|
||||||
}
|
!record.part_detail?.default_location &&
|
||||||
icon={<InvenTreeIcon icon='default_location' />}
|
!record.part_detail?.category_default_location
|
||||||
tooltip={t`Move to default location`}
|
}
|
||||||
tooltipAlignment='top'
|
/>
|
||||||
disabled={
|
))}
|
||||||
!record.part_detail?.default_location &&
|
|
||||||
!record.part_detail?.category_default_location
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{changeStatus && (
|
{changeStatus && (
|
||||||
<ActionButton
|
<ActionButton
|
||||||
size='sm'
|
size='sm'
|
||||||
@@ -758,12 +860,12 @@ function StockOperationsRow({
|
|||||||
icon={<InvenTreeIcon icon='merge' />}
|
icon={<InvenTreeIcon icon='merge' />}
|
||||||
tooltip={t`Merge into existing stock`}
|
tooltip={t`Merge into existing stock`}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
callChangeFn(props.idx, 'merge', !props.item?.merge)
|
props.changeFn(rowId, 'merge', !props.item?.merge)
|
||||||
}
|
}
|
||||||
variant={props.item?.merge ? 'filled' : 'transparent'}
|
variant={props.item?.merge ? 'filled' : 'transparent'}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<RemoveRowButton onClick={() => props.removeFn(props.idx)} />
|
<RemoveRowButton onClick={() => props.removeFn(rowId)} />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
@@ -772,7 +874,7 @@ function StockOperationsRow({
|
|||||||
visible={statusOpen}
|
visible={statusOpen}
|
||||||
onValueChange={(value: any) => {
|
onValueChange={(value: any) => {
|
||||||
setStatus(value);
|
setStatus(value);
|
||||||
props.changeFn(props.idx, 'status', value || undefined);
|
props.changeFn(rowId, 'status', value || undefined);
|
||||||
}}
|
}}
|
||||||
fieldName='status'
|
fieldName='status'
|
||||||
fieldDefinition={{
|
fieldDefinition={{
|
||||||
@@ -788,7 +890,7 @@ function StockOperationsRow({
|
|||||||
<TableFieldExtraRow
|
<TableFieldExtraRow
|
||||||
visible={transfer && packagingOpen}
|
visible={transfer && packagingOpen}
|
||||||
onValueChange={(value: any) => {
|
onValueChange={(value: any) => {
|
||||||
props.changeFn(props.idx, 'packaging', value || undefined);
|
props.changeFn(rowId, 'packaging', value || undefined);
|
||||||
}}
|
}}
|
||||||
fieldName='packaging'
|
fieldName='packaging'
|
||||||
fieldDefinition={{
|
fieldDefinition={{
|
||||||
@@ -814,15 +916,15 @@ type StockAdjustmentItem = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function mapAdjustmentItems(items: any[], mergeDefault?: boolean) {
|
function mapAdjustmentItems(items: any[], mergeDefault?: boolean) {
|
||||||
const mappedItems: StockAdjustmentItemWithRecord[] = items.map((elem) => {
|
const mappedItems: StockAdjustmentItem[] = items.map((elem) => {
|
||||||
return {
|
return {
|
||||||
|
id: elem.pk,
|
||||||
pk: elem.pk,
|
pk: elem.pk,
|
||||||
quantity: elem.quantity,
|
quantity: elem.quantity,
|
||||||
batch: elem.batch || undefined,
|
batch: elem.batch || undefined,
|
||||||
status: elem.status || undefined,
|
status: elem.status || undefined,
|
||||||
packaging: elem.packaging || undefined,
|
packaging: elem.packaging || undefined,
|
||||||
merge: elem.merge ?? mergeDefault ?? false,
|
merge: elem.merge ?? mergeDefault ?? false
|
||||||
obj: elem
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -856,7 +958,7 @@ function stockTransferFields(
|
|||||||
changeStatus
|
changeStatus
|
||||||
setMax
|
setMax
|
||||||
transferMerge
|
transferMerge
|
||||||
key={record.pk}
|
key={row.rowId}
|
||||||
record={record}
|
record={record}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -901,7 +1003,7 @@ function stockReturnFields(items: any[]): ApiFormFieldSet {
|
|||||||
return (
|
return (
|
||||||
<StockOperationsRow
|
<StockOperationsRow
|
||||||
props={row}
|
props={row}
|
||||||
key={record.pk}
|
key={row.rowId}
|
||||||
record={record}
|
record={record}
|
||||||
transfer
|
transfer
|
||||||
returnStock
|
returnStock
|
||||||
@@ -948,9 +1050,12 @@ function stockRemoveFields(items: any[]): ApiFormFieldSet {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const records = Object.fromEntries(items.map((item) => [item.pk, item]));
|
// Only include items which are not serialized (serial number field is empty)
|
||||||
|
const validItems = items.filter((item) => !item.serial && item.quantity > 0);
|
||||||
|
|
||||||
const initialValue = mapAdjustmentItems(items).map((elem) => {
|
const records = Object.fromEntries(validItems.map((item) => [item.pk, item]));
|
||||||
|
|
||||||
|
const initialValue = mapAdjustmentItems(validItems).map((elem) => {
|
||||||
return {
|
return {
|
||||||
...elem,
|
...elem,
|
||||||
quantity: 0
|
quantity: 0
|
||||||
@@ -970,7 +1075,7 @@ function stockRemoveFields(items: any[]): ApiFormFieldSet {
|
|||||||
setMax
|
setMax
|
||||||
changeStatus
|
changeStatus
|
||||||
add
|
add
|
||||||
key={record.pk}
|
key={row.rowId}
|
||||||
record={record}
|
record={record}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -995,9 +1100,12 @@ function stockAddFields(items: any[]): ApiFormFieldSet {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const records = Object.fromEntries(items.map((item) => [item.pk, item]));
|
// Only include items which are not serialized (serial number field is empty)
|
||||||
|
const validItems = items.filter((item) => !item.serial);
|
||||||
|
|
||||||
const initialValue = mapAdjustmentItems(items).map((elem) => {
|
const records = Object.fromEntries(validItems.map((item) => [item.pk, item]));
|
||||||
|
|
||||||
|
const initialValue = mapAdjustmentItems(validItems).map((elem) => {
|
||||||
return {
|
return {
|
||||||
...elem,
|
...elem,
|
||||||
quantity: 0
|
quantity: 0
|
||||||
@@ -1016,7 +1124,7 @@ function stockAddFields(items: any[]): ApiFormFieldSet {
|
|||||||
changeStatus
|
changeStatus
|
||||||
props={row}
|
props={row}
|
||||||
add
|
add
|
||||||
key={record.pk}
|
key={row.rowId}
|
||||||
record={record}
|
record={record}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -1037,16 +1145,14 @@ function stockAddFields(items: any[]): ApiFormFieldSet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function stockCountFields(items: any[]): ApiFormFieldSet {
|
function stockCountFields(items: any[]): ApiFormFieldSet {
|
||||||
if (!items) {
|
const records = Object.fromEntries(
|
||||||
return {};
|
items?.map((item) => [item.pk, item]) ?? []
|
||||||
}
|
);
|
||||||
|
|
||||||
const records = Object.fromEntries(items.map((item) => [item.pk, item]));
|
const initialValue = items ? mapAdjustmentItems(items) : [];
|
||||||
|
|
||||||
const initialValue = mapAdjustmentItems(items);
|
|
||||||
|
|
||||||
// Extract all location values from the items
|
// Extract all location values from the items
|
||||||
const locations = [...new Set(items.map((item) => item.location))];
|
const locations = [...new Set(items?.map((item) => item.location))];
|
||||||
|
|
||||||
const fields: ApiFormFieldSet = {
|
const fields: ApiFormFieldSet = {
|
||||||
items: {
|
items: {
|
||||||
@@ -1057,8 +1163,8 @@ function stockCountFields(items: any[]): ApiFormFieldSet {
|
|||||||
<StockOperationsRow
|
<StockOperationsRow
|
||||||
props={row}
|
props={row}
|
||||||
changeStatus
|
changeStatus
|
||||||
key={row.item.pk}
|
key={row.rowId}
|
||||||
record={records[row.item.pk]}
|
record={records[row.item?.pk]}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -1105,7 +1211,7 @@ function stockChangeStatusFields(items: any[]): ApiFormFieldSet {
|
|||||||
return (
|
return (
|
||||||
<StockOperationsRow
|
<StockOperationsRow
|
||||||
props={row}
|
props={row}
|
||||||
key={row.item}
|
key={row.rowId}
|
||||||
merge
|
merge
|
||||||
record={records[row.item]}
|
record={records[row.item]}
|
||||||
/>
|
/>
|
||||||
@@ -1133,19 +1239,22 @@ function stockMergeFields(items: any[]): ApiFormFieldSet {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const records = Object.fromEntries(items.map((item) => [item.pk, item]));
|
// Only include items which are not serialized (serial number field is empty)
|
||||||
|
const validItems = items.filter((item) => !item.serial);
|
||||||
|
|
||||||
|
const records = Object.fromEntries(validItems.map((item) => [item.pk, item]));
|
||||||
|
|
||||||
// Extract all non-null location values from the items
|
// Extract all non-null location values from the items
|
||||||
const locationValues = [
|
const locationValues = [
|
||||||
...new Set(
|
...new Set(
|
||||||
items.filter((item) => item.location).map((item) => item.location)
|
validItems.filter((item) => item.location).map((item) => item.location)
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
|
|
||||||
// Extract all non-null default location values from the items
|
// Extract all non-null default location values from the items
|
||||||
const defaultLocationValues = [
|
const defaultLocationValues = [
|
||||||
...new Set(
|
...new Set(
|
||||||
items
|
validItems
|
||||||
.filter((item) => item.part_detail?.default_location)
|
.filter((item) => item.part_detail?.default_location)
|
||||||
.map((item) => item.part_detail?.default_location)
|
.map((item) => item.part_detail?.default_location)
|
||||||
)
|
)
|
||||||
@@ -1162,17 +1271,16 @@ function stockMergeFields(items: any[]): ApiFormFieldSet {
|
|||||||
const fields: ApiFormFieldSet = {
|
const fields: ApiFormFieldSet = {
|
||||||
items: {
|
items: {
|
||||||
field_type: 'table',
|
field_type: 'table',
|
||||||
value: items.map((elem) => {
|
value: validItems.map((elem) => {
|
||||||
return {
|
return {
|
||||||
item: elem.pk,
|
item: elem.pk
|
||||||
obj: elem
|
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
modelRenderer: (row: TableFieldRowProps) => {
|
modelRenderer: (row: TableFieldRowProps) => {
|
||||||
return (
|
return (
|
||||||
<StockOperationsRow
|
<StockOperationsRow
|
||||||
props={row}
|
props={row}
|
||||||
key={row.item.item}
|
key={row.rowId}
|
||||||
merge
|
merge
|
||||||
changeStatus
|
changeStatus
|
||||||
record={records[row.item.item]}
|
record={records[row.item.item]}
|
||||||
@@ -1213,15 +1321,14 @@ function stockAssignFields(items: any[]): ApiFormFieldSet {
|
|||||||
field_type: 'table',
|
field_type: 'table',
|
||||||
value: items.map((elem) => {
|
value: items.map((elem) => {
|
||||||
return {
|
return {
|
||||||
item: elem.pk,
|
item: elem.pk
|
||||||
obj: elem
|
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
modelRenderer: (row: TableFieldRowProps) => {
|
modelRenderer: (row: TableFieldRowProps) => {
|
||||||
return (
|
return (
|
||||||
<StockOperationsRow
|
<StockOperationsRow
|
||||||
props={row}
|
props={row}
|
||||||
key={row.item.item}
|
key={row.rowId}
|
||||||
merge
|
merge
|
||||||
record={records[row.item.item]}
|
record={records[row.item.item]}
|
||||||
/>
|
/>
|
||||||
@@ -1265,7 +1372,7 @@ function stockDeleteFields(items: any[]): ApiFormFieldSet {
|
|||||||
return (
|
return (
|
||||||
<StockOperationsRow
|
<StockOperationsRow
|
||||||
props={row}
|
props={row}
|
||||||
key={record.pk}
|
key={row.rowId}
|
||||||
merge
|
merge
|
||||||
record={record}
|
record={record}
|
||||||
/>
|
/>
|
||||||
@@ -1293,8 +1400,6 @@ type apiModalFunc = (props: ApiFormModalProps) => {
|
|||||||
|
|
||||||
function useStockOperationModal({
|
function useStockOperationModal({
|
||||||
items,
|
items,
|
||||||
pk,
|
|
||||||
model,
|
|
||||||
refresh,
|
refresh,
|
||||||
fieldGenerator,
|
fieldGenerator,
|
||||||
endpoint,
|
endpoint,
|
||||||
@@ -1305,9 +1410,7 @@ function useStockOperationModal({
|
|||||||
modalFunc = useCreateApiFormModal
|
modalFunc = useCreateApiFormModal
|
||||||
}: {
|
}: {
|
||||||
items?: object;
|
items?: object;
|
||||||
pk?: number;
|
|
||||||
filters?: any;
|
filters?: any;
|
||||||
model: ModelType | string;
|
|
||||||
refresh: () => void;
|
refresh: () => void;
|
||||||
fieldGenerator: (items: any[]) => ApiFormFieldSet;
|
fieldGenerator: (items: any[]) => ApiFormFieldSet;
|
||||||
endpoint: ApiEndpoints;
|
endpoint: ApiEndpoints;
|
||||||
@@ -1316,51 +1419,19 @@ function useStockOperationModal({
|
|||||||
successMessage?: string;
|
successMessage?: string;
|
||||||
modalFunc?: apiModalFunc;
|
modalFunc?: apiModalFunc;
|
||||||
}) {
|
}) {
|
||||||
const baseParams: any = {
|
|
||||||
part_detail: true,
|
|
||||||
location_detail: true,
|
|
||||||
cascade: false
|
|
||||||
};
|
|
||||||
|
|
||||||
const params = useMemo(() => {
|
|
||||||
const query_params: any = {
|
|
||||||
...baseParams,
|
|
||||||
...(filters ?? {})
|
|
||||||
};
|
|
||||||
|
|
||||||
query_params[model] =
|
|
||||||
pk === undefined && model === 'location' ? 'null' : pk;
|
|
||||||
|
|
||||||
return query_params;
|
|
||||||
}, [baseParams, filters, model, pk]);
|
|
||||||
|
|
||||||
const [opened, setOpened] = useState<boolean>(false);
|
const [opened, setOpened] = useState<boolean>(false);
|
||||||
|
|
||||||
const { data } = useQuery({
|
const stockItems = useStockItems({
|
||||||
queryKey: ['stockitems', opened, model, pk, items, params],
|
opened: opened,
|
||||||
queryFn: async () => {
|
items: items,
|
||||||
if (items) {
|
filters: filters
|
||||||
// If a list of items is provided, use that directly
|
|
||||||
return Array.isArray(items) ? items : [items];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!pk || !opened) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = apiUrl(ApiEndpoints.stock_item_list);
|
|
||||||
|
|
||||||
return api
|
|
||||||
.get(url, {
|
|
||||||
params: params
|
|
||||||
})
|
|
||||||
.then((response) => response.data ?? []);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const fields = useMemo(() => {
|
// Rebuild the "fields" object
|
||||||
return fieldGenerator(data);
|
const fields = useMemo(
|
||||||
}, [data]);
|
() => fieldGenerator(stockItems),
|
||||||
|
[fieldGenerator, stockItems]
|
||||||
|
);
|
||||||
|
|
||||||
return modalFunc({
|
return modalFunc({
|
||||||
url: endpoint,
|
url: endpoint,
|
||||||
@@ -1447,9 +1518,14 @@ export function useReturnStockItem(props: StockOperationProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useCountStockItem(props: StockOperationProps) {
|
export function useCountStockItem(props: StockOperationProps) {
|
||||||
|
const fieldGenerator = useCallback(
|
||||||
|
(items: any[]) => stockCountFields(items),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
return useStockOperationModal({
|
return useStockOperationModal({
|
||||||
...props,
|
...props,
|
||||||
fieldGenerator: stockCountFields,
|
fieldGenerator: fieldGenerator,
|
||||||
endpoint: ApiEndpoints.stock_count,
|
endpoint: ApiEndpoints.stock_count,
|
||||||
title: t`Count Stock`,
|
title: t`Count Stock`,
|
||||||
successMessage: t`Stock counted`,
|
successMessage: t`Stock counted`,
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ import { IconCalendar, IconUsers } from '@tabler/icons-react';
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import RemoveRowButton from '../components/buttons/RemoveRowButton';
|
import RemoveRowButton from '../components/buttons/RemoveRowButton';
|
||||||
import { StandaloneField } from '../components/forms/StandaloneField';
|
import { StandaloneField } from '../components/forms/StandaloneField';
|
||||||
import type { TableFieldRowProps } from '../components/forms/fields/TableField';
|
import {
|
||||||
|
TableFieldQuantityInput,
|
||||||
|
type TableFieldRowProps
|
||||||
|
} from '../components/forms/fields/TableField';
|
||||||
import { useCreateApiFormModal } from '../hooks/UseForm';
|
import { useCreateApiFormModal } from '../hooks/UseForm';
|
||||||
import { useGlobalSettingsState } from '../states/SettingsStates';
|
import { useGlobalSettingsState } from '../states/SettingsStates';
|
||||||
import { RenderPartColumn } from '../tables/ColumnRenderers';
|
import { RenderPartColumn } from '../tables/ColumnRenderers';
|
||||||
@@ -110,6 +113,10 @@ function TransferOrderAllocateLineRow({
|
|||||||
record: any;
|
record: any;
|
||||||
sourceLocation?: number | null;
|
sourceLocation?: number | null;
|
||||||
}>) {
|
}>) {
|
||||||
|
const [quantity, setQuantity] = useState<number | ''>(
|
||||||
|
props.item?.quantity ?? ''
|
||||||
|
);
|
||||||
|
|
||||||
// Statically defined field for selecting the stock item
|
// Statically defined field for selecting the stock item
|
||||||
const stockItemField: ApiFormFieldType = useMemo(() => {
|
const stockItemField: ApiFormFieldType = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
@@ -128,41 +135,27 @@ function TransferOrderAllocateLineRow({
|
|||||||
value: props.item.stock_item,
|
value: props.item.stock_item,
|
||||||
name: 'stock_item',
|
name: 'stock_item',
|
||||||
onValueChange: (value: any, instance: any) => {
|
onValueChange: (value: any, instance: any) => {
|
||||||
props.changeFn(props.idx, 'stock_item', value);
|
props.changeFn(props.rowId, 'stock_item', value);
|
||||||
|
|
||||||
// Update the allocated quantity based on the selected stock item
|
// Update the allocated quantity based on the selected stock item
|
||||||
if (instance) {
|
if (instance) {
|
||||||
const available = instance.quantity - instance.allocated;
|
const available = instance.quantity - instance.allocated;
|
||||||
const required = record.quantity - record.allocated;
|
const required = record.quantity - record.allocated;
|
||||||
|
|
||||||
let quantity = props.item?.quantity ?? 0;
|
let q = props.item?.quantity ?? 0;
|
||||||
|
|
||||||
quantity = Math.max(quantity, required);
|
q = Math.max(q, required);
|
||||||
quantity = Math.min(quantity, available);
|
q = Math.min(q, available);
|
||||||
|
|
||||||
if (quantity != props.item.quantity) {
|
setQuantity(q);
|
||||||
props.changeFn(props.idx, 'quantity', quantity);
|
props.changeFn(props.rowId, 'quantity', q);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [sourceLocation, record, props]);
|
}, [sourceLocation, record, quantity, props.changeFn]);
|
||||||
|
|
||||||
// Statically defined field for selecting the allocation quantity
|
|
||||||
const quantityField: ApiFormFieldType = useMemo(() => {
|
|
||||||
return {
|
|
||||||
field_type: 'number',
|
|
||||||
name: 'quantity',
|
|
||||||
required: true,
|
|
||||||
value: props.item.quantity,
|
|
||||||
onValueChange: (value: any) => {
|
|
||||||
props.changeFn(props.idx, 'quantity', value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [props]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table.Tr key={`table-row-${props.idx}-${record.pk}`}>
|
<Table.Tr key={`table-row-${props.rowId}`}>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<RenderPartColumn part={record.part_detail} />
|
<RenderPartColumn part={record.part_detail} />
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
@@ -181,14 +174,18 @@ function TransferOrderAllocateLineRow({
|
|||||||
/>
|
/>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<StandaloneField
|
<TableFieldQuantityInput
|
||||||
fieldName='quantity'
|
min={0}
|
||||||
fieldDefinition={quantityField}
|
value={quantity}
|
||||||
|
onChange={(value) => {
|
||||||
|
setQuantity(value === '' ? 0 : value);
|
||||||
|
props.changeFn(props.rowId, 'quantity', value);
|
||||||
|
}}
|
||||||
error={props.rowErrors?.quantity?.message}
|
error={props.rowErrors?.quantity?.message}
|
||||||
/>
|
/>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<RemoveRowButton onClick={() => props.removeFn(props.idx)} />
|
<RemoveRowButton onClick={() => props.removeFn(props.rowId)} />
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
);
|
);
|
||||||
@@ -209,6 +206,16 @@ export function useAllocateToTransferOrderForm({
|
|||||||
sourceLocationId || null
|
sourceLocationId || null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Memoize the line items to prevent re-rendering
|
||||||
|
const lines = useMemo(() => {
|
||||||
|
return lineItems.map((item) => {
|
||||||
|
return {
|
||||||
|
id: item.pk,
|
||||||
|
...item
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [lineItems]);
|
||||||
|
|
||||||
const fields: ApiFormFieldSet = useMemo(() => {
|
const fields: ApiFormFieldSet = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
// Non-submitted field to select the source location
|
// Non-submitted field to select the source location
|
||||||
@@ -237,11 +244,11 @@ export function useAllocateToTransferOrderForm({
|
|||||||
],
|
],
|
||||||
modelRenderer: (row: TableFieldRowProps) => {
|
modelRenderer: (row: TableFieldRowProps) => {
|
||||||
const record =
|
const record =
|
||||||
lineItems.find((item) => item.pk == row.item.line_item) ?? {};
|
lines.find((item) => item.pk == row.item.line_item) ?? {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TransferOrderAllocateLineRow
|
<TransferOrderAllocateLineRow
|
||||||
key={`table-row-${row.idx}-${record.pk}`}
|
key={row.rowId}
|
||||||
props={row}
|
props={row}
|
||||||
record={record}
|
record={record}
|
||||||
sourceLocation={sourceLocation}
|
sourceLocation={sourceLocation}
|
||||||
@@ -263,6 +270,7 @@ export function useAllocateToTransferOrderForm({
|
|||||||
initialData: {
|
initialData: {
|
||||||
items: lineItems.map((item) => {
|
items: lineItems.map((item) => {
|
||||||
return {
|
return {
|
||||||
|
id: item.pk,
|
||||||
line_item: item.pk,
|
line_item: item.pk,
|
||||||
quantity: 0,
|
quantity: 0,
|
||||||
stock_item: null
|
stock_item: null
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/** Various debugging helper functions for development */
|
||||||
|
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A custom hook that logs the previous and current props of a component whenever it updates.
|
||||||
|
*/
|
||||||
|
export function useWhyDidYouUpdate(name: string, props: any) {
|
||||||
|
const previousProps = useRef({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.error(
|
||||||
|
'useWhyDidYouUpdate should not be used in production code. It is intended for debugging purposes only.'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (previousProps.current) {
|
||||||
|
const allKeys = Object.keys({ ...previousProps.current, ...props });
|
||||||
|
const changedProps: any = {};
|
||||||
|
|
||||||
|
allKeys.forEach((key) => {
|
||||||
|
if ((previousProps as any).current[key] !== props[key]) {
|
||||||
|
(changedProps as any)[key] = {
|
||||||
|
from: (previousProps as any).current[key],
|
||||||
|
to: props[key]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Object.keys(changedProps).length > 0) {
|
||||||
|
console.log(`[${name}] Changed props:`, changedProps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
previousProps.current = props;
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1064,10 +1064,9 @@ export default function PartDetail() {
|
|||||||
|
|
||||||
const stockOperationProps: StockOperationProps = useMemo(() => {
|
const stockOperationProps: StockOperationProps = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
pk: part.pk,
|
|
||||||
model: ModelType.part,
|
|
||||||
refresh: refreshInstance,
|
refresh: refreshInstance,
|
||||||
filters: {
|
filters: {
|
||||||
|
part: part.pk,
|
||||||
in_stock: true
|
in_stock: true
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -349,10 +349,9 @@ export default function Stock() {
|
|||||||
|
|
||||||
const stockOperationProps: StockOperationProps = useMemo(() => {
|
const stockOperationProps: StockOperationProps = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
pk: location.pk,
|
|
||||||
model: 'location',
|
|
||||||
refresh: refreshInstance,
|
refresh: refreshInstance,
|
||||||
filters: {
|
filters: {
|
||||||
|
location: location.pk,
|
||||||
in_stock: true
|
in_stock: true
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -805,7 +805,6 @@ export default function StockDetail() {
|
|||||||
const stockOperationProps: StockOperationProps = useMemo(() => {
|
const stockOperationProps: StockOperationProps = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
items: [stockitem],
|
items: [stockitem],
|
||||||
model: ModelType.stockitem,
|
|
||||||
refresh: () => {
|
refresh: () => {
|
||||||
const location = stockitem?.location;
|
const location = stockitem?.location;
|
||||||
refreshInstancePromise().then((response) => {
|
refreshInstancePromise().then((response) => {
|
||||||
@@ -830,6 +829,8 @@ export default function StockDetail() {
|
|||||||
formProps: stockOperationProps,
|
formProps: stockOperationProps,
|
||||||
delete: false,
|
delete: false,
|
||||||
changeBatch: false,
|
changeBatch: false,
|
||||||
|
add: !stockitem.serial,
|
||||||
|
remove: !stockitem.serial,
|
||||||
assign: !!stockitem.in_stock && stockitem.part_detail?.salable,
|
assign: !!stockitem.in_stock && stockitem.part_detail?.salable,
|
||||||
return: !!stockitem.consumed_by || !!stockitem.customer,
|
return: !!stockitem.consumed_by || !!stockitem.customer,
|
||||||
merge: false
|
merge: false
|
||||||
|
|||||||
@@ -181,6 +181,6 @@ export function patchUser(key: 'language' | 'theme' | 'widgets', val: any) {
|
|||||||
if (uid) {
|
if (uid) {
|
||||||
api.patch(apiUrl(ApiEndpoints.user_me_profile), { [key]: val });
|
api.patch(apiUrl(ApiEndpoints.user_me_profile), { [key]: val });
|
||||||
} else {
|
} else {
|
||||||
console.log('user not logged in, not patching');
|
console.warn('user not logged in, not patching');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -233,7 +233,6 @@ export default function BuildAllocatedStockTable({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
items: stockItems,
|
items: stockItems,
|
||||||
model: ModelType.stockitem,
|
|
||||||
refresh: table.refreshTable
|
refresh: table.refreshTable
|
||||||
};
|
};
|
||||||
}, [table.selectedRecords, table.refreshTable]);
|
}, [table.selectedRecords, table.refreshTable]);
|
||||||
|
|||||||
@@ -309,7 +309,10 @@ export default function BuildOutputTable({
|
|||||||
allocated += allocation.quantity;
|
allocated += allocation.quantity;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (allocated >= item.bom_item_detail.quantity) {
|
if (
|
||||||
|
item.bom_item_detail?.quantity &&
|
||||||
|
allocated >= item.bom_item_detail.quantity
|
||||||
|
) {
|
||||||
fullyAllocatedCount += 1;
|
fullyAllocatedCount += 1;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -510,7 +513,6 @@ export default function BuildOutputTable({
|
|||||||
const stockOperationProps: StockOperationProps = useMemo(() => {
|
const stockOperationProps: StockOperationProps = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
items: table.selectedRecords,
|
items: table.selectedRecords,
|
||||||
model: ModelType.stockitem,
|
|
||||||
refresh: table.refreshTable,
|
refresh: table.refreshTable,
|
||||||
filters: {}
|
filters: {}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -285,7 +285,6 @@ export default function SalesOrderAllocationTable({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
items: stockItems,
|
items: stockItems,
|
||||||
model: ModelType.stockitem,
|
|
||||||
refresh: table.refreshTable
|
refresh: table.refreshTable
|
||||||
};
|
};
|
||||||
}, [table.selectedRecords, table.refreshTable]);
|
}, [table.selectedRecords, table.refreshTable]);
|
||||||
|
|||||||
@@ -105,14 +105,12 @@ function SelectionListEntriesTable({
|
|||||||
return [
|
return [
|
||||||
RowEditAction({
|
RowEditAction({
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
console.log('record:', record);
|
|
||||||
setSelectedEntry(record.id);
|
setSelectedEntry(record.id);
|
||||||
editEntry.open();
|
editEntry.open();
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
RowDeleteAction({
|
RowDeleteAction({
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
console.log('record:', record);
|
|
||||||
setSelectedEntry(record.id);
|
setSelectedEntry(record.id);
|
||||||
deleteEntry.open();
|
deleteEntry.open();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -399,7 +399,6 @@ export function StockItemTable({
|
|||||||
const stockOperationProps: StockOperationProps = useMemo(() => {
|
const stockOperationProps: StockOperationProps = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
items: table.selectedRecords,
|
items: table.selectedRecords,
|
||||||
model: ModelType.stockitem,
|
|
||||||
refresh: () => {
|
refresh: () => {
|
||||||
table.clearSelectedRecords();
|
table.clearSelectedRecords();
|
||||||
table.refreshTable();
|
table.refreshTable();
|
||||||
|
|||||||
@@ -224,7 +224,6 @@ export default function TransferOrderAllocationTable({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
items: stockItems,
|
items: stockItems,
|
||||||
model: ModelType.stockitem,
|
|
||||||
refresh: table.refreshTable
|
refresh: table.refreshTable
|
||||||
};
|
};
|
||||||
}, [table.selectedRecords, table.refreshTable]);
|
}, [table.selectedRecords, table.refreshTable]);
|
||||||
|
|||||||
@@ -159,8 +159,6 @@ test('Dashboard - Preserve widget sizes', async ({ browser }) => {
|
|||||||
for (const [bp, items] of Object.entries(await readLayouts(page))) {
|
for (const [bp, items] of Object.entries(await readLayouts(page))) {
|
||||||
const entry = (items as any[]).find((i) => i?.i === 'ovr-so');
|
const entry = (items as any[]).find((i) => i?.i === 'ovr-so');
|
||||||
|
|
||||||
console.log('entry:', bp, entry);
|
|
||||||
|
|
||||||
expect(entry?.w, `${bp}: ovr-so missing or wrong w`).toBe(TARGET_W);
|
expect(entry?.w, `${bp}: ovr-so missing or wrong w`).toBe(TARGET_W);
|
||||||
expect(entry?.h, `${bp}: ovr-so missing or wrong h`).toBe(TARGET_H);
|
expect(entry?.h, `${bp}: ovr-so missing or wrong h`).toBe(TARGET_H);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user