// Medicine Cabinet — the hero screen. // Layout: summary strip + filter rail + "cabinet grid" of med cards with // days-of-supply bar, expiry, lot info, and inline quick actions. const { CABINET, MED, MEDICINES, PRICES, STORES } = window.MESHI; const cabinetStatus = (row) => { const days = Math.floor(row.qty / (row.perDose * (1 / row.daysPerUnit))); // days until expiry const exp = new Date(row.expiry); const daysToExp = Math.round((exp - new Date('2026-04-18')) / 86400000); let level = 'ok'; if (row.qty <= row.lowAt) level = 'low'; if (daysToExp < 60) level = level === 'low' ? 'critical' : 'expiring'; if (row.qty <= Math.ceil(row.lowAt / 2)) level = 'critical'; return { days, daysToExp, level }; }; const fmtJPY = (n) => '¥' + Number(n).toLocaleString('en-US'); const CabinetSummary = () => { const rows = CABINET.map(r => ({ ...r, ...cabinetStatus(r) })); const total = rows.length; const low = rows.filter(r => r.level === 'low' || r.level === 'critical').length; const expiring = rows.filter(r => r.level === 'expiring' || r.level === 'critical').length; const inventoryValue = rows.reduce((s, r) => { const p = PRICES.find(p => p.medId === r.medId); return s + (p ? (p.price / p.pkg) * r.qty : 0); }, 0); const stats = [ { label: 'Items', value: total, hint: 'distinct medicines', tone: 'ink' }, { label: 'Running low', value: low, hint: 'need a refill soon', tone: 'danger' }, { label: 'Expiring <60d', value: expiring, hint: 'check dates', tone: 'warn' }, { label: 'Cabinet value', value: fmtJPY(Math.round(inventoryValue)), hint: 'at last paid price', tone: 'brand' }, ]; return (
{stats.map(s => (
{s.label}
{s.value}
{s.hint}
))}
); }; const DaysBar = ({ days, low = 14 }) => { const cap = 60; const pct = Math.min(100, (days / cap) * 100); const tone = days <= low / 2 ? 'critical' : days <= low ? 'low' : 'ok'; return (
{days} days
); }; const CabinetCard = ({ row, onOpen }) => { const med = MED[row.medId]; const { days, daysToExp, level } = cabinetStatus(row); const price = PRICES.find(p => p.medId === row.medId); return ( ); }; const CabinetFilters = ({ filter, setFilter, view, setView, query, setQuery }) => { const tabs = [ { id: 'all', label: 'All', count: CABINET.length }, { id: 'low', label: 'Running low', count: CABINET.filter(r => cabinetStatus(r).level !== 'ok').length }, { id: 'rx', label: 'Prescription', count: CABINET.filter(r => MED[r.medId].category === 'Prescription').length }, { id: 'supplement', label: 'Supplements', count: CABINET.filter(r => MED[r.medId].category === 'Supplement').length }, ]; return (
{tabs.map(t => ( ))}
setQuery(e.target.value)} />
); }; const CabinetTable = ({ rows, onOpen }) => (
Medicine
Qty
Supply
Expiry
Unit cost
Status
{rows.map(r => { const med = MED[r.medId]; const { days, daysToExp, level } = cabinetStatus(r); const price = PRICES.find(p => p.medId === r.medId); return ( ); })}
); const CabinetShelf = ({ rows, onOpen }) => { // Group by category, then render as "shelves" const groups = {}; rows.forEach(r => { const cat = MED[r.medId].category; (groups[cat] = groups[cat] || []).push(r); }); return (
{Object.entries(groups).map(([cat, items]) => (
{cat}
{items.map(r => { const med = MED[r.medId]; const { days, level } = cabinetStatus(r); const fillPct = Math.min(100, (days / 60) * 100); return ( ); })}
))}
); }; const CabinetDetail = ({ row, onClose }) => { if (!row) return null; const med = MED[row.medId]; const { days, daysToExp, level } = cabinetStatus(row); const price = PRICES.find(p => p.medId === row.medId); const allPrices = PRICES.filter(p => p.medId === row.medId); const cheapest = allPrices.reduce((a, b) => (a && a.price / a.pkg < b.price / b.pkg ? a : b), null); return (
e.target === e.currentTarget && onClose()}>
{med.name}
{med.strength} · {med.form} · {med.category}
On hand
{row.qty} {row.unit}
Supply
{days} days
Expires
{row.expiry}
Prices across stores
{allPrices.map(p => { const s = STORES.find(s => s.id === p.store); const isCheap = p === cheapest; return (
{s?.name}
{p.date}
{fmtJPY(p.price)}
{fmtJPY(Math.round(p.price / p.pkg))} / {p.unit}
{isCheap && Best}
); })}
{med.notes && (
Notes
{med.notes}
)}
); }; const CabinetPage = () => { const [filter, setFilter] = useState('all'); const [view, setView] = useState('grid'); const [query, setQuery] = useState(''); const [detail, setDetail] = useState(null); const rows = useMemo(() => { return CABINET .filter(r => { if (filter === 'low') return cabinetStatus(r).level !== 'ok'; if (filter === 'rx') return MED[r.medId].category === 'Prescription'; if (filter === 'supplement') return MED[r.medId].category === 'Supplement'; return true; }) .filter(r => !query || MED[r.medId].name.toLowerCase().includes(query.toLowerCase())) .sort((a, b) => cabinetStatus(a).days - cabinetStatus(b).days); }, [filter, query]); return (
{view === 'grid' && (
{rows.map(r => setDetail(r)} />)}
)} {view === 'table' && } {view === 'shelf' && } setDetail(null)} />
); }; window.CabinetPage = CabinetPage; window.cabinetStatus = cabinetStatus; window.fmtJPY = fmtJPY;