mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-29 03:56:43 +00:00
[PUI] Part scheduling (#8163)
* Placeholder part scheduling panel * Add API endpoint definition * Add defined serializer to scheduling endpoint * Refactor add_schedule_entry * Fix field type * API tweak * Render scheduling data * Make links clickable * Correctly account for dates in the past * Cleanup table * Bump API version information * js linting
This commit is contained in:
parent
82b7e97df6
commit
4f2baab43b
@ -1,13 +1,17 @@
|
|||||||
"""InvenTree API version information."""
|
"""InvenTree API version information."""
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 257
|
INVENTREE_API_VERSION = 258
|
||||||
|
|
||||||
"""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 = """
|
||||||
|
|
||||||
|
v258 - 2024-09-24 : https://github.com/inventree/InvenTree/pull/8163
|
||||||
|
- Enhances the existing PartScheduling API endpoint
|
||||||
|
- Adds a formal DRF serializer to the endpoint
|
||||||
|
|
||||||
v257 - 2024-09-22 : https://github.com/inventree/InvenTree/pull/8150
|
v257 - 2024-09-22 : https://github.com/inventree/InvenTree/pull/8150
|
||||||
- Adds API endpoint for reporting barcode scan history
|
- Adds API endpoint for reporting barcode scan history
|
||||||
|
|
||||||
|
@ -559,7 +559,7 @@ class PartScheduling(RetrieveAPI):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
queryset = Part.objects.all()
|
queryset = Part.objects.all()
|
||||||
serializer_class = EmptySerializer
|
serializer_class = part_serializers.PartSchedulingSerializer
|
||||||
|
|
||||||
def retrieve(self, request, *args, **kwargs):
|
def retrieve(self, request, *args, **kwargs):
|
||||||
"""Return scheduling information for the referenced Part instance."""
|
"""Return scheduling information for the referenced Part instance."""
|
||||||
@ -567,23 +567,24 @@ class PartScheduling(RetrieveAPI):
|
|||||||
|
|
||||||
schedule = []
|
schedule = []
|
||||||
|
|
||||||
def add_schedule_entry(
|
def add_schedule_entry(date, quantity, title, instance, speculative_quantity=0):
|
||||||
date, quantity, title, label, url, speculative_quantity=0
|
"""Add a new entry to the schedule list.
|
||||||
):
|
|
||||||
"""Check if a scheduled entry should be added.
|
|
||||||
|
|
||||||
Rules:
|
Arguments:
|
||||||
- date must be non-null
|
- date: The date of the scheduled event
|
||||||
- date cannot be in the "past"
|
- quantity: The quantity of stock to be added or removed
|
||||||
- quantity must not be zero
|
- title: The title of the scheduled event
|
||||||
|
- instance: The associated model instance (e.g. SalesOrder object)
|
||||||
|
- speculative_quantity: A speculative quantity to be added or removed
|
||||||
"""
|
"""
|
||||||
schedule.append({
|
schedule.append({
|
||||||
'date': date,
|
'date': date,
|
||||||
'quantity': quantity,
|
'quantity': quantity,
|
||||||
'speculative_quantity': speculative_quantity,
|
'speculative_quantity': speculative_quantity,
|
||||||
'title': title,
|
'title': title,
|
||||||
'label': label,
|
'label': str(instance.reference),
|
||||||
'url': url,
|
'model': instance.__class__.__name__.lower(),
|
||||||
|
'model_id': instance.pk,
|
||||||
})
|
})
|
||||||
|
|
||||||
# Add purchase order (incoming stock) information
|
# Add purchase order (incoming stock) information
|
||||||
@ -600,11 +601,7 @@ class PartScheduling(RetrieveAPI):
|
|||||||
quantity = line.part.base_quantity(line_quantity)
|
quantity = line.part.base_quantity(line_quantity)
|
||||||
|
|
||||||
add_schedule_entry(
|
add_schedule_entry(
|
||||||
target_date,
|
target_date, quantity, _('Incoming Purchase Order'), line.order
|
||||||
quantity,
|
|
||||||
_('Incoming Purchase Order'),
|
|
||||||
str(line.order),
|
|
||||||
line.order.get_absolute_url(),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add sales order (outgoing stock) information
|
# Add sales order (outgoing stock) information
|
||||||
@ -618,11 +615,7 @@ class PartScheduling(RetrieveAPI):
|
|||||||
quantity = max(line.quantity - line.shipped, 0)
|
quantity = max(line.quantity - line.shipped, 0)
|
||||||
|
|
||||||
add_schedule_entry(
|
add_schedule_entry(
|
||||||
target_date,
|
target_date, -quantity, _('Outgoing Sales Order'), line.order
|
||||||
-quantity,
|
|
||||||
_('Outgoing Sales Order'),
|
|
||||||
str(line.order),
|
|
||||||
line.order.get_absolute_url(),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add build orders (incoming stock) information
|
# Add build orders (incoming stock) information
|
||||||
@ -634,11 +627,7 @@ class PartScheduling(RetrieveAPI):
|
|||||||
quantity = max(build.quantity - build.completed, 0)
|
quantity = max(build.quantity - build.completed, 0)
|
||||||
|
|
||||||
add_schedule_entry(
|
add_schedule_entry(
|
||||||
build.target_date,
|
build.target_date, quantity, _('Stock produced by Build Order'), build
|
||||||
quantity,
|
|
||||||
_('Stock produced by Build Order'),
|
|
||||||
str(build),
|
|
||||||
build.get_absolute_url(),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@ -721,8 +710,7 @@ class PartScheduling(RetrieveAPI):
|
|||||||
build.target_date,
|
build.target_date,
|
||||||
-part_allocated_quantity,
|
-part_allocated_quantity,
|
||||||
_('Stock required for Build Order'),
|
_('Stock required for Build Order'),
|
||||||
str(build),
|
build,
|
||||||
build.get_absolute_url(),
|
|
||||||
speculative_quantity=speculative_quantity,
|
speculative_quantity=speculative_quantity,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -742,9 +730,13 @@ class PartScheduling(RetrieveAPI):
|
|||||||
return -1 if date_1 < date_2 else 1
|
return -1 if date_1 < date_2 else 1
|
||||||
|
|
||||||
# Sort by incrementing date values
|
# Sort by incrementing date values
|
||||||
schedule = sorted(schedule, key=functools.cmp_to_key(compare))
|
schedules = sorted(schedule, key=functools.cmp_to_key(compare))
|
||||||
|
|
||||||
return Response(schedule)
|
serializers = part_serializers.PartSchedulingSerializer(
|
||||||
|
schedules, many=True, context={'request': request}
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(serializers.data)
|
||||||
|
|
||||||
|
|
||||||
class PartRequirements(RetrieveAPI):
|
class PartRequirements(RetrieveAPI):
|
||||||
|
@ -244,6 +244,39 @@ class PartInternalPriceSerializer(InvenTree.serializers.InvenTreeModelSerializer
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PartSchedulingSerializer(serializers.Serializer):
|
||||||
|
"""Serializer class for a PartScheduling entry."""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Metaclass options for this serializer."""
|
||||||
|
|
||||||
|
fields = [
|
||||||
|
'date',
|
||||||
|
'quantity',
|
||||||
|
'speculative_quantity',
|
||||||
|
'title',
|
||||||
|
'label',
|
||||||
|
'model',
|
||||||
|
'model_id',
|
||||||
|
]
|
||||||
|
|
||||||
|
date = serializers.DateField(label=_('Date'), required=True, allow_null=True)
|
||||||
|
|
||||||
|
quantity = serializers.FloatField(label=_('Quantity'), required=True)
|
||||||
|
|
||||||
|
speculative_quantity = serializers.FloatField(
|
||||||
|
label=_('Speculative Quantity'), required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
title = serializers.CharField(label=_('Title'), required=True)
|
||||||
|
|
||||||
|
label = serializers.CharField(label=_('Label'), required=True)
|
||||||
|
|
||||||
|
model = serializers.CharField(label=_('Model'), required=True)
|
||||||
|
|
||||||
|
model_id = serializers.IntegerField(label=_('Model ID'), required=True)
|
||||||
|
|
||||||
|
|
||||||
class PartThumbSerializer(serializers.Serializer):
|
class PartThumbSerializer(serializers.Serializer):
|
||||||
"""Serializer for the 'image' field of the Part model.
|
"""Serializer for the 'image' field of the Part model.
|
||||||
|
|
||||||
|
@ -3078,10 +3078,26 @@ function loadPartSchedulingChart(canvas_id, part_id) {
|
|||||||
quantity_string += makeIconBadge('fa-question-circle icon-blue', '{% trans "Speculative" %}');
|
quantity_string += makeIconBadge('fa-question-circle icon-blue', '{% trans "Speculative" %}');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let url = '#';
|
||||||
|
|
||||||
|
switch (entry.model) {
|
||||||
|
case 'salesorder':
|
||||||
|
url = `/order/sales-order/${entry.model_id}/`;
|
||||||
|
break;
|
||||||
|
case 'purchaseorder':
|
||||||
|
url = `/order/purchase-order/${entry.model_id}/`;
|
||||||
|
break;
|
||||||
|
case 'build':
|
||||||
|
url = `/build/${entry.model_id}/`;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// Add an entry to the scheduling table
|
// Add an entry to the scheduling table
|
||||||
table_html += `
|
table_html += `
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="${entry.url}">${entry.label}</a></td>
|
<td><a href="${url}">${entry.label}</a></td>
|
||||||
<td>${entry.title}</td>
|
<td>${entry.title}</td>
|
||||||
<td>${date_string}</td>
|
<td>${date_string}</td>
|
||||||
<td>${quantity_string}</td>
|
<td>${quantity_string}</td>
|
||||||
|
@ -95,6 +95,7 @@ export enum ApiEndpoints {
|
|||||||
part_thumbs_list = 'part/thumbs/',
|
part_thumbs_list = 'part/thumbs/',
|
||||||
part_pricing_get = 'part/:id/pricing/',
|
part_pricing_get = 'part/:id/pricing/',
|
||||||
part_serial_numbers = 'part/:id/serial-numbers/',
|
part_serial_numbers = 'part/:id/serial-numbers/',
|
||||||
|
part_scheduling = 'part/:id/scheduling/',
|
||||||
part_pricing_internal = 'part/internal-price/',
|
part_pricing_internal = 'part/internal-price/',
|
||||||
part_pricing_sale = 'part/sale-price/',
|
part_pricing_sale = 'part/sale-price/',
|
||||||
part_stocktake_list = 'part/stocktake/',
|
part_stocktake_list = 'part/stocktake/',
|
||||||
|
@ -104,6 +104,7 @@ import { SalesOrderTable } from '../../tables/sales/SalesOrderTable';
|
|||||||
import { StockItemTable } from '../../tables/stock/StockItemTable';
|
import { StockItemTable } from '../../tables/stock/StockItemTable';
|
||||||
import { TestStatisticsTable } from '../../tables/stock/TestStatisticsTable';
|
import { TestStatisticsTable } from '../../tables/stock/TestStatisticsTable';
|
||||||
import PartPricingPanel from './PartPricingPanel';
|
import PartPricingPanel from './PartPricingPanel';
|
||||||
|
import PartSchedulingDetail from './PartSchedulingDetail';
|
||||||
import PartStocktakeDetail from './PartStocktakeDetail';
|
import PartStocktakeDetail from './PartStocktakeDetail';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -705,7 +706,7 @@ export default function PartDetail() {
|
|||||||
name: 'scheduling',
|
name: 'scheduling',
|
||||||
label: t`Scheduling`,
|
label: t`Scheduling`,
|
||||||
icon: <IconCalendarStats />,
|
icon: <IconCalendarStats />,
|
||||||
content: <PlaceholderPanel />,
|
content: part ? <PartSchedulingDetail part={part} /> : <Skeleton />,
|
||||||
hidden: !userSettings.isSet('DISPLAY_SCHEDULE_TAB')
|
hidden: !userSettings.isSet('DISPLAY_SCHEDULE_TAB')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
312
src/frontend/src/pages/part/PartSchedulingDetail.tsx
Normal file
312
src/frontend/src/pages/part/PartSchedulingDetail.tsx
Normal file
@ -0,0 +1,312 @@
|
|||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { ChartTooltipProps, LineChart } from '@mantine/charts';
|
||||||
|
import {
|
||||||
|
Anchor,
|
||||||
|
Center,
|
||||||
|
Divider,
|
||||||
|
DrawerOverlay,
|
||||||
|
Loader,
|
||||||
|
Paper,
|
||||||
|
SimpleGrid,
|
||||||
|
Text
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { ReactNode, useMemo } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { formatDate } from '../../defaults/formatters';
|
||||||
|
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||||
|
import { navigateToLink } from '../../functions/navigation';
|
||||||
|
import { getDetailUrl } from '../../functions/urls';
|
||||||
|
import { useTable } from '../../hooks/UseTable';
|
||||||
|
import { apiUrl } from '../../states/ApiState';
|
||||||
|
import { TableColumn } from '../../tables/Column';
|
||||||
|
import { DateColumn, DescriptionColumn } from '../../tables/ColumnRenderers';
|
||||||
|
import { InvenTreeTable } from '../../tables/InvenTreeTable';
|
||||||
|
import { TableHoverCard } from '../../tables/TableHoverCard';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Render a tooltip for the chart, with correct date information
|
||||||
|
*/
|
||||||
|
function ChartTooltip({ label, payload }: ChartTooltipProps) {
|
||||||
|
if (!payload) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (label && typeof label == 'number') {
|
||||||
|
label = formatDate(new Date(label).toISOString());
|
||||||
|
}
|
||||||
|
|
||||||
|
const scheduled = payload.find((item) => item.name == 'scheduled');
|
||||||
|
const minimum = payload.find((item) => item.name == 'minimum');
|
||||||
|
const maximum = payload.find((item) => item.name == 'maximum');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper px="md" py="sm" withBorder shadow="md" radius="md">
|
||||||
|
<Text key="title">{label}</Text>
|
||||||
|
<Divider />
|
||||||
|
<Text key="maximum" c={maximum?.color} fz="sm">
|
||||||
|
{t`Maximum`} : {maximum?.value}
|
||||||
|
</Text>
|
||||||
|
<Text key="scheduled" c={scheduled?.color} fz="sm">
|
||||||
|
{t`Scheduled`} : {scheduled?.value}
|
||||||
|
</Text>
|
||||||
|
<Text key="minimum" c={minimum?.color} fz="sm">
|
||||||
|
{t`Minimum`} : {minimum?.value}
|
||||||
|
</Text>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PartSchedulingDetail({ part }: { part: any }) {
|
||||||
|
const table = useTable('part-scheduling');
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const tableColumns: TableColumn[] = useMemo(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
accessor: 'label',
|
||||||
|
switchable: false,
|
||||||
|
title: t`Order`,
|
||||||
|
render: (record: any) => {
|
||||||
|
const url = getDetailUrl(record.model, record.model_id);
|
||||||
|
|
||||||
|
if (url) {
|
||||||
|
return (
|
||||||
|
<Anchor
|
||||||
|
href="#"
|
||||||
|
onClick={(event: any) => navigateToLink(url, navigate, event)}
|
||||||
|
>
|
||||||
|
{record.label}
|
||||||
|
</Anchor>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return record.label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
DescriptionColumn({
|
||||||
|
accessor: 'title',
|
||||||
|
switchable: false
|
||||||
|
}),
|
||||||
|
DateColumn({
|
||||||
|
sortable: false,
|
||||||
|
switchable: false
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
accessor: 'quantity',
|
||||||
|
title: t`Quantity`,
|
||||||
|
switchable: false,
|
||||||
|
render: (record: any) => {
|
||||||
|
let q = record.quantity;
|
||||||
|
let extra: ReactNode[] = [];
|
||||||
|
|
||||||
|
if (record.speculative_quantity != 0) {
|
||||||
|
q = record.speculative_quantity;
|
||||||
|
extra.push(
|
||||||
|
<Text
|
||||||
|
size="sm"
|
||||||
|
key={'speculative'}
|
||||||
|
>{t`Quantity is speculative`}</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!record.date) {
|
||||||
|
extra.push(
|
||||||
|
<Text
|
||||||
|
key={'null-date'}
|
||||||
|
size="sm"
|
||||||
|
>{t`No date available for provided quantity`}</Text>
|
||||||
|
);
|
||||||
|
} else if (new Date(record.date) < new Date()) {
|
||||||
|
extra.push(
|
||||||
|
<Text size="sm" key={'past-date'}>{t`Date is in the past`}</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableHoverCard
|
||||||
|
value={<Text key="quantity">{q}</Text>}
|
||||||
|
title={t`Scheduled Quantity`}
|
||||||
|
extra={extra}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const chartData = useMemo(() => {
|
||||||
|
/* Rebuild chart data whenever the table data changes.
|
||||||
|
* Note: We assume that the data is provided in increasing date order,
|
||||||
|
* with "null" date entries placed first.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const today = new Date();
|
||||||
|
today.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
// Date bounds
|
||||||
|
let min_date: Date = new Date();
|
||||||
|
let max_date: Date = new Date();
|
||||||
|
|
||||||
|
// Track stock scheduling throughout time
|
||||||
|
let stock = part.in_stock ?? 0;
|
||||||
|
let stock_min = stock;
|
||||||
|
let stock_max = stock;
|
||||||
|
|
||||||
|
// First, iterate through each entry and find any entries without an associated date, or in the past
|
||||||
|
table.records.forEach((record) => {
|
||||||
|
let q = record.quantity + record.speculative_quantity;
|
||||||
|
|
||||||
|
if (record.date == null || new Date(record.date) < today) {
|
||||||
|
if (q < 0) {
|
||||||
|
stock_min += q;
|
||||||
|
} else {
|
||||||
|
stock_max += q;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Construct initial chart entry (for today)
|
||||||
|
let entries: any[] = [
|
||||||
|
{
|
||||||
|
// date: formatDate(today.toISOString()),
|
||||||
|
date: today.valueOf(),
|
||||||
|
delta: 0,
|
||||||
|
scheduled: stock,
|
||||||
|
minimum: stock_min,
|
||||||
|
maximum: stock_max,
|
||||||
|
low_stock: part.minimum_stock
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
table.records.forEach((record) => {
|
||||||
|
let q = record.quantity + record.speculative_quantity;
|
||||||
|
|
||||||
|
if (!record.date) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = new Date(record.date);
|
||||||
|
|
||||||
|
// In the past? Ignore this entry
|
||||||
|
if (date < today) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update date limits
|
||||||
|
|
||||||
|
if (date < min_date) {
|
||||||
|
min_date = date;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (date > max_date) {
|
||||||
|
max_date = date;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update stock levels
|
||||||
|
stock += record.quantity;
|
||||||
|
|
||||||
|
stock_min += record.quantity;
|
||||||
|
stock_max += record.quantity;
|
||||||
|
|
||||||
|
// Speculative quantities expand the expected stock range
|
||||||
|
if (record.speculative_quantity < 0) {
|
||||||
|
stock_min += record.speculative_quantity;
|
||||||
|
} else if (record.speculative_quantity > 0) {
|
||||||
|
stock_max += record.speculative_quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.push({
|
||||||
|
...record,
|
||||||
|
date: new Date(record.date).valueOf(),
|
||||||
|
scheduled: stock,
|
||||||
|
minimum: stock_min,
|
||||||
|
maximum: stock_max,
|
||||||
|
low_stock: part.minimum_stock
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
}, [part, table.records]);
|
||||||
|
|
||||||
|
// Calculate the date limits of the chart
|
||||||
|
const chartLimits: number[] = useMemo(() => {
|
||||||
|
let min_date = new Date();
|
||||||
|
let max_date = new Date();
|
||||||
|
|
||||||
|
if (chartData.length > 0) {
|
||||||
|
min_date = new Date(chartData[0].date);
|
||||||
|
max_date = new Date(chartData[chartData.length - 1].date);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expand limits by one day on either side
|
||||||
|
min_date.setDate(min_date.getDate() - 1);
|
||||||
|
max_date.setDate(max_date.getDate() + 1);
|
||||||
|
|
||||||
|
return [min_date.valueOf(), max_date.valueOf()];
|
||||||
|
}, [chartData]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SimpleGrid cols={2}>
|
||||||
|
<InvenTreeTable
|
||||||
|
url={apiUrl(ApiEndpoints.part_scheduling, part.pk)}
|
||||||
|
tableState={table}
|
||||||
|
columns={tableColumns}
|
||||||
|
props={{
|
||||||
|
enableSearch: false
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{table.isLoading ? (
|
||||||
|
<Center>
|
||||||
|
<Loader />
|
||||||
|
</Center>
|
||||||
|
) : (
|
||||||
|
<LineChart
|
||||||
|
data={chartData}
|
||||||
|
dataKey="date"
|
||||||
|
withLegend
|
||||||
|
withYAxis
|
||||||
|
tooltipProps={{
|
||||||
|
content: ({ label, payload }) => (
|
||||||
|
<ChartTooltip label={label} payload={payload} />
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
yAxisLabel={t`Expected Quantity`}
|
||||||
|
xAxisLabel={t`Date`}
|
||||||
|
xAxisProps={{
|
||||||
|
domain: [chartLimits[0], chartLimits[1]],
|
||||||
|
scale: 'time',
|
||||||
|
type: 'number',
|
||||||
|
tickFormatter: (value: number) => {
|
||||||
|
return formatDate(new Date(value).toISOString());
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
series={[
|
||||||
|
{
|
||||||
|
name: 'scheduled',
|
||||||
|
label: t`Scheduled`,
|
||||||
|
color: 'blue.6'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'minimum',
|
||||||
|
label: t`Minimum`,
|
||||||
|
color: 'yellow.6'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'maximum',
|
||||||
|
label: t`Maximum`,
|
||||||
|
color: 'teal.6'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'low_stock',
|
||||||
|
label: t`Low Stock`,
|
||||||
|
color: 'red.6'
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</SimpleGrid>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -500,7 +500,7 @@ export function InvenTreeTable<T extends Record<string, any>>({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const { data, isFetching, refetch } = useQuery({
|
const { data, isFetching, isLoading, refetch } = useQuery({
|
||||||
queryKey: [
|
queryKey: [
|
||||||
tableState.page,
|
tableState.page,
|
||||||
props.params,
|
props.params,
|
||||||
@ -515,8 +515,13 @@ export function InvenTreeTable<T extends Record<string, any>>({
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
tableState.setIsLoading(isFetching);
|
tableState.setIsLoading(
|
||||||
}, [isFetching]);
|
isFetching ||
|
||||||
|
isLoading ||
|
||||||
|
tableOptionQuery.isFetching ||
|
||||||
|
tableOptionQuery.isLoading
|
||||||
|
);
|
||||||
|
}, [isFetching, isLoading, tableOptionQuery]);
|
||||||
|
|
||||||
// Update tableState.records when new data received
|
// Update tableState.records when new data received
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user