'use client'; import { useState, useEffect, useCallback } from 'react'; import Link from 'next/link'; import { useApi } from '@/lib/useApi'; import { getRefillAlerts, listRefillLists, createRefillList, updateRefillList, updateRefillListItem, addToCabinet, } from '@/services/refills'; import { RefillListStatus } from '@meshitrack/shared'; import type { z } from 'zod/v4'; import type { RefillAlertResponseSchema, RefillListResponseSchema } from '@meshitrack/shared'; type RefillAlert = z.infer; type RefillList = z.infer; type RefillListItem = RefillList['items'][number]; const STATUS_LABELS: Record = { active: 'Active', shopping: 'Shopping', completed: 'Completed', archived: 'Archived', }; const STATUS_COLORS: Record = { active: 'bg-green-100 text-green-700', shopping: 'bg-blue-100 text-blue-700', completed: 'bg-gray-100 text-gray-600', archived: 'bg-gray-100 text-gray-400', }; function formatDate(dateStr: string): string { return new Date(dateStr).toLocaleDateString(); } // --- Refill alerts panel --- function AlertsPanel({ householdId, onGenerateList, }: { householdId: string; onGenerateList: () => void; }) { const [alerts, setAlerts] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [thresholdDays, setThresholdDays] = useState(7); const [generating, setGenerating] = useState(false); const [listName, setListName] = useState(''); const [showGenerateForm, setShowGenerateForm] = useState(false); const fetchAlerts = useCallback(async () => { setLoading(true); setError(''); try { const result = await getRefillAlerts(householdId, { thresholdDays }); setAlerts(result.data); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load alerts'); } finally { setLoading(false); } }, [householdId, thresholdDays]); useEffect(() => { fetchAlerts(); }, [fetchAlerts]); async function handleGenerateList(e: React.FormEvent) { e.preventDefault(); if (!listName.trim()) return; setGenerating(true); setError(''); try { await createRefillList(householdId, { name: listName.trim(), fromAlerts: true, thresholdDays, }); setShowGenerateForm(false); setListName(''); onGenerateList(); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to generate list'); } finally { setGenerating(false); } } return (

Refill Alerts

Threshold:
{alerts.length > 0 && ( )}
{showGenerateForm && (
setListName(e.target.value)} placeholder="List name, e.g. Weekly refills" className="flex-1 rounded-lg border px-3 py-2 text-sm focus:border-primary-500 focus:ring-1 focus:ring-primary-500 focus:outline-none" />
)} {error && (
{error}
)} {loading ? (
{Array.from({ length: 2 }).map((_, i) => (
))}
) : alerts.length === 0 ? (
No medicines running low within {thresholdDays} days. {thresholdDays < 30 && ( Try increasing the threshold to see more. )}
) : (
{alerts.map((alert) => { const daysColor = alert.daysUntilEmpty <= 3 ? 'text-red-600 font-bold' : alert.daysUntilEmpty <= 7 ? 'text-red-500 font-semibold' : 'text-yellow-600'; return (

{alert.medicineName} {alert.medicineStrength} {alert.medicineStrengthUnit}

{alert.daysUntilEmpty} day{alert.daysUntilEmpty !== 1 ? 's' : ''} left {alert.currentStock} in cabinet {alert.dailyConsumption.toFixed(2)}/day

Suggested: {alert.suggestedQuantity} units

{alert.cheapestOption && (

Best: {alert.cheapestOption.storeName} —{' '} {alert.cheapestOption.price.toFixed(2)}

)} {!alert.cheapestOption && alert.lastKnownPrice && (

Last: {alert.lastKnownPrice.storeName} —{' '} {alert.lastKnownPrice.price.toFixed(2)}

)}
); })}
)}
); } // --- Refill list detail --- function RefillListDetail({ list, householdId, onUpdated, onClose, }: { list: RefillList; householdId: string; onUpdated: () => void; onClose: () => void; }) { const [items, setItems] = useState(list.items); const [actualPrices, setActualPrices] = useState>({}); const [adding, setAdding] = useState(false); const [error, setError] = useState(''); async function handleToggleItem(item: RefillListItem) { setError(''); try { const updated = await updateRefillListItem(householdId, list._id, item._id, { checked: !item.checked, actualPrice: !item.checked && actualPrices[item._id] ? Number(actualPrices[item._id]) : undefined, }); setItems(updated.items); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to update item'); } } async function handleUpdateStatus(status: RefillListStatus) { setError(''); try { await updateRefillList(householdId, list._id, { status }); onUpdated(); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to update status'); } } async function handleAddToCabinet() { const checkedCount = items.filter((i) => i.checked && !i.addedToCabinet).length; /* v8 ignore next 4 */ if (checkedCount === 0) { setError('No checked items to add to cabinet.'); return; } if (!confirm(`Add ${checkedCount} checked item${checkedCount !== 1 ? 's' : ''} to your cabinet?`)) return; setAdding(true); setError(''); try { const result = await addToCabinet(householdId, list._id); onUpdated(); setAdding(false); if (result.addedCount > 0) { alert(`Added ${result.addedCount} item${result.addedCount !== 1 ? 's' : ''} to your cabinet.`); } } catch (err) { setError(err instanceof Error ? err.message : 'Failed to add to cabinet'); setAdding(false); } } const checkedNotAdded = items.filter((i) => i.checked && !i.addedToCabinet).length; const totalChecked = items.filter((i) => i.checked).length; return (

