mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-29 12:06:44 +00:00
[PUI] Part charts fix (#8171)
* Refactor "stocktake" chart - Format date axis correctly - Better tooltips * Fix datatable queries - Prevent potential infinite loop * Fix tooltip for BOM table - Bug due to '.' char in names * Do not reassign parameter * Another fix * Check global config value * Cleanup tooltip for BOM pie * Edit and refresh pricing from panel
This commit is contained in:
parent
b1c1981b6d
commit
0f2cfdcfd4
@ -93,7 +93,7 @@ export enum ApiEndpoints {
|
|||||||
part_parameter_list = 'part/parameter/',
|
part_parameter_list = 'part/parameter/',
|
||||||
part_parameter_template_list = 'part/parameter/template/',
|
part_parameter_template_list = 'part/parameter/template/',
|
||||||
part_thumbs_list = 'part/thumbs/',
|
part_thumbs_list = 'part/thumbs/',
|
||||||
part_pricing_get = 'part/:id/pricing/',
|
part_pricing = 'part/:id/pricing/',
|
||||||
part_serial_numbers = 'part/:id/serial-numbers/',
|
part_serial_numbers = 'part/:id/serial-numbers/',
|
||||||
part_scheduling = 'part/:id/scheduling/',
|
part_scheduling = 'part/:id/scheduling/',
|
||||||
part_pricing_internal = 'part/internal-price/',
|
part_pricing_internal = 'part/internal-price/',
|
||||||
|
@ -64,6 +64,7 @@ import {
|
|||||||
IconProps,
|
IconProps,
|
||||||
IconQrcode,
|
IconQrcode,
|
||||||
IconQuestionMark,
|
IconQuestionMark,
|
||||||
|
IconRefresh,
|
||||||
IconRulerMeasure,
|
IconRulerMeasure,
|
||||||
IconShoppingCart,
|
IconShoppingCart,
|
||||||
IconShoppingCartHeart,
|
IconShoppingCartHeart,
|
||||||
@ -143,6 +144,7 @@ const icons = {
|
|||||||
photo: IconPhoto,
|
photo: IconPhoto,
|
||||||
upload: IconFileUpload,
|
upload: IconFileUpload,
|
||||||
reject: IconX,
|
reject: IconX,
|
||||||
|
refresh: IconRefresh,
|
||||||
select_image: IconGridDots,
|
select_image: IconGridDots,
|
||||||
delete: IconTrash,
|
delete: IconTrash,
|
||||||
packaging: IconPackage,
|
packaging: IconPackage,
|
||||||
|
@ -60,6 +60,6 @@ export function showLoginNotification({
|
|||||||
color: success ? 'green' : 'red',
|
color: success ? 'green' : 'red',
|
||||||
icon: success ? <IconCircleCheck /> : <IconExclamationCircle />,
|
icon: success ? <IconCircleCheck /> : <IconExclamationCircle />,
|
||||||
id: 'login',
|
id: 'login',
|
||||||
autoClose: 5000
|
autoClose: 2500
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -54,7 +54,7 @@ export type TableState = {
|
|||||||
export function useTable(tableName: string): TableState {
|
export function useTable(tableName: string): TableState {
|
||||||
// Function to generate a new ID (to refresh the table)
|
// Function to generate a new ID (to refresh the table)
|
||||||
function generateTableName() {
|
function generateTableName() {
|
||||||
return `${tableName}-${randomId()}`;
|
return `${tableName.replaceAll('-', '')}-${randomId()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [tableKey, setTableKey] = useState<string>(generateTableName());
|
const [tableKey, setTableKey] = useState<string>(generateTableName());
|
||||||
|
@ -55,7 +55,6 @@ import {
|
|||||||
EditItemAction,
|
EditItemAction,
|
||||||
OptionsActionDropdown
|
OptionsActionDropdown
|
||||||
} from '../../components/items/ActionDropdown';
|
} from '../../components/items/ActionDropdown';
|
||||||
import { PlaceholderPanel } from '../../components/items/Placeholder';
|
|
||||||
import { StylishText } from '../../components/items/StylishText';
|
import { StylishText } from '../../components/items/StylishText';
|
||||||
import InstanceDetail from '../../components/nav/InstanceDetail';
|
import InstanceDetail from '../../components/nav/InstanceDetail';
|
||||||
import NavigationTree from '../../components/nav/NavigationTree';
|
import NavigationTree from '../../components/nav/NavigationTree';
|
||||||
@ -401,7 +400,7 @@ export default function PartDetail() {
|
|||||||
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, null, {
|
||||||
id: id
|
id: id
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import { useMemo, useState } from 'react';
|
|||||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||||
import { UserRoles } from '../../enums/Roles';
|
import { UserRoles } from '../../enums/Roles';
|
||||||
import { useInstance } from '../../hooks/UseInstance';
|
import { useInstance } from '../../hooks/UseInstance';
|
||||||
|
import { useGlobalSettingsState } from '../../states/SettingsState';
|
||||||
import { useUserState } from '../../states/UserState';
|
import { useUserState } from '../../states/UserState';
|
||||||
import BomPricingPanel from './pricing/BomPricingPanel';
|
import BomPricingPanel from './pricing/BomPricingPanel';
|
||||||
import PriceBreakPanel from './pricing/PriceBreakPanel';
|
import PriceBreakPanel from './pricing/PriceBreakPanel';
|
||||||
@ -31,15 +32,18 @@ export enum panelOptions {
|
|||||||
export default function PartPricingPanel({ part }: Readonly<{ part: any }>) {
|
export default function PartPricingPanel({ part }: Readonly<{ part: any }>) {
|
||||||
const user = useUserState();
|
const user = useUserState();
|
||||||
|
|
||||||
|
const globalSettings = useGlobalSettingsState();
|
||||||
|
|
||||||
const { instance: pricing, instanceQuery } = useInstance({
|
const { instance: pricing, instanceQuery } = useInstance({
|
||||||
pk: part?.pk,
|
pk: part?.pk,
|
||||||
hasPrimaryKey: true,
|
hasPrimaryKey: true,
|
||||||
endpoint: ApiEndpoints.part_pricing_get,
|
endpoint: ApiEndpoints.part_pricing,
|
||||||
defaultValue: {}
|
defaultValue: {}
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: Do we display internal price? This is a global setting
|
const internalPricing: boolean = useMemo(() => {
|
||||||
const internalPricing = true;
|
return globalSettings.isSet('PART_INTERNAL_PRICE');
|
||||||
|
}, [globalSettings]);
|
||||||
|
|
||||||
const purchaseOrderPricing = useMemo(() => {
|
const purchaseOrderPricing = useMemo(() => {
|
||||||
return user.hasViewRole(UserRoles.purchase_order) && part?.purchaseable;
|
return user.hasViewRole(UserRoles.purchase_order) && part?.purchaseable;
|
||||||
@ -75,6 +79,7 @@ export default function PartPricingPanel({ part }: Readonly<{ part: any }>) {
|
|||||||
<PricingOverviewPanel
|
<PricingOverviewPanel
|
||||||
part={part}
|
part={part}
|
||||||
pricing={pricing}
|
pricing={pricing}
|
||||||
|
pricingQuery={instanceQuery}
|
||||||
doNavigation={doNavigation}
|
doNavigation={doNavigation}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
@ -275,7 +275,7 @@ export default function PartSchedulingDetail({ part }: { part: any }) {
|
|||||||
yAxisLabel={t`Expected Quantity`}
|
yAxisLabel={t`Expected Quantity`}
|
||||||
xAxisLabel={t`Date`}
|
xAxisLabel={t`Date`}
|
||||||
xAxisProps={{
|
xAxisProps={{
|
||||||
domain: [chartLimits[0], chartLimits[1]],
|
domain: chartLimits,
|
||||||
scale: 'time',
|
scale: 'time',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
tickFormatter: (value: number) => {
|
tickFormatter: (value: number) => {
|
||||||
|
@ -1,10 +1,17 @@
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { LineChart } from '@mantine/charts';
|
import { ChartTooltipProps, LineChart } from '@mantine/charts';
|
||||||
import { Center, Loader, SimpleGrid } from '@mantine/core';
|
import {
|
||||||
|
Center,
|
||||||
|
Divider,
|
||||||
|
Loader,
|
||||||
|
Paper,
|
||||||
|
SimpleGrid,
|
||||||
|
Text
|
||||||
|
} from '@mantine/core';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { AddItemButton } from '../../components/buttons/AddItemButton';
|
import { AddItemButton } from '../../components/buttons/AddItemButton';
|
||||||
import { formatPriceRange } from '../../defaults/formatters';
|
import { formatDate, formatPriceRange } from '../../defaults/formatters';
|
||||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||||
import { UserRoles } from '../../enums/Roles';
|
import { UserRoles } from '../../enums/Roles';
|
||||||
import {
|
import {
|
||||||
@ -23,6 +30,42 @@ import { TableColumn } from '../../tables/Column';
|
|||||||
import { InvenTreeTable } from '../../tables/InvenTreeTable';
|
import { InvenTreeTable } from '../../tables/InvenTreeTable';
|
||||||
import { RowDeleteAction, RowEditAction } from '../../tables/RowActions';
|
import { RowDeleteAction, RowEditAction } from '../../tables/RowActions';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Render a tooltip for the chart, with correct date information
|
||||||
|
*/
|
||||||
|
function ChartTooltip({ label, payload }: ChartTooltipProps) {
|
||||||
|
const formattedLabel: string = useMemo(() => {
|
||||||
|
if (label && typeof label === 'number') {
|
||||||
|
return formatDate(new Date(label).toISOString()) ?? label;
|
||||||
|
} else if (!!label) {
|
||||||
|
return label.toString();
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}, [label]);
|
||||||
|
|
||||||
|
if (!payload) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const quantity = payload.find((item) => item.name == 'quantity');
|
||||||
|
const value_min = payload.find((item) => item.name == 'value_min');
|
||||||
|
const value_max = payload.find((item) => item.name == 'value_max');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper px="md" py="sm" withBorder shadow="md" radius="md">
|
||||||
|
<Text key="title">{formattedLabel}</Text>
|
||||||
|
<Divider />
|
||||||
|
<Text key="quantity" fz="sm">
|
||||||
|
{t`Quantity`} : {quantity?.value}
|
||||||
|
</Text>
|
||||||
|
<Text key="values" fz="sm">
|
||||||
|
{t`Value`} : {formatPriceRange(value_min?.value, value_max?.value)}
|
||||||
|
</Text>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function PartStocktakeDetail({ partId }: { partId: number }) {
|
export default function PartStocktakeDetail({ partId }: { partId: number }) {
|
||||||
const user = useUserState();
|
const user = useUserState();
|
||||||
const table = useTable('part-stocktake');
|
const table = useTable('part-stocktake');
|
||||||
@ -126,7 +169,7 @@ export default function PartStocktakeDetail({ partId }: { partId: number }) {
|
|||||||
let records =
|
let records =
|
||||||
table.records?.map((record: any) => {
|
table.records?.map((record: any) => {
|
||||||
return {
|
return {
|
||||||
date: record.date,
|
date: new Date(record.date).valueOf(),
|
||||||
quantity: record.quantity,
|
quantity: record.quantity,
|
||||||
value_min: record.cost_min,
|
value_min: record.cost_min,
|
||||||
value_max: record.cost_max
|
value_max: record.cost_max
|
||||||
@ -135,12 +178,29 @@ export default function PartStocktakeDetail({ partId }: { partId: number }) {
|
|||||||
|
|
||||||
// Sort records to ensure correct date order
|
// Sort records to ensure correct date order
|
||||||
records.sort((a, b) => {
|
records.sort((a, b) => {
|
||||||
return new Date(a.date) < new Date(b.date) ? -1 : 1;
|
return a < b ? -1 : 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
return records;
|
return records;
|
||||||
}, [table.records]);
|
}, [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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{generateReport.modal}
|
{generateReport.modal}
|
||||||
@ -172,6 +232,19 @@ export default function PartStocktakeDetail({ partId }: { partId: number }) {
|
|||||||
withRightYAxis
|
withRightYAxis
|
||||||
yAxisLabel={t`Quantity`}
|
yAxisLabel={t`Quantity`}
|
||||||
rightYAxisLabel={t`Stock Value`}
|
rightYAxisLabel={t`Stock Value`}
|
||||||
|
tooltipProps={{
|
||||||
|
content: ({ label, payload }) => (
|
||||||
|
<ChartTooltip label={label} payload={payload} />
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
xAxisProps={{
|
||||||
|
scale: 'time',
|
||||||
|
type: 'number',
|
||||||
|
domain: chartLimits,
|
||||||
|
tickFormatter: (value: number) => {
|
||||||
|
return formatDate(new Date(value).toISOString());
|
||||||
|
}
|
||||||
|
}}
|
||||||
series={[
|
series={[
|
||||||
{
|
{
|
||||||
name: 'quantity',
|
name: 'quantity',
|
||||||
@ -181,14 +254,14 @@ export default function PartStocktakeDetail({ partId }: { partId: number }) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'value_min',
|
name: 'value_min',
|
||||||
label: t`Min Value`,
|
label: t`Minimum Value`,
|
||||||
color: 'teal.6',
|
color: 'yellow.6',
|
||||||
yAxisId: 'right'
|
yAxisId: 'right'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'value_max',
|
name: 'value_max',
|
||||||
label: t`Max Value`,
|
label: t`Maximum Value`,
|
||||||
color: 'red.6',
|
color: 'teal.6',
|
||||||
yAxisId: 'right'
|
yAxisId: 'right'
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { BarChart, DonutChart } from '@mantine/charts';
|
import { BarChart, ChartTooltipProps, DonutChart } from '@mantine/charts';
|
||||||
import {
|
import {
|
||||||
Center,
|
Center,
|
||||||
Group,
|
Group,
|
||||||
|
Paper,
|
||||||
SegmentedControl,
|
SegmentedControl,
|
||||||
SimpleGrid,
|
SimpleGrid,
|
||||||
Stack,
|
Stack,
|
||||||
@ -12,7 +13,11 @@ import { ReactNode, useMemo, useState } from 'react';
|
|||||||
|
|
||||||
import { CHART_COLORS } from '../../../components/charts/colors';
|
import { CHART_COLORS } from '../../../components/charts/colors';
|
||||||
import { tooltipFormatter } from '../../../components/charts/tooltipFormatter';
|
import { tooltipFormatter } from '../../../components/charts/tooltipFormatter';
|
||||||
import { formatDecimal, formatPriceRange } from '../../../defaults/formatters';
|
import {
|
||||||
|
formatCurrency,
|
||||||
|
formatDecimal,
|
||||||
|
formatPriceRange
|
||||||
|
} from '../../../defaults/formatters';
|
||||||
import { ApiEndpoints } from '../../../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../../../enums/ApiEndpoints';
|
||||||
import { ModelType } from '../../../enums/ModelType';
|
import { ModelType } from '../../../enums/ModelType';
|
||||||
import { useTable } from '../../../hooks/UseTable';
|
import { useTable } from '../../../hooks/UseTable';
|
||||||
@ -22,6 +27,30 @@ import { DateColumn, PartColumn } from '../../../tables/ColumnRenderers';
|
|||||||
import { InvenTreeTable } from '../../../tables/InvenTreeTable';
|
import { InvenTreeTable } from '../../../tables/InvenTreeTable';
|
||||||
import { LoadingPricingData, NoPricingData } from './PricingPanel';
|
import { LoadingPricingData, NoPricingData } from './PricingPanel';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Render a tooltip for the chart, with correct date information
|
||||||
|
*/
|
||||||
|
function ChartTooltip({ label, payload }: ChartTooltipProps) {
|
||||||
|
if (!payload) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = payload[0] ?? {};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper px="md" py="sm" withBorder shadow="md" radius="md">
|
||||||
|
<Group justify="space-between" wrap="nowrap">
|
||||||
|
<Text key="title" c={data.payload?.color}>
|
||||||
|
{data.name}
|
||||||
|
</Text>
|
||||||
|
<Text key="price" fz="sm">
|
||||||
|
{formatCurrency(data.payload?.value)}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Display BOM data as a pie chart
|
// Display BOM data as a pie chart
|
||||||
function BomPieChart({
|
function BomPieChart({
|
||||||
data,
|
data,
|
||||||
@ -57,6 +86,11 @@ function BomPieChart({
|
|||||||
tooltipDataSource="segment"
|
tooltipDataSource="segment"
|
||||||
chartLabel={t`Total Price`}
|
chartLabel={t`Total Price`}
|
||||||
valueFormatter={(value) => tooltipFormatter(value, currency)}
|
valueFormatter={(value) => tooltipFormatter(value, currency)}
|
||||||
|
tooltipProps={{
|
||||||
|
content: ({ label, payload }) => (
|
||||||
|
<ChartTooltip label={label} payload={payload} />
|
||||||
|
)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
);
|
);
|
||||||
@ -78,10 +112,15 @@ function BomBarChart({
|
|||||||
xAxisLabel={t`Component`}
|
xAxisLabel={t`Component`}
|
||||||
yAxisLabel={t`Price Range`}
|
yAxisLabel={t`Price Range`}
|
||||||
series={[
|
series={[
|
||||||
{ name: 'total_price_min', label: t`Minimum Price`, color: 'blue.6' },
|
{ name: 'total_price_min', label: t`Minimum Price`, color: 'yellow.6' },
|
||||||
{ name: 'total_price_max', label: t`Maximum Price`, color: 'teal.6' }
|
{ name: 'total_price_max', label: t`Maximum Price`, color: 'teal.6' }
|
||||||
]}
|
]}
|
||||||
valueFormatter={(value) => tooltipFormatter(value, currency)}
|
valueFormatter={(value) => tooltipFormatter(value, currency)}
|
||||||
|
tooltipProps={{
|
||||||
|
content: ({ label, payload }) => (
|
||||||
|
<ChartTooltip label={label} payload={payload} />
|
||||||
|
)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -93,7 +132,7 @@ export default function BomPricingPanel({
|
|||||||
readonly part: any;
|
readonly part: any;
|
||||||
readonly pricing: any;
|
readonly pricing: any;
|
||||||
}): ReactNode {
|
}): ReactNode {
|
||||||
const table = useTable('pricing-bom');
|
const table = useTable('pricingbom');
|
||||||
|
|
||||||
const columns: TableColumn[] = useMemo(() => {
|
const columns: TableColumn[] = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
|
@ -34,7 +34,7 @@ export default function PriceBreakPanel({
|
|||||||
endpoint: ApiEndpoints;
|
endpoint: ApiEndpoints;
|
||||||
}>) {
|
}>) {
|
||||||
const user = useUserState();
|
const user = useUserState();
|
||||||
const table = useTable('pricing-internal');
|
const table = useTable('pricinginternal');
|
||||||
|
|
||||||
const priceBreakFields: ApiFormFieldSet = useMemo(() => {
|
const priceBreakFields: ApiFormFieldSet = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
|
@ -9,20 +9,33 @@ import {
|
|||||||
Stack,
|
Stack,
|
||||||
Text
|
Text
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
|
import { notifications } from '@mantine/notifications';
|
||||||
import {
|
import {
|
||||||
IconBuildingWarehouse,
|
IconBuildingWarehouse,
|
||||||
IconChartDonut,
|
IconChartDonut,
|
||||||
|
IconCircleCheck,
|
||||||
IconExclamationCircle,
|
IconExclamationCircle,
|
||||||
IconList,
|
IconList,
|
||||||
IconReportAnalytics,
|
IconReportAnalytics,
|
||||||
IconShoppingCart,
|
IconShoppingCart,
|
||||||
IconTriangleSquareCircle
|
IconTriangleSquareCircle
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
|
import { UseQueryResult } from '@tanstack/react-query';
|
||||||
|
import { toggleUnorderedList } from 'easymde';
|
||||||
import { DataTable } from 'mantine-datatable';
|
import { DataTable } from 'mantine-datatable';
|
||||||
import { ReactNode, useMemo } from 'react';
|
import { ReactNode, useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
|
import { api } from '../../../App';
|
||||||
import { tooltipFormatter } from '../../../components/charts/tooltipFormatter';
|
import { tooltipFormatter } from '../../../components/charts/tooltipFormatter';
|
||||||
|
import {
|
||||||
|
EditItemAction,
|
||||||
|
OptionsActionDropdown
|
||||||
|
} from '../../../components/items/ActionDropdown';
|
||||||
import { formatCurrency, formatDate } from '../../../defaults/formatters';
|
import { formatCurrency, formatDate } from '../../../defaults/formatters';
|
||||||
|
import { ApiEndpoints } from '../../../enums/ApiEndpoints';
|
||||||
|
import { InvenTreeIcon } from '../../../functions/icons';
|
||||||
|
import { useEditApiFormModal } from '../../../hooks/UseForm';
|
||||||
|
import { apiUrl } from '../../../states/ApiState';
|
||||||
import { panelOptions } from '../PartPricingPanel';
|
import { panelOptions } from '../PartPricingPanel';
|
||||||
|
|
||||||
interface PricingOverviewEntry {
|
interface PricingOverviewEntry {
|
||||||
@ -38,12 +51,73 @@ interface PricingOverviewEntry {
|
|||||||
export default function PricingOverviewPanel({
|
export default function PricingOverviewPanel({
|
||||||
part,
|
part,
|
||||||
pricing,
|
pricing,
|
||||||
|
pricingQuery,
|
||||||
doNavigation
|
doNavigation
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
part: any;
|
part: any;
|
||||||
pricing: any;
|
pricing: any;
|
||||||
|
pricingQuery: UseQueryResult;
|
||||||
doNavigation: (panel: panelOptions) => void;
|
doNavigation: (panel: panelOptions) => void;
|
||||||
}>): ReactNode {
|
}>): ReactNode {
|
||||||
|
const refreshPricing = useCallback(() => {
|
||||||
|
const url = apiUrl(ApiEndpoints.part_pricing, part.pk);
|
||||||
|
|
||||||
|
notifications.hide('pricing-refresh');
|
||||||
|
|
||||||
|
notifications.show({
|
||||||
|
message: t`Refreshing pricing data`,
|
||||||
|
color: 'green',
|
||||||
|
id: 'pricing-refresh',
|
||||||
|
loading: true,
|
||||||
|
autoClose: false
|
||||||
|
});
|
||||||
|
|
||||||
|
let success: boolean = false;
|
||||||
|
|
||||||
|
api
|
||||||
|
.patch(url, { update: true })
|
||||||
|
.then((response) => {
|
||||||
|
success = response.status === 200;
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
.finally(() => {
|
||||||
|
notifications.hide('pricing-refresh');
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
notifications.show({
|
||||||
|
message: t`Pricing data updated`,
|
||||||
|
color: 'green',
|
||||||
|
icon: <IconCircleCheck />
|
||||||
|
});
|
||||||
|
pricingQuery.refetch();
|
||||||
|
} else {
|
||||||
|
notifications.show({
|
||||||
|
message: t`Failed to update pricing data`,
|
||||||
|
color: 'red',
|
||||||
|
icon: <IconExclamationCircle />
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [part]);
|
||||||
|
|
||||||
|
const editPricing = useEditApiFormModal({
|
||||||
|
title: t`Edit Pricing`,
|
||||||
|
url: apiUrl(ApiEndpoints.part_pricing, part.pk),
|
||||||
|
fields: {
|
||||||
|
override_min: {},
|
||||||
|
override_min_currency: {},
|
||||||
|
override_max: {},
|
||||||
|
override_max_currency: {},
|
||||||
|
update: {
|
||||||
|
hidden: true,
|
||||||
|
value: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onFormSuccess: () => {
|
||||||
|
pricingQuery.refetch();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const columns: any[] = useMemo(() => {
|
const columns: any[] = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@ -163,20 +237,49 @@ export default function PricingOverviewPanel({
|
|||||||
});
|
});
|
||||||
}, [part, pricing]);
|
}, [part, pricing]);
|
||||||
|
|
||||||
// TODO: Add display of "last updated"
|
|
||||||
// TODO: Add "update now" button
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
{editPricing.modal}
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
<SimpleGrid cols={2}>
|
<SimpleGrid cols={2}>
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
{pricing?.updated && (
|
|
||||||
<Paper p="xs">
|
<Paper p="xs">
|
||||||
<Alert color="blue" title={t`Last Updated`}>
|
<Group justify="space-between" wrap="nowrap">
|
||||||
|
{pricing?.updated ? (
|
||||||
|
<Alert color="blue" title={t`Last Updated`} flex={1}>
|
||||||
<Text>{formatDate(pricing.updated)}</Text>
|
<Text>{formatDate(pricing.updated)}</Text>
|
||||||
</Alert>
|
</Alert>
|
||||||
</Paper>
|
) : (
|
||||||
|
<Alert color="orange" title={t`Pricing Not Set`} flex={1}>
|
||||||
|
<Text>{t`Pricing data has not been calculated for this part`}</Text>
|
||||||
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
<OptionsActionDropdown
|
||||||
|
tooltip={t`Pricing Actions`}
|
||||||
|
actions={[
|
||||||
|
{
|
||||||
|
name: t`Refresh`,
|
||||||
|
tooltip: t`Refresh pricing data`,
|
||||||
|
icon: (
|
||||||
|
<InvenTreeIcon
|
||||||
|
icon="refresh"
|
||||||
|
iconProps={{ color: 'green' }}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
onClick: () => {
|
||||||
|
refreshPricing();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
EditItemAction({
|
||||||
|
onClick: () => {
|
||||||
|
editPricing.open();
|
||||||
|
},
|
||||||
|
tooltip: t`Edit pricing data`
|
||||||
|
})
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</Paper>
|
||||||
<DataTable
|
<DataTable
|
||||||
idAccessor="name"
|
idAccessor="name"
|
||||||
records={overviewData}
|
records={overviewData}
|
||||||
@ -192,9 +295,12 @@ export default function PricingOverviewPanel({
|
|||||||
{ name: 'min_value', label: t`Minimum Value`, color: 'blue.6' },
|
{ name: 'min_value', label: t`Minimum Value`, color: 'blue.6' },
|
||||||
{ name: 'max_value', label: t`Maximum Value`, color: 'teal.6' }
|
{ name: 'max_value', label: t`Maximum Value`, color: 'teal.6' }
|
||||||
]}
|
]}
|
||||||
valueFormatter={(value) => tooltipFormatter(value, pricing?.currency)}
|
valueFormatter={(value) =>
|
||||||
|
tooltipFormatter(value, pricing?.currency)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ export default function PurchaseHistoryPanel({
|
|||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
part: any;
|
part: any;
|
||||||
}>): ReactNode {
|
}>): ReactNode {
|
||||||
const table = useTable('pricing-purchase-history');
|
const table = useTable('pricingpurchasehistory');
|
||||||
|
|
||||||
const calculateUnitPrice = useCallback((record: any) => {
|
const calculateUnitPrice = useCallback((record: any) => {
|
||||||
let pack_quantity = record?.supplier_part_detail?.pack_quantity_native ?? 1;
|
let pack_quantity = record?.supplier_part_detail?.pack_quantity_native ?? 1;
|
||||||
|
@ -15,7 +15,7 @@ import { NoPricingData } from './PricingPanel';
|
|||||||
export default function SaleHistoryPanel({
|
export default function SaleHistoryPanel({
|
||||||
part
|
part
|
||||||
}: Readonly<{ part: any }>): ReactNode {
|
}: Readonly<{ part: any }>): ReactNode {
|
||||||
const table = useTable('pricing-sale-history');
|
const table = useTable('pricingsalehistory');
|
||||||
|
|
||||||
const columns: TableColumn[] = useMemo(() => {
|
const columns: TableColumn[] = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
|
@ -18,7 +18,7 @@ import { NoPricingData } from './PricingPanel';
|
|||||||
export default function SupplierPricingPanel({
|
export default function SupplierPricingPanel({
|
||||||
part
|
part
|
||||||
}: Readonly<{ part: any }>) {
|
}: Readonly<{ part: any }>) {
|
||||||
const table = useTable('pricing-supplier');
|
const table = useTable('pricingsupplier');
|
||||||
|
|
||||||
const columns: TableColumn[] = useMemo(() => {
|
const columns: TableColumn[] = useMemo(() => {
|
||||||
return SupplierPriceBreakColumns();
|
return SupplierPriceBreakColumns();
|
||||||
|
@ -21,7 +21,7 @@ export default function VariantPricingPanel({
|
|||||||
part: any;
|
part: any;
|
||||||
pricing: any;
|
pricing: any;
|
||||||
}>): ReactNode {
|
}>): ReactNode {
|
||||||
const table = useTable('pricing-variants');
|
const table = useTable('pricingvariants');
|
||||||
|
|
||||||
const columns: TableColumn[] = useMemo(() => {
|
const columns: TableColumn[] = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
|
@ -79,7 +79,7 @@ export const useLocalState = create<LocalStateProps>()(
|
|||||||
// tables
|
// tables
|
||||||
tableColumnNames: {},
|
tableColumnNames: {},
|
||||||
getTableColumnNames: (tableKey) => {
|
getTableColumnNames: (tableKey) => {
|
||||||
return get().tableColumnNames[tableKey] || {};
|
return get().tableColumnNames[tableKey] || null;
|
||||||
},
|
},
|
||||||
setTableColumnNames: (tableKey) => (names) => {
|
setTableColumnNames: (tableKey) => (names) => {
|
||||||
// Update the table column names for the given table
|
// Update the table column names for the given table
|
||||||
|
@ -179,10 +179,17 @@ export function InvenTreeTable<T extends Record<string, any>>({
|
|||||||
queryKey: ['options', url, tableState.tableKey, props.enableColumnCaching],
|
queryKey: ['options', url, tableState.tableKey, props.enableColumnCaching],
|
||||||
retry: 3,
|
retry: 3,
|
||||||
refetchOnMount: true,
|
refetchOnMount: true,
|
||||||
|
gcTime: 5000,
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (props.enableColumnCaching == false) {
|
if (props.enableColumnCaching == false) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we already have field names, no need to fetch them again
|
||||||
|
if (fieldNames && Object.keys(fieldNames).length > 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return api
|
return api
|
||||||
.options(url, {
|
.options(url, {
|
||||||
params: tableProps.params
|
params: tableProps.params
|
||||||
@ -203,7 +210,7 @@ export function InvenTreeTable<T extends Record<string, any>>({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const cacheKey = tableState.tableKey.split('-')[0];
|
const cacheKey = tableState.tableKey.replaceAll('-', '');
|
||||||
|
|
||||||
setFieldNames(names);
|
setFieldNames(names);
|
||||||
setTableColumnNames(cacheKey)(names);
|
setTableColumnNames(cacheKey)(names);
|
||||||
@ -220,20 +227,19 @@ export function InvenTreeTable<T extends Record<string, any>>({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cacheKey = tableState.tableKey.split('-')[0];
|
const cacheKey = tableState.tableKey.replaceAll('-', '');
|
||||||
|
|
||||||
// First check the local cache
|
// First check the local cache
|
||||||
const cachedNames = getTableColumnNames(cacheKey);
|
const cachedNames = getTableColumnNames(cacheKey);
|
||||||
|
|
||||||
if (Object.keys(cachedNames).length > 0) {
|
if (cachedNames != null) {
|
||||||
// Cached names are available - use them!
|
// Cached names are available - use them!
|
||||||
setFieldNames(cachedNames);
|
setFieldNames(cachedNames);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, fetch the data from the API
|
|
||||||
tableOptionQuery.refetch();
|
tableOptionQuery.refetch();
|
||||||
}, [url, tableState.tableKey, props.params, props.enableColumnCaching]);
|
}, [url, props.params, props.enableColumnCaching]);
|
||||||
|
|
||||||
// Build table properties based on provided props (and default props)
|
// Build table properties based on provided props (and default props)
|
||||||
const tableProps: InvenTreeTableProps<T> = useMemo(() => {
|
const tableProps: InvenTreeTableProps<T> = useMemo(() => {
|
||||||
@ -502,6 +508,8 @@ export function InvenTreeTable<T extends Record<string, any>>({
|
|||||||
|
|
||||||
const { data, isFetching, isLoading, refetch } = useQuery({
|
const { data, isFetching, isLoading, refetch } = useQuery({
|
||||||
queryKey: [
|
queryKey: [
|
||||||
|
'tabledata',
|
||||||
|
url,
|
||||||
tableState.page,
|
tableState.page,
|
||||||
props.params,
|
props.params,
|
||||||
sortStatus.columnAccessor,
|
sortStatus.columnAccessor,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user