diff --git a/src/frontend/src/components/charts/tooltipFormatter.tsx b/src/frontend/src/components/charts/tooltipFormatter.tsx new file mode 100644 index 0000000000..73b3b97703 --- /dev/null +++ b/src/frontend/src/components/charts/tooltipFormatter.tsx @@ -0,0 +1,9 @@ +import { formatCurrency } from '../../defaults/formatters'; + +export function tooltipFormatter(label: any, currency: string) { + return ( + formatCurrency(label, { + currency: currency + })?.toString() ?? '' + ); +} diff --git a/src/frontend/src/pages/part/PartPricingPanel.tsx b/src/frontend/src/pages/part/PartPricingPanel.tsx index 0f570e681e..44e8439405 100644 --- a/src/frontend/src/pages/part/PartPricingPanel.tsx +++ b/src/frontend/src/pages/part/PartPricingPanel.tsx @@ -1,6 +1,6 @@ import { t } from '@lingui/macro'; import { Accordion, Alert, LoadingOverlay, Stack, Text } from '@mantine/core'; -import { ReactNode, useMemo } from 'react'; +import { useMemo, useState } from 'react'; import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { UserRoles } from '../../enums/Roles'; @@ -15,6 +15,19 @@ import SaleHistoryPanel from './pricing/SaleHistoryPanel'; import SupplierPricingPanel from './pricing/SupplierPricingPanel'; import VariantPricingPanel from './pricing/VariantPricingPanel'; +export enum panelOptions { + overview = 'overview', + purchase = 'purchase', + internal = 'internal', + supplier = 'supplier', + bom = 'bom', + variant = 'variant', + sale_pricing = 'sale-pricing', + sale_history = 'sale-history', + override = 'override', + overall = 'overall' +} + export default function PartPricingPanel({ part }: { part: any }) { const user = useUserState(); @@ -40,6 +53,17 @@ export default function PartPricingPanel({ part }: { part: any }) { return user.hasViewRole(UserRoles.sales_order) && part?.salable; }, [user, part]); + const [value, setValue] = useState([panelOptions.overview]); + function doNavigation(panel: panelOptions) { + if (!value.includes(panel)) { + setValue([...value, panel]); + } + const element = document.getElementById(panel); + if (element) { + element.scrollIntoView(); + } + } + return ( @@ -49,18 +73,27 @@ export default function PartPricingPanel({ part }: { part: any }) { )} {pricing && ( - + } - label="overview" + content={ + + } + label={panelOptions.overview} title={t`Pricing Overview`} visible={true} /> } - label="purchase" + label={panelOptions.purchase} title={t`Purchase History`} visible={purchaseOrderPricing} + disabled={ + !pricing?.purchase_cost_min || !pricing?.purchase_cost_max + } /> } - label="internal" + label={panelOptions.internal} title={t`Internal Pricing`} visible={internalPricing} + disabled={ + !pricing?.internal_cost_min || !pricing?.internal_cost_max + } /> } - label="supplier" + label={panelOptions.supplier} title={t`Supplier Pricing`} visible={purchaseOrderPricing} + disabled={ + !pricing?.supplier_price_min || !pricing?.supplier_price_max + } /> } - label="bom" + label={panelOptions.bom} title={t`BOM Pricing`} visible={part?.assembly} + disabled={!pricing?.bom_cost_min || !pricing?.bom_cost_max} /> } - label="variant" + label={panelOptions.variant} title={t`Variant Pricing`} visible={part?.is_template} + disabled={!pricing?.variant_cost_min || !pricing?.variant_cost_max} /> } - label="sale-pricing" + label={panelOptions.sale_pricing} title={t`Sale Pricing`} visible={salesOrderPricing} + disabled={!pricing?.sale_price_min || !pricing?.sale_price_max} /> } - label="sale-history" + label={panelOptions.sale_history} title={t`Sale History`} visible={salesOrderPricing} + disabled={!pricing?.sale_history_min || !pricing?.sale_history_max} /> )} diff --git a/src/frontend/src/pages/part/pricing/BomPricingPanel.tsx b/src/frontend/src/pages/part/pricing/BomPricingPanel.tsx index 98a561d6c0..b8228b0334 100644 --- a/src/frontend/src/pages/part/pricing/BomPricingPanel.tsx +++ b/src/frontend/src/pages/part/pricing/BomPricingPanel.tsx @@ -21,7 +21,12 @@ import { } from 'recharts'; import { CHART_COLORS } from '../../../components/charts/colors'; -import { formatDecimal, formatPriceRange } from '../../../defaults/formatters'; +import { tooltipFormatter } from '../../../components/charts/tooltipFormatter'; +import { + formatCurrency, + formatDecimal, + formatPriceRange +} from '../../../defaults/formatters'; import { ApiEndpoints } from '../../../enums/ApiEndpoints'; import { ModelType } from '../../../enums/ModelType'; import { useTable } from '../../../hooks/UseTable'; @@ -32,7 +37,7 @@ import { InvenTreeTable } from '../../../tables/InvenTreeTable'; import { NoPricingData } from './PricingPanel'; // Display BOM data as a pie chart -function BomPieChart({ data }: { data: any[] }) { +function BomPieChart({ data, currency }: { data: any[]; currency: string }) { return ( @@ -64,20 +69,30 @@ function BomPieChart({ data }: { data: any[] }) { /> ))} - + tooltipFormatter(label, currency)} + /> ); } // Display BOM data as a bar chart -function BomBarChart({ data }: { data: any[] }) { +function BomBarChart({ data, currency }: { data: any[]; currency: string }) { return ( - - + + formatCurrency(value, { + currency: currency + })?.toString() ?? '' + } + /> + tooltipFormatter(label, currency)} + /> {bomPricingData.length > 0 ? ( - {chartType == 'bar' && } - {chartType == 'pie' && } + {chartType == 'bar' && ( + + )} + {chartType == 'pie' && ( + + )} { + if (table.records.length === 0) { + return ''; + } + return table.records[0].currency; + }, [table.records]); + return ( <> {newPriceBreak.modal} @@ -166,8 +174,18 @@ export default function PriceBreakPanel({ - - + + formatCurrency(value, { + currency: currency + })?.toString() ?? '' + } + /> + + tooltipFormatter(label, currency) + } + /> void; }): ReactNode { const columns: DataTableColumn[] = useMemo(() => { return [ @@ -47,10 +59,17 @@ export default function PricingOverviewPanel({ accessor: 'title', title: t`Pricing Category`, render: (record: PricingOverviewEntry) => { + const is_link = record.name !== panelOptions.overall; return ( {record.icon} - {record.title} + {is_link ? ( + doNavigation(record.name)}> + {record.title} + + ) : ( + {record.title} + )} ); } @@ -86,56 +105,70 @@ export default function PricingOverviewPanel({ const overviewData: PricingOverviewEntry[] = useMemo(() => { return [ { - name: 'internal', + name: panelOptions.internal, title: t`Internal Pricing`, icon: , min_value: pricing?.internal_cost_min, max_value: pricing?.internal_cost_max }, { - name: 'bom', + name: panelOptions.bom, title: t`BOM Pricing`, icon: , min_value: pricing?.bom_cost_min, max_value: pricing?.bom_cost_max }, { - name: 'purchase', + name: panelOptions.purchase, title: t`Purchase Pricing`, icon: , min_value: pricing?.purchase_cost_min, max_value: pricing?.purchase_cost_max }, { - name: 'supplier', + name: panelOptions.supplier, title: t`Supplier Pricing`, icon: , min_value: pricing?.supplier_price_min, max_value: pricing?.supplier_price_max }, { - name: 'variants', + name: panelOptions.variant, title: t`Variant Pricing`, icon: , min_value: pricing?.variant_cost_min, max_value: pricing?.variant_cost_max }, { - name: 'override', + name: panelOptions.sale_pricing, + title: t`Sale Pricing`, + icon: , + min_value: pricing?.sale_price_min, + max_value: pricing?.sale_price_max + }, + { + name: panelOptions.sale_history, + title: t`Sale History`, + icon: , + min_value: pricing?.sale_history_min, + max_value: pricing?.sale_history_max + }, + { + name: panelOptions.override, title: t`Override Pricing`, icon: , min_value: pricing?.override_min, max_value: pricing?.override_max }, { - name: 'overall', + name: panelOptions.overall, title: t`Overall Pricing`, icon: , min_value: pricing?.overall_min, max_value: pricing?.overall_max } ].filter((entry) => { - return entry.min_value !== null || entry.max_value !== null; + return !(entry.min_value == null || entry.max_value == null); }); }, [part, pricing]); @@ -158,8 +191,18 @@ export default function PricingOverviewPanel({ - - + + formatCurrency(value, { + currency: pricing?.currency + })?.toString() ?? '' + } + /> + + tooltipFormatter(label, pricing?.currency) + } + /> + {props.disabled && ( + } + /> + )} + + + ); +} export default function PricingPanel({ content, label, title, - visible + visible, + disabled = undefined }: { content: ReactNode; - label: string; + label: panelOptions; title: string; visible: boolean; + disabled?: boolean | undefined; }): ReactNode { + const is_disabled = disabled === undefined ? false : disabled; return ( visible && ( - - + + {title} - - {content} + + {!is_disabled && content} ) ); diff --git a/src/frontend/src/pages/part/pricing/PurchaseHistoryPanel.tsx b/src/frontend/src/pages/part/pricing/PurchaseHistoryPanel.tsx index e71854efa9..ae88a6d7c4 100644 --- a/src/frontend/src/pages/part/pricing/PurchaseHistoryPanel.tsx +++ b/src/frontend/src/pages/part/pricing/PurchaseHistoryPanel.tsx @@ -12,6 +12,7 @@ import { } from 'recharts'; import { CHART_COLORS } from '../../../components/charts/colors'; +import { tooltipFormatter } from '../../../components/charts/tooltipFormatter'; import { formatCurrency, renderDate } from '../../../defaults/formatters'; import { ApiEndpoints } from '../../../enums/ApiEndpoints'; import { useTable } from '../../../hooks/UseTable'; @@ -95,6 +96,13 @@ export default function PurchaseHistoryPanel({ ]; }, []); + const currency: string = useMemo(() => { + if (table.records.length === 0) { + return ''; + } + return table.records[0].purchase_price_currency; + }, [table.records]); + const purchaseHistoryData = useMemo(() => { return table.records.map((record: any) => { return { @@ -126,8 +134,16 @@ export default function PurchaseHistoryPanel({ - - + + formatCurrency(value, { + currency: currency + })?.toString() ?? '' + } + /> + tooltipFormatter(label, currency)} + /> { + if (table.records.length === 0) { + return ''; + } + return table.records[0].sale_price_currency; + }, [table.records]); + const saleHistoryData = useMemo(() => { return table.records.map((record: any) => { return { @@ -90,8 +98,16 @@ export default function SaleHistoryPanel({ part }: { part: any }): ReactNode { - - + + formatCurrency(value, { + currency: currency + })?.toString() ?? '' + } + /> + tooltipFormatter(label, currency)} + /> { + if (table.records.length === 0) { + return ''; + } + return table.records[0].currency; + }, [table.records]); + const supplierPricingData = useMemo(() => { return table.records.map((record: any) => { return { @@ -58,8 +67,16 @@ export default function SupplierPricingPanel({ part }: { part: any }) { - - + + formatCurrency(value, { + currency: currency + })?.toString() ?? '' + } + /> + tooltipFormatter(label, currency)} + /> - - + + formatCurrency(value, { + currency: pricing?.currency + })?.toString() ?? '' + } + /> + + tooltipFormatter(label, pricing?.currency) + } + />