From 2c5255cd13ebb8edf673e10c38c45c3f9807571a Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 26 Sep 2025 11:51:45 -0400 Subject: [PATCH] Restyle config panel and results table --- .../discount-simulator/ConfigPanel.tsx | 478 +++++++++--------- .../discount-simulator/ResultsTable.tsx | 264 +++++++--- .../src/components/ui/date-range-picker.tsx | 2 +- 3 files changed, 443 insertions(+), 301 deletions(-) diff --git a/inventory/src/components/discount-simulator/ConfigPanel.tsx b/inventory/src/components/discount-simulator/ConfigPanel.tsx index b62598f..4b6d473 100644 --- a/inventory/src/components/discount-simulator/ConfigPanel.tsx +++ b/inventory/src/components/discount-simulator/ConfigPanel.tsx @@ -9,8 +9,6 @@ import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { ScrollArea } from "@/components/ui/scroll-area"; import { DiscountPromoOption, DiscountPromoType, ShippingPromoType, ShippingTierConfig, DiscountSimulationResponse, CogsCalculationMode } from "@/types/discount-simulator"; -import { Separator } from "@/components/ui/separator"; -import { Badge } from "@/components/ui/badge"; import { formatNumber } from "@/utils/productUtils"; import { PlusIcon, X } from "lucide-react"; import { Skeleton } from "@/components/ui/skeleton"; @@ -118,8 +116,6 @@ export function ConfigPanel({ promoLoading = false, onRunSimulation, isRunning, - recommendedPointDollarValue, - onApplyRecommendedPointDollarValue, result }: ConfigPanelProps) { const promoOptions = useMemo(() => { @@ -171,21 +167,22 @@ export function ConfigPanel({ if (!current) { return; } + const tierId = current.id ?? generateTierId(); - tiers[index] = { + const mergedTier = { ...current, ...update, id: tierId, }; - const sorted = tiers - .filter((tier) => tier != null) - .map((tier) => ({ - ...tier, - threshold: Number.isFinite(tier.threshold) ? tier.threshold : 0, - value: Number.isFinite(tier.value) ? tier.value : 0, - })) - .sort((a, b) => a.threshold - b.threshold); - onShippingTiersChange(sorted); + + const normalizedTier: ShippingTierConfig = { + ...mergedTier, + threshold: Number.isFinite(mergedTier.threshold) ? mergedTier.threshold ?? 0 : 0, + value: Number.isFinite(mergedTier.value) ? mergedTier.value ?? 0 : 0, + }; + + tiers[index] = normalizedTier; + onShippingTiersChange(tiers); }; const handleTierRemove = (index: number) => { @@ -209,118 +206,152 @@ export function ConfigPanel({ onShippingTiersChange(tiers); }; - const recommendedDiffers = typeof recommendedPointDollarValue === 'number' - ? recommendedPointDollarValue !== pointDollarValue - : false; - - const fieldClass = "flex flex-col gap-1.5"; - const labelClass = "text-[0.65rem] uppercase tracking-wide text-muted-foreground"; - const sectionTitleClass = "text-xs font-semibold uppercase tracking-wide text-muted-foreground"; - const sectionClass = "flex flex-col gap-3"; - const fieldRowClass = "flex flex-col gap-3"; - const fieldRowHorizontalClass = "flex gap-3"; - const compactTriggerClass = "h-8 px-2 text-xs"; - const compactNumberClass = "h-8 px-2 text-sm"; - const compactWideNumberClass = "h-8 px-2 text-sm"; - const showProductAdjustments = productPromo.type !== "none"; - const showShippingAdjustments = shippingPromo.type !== "none"; - const promoSelectValue = selectedPromoId != null ? selectedPromoId.toString() : "__all__"; - const handleFieldBlur = useCallback(() => { setTimeout(onRunSimulation, 0); }, [onRunSimulation]); + const sortShippingTiers = useCallback(() => { + if (shippingTiers.length < 2) { + return; + } + + const originalIds = shippingTiers.map((tier) => tier.id); + const sortedTiers = [...shippingTiers] + .map((tier) => ({ + ...tier, + threshold: Number.isFinite(tier.threshold) ? tier.threshold : 0, + value: Number.isFinite(tier.value) ? tier.value : 0, + })) + .sort((a, b) => a.threshold - b.threshold); + + const orderChanged = sortedTiers.some((tier, index) => tier.id !== originalIds[index]); + if (orderChanged) { + onShippingTiersChange(sortedTiers); + } + }, [shippingTiers, onShippingTiersChange]); + + const handleTierBlur = useCallback(() => { + sortShippingTiers(); + handleFieldBlur(); + }, [sortShippingTiers, handleFieldBlur]); + + + const sectionTitleClass = "text-[0.65rem] font-semibold uppercase tracking-[0.18em] text-muted-foreground"; + const sectionBaseClass = "flex flex-col rounded-md border border-border/60 bg-muted/30 px-3 py-2.5"; + const sectionClass = `${sectionBaseClass} gap-3`; + const compactSectionClass = `${sectionBaseClass} gap-2`; + const sectionHeaderClass = "flex items-center justify-between"; + const fieldClass = "flex flex-col gap-1"; + const labelClass = "text-[0.65rem] uppercase tracking-wide text-muted-foreground"; + const fieldRowClass = "flex flex-col gap-2"; + const fieldRowHorizontalClass = "flex flex-col gap-2 sm:flex-row sm:items-end sm:gap-3"; + const compactTriggerClass = "h-8 px-2 text-xs"; + const compactNumberClass = "h-8 px-2 text-sm"; + const compactWideNumberClass = "h-8 px-2 text-sm"; + const metricPillClass = "flex items-center gap-1 rounded border border-border/60 bg-background px-2 py-1 text-[0.68rem] font-medium text-foreground"; + const showProductAdjustments = productPromo.type !== "none"; + const showShippingAdjustments = shippingPromo.type !== "none"; + const promoSelectValue = selectedPromoId != null ? selectedPromoId.toString() : "__all__"; + return ( - -
+ +
- Calculated Metrics Filters
-
- - onDateRangeChange(range)} - /> -
-
- - { + if (value === '__all__') { + onPromoChange(undefined); + return; + } + const parsed = Number(value); + onPromoChange(Number.isNaN(parsed) ? undefined : parsed); + }} + > + + {promoLoading ? ( +
+
- - ))} - - + ) : ( + + {promoSelectValue === "__all__" + ? "All promos" + : promoOptions.find(p => p.value === promoSelectValue)?.label + } + + )} +
+ + All promos + {promoOptions.map((promo) => ( + +
+ {promo.label} + {promo.description && ( + {promo.description} + )} +
+
+ ))} +
+ +
-
- - {/* Calculated Metrics */} + {result?.totals && ( -
- - {formatNumber(result.totals.orders)} orders - - - {formatPercent(result.totals.productDiscountRate)} avg discount - +
+
+ {formatNumber(result.totals.orders)} + orders +
+
+ {formatPercent(result.totals.productDiscountRate)} + avg discount +
{result.totals.overallCogsPercentage != null && ( - - {formatPercent(result.totals.overallCogsPercentage)} avg COGS - +
+ {formatPercent(result.totals.overallCogsPercentage)} + avg COGS +
)}
)} -
- Product promo +
+ Product promo +
- - -
+ + +
{showProductAdjustments && (
@@ -359,24 +390,26 @@ export function ConfigPanel({
- Shipping promo +
+ Shipping promo +
- - -
+ + +
{showShippingAdjustments && ( <>
@@ -430,100 +463,100 @@ export function ConfigPanel({
-
-
- Shipping tiers - -
- {shippingTiers.length === 0 ? ( -

Add tiers to model automatic shipping discounts.

- ) : ( - -
- {/* Header row */} -
-
$ Amount
-
Type
-
Value
-
+
+
+ Shipping tiers + +
+ {shippingTiers.length === 0 ? ( +

Add tiers to model automatic shipping discounts.

+ ) : ( + +
+
+
Amount
+
Type
+
Value
+ + {shippingTiers.map((tier, index) => { + const tierKey = tier.id ?? `tier-${index}`; + return ( +
+
+ { + onConfigInputChange(); + handleTierUpdate(index, { + threshold: parseNumber(event.target.value, 0), + }); + }} + onBlur={handleTierBlur} + /> +
+
+ +
+
+ { + onConfigInputChange(); + handleTierUpdate(index, { value: parseNumber(event.target.value, 0) }); + }} + onBlur={handleTierBlur} + /> +
+ + ); + })}
- {shippingTiers.map((tier, index) => { - const tierKey = tier.id ?? `tier-${index}`; - return ( -
-
- { - onConfigInputChange(); - handleTierUpdate(index, { - threshold: parseNumber(event.target.value, 0), - }); - }} - onBlur={handleFieldBlur} - /> -
-
- -
-
- { - onConfigInputChange(); - handleTierUpdate(index, { value: parseNumber(event.target.value, 0) }); - }} - onBlur={handleFieldBlur} - /> -
-
-
- -
-
- ); - })} -
- - )} -
- + + )} +
- Order costs +
+ Order costs +
@@ -559,7 +592,7 @@ export function ConfigPanel({ />
- +
- Rewards points +
+ Rewards points +
@@ -585,7 +620,7 @@ export function ConfigPanel({ {Number.isFinite(pointsPerDollar) ? pointsPerDollar.toFixed(4) : '—'}
- Redemption rate (%) + Redemption rate {formatPercent(redemptionRate)}
@@ -603,15 +638,6 @@ export function ConfigPanel({ onBlur={handleFieldBlur} />
- {typeof recommendedPointDollarValue === 'number' && ( -
- {recommendedDiffers && onApplyRecommendedPointDollarValue && ( - - )} -
- )}
diff --git a/inventory/src/components/discount-simulator/ResultsTable.tsx b/inventory/src/components/discount-simulator/ResultsTable.tsx index ab214de..da7e33e 100644 --- a/inventory/src/components/discount-simulator/ResultsTable.tsx +++ b/inventory/src/components/discount-simulator/ResultsTable.tsx @@ -1,3 +1,5 @@ +import { Fragment } from "react"; + import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Table, @@ -16,6 +18,7 @@ import { import { DiscountSimulationBucket } from "@/types/discount-simulator"; import { formatCurrency, formatNumber } from "@/utils/productUtils"; import { Skeleton } from "@/components/ui/skeleton"; +import { cn } from "@/lib/utils"; // Utility function to interpolate between two colors const interpolateColor = (color1: [number, number, number], color2: [number, number, number], ratio: number): [number, number, number] => { @@ -63,36 +66,81 @@ interface ResultsTableProps { isLoading: boolean; } -const rowLabels = [ - // Most important metrics first - prominently styled - { key: "totalRevenue", label: "Total Revenue", format: (value: number) => formatCurrency(value), important: true, isCalculated: true }, - { key: "totalCosts", label: "Total Cost", format: (value: number) => formatCurrency(value), important: true, isCalculated: true }, - { key: "profit", label: "Profit $", format: (value: number) => formatCurrency(value), important: true, isCalculated: true }, - { key: "profitPercent", label: "Profit %", format: (value: number) => `${(value * 100).toFixed(2)}%`, important: true, isCalculated: true }, - - // Order metrics - from database - { key: "orderCount", label: "Orders", format: (value: number) => formatNumber(value), important: false, isCalculated: false }, - { key: "weight", label: "Weight", format: (value: number) => `${(value * 100).toFixed(2)}%`, important: false, isCalculated: false }, - { key: "orderValue", label: "Order Value", format: (value: number) => formatCurrency(value), important: false, isCalculated: false }, - - // Customer costs - { key: "customerItemCost", label: "Cust Item Cost", format: (value: number) => formatCurrency(value), important: false, isCalculated: true }, - { key: "customerShipCost", label: "Cust Ship Cost", format: (value: number) => formatCurrency(value), important: false, isCalculated: true }, - - // Discounts - { key: "productDiscountAmount", label: "Prod Discount", format: (value: number) => formatCurrency(value), important: false, isCalculated: true }, - { key: "promoProductDiscount", label: "Promo Prod Disc", format: (value: number) => formatCurrency(value), important: false, isCalculated: true }, - { key: "shipPromoDiscount", label: "Ship Promo Disc", format: (value: number) => formatCurrency(value), important: false, isCalculated: true }, - - // Shipping - { key: "shippingAfterAuto", label: "Ship Charge", format: (value: number) => formatCurrency(value), important: false, isCalculated: true }, - { key: "actualShippingCost", label: "Actual Ship Cost", format: (value: number) => formatCurrency(value), important: false, isCalculated: false }, - - // Cost breakdown - { key: "productCogs", label: "Product COGS", format: (value: number) => formatCurrency(value), important: false, isCalculated: false }, - { key: "merchantFees", label: "Merchant Fees", format: (value: number) => formatCurrency(value), important: false, isCalculated: true }, - { key: "pointsCost", label: "Points Cost", format: (value: number) => formatCurrency(value), important: false, isCalculated: true }, - { key: "fixedCosts", label: "Fixed Cost", format: (value: number) => formatCurrency(value), important: false, isCalculated: true }, +type MetricGroup = + | "orderVolume" + | "customerSpend" + | "discounts" + | "shipping" + | "costs" + | "outcomes"; + +const metricGroupStyles: Record = { + orderVolume: { + label: "Order Volume", + indicatorClass: "bg-sky-500", + badgeClass: "bg-sky-100 text-sky-900 border-sky-200 uppercase", + rowClass: "bg-sky-50", + }, + customerSpend: { + label: "Customer Spend", + indicatorClass: "bg-indigo-500", + badgeClass: "bg-indigo-100 text-indigo-900 border-indigo-200 uppercase", + rowClass: "bg-indigo-50", + }, + discounts: { + label: "Discounts", + indicatorClass: "bg-amber-500", + badgeClass: "bg-amber-100 text-amber-900 border-amber-200 uppercase", + rowClass: "bg-amber-50", + }, + shipping: { + label: "Shipping Impact", + indicatorClass: "bg-cyan-500", + badgeClass: "bg-cyan-100 text-cyan-900 border-cyan-200 uppercase", + rowClass: "bg-cyan-50", + }, + costs: { + label: "Cost Inputs", + indicatorClass: "bg-rose-500", + badgeClass: "bg-rose-100 text-rose-900 border-rose-200 uppercase", + rowClass: "bg-rose-50", + }, + outcomes: { + label: "Results", + indicatorClass: "bg-emerald-600", + badgeClass: "bg-emerald-100 text-emerald-900 border-emerald-200 uppercase", + rowClass: "bg-emerald-50", + }, +}; + +interface RowConfig { + key: keyof DiscountSimulationBucket; + label: string; + format: (value: number) => string; + important?: boolean; + isCalculated: boolean; + group: MetricGroup; +} + +const rowLabels: RowConfig[] = [ + { key: "orderCount", label: "Orders", format: (value: number) => formatNumber(value), isCalculated: false, group: "orderVolume" }, + { key: "weight", label: "Weight", format: (value: number) => `${(value * 100).toFixed(2)}%`, isCalculated: false, group: "orderVolume" }, + { key: "orderValue", label: "Order Value", format: (value: number) => formatCurrency(value), isCalculated: false, group: "customerSpend" }, + { key: "customerItemCost", label: "Cust Item Cost", format: (value: number) => formatCurrency(value), isCalculated: true, group: "customerSpend" }, + { key: "customerShipCost", label: "Cust Ship Cost", format: (value: number) => formatCurrency(value), isCalculated: true, group: "customerSpend" }, + { key: "productDiscountAmount", label: "Prod Discount", format: (value: number) => formatCurrency(value), isCalculated: true, group: "discounts" }, + { key: "promoProductDiscount", label: "Promo Prod Disc", format: (value: number) => formatCurrency(value), isCalculated: true, group: "discounts" }, + { key: "shipPromoDiscount", label: "Ship Promo Disc", format: (value: number) => formatCurrency(value), isCalculated: true, group: "discounts" }, + { key: "shippingAfterAuto", label: "Ship Charge", format: (value: number) => formatCurrency(value), isCalculated: true, group: "shipping" }, + { key: "actualShippingCost", label: "Actual Ship Cost", format: (value: number) => formatCurrency(value), isCalculated: false, group: "shipping" }, + { key: "productCogs", label: "Product COGS", format: (value: number) => formatCurrency(value), isCalculated: false, group: "costs" }, + { key: "merchantFees", label: "Merchant Fees", format: (value: number) => formatCurrency(value), isCalculated: true, group: "costs" }, + { key: "pointsCost", label: "Points Cost", format: (value: number) => formatCurrency(value), isCalculated: true, group: "costs" }, + { key: "fixedCosts", label: "Fixed Cost", format: (value: number) => formatCurrency(value), isCalculated: true, group: "costs" }, + { key: "totalRevenue", label: "Total Revenue", format: (value: number) => formatCurrency(value), important: true, isCalculated: true, group: "outcomes" }, + { key: "totalCosts", label: "Total Cost", format: (value: number) => formatCurrency(value), important: true, isCalculated: true, group: "outcomes" }, + { key: "profit", label: "Profit $", format: (value: number) => formatCurrency(value), important: true, isCalculated: true, group: "outcomes" }, + { key: "profitPercent", label: "Profit %", format: (value: number) => `${(value * 100).toFixed(2)}%`, important: true, isCalculated: true, group: "outcomes" }, ]; const formatRangeUpperBound = (bucket: DiscountSimulationBucket) => { @@ -123,7 +171,7 @@ export function ResultsTable({ buckets, isLoading }: ResultsTableProps) {
- + {buckets.map((bucket) => ( ))} @@ -146,51 +194,119 @@ export function ResultsTable({ buckets, isLoading }: ResultsTableProps) { ) : ( - rowLabels.map((row) => ( - - -
- {/* Subtle indicator for data source */} - - - - - - -

{row.isCalculated ? 'Calculated value' : 'Database value'}

-
-
-
- {row.label} -
-
- {buckets.map((bucket) => { - const value = bucket[row.key as keyof DiscountSimulationBucket] as number; - const formattedValue = row.format(value); - - // Apply color gradient for profit percentage - if (row.key === 'profitPercent') { - const backgroundColor = getProfitPercentageColor(value); - return ( - - { + const groupStyle = metricGroupStyles[row.group]; + const isImportant = Boolean(row.important); + const prevGroup = index > 0 ? rowLabels[index - 1].group : null; + const isFirstOfGroup = prevGroup !== row.group; + + return ( + + {isFirstOfGroup && ( + + +
+ +
+ {groupStyle.label} +
+
+
+
+ )} + + +
+ + + {row.label} + + + + + + + +

{row.isCalculated ? "Calculated value" : "Database value"}

+
+
+
+
+
+ {buckets.map((bucket) => { + const value = bucket[row.key as keyof DiscountSimulationBucket] as number; + const formattedValue = row.format(value); + + if (row.key === "profitPercent") { + const backgroundColor = getProfitPercentageColor(value); + return ( + + + {formattedValue} + + + ); + } + + return ( + {formattedValue} -
-
- ); - } - - return ( - - {formattedValue} - - ); - })} -
- )) + + ); + })} + + + ); + }) )}
diff --git a/inventory/src/components/ui/date-range-picker.tsx b/inventory/src/components/ui/date-range-picker.tsx index a03c0c0..c924817 100644 --- a/inventory/src/components/ui/date-range-picker.tsx +++ b/inventory/src/components/ui/date-range-picker.tsx @@ -29,7 +29,7 @@ export function DateRangePicker({ id="date" variant={"outline"} className={cn( - "h-8 w-full justify-start text-left font-normal", + "h-8 px-2 justify-start text-left font-normal", !value && "text-muted-foreground" )} >