mirror of
https://github.com/inventree/InvenTree.git
synced 2025-05-05 23:08:48 +00:00
Details updates (#6605)
* Fix onClick behaviour for details image * Moving items - These are not "tables" per-se * Refactoring for DetailsTable * Skip hidden fields * Cleanup table column widths * Update part details * Fix icons * Add image back to part details - Also fix onClick events * Update stockitem details page * Implement details page for build order * Implement CompanyDetails page * Implemented salesorder details * Update SalesOrder detalis * ReturnOrder detail * PurchaseOrder detail page * Cleanup build details page * Stock location detail * Part Category detail * Bump API version * Bug fixes * Use image, not thumbnail * Fix field copy * Cleanup imgae hover * Improve PartDetail - Add more data - Add icons * Refactoring - Move Details out of "tables" directory * Remove old file * Revert "Remove old file" This reverts commit 6fd131f2a597963f28434d665f6f24c90d909357. * Fix files * Fix unused import
This commit is contained in:
parent
c8d6f2246b
commit
69871699c0
@ -1,11 +1,17 @@
|
|||||||
"""InvenTree API version information."""
|
"""InvenTree API version information."""
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 178
|
INVENTREE_API_VERSION = 179
|
||||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||||
|
|
||||||
INVENTREE_API_TEXT = """
|
INVENTREE_API_TEXT = """
|
||||||
|
|
||||||
|
v179 - 2024-03-01 : https://github.com/inventree/InvenTree/pull/6605
|
||||||
|
- Adds "subcategories" count to PartCategory serializer
|
||||||
|
- Adds "sublocations" count to StockLocation serializer
|
||||||
|
- Adds "image" field to PartBrief serializer
|
||||||
|
- Adds "image" field to CompanyBrief serializer
|
||||||
|
|
||||||
v178 - 2024-02-29 : https://github.com/inventree/InvenTree/pull/6604
|
v178 - 2024-02-29 : https://github.com/inventree/InvenTree/pull/6604
|
||||||
- Adds "external_stock" field to the Part API endpoint
|
- Adds "external_stock" field to the Part API endpoint
|
||||||
- Adds "external_stock" field to the BomItem API endpoint
|
- Adds "external_stock" field to the BomItem API endpoint
|
||||||
|
@ -42,11 +42,13 @@ class CompanyBriefSerializer(InvenTreeModelSerializer):
|
|||||||
"""Metaclass options."""
|
"""Metaclass options."""
|
||||||
|
|
||||||
model = Company
|
model = Company
|
||||||
fields = ['pk', 'url', 'name', 'description', 'image']
|
fields = ['pk', 'url', 'name', 'description', 'image', 'thumbnail']
|
||||||
|
|
||||||
url = serializers.CharField(source='get_absolute_url', read_only=True)
|
url = serializers.CharField(source='get_absolute_url', read_only=True)
|
||||||
|
|
||||||
image = serializers.CharField(source='get_thumbnail_url', read_only=True)
|
image = InvenTreeImageSerializerField(read_only=True)
|
||||||
|
|
||||||
|
thumbnail = serializers.CharField(source='get_thumbnail_url', read_only=True)
|
||||||
|
|
||||||
|
|
||||||
class AddressSerializer(InvenTreeModelSerializer):
|
class AddressSerializer(InvenTreeModelSerializer):
|
||||||
|
@ -74,6 +74,7 @@ class CategorySerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
|||||||
'level',
|
'level',
|
||||||
'parent',
|
'parent',
|
||||||
'part_count',
|
'part_count',
|
||||||
|
'subcategories',
|
||||||
'pathstring',
|
'pathstring',
|
||||||
'path',
|
'path',
|
||||||
'starred',
|
'starred',
|
||||||
@ -99,13 +100,18 @@ class CategorySerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
|||||||
def annotate_queryset(queryset):
|
def annotate_queryset(queryset):
|
||||||
"""Annotate extra information to the queryset."""
|
"""Annotate extra information to the queryset."""
|
||||||
# Annotate the number of 'parts' which exist in each category (including subcategories!)
|
# Annotate the number of 'parts' which exist in each category (including subcategories!)
|
||||||
queryset = queryset.annotate(part_count=part.filters.annotate_category_parts())
|
queryset = queryset.annotate(
|
||||||
|
part_count=part.filters.annotate_category_parts(),
|
||||||
|
subcategories=part.filters.annotate_sub_categories(),
|
||||||
|
)
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
url = serializers.CharField(source='get_absolute_url', read_only=True)
|
url = serializers.CharField(source='get_absolute_url', read_only=True)
|
||||||
|
|
||||||
part_count = serializers.IntegerField(read_only=True)
|
part_count = serializers.IntegerField(read_only=True, label=_('Parts'))
|
||||||
|
|
||||||
|
subcategories = serializers.IntegerField(read_only=True, label=_('Subcategories'))
|
||||||
|
|
||||||
level = serializers.IntegerField(read_only=True)
|
level = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
@ -282,6 +288,7 @@ class PartBriefSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
|||||||
'revision',
|
'revision',
|
||||||
'full_name',
|
'full_name',
|
||||||
'description',
|
'description',
|
||||||
|
'image',
|
||||||
'thumbnail',
|
'thumbnail',
|
||||||
'active',
|
'active',
|
||||||
'assembly',
|
'assembly',
|
||||||
@ -307,6 +314,7 @@ class PartBriefSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
|||||||
self.fields.pop('pricing_min')
|
self.fields.pop('pricing_min')
|
||||||
self.fields.pop('pricing_max')
|
self.fields.pop('pricing_max')
|
||||||
|
|
||||||
|
image = InvenTree.serializers.InvenTreeImageSerializerField(read_only=True)
|
||||||
thumbnail = serializers.CharField(source='get_thumbnail_url', read_only=True)
|
thumbnail = serializers.CharField(source='get_thumbnail_url', read_only=True)
|
||||||
|
|
||||||
# Pricing fields
|
# Pricing fields
|
||||||
|
@ -886,6 +886,7 @@ class LocationSerializer(InvenTree.serializers.InvenTreeTagModelSerializer):
|
|||||||
'pathstring',
|
'pathstring',
|
||||||
'path',
|
'path',
|
||||||
'items',
|
'items',
|
||||||
|
'sublocations',
|
||||||
'owner',
|
'owner',
|
||||||
'icon',
|
'icon',
|
||||||
'custom_icon',
|
'custom_icon',
|
||||||
@ -911,13 +912,18 @@ class LocationSerializer(InvenTree.serializers.InvenTreeTagModelSerializer):
|
|||||||
def annotate_queryset(queryset):
|
def annotate_queryset(queryset):
|
||||||
"""Annotate extra information to the queryset."""
|
"""Annotate extra information to the queryset."""
|
||||||
# Annotate the number of stock items which exist in this category (including subcategories)
|
# Annotate the number of stock items which exist in this category (including subcategories)
|
||||||
queryset = queryset.annotate(items=stock.filters.annotate_location_items())
|
queryset = queryset.annotate(
|
||||||
|
items=stock.filters.annotate_location_items(),
|
||||||
|
sublocations=stock.filters.annotate_sub_locations(),
|
||||||
|
)
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
url = serializers.CharField(source='get_absolute_url', read_only=True)
|
url = serializers.CharField(source='get_absolute_url', read_only=True)
|
||||||
|
|
||||||
items = serializers.IntegerField(read_only=True)
|
items = serializers.IntegerField(read_only=True, label=_('Stock Items'))
|
||||||
|
|
||||||
|
sublocations = serializers.IntegerField(read_only=True, label=_('Sublocations'))
|
||||||
|
|
||||||
level = serializers.IntegerField(read_only=True)
|
level = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
|
@ -14,15 +14,17 @@ import {
|
|||||||
import { useSuspenseQuery } from '@tanstack/react-query';
|
import { useSuspenseQuery } from '@tanstack/react-query';
|
||||||
import { Suspense, useMemo } from 'react';
|
import { Suspense, useMemo } from 'react';
|
||||||
|
|
||||||
import { api } from '../App';
|
import { api } from '../../App';
|
||||||
import { ProgressBar } from '../components/items/ProgressBar';
|
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||||
import { getModelInfo } from '../components/render/ModelType';
|
import { ModelType } from '../../enums/ModelType';
|
||||||
import { ApiEndpoints } from '../enums/ApiEndpoints';
|
import { InvenTreeIcon } from '../../functions/icons';
|
||||||
import { ModelType } from '../enums/ModelType';
|
import { getDetailUrl } from '../../functions/urls';
|
||||||
import { InvenTreeIcon } from '../functions/icons';
|
import { apiUrl } from '../../states/ApiState';
|
||||||
import { getDetailUrl } from '../functions/urls';
|
import { useGlobalSettingsState } from '../../states/SettingsState';
|
||||||
import { apiUrl } from '../states/ApiState';
|
import { ProgressBar } from '../items/ProgressBar';
|
||||||
import { useGlobalSettingsState } from '../states/SettingsState';
|
import { YesNoButton } from '../items/YesNoButton';
|
||||||
|
import { getModelInfo } from '../render/ModelType';
|
||||||
|
import { StatusRenderer } from '../render/StatusRenderer';
|
||||||
|
|
||||||
export type PartIconsType = {
|
export type PartIconsType = {
|
||||||
assembly: boolean;
|
assembly: boolean;
|
||||||
@ -37,12 +39,20 @@ export type PartIconsType = {
|
|||||||
|
|
||||||
export type DetailsField =
|
export type DetailsField =
|
||||||
| {
|
| {
|
||||||
|
hidden?: boolean;
|
||||||
|
icon?: string;
|
||||||
name: string;
|
name: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
badge?: BadgeType;
|
badge?: BadgeType;
|
||||||
copy?: boolean;
|
copy?: boolean;
|
||||||
value_formatter?: () => ValueFormatterReturn;
|
value_formatter?: () => ValueFormatterReturn;
|
||||||
} & (StringDetailField | LinkDetailField | ProgressBarfield);
|
} & (
|
||||||
|
| StringDetailField
|
||||||
|
| BooleanField
|
||||||
|
| LinkDetailField
|
||||||
|
| ProgressBarfield
|
||||||
|
| StatusField
|
||||||
|
);
|
||||||
|
|
||||||
type BadgeType = 'owner' | 'user' | 'group';
|
type BadgeType = 'owner' | 'user' | 'group';
|
||||||
type ValueFormatterReturn = string | number | null;
|
type ValueFormatterReturn = string | number | null;
|
||||||
@ -52,12 +62,20 @@ type StringDetailField = {
|
|||||||
unit?: boolean;
|
unit?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type BooleanField = {
|
||||||
|
type: 'boolean';
|
||||||
|
};
|
||||||
|
|
||||||
type LinkDetailField = {
|
type LinkDetailField = {
|
||||||
type: 'link';
|
type: 'link';
|
||||||
|
link?: boolean;
|
||||||
} & (InternalLinkField | ExternalLinkField);
|
} & (InternalLinkField | ExternalLinkField);
|
||||||
|
|
||||||
type InternalLinkField = {
|
type InternalLinkField = {
|
||||||
model: ModelType;
|
model: ModelType;
|
||||||
|
model_field?: string;
|
||||||
|
model_formatter?: (value: any) => string;
|
||||||
|
backup_value?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ExternalLinkField = {
|
type ExternalLinkField = {
|
||||||
@ -70,6 +88,11 @@ type ProgressBarfield = {
|
|||||||
total: number;
|
total: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type StatusField = {
|
||||||
|
type: 'status';
|
||||||
|
model: ModelType;
|
||||||
|
};
|
||||||
|
|
||||||
type FieldValueType = string | number | undefined;
|
type FieldValueType = string | number | undefined;
|
||||||
|
|
||||||
type FieldProps = {
|
type FieldProps = {
|
||||||
@ -78,101 +101,6 @@ type FieldProps = {
|
|||||||
unit?: string | null;
|
unit?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches and wraps an InvenTreeIcon in a flex div
|
|
||||||
* @param icon name of icon
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
function PartIcon(icon: string) {
|
|
||||||
return (
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
|
|
||||||
<InvenTreeIcon icon={icon} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates a table cell with Part icons.
|
|
||||||
* Only used for Part Model Details
|
|
||||||
*/
|
|
||||||
function PartIcons({
|
|
||||||
assembly,
|
|
||||||
template,
|
|
||||||
component,
|
|
||||||
trackable,
|
|
||||||
purchaseable,
|
|
||||||
saleable,
|
|
||||||
virtual,
|
|
||||||
active
|
|
||||||
}: PartIconsType) {
|
|
||||||
return (
|
|
||||||
<td colSpan={2}>
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
|
||||||
{!active && (
|
|
||||||
<Tooltip label={t`Part is not active`}>
|
|
||||||
<Badge color="red" variant="filled">
|
|
||||||
<div
|
|
||||||
style={{ display: 'flex', alignItems: 'center', gap: '5px' }}
|
|
||||||
>
|
|
||||||
<InvenTreeIcon icon="inactive" iconProps={{ size: 19 }} />{' '}
|
|
||||||
<Trans>Inactive</Trans>
|
|
||||||
</div>
|
|
||||||
</Badge>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
{template && (
|
|
||||||
<Tooltip
|
|
||||||
label={t`Part is a template part (variants can be made from this part)`}
|
|
||||||
children={PartIcon('template')}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{assembly && (
|
|
||||||
<Tooltip
|
|
||||||
label={t`Part can be assembled from other parts`}
|
|
||||||
children={PartIcon('assembly')}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{component && (
|
|
||||||
<Tooltip
|
|
||||||
label={t`Part can be used in assemblies`}
|
|
||||||
children={PartIcon('component')}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{trackable && (
|
|
||||||
<Tooltip
|
|
||||||
label={t`Part stock is tracked by serial number`}
|
|
||||||
children={PartIcon('trackable')}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{purchaseable && (
|
|
||||||
<Tooltip
|
|
||||||
label={t`Part can be purchased from external suppliers`}
|
|
||||||
children={PartIcon('purchaseable')}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{saleable && (
|
|
||||||
<Tooltip
|
|
||||||
label={t`Part can be sold to customers`}
|
|
||||||
children={PartIcon('saleable')}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{virtual && (
|
|
||||||
<Tooltip label={t`Part is virtual (not a physical part)`}>
|
|
||||||
<Badge color="yellow" variant="filled">
|
|
||||||
<div
|
|
||||||
style={{ display: 'flex', alignItems: 'center', gap: '5px' }}
|
|
||||||
>
|
|
||||||
<InvenTreeIcon icon="virtual" iconProps={{ size: 18 }} />{' '}
|
|
||||||
<Trans>Virtual</Trans>
|
|
||||||
</div>
|
|
||||||
</Badge>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches user or group info from backend and formats into a badge.
|
* Fetches user or group info from backend and formats into a badge.
|
||||||
* Badge shows username, full name, or group name depending on server settings.
|
* Badge shows username, full name, or group name depending on server settings.
|
||||||
@ -253,13 +181,17 @@ function NameBadge({ pk, type }: { pk: string | number; type: BadgeType }) {
|
|||||||
* If user is defined, a badge is rendered in addition to main value
|
* If user is defined, a badge is rendered in addition to main value
|
||||||
*/
|
*/
|
||||||
function TableStringValue(props: FieldProps) {
|
function TableStringValue(props: FieldProps) {
|
||||||
let value = props.field_value;
|
let value = props?.field_value;
|
||||||
|
|
||||||
if (props.field_data.value_formatter) {
|
if (value === undefined) {
|
||||||
|
return '---';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.field_data?.value_formatter) {
|
||||||
value = props.field_data.value_formatter();
|
value = props.field_data.value_formatter();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.field_data.badge) {
|
if (props.field_data?.badge) {
|
||||||
return <NameBadge pk={value} type={props.field_data.badge} />;
|
return <NameBadge pk={value} type={props.field_data.badge} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -267,17 +199,21 @@ function TableStringValue(props: FieldProps) {
|
|||||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
<Suspense fallback={<Skeleton width={200} height={20} radius="xl" />}>
|
<Suspense fallback={<Skeleton width={200} height={20} radius="xl" />}>
|
||||||
<span>
|
<span>
|
||||||
{value ? value : props.field_data.unit && '0'}{' '}
|
{value ? value : props.field_data?.unit && '0'}{' '}
|
||||||
{props.field_data.unit == true && props.unit}
|
{props.field_data.unit == true && props.unit}
|
||||||
</span>
|
</span>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
{props.field_data.user && (
|
{props.field_data.user && (
|
||||||
<NameBadge pk={props.field_data.user} type="user" />
|
<NameBadge pk={props.field_data?.user} type="user" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function BooleanValue(props: FieldProps) {
|
||||||
|
return <YesNoButton value={props.field_value} />;
|
||||||
|
}
|
||||||
|
|
||||||
function TableAnchorValue(props: FieldProps) {
|
function TableAnchorValue(props: FieldProps) {
|
||||||
if (props.field_data.external) {
|
if (props.field_data.external) {
|
||||||
return (
|
return (
|
||||||
@ -299,7 +235,7 @@ function TableAnchorValue(props: FieldProps) {
|
|||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const modelDef = getModelInfo(props.field_data.model);
|
const modelDef = getModelInfo(props.field_data.model);
|
||||||
|
|
||||||
if (!modelDef.api_endpoint) {
|
if (!modelDef?.api_endpoint) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -325,15 +261,37 @@ function TableAnchorValue(props: FieldProps) {
|
|||||||
return getDetailUrl(props.field_data.model, props.field_value);
|
return getDetailUrl(props.field_data.model, props.field_value);
|
||||||
}, [props.field_data.model, props.field_value]);
|
}, [props.field_data.model, props.field_value]);
|
||||||
|
|
||||||
|
let make_link = props.field_data?.link ?? true;
|
||||||
|
|
||||||
|
// Construct the "return value" for the fetched data
|
||||||
|
let value = undefined;
|
||||||
|
|
||||||
|
if (props.field_data.model_formatter) {
|
||||||
|
value = props.field_data.model_formatter(data) ?? value;
|
||||||
|
} else if (props.field_data.model_field) {
|
||||||
|
value = data?.[props.field_data.model_field] ?? value;
|
||||||
|
} else {
|
||||||
|
value = data?.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value === undefined) {
|
||||||
|
value = data?.name ?? props.field_data?.backup_value ?? 'No name defined';
|
||||||
|
make_link = false;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<Skeleton width={200} height={20} radius="xl" />}>
|
<Suspense fallback={<Skeleton width={200} height={20} radius="xl" />}>
|
||||||
<Anchor
|
{make_link ? (
|
||||||
href={`/platform${detailUrl}`}
|
<Anchor
|
||||||
target={data?.external ? '_blank' : undefined}
|
href={`/platform${detailUrl}`}
|
||||||
rel={data?.external ? 'noreferrer noopener' : undefined}
|
target={data?.external ? '_blank' : undefined}
|
||||||
>
|
rel={data?.external ? 'noreferrer noopener' : undefined}
|
||||||
<Text>{data.name ?? 'No name defined'}</Text>
|
>
|
||||||
</Anchor>
|
<Text>{value}</Text>
|
||||||
|
</Anchor>
|
||||||
|
) : (
|
||||||
|
<Text>{value}</Text>
|
||||||
|
)}
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -348,6 +306,12 @@ function ProgressBarValue(props: FieldProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function StatusValue(props: FieldProps) {
|
||||||
|
return (
|
||||||
|
<StatusRenderer type={props.field_data.model} status={props.field_value} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function CopyField({ value }: { value: string }) {
|
function CopyField({ value }: { value: string }) {
|
||||||
return (
|
return (
|
||||||
<CopyButton value={value}>
|
<CopyButton value={value}>
|
||||||
@ -366,27 +330,33 @@ function CopyField({ value }: { value: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TableField({
|
export function DetailsTableField({
|
||||||
field_data,
|
item,
|
||||||
field_value,
|
field
|
||||||
unit = null
|
|
||||||
}: {
|
}: {
|
||||||
field_data: DetailsField[];
|
item: any;
|
||||||
field_value: FieldValueType[];
|
field: DetailsField;
|
||||||
unit?: string | null;
|
|
||||||
}) {
|
}) {
|
||||||
function getFieldType(type: string) {
|
function getFieldType(type: string) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'text':
|
case 'text':
|
||||||
case 'string':
|
case 'string':
|
||||||
return TableStringValue;
|
return TableStringValue;
|
||||||
|
case 'boolean':
|
||||||
|
return BooleanValue;
|
||||||
case 'link':
|
case 'link':
|
||||||
return TableAnchorValue;
|
return TableAnchorValue;
|
||||||
case 'progressbar':
|
case 'progressbar':
|
||||||
return ProgressBarValue;
|
return ProgressBarValue;
|
||||||
|
case 'status':
|
||||||
|
return StatusValue;
|
||||||
|
default:
|
||||||
|
return TableStringValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FieldType: any = getFieldType(field.type);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr>
|
<tr>
|
||||||
<td
|
<td
|
||||||
@ -394,35 +364,20 @@ function TableField({
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: '20px',
|
gap: '20px',
|
||||||
|
width: '50',
|
||||||
justifyContent: 'flex-start'
|
justifyContent: 'flex-start'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<InvenTreeIcon icon={field_data[0].name} />
|
<InvenTreeIcon icon={field.icon ?? field.name} />
|
||||||
<Text>{field_data[0].label}</Text>
|
</td>
|
||||||
|
<td>
|
||||||
|
<Text>{field.label}</Text>
|
||||||
</td>
|
</td>
|
||||||
<td style={{ minWidth: '40%' }}>
|
<td style={{ minWidth: '40%' }}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
<FieldType field_data={field} field_value={item[field.name]} />
|
||||||
<div
|
</td>
|
||||||
style={{
|
<td style={{ width: '50' }}>
|
||||||
display: 'flex',
|
{field.copy && <CopyField value={item[field.name]} />}
|
||||||
justifyContent: 'space-between',
|
|
||||||
flexGrow: '1'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{field_data.map((data: DetailsField, index: number) => {
|
|
||||||
let FieldType: any = getFieldType(data.type);
|
|
||||||
return (
|
|
||||||
<FieldType
|
|
||||||
field_data={data}
|
|
||||||
field_value={field_value[index]}
|
|
||||||
unit={unit}
|
|
||||||
key={index}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
{field_data[0].copy && <CopyField value={`${field_value[0]}`} />}
|
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
@ -430,50 +385,20 @@ function TableField({
|
|||||||
|
|
||||||
export function DetailsTable({
|
export function DetailsTable({
|
||||||
item,
|
item,
|
||||||
fields,
|
fields
|
||||||
partIcons = false
|
|
||||||
}: {
|
}: {
|
||||||
item: any;
|
item: any;
|
||||||
fields: DetailsField[][];
|
fields: DetailsField[];
|
||||||
partIcons?: boolean;
|
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Paper p="xs" withBorder radius="xs">
|
<Paper p="xs" withBorder radius="xs">
|
||||||
<Table striped>
|
<Table striped>
|
||||||
<tbody>
|
<tbody>
|
||||||
{partIcons && (
|
{fields
|
||||||
<tr>
|
.filter((field: DetailsField) => !field.hidden)
|
||||||
<PartIcons
|
.map((field: DetailsField, index: number) => (
|
||||||
assembly={item.assembly}
|
<DetailsTableField field={field} item={item} key={index} />
|
||||||
template={item.is_template}
|
))}
|
||||||
component={item.component}
|
|
||||||
trackable={item.trackable}
|
|
||||||
purchaseable={item.purchaseable}
|
|
||||||
saleable={item.salable}
|
|
||||||
virtual={item.virtual}
|
|
||||||
active={item.active}
|
|
||||||
/>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
{fields.map((data: DetailsField[], index: number) => {
|
|
||||||
let value: FieldValueType[] = [];
|
|
||||||
for (const val of data) {
|
|
||||||
if (val.value_formatter) {
|
|
||||||
value.push(undefined);
|
|
||||||
} else {
|
|
||||||
value.push(item[val.name]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableField
|
|
||||||
field_data={data}
|
|
||||||
field_value={value}
|
|
||||||
key={index}
|
|
||||||
unit={item.units}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</Table>
|
</Table>
|
||||||
</Paper>
|
</Paper>
|
@ -4,7 +4,6 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
Group,
|
Group,
|
||||||
Image,
|
Image,
|
||||||
Modal,
|
|
||||||
Overlay,
|
Overlay,
|
||||||
Paper,
|
Paper,
|
||||||
Text,
|
Text,
|
||||||
@ -12,9 +11,9 @@ import {
|
|||||||
useMantineTheme
|
useMantineTheme
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { Dropzone, FileWithPath, IMAGE_MIME_TYPE } from '@mantine/dropzone';
|
import { Dropzone, FileWithPath, IMAGE_MIME_TYPE } from '@mantine/dropzone';
|
||||||
import { useDisclosure, useHover } from '@mantine/hooks';
|
import { useHover } from '@mantine/hooks';
|
||||||
import { modals } from '@mantine/modals';
|
import { modals } from '@mantine/modals';
|
||||||
import { useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { api } from '../../App';
|
import { api } from '../../App';
|
||||||
import { UserRoles } from '../../enums/Roles';
|
import { UserRoles } from '../../enums/Roles';
|
||||||
@ -22,8 +21,8 @@ import { InvenTreeIcon } from '../../functions/icons';
|
|||||||
import { useUserState } from '../../states/UserState';
|
import { useUserState } from '../../states/UserState';
|
||||||
import { PartThumbTable } from '../../tables/part/PartThumbTable';
|
import { PartThumbTable } from '../../tables/part/PartThumbTable';
|
||||||
import { ActionButton } from '../buttons/ActionButton';
|
import { ActionButton } from '../buttons/ActionButton';
|
||||||
|
import { ApiImage } from '../images/ApiImage';
|
||||||
import { StylishText } from '../items/StylishText';
|
import { StylishText } from '../items/StylishText';
|
||||||
import { ApiImage } from './ApiImage';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Props for detail image
|
* Props for detail image
|
||||||
@ -32,7 +31,7 @@ export type DetailImageProps = {
|
|||||||
appRole: UserRoles;
|
appRole: UserRoles;
|
||||||
src: string;
|
src: string;
|
||||||
apiPath: string;
|
apiPath: string;
|
||||||
refresh: () => void;
|
refresh?: () => void;
|
||||||
imageActions?: DetailImageButtonProps;
|
imageActions?: DetailImageButtonProps;
|
||||||
pk: string;
|
pk: string;
|
||||||
};
|
};
|
||||||
@ -267,7 +266,10 @@ function ImageActionButtons({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="lg"
|
size="lg"
|
||||||
tooltipAlignment="top"
|
tooltipAlignment="top"
|
||||||
onClick={() => {
|
onClick={(event: any) => {
|
||||||
|
event?.preventDefault();
|
||||||
|
event?.stopPropagation();
|
||||||
|
event?.nativeEvent?.stopImmediatePropagation();
|
||||||
modals.open({
|
modals.open({
|
||||||
title: <StylishText size="xl">{t`Select Image`}</StylishText>,
|
title: <StylishText size="xl">{t`Select Image`}</StylishText>,
|
||||||
size: 'xxl',
|
size: 'xxl',
|
||||||
@ -285,7 +287,10 @@ function ImageActionButtons({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="lg"
|
size="lg"
|
||||||
tooltipAlignment="top"
|
tooltipAlignment="top"
|
||||||
onClick={() => {
|
onClick={(event: any) => {
|
||||||
|
event?.preventDefault();
|
||||||
|
event?.stopPropagation();
|
||||||
|
event?.nativeEvent?.stopImmediatePropagation();
|
||||||
modals.open({
|
modals.open({
|
||||||
title: <StylishText size="xl">{t`Upload Image`}</StylishText>,
|
title: <StylishText size="xl">{t`Upload Image`}</StylishText>,
|
||||||
children: (
|
children: (
|
||||||
@ -304,7 +309,12 @@ function ImageActionButtons({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="lg"
|
size="lg"
|
||||||
tooltipAlignment="top"
|
tooltipAlignment="top"
|
||||||
onClick={() => removeModal(apiPath, setImage)}
|
onClick={(event: any) => {
|
||||||
|
event?.preventDefault();
|
||||||
|
event?.stopPropagation();
|
||||||
|
event?.nativeEvent?.stopImmediatePropagation();
|
||||||
|
removeModal(apiPath, setImage);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
@ -324,11 +334,30 @@ export function DetailsImage(props: DetailImageProps) {
|
|||||||
// Sets a new image, and triggers upstream instance refresh
|
// Sets a new image, and triggers upstream instance refresh
|
||||||
const setAndRefresh = (image: string) => {
|
const setAndRefresh = (image: string) => {
|
||||||
setImg(image);
|
setImg(image);
|
||||||
props.refresh();
|
props.refresh && props.refresh();
|
||||||
};
|
};
|
||||||
|
|
||||||
const permissions = useUserState();
|
const permissions = useUserState();
|
||||||
|
|
||||||
|
const hasOverlay: boolean = useMemo(() => {
|
||||||
|
return (
|
||||||
|
props.imageActions?.selectExisting ||
|
||||||
|
props.imageActions?.uploadFile ||
|
||||||
|
props.imageActions?.deleteFile ||
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}, [props.imageActions]);
|
||||||
|
|
||||||
|
const expandImage = (event: any) => {
|
||||||
|
event?.preventDefault();
|
||||||
|
event?.stopPropagation();
|
||||||
|
event?.nativeEvent?.stopImmediatePropagation();
|
||||||
|
modals.open({
|
||||||
|
children: <ApiImage src={img} />,
|
||||||
|
withCloseButton: false
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AspectRatio ref={ref} maw={IMAGE_DIMENSION} ratio={1}>
|
<AspectRatio ref={ref} maw={IMAGE_DIMENSION} ratio={1}>
|
||||||
@ -337,25 +366,22 @@ export function DetailsImage(props: DetailImageProps) {
|
|||||||
src={img}
|
src={img}
|
||||||
height={IMAGE_DIMENSION}
|
height={IMAGE_DIMENSION}
|
||||||
width={IMAGE_DIMENSION}
|
width={IMAGE_DIMENSION}
|
||||||
onClick={() => {
|
onClick={expandImage}
|
||||||
modals.open({
|
|
||||||
children: <ApiImage src={img} />,
|
|
||||||
withCloseButton: false
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
{permissions.hasChangeRole(props.appRole) && hovered && (
|
{permissions.hasChangeRole(props.appRole) &&
|
||||||
<Overlay color="black" opacity={0.8}>
|
hasOverlay &&
|
||||||
<ImageActionButtons
|
hovered && (
|
||||||
visible={hovered}
|
<Overlay color="black" opacity={0.8} onClick={expandImage}>
|
||||||
actions={props.imageActions}
|
<ImageActionButtons
|
||||||
apiPath={props.apiPath}
|
visible={hovered}
|
||||||
hasImage={props.src ? true : false}
|
actions={props.imageActions}
|
||||||
pk={props.pk}
|
apiPath={props.apiPath}
|
||||||
setImage={setAndRefresh}
|
hasImage={props.src ? true : false}
|
||||||
/>
|
pk={props.pk}
|
||||||
</Overlay>
|
setImage={setAndRefresh}
|
||||||
)}
|
/>
|
||||||
|
</Overlay>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
</AspectRatio>
|
</AspectRatio>
|
||||||
</>
|
</>
|
14
src/frontend/src/components/details/ItemDetails.tsx
Normal file
14
src/frontend/src/components/details/ItemDetails.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { Paper, SimpleGrid } from '@mantine/core';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { DetailImageButtonProps } from './DetailsImage';
|
||||||
|
|
||||||
|
export function ItemDetailsGrid(props: React.PropsWithChildren<{}>) {
|
||||||
|
return (
|
||||||
|
<Paper p="xs">
|
||||||
|
<SimpleGrid cols={2} spacing="xs" verticalSpacing="xs">
|
||||||
|
{props.children}
|
||||||
|
</SimpleGrid>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
90
src/frontend/src/components/details/PartIcons.tsx
Normal file
90
src/frontend/src/components/details/PartIcons.tsx
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import { Trans, t } from '@lingui/macro';
|
||||||
|
import { Badge, Tooltip } from '@mantine/core';
|
||||||
|
|
||||||
|
import { InvenTreeIcon } from '../../functions/icons';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches and wraps an InvenTreeIcon in a flex div
|
||||||
|
* @param icon name of icon
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
function PartIcon(icon: string) {
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
|
||||||
|
<InvenTreeIcon icon={icon} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a table cell with Part icons.
|
||||||
|
* Only used for Part Model Details
|
||||||
|
*/
|
||||||
|
export function PartIcons({ part }: { part: any }) {
|
||||||
|
return (
|
||||||
|
<td colSpan={2}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
||||||
|
{!part.active && (
|
||||||
|
<Tooltip label={t`Part is not active`}>
|
||||||
|
<Badge color="red" variant="filled">
|
||||||
|
<div
|
||||||
|
style={{ display: 'flex', alignItems: 'center', gap: '5px' }}
|
||||||
|
>
|
||||||
|
<InvenTreeIcon icon="inactive" iconProps={{ size: 19 }} />{' '}
|
||||||
|
<Trans>Inactive</Trans>
|
||||||
|
</div>
|
||||||
|
</Badge>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{part.template && (
|
||||||
|
<Tooltip
|
||||||
|
label={t`Part is a template part (variants can be made from this part)`}
|
||||||
|
children={PartIcon('template')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{part.assembly && (
|
||||||
|
<Tooltip
|
||||||
|
label={t`Part can be assembled from other parts`}
|
||||||
|
children={PartIcon('assembly')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{part.component && (
|
||||||
|
<Tooltip
|
||||||
|
label={t`Part can be used in assemblies`}
|
||||||
|
children={PartIcon('component')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{part.trackable && (
|
||||||
|
<Tooltip
|
||||||
|
label={t`Part stock is tracked by serial number`}
|
||||||
|
children={PartIcon('trackable')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{part.purchaseable && (
|
||||||
|
<Tooltip
|
||||||
|
label={t`Part can be purchased from external suppliers`}
|
||||||
|
children={PartIcon('purchaseable')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{part.saleable && (
|
||||||
|
<Tooltip
|
||||||
|
label={t`Part can be sold to customers`}
|
||||||
|
children={PartIcon('saleable')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{part.virtual && (
|
||||||
|
<Tooltip label={t`Part is virtual (not a physical part)`}>
|
||||||
|
<Badge color="yellow" variant="filled">
|
||||||
|
<div
|
||||||
|
style={{ display: 'flex', alignItems: 'center', gap: '5px' }}
|
||||||
|
>
|
||||||
|
<InvenTreeIcon icon="virtual" iconProps={{ size: 18 }} />{' '}
|
||||||
|
<Trans>Virtual</Trans>
|
||||||
|
</div>
|
||||||
|
</Badge>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
}
|
@ -22,7 +22,7 @@ interface renderStatusLabelOptionsInterface {
|
|||||||
* Generic function to render a status label
|
* Generic function to render a status label
|
||||||
*/
|
*/
|
||||||
function renderStatusLabel(
|
function renderStatusLabel(
|
||||||
key: string,
|
key: string | number,
|
||||||
codes: StatusCodeListInterface,
|
codes: StatusCodeListInterface,
|
||||||
options: renderStatusLabelOptionsInterface = {}
|
options: renderStatusLabelOptionsInterface = {}
|
||||||
) {
|
) {
|
||||||
@ -68,7 +68,7 @@ export const StatusRenderer = ({
|
|||||||
type,
|
type,
|
||||||
options
|
options
|
||||||
}: {
|
}: {
|
||||||
status: string;
|
status: string | number;
|
||||||
type: ModelType | string;
|
type: ModelType | string;
|
||||||
options?: renderStatusLabelOptionsInterface;
|
options?: renderStatusLabelOptionsInterface;
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -2,32 +2,44 @@ import {
|
|||||||
Icon123,
|
Icon123,
|
||||||
IconBinaryTree2,
|
IconBinaryTree2,
|
||||||
IconBookmarks,
|
IconBookmarks,
|
||||||
|
IconBox,
|
||||||
IconBuilding,
|
IconBuilding,
|
||||||
IconBuildingFactory2,
|
IconBuildingFactory2,
|
||||||
|
IconBuildingStore,
|
||||||
|
IconCalendar,
|
||||||
IconCalendarStats,
|
IconCalendarStats,
|
||||||
IconCheck,
|
IconCheck,
|
||||||
IconClipboardList,
|
IconClipboardList,
|
||||||
IconCopy,
|
IconCopy,
|
||||||
IconCornerUpRightDouble,
|
IconCornerUpRightDouble,
|
||||||
IconCurrencyDollar,
|
IconCurrencyDollar,
|
||||||
|
IconDotsCircleHorizontal,
|
||||||
IconExternalLink,
|
IconExternalLink,
|
||||||
IconFileUpload,
|
IconFileUpload,
|
||||||
IconGitBranch,
|
IconGitBranch,
|
||||||
IconGridDots,
|
IconGridDots,
|
||||||
|
IconHash,
|
||||||
IconLayersLinked,
|
IconLayersLinked,
|
||||||
IconLink,
|
IconLink,
|
||||||
IconList,
|
IconList,
|
||||||
IconListTree,
|
IconListTree,
|
||||||
|
IconMail,
|
||||||
|
IconMapPin,
|
||||||
IconMapPinHeart,
|
IconMapPinHeart,
|
||||||
IconNotes,
|
IconNotes,
|
||||||
|
IconNumbers,
|
||||||
IconPackage,
|
IconPackage,
|
||||||
|
IconPackageImport,
|
||||||
IconPackages,
|
IconPackages,
|
||||||
IconPaperclip,
|
IconPaperclip,
|
||||||
|
IconPhone,
|
||||||
IconPhoto,
|
IconPhoto,
|
||||||
|
IconProgressCheck,
|
||||||
IconQuestionMark,
|
IconQuestionMark,
|
||||||
IconRulerMeasure,
|
IconRulerMeasure,
|
||||||
IconShoppingCart,
|
IconShoppingCart,
|
||||||
IconShoppingCartHeart,
|
IconShoppingCartHeart,
|
||||||
|
IconSitemap,
|
||||||
IconStack2,
|
IconStack2,
|
||||||
IconStatusChange,
|
IconStatusChange,
|
||||||
IconTag,
|
IconTag,
|
||||||
@ -41,6 +53,7 @@ import {
|
|||||||
IconUserStar,
|
IconUserStar,
|
||||||
IconUsersGroup,
|
IconUsersGroup,
|
||||||
IconVersions,
|
IconVersions,
|
||||||
|
IconWorld,
|
||||||
IconWorldCode,
|
IconWorldCode,
|
||||||
IconX
|
IconX
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
@ -67,6 +80,8 @@ const icons: { [key: string]: (props: TablerIconsProps) => React.JSX.Element } =
|
|||||||
revision: IconGitBranch,
|
revision: IconGitBranch,
|
||||||
units: IconRulerMeasure,
|
units: IconRulerMeasure,
|
||||||
keywords: IconTag,
|
keywords: IconTag,
|
||||||
|
status: IconInfoCircle,
|
||||||
|
info: IconInfoCircle,
|
||||||
details: IconInfoCircle,
|
details: IconInfoCircle,
|
||||||
parameters: IconList,
|
parameters: IconList,
|
||||||
stock: IconPackages,
|
stock: IconPackages,
|
||||||
@ -77,8 +92,10 @@ const icons: { [key: string]: (props: TablerIconsProps) => React.JSX.Element } =
|
|||||||
used_in: IconStack2,
|
used_in: IconStack2,
|
||||||
manufacturers: IconBuildingFactory2,
|
manufacturers: IconBuildingFactory2,
|
||||||
suppliers: IconBuilding,
|
suppliers: IconBuilding,
|
||||||
|
customers: IconBuildingStore,
|
||||||
purchase_orders: IconShoppingCart,
|
purchase_orders: IconShoppingCart,
|
||||||
sales_orders: IconTruckDelivery,
|
sales_orders: IconTruckDelivery,
|
||||||
|
shipment: IconTruckDelivery,
|
||||||
scheduling: IconCalendarStats,
|
scheduling: IconCalendarStats,
|
||||||
test_templates: IconTestPipe,
|
test_templates: IconTestPipe,
|
||||||
related_parts: IconLayersLinked,
|
related_parts: IconLayersLinked,
|
||||||
@ -91,6 +108,7 @@ const icons: { [key: string]: (props: TablerIconsProps) => React.JSX.Element } =
|
|||||||
delete: IconTrash,
|
delete: IconTrash,
|
||||||
|
|
||||||
// Part Icons
|
// Part Icons
|
||||||
|
active: IconCheck,
|
||||||
template: IconCopy,
|
template: IconCopy,
|
||||||
assembly: IconTool,
|
assembly: IconTool,
|
||||||
component: IconGridDots,
|
component: IconGridDots,
|
||||||
@ -99,19 +117,31 @@ const icons: { [key: string]: (props: TablerIconsProps) => React.JSX.Element } =
|
|||||||
saleable: IconCurrencyDollar,
|
saleable: IconCurrencyDollar,
|
||||||
virtual: IconWorldCode,
|
virtual: IconWorldCode,
|
||||||
inactive: IconX,
|
inactive: IconX,
|
||||||
|
part: IconBox,
|
||||||
|
supplier_part: IconPackageImport,
|
||||||
|
|
||||||
|
calendar: IconCalendar,
|
||||||
external: IconExternalLink,
|
external: IconExternalLink,
|
||||||
creation_date: IconCalendarTime,
|
creation_date: IconCalendarTime,
|
||||||
|
location: IconMapPin,
|
||||||
default_location: IconMapPinHeart,
|
default_location: IconMapPinHeart,
|
||||||
default_supplier: IconShoppingCartHeart,
|
default_supplier: IconShoppingCartHeart,
|
||||||
link: IconLink,
|
link: IconLink,
|
||||||
responsible: IconUserStar,
|
responsible: IconUserStar,
|
||||||
pricing: IconCurrencyDollar,
|
pricing: IconCurrencyDollar,
|
||||||
|
currency: IconCurrencyDollar,
|
||||||
stocktake: IconClipboardList,
|
stocktake: IconClipboardList,
|
||||||
user: IconUser,
|
user: IconUser,
|
||||||
group: IconUsersGroup,
|
group: IconUsersGroup,
|
||||||
check: IconCheck,
|
check: IconCheck,
|
||||||
copy: IconCopy
|
copy: IconCopy,
|
||||||
|
quantity: IconNumbers,
|
||||||
|
progress: IconProgressCheck,
|
||||||
|
reference: IconHash,
|
||||||
|
website: IconWorld,
|
||||||
|
email: IconMail,
|
||||||
|
phone: IconPhone,
|
||||||
|
sitemap: IconSitemap
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -138,3 +168,6 @@ export function InvenTreeIcon(props: IconProps) {
|
|||||||
|
|
||||||
return <Icon {...props.iconProps} />;
|
return <Icon {...props.iconProps} />;
|
||||||
}
|
}
|
||||||
|
function IconShapes(props: TablerIconsProps): Element {
|
||||||
|
throw new Error('Function not implemented.');
|
||||||
|
}
|
||||||
|
@ -7,10 +7,14 @@ import { ModelType } from '../enums/ModelType';
|
|||||||
export function getDetailUrl(model: ModelType, pk: number | string): string {
|
export function getDetailUrl(model: ModelType, pk: number | string): string {
|
||||||
const modelInfo = ModelInformationDict[model];
|
const modelInfo = ModelInformationDict[model];
|
||||||
|
|
||||||
if (modelInfo && modelInfo.url_detail) {
|
if (pk === undefined || pk === null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!!pk && modelInfo && modelInfo.url_detail) {
|
||||||
return modelInfo.url_detail.replace(':pk', pk.toString());
|
return modelInfo.url_detail.replace(':pk', pk.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error(`No detail URL found for model ${model}!`);
|
console.error(`No detail URL found for model ${model} <${pk}>`);
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Group, LoadingOverlay, Skeleton, Stack, Table } from '@mantine/core';
|
import { Grid, LoadingOverlay, Skeleton, Stack } from '@mantine/core';
|
||||||
import {
|
import {
|
||||||
IconClipboardCheck,
|
IconClipboardCheck,
|
||||||
IconClipboardList,
|
IconClipboardList,
|
||||||
@ -17,6 +17,9 @@ import {
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { DetailsField, DetailsTable } from '../../components/details/Details';
|
||||||
|
import { DetailsImage } from '../../components/details/DetailsImage';
|
||||||
|
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
||||||
import {
|
import {
|
||||||
ActionDropdown,
|
ActionDropdown,
|
||||||
DuplicateItemAction,
|
DuplicateItemAction,
|
||||||
@ -33,6 +36,7 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
|||||||
import { ModelType } from '../../enums/ModelType';
|
import { ModelType } from '../../enums/ModelType';
|
||||||
import { UserRoles } from '../../enums/Roles';
|
import { UserRoles } from '../../enums/Roles';
|
||||||
import { buildOrderFields } from '../../forms/BuildForms';
|
import { buildOrderFields } from '../../forms/BuildForms';
|
||||||
|
import { partCategoryFields } from '../../forms/PartForms';
|
||||||
import { useEditApiFormModal } from '../../hooks/UseForm';
|
import { useEditApiFormModal } from '../../hooks/UseForm';
|
||||||
import { useInstance } from '../../hooks/UseInstance';
|
import { useInstance } from '../../hooks/UseInstance';
|
||||||
import { apiUrl } from '../../states/ApiState';
|
import { apiUrl } from '../../states/ApiState';
|
||||||
@ -63,36 +67,127 @@ export default function BuildDetail() {
|
|||||||
refetchOnMount: true
|
refetchOnMount: true
|
||||||
});
|
});
|
||||||
|
|
||||||
const buildDetailsPanel = useMemo(() => {
|
const detailsPanel = useMemo(() => {
|
||||||
|
if (instanceQuery.isFetching) {
|
||||||
|
return <Skeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
let tl: DetailsField[] = [
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
name: 'part',
|
||||||
|
label: t`Part`,
|
||||||
|
model: ModelType.part
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'status',
|
||||||
|
name: 'status',
|
||||||
|
label: t`Status`,
|
||||||
|
model: ModelType.build
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'reference',
|
||||||
|
label: t`Reference`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'title',
|
||||||
|
label: t`Description`,
|
||||||
|
icon: 'description'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
name: 'parent',
|
||||||
|
icon: 'builds',
|
||||||
|
label: t`Parent Build`,
|
||||||
|
model_field: 'reference',
|
||||||
|
model: ModelType.build,
|
||||||
|
hidden: !build.parent
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
let tr: DetailsField[] = [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'quantity',
|
||||||
|
label: t`Build Quantity`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'progressbar',
|
||||||
|
name: 'completed',
|
||||||
|
icon: 'progress',
|
||||||
|
total: build.quantity,
|
||||||
|
progress: build.completed,
|
||||||
|
label: t`Completed Outputs`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
name: 'sales_order',
|
||||||
|
label: t`Sales Order`,
|
||||||
|
icon: 'sales_orders',
|
||||||
|
model: ModelType.salesorder,
|
||||||
|
model_field: 'reference',
|
||||||
|
hidden: !build.sales_order
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
let bl: DetailsField[] = [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'issued_by',
|
||||||
|
label: t`Issued By`,
|
||||||
|
badge: 'user'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'responsible',
|
||||||
|
label: t`Responsible`,
|
||||||
|
badge: 'owner',
|
||||||
|
hidden: !build.responsible
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
let br: DetailsField[] = [
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
name: 'take_from',
|
||||||
|
icon: 'location',
|
||||||
|
model: ModelType.stocklocation,
|
||||||
|
label: t`Source Location`,
|
||||||
|
backup_value: t`Any location`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
name: 'destination',
|
||||||
|
icon: 'location',
|
||||||
|
model: ModelType.stocklocation,
|
||||||
|
label: t`Destination Location`,
|
||||||
|
hidden: !build.destination
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group position="apart" grow>
|
<ItemDetailsGrid>
|
||||||
<Table striped>
|
<Grid>
|
||||||
<tbody>
|
<Grid.Col span={4}>
|
||||||
<tr>
|
<DetailsImage
|
||||||
<td>{t`Base Part`}</td>
|
appRole={UserRoles.part}
|
||||||
<td>{build.part_detail?.name}</td>
|
apiPath={ApiEndpoints.part_list}
|
||||||
</tr>
|
src={build.part_detail?.image ?? build.part_detail?.thumbnail}
|
||||||
<tr>
|
pk={build.part}
|
||||||
<td>{t`Quantity`}</td>
|
/>
|
||||||
<td>{build.quantity}</td>
|
</Grid.Col>
|
||||||
</tr>
|
<Grid.Col span={8}>
|
||||||
<tr>
|
<DetailsTable fields={tl} item={build} />
|
||||||
<td>{t`Build Status`}</td>
|
</Grid.Col>
|
||||||
<td>
|
</Grid>
|
||||||
{build?.status && (
|
<DetailsTable fields={tr} item={build} />
|
||||||
<StatusRenderer
|
<DetailsTable fields={bl} item={build} />
|
||||||
status={build.status}
|
<DetailsTable fields={br} item={build} />
|
||||||
type={ModelType.build}
|
</ItemDetailsGrid>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</Table>
|
|
||||||
<Table></Table>
|
|
||||||
</Group>
|
|
||||||
);
|
);
|
||||||
}, [build]);
|
}, [build, instanceQuery]);
|
||||||
|
|
||||||
const buildPanels: PanelType[] = useMemo(() => {
|
const buildPanels: PanelType[] = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
@ -100,7 +195,7 @@ export default function BuildDetail() {
|
|||||||
name: 'details',
|
name: 'details',
|
||||||
label: t`Build Details`,
|
label: t`Build Details`,
|
||||||
icon: <IconInfoCircle />,
|
icon: <IconInfoCircle />,
|
||||||
content: buildDetailsPanel
|
content: detailsPanel
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'allocate-stock',
|
name: 'allocate-stock',
|
||||||
@ -259,7 +354,7 @@ export default function BuildDetail() {
|
|||||||
title={build.reference}
|
title={build.reference}
|
||||||
subtitle={build.title}
|
subtitle={build.title}
|
||||||
detail={buildDetail}
|
detail={buildDetail}
|
||||||
imageUrl={build.part_detail?.thumbnail}
|
imageUrl={build.part_detail?.image ?? build.part_detail?.thumbnail}
|
||||||
breadcrumbs={[
|
breadcrumbs={[
|
||||||
{ name: t`Build Orders`, url: '/build' },
|
{ name: t`Build Orders`, url: '/build' },
|
||||||
{ name: build.reference, url: `/build/${build.pk}` }
|
{ name: build.reference, url: `/build/${build.pk}` }
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { LoadingOverlay, Skeleton, Stack } from '@mantine/core';
|
import { Grid, LoadingOverlay, Skeleton, Stack } from '@mantine/core';
|
||||||
import {
|
import {
|
||||||
IconBuildingFactory2,
|
IconBuildingFactory2,
|
||||||
IconBuildingWarehouse,
|
IconBuildingWarehouse,
|
||||||
@ -18,6 +18,9 @@ import {
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { DetailsField, DetailsTable } from '../../components/details/Details';
|
||||||
|
import { DetailsImage } from '../../components/details/DetailsImage';
|
||||||
|
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
||||||
import {
|
import {
|
||||||
ActionDropdown,
|
ActionDropdown,
|
||||||
DeleteItemAction,
|
DeleteItemAction,
|
||||||
@ -69,12 +72,99 @@ export default function CompanyDetail(props: CompanyDetailProps) {
|
|||||||
refetchOnMount: true
|
refetchOnMount: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const detailsPanel = useMemo(() => {
|
||||||
|
if (instanceQuery.isFetching) {
|
||||||
|
return <Skeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
let tl: DetailsField[] = [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'description',
|
||||||
|
label: t`Description`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
name: 'website',
|
||||||
|
label: t`Website`,
|
||||||
|
external: true,
|
||||||
|
copy: true,
|
||||||
|
hidden: !company.website
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'phone',
|
||||||
|
label: t`Phone Number`,
|
||||||
|
copy: true,
|
||||||
|
hidden: !company.phone
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'email',
|
||||||
|
label: t`Email Address`,
|
||||||
|
copy: true,
|
||||||
|
hidden: !company.email
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
let tr: DetailsField[] = [
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
name: 'currency',
|
||||||
|
label: t`Default Currency`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'boolean',
|
||||||
|
name: 'is_supplier',
|
||||||
|
label: t`Supplier`,
|
||||||
|
icon: 'suppliers'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'boolean',
|
||||||
|
name: 'is_manufacturer',
|
||||||
|
label: t`Manufacturer`,
|
||||||
|
icon: 'manufacturers'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'boolean',
|
||||||
|
name: 'is_customer',
|
||||||
|
label: t`Customer`,
|
||||||
|
icon: 'customers'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ItemDetailsGrid>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<DetailsImage
|
||||||
|
appRole={UserRoles.purchase_order}
|
||||||
|
apiPath={ApiEndpoints.company_list}
|
||||||
|
src={company.image}
|
||||||
|
pk={company.pk}
|
||||||
|
refresh={refreshInstance}
|
||||||
|
imageActions={{
|
||||||
|
uploadFile: true,
|
||||||
|
deleteFile: true
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={8}>
|
||||||
|
<DetailsTable item={company} fields={tl} />
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
<DetailsTable item={company} fields={tr} />
|
||||||
|
</ItemDetailsGrid>
|
||||||
|
);
|
||||||
|
}, [company, instanceQuery]);
|
||||||
|
|
||||||
const companyPanels: PanelType[] = useMemo(() => {
|
const companyPanels: PanelType[] = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: 'details',
|
name: 'details',
|
||||||
label: t`Details`,
|
label: t`Details`,
|
||||||
icon: <IconInfoCircle />
|
icon: <IconInfoCircle />,
|
||||||
|
content: detailsPanel
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'manufactured-parts',
|
name: 'manufactured-parts',
|
||||||
|
@ -1,21 +1,24 @@
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { LoadingOverlay, Stack, Text } from '@mantine/core';
|
import { LoadingOverlay, Skeleton, Stack, Text } from '@mantine/core';
|
||||||
import {
|
import {
|
||||||
IconCategory,
|
IconCategory,
|
||||||
|
IconInfoCircle,
|
||||||
IconListDetails,
|
IconListDetails,
|
||||||
IconSitemap
|
IconSitemap
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { DetailsField, DetailsTable } from '../../components/details/Details';
|
||||||
|
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
||||||
import { PageDetail } from '../../components/nav/PageDetail';
|
import { PageDetail } from '../../components/nav/PageDetail';
|
||||||
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
|
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
|
||||||
import { PartCategoryTree } from '../../components/nav/PartCategoryTree';
|
import { PartCategoryTree } from '../../components/nav/PartCategoryTree';
|
||||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||||
|
import { ModelType } from '../../enums/ModelType';
|
||||||
import { useInstance } from '../../hooks/UseInstance';
|
import { useInstance } from '../../hooks/UseInstance';
|
||||||
import ParametricPartTable from '../../tables/part/ParametricPartTable';
|
import ParametricPartTable from '../../tables/part/ParametricPartTable';
|
||||||
import { PartCategoryTable } from '../../tables/part/PartCategoryTable';
|
import { PartCategoryTable } from '../../tables/part/PartCategoryTable';
|
||||||
import { PartParameterTable } from '../../tables/part/PartParameterTable';
|
|
||||||
import { PartListTable } from '../../tables/part/PartTable';
|
import { PartListTable } from '../../tables/part/PartTable';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -45,8 +48,86 @@ export default function CategoryDetail({}: {}) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const detailsPanel = useMemo(() => {
|
||||||
|
if (id && instanceQuery.isFetching) {
|
||||||
|
return <Skeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
let left: DetailsField[] = [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'name',
|
||||||
|
label: t`Name`,
|
||||||
|
copy: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'pathstring',
|
||||||
|
label: t`Path`,
|
||||||
|
icon: 'sitemap',
|
||||||
|
copy: true,
|
||||||
|
hidden: !id
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'description',
|
||||||
|
label: t`Description`,
|
||||||
|
copy: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
name: 'parent',
|
||||||
|
model_field: 'name',
|
||||||
|
icon: 'location',
|
||||||
|
label: t`Parent Category`,
|
||||||
|
model: ModelType.partcategory,
|
||||||
|
hidden: !category?.parent
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
let right: DetailsField[] = [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'part_count',
|
||||||
|
label: t`Parts`,
|
||||||
|
icon: 'part'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'subcategories',
|
||||||
|
label: t`Subcategories`,
|
||||||
|
icon: 'sitemap',
|
||||||
|
hidden: !category?.subcategories
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'boolean',
|
||||||
|
name: 'structural',
|
||||||
|
label: t`Structural`,
|
||||||
|
icon: 'sitemap'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ItemDetailsGrid>
|
||||||
|
{id && category?.pk ? (
|
||||||
|
<DetailsTable item={category} fields={left} />
|
||||||
|
) : (
|
||||||
|
<Text>{t`Top level part category`}</Text>
|
||||||
|
)}
|
||||||
|
{id && category?.pk && <DetailsTable item={category} fields={right} />}
|
||||||
|
</ItemDetailsGrid>
|
||||||
|
);
|
||||||
|
}, [category, instanceQuery]);
|
||||||
|
|
||||||
const categoryPanels: PanelType[] = useMemo(
|
const categoryPanels: PanelType[] = useMemo(
|
||||||
() => [
|
() => [
|
||||||
|
{
|
||||||
|
name: 'details',
|
||||||
|
label: t`Category Details`,
|
||||||
|
icon: <IconInfoCircle />,
|
||||||
|
content: detailsPanel
|
||||||
|
// hidden: !category?.pk,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'parts',
|
name: 'parts',
|
||||||
label: t`Parts`,
|
label: t`Parts`,
|
||||||
|
@ -1,5 +1,12 @@
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Group, LoadingOverlay, Skeleton, Stack, Text } from '@mantine/core';
|
import {
|
||||||
|
Grid,
|
||||||
|
Group,
|
||||||
|
LoadingOverlay,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Text
|
||||||
|
} from '@mantine/core';
|
||||||
import {
|
import {
|
||||||
IconBookmarks,
|
IconBookmarks,
|
||||||
IconBuilding,
|
IconBuilding,
|
||||||
@ -28,6 +35,10 @@ import { useMemo, useState } from 'react';
|
|||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { api } from '../../App';
|
import { api } from '../../App';
|
||||||
|
import { DetailsField, DetailsTable } from '../../components/details/Details';
|
||||||
|
import { DetailsImage } from '../../components/details/DetailsImage';
|
||||||
|
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
||||||
|
import { PartIcons } from '../../components/details/PartIcons';
|
||||||
import {
|
import {
|
||||||
ActionDropdown,
|
ActionDropdown,
|
||||||
BarcodeActionDropdown,
|
BarcodeActionDropdown,
|
||||||
@ -51,12 +62,6 @@ import { useEditApiFormModal } from '../../hooks/UseForm';
|
|||||||
import { useInstance } from '../../hooks/UseInstance';
|
import { useInstance } from '../../hooks/UseInstance';
|
||||||
import { apiUrl } from '../../states/ApiState';
|
import { apiUrl } from '../../states/ApiState';
|
||||||
import { useUserState } from '../../states/UserState';
|
import { useUserState } from '../../states/UserState';
|
||||||
import { DetailsField } from '../../tables/Details';
|
|
||||||
import {
|
|
||||||
DetailsImageType,
|
|
||||||
ItemDetailFields,
|
|
||||||
ItemDetails
|
|
||||||
} from '../../tables/ItemDetails';
|
|
||||||
import { BomTable } from '../../tables/bom/BomTable';
|
import { BomTable } from '../../tables/bom/BomTable';
|
||||||
import { UsedInTable } from '../../tables/bom/UsedInTable';
|
import { UsedInTable } from '../../tables/bom/UsedInTable';
|
||||||
import { BuildOrderTable } from '../../tables/build/BuildOrderTable';
|
import { BuildOrderTable } from '../../tables/build/BuildOrderTable';
|
||||||
@ -93,188 +98,186 @@ export default function PartDetail() {
|
|||||||
refetchOnMount: true
|
refetchOnMount: true
|
||||||
});
|
});
|
||||||
|
|
||||||
const detailFields = (part: any): ItemDetailFields => {
|
const detailsPanel = useMemo(() => {
|
||||||
let left: DetailsField[][] = [];
|
if (instanceQuery.isFetching) {
|
||||||
let right: DetailsField[][] = [];
|
return <Skeleton />;
|
||||||
let bottom_right: DetailsField[][] = [];
|
}
|
||||||
let bottom_left: DetailsField[][] = [];
|
|
||||||
|
|
||||||
let image: DetailsImageType = {
|
// Construct the details tables
|
||||||
name: 'image',
|
let tl: DetailsField[] = [
|
||||||
imageActions: {
|
|
||||||
selectExisting: true,
|
|
||||||
uploadFile: true,
|
|
||||||
deleteFile: true
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
left.push([
|
|
||||||
{
|
{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
name: 'description',
|
name: 'description',
|
||||||
label: t`Description`,
|
label: t`Description`,
|
||||||
copy: true
|
copy: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
name: 'variant_of',
|
||||||
|
label: t`Variant of`,
|
||||||
|
model: ModelType.part,
|
||||||
|
hidden: !part.variant_of
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
name: 'category',
|
||||||
|
label: t`Category`,
|
||||||
|
model: ModelType.partcategory
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
name: 'default_location',
|
||||||
|
label: t`Default Location`,
|
||||||
|
model: ModelType.stocklocation,
|
||||||
|
hidden: !part.default_location
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
name: 'IPN',
|
||||||
|
label: t`IPN`,
|
||||||
|
copy: true,
|
||||||
|
hidden: !part.IPN
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
name: 'revision',
|
||||||
|
label: t`Revision`,
|
||||||
|
copy: true,
|
||||||
|
hidden: !part.revision
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
name: 'units',
|
||||||
|
label: t`Units`,
|
||||||
|
copy: true,
|
||||||
|
hidden: !part.units
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
name: 'keywords',
|
||||||
|
label: t`Keywords`,
|
||||||
|
copy: true,
|
||||||
|
hidden: !part.keywords
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
name: 'link',
|
||||||
|
label: t`Link`,
|
||||||
|
external: true,
|
||||||
|
copy: true,
|
||||||
|
hidden: !part.link
|
||||||
}
|
}
|
||||||
]);
|
];
|
||||||
|
|
||||||
if (part.variant_of) {
|
let tr: DetailsField[] = [
|
||||||
left.push([
|
|
||||||
{
|
|
||||||
type: 'link',
|
|
||||||
name: 'variant_of',
|
|
||||||
label: t`Variant of`,
|
|
||||||
model: ModelType.part
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
right.push([
|
|
||||||
{
|
{
|
||||||
type: 'string',
|
type: 'string',
|
||||||
name: 'unallocated_stock',
|
name: 'unallocated_stock',
|
||||||
unit: true,
|
unit: true,
|
||||||
label: t`Available Stock`
|
label: t`Available Stock`
|
||||||
}
|
},
|
||||||
]);
|
|
||||||
|
|
||||||
right.push([
|
|
||||||
{
|
{
|
||||||
type: 'string',
|
type: 'string',
|
||||||
name: 'total_in_stock',
|
name: 'total_in_stock',
|
||||||
unit: true,
|
unit: true,
|
||||||
label: t`In Stock`
|
label: t`In Stock`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
name: 'minimum_stock',
|
||||||
|
unit: true,
|
||||||
|
label: t`Minimum Stock`,
|
||||||
|
hidden: part.minimum_stock <= 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
name: 'ordering',
|
||||||
|
label: t`On order`,
|
||||||
|
unit: true,
|
||||||
|
hidden: part.ordering <= 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'progressbar',
|
||||||
|
name: 'allocated_to_build_orders',
|
||||||
|
total: part.required_for_build_orders,
|
||||||
|
progress: part.allocated_to_build_orders,
|
||||||
|
label: t`Allocated to Build Orders`,
|
||||||
|
hidden:
|
||||||
|
!part.assembly ||
|
||||||
|
(part.allocated_to_build_orders <= 0 &&
|
||||||
|
part.required_for_build_orders <= 0)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'progressbar',
|
||||||
|
name: 'allocated_to_sales_orders',
|
||||||
|
total: part.required_for_sales_orders,
|
||||||
|
progress: part.allocated_to_sales_orders,
|
||||||
|
label: t`Allocated to Sales Orders`,
|
||||||
|
hidden:
|
||||||
|
!part.salable ||
|
||||||
|
(part.allocated_to_sales_orders <= 0 &&
|
||||||
|
part.required_for_sales_orders <= 0)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
name: 'can_build',
|
||||||
|
unit: true,
|
||||||
|
label: t`Can Build`,
|
||||||
|
hidden: !part.assembly
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
name: 'building',
|
||||||
|
unit: true,
|
||||||
|
label: t`Building`,
|
||||||
|
hidden: !part.assembly
|
||||||
}
|
}
|
||||||
]);
|
];
|
||||||
|
|
||||||
if (part.minimum_stock) {
|
let bl: DetailsField[] = [
|
||||||
right.push([
|
{
|
||||||
{
|
type: 'boolean',
|
||||||
type: 'string',
|
name: 'active',
|
||||||
name: 'minimum_stock',
|
label: t`Active`
|
||||||
unit: true,
|
},
|
||||||
label: t`Minimum Stock`
|
{
|
||||||
}
|
type: 'boolean',
|
||||||
]);
|
name: 'template',
|
||||||
}
|
label: t`Template Part`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'boolean',
|
||||||
|
name: 'assembly',
|
||||||
|
label: t`Assembled Part`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'boolean',
|
||||||
|
name: 'component',
|
||||||
|
label: t`Component Part`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'boolean',
|
||||||
|
name: 'trackable',
|
||||||
|
label: t`Trackable Part`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'boolean',
|
||||||
|
name: 'purchaseable',
|
||||||
|
label: t`Purchaseable Part`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'boolean',
|
||||||
|
name: 'saleable',
|
||||||
|
label: t`Saleable Part`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'boolean',
|
||||||
|
name: 'virtual',
|
||||||
|
label: t`Virtual Part`
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
if (part.ordering <= 0) {
|
let br: DetailsField[] = [
|
||||||
right.push([
|
|
||||||
{
|
|
||||||
type: 'string',
|
|
||||||
name: 'ordering',
|
|
||||||
label: t`On order`,
|
|
||||||
unit: true
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
part.assembly &&
|
|
||||||
(part.allocated_to_build_orders > 0 || part.required_for_build_orders > 0)
|
|
||||||
) {
|
|
||||||
right.push([
|
|
||||||
{
|
|
||||||
type: 'progressbar',
|
|
||||||
name: 'allocated_to_build_orders',
|
|
||||||
total: part.required_for_build_orders,
|
|
||||||
progress: part.allocated_to_build_orders,
|
|
||||||
label: t`Allocated to Build Orders`
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
part.salable &&
|
|
||||||
(part.allocated_to_sales_orders > 0 || part.required_for_sales_orders > 0)
|
|
||||||
) {
|
|
||||||
right.push([
|
|
||||||
{
|
|
||||||
type: 'progressbar',
|
|
||||||
name: 'allocated_to_sales_orders',
|
|
||||||
total: part.required_for_sales_orders,
|
|
||||||
progress: part.allocated_to_sales_orders,
|
|
||||||
label: t`Allocated to Sales Orders`
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (part.assembly) {
|
|
||||||
right.push([
|
|
||||||
{
|
|
||||||
type: 'string',
|
|
||||||
name: 'can_build',
|
|
||||||
unit: true,
|
|
||||||
label: t`Can Build`
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (part.assembly) {
|
|
||||||
right.push([
|
|
||||||
{
|
|
||||||
type: 'string',
|
|
||||||
name: 'building',
|
|
||||||
unit: true,
|
|
||||||
label: t`Building`
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (part.category) {
|
|
||||||
bottom_left.push([
|
|
||||||
{
|
|
||||||
type: 'link',
|
|
||||||
name: 'category',
|
|
||||||
label: t`Category`,
|
|
||||||
model: ModelType.partcategory
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (part.IPN) {
|
|
||||||
bottom_left.push([
|
|
||||||
{
|
|
||||||
type: 'string',
|
|
||||||
name: 'IPN',
|
|
||||||
label: t`IPN`,
|
|
||||||
copy: true
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (part.revision) {
|
|
||||||
bottom_left.push([
|
|
||||||
{
|
|
||||||
type: 'string',
|
|
||||||
name: 'revision',
|
|
||||||
label: t`Revision`,
|
|
||||||
copy: true
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (part.units) {
|
|
||||||
bottom_left.push([
|
|
||||||
{
|
|
||||||
type: 'string',
|
|
||||||
name: 'units',
|
|
||||||
label: t`Units`
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (part.keywords) {
|
|
||||||
bottom_left.push([
|
|
||||||
{
|
|
||||||
type: 'string',
|
|
||||||
name: 'keywords',
|
|
||||||
label: t`Keywords`,
|
|
||||||
copy: true
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
bottom_right.push([
|
|
||||||
{
|
{
|
||||||
type: 'string',
|
type: 'string',
|
||||||
name: 'creation_date',
|
name: 'creation_date',
|
||||||
@ -283,181 +286,169 @@ export default function PartDetail() {
|
|||||||
{
|
{
|
||||||
type: 'string',
|
type: 'string',
|
||||||
name: 'creation_user',
|
name: 'creation_user',
|
||||||
badge: 'user'
|
label: t`Created By`,
|
||||||
|
badge: 'user',
|
||||||
|
icon: 'user'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
name: 'responsible',
|
||||||
|
label: t`Responsible`,
|
||||||
|
badge: 'owner',
|
||||||
|
hidden: !part.responsible
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
name: 'default_supplier',
|
||||||
|
label: t`Default Supplier`,
|
||||||
|
model: ModelType.supplierpart,
|
||||||
|
hidden: !part.default_supplier
|
||||||
}
|
}
|
||||||
]);
|
];
|
||||||
|
|
||||||
|
// Add in price range data
|
||||||
id &&
|
id &&
|
||||||
bottom_right.push([
|
br.push({
|
||||||
{
|
type: 'string',
|
||||||
type: 'string',
|
name: 'pricing',
|
||||||
name: 'pricing',
|
label: t`Price Range`,
|
||||||
label: t`Price Range`,
|
value_formatter: () => {
|
||||||
value_formatter: () => {
|
const { data } = useSuspenseQuery({
|
||||||
const { data } = useSuspenseQuery({
|
queryKey: ['pricing', id],
|
||||||
queryKey: ['pricing', id],
|
queryFn: async () => {
|
||||||
queryFn: async () => {
|
const url = apiUrl(ApiEndpoints.part_pricing_get, null, {
|
||||||
const url = apiUrl(ApiEndpoints.part_pricing_get, null, {
|
id: id
|
||||||
id: id
|
});
|
||||||
|
|
||||||
|
return api
|
||||||
|
.get(url)
|
||||||
|
.then((response) => {
|
||||||
|
switch (response.status) {
|
||||||
|
case 200:
|
||||||
|
return response.data;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
return null;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return `${formatPriceRange(data.overall_min, data.overall_max)}${
|
||||||
|
part.units && ' / ' + part.units
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return api
|
// Add in stocktake information
|
||||||
.get(url)
|
if (id && part.last_stocktake) {
|
||||||
.then((response) => {
|
br.push({
|
||||||
switch (response.status) {
|
type: 'string',
|
||||||
case 200:
|
name: 'stocktake',
|
||||||
return response.data;
|
label: t`Last Stocktake`,
|
||||||
default:
|
unit: true,
|
||||||
return null;
|
value_formatter: () => {
|
||||||
}
|
const { data } = useSuspenseQuery({
|
||||||
})
|
queryKey: ['stocktake', id],
|
||||||
.catch(() => {
|
queryFn: async () => {
|
||||||
return null;
|
const url = apiUrl(ApiEndpoints.part_stocktake_list);
|
||||||
});
|
|
||||||
}
|
return api
|
||||||
});
|
.get(url, { params: { part: id, ordering: 'date' } })
|
||||||
return `${formatPriceRange(data.overall_min, data.overall_max)}${
|
.then((response) => {
|
||||||
part.units && ' / ' + part.units
|
switch (response.status) {
|
||||||
}`;
|
case 200:
|
||||||
|
return response.data[response.data.length - 1];
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data.quantity) {
|
||||||
|
return `${data.quantity} (${data.date})`;
|
||||||
|
} else {
|
||||||
|
return '-';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]);
|
});
|
||||||
|
|
||||||
id &&
|
br.push({
|
||||||
part.last_stocktake &&
|
type: 'string',
|
||||||
bottom_right.push([
|
name: 'stocktake_user',
|
||||||
{
|
label: t`Stocktake By`,
|
||||||
type: 'string',
|
badge: 'user',
|
||||||
name: 'stocktake',
|
icon: 'user',
|
||||||
label: t`Last Stocktake`,
|
value_formatter: () => {
|
||||||
unit: true,
|
const { data } = useSuspenseQuery({
|
||||||
value_formatter: () => {
|
queryKey: ['stocktake', id],
|
||||||
const { data } = useSuspenseQuery({
|
queryFn: async () => {
|
||||||
queryKey: ['stocktake', id],
|
const url = apiUrl(ApiEndpoints.part_stocktake_list);
|
||||||
queryFn: async () => {
|
|
||||||
const url = apiUrl(ApiEndpoints.part_stocktake_list);
|
|
||||||
|
|
||||||
return api
|
return api
|
||||||
.get(url, { params: { part: id, ordering: 'date' } })
|
.get(url, { params: { part: id, ordering: 'date' } })
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
switch (response.status) {
|
switch (response.status) {
|
||||||
case 200:
|
case 200:
|
||||||
return response.data[response.data.length - 1];
|
return response.data[response.data.length - 1];
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return data?.quantity;
|
return data?.user;
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'string',
|
|
||||||
name: 'stocktake_user',
|
|
||||||
badge: 'user',
|
|
||||||
value_formatter: () => {
|
|
||||||
const { data } = useSuspenseQuery({
|
|
||||||
queryKey: ['stocktake', id],
|
|
||||||
queryFn: async () => {
|
|
||||||
const url = apiUrl(ApiEndpoints.part_stocktake_list);
|
|
||||||
|
|
||||||
return api
|
|
||||||
.get(url, { params: { part: id, ordering: 'date' } })
|
|
||||||
.then((response) => {
|
|
||||||
switch (response.status) {
|
|
||||||
case 200:
|
|
||||||
return response.data[response.data.length - 1];
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return data?.user;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]);
|
});
|
||||||
|
|
||||||
if (part.default_location) {
|
|
||||||
bottom_right.push([
|
|
||||||
{
|
|
||||||
type: 'link',
|
|
||||||
name: 'default_location',
|
|
||||||
label: t`Default Location`,
|
|
||||||
model: ModelType.stocklocation
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (part.default_supplier) {
|
return (
|
||||||
bottom_right.push([
|
<ItemDetailsGrid>
|
||||||
{
|
<Grid>
|
||||||
type: 'link',
|
<Grid.Col span={4}>
|
||||||
name: 'default_supplier',
|
<DetailsImage
|
||||||
label: t`Default Supplier`,
|
appRole={UserRoles.part}
|
||||||
model: ModelType.supplierpart
|
imageActions={{
|
||||||
}
|
selectExisting: true,
|
||||||
]);
|
uploadFile: true,
|
||||||
}
|
deleteFile: true
|
||||||
|
}}
|
||||||
if (part.link) {
|
src={part.image}
|
||||||
bottom_right.push([
|
apiPath={apiUrl(ApiEndpoints.part_list, part.pk)}
|
||||||
{
|
refresh={refreshInstance}
|
||||||
type: 'link',
|
pk={part.pk}
|
||||||
name: 'link',
|
/>
|
||||||
label: t`Link`,
|
</Grid.Col>
|
||||||
external: true,
|
<Grid.Col span={8}>
|
||||||
copy: true
|
<Stack spacing="xs">
|
||||||
}
|
<PartIcons part={part} />
|
||||||
]);
|
<DetailsTable fields={tl} item={part} />
|
||||||
}
|
</Stack>
|
||||||
|
</Grid.Col>
|
||||||
if (part.responsible) {
|
</Grid>
|
||||||
bottom_right.push([
|
<DetailsTable fields={tr} item={part} />
|
||||||
{
|
<DetailsTable fields={bl} item={part} />
|
||||||
type: 'string',
|
<DetailsTable fields={br} item={part} />
|
||||||
name: 'responsible',
|
</ItemDetailsGrid>
|
||||||
label: t`Responsible`,
|
);
|
||||||
badge: 'owner'
|
}, [part, instanceQuery]);
|
||||||
}
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
let fields: ItemDetailFields = {
|
|
||||||
left: left,
|
|
||||||
right: right,
|
|
||||||
bottom_left: bottom_left,
|
|
||||||
bottom_right: bottom_right,
|
|
||||||
image: image
|
|
||||||
};
|
|
||||||
|
|
||||||
return fields;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Part data panels (recalculate when part data changes)
|
// Part data panels (recalculate when part data changes)
|
||||||
const partPanels: PanelType[] = useMemo(() => {
|
const partPanels: PanelType[] = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: 'details',
|
name: 'details',
|
||||||
label: t`Details`,
|
label: t`Part Details`,
|
||||||
icon: <IconInfoCircle />,
|
icon: <IconInfoCircle />,
|
||||||
content: !instanceQuery.isFetching && (
|
content: detailsPanel
|
||||||
<ItemDetails
|
|
||||||
appRole={UserRoles.part}
|
|
||||||
params={part}
|
|
||||||
apiPath={apiUrl(ApiEndpoints.part_list, part.pk)}
|
|
||||||
refresh={refreshInstance}
|
|
||||||
fields={detailFields(part)}
|
|
||||||
partModel
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'parameters',
|
name: 'parameters',
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { LoadingOverlay, Stack } from '@mantine/core';
|
import { Grid, LoadingOverlay, Skeleton, Stack } from '@mantine/core';
|
||||||
import {
|
import {
|
||||||
IconDots,
|
IconDots,
|
||||||
IconInfoCircle,
|
IconInfoCircle,
|
||||||
@ -11,6 +11,9 @@ import {
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { DetailsField, DetailsTable } from '../../components/details/Details';
|
||||||
|
import { DetailsImage } from '../../components/details/DetailsImage';
|
||||||
|
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
||||||
import {
|
import {
|
||||||
ActionDropdown,
|
ActionDropdown,
|
||||||
BarcodeActionDropdown,
|
BarcodeActionDropdown,
|
||||||
@ -24,6 +27,10 @@ import { PageDetail } from '../../components/nav/PageDetail';
|
|||||||
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
|
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
|
||||||
import { NotesEditor } from '../../components/widgets/MarkdownEditor';
|
import { NotesEditor } from '../../components/widgets/MarkdownEditor';
|
||||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||||
|
import { ModelType } from '../../enums/ModelType';
|
||||||
|
import { UserRoles } from '../../enums/Roles';
|
||||||
|
import { purchaseOrderFields } from '../../forms/PurchaseOrderForms';
|
||||||
|
import { useEditApiFormModal } from '../../hooks/UseForm';
|
||||||
import { useInstance } from '../../hooks/UseInstance';
|
import { useInstance } from '../../hooks/UseInstance';
|
||||||
import { apiUrl } from '../../states/ApiState';
|
import { apiUrl } from '../../states/ApiState';
|
||||||
import { useUserState } from '../../states/UserState';
|
import { useUserState } from '../../states/UserState';
|
||||||
@ -39,7 +46,11 @@ export default function PurchaseOrderDetail() {
|
|||||||
|
|
||||||
const user = useUserState();
|
const user = useUserState();
|
||||||
|
|
||||||
const { instance: order, instanceQuery } = useInstance({
|
const {
|
||||||
|
instance: order,
|
||||||
|
instanceQuery,
|
||||||
|
refreshInstance
|
||||||
|
} = useInstance({
|
||||||
endpoint: ApiEndpoints.purchase_order_list,
|
endpoint: ApiEndpoints.purchase_order_list,
|
||||||
pk: id,
|
pk: id,
|
||||||
params: {
|
params: {
|
||||||
@ -48,12 +59,167 @@ export default function PurchaseOrderDetail() {
|
|||||||
refetchOnMount: true
|
refetchOnMount: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const editPurchaseOrder = useEditApiFormModal({
|
||||||
|
url: ApiEndpoints.purchase_order_list,
|
||||||
|
pk: id,
|
||||||
|
title: t`Edit Purchase Order`,
|
||||||
|
fields: purchaseOrderFields(),
|
||||||
|
onFormSuccess: () => {
|
||||||
|
refreshInstance();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const detailsPanel = useMemo(() => {
|
||||||
|
if (instanceQuery.isFetching) {
|
||||||
|
return <Skeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
let tl: DetailsField[] = [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'reference',
|
||||||
|
label: t`Reference`,
|
||||||
|
copy: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'supplier_reference',
|
||||||
|
label: t`Supplier Reference`,
|
||||||
|
icon: 'reference',
|
||||||
|
hidden: !order.supplier_reference,
|
||||||
|
copy: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
name: 'supplier',
|
||||||
|
icon: 'suppliers',
|
||||||
|
label: t`Supplier`,
|
||||||
|
model: ModelType.company
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'description',
|
||||||
|
label: t`Description`,
|
||||||
|
copy: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'status',
|
||||||
|
name: 'status',
|
||||||
|
label: t`Status`,
|
||||||
|
model: ModelType.purchaseorder
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
let tr: DetailsField[] = [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'line_items',
|
||||||
|
label: t`Line Items`,
|
||||||
|
icon: 'list'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'progressbar',
|
||||||
|
name: 'completed',
|
||||||
|
icon: 'progress',
|
||||||
|
label: t`Completed Line Items`,
|
||||||
|
total: order.line_items,
|
||||||
|
progress: order.completed_lines
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'progressbar',
|
||||||
|
name: 'shipments',
|
||||||
|
icon: 'shipment',
|
||||||
|
label: t`Completed Shipments`,
|
||||||
|
total: order.shipments,
|
||||||
|
progress: order.completed_shipments
|
||||||
|
// TODO: Fix this progress bar
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'currency',
|
||||||
|
label: t`Order Currency,`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'total_cost',
|
||||||
|
label: t`Total Cost`
|
||||||
|
// TODO: Implement this!
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
let bl: DetailsField[] = [
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
external: true,
|
||||||
|
name: 'link',
|
||||||
|
label: t`Link`,
|
||||||
|
copy: true,
|
||||||
|
hidden: !order.link
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
model: ModelType.contact,
|
||||||
|
link: false,
|
||||||
|
name: 'contact',
|
||||||
|
label: t`Contact`,
|
||||||
|
icon: 'user',
|
||||||
|
copy: true
|
||||||
|
}
|
||||||
|
// TODO: Project code
|
||||||
|
];
|
||||||
|
|
||||||
|
let br: DetailsField[] = [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'creation_date',
|
||||||
|
label: t`Created On`,
|
||||||
|
icon: 'calendar'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'target_date',
|
||||||
|
label: t`Target Date`,
|
||||||
|
icon: 'calendar',
|
||||||
|
hidden: !order.target_date
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'responsible',
|
||||||
|
label: t`Responsible`,
|
||||||
|
badge: 'owner',
|
||||||
|
hidden: !order.responsible
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ItemDetailsGrid>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<DetailsImage
|
||||||
|
appRole={UserRoles.purchase_order}
|
||||||
|
apiPath={ApiEndpoints.company_list}
|
||||||
|
src={order.supplier_detail?.image}
|
||||||
|
pk={order.supplier}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={8}>
|
||||||
|
<DetailsTable fields={tl} item={order} />
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
<DetailsTable fields={tr} item={order} />
|
||||||
|
<DetailsTable fields={bl} item={order} />
|
||||||
|
<DetailsTable fields={br} item={order} />
|
||||||
|
</ItemDetailsGrid>
|
||||||
|
);
|
||||||
|
}, [order, instanceQuery]);
|
||||||
|
|
||||||
const orderPanels: PanelType[] = useMemo(() => {
|
const orderPanels: PanelType[] = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: 'detail',
|
name: 'detail',
|
||||||
label: t`Order Details`,
|
label: t`Order Details`,
|
||||||
icon: <IconInfoCircle />
|
icon: <IconInfoCircle />,
|
||||||
|
content: detailsPanel
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'line-items',
|
name: 'line-items',
|
||||||
@ -118,13 +284,21 @@ export default function PurchaseOrderDetail() {
|
|||||||
key="order-actions"
|
key="order-actions"
|
||||||
tooltip={t`Order Actions`}
|
tooltip={t`Order Actions`}
|
||||||
icon={<IconDots />}
|
icon={<IconDots />}
|
||||||
actions={[EditItemAction({}), DeleteItemAction({})]}
|
actions={[
|
||||||
|
EditItemAction({
|
||||||
|
onClick: () => {
|
||||||
|
editPurchaseOrder.open();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
DeleteItemAction({})
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
];
|
];
|
||||||
}, [id, order, user]);
|
}, [id, order, user]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{editPurchaseOrder.modal}
|
||||||
<Stack spacing="xs">
|
<Stack spacing="xs">
|
||||||
<LoadingOverlay visible={instanceQuery.isFetching} />
|
<LoadingOverlay visible={instanceQuery.isFetching} />
|
||||||
<PageDetail
|
<PageDetail
|
||||||
|
@ -1,13 +1,23 @@
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { LoadingOverlay, Stack } from '@mantine/core';
|
import { Grid, LoadingOverlay, Skeleton, Stack } from '@mantine/core';
|
||||||
import { IconInfoCircle, IconNotes, IconPaperclip } from '@tabler/icons-react';
|
import {
|
||||||
|
IconInfoCircle,
|
||||||
|
IconList,
|
||||||
|
IconNotes,
|
||||||
|
IconPaperclip
|
||||||
|
} from '@tabler/icons-react';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { DetailsField, DetailsTable } from '../../components/details/Details';
|
||||||
|
import { DetailsImage } from '../../components/details/DetailsImage';
|
||||||
|
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
||||||
import { PageDetail } from '../../components/nav/PageDetail';
|
import { PageDetail } from '../../components/nav/PageDetail';
|
||||||
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
|
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
|
||||||
import { NotesEditor } from '../../components/widgets/MarkdownEditor';
|
import { NotesEditor } from '../../components/widgets/MarkdownEditor';
|
||||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||||
|
import { ModelType } from '../../enums/ModelType';
|
||||||
|
import { UserRoles } from '../../enums/Roles';
|
||||||
import { useInstance } from '../../hooks/UseInstance';
|
import { useInstance } from '../../hooks/UseInstance';
|
||||||
import { apiUrl } from '../../states/ApiState';
|
import { apiUrl } from '../../states/ApiState';
|
||||||
import { AttachmentTable } from '../../tables/general/AttachmentTable';
|
import { AttachmentTable } from '../../tables/general/AttachmentTable';
|
||||||
@ -26,12 +36,161 @@ export default function ReturnOrderDetail() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const detailsPanel = useMemo(() => {
|
||||||
|
if (instanceQuery.isFetching) {
|
||||||
|
return <Skeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
let tl: DetailsField[] = [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'reference',
|
||||||
|
label: t`Reference`,
|
||||||
|
copy: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'customer_reference',
|
||||||
|
label: t`Customer Reference`,
|
||||||
|
copy: true,
|
||||||
|
hidden: !order.customer_reference
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
name: 'customer',
|
||||||
|
icon: 'customers',
|
||||||
|
label: t`Customer`,
|
||||||
|
model: ModelType.company
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'description',
|
||||||
|
label: t`Description`,
|
||||||
|
copy: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'status',
|
||||||
|
name: 'status',
|
||||||
|
label: t`Status`,
|
||||||
|
model: ModelType.salesorder
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
let tr: DetailsField[] = [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'line_items',
|
||||||
|
label: t`Line Items`,
|
||||||
|
icon: 'list'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'progressbar',
|
||||||
|
name: 'completed',
|
||||||
|
icon: 'progress',
|
||||||
|
label: t`Completed Line Items`,
|
||||||
|
total: order.line_items,
|
||||||
|
progress: order.completed_lines
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'progressbar',
|
||||||
|
name: 'shipments',
|
||||||
|
icon: 'shipment',
|
||||||
|
label: t`Completed Shipments`,
|
||||||
|
total: order.shipments,
|
||||||
|
progress: order.completed_shipments
|
||||||
|
// TODO: Fix this progress bar
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'currency',
|
||||||
|
label: t`Order Currency,`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'total_cost',
|
||||||
|
label: t`Total Cost`
|
||||||
|
// TODO: Implement this!
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
let bl: DetailsField[] = [
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
external: true,
|
||||||
|
name: 'link',
|
||||||
|
label: t`Link`,
|
||||||
|
copy: true,
|
||||||
|
hidden: !order.link
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
model: ModelType.contact,
|
||||||
|
link: false,
|
||||||
|
name: 'contact',
|
||||||
|
label: t`Contact`,
|
||||||
|
icon: 'user',
|
||||||
|
copy: true
|
||||||
|
}
|
||||||
|
// TODO: Project code
|
||||||
|
];
|
||||||
|
|
||||||
|
let br: DetailsField[] = [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'creation_date',
|
||||||
|
label: t`Created On`,
|
||||||
|
icon: 'calendar'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'target_date',
|
||||||
|
label: t`Target Date`,
|
||||||
|
icon: 'calendar',
|
||||||
|
hidden: !order.target_date
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'responsible',
|
||||||
|
label: t`Responsible`,
|
||||||
|
badge: 'owner',
|
||||||
|
hidden: !order.responsible
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ItemDetailsGrid>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<DetailsImage
|
||||||
|
appRole={UserRoles.purchase_order}
|
||||||
|
apiPath={ApiEndpoints.company_list}
|
||||||
|
src={order.customer_detail?.image}
|
||||||
|
pk={order.customer}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={8}>
|
||||||
|
<DetailsTable fields={tl} item={order} />
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
<DetailsTable fields={tr} item={order} />
|
||||||
|
<DetailsTable fields={bl} item={order} />
|
||||||
|
<DetailsTable fields={br} item={order} />
|
||||||
|
</ItemDetailsGrid>
|
||||||
|
);
|
||||||
|
}, [order, instanceQuery]);
|
||||||
|
|
||||||
const orderPanels: PanelType[] = useMemo(() => {
|
const orderPanels: PanelType[] = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: 'detail',
|
name: 'detail',
|
||||||
label: t`Order Details`,
|
label: t`Order Details`,
|
||||||
icon: <IconInfoCircle />
|
icon: <IconInfoCircle />,
|
||||||
|
content: detailsPanel
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'line-items',
|
||||||
|
label: t`Line Items`,
|
||||||
|
icon: <IconList />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'attachments',
|
name: 'attachments',
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { LoadingOverlay, Skeleton, Stack } from '@mantine/core';
|
import { Grid, LoadingOverlay, Skeleton, Stack } from '@mantine/core';
|
||||||
import {
|
import {
|
||||||
IconInfoCircle,
|
IconInfoCircle,
|
||||||
IconList,
|
IconList,
|
||||||
@ -12,10 +12,15 @@ import {
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { DetailsField, DetailsTable } from '../../components/details/Details';
|
||||||
|
import { DetailsImage } from '../../components/details/DetailsImage';
|
||||||
|
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
||||||
import { PageDetail } from '../../components/nav/PageDetail';
|
import { PageDetail } from '../../components/nav/PageDetail';
|
||||||
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
|
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
|
||||||
import { NotesEditor } from '../../components/widgets/MarkdownEditor';
|
import { NotesEditor } from '../../components/widgets/MarkdownEditor';
|
||||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||||
|
import { ModelType } from '../../enums/ModelType';
|
||||||
|
import { UserRoles } from '../../enums/Roles';
|
||||||
import { useInstance } from '../../hooks/UseInstance';
|
import { useInstance } from '../../hooks/UseInstance';
|
||||||
import { apiUrl } from '../../states/ApiState';
|
import { apiUrl } from '../../states/ApiState';
|
||||||
import { BuildOrderTable } from '../../tables/build/BuildOrderTable';
|
import { BuildOrderTable } from '../../tables/build/BuildOrderTable';
|
||||||
@ -35,12 +40,156 @@ export default function SalesOrderDetail() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const detailsPanel = useMemo(() => {
|
||||||
|
if (instanceQuery.isFetching) {
|
||||||
|
return <Skeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
let tl: DetailsField[] = [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'reference',
|
||||||
|
label: t`Reference`,
|
||||||
|
copy: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'customer_reference',
|
||||||
|
label: t`Customer Reference`,
|
||||||
|
copy: true,
|
||||||
|
hidden: !order.customer_reference
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
name: 'customer',
|
||||||
|
icon: 'customers',
|
||||||
|
label: t`Customer`,
|
||||||
|
model: ModelType.company
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'description',
|
||||||
|
label: t`Description`,
|
||||||
|
copy: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'status',
|
||||||
|
name: 'status',
|
||||||
|
label: t`Status`,
|
||||||
|
model: ModelType.salesorder
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
let tr: DetailsField[] = [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'line_items',
|
||||||
|
label: t`Line Items`,
|
||||||
|
icon: 'list'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'progressbar',
|
||||||
|
name: 'completed',
|
||||||
|
icon: 'progress',
|
||||||
|
label: t`Completed Line Items`,
|
||||||
|
total: order.line_items,
|
||||||
|
progress: order.completed_lines
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'progressbar',
|
||||||
|
name: 'shipments',
|
||||||
|
icon: 'shipment',
|
||||||
|
label: t`Completed Shipments`,
|
||||||
|
total: order.shipments,
|
||||||
|
progress: order.completed_shipments
|
||||||
|
// TODO: Fix this progress bar
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'currency',
|
||||||
|
label: t`Order Currency,`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'total_cost',
|
||||||
|
label: t`Total Cost`
|
||||||
|
// TODO: Implement this!
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
let bl: DetailsField[] = [
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
external: true,
|
||||||
|
name: 'link',
|
||||||
|
label: t`Link`,
|
||||||
|
copy: true,
|
||||||
|
hidden: !order.link
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
model: ModelType.contact,
|
||||||
|
link: false,
|
||||||
|
name: 'contact',
|
||||||
|
label: t`Contact`,
|
||||||
|
icon: 'user',
|
||||||
|
copy: true
|
||||||
|
}
|
||||||
|
// TODO: Project code
|
||||||
|
];
|
||||||
|
|
||||||
|
let br: DetailsField[] = [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'creation_date',
|
||||||
|
label: t`Created On`,
|
||||||
|
icon: 'calendar'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'target_date',
|
||||||
|
label: t`Target Date`,
|
||||||
|
icon: 'calendar',
|
||||||
|
hidden: !order.target_date
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'responsible',
|
||||||
|
label: t`Responsible`,
|
||||||
|
badge: 'owner',
|
||||||
|
hidden: !order.responsible
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ItemDetailsGrid>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<DetailsImage
|
||||||
|
appRole={UserRoles.purchase_order}
|
||||||
|
apiPath={ApiEndpoints.company_list}
|
||||||
|
src={order.customer_detail?.image}
|
||||||
|
pk={order.customer}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={8}>
|
||||||
|
<DetailsTable fields={tl} item={order} />
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
<DetailsTable fields={tr} item={order} />
|
||||||
|
<DetailsTable fields={bl} item={order} />
|
||||||
|
<DetailsTable fields={br} item={order} />
|
||||||
|
</ItemDetailsGrid>
|
||||||
|
);
|
||||||
|
}, [order, instanceQuery]);
|
||||||
|
|
||||||
const orderPanels: PanelType[] = useMemo(() => {
|
const orderPanels: PanelType[] = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: 'detail',
|
name: 'detail',
|
||||||
label: t`Order Details`,
|
label: t`Order Details`,
|
||||||
icon: <IconInfoCircle />
|
icon: <IconInfoCircle />,
|
||||||
|
content: detailsPanel
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'line-items',
|
name: 'line-items',
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { LoadingOverlay, Stack, Text } from '@mantine/core';
|
import { LoadingOverlay, Skeleton, Stack, Text } from '@mantine/core';
|
||||||
import { IconPackages, IconSitemap } from '@tabler/icons-react';
|
import { IconInfoCircle, IconPackages, IconSitemap } from '@tabler/icons-react';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { DetailsField, DetailsTable } from '../../components/details/Details';
|
||||||
|
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
||||||
import { PageDetail } from '../../components/nav/PageDetail';
|
import { PageDetail } from '../../components/nav/PageDetail';
|
||||||
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
|
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
|
||||||
import { StockLocationTree } from '../../components/nav/StockLocationTree';
|
import { StockLocationTree } from '../../components/nav/StockLocationTree';
|
||||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||||
|
import { ModelType } from '../../enums/ModelType';
|
||||||
import { useInstance } from '../../hooks/UseInstance';
|
import { useInstance } from '../../hooks/UseInstance';
|
||||||
import { StockItemTable } from '../../tables/stock/StockItemTable';
|
import { StockItemTable } from '../../tables/stock/StockItemTable';
|
||||||
import { StockLocationTable } from '../../tables/stock/StockLocationTable';
|
import { StockLocationTable } from '../../tables/stock/StockLocationTable';
|
||||||
@ -35,8 +38,90 @@ export default function Stock() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const detailsPanel = useMemo(() => {
|
||||||
|
if (id && instanceQuery.isFetching) {
|
||||||
|
return <Skeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
let left: DetailsField[] = [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'name',
|
||||||
|
label: t`Name`,
|
||||||
|
copy: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'pathstring',
|
||||||
|
label: t`Path`,
|
||||||
|
icon: 'sitemap',
|
||||||
|
copy: true,
|
||||||
|
hidden: !id
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'description',
|
||||||
|
label: t`Description`,
|
||||||
|
copy: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
name: 'parent',
|
||||||
|
model_field: 'name',
|
||||||
|
icon: 'location',
|
||||||
|
label: t`Parent Location`,
|
||||||
|
model: ModelType.stocklocation,
|
||||||
|
hidden: !location?.parent
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
let right: DetailsField[] = [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'items',
|
||||||
|
icon: 'stock',
|
||||||
|
label: t`Stock Items`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'sublocations',
|
||||||
|
icon: 'location',
|
||||||
|
label: t`Sublocations`,
|
||||||
|
hidden: !location?.sublocations
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'boolean',
|
||||||
|
name: 'structural',
|
||||||
|
label: t`Structural`,
|
||||||
|
icon: 'sitemap'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'boolean',
|
||||||
|
name: 'external',
|
||||||
|
label: t`External`
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ItemDetailsGrid>
|
||||||
|
{id && location?.pk ? (
|
||||||
|
<DetailsTable item={location} fields={left} />
|
||||||
|
) : (
|
||||||
|
<Text>{t`Top level stock location`}</Text>
|
||||||
|
)}
|
||||||
|
{id && location?.pk && <DetailsTable item={location} fields={right} />}
|
||||||
|
</ItemDetailsGrid>
|
||||||
|
);
|
||||||
|
}, [location, instanceQuery]);
|
||||||
|
|
||||||
const locationPanels: PanelType[] = useMemo(() => {
|
const locationPanels: PanelType[] = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
|
{
|
||||||
|
name: 'details',
|
||||||
|
label: t`Location Details`,
|
||||||
|
icon: <IconInfoCircle />,
|
||||||
|
content: detailsPanel
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'stock-items',
|
name: 'stock-items',
|
||||||
label: t`Stock Items`,
|
label: t`Stock Items`,
|
||||||
|
@ -1,5 +1,12 @@
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Alert, LoadingOverlay, Skeleton, Stack, Text } from '@mantine/core';
|
import {
|
||||||
|
Alert,
|
||||||
|
Grid,
|
||||||
|
LoadingOverlay,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Text
|
||||||
|
} from '@mantine/core';
|
||||||
import {
|
import {
|
||||||
IconBookmark,
|
IconBookmark,
|
||||||
IconBoxPadding,
|
IconBoxPadding,
|
||||||
@ -20,6 +27,9 @@ import {
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { DetailsField, DetailsTable } from '../../components/details/Details';
|
||||||
|
import { DetailsImage } from '../../components/details/DetailsImage';
|
||||||
|
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
||||||
import {
|
import {
|
||||||
ActionDropdown,
|
ActionDropdown,
|
||||||
BarcodeActionDropdown,
|
BarcodeActionDropdown,
|
||||||
@ -34,6 +44,8 @@ import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
|
|||||||
import { StockLocationTree } from '../../components/nav/StockLocationTree';
|
import { StockLocationTree } from '../../components/nav/StockLocationTree';
|
||||||
import { NotesEditor } from '../../components/widgets/MarkdownEditor';
|
import { NotesEditor } from '../../components/widgets/MarkdownEditor';
|
||||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||||
|
import { ModelType } from '../../enums/ModelType';
|
||||||
|
import { UserRoles } from '../../enums/Roles';
|
||||||
import { useEditStockItem } from '../../forms/StockForms';
|
import { useEditStockItem } from '../../forms/StockForms';
|
||||||
import { useInstance } from '../../hooks/UseInstance';
|
import { useInstance } from '../../hooks/UseInstance';
|
||||||
import { apiUrl } from '../../states/ApiState';
|
import { apiUrl } from '../../states/ApiState';
|
||||||
@ -63,12 +75,155 @@ export default function StockDetail() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const detailsPanel = useMemo(() => {
|
||||||
|
let data = stockitem;
|
||||||
|
|
||||||
|
data.available_stock = Math.max(0, data.quantity - data.allocated);
|
||||||
|
|
||||||
|
if (instanceQuery.isFetching) {
|
||||||
|
return <Skeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Top left - core part information
|
||||||
|
let tl: DetailsField[] = [
|
||||||
|
{
|
||||||
|
name: 'part',
|
||||||
|
label: t`Base Part`,
|
||||||
|
type: 'link',
|
||||||
|
model: ModelType.part
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'status',
|
||||||
|
type: 'text',
|
||||||
|
label: t`Stock Status`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'tests',
|
||||||
|
label: `Completed Tests`,
|
||||||
|
icon: 'progress'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'updated',
|
||||||
|
icon: 'calendar',
|
||||||
|
label: t`Last Updated`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'stocktake',
|
||||||
|
icon: 'calendar',
|
||||||
|
label: t`Last Stocktake`,
|
||||||
|
hidden: !stockitem.stocktake
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Top right - available stock information
|
||||||
|
let tr: DetailsField[] = [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'quantity',
|
||||||
|
label: t`Quantity`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'serial',
|
||||||
|
label: t`Serial Number`,
|
||||||
|
hidden: !stockitem.serial
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'available_stock',
|
||||||
|
label: t`Available`
|
||||||
|
}
|
||||||
|
// TODO: allocated_to_sales_orders
|
||||||
|
// TODO: allocated_to_build_orders
|
||||||
|
];
|
||||||
|
|
||||||
|
// Bottom left: location information
|
||||||
|
let bl: DetailsField[] = [
|
||||||
|
{
|
||||||
|
name: 'supplier_part',
|
||||||
|
label: t`Supplier Part`,
|
||||||
|
type: 'link',
|
||||||
|
model: ModelType.supplierpart,
|
||||||
|
hidden: !stockitem.supplier_part
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
name: 'location',
|
||||||
|
label: t`Location`,
|
||||||
|
model: ModelType.stocklocation,
|
||||||
|
hidden: !stockitem.location
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
name: 'belongs_to',
|
||||||
|
label: t`Installed In`,
|
||||||
|
model: ModelType.stockitem,
|
||||||
|
hidden: !stockitem.belongs_to
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
name: 'consumed_by',
|
||||||
|
label: t`Consumed By`,
|
||||||
|
model: ModelType.build,
|
||||||
|
hidden: !stockitem.consumed_by
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
name: 'sales_order',
|
||||||
|
label: t`Sales Order`,
|
||||||
|
model: ModelType.salesorder,
|
||||||
|
hidden: !stockitem.sales_order
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Bottom right - any other information
|
||||||
|
let br: DetailsField[] = [
|
||||||
|
// TODO: Expiry date
|
||||||
|
// TODO: Ownership
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'packaging',
|
||||||
|
icon: 'part',
|
||||||
|
label: t`Packaging`,
|
||||||
|
hidden: !stockitem.packaging
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ItemDetailsGrid>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<DetailsImage
|
||||||
|
appRole={UserRoles.part}
|
||||||
|
apiPath={ApiEndpoints.part_list}
|
||||||
|
src={
|
||||||
|
stockitem.part_detail?.image ??
|
||||||
|
stockitem?.part_detail?.thumbnail
|
||||||
|
}
|
||||||
|
pk={stockitem.part}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={8}>
|
||||||
|
<DetailsTable fields={tl} item={stockitem} />
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
<DetailsTable fields={tr} item={stockitem} />
|
||||||
|
<DetailsTable fields={bl} item={stockitem} />
|
||||||
|
<DetailsTable fields={br} item={stockitem} />
|
||||||
|
</ItemDetailsGrid>
|
||||||
|
);
|
||||||
|
}, [stockitem, instanceQuery]);
|
||||||
|
|
||||||
const stockPanels: PanelType[] = useMemo(() => {
|
const stockPanels: PanelType[] = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: 'details',
|
name: 'details',
|
||||||
label: t`Details`,
|
label: t`Stock Details`,
|
||||||
icon: <IconInfoCircle />
|
icon: <IconInfoCircle />,
|
||||||
|
content: detailsPanel
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'tracking',
|
name: 'tracking',
|
||||||
|
@ -1,88 +0,0 @@
|
|||||||
import { Grid, Group, Paper, SimpleGrid } from '@mantine/core';
|
|
||||||
|
|
||||||
import {
|
|
||||||
DetailImageButtonProps,
|
|
||||||
DetailsImage
|
|
||||||
} from '../components/images/DetailsImage';
|
|
||||||
import { UserRoles } from '../enums/Roles';
|
|
||||||
import { DetailsField, DetailsTable } from './Details';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Type for defining field arrays
|
|
||||||
*/
|
|
||||||
export type ItemDetailFields = {
|
|
||||||
left: DetailsField[][];
|
|
||||||
right?: DetailsField[][];
|
|
||||||
bottom_left?: DetailsField[][];
|
|
||||||
bottom_right?: DetailsField[][];
|
|
||||||
image?: DetailsImageType;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Type for defining details image
|
|
||||||
*/
|
|
||||||
export type DetailsImageType = {
|
|
||||||
name: string;
|
|
||||||
imageActions: DetailImageButtonProps;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render a Details panel of the given model
|
|
||||||
* @param params Object with the data of the model to render
|
|
||||||
* @param apiPath Path to use for image updating
|
|
||||||
* @param refresh useInstance refresh method to refresh when making updates
|
|
||||||
* @param fields Object with all field sections
|
|
||||||
* @param partModel set to true only if source model is Part
|
|
||||||
*/
|
|
||||||
export function ItemDetails({
|
|
||||||
appRole,
|
|
||||||
params = {},
|
|
||||||
apiPath,
|
|
||||||
refresh,
|
|
||||||
fields,
|
|
||||||
partModel = false
|
|
||||||
}: {
|
|
||||||
appRole: UserRoles;
|
|
||||||
params?: any;
|
|
||||||
apiPath: string;
|
|
||||||
refresh: () => void;
|
|
||||||
fields: ItemDetailFields;
|
|
||||||
partModel: boolean;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<Paper p="xs">
|
|
||||||
<SimpleGrid cols={2} spacing="xs" verticalSpacing="xs">
|
|
||||||
<Grid>
|
|
||||||
{fields.image && (
|
|
||||||
<Grid.Col span={4}>
|
|
||||||
<DetailsImage
|
|
||||||
appRole={appRole}
|
|
||||||
imageActions={fields.image.imageActions}
|
|
||||||
src={params.image}
|
|
||||||
apiPath={apiPath}
|
|
||||||
refresh={refresh}
|
|
||||||
pk={params.pk}
|
|
||||||
/>
|
|
||||||
</Grid.Col>
|
|
||||||
)}
|
|
||||||
<Grid.Col span={8}>
|
|
||||||
{fields.left && (
|
|
||||||
<DetailsTable
|
|
||||||
item={params}
|
|
||||||
fields={fields.left}
|
|
||||||
partIcons={partModel}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Grid.Col>
|
|
||||||
</Grid>
|
|
||||||
{fields.right && <DetailsTable item={params} fields={fields.right} />}
|
|
||||||
{fields.bottom_left && (
|
|
||||||
<DetailsTable item={params} fields={fields.bottom_left} />
|
|
||||||
)}
|
|
||||||
{fields.bottom_right && (
|
|
||||||
<DetailsTable item={params} fields={fields.bottom_right} />
|
|
||||||
)}
|
|
||||||
</SimpleGrid>
|
|
||||||
</Paper>
|
|
||||||
);
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user