"use client";
import { RiCheckLine as Check, RiFileCopyLine as Copy, RiRepeat2Line as Repeat2, RiCloseLine as X } from "@remixicon/react";
import { useEffect, useRef, useState } from "react";
import { formatAmount, formatUsd } from "@/app/lib/units";
import type { Aisle, GroceryLine, GrocerySummary, ItemId } from "@/app/types";
import { cx } from "@/utils/cx";
import { BudgetMeter } from "./BudgetMeter";
interface PanelProps {
summary: GrocerySummary;
budget: number;
priceSourceLabel: string;
onToggleOnHand: (itemId: ItemId, onHand: boolean) => void;
}
export function GrocerySummaryPanel({ summary, budget, priceSourceLabel, onToggleOnHand }: PanelProps) {
const [copied, setCopied] = useState(false);
const byAisle = groupByAisle(summary.lines);
const remaining = budget - summary.checkoutTotal;
async function copyList() {
const text = summary.lines
.filter((l) => !l.onHand)
.map((l) => `☐ ${l.packages} × ${l.item.name} (${l.item.packageLabel})`)
.join("\n");
try {
await navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch {
// Clipboard can be blocked; list remains visible on screen.
}
}
return (
{/* --- Tactile Market Receipt Header -------------------------------- */}
= 0 ? "Left in budget" : "Over budget"}
value={formatUsd(Math.abs(remaining))}
tone={remaining >= 0 ? "text-status-ok" : "text-status-danger"}
/>
{summary.sharedItemCount > 0 && (
{summary.sharedItemCount} store item{summary.sharedItemCount > 1 ? "s" : ""} shared across meals
)}
{/* --- Shopping List Header ----------------------------------------- */}
Shopping list
Tap “I have this” on anything in your kitchen and it drops off the bill.
{summary.lines.length > 0 && (
)}
{summary.lines.length === 0 ? (
Add meals to build your aisle-sorted grocery receipt.
) : (
{byAisle.map(([aisle, lines]) => (
{aisle}
{lines.map((line) => (
))}
))}
)}
{priceSourceLabel}. Actual store prices vary by location.
);
}
function GroceryRow({ line, onToggleOnHand }: { line: GroceryLine; onToggleOnHand: PanelProps["onToggleOnHand"] }) {
const { item, onHand } = line;
return (
{line.packages > 1 && {line.packages} × }
{item.name}
{item.packageLabel} · need {formatAmount(line.neededAmount, item.baseUnit)}
{line.usedBy.length > 1 && (
Used in {line.usedBy.length} meals
)}
{onHand ? "$0.00" : formatUsd(line.checkoutCost)}
);
}
function Stat({ label, value, hint, tone = "text-ink" }: { label: string; value: string; hint?: string; tone?: string }) {
return (
{label}
{value}
);
}
function groupByAisle(lines: GroceryLine[]): [Aisle, GroceryLine[]][] {
const groups = new Map();
for (const line of lines) {
const group = groups.get(line.item.aisle) ?? [];
group.push(line);
groups.set(line.item.aisle, group);
}
return [...groups];
}
// ---------------------------------------------------------------------------
// GroceryListDrawer: Native