{list.name}

{STATUS_LABELS[list.status] ?? list.status} {totalChecked}/{items.length} checked {list.totalEstimatedCost != null && ( Est. {list.totalEstimatedCost.toFixed(2)} )}
{error && (
{error}
)} {items.length === 0 ? (

No items in this list.

) : (
{items.map((item) => (
handleToggleItem(item)} disabled={item.addedToCabinet} className="mt-0.5 h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500" />
{item.medicineName} {item.quantity} {item.unit} {item.estimatedPrice != null && ( est. {item.estimatedPrice.toFixed(2)} )} {item.addedToCabinet && ( in cabinet )}
{item.notes &&

{item.notes}

}
{item.checked && !item.addedToCabinet && (
setActualPrices((prev) => ({ ...prev, [item._id]: e.target.value })) } placeholder="Actual price" className="w-28 rounded-lg border px-2 py-1 text-xs focus:border-primary-500 focus:ring-1 focus:ring-primary-500 focus:outline-none" />
)}
))}
)}
{checkedNotAdded > 0 && ( )} {list.status === RefillListStatus.ACTIVE && ( )} {list.status === RefillListStatus.SHOPPING && ( )} {(list.status === RefillListStatus.ACTIVE || list.status === RefillListStatus.SHOPPING) && ( )}
); } // --- Create list form --- function CreateListForm({ householdId, onCreated, onCancel, }: { householdId: string; onCreated: () => void; onCancel: () => void; }) { const [name, setName] = useState(''); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(''); async function handleSubmit(e: React.FormEvent) { e.preventDefault(); setError(''); setSubmitting(true); try { await createRefillList(householdId, { name: name.trim(), fromAlerts: false, thresholdDays: 7 }); onCreated(); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to create list'); } finally { setSubmitting(false); } } return (

New Refill List

{error && (
{error}
)}
setName(e.target.value)} placeholder="List name" className="flex-1 rounded-lg border px-3 py-2 text-sm focus:border-primary-500 focus:ring-1 focus:ring-primary-500 focus:outline-none" />
); } // --- Refill lists panel --- function RefillListsPanel({ householdId }: { householdId: string }) { const [lists, setLists] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [filterStatus, setFilterStatus] = useState(''); const [showForm, setShowForm] = useState(false); const [selectedList, setSelectedList] = useState(null); const fetchLists = useCallback(async () => { setLoading(true); setError(''); try { const result = await listRefillLists(householdId, { status: (filterStatus as RefillListStatus) || undefined, limit: 30, }); setLists(result.data); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load refill lists'); } finally { setLoading(false); } }, [householdId, filterStatus]); useEffect(() => { fetchLists(); }, [fetchLists]); function handleSelectList(list: RefillList) { setSelectedList((prev) => (prev?._id === list._id ? null : list)); } return (

Refill Lists

{showForm && ( { setShowForm(false); fetchLists(); }} onCancel={() => setShowForm(false)} /> )} {selectedList && (
{ fetchLists(); setSelectedList(null); }} onClose={() => setSelectedList(null)} />
)} {error && (
{error}
)} {loading ? (
{Array.from({ length: 3 }).map((_, i) => (
))}
) : lists.length === 0 ? (
{filterStatus ? `No ${STATUS_LABELS[filterStatus] ?? filterStatus} lists.` : 'No refill lists yet. Create one above or generate from alerts.'}
) : (
{lists.map((list) => { const checkedCount = list.items.filter((i) => i.checked).length; const isSelected = selectedList?._id === list._id; return ( ); })}
)}
); } // --- Main page --- function RefillsContent({ householdId }: { householdId: string }) { const [listsKey, setListsKey] = useState(0); return (

Refills

setListsKey((k) => k + 1)} />
); } export default function RefillsPage() { const { householdId, isLoading: sessionLoading } = useApi(); if (sessionLoading) { return (

Refills

); } if (!householdId) { return (

Refills

You need to{' '} create or join a household {' '} before managing refills.

); } return ; }