import { useEffect } from "react"; import type { Bootstrap, Shop, Brand, Product, Item } from "../types.js"; import { helpers, enrichItems } from "../types.js"; import { getToday, getStoredTimezone } from "../tz.js"; import { fmt } from "../format.js"; import { Btn, Pill, Icon } from "./primitives/index.js"; import { remainingShort } from "../stats.js"; import { useExitAnimation } from "../hooks/useExitAnimation.js"; import { useFocusTrap } from "../hooks/useFocusTrap.js"; export function ShopDetail({ shop, data, onClose, onEdit, onDelete, onSelectBrand, onSelectItem, }: { shop: Shop; data: Bootstrap; onClose: () => void; onEdit: () => void; onDelete: () => void; onSelectBrand: (b: Brand) => void; onSelectItem: (i: Item) => void; }) { const allItems = enrichItems(data).filter((i) => i.shopId === shop.id); const hasItems = allItems.length > 0; const active = allItems.filter((i) => i.status === "active" || i.status === "checked-out"); const consumed = allItems.filter((i) => i.status === "consumed"); const gone = allItems.filter((i) => i.status === "gone"); const totalSpend = allItems.reduce((s, i) => s + i.price, 0); const avgPrice = hasItems ? totalSpend / allItems.length : 0; const rated = allItems.filter((i) => i.rating != null); const avgRating = rated.length > 0 ? rated.reduce((s, i) => s + i.rating!, 0) / rated.length : null; const brandIds = [...new Set(allItems.map((i) => i.brandId).filter(Boolean))] as string[]; const brands = brandIds .map((id) => data.brands.find((b) => b.id === id)) .filter(Boolean) as Brand[]; const sortedItems = [...allItems].sort( (a, b) => +new Date(b.purchaseDate) - +new Date(a.purchaseDate), ); const recentItems = sortedItems.slice(0, 20); const todayStr = getToday(getStoredTimezone()); const tz = getStoredTimezone(); const { closing, triggerClose } = useExitAnimation(220, onClose); const trapRef = useFocusTrap(); useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape") triggerClose(); }; document.addEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown); }, [triggerClose]); const statCards: [string, React.ReactNode][] = [ ["Purchases", String(allItems.length)], ["Total spent", hasItems ? fmt.money(totalSpend) : "—"], ["Avg price", hasItems ? fmt.money(avgPrice) : "—"], ]; return (
e.stopPropagation()} style={{ width: "min(720px, 100vw)", height: "100%", animation: closing ? "drawer-out 220ms ease-in forwards" : "drawer-in 250ms ease-out", background: "var(--bg)", borderLeft: "1px solid var(--line)", overflow: "auto", boxShadow: "var(--shadow-lg)", }} >
Shop

{shop.name}

{shop.location && (
{shop.location}
)} {hasItems && (
{statCards.map(([l, v], i) => (
{l}
{v}
))}
)} {hasItems && (
Lifecycle
{active.length > 0 && {active.length} active} {consumed.length > 0 && {consumed.length} consumed} {gone.length > 0 && {gone.length} gone}
)} {avgRating != null && (
Ratings
{[1, 2, 3, 4, 5].map((n) => ( ))}
{avgRating.toFixed(1)} from {rated.length} review{rated.length === 1 ? "" : "s"}
)} {brands.length > 0 && (
Brands ({brands.length})
{brands.map((b, idx) => { const brandItemCount = allItems.filter((i) => i.brandId === b.id).length; return (
onSelectBrand(b)} className="inv-row" style={{ padding: "12px 16px", borderBottom: idx < brands.length - 1 ? "1px solid var(--line)" : "none", display: "grid", gridTemplateColumns: "1fr auto auto", alignItems: "center", gap: 12, background: "var(--surface)", cursor: "pointer", }} >
{b.name}
{brandItemCount} item{brandItemCount === 1 ? "" : "s"}
); })}
)} {hasItems && (
Recent purchases ({allItems.length})
{recentItems.map((item, idx) => { const isInactive = item.status !== "active" && item.status !== "checked-out"; return (
onSelectItem(item)} className="inv-row" style={{ padding: "12px 16px", borderBottom: idx < recentItems.length - 1 ? "1px solid var(--line)" : "none", display: "grid", gridTemplateColumns: "auto 1fr auto auto auto", alignItems: "center", gap: 12, background: "var(--surface)", cursor: "pointer", opacity: isInactive ? 0.55 : 1, }} > {item.assetId} {item.status === "consumed" && Consumed} {item.status === "gone" && Gone} {item.status === "checked-out" && Checked out} {item.status === "active" && helpers.auditOverdue(item, todayStr) && ( Audit due )} {item.status === "active" && !helpers.auditOverdue(item, todayStr) && ( Active )} {fmt.money(item.price)} {fmt.dateShort(item.purchaseDate, tz)} {(item.status === "active" || item.status === "checked-out") ? remainingShort(item) : ""}
); })}
)} {!hasItems && (
No inventory items yet
Purchases from this shop will appear here.
)} {hasItems && (
Cannot delete this shop while it has associated inventory items.
)}
); }