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,39 +206,65 @@ 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>
<div className={fieldClass}> <div className={fieldClass}>
@@ -263,7 +286,12 @@ export function ConfigPanel({
<Skeleton className="h-4 w-3/4" /> <Skeleton className="h-4 w-3/4" />
</div> </div>
) : ( ) : (
<SelectValue placeholder="All promos" /> <SelectValue placeholder="All promos">
{promoSelectValue === "__all__"
? "All promos"
: promoOptions.find(p => p.value === promoSelectValue)?.label
}
</SelectValue>
)} )}
</SelectTrigger> </SelectTrigger>
<SelectContent className="max-h-56"> <SelectContent className="max-h-56">
@@ -283,26 +311,29 @@ export function ConfigPanel({
</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}>
<div className={sectionHeaderClass}>
<span className={sectionTitleClass}>Product promo</span> <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>
@@ -359,7 +390,9 @@ export function ConfigPanel({
</section> </section>
<section className={sectionClass}> <section className={sectionClass}>
<div className={sectionHeaderClass}>
<span className={sectionTitleClass}>Shipping promo</span> <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>
@@ -430,8 +463,8 @@ 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" />
@@ -441,21 +474,20 @@ export function ConfigPanel({
{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"></div> <div className="w-1.5" aria-hidden="true" />
</div> </div>
{shippingTiers.map((tier, index) => { {shippingTiers.map((tier, index) => {
const tierKey = tier.id ?? `tier-${index}`; const tierKey = tier.id ?? `tier-${index}`;
return ( return (
<div <div
key={tierKey} 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" 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> <div>
<Input <Input
@@ -469,7 +501,7 @@ export function ConfigPanel({
threshold: parseNumber(event.target.value, 0), threshold: parseNumber(event.target.value, 0),
}); });
}} }}
onBlur={handleFieldBlur} onBlur={handleTierBlur}
/> />
</div> </div>
<div> <div>
@@ -499,18 +531,18 @@ export function ConfigPanel({
onConfigInputChange(); onConfigInputChange();
handleTierUpdate(index, { value: parseNumber(event.target.value, 0) }); handleTierUpdate(index, { value: parseNumber(event.target.value, 0) });
}} }}
onBlur={handleFieldBlur} onBlur={handleTierBlur}
/> />
</div> </div>
<div className="w-1.5"></div> <div className="w-1.5" aria-hidden="true" />
<div className="flex justify-end sm:col-span-1 absolute -right-0.5 top-1/2 -translate-y-1/2"> <div className="absolute -right-0.5 top-1/2 -translate-y-1/2 flex justify-end">
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => handleTierRemove(index)} onClick={() => handleTierRemove(index)}
className="p-1" className="p-1"
> >
<X className="w-3 h-3" /> <X className="h-3 w-3" />
</Button> </Button>
</div> </div>
</div> </div>
@@ -520,10 +552,11 @@ export function ConfigPanel({
</ScrollArea> </ScrollArea>
)} )}
</section> </section>
<Separator />
<section className={sectionClass}> <section className={sectionClass}>
<div className={sectionHeaderClass}>
<span className={sectionTitleClass}>Order costs</span> <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}>
<div className={sectionHeaderClass}>
<span className={sectionTitleClass}>Rewards points</span> <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 const metricGroupStyles: Record<MetricGroup, { label: string; indicatorClass: string; badgeClass: string; rowClass: string }> = {
{ key: "orderCount", label: "Orders", format: (value: number) => formatNumber(value), important: false, isCalculated: false }, orderVolume: {
{ key: "weight", label: "Weight", format: (value: number) => `${(value * 100).toFixed(2)}%`, important: false, isCalculated: false }, label: "Order Volume",
{ key: "orderValue", label: "Order Value", format: (value: number) => formatCurrency(value), important: false, isCalculated: false }, 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",
},
};
// Customer costs interface RowConfig {
{ key: "customerItemCost", label: "Cust Item Cost", format: (value: number) => formatCurrency(value), important: false, isCalculated: true }, key: keyof DiscountSimulationBucket;
{ key: "customerShipCost", label: "Cust Ship Cost", format: (value: number) => formatCurrency(value), important: false, isCalculated: true }, label: string;
format: (value: number) => string;
important?: boolean;
isCalculated: boolean;
group: MetricGroup;
}
// Discounts const rowLabels: RowConfig[] = [
{ key: "productDiscountAmount", label: "Prod Discount", format: (value: number) => formatCurrency(value), important: false, isCalculated: true }, { key: "orderCount", label: "Orders", format: (value: number) => formatNumber(value), isCalculated: false, group: "orderVolume" },
{ key: "promoProductDiscount", label: "Promo Prod Disc", format: (value: number) => formatCurrency(value), important: false, isCalculated: true }, { key: "weight", label: "Weight", format: (value: number) => `${(value * 100).toFixed(2)}%`, isCalculated: false, group: "orderVolume" },
{ key: "shipPromoDiscount", label: "Ship Promo Disc", format: (value: number) => formatCurrency(value), important: false, isCalculated: true }, { 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" },
// Shipping { key: "customerShipCost", label: "Cust Ship Cost", format: (value: number) => formatCurrency(value), isCalculated: true, group: "customerSpend" },
{ key: "shippingAfterAuto", label: "Ship Charge", format: (value: number) => formatCurrency(value), important: false, isCalculated: true }, { key: "productDiscountAmount", label: "Prod Discount", format: (value: number) => formatCurrency(value), isCalculated: true, group: "discounts" },
{ key: "actualShippingCost", label: "Actual Ship Cost", format: (value: number) => formatCurrency(value), important: false, isCalculated: false }, { 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" },
// Cost breakdown { key: "shippingAfterAuto", label: "Ship Charge", format: (value: number) => formatCurrency(value), isCalculated: true, group: "shipping" },
{ key: "productCogs", label: "Product COGS", format: (value: number) => formatCurrency(value), important: false, isCalculated: false }, { key: "actualShippingCost", label: "Actual Ship Cost", format: (value: number) => formatCurrency(value), isCalculated: false, group: "shipping" },
{ key: "merchantFees", label: "Merchant Fees", format: (value: number) => formatCurrency(value), important: false, isCalculated: true }, { key: "productCogs", label: "Product COGS", format: (value: number) => formatCurrency(value), isCalculated: false, group: "costs" },
{ key: "pointsCost", label: "Points Cost", format: (value: number) => formatCurrency(value), important: false, isCalculated: true }, { key: "merchantFees", label: "Merchant Fees", format: (value: number) => formatCurrency(value), isCalculated: true, group: "costs" },
{ key: "fixedCosts", label: "Fixed Cost", format: (value: number) => formatCurrency(value), important: false, isCalculated: true }, { 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,35 +194,94 @@ 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);
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="flex items-center gap-2">
{/* Subtle indicator for data source */}
<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> <TooltipProvider>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<span className={`inline-block w-2 h-2 rounded-full ${row.isCalculated ? 'bg-blue-400' : 'bg-green-400'}`} /> <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> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<p>{row.isCalculated ? 'Calculated value' : 'Database value'}</p> <p>{row.isCalculated ? "Calculated value" : "Database value"}</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
<span>{row.label}</span>
</div> </div>
</TableCell> </TableCell>
{buckets.map((bucket) => { {buckets.map((bucket) => {
const value = bucket[row.key as keyof DiscountSimulationBucket] as number; const value = bucket[row.key as keyof DiscountSimulationBucket] as number;
const formattedValue = row.format(value); const formattedValue = row.format(value);
// Apply color gradient for profit percentage if (row.key === "profitPercent") {
if (row.key === 'profitPercent') {
const backgroundColor = getProfitPercentageColor(value); const backgroundColor = getProfitPercentageColor(value);
return ( return (
<TableCell key={`${row.key}-${bucket.key}`} className={`text-center whitespace-nowrap px-1 py-1 ${row.important ? 'font-semibold' : ''}`}> <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 <span
className="inline-block px-1.5 py-0.5 rounded text-white font-medium" className="inline-block rounded px-1.5 py-0.5 text-white font-medium"
style={{ backgroundColor }} style={{ backgroundColor }}
> >
{formattedValue} {formattedValue}
@@ -184,13 +291,22 @@ export function ResultsTable({ buckets, isLoading }: ResultsTableProps) {
} }
return ( return (
<TableCell key={`${row.key}-${bucket.key}`} className={`text-center whitespace-nowrap px-1 py-1 ${row.important ? 'font-semibold text-primary' : ''}`}> <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}
</TableCell> </TableCell>
); );
})} })}
</TableRow> </TableRow>
)) </Fragment>
);
})
)} )}
</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"
)} )}
> >