Restyle config panel and results table

This commit is contained in:
2025-09-26 11:51:45 -04:00
parent 1696ecf591
commit 2c5255cd13
3 changed files with 443 additions and 301 deletions

View File

@@ -9,8 +9,6 @@ import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
import { DiscountPromoOption, DiscountPromoType, ShippingPromoType, ShippingTierConfig, DiscountSimulationResponse, CogsCalculationMode } from "@/types/discount-simulator"; 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 { formatNumber } from "@/utils/productUtils";
import { PlusIcon, X } from "lucide-react"; import { PlusIcon, X } from "lucide-react";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
@@ -118,8 +116,6 @@ export function ConfigPanel({
promoLoading = false, promoLoading = false,
onRunSimulation, onRunSimulation,
isRunning, isRunning,
recommendedPointDollarValue,
onApplyRecommendedPointDollarValue,
result result
}: ConfigPanelProps) { }: ConfigPanelProps) {
const promoOptions = useMemo(() => { const promoOptions = useMemo(() => {
@@ -171,21 +167,22 @@ export function ConfigPanel({
if (!current) { if (!current) {
return; return;
} }
const tierId = current.id ?? generateTierId(); const tierId = current.id ?? generateTierId();
tiers[index] = { const mergedTier = {
...current, ...current,
...update, ...update,
id: tierId, id: tierId,
}; };
const sorted = tiers
.filter((tier) => tier != null) const normalizedTier: ShippingTierConfig = {
.map((tier) => ({ ...mergedTier,
...tier, threshold: Number.isFinite(mergedTier.threshold) ? mergedTier.threshold ?? 0 : 0,
threshold: Number.isFinite(tier.threshold) ? tier.threshold : 0, value: Number.isFinite(mergedTier.value) ? mergedTier.value ?? 0 : 0,
value: Number.isFinite(tier.value) ? tier.value : 0, };
}))
.sort((a, b) => a.threshold - b.threshold); tiers[index] = normalizedTier;
onShippingTiersChange(sorted); onShippingTiersChange(tiers);
}; };
const handleTierRemove = (index: number) => { const handleTierRemove = (index: number) => {
@@ -209,118 +206,152 @@ export function ConfigPanel({
onShippingTiersChange(tiers); 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(() => { const handleFieldBlur = useCallback(() => {
setTimeout(onRunSimulation, 0); setTimeout(onRunSimulation, 0);
}, [onRunSimulation]); }, [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 ( return (
<Card className="w-full"> <Card className="w-full">
<CardContent className="flex flex-col gap-4 px-4 py-4"> <CardContent className="flex flex-col gap-3 px-4 py-4">
<div className="space-y-6"> <div className="space-y-4">
<section className={sectionClass}> <section className={sectionClass}>
<span className={sectionTitleClass}>Calculated Metrics Filters</span>
<div className={fieldRowClass}> <div className={fieldRowClass}>
<div className={fieldClass}> <div className={fieldClass}>
<Label className={labelClass}>Date range</Label> <Label className={labelClass}>Date range</Label>
<DateRangePicker <DateRangePicker
value={dateRange} value={dateRange}
onChange={(range) => onDateRangeChange(range)} onChange={(range) => onDateRangeChange(range)}
/> className=""
</div> />
<div className={fieldClass}> </div>
<Label className={labelClass}>Promo code</Label> <div className={fieldClass}>
<Select <Label className={labelClass}>Promo code</Label>
value={promoSelectValue} <Select
onValueChange={(value) => { value={promoSelectValue}
if (value === '__all__') { onValueChange={(value) => {
onPromoChange(undefined); if (value === '__all__') {
return; onPromoChange(undefined);
} return;
const parsed = Number(value); }
onPromoChange(Number.isNaN(parsed) ? undefined : parsed); const parsed = Number(value);
}} onPromoChange(Number.isNaN(parsed) ? undefined : parsed);
> }}
<SelectTrigger className={`${compactTriggerClass} w-full`}> >
{promoLoading ? ( <SelectTrigger className={`${compactTriggerClass} w-full`}>
<div className="flex w-full items-center"> {promoLoading ? (
<Skeleton className="h-4 w-3/4" /> <div className="flex w-full items-center">
</div> <Skeleton className="h-4 w-3/4" />
) : (
<SelectValue placeholder="All promos" />
)}
</SelectTrigger>
<SelectContent className="max-h-56">
<SelectItem value="__all__">All promos</SelectItem>
{promoOptions.map((promo) => (
<SelectItem key={promo.value} value={promo.value}>
<div className="flex flex-col text-xs">
<span>{promo.label}</span>
{promo.description && (
<span className="text-muted-foreground">{promo.description}</span>
)}
</div> </div>
</SelectItem> ) : (
))} <SelectValue placeholder="All promos">
</SelectContent> {promoSelectValue === "__all__"
</Select> ? "All promos"
: promoOptions.find(p => p.value === promoSelectValue)?.label
}
</SelectValue>
)}
</SelectTrigger>
<SelectContent className="max-h-56">
<SelectItem value="__all__">All promos</SelectItem>
{promoOptions.map((promo) => (
<SelectItem key={promo.value} value={promo.value}>
<div className="flex flex-col text-xs">
<span>{promo.label}</span>
{promo.description && (
<span className="text-muted-foreground">{promo.description}</span>
)}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div> </div>
</div>
{/* Calculated Metrics */}
{result?.totals && ( {result?.totals && (
<div className="flex flex-wrap gap-2 mt-3"> <div className="flex flex-wrap gap-2">
<Badge variant="secondary" className="text-xs"> <div className={`${metricPillClass} whitespace-nowrap`}>
{formatNumber(result.totals.orders)} orders <span className="text-xs font-semibold">{formatNumber(result.totals.orders)}</span>
</Badge> <span className="text-[0.6rem] uppercase text-muted-foreground">orders</span>
<Badge variant="secondary" className="text-xs"> </div>
{formatPercent(result.totals.productDiscountRate)} avg discount <div className={`${metricPillClass} whitespace-nowrap`}>
</Badge> <span className="text-xs font-semibold">{formatPercent(result.totals.productDiscountRate)}</span>
<span className="text-[0.6rem] uppercase text-muted-foreground">avg discount</span>
</div>
{result.totals.overallCogsPercentage != null && ( {result.totals.overallCogsPercentage != null && (
<Badge variant="secondary" className="text-xs"> <div className={`${metricPillClass} whitespace-nowrap`}>
{formatPercent(result.totals.overallCogsPercentage)} avg COGS <span className="text-xs font-semibold">{formatPercent(result.totals.overallCogsPercentage)}</span>
</Badge> <span className="text-[0.6rem] uppercase text-muted-foreground">avg COGS</span>
</div>
)} )}
</div> </div>
)} )}
</section> </section>
<Separator />
<section className={sectionClass}> <section className={sectionClass}>
<span className={sectionTitleClass}>Product promo</span> <div className={sectionHeaderClass}>
<span className={sectionTitleClass}>Product promo</span>
</div>
<div className={fieldRowClass}> <div className={fieldRowClass}>
<div className={fieldClass}> <div className={fieldClass}>
<Label className={labelClass}>Promo type</Label> <Label className={labelClass}>Promo type</Label>
<Select <Select
value={productPromo.type} value={productPromo.type}
onValueChange={(value) => onProductPromoChange({ type: value as DiscountPromoType })} onValueChange={(value) => onProductPromoChange({ type: value as DiscountPromoType })}
> >
<SelectTrigger className={`${compactTriggerClass} w-full`}> <SelectTrigger className={`${compactTriggerClass} w-full`}>
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="none">No additional promo</SelectItem> <SelectItem value="none">No additional promo</SelectItem>
<SelectItem value="percentage_subtotal">% off subtotal</SelectItem> <SelectItem value="percentage_subtotal">% off subtotal</SelectItem>
<SelectItem value="percentage_regular">% off regular price</SelectItem> <SelectItem value="percentage_regular">% off regular price</SelectItem>
<SelectItem value="fixed_amount">Fixed dollar discount</SelectItem> <SelectItem value="fixed_amount">Fixed dollar discount</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
{showProductAdjustments && ( {showProductAdjustments && (
<div className={fieldRowHorizontalClass}> <div className={fieldRowHorizontalClass}>
<div className={fieldClass}> <div className={fieldClass}>
@@ -359,24 +390,26 @@ export function ConfigPanel({
</section> </section>
<section className={sectionClass}> <section className={sectionClass}>
<span className={sectionTitleClass}>Shipping promo</span> <div className={sectionHeaderClass}>
<span className={sectionTitleClass}>Shipping promo</span>
</div>
<div className={fieldRowClass}> <div className={fieldRowClass}>
<div className={fieldClass}> <div className={fieldClass}>
<Label className={labelClass}>Promo type</Label> <Label className={labelClass}>Promo type</Label>
<Select <Select
value={shippingPromo.type} value={shippingPromo.type}
onValueChange={(value) => onShippingPromoChange({ type: value as ShippingPromoType })} onValueChange={(value) => onShippingPromoChange({ type: value as ShippingPromoType })}
> >
<SelectTrigger className={`${compactTriggerClass} w-full`}> <SelectTrigger className={`${compactTriggerClass} w-full`}>
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="none">No shipping promo</SelectItem> <SelectItem value="none">No shipping promo</SelectItem>
<SelectItem value="percentage">% off shipping charge</SelectItem> <SelectItem value="percentage">% off shipping charge</SelectItem>
<SelectItem value="fixed">Fixed dollar discount</SelectItem> <SelectItem value="fixed">Fixed dollar discount</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
{showShippingAdjustments && ( {showShippingAdjustments && (
<> <>
<div className={fieldRowHorizontalClass}> <div className={fieldRowHorizontalClass}>
@@ -430,100 +463,100 @@ export function ConfigPanel({
</div> </div>
</section> </section>
<section className="flex flex-col gap-2"> <section className={compactSectionClass}>
<div className="flex items-center justify-between"> <div className={sectionHeaderClass}>
<span className={sectionTitleClass}>Shipping tiers</span> <span className={sectionTitleClass}>Shipping tiers</span>
<Button variant="outline" size="sm" onClick={handleTierAdd} className="flex items-center gap-1"> <Button variant="outline" size="sm" onClick={handleTierAdd} className="flex items-center gap-1">
<PlusIcon className="w-3 h-3" /> <PlusIcon className="w-3 h-3" />
Add tier Add tier
</Button> </Button>
</div> </div>
{shippingTiers.length === 0 ? ( {shippingTiers.length === 0 ? (
<p className="text-xs text-muted-foreground">Add tiers to model automatic shipping discounts.</p> <p className="text-xs text-muted-foreground">Add tiers to model automatic shipping discounts.</p>
) : ( ) : (
<ScrollArea className=""> <ScrollArea>
<div className="flex flex-col gap-2 pr-1"> <div className="flex flex-col gap-2 pr-1 -mx-2">
{/* Header row */} <div className="grid gap-2 px-2 py-1 text-[0.65rem] font-medium uppercase tracking-[0.18em] text-muted-foreground sm:grid-cols-[minmax(0,0.9fr)_minmax(0,1.1fr)_minmax(0,1fr)_auto]">
<div className="grid gap-2 px-2 py-1 text-xs font-medium text-muted-foreground sm:grid-cols-[minmax(0,0.9fr)_minmax(0,1.1fr)_minmax(0,1fr)_auto]"> <div>Amount</div>
<div>$ Amount</div> <div>Type</div>
<div>Type</div> <div>Value</div>
<div>Value</div> <div className="w-1.5" aria-hidden="true" />
<div className="w-1.5"></div> </div>
{shippingTiers.map((tier, index) => {
const tierKey = tier.id ?? `tier-${index}`;
return (
<div
key={tierKey}
className="relative grid gap-2 rounded px-2 py-2 text-xs sm:grid-cols-[minmax(0,0.9fr)_minmax(0,1.1fr)_minmax(0,1fr)_auto] sm:items-end"
>
<div>
<Input
className={compactNumberClass}
type="number"
step="1"
value={tier.threshold}
onChange={(event) => {
onConfigInputChange();
handleTierUpdate(index, {
threshold: parseNumber(event.target.value, 0),
});
}}
onBlur={handleTierBlur}
/>
</div>
<div>
<Select
value={tier.mode}
onValueChange={(value) => {
onConfigInputChange();
handleTierUpdate(index, { mode: value as ShippingTierConfig["mode"] });
}}
>
<SelectTrigger className={`${compactTriggerClass} w-full`}>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="percentage">% off</SelectItem>
<SelectItem value="flat">Flat rate</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Input
className={compactNumberClass}
type="number"
step="1"
value={tier.value}
onChange={(event) => {
onConfigInputChange();
handleTierUpdate(index, { value: parseNumber(event.target.value, 0) });
}}
onBlur={handleTierBlur}
/>
</div>
<div className="w-1.5" aria-hidden="true" />
<div className="absolute -right-0.5 top-1/2 -translate-y-1/2 flex justify-end">
<Button
variant="ghost"
size="sm"
onClick={() => handleTierRemove(index)}
className="p-1"
>
<X className="h-3 w-3" />
</Button>
</div>
</div>
);
})}
</div> </div>
{shippingTiers.map((tier, index) => { </ScrollArea>
const tierKey = tier.id ?? `tier-${index}`; )}
return ( </section>
<div
key={tierKey}
className="relative grid gap-2 rounded border px-2 py-2 text-xs sm:grid-cols-[minmax(0,0.9fr)_minmax(0,1.1fr)_minmax(0,1fr)_auto] sm:items-end"
>
<div>
<Input
className={compactNumberClass}
type="number"
step="1"
value={tier.threshold}
onChange={(event) => {
onConfigInputChange();
handleTierUpdate(index, {
threshold: parseNumber(event.target.value, 0),
});
}}
onBlur={handleFieldBlur}
/>
</div>
<div>
<Select
value={tier.mode}
onValueChange={(value) => {
onConfigInputChange();
handleTierUpdate(index, { mode: value as ShippingTierConfig["mode"] });
}}
>
<SelectTrigger className={`${compactTriggerClass} w-full`}>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="percentage">% off</SelectItem>
<SelectItem value="flat">Flat rate</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Input
className={compactNumberClass}
type="number"
step="1"
value={tier.value}
onChange={(event) => {
onConfigInputChange();
handleTierUpdate(index, { value: parseNumber(event.target.value, 0) });
}}
onBlur={handleFieldBlur}
/>
</div>
<div className="w-1.5"></div>
<div className="flex justify-end sm:col-span-1 absolute -right-0.5 top-1/2 -translate-y-1/2">
<Button
variant="ghost"
size="sm"
onClick={() => handleTierRemove(index)}
className="p-1"
>
<X className="w-3 h-3" />
</Button>
</div>
</div>
);
})}
</div>
</ScrollArea>
)}
</section>
<Separator />
<section className={sectionClass}> <section className={sectionClass}>
<span className={sectionTitleClass}>Order costs</span> <div className={sectionHeaderClass}>
<span className={sectionTitleClass}>Order costs</span>
</div>
<div className={fieldRowClass}> <div className={fieldRowClass}>
<div className={fieldClass}> <div className={fieldClass}>
<Label className={labelClass}>COGS calculation</Label> <Label className={labelClass}>COGS calculation</Label>
@@ -559,7 +592,7 @@ export function ConfigPanel({
/> />
</div> </div>
<div className={fieldClass}> <div className={fieldClass}>
<Label className={labelClass}>Fixed cost/order ($)</Label> <Label className={labelClass}>Fixed costs ($)</Label>
<Input <Input
className={compactNumberClass} className={compactNumberClass}
type="number" type="number"
@@ -577,7 +610,9 @@ export function ConfigPanel({
</section> </section>
<section className={sectionClass}> <section className={sectionClass}>
<span className={sectionTitleClass}>Rewards points</span> <div className={sectionHeaderClass}>
<span className={sectionTitleClass}>Rewards points</span>
</div>
<div className={fieldRowClass}> <div className={fieldRowClass}>
<div className="grid gap-3 sm:grid-cols-2"> <div className="grid gap-3 sm:grid-cols-2">
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
@@ -585,7 +620,7 @@ export function ConfigPanel({
<span className="text-sm font-medium">{Number.isFinite(pointsPerDollar) ? pointsPerDollar.toFixed(4) : '—'}</span> <span className="text-sm font-medium">{Number.isFinite(pointsPerDollar) ? pointsPerDollar.toFixed(4) : '—'}</span>
</div> </div>
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
<span className={labelClass}>Redemption rate (%)</span> <span className={labelClass}>Redemption rate</span>
<span className="text-sm font-medium">{formatPercent(redemptionRate)}</span> <span className="text-sm font-medium">{formatPercent(redemptionRate)}</span>
</div> </div>
</div> </div>
@@ -603,15 +638,6 @@ export function ConfigPanel({
onBlur={handleFieldBlur} onBlur={handleFieldBlur}
/> />
</div> </div>
{typeof recommendedPointDollarValue === 'number' && (
<div className="flex flex-col gap-1 text-xs text-muted-foreground">
{recommendedDiffers && onApplyRecommendedPointDollarValue && (
<Button variant="outline" size="sm" onClick={onApplyRecommendedPointDollarValue}>
Use recommended
</Button>
)}
</div>
)}
</div> </div>
</section> </section>
</div> </div>

View File

@@ -1,3 +1,5 @@
import { Fragment } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { import {
Table, Table,
@@ -16,6 +18,7 @@ import {
import { DiscountSimulationBucket } from "@/types/discount-simulator"; import { DiscountSimulationBucket } from "@/types/discount-simulator";
import { formatCurrency, formatNumber } from "@/utils/productUtils"; import { formatCurrency, formatNumber } from "@/utils/productUtils";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { cn } from "@/lib/utils";
// Utility function to interpolate between two colors // Utility function to interpolate between two colors
const interpolateColor = (color1: [number, number, number], color2: [number, number, number], ratio: number): [number, number, number] => { const interpolateColor = (color1: [number, number, number], color2: [number, number, number], ratio: number): [number, number, number] => {
@@ -63,36 +66,81 @@ interface ResultsTableProps {
isLoading: boolean; isLoading: boolean;
} }
const rowLabels = [ type MetricGroup =
// Most important metrics first - prominently styled | "orderVolume"
{ key: "totalRevenue", label: "Total Revenue", format: (value: number) => formatCurrency(value), important: true, isCalculated: true }, | "customerSpend"
{ key: "totalCosts", label: "Total Cost", format: (value: number) => formatCurrency(value), important: true, isCalculated: true }, | "discounts"
{ key: "profit", label: "Profit $", format: (value: number) => formatCurrency(value), important: true, isCalculated: true }, | "shipping"
{ key: "profitPercent", label: "Profit %", format: (value: number) => `${(value * 100).toFixed(2)}%`, important: true, isCalculated: true }, | "costs"
| "outcomes";
// Order metrics - from database
{ key: "orderCount", label: "Orders", format: (value: number) => formatNumber(value), important: false, isCalculated: false }, const metricGroupStyles: Record<MetricGroup, { label: string; indicatorClass: string; badgeClass: string; rowClass: string }> = {
{ key: "weight", label: "Weight", format: (value: number) => `${(value * 100).toFixed(2)}%`, important: false, isCalculated: false }, orderVolume: {
{ key: "orderValue", label: "Order Value", format: (value: number) => formatCurrency(value), important: false, isCalculated: false }, label: "Order Volume",
indicatorClass: "bg-sky-500",
// Customer costs badgeClass: "bg-sky-100 text-sky-900 border-sky-200 uppercase",
{ key: "customerItemCost", label: "Cust Item Cost", format: (value: number) => formatCurrency(value), important: false, isCalculated: true }, rowClass: "bg-sky-50",
{ key: "customerShipCost", label: "Cust Ship Cost", format: (value: number) => formatCurrency(value), important: false, isCalculated: true }, },
customerSpend: {
// Discounts label: "Customer Spend",
{ key: "productDiscountAmount", label: "Prod Discount", format: (value: number) => formatCurrency(value), important: false, isCalculated: true }, indicatorClass: "bg-indigo-500",
{ key: "promoProductDiscount", label: "Promo Prod Disc", format: (value: number) => formatCurrency(value), important: false, isCalculated: true }, badgeClass: "bg-indigo-100 text-indigo-900 border-indigo-200 uppercase",
{ key: "shipPromoDiscount", label: "Ship Promo Disc", format: (value: number) => formatCurrency(value), important: false, isCalculated: true }, rowClass: "bg-indigo-50",
},
// Shipping discounts: {
{ key: "shippingAfterAuto", label: "Ship Charge", format: (value: number) => formatCurrency(value), important: false, isCalculated: true }, label: "Discounts",
{ key: "actualShippingCost", label: "Actual Ship Cost", format: (value: number) => formatCurrency(value), important: false, isCalculated: false }, indicatorClass: "bg-amber-500",
badgeClass: "bg-amber-100 text-amber-900 border-amber-200 uppercase",
// Cost breakdown rowClass: "bg-amber-50",
{ 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 }, shipping: {
{ key: "pointsCost", label: "Points Cost", format: (value: number) => formatCurrency(value), important: false, isCalculated: true }, label: "Shipping Impact",
{ key: "fixedCosts", label: "Fixed Cost", format: (value: number) => formatCurrency(value), important: false, isCalculated: true }, 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) => { const formatRangeUpperBound = (bucket: DiscountSimulationBucket) => {
@@ -123,7 +171,7 @@ export function ResultsTable({ buckets, isLoading }: ResultsTableProps) {
<div className="overflow-x-auto max-w-full"> <div className="overflow-x-auto max-w-full">
<Table className="text-sm table-fixed"> <Table className="text-sm table-fixed">
<colgroup> <colgroup>
<col style={{ width: '152px' }} /> <col style={{ width: '156px' }} />
{buckets.map((bucket) => ( {buckets.map((bucket) => (
<col key={bucket.key} style={{ minWidth: '70px', width: '75px' }} /> <col key={bucket.key} style={{ minWidth: '70px', width: '75px' }} />
))} ))}
@@ -146,51 +194,119 @@ export function ResultsTable({ buckets, isLoading }: ResultsTableProps) {
</TableCell> </TableCell>
</TableRow> </TableRow>
) : ( ) : (
rowLabels.map((row) => ( rowLabels.map((row, index) => {
<TableRow key={row.key} className={row.important ? "bg-muted/30 border-l-4 border-l-primary" : ""}> const groupStyle = metricGroupStyles[row.group];
<TableCell className={`font-medium sticky left-0 bg-background z-10 px-3 py-2 border-r ${row.important ? 'font-semibold text-primary' : ''}`}> const isImportant = Boolean(row.important);
<div className="flex items-center gap-2"> const prevGroup = index > 0 ? rowLabels[index - 1].group : null;
{/* Subtle indicator for data source */} const isFirstOfGroup = prevGroup !== row.group;
<TooltipProvider>
<Tooltip> return (
<TooltipTrigger asChild> <Fragment key={`${row.group}-${row.key}`}>
<span className={`inline-block w-2 h-2 rounded-full ${row.isCalculated ? 'bg-blue-400' : 'bg-green-400'}`} /> {isFirstOfGroup && (
</TooltipTrigger> <TableRow className={cn(groupStyle.badgeClass)}>
<TooltipContent> <TableCell
<p>{row.isCalculated ? 'Calculated value' : 'Database value'}</p> colSpan={buckets.length + 1}
</TooltipContent> className={cn(
</Tooltip> "border-0 px-3 py-0",
</TooltipProvider> groupStyle.badgeClass,
<span>{row.label}</span> index !== 0 && "pt-0"
</div> )}
</TableCell> >
{buckets.map((bucket) => { <div className="flex items-center gap-2">
const value = bucket[row.key as keyof DiscountSimulationBucket] as number;
const formattedValue = row.format(value); <div
// Apply color gradient for profit percentage className={cn(
if (row.key === 'profitPercent') { "px-2 py-1 font-semibold tracking-wide text-xs",
const backgroundColor = getProfitPercentageColor(value); groupStyle.badgeClass
return ( )}
<TableCell key={`${row.key}-${bucket.key}`} className={`text-center whitespace-nowrap px-1 py-1 ${row.important ? 'font-semibold' : ''}`}> >
<span {groupStyle.label}
className="inline-block px-1.5 py-0.5 rounded text-white font-medium" </div>
style={{ backgroundColor }} </div>
</TableCell>
</TableRow>
)}
<TableRow className={cn("align-top", groupStyle.rowClass)}>
<TableCell
className={cn(
"sticky left-0 z-10 border-r px-3 py-2",
groupStyle.rowClass,
isImportant ? "font-semibold text-emerald-800" : "font-medium text-slate-700"
)}
>
<div className="flex items-center gap-2">
<span
aria-hidden
className={cn("h-4 w-1 rounded-full", groupStyle.indicatorClass)}
/>
<span
className={cn(
"text-sm leading-tight pr-2",
isImportant ? "font-semibold text-emerald-900" : "font-medium text-slate-800"
)}
>
{row.label}
</span>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span
className={cn(
"inline-block h-2.5 w-2.5 rounded-full shadow-sm absolute right-1 top-1/2 -translate-y-1/2",
row.isCalculated ? "bg-blue-400" : "bg-emerald-400"
)}
/>
</TooltipTrigger>
<TooltipContent>
<p>{row.isCalculated ? "Calculated value" : "Database value"}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</TableCell>
{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 (
<TableCell
key={`${row.key}-${bucket.key}`}
className={cn(
"text-center whitespace-nowrap px-1 py-1 text-slate-700",
groupStyle.rowClass,
isImportant && "font-semibold text-emerald-700"
)}
>
<span
className="inline-block rounded px-1.5 py-0.5 text-white font-medium"
style={{ backgroundColor }}
>
{formattedValue}
</span>
</TableCell>
);
}
return (
<TableCell
key={`${row.key}-${bucket.key}`}
className={cn(
"text-center whitespace-nowrap px-1 py-1 text-slate-700",
groupStyle.rowClass,
isImportant && "font-semibold text-emerald-700"
)}
> >
{formattedValue} {formattedValue}
</span> </TableCell>
</TableCell> );
); })}
} </TableRow>
</Fragment>
return ( );
<TableCell key={`${row.key}-${bucket.key}`} className={`text-center whitespace-nowrap px-1 py-1 ${row.important ? 'font-semibold text-primary' : ''}`}> })
{formattedValue}
</TableCell>
);
})}
</TableRow>
))
)} )}
</TableBody> </TableBody>
</Table> </Table>

View File

@@ -29,7 +29,7 @@ export function DateRangePicker({
id="date" id="date"
variant={"outline"} variant={"outline"}
className={cn( 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" !value && "text-muted-foreground"
)} )}
> >