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 { 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 (
<Card className="w-full">
<CardContent className="flex flex-col gap-4 px-4 py-4">
<div className="space-y-6">
<CardContent className="flex flex-col gap-3 px-4 py-4">
<div className="space-y-4">
<section className={sectionClass}>
<span className={sectionTitleClass}>Calculated Metrics Filters</span>
<div className={fieldRowClass}>
<div className={fieldClass}>
<Label className={labelClass}>Date range</Label>
<DateRangePicker
value={dateRange}
onChange={(range) => onDateRangeChange(range)}
/>
</div>
<div className={fieldClass}>
<Label className={labelClass}>Promo code</Label>
<Select
value={promoSelectValue}
onValueChange={(value) => {
if (value === '__all__') {
onPromoChange(undefined);
return;
}
const parsed = Number(value);
onPromoChange(Number.isNaN(parsed) ? undefined : parsed);
}}
>
<SelectTrigger className={`${compactTriggerClass} w-full`}>
{promoLoading ? (
<div className="flex w-full items-center">
<Skeleton className="h-4 w-3/4" />
</div>
) : (
<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 className={fieldClass}>
<Label className={labelClass}>Date range</Label>
<DateRangePicker
value={dateRange}
onChange={(range) => onDateRangeChange(range)}
className=""
/>
</div>
<div className={fieldClass}>
<Label className={labelClass}>Promo code</Label>
<Select
value={promoSelectValue}
onValueChange={(value) => {
if (value === '__all__') {
onPromoChange(undefined);
return;
}
const parsed = Number(value);
onPromoChange(Number.isNaN(parsed) ? undefined : parsed);
}}
>
<SelectTrigger className={`${compactTriggerClass} w-full`}>
{promoLoading ? (
<div className="flex w-full items-center">
<Skeleton className="h-4 w-3/4" />
</div>
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<SelectValue placeholder="All promos">
{promoSelectValue === "__all__"
? "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>
{/* Calculated Metrics */}
{result?.totals && (
<div className="flex flex-wrap gap-2 mt-3">
<Badge variant="secondary" className="text-xs">
{formatNumber(result.totals.orders)} orders
</Badge>
<Badge variant="secondary" className="text-xs">
{formatPercent(result.totals.productDiscountRate)} avg discount
</Badge>
<div className="flex flex-wrap gap-2">
<div className={`${metricPillClass} whitespace-nowrap`}>
<span className="text-xs font-semibold">{formatNumber(result.totals.orders)}</span>
<span className="text-[0.6rem] uppercase text-muted-foreground">orders</span>
</div>
<div className={`${metricPillClass} whitespace-nowrap`}>
<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 && (
<Badge variant="secondary" className="text-xs">
{formatPercent(result.totals.overallCogsPercentage)} avg COGS
</Badge>
<div className={`${metricPillClass} whitespace-nowrap`}>
<span className="text-xs font-semibold">{formatPercent(result.totals.overallCogsPercentage)}</span>
<span className="text-[0.6rem] uppercase text-muted-foreground">avg COGS</span>
</div>
)}
</div>
)}
</section>
<Separator />
<section className={sectionClass}>
<span className={sectionTitleClass}>Product promo</span>
<div className={sectionHeaderClass}>
<span className={sectionTitleClass}>Product promo</span>
</div>
<div className={fieldRowClass}>
<div className={fieldClass}>
<Label className={labelClass}>Promo type</Label>
<Select
value={productPromo.type}
onValueChange={(value) => onProductPromoChange({ type: value as DiscountPromoType })}
>
<SelectTrigger className={`${compactTriggerClass} w-full`}>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">No additional promo</SelectItem>
<SelectItem value="percentage_subtotal">% off subtotal</SelectItem>
<SelectItem value="percentage_regular">% off regular price</SelectItem>
<SelectItem value="fixed_amount">Fixed dollar discount</SelectItem>
</SelectContent>
</Select>
</div>
<Label className={labelClass}>Promo type</Label>
<Select
value={productPromo.type}
onValueChange={(value) => onProductPromoChange({ type: value as DiscountPromoType })}
>
<SelectTrigger className={`${compactTriggerClass} w-full`}>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">No additional promo</SelectItem>
<SelectItem value="percentage_subtotal">% off subtotal</SelectItem>
<SelectItem value="percentage_regular">% off regular price</SelectItem>
<SelectItem value="fixed_amount">Fixed dollar discount</SelectItem>
</SelectContent>
</Select>
</div>
{showProductAdjustments && (
<div className={fieldRowHorizontalClass}>
<div className={fieldClass}>
@@ -359,24 +390,26 @@ export function ConfigPanel({
</section>
<section className={sectionClass}>
<span className={sectionTitleClass}>Shipping promo</span>
<div className={sectionHeaderClass}>
<span className={sectionTitleClass}>Shipping promo</span>
</div>
<div className={fieldRowClass}>
<div className={fieldClass}>
<Label className={labelClass}>Promo type</Label>
<Select
value={shippingPromo.type}
onValueChange={(value) => onShippingPromoChange({ type: value as ShippingPromoType })}
>
<SelectTrigger className={`${compactTriggerClass} w-full`}>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">No shipping promo</SelectItem>
<SelectItem value="percentage">% off shipping charge</SelectItem>
<SelectItem value="fixed">Fixed dollar discount</SelectItem>
</SelectContent>
</Select>
</div>
<Label className={labelClass}>Promo type</Label>
<Select
value={shippingPromo.type}
onValueChange={(value) => onShippingPromoChange({ type: value as ShippingPromoType })}
>
<SelectTrigger className={`${compactTriggerClass} w-full`}>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">No shipping promo</SelectItem>
<SelectItem value="percentage">% off shipping charge</SelectItem>
<SelectItem value="fixed">Fixed dollar discount</SelectItem>
</SelectContent>
</Select>
</div>
{showShippingAdjustments && (
<>
<div className={fieldRowHorizontalClass}>
@@ -430,100 +463,100 @@ export function ConfigPanel({
</div>
</section>
<section className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<span className={sectionTitleClass}>Shipping tiers</span>
<Button variant="outline" size="sm" onClick={handleTierAdd} className="flex items-center gap-1">
<PlusIcon className="w-3 h-3" />
Add tier
</Button>
</div>
{shippingTiers.length === 0 ? (
<p className="text-xs text-muted-foreground">Add tiers to model automatic shipping discounts.</p>
) : (
<ScrollArea className="">
<div className="flex flex-col gap-2 pr-1">
{/* Header row */}
<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>Type</div>
<div>Value</div>
<div className="w-1.5"></div>
<section className={compactSectionClass}>
<div className={sectionHeaderClass}>
<span className={sectionTitleClass}>Shipping tiers</span>
<Button variant="outline" size="sm" onClick={handleTierAdd} className="flex items-center gap-1">
<PlusIcon className="w-3 h-3" />
Add tier
</Button>
</div>
{shippingTiers.length === 0 ? (
<p className="text-xs text-muted-foreground">Add tiers to model automatic shipping discounts.</p>
) : (
<ScrollArea>
<div className="flex flex-col gap-2 pr-1 -mx-2">
<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>Amount</div>
<div>Type</div>
<div>Value</div>
<div className="w-1.5" aria-hidden="true" />
</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>
{shippingTiers.map((tier, index) => {
const tierKey = tier.id ?? `tier-${index}`;
return (
<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 />
</ScrollArea>
)}
</section>
<section className={sectionClass}>
<span className={sectionTitleClass}>Order costs</span>
<div className={sectionHeaderClass}>
<span className={sectionTitleClass}>Order costs</span>
</div>
<div className={fieldRowClass}>
<div className={fieldClass}>
<Label className={labelClass}>COGS calculation</Label>
@@ -559,7 +592,7 @@ export function ConfigPanel({
/>
</div>
<div className={fieldClass}>
<Label className={labelClass}>Fixed cost/order ($)</Label>
<Label className={labelClass}>Fixed costs ($)</Label>
<Input
className={compactNumberClass}
type="number"
@@ -577,7 +610,9 @@ export function ConfigPanel({
</section>
<section className={sectionClass}>
<span className={sectionTitleClass}>Rewards points</span>
<div className={sectionHeaderClass}>
<span className={sectionTitleClass}>Rewards points</span>
</div>
<div className={fieldRowClass}>
<div className="grid gap-3 sm:grid-cols-2">
<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>
</div>
<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>
</div>
</div>
@@ -603,15 +638,6 @@ export function ConfigPanel({
onBlur={handleFieldBlur}
/>
</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>
</section>
</div>

View File

@@ -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<MetricGroup, { label: string; indicatorClass: string; badgeClass: string; rowClass: string }> = {
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) {
<div className="overflow-x-auto max-w-full">
<Table className="text-sm table-fixed">
<colgroup>
<col style={{ width: '152px' }} />
<col style={{ width: '156px' }} />
{buckets.map((bucket) => (
<col key={bucket.key} style={{ minWidth: '70px', width: '75px' }} />
))}
@@ -146,51 +194,119 @@ export function ResultsTable({ buckets, isLoading }: ResultsTableProps) {
</TableCell>
</TableRow>
) : (
rowLabels.map((row) => (
<TableRow key={row.key} className={row.important ? "bg-muted/30 border-l-4 border-l-primary" : ""}>
<TableCell className={`font-medium sticky left-0 bg-background z-10 px-3 py-2 border-r ${row.important ? 'font-semibold text-primary' : ''}`}>
<div className="flex items-center gap-2">
{/* Subtle indicator for data source */}
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span className={`inline-block w-2 h-2 rounded-full ${row.isCalculated ? 'bg-blue-400' : 'bg-green-400'}`} />
</TooltipTrigger>
<TooltipContent>
<p>{row.isCalculated ? 'Calculated value' : 'Database value'}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<span>{row.label}</span>
</div>
</TableCell>
{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 (
<TableCell key={`${row.key}-${bucket.key}`} className={`text-center whitespace-nowrap px-1 py-1 ${row.important ? 'font-semibold' : ''}`}>
<span
className="inline-block px-1.5 py-0.5 rounded text-white font-medium"
style={{ backgroundColor }}
rowLabels.map((row, index) => {
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 (
<Fragment key={`${row.group}-${row.key}`}>
{isFirstOfGroup && (
<TableRow className={cn(groupStyle.badgeClass)}>
<TableCell
colSpan={buckets.length + 1}
className={cn(
"border-0 px-3 py-0",
groupStyle.badgeClass,
index !== 0 && "pt-0"
)}
>
<div className="flex items-center gap-2">
<div
className={cn(
"px-2 py-1 font-semibold tracking-wide text-xs",
groupStyle.badgeClass
)}
>
{groupStyle.label}
</div>
</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}
</span>
</TableCell>
);
}
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>
))
</TableCell>
);
})}
</TableRow>
</Fragment>
);
})
)}
</TableBody>
</Table>

View File

@@ -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"
)}
>