Add baseline comparison for discount simulator

This commit is contained in:
2026-01-15 16:26:28 -05:00
parent 738ed94ad5
commit 0ffd02e22e
2 changed files with 133 additions and 25 deletions

View File

@@ -1,9 +1,12 @@
import { differenceInDays } from "date-fns";
import { Card, CardContent } from "@/components/ui/card";
import { formatCurrency } from "@/utils/productUtils";
import { DiscountSimulationResponse } from "@/types/discount-simulator";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { Separator } from "@/components/ui/separator";
import { Bookmark, X } from "lucide-react";
// Utility function to interpolate between two colors
const interpolateColor = (color1: [number, number, number], color2: [number, number, number], ratio: number): [number, number, number] => {
@@ -49,6 +52,37 @@ const getProfitPercentageColor = (percentage: number): string => {
interface SummaryCardProps {
result?: DiscountSimulationResponse;
isLoading: boolean;
baselineResult?: DiscountSimulationResponse;
onSaveAsBaseline?: () => void;
onClearBaseline?: () => void;
}
function calculateAnnualizedProfitDiff(
current: DiscountSimulationResponse,
baseline: DiscountSimulationResponse
): number | null {
const currentTotals = current.totals;
const baselineTotals = baseline.totals;
if (!currentTotals || !baselineTotals) return null;
// Calculate days in the current simulation period
const startDate = new Date(current.dateRange.start);
const endDate = new Date(current.dateRange.end);
const daysInPeriod = differenceInDays(endDate, startDate) + 1; // +1 to include both start and end days
if (daysInPeriod <= 0) return null;
// Profit difference per order
const profitDiffPerOrder = currentTotals.weightedProfitAmount - baselineTotals.weightedProfitAmount;
// Total profit difference for the period
const totalProfitDiff = profitDiffPerOrder * currentTotals.orders;
// Annualize: scale to 365 days
const annualizedDiff = (totalProfitDiff / daysInPeriod) * 365;
return annualizedDiff;
}
const formatPercent = (value: number) => {
@@ -56,16 +90,23 @@ const formatPercent = (value: number) => {
return `${(value * 100).toFixed(2)}%`;
};
export function SummaryCard({ result, isLoading }: SummaryCardProps) {
export function SummaryCard({
result,
isLoading,
baselineResult,
onSaveAsBaseline,
onClearBaseline
}: SummaryCardProps) {
if (isLoading && !result) {
return (
<Card className="pt-6">
<CardContent>
<div className="h-72 flex flex-col items-center justify-center gap-8">
<Card>
<CardContent className="py-4">
<div className="flex items-center justify-center gap-8">
<div className="text-center space-y-2">
<Skeleton className="h-4 w-24 mx-auto" />
<Skeleton className="h-10 w-20 mx-auto" />
<Skeleton className="h-8 w-20 mx-auto" />
</div>
<Separator orientation="vertical" className="h-12" />
<div className="text-center space-y-2">
<Skeleton className="h-4 w-32 mx-auto" />
<Skeleton className="h-8 w-24 mx-auto" />
@@ -89,32 +130,84 @@ export function SummaryCard({ result, isLoading }: SummaryCardProps) {
: "destructive"
: "secondary";
// Calculate annualized profit difference if baseline exists
const annualizedDiff = result && baselineResult
? calculateAnnualizedProfitDiff(result, baselineResult)
: null;
const hasBaseline = !!baselineResult;
const isPositiveDiff = annualizedDiff !== null && annualizedDiff >= 0;
return (
<Card className="pt-6">
<CardContent>
<div className="h-72 flex flex-col items-center justify-center gap-5">
<Card>
<CardContent className="py-4">
<div className="flex items-center justify-center gap-6">
{/* Weighted Average Profit */}
<div className="text-center">
<p className="text-sm text-muted-foreground mb-3">Weighted Average Profit</p>
<p className="text-sm text-muted-foreground mb-2">Weighted Average Profit</p>
{totals ? (
<span
className="inline-block px-4 py-2 rounded text-white font-semibold text-2xl"
className="inline-block px-3 py-1.5 rounded text-white font-semibold text-lg"
style={{ backgroundColor: profitPercentageColor }}
>
{weightedProfitPercent}
</span>
) : (
<Badge variant={weightedBadgeVariant} className="text-2xl py-2 px-4 font-semibold">
<Badge variant={weightedBadgeVariant} className="text-lg py-1.5 px-3 font-semibold">
{weightedProfitPercent}
</Badge>
)}
</div>
<Separator orientation="horizontal" />
<Separator orientation="vertical" className="h-12" />
{/* Weighted Profit Per Order */}
<div className="text-center">
<p className="text-sm text-muted-foreground mb-3">Weighted Profit Per Order</p>
<Badge variant="secondary" className="text-2xl py-2 px-4 font-semibold">
<p className="text-sm text-muted-foreground mb-2">Weighted Profit Per Order</p>
<Badge variant="secondary" className="text-lg py-1.5 px-3 font-semibold">
{weightedProfitAmount}
</Badge>
</div>
{/* Baseline comparison section */}
{hasBaseline && annualizedDiff !== null && (
<>
<Separator orientation="vertical" className="h-12" />
<div className="text-center">
<p className="text-sm text-muted-foreground mb-2">Annual Change vs Baseline</p>
<span
className={`inline-block px-3 py-1.5 rounded bg-secondary font-semibold text-lg ${
isPositiveDiff ? 'text-green-600' : 'text-red-600'
}`}
>
{isPositiveDiff ? '+' : ''}{formatCurrency(annualizedDiff)}
</span>
</div>
<Button
variant="ghost"
size="sm"
className="text-xs text-muted-foreground hover:text-foreground"
onClick={onClearBaseline}
>
<X className="h-3 w-3 mr-1" />
Clear
</Button>
</>
)}
{/* Save as baseline button */}
{result && !hasBaseline && (
<>
<Separator orientation="vertical" className="h-12" />
<Button
variant="outline"
size="sm"
onClick={onSaveAsBaseline}
>
<Bookmark className="h-3 w-3 mr-1" />
Set as baseline
</Button>
</>
)}
</div>
</CardContent>
</Card>

View File

@@ -62,6 +62,7 @@ export function DiscountSimulator() {
const [pointDollarValue, setPointDollarValue] = useState(DEFAULT_POINT_VALUE);
const [pointDollarTouched, setPointDollarTouched] = useState(false);
const [simulationResult, setSimulationResult] = useState<DiscountSimulationResponse | undefined>(undefined);
const [baselineResult, setBaselineResult] = useState<DiscountSimulationResponse | undefined>(undefined);
const [isSimulating, setIsSimulating] = useState(false);
const [hasLoadedConfig, setHasLoadedConfig] = useState(false);
const [loadedFromStorage, setLoadedFromStorage] = useState(false);
@@ -402,6 +403,16 @@ export function DiscountSimulator() {
}
};
const handleSaveAsBaseline = useCallback(() => {
if (simulationResult) {
setBaselineResult(simulationResult);
}
}, [simulationResult]);
const handleClearBaseline = useCallback(() => {
setBaselineResult(undefined);
}, []);
const resetConfig = useCallback(() => {
skipAutoRunRef.current = true;
const defaultRange = getDefaultDateRange();
@@ -478,17 +489,21 @@ export function DiscountSimulator() {
{/* Right Side - Results */}
<div className="space-y-4 min-w-0 flex-1 overflow-hidden">
{/* Top Right - Summary and Chart */}
<div className="grid gap-4 lg:grid-cols-[160px,1fr] min-w-0">
<div className="w-full min-w-0">
<SummaryCard result={simulationResult} isLoading={isLoading} />
</div>
{/* Top - Summary (horizontal) */}
<SummaryCard
result={simulationResult}
isLoading={isLoading}
baselineResult={baselineResult}
onSaveAsBaseline={handleSaveAsBaseline}
onClearBaseline={handleClearBaseline}
/>
{/* Chart */}
<div className="w-full min-w-0 overflow-hidden">
<ResultsChart buckets={simulationResult?.buckets ?? []} isLoading={isSimulating} />
</div>
</div>
{/* Bottom Right - Table */}
{/* Table */}
<div className="w-full min-w-0 overflow-hidden">
<ResultsTable buckets={simulationResult?.buckets ?? []} isLoading={isSimulating} />
</div>