// 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 (
);
};
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 => (
))}
);
};
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}
Prices across stores
{allPrices.map(p => {
const s = STORES.find(s => s.id === p.store);
const isCheap = p === cheapest;
return (
{fmtJPY(p.price)}
{fmtJPY(Math.round(p.price / p.pkg))} / {p.unit}
{isCheap &&
Best}
);
})}
{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;