710 lines
23 KiB
TypeScript
710 lines
23 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect, useCallback } from 'react';
|
|
import Link from 'next/link';
|
|
import { useApi } from '@/lib/useApi';
|
|
import { SetPageHeader } from '@/components/layout/SetPageHeader';
|
|
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<typeof RefillAlertResponseSchema>;
|
|
type RefillList = z.infer<typeof RefillListResponseSchema>;
|
|
type RefillListItem = RefillList['items'][number];
|
|
|
|
const STATUS_LABELS: Record<string, string> = {
|
|
active: 'Active',
|
|
shopping: 'Shopping',
|
|
completed: 'Completed',
|
|
archived: 'Archived',
|
|
};
|
|
|
|
const STATUS_PILL: Record<string, string> = {
|
|
active: 'mt-pill--ok',
|
|
shopping: 'mt-pill--info',
|
|
completed: 'mt-pill--ghost',
|
|
archived: 'mt-pill--ghost',
|
|
};
|
|
|
|
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<RefillAlert[]>([]);
|
|
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 (
|
|
<div className="mt-card">
|
|
<div className="flex items-center justify-between mb-4 flex-wrap gap-3">
|
|
<h2 className="text-lg font-semibold">Refill Alerts</h2>
|
|
<div className="flex items-center gap-3">
|
|
<div className="flex items-center gap-2 text-sm text-gray-600">
|
|
<span>Threshold:</span>
|
|
<select
|
|
value={thresholdDays}
|
|
onChange={(e) => setThresholdDays(Number(e.target.value))}
|
|
className="mt-field"
|
|
style={{ width: 'auto' }}
|
|
>
|
|
{[3, 5, 7, 10, 14, 30].map((d) => (
|
|
<option key={d} value={d}>
|
|
{d} days
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
{alerts.length > 0 && (
|
|
<button
|
|
onClick={() => setShowGenerateForm(!showGenerateForm)}
|
|
className="mt-btn mt-btn--primary"
|
|
>
|
|
Generate Refill List
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{showGenerateForm && (
|
|
<form onSubmit={handleGenerateList} className="mb-4 flex items-center gap-3">
|
|
<input
|
|
type="text"
|
|
required
|
|
maxLength={200}
|
|
value={listName}
|
|
onChange={(e) => setListName(e.target.value)}
|
|
placeholder="List name, e.g. Weekly refills"
|
|
className="mt-field"
|
|
/>
|
|
<button type="submit" disabled={generating} className="mt-btn mt-btn--primary">
|
|
{generating ? 'Creating...' : 'Create'}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowGenerateForm(false)}
|
|
className="mt-btn mt-btn--ghost"
|
|
>
|
|
Cancel
|
|
</button>
|
|
</form>
|
|
)}
|
|
|
|
{error && (
|
|
<div className="mt-alert mt-alert--danger mb-4">
|
|
{error}
|
|
<button onClick={() => setError('')} className="ml-2 underline">
|
|
Dismiss
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{loading ? (
|
|
<div className="space-y-3">
|
|
{Array.from({ length: 2 }).map((_, i) => (
|
|
<div key={i} className="animate-pulse h-20 rounded-lg bg-gray-200" />
|
|
))}
|
|
</div>
|
|
) : alerts.length === 0 ? (
|
|
<div className="py-6 text-center text-sm text-gray-500">
|
|
No medicines running low within {thresholdDays} days.
|
|
{thresholdDays < 30 && (
|
|
<span className="block mt-1 text-xs">Try increasing the threshold to see more.</span>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{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 (
|
|
<div key={alert.medicineId} className="rounded-lg border bg-gray-50 p-4">
|
|
<div className="flex items-start justify-between gap-4 flex-wrap">
|
|
<div>
|
|
<h3 className="font-semibold text-gray-900">
|
|
{alert.medicineName}
|
|
<span className="ml-2 text-sm font-normal text-gray-500">
|
|
{alert.medicineStrength} {alert.medicineStrengthUnit}
|
|
</span>
|
|
</h3>
|
|
<div className="flex flex-wrap gap-4 mt-1 text-sm">
|
|
<span className={daysColor}>
|
|
{alert.daysUntilEmpty} day{alert.daysUntilEmpty !== 1 ? 's' : ''} left
|
|
</span>
|
|
<span className="text-gray-500">{alert.currentStock} in cabinet</span>
|
|
<span className="text-gray-500">{alert.dailyConsumption.toFixed(2)}/day</span>
|
|
</div>
|
|
</div>
|
|
<div className="text-right text-sm">
|
|
<p className="text-gray-600">
|
|
Suggested:{' '}
|
|
<span className="font-medium">{alert.suggestedQuantity} units</span>
|
|
</p>
|
|
{alert.cheapestOption && (
|
|
<p className="text-green-700 font-medium">
|
|
Best: {alert.cheapestOption.storeName} —{' '}
|
|
{alert.cheapestOption.price.toFixed(2)}
|
|
</p>
|
|
)}
|
|
{!alert.cheapestOption && alert.lastKnownPrice && (
|
|
<p className="text-gray-500">
|
|
Last: {alert.lastKnownPrice.storeName} —{' '}
|
|
{alert.lastKnownPrice.price.toFixed(2)}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// --- Refill list detail ---
|
|
|
|
function RefillListDetail({
|
|
list,
|
|
householdId,
|
|
onUpdated,
|
|
onClose,
|
|
}: {
|
|
list: RefillList;
|
|
householdId: string;
|
|
onUpdated: () => void;
|
|
onClose: () => void;
|
|
}) {
|
|
const [items, setItems] = useState<RefillListItem[]>(list.items);
|
|
const [actualPrices, setActualPrices] = useState<Record<string, string>>({});
|
|
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 (
|
|
<div className="mt-card">
|
|
<div className="flex items-start justify-between gap-4 mb-4">
|
|
<div>
|
|
<h2 className="text-lg font-semibold text-gray-900">{list.name}</h2>
|
|
<div className="flex items-center gap-2 mt-1">
|
|
<span className={`mt-pill ${STATUS_PILL[list.status] ?? STATUS_PILL['active']}`}>
|
|
{STATUS_LABELS[list.status] ?? list.status}
|
|
</span>
|
|
<span className="text-xs text-gray-400">
|
|
{totalChecked}/{items.length} checked
|
|
</span>
|
|
{list.totalEstimatedCost != null && (
|
|
<span className="text-xs text-gray-500">
|
|
Est. {list.totalEstimatedCost.toFixed(2)}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<button onClick={onClose} className="mt-btn mt-btn--icon" title="Close">
|
|
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M6 18L18 6M6 6l12 12"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="mt-alert mt-alert--danger mb-4">
|
|
{error}
|
|
<button onClick={() => setError('')} className="ml-2 underline">
|
|
Dismiss
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{items.length === 0 ? (
|
|
<p className="text-sm text-gray-500 py-4 text-center">No items in this list.</p>
|
|
) : (
|
|
<div className="space-y-2 mb-4">
|
|
{items.map((item) => (
|
|
<div
|
|
key={item._id}
|
|
className={`rounded-lg border p-3 flex items-start gap-3 ${item.addedToCabinet ? 'opacity-50' : ''}`}
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
checked={item.checked}
|
|
onChange={() => handleToggleItem(item)}
|
|
disabled={item.addedToCabinet}
|
|
className="h-4 w-4 rounded border-gray-300"
|
|
/>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-2 flex-wrap">
|
|
<span
|
|
className={`text-sm font-medium ${item.checked ? 'line-through text-gray-400' : 'text-gray-900'}`}
|
|
>
|
|
{item.medicineName}
|
|
</span>
|
|
<span className="text-xs text-gray-500">
|
|
{item.quantity} {item.unit}
|
|
</span>
|
|
{item.estimatedPrice != null && (
|
|
<span className="text-xs text-gray-400">
|
|
est. {item.estimatedPrice.toFixed(2)}
|
|
</span>
|
|
)}
|
|
{item.addedToCabinet && <span className="mt-pill mt-pill--ok">in cabinet</span>}
|
|
</div>
|
|
{item.notes && <p className="text-xs text-gray-400 mt-0.5">{item.notes}</p>}
|
|
</div>
|
|
{item.checked && !item.addedToCabinet && (
|
|
<div className="shrink-0">
|
|
<input
|
|
type="number"
|
|
min={0}
|
|
step="any"
|
|
value={actualPrices[item._id] ?? ''}
|
|
onChange={(e) =>
|
|
setActualPrices((prev) => ({ ...prev, [item._id]: e.target.value }))
|
|
}
|
|
placeholder="Actual price"
|
|
className="mt-field"
|
|
style={{ width: '7rem', fontSize: '0.75rem' }}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex flex-wrap items-center gap-3 border-t pt-4">
|
|
{checkedNotAdded > 0 && (
|
|
<button onClick={handleAddToCabinet} disabled={adding} className="mt-btn mt-btn--primary">
|
|
{adding ? 'Adding...' : `Add ${checkedNotAdded} to Cabinet`}
|
|
</button>
|
|
)}
|
|
{list.status === RefillListStatus.ACTIVE && (
|
|
<button
|
|
onClick={() => handleUpdateStatus(RefillListStatus.SHOPPING)}
|
|
className="mt-btn mt-btn--ghost"
|
|
>
|
|
Start shopping
|
|
</button>
|
|
)}
|
|
{list.status === RefillListStatus.SHOPPING && (
|
|
<button
|
|
onClick={() => handleUpdateStatus(RefillListStatus.COMPLETED)}
|
|
className="mt-btn mt-btn--ghost"
|
|
>
|
|
Mark complete
|
|
</button>
|
|
)}
|
|
{(list.status === RefillListStatus.ACTIVE || list.status === RefillListStatus.SHOPPING) && (
|
|
<button
|
|
onClick={() => handleUpdateStatus(RefillListStatus.ARCHIVED)}
|
|
className="mt-btn mt-btn--ghost"
|
|
>
|
|
Archive
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// --- 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 (
|
|
<div className="mt-card mb-4">
|
|
<h3 className="text-base font-semibold mb-3">New Refill List</h3>
|
|
{error && <div className="mt-alert mt-alert--danger mb-3">{error}</div>}
|
|
<form onSubmit={handleSubmit} className="flex items-center gap-3">
|
|
<input
|
|
type="text"
|
|
required
|
|
maxLength={200}
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
placeholder="List name"
|
|
className="mt-field"
|
|
/>
|
|
<button type="submit" disabled={submitting} className="mt-btn mt-btn--primary">
|
|
{submitting ? 'Creating...' : 'Create'}
|
|
</button>
|
|
<button type="button" onClick={onCancel} className="mt-btn mt-btn--ghost">
|
|
Cancel
|
|
</button>
|
|
</form>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// --- Refill lists panel ---
|
|
|
|
function RefillListsPanel({ householdId }: { householdId: string }) {
|
|
const [lists, setLists] = useState<RefillList[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState('');
|
|
const [filterStatus, setFilterStatus] = useState('');
|
|
const [showForm, setShowForm] = useState(false);
|
|
const [selectedList, setSelectedList] = useState<RefillList | null>(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 (
|
|
<div>
|
|
<div className="flex items-center justify-between mb-4 flex-wrap gap-3">
|
|
<h2 className="text-lg font-semibold">Refill Lists</h2>
|
|
<div className="flex items-center gap-3">
|
|
<select
|
|
value={filterStatus}
|
|
onChange={(e) => setFilterStatus(e.target.value)}
|
|
className="mt-field"
|
|
style={{ width: 'auto' }}
|
|
>
|
|
<option value="">All statuses</option>
|
|
{Object.values(RefillListStatus).map((s) => (
|
|
<option key={s} value={s}>
|
|
{STATUS_LABELS[s] ?? s}
|
|
</option>
|
|
))}
|
|
</select>
|
|
<button onClick={() => setShowForm(!showForm)} className="mt-btn mt-btn--primary">
|
|
{showForm ? 'Cancel' : 'New List'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{showForm && (
|
|
<CreateListForm
|
|
householdId={householdId}
|
|
onCreated={() => {
|
|
setShowForm(false);
|
|
fetchLists();
|
|
}}
|
|
onCancel={() => setShowForm(false)}
|
|
/>
|
|
)}
|
|
|
|
{selectedList && (
|
|
<div className="mb-4">
|
|
<RefillListDetail
|
|
key={selectedList._id}
|
|
list={selectedList}
|
|
householdId={householdId}
|
|
onUpdated={() => {
|
|
fetchLists();
|
|
setSelectedList(null);
|
|
}}
|
|
onClose={() => setSelectedList(null)}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{error && (
|
|
<div className="mt-alert mt-alert--danger mb-4">
|
|
{error}
|
|
<button onClick={() => setError('')} className="ml-2 underline">
|
|
Dismiss
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{loading ? (
|
|
<div className="space-y-3">
|
|
{Array.from({ length: 3 }).map((_, i) => (
|
|
<div key={i} className="animate-pulse rounded-xl border bg-white p-4 h-16" />
|
|
))}
|
|
</div>
|
|
) : lists.length === 0 ? (
|
|
<div className="mt-card text-center" style={{ color: 'var(--ink-muted)' }}>
|
|
{filterStatus
|
|
? `No ${STATUS_LABELS[filterStatus] ?? filterStatus} lists.`
|
|
: 'No refill lists yet. Create one above or generate from alerts.'}
|
|
</div>
|
|
) : (
|
|
<div className="space-y-2">
|
|
{lists.map((list) => {
|
|
const checkedCount = list.items.filter((i) => i.checked).length;
|
|
const isSelected = selectedList?._id === list._id;
|
|
|
|
return (
|
|
<button
|
|
key={list._id}
|
|
onClick={() => handleSelectList(list)}
|
|
className={`w-full mt-card text-left transition-colors ${
|
|
isSelected ? 'outline outline-2 outline-[var(--brand)]' : ''
|
|
}`}
|
|
>
|
|
<div className="flex items-center justify-between gap-4">
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-2 flex-wrap mb-1">
|
|
<span className="font-medium text-gray-900 truncate">{list.name}</span>
|
|
<span
|
|
className={`mt-pill ${STATUS_PILL[list.status] ?? STATUS_PILL['active']}`}
|
|
>
|
|
{STATUS_LABELS[list.status] ?? list.status}
|
|
</span>
|
|
</div>
|
|
<p className="text-xs text-gray-500">
|
|
{list.items.length} item{list.items.length !== 1 ? 's' : ''}
|
|
{list.items.length > 0 && ` — ${checkedCount} checked`}
|
|
{list.totalEstimatedCost != null &&
|
|
` — est. ${list.totalEstimatedCost.toFixed(2)}`}
|
|
</p>
|
|
</div>
|
|
<div className="text-xs text-gray-400 shrink-0">{formatDate(list.createdAt)}</div>
|
|
</div>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// --- Main page ---
|
|
|
|
function RefillsContent({ householdId }: { householdId: string }) {
|
|
const [listsKey, setListsKey] = useState(0);
|
|
|
|
return (
|
|
<div>
|
|
<div className="space-y-6">
|
|
<AlertsPanel householdId={householdId} onGenerateList={() => setListsKey((k) => k + 1)} />
|
|
<div className="mt-card">
|
|
<RefillListsPanel key={listsKey} householdId={householdId} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function RefillsPage() {
|
|
const { householdId, isLoading: sessionLoading } = useApi();
|
|
|
|
if (sessionLoading) {
|
|
return (
|
|
<>
|
|
<SetPageHeader title="Refills" subtitle="Running-low alerts and shopping lists" />
|
|
<div style={{ padding: '28px 32px' }}>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
|
{[1, 2].map((i) => (
|
|
<div
|
|
key={i}
|
|
style={{ height: 96, borderRadius: 'var(--r-md)', background: 'var(--bg-inset)' }}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
if (!householdId) {
|
|
return (
|
|
<>
|
|
<SetPageHeader title="Refills" subtitle="Running-low alerts and shopping lists" />
|
|
<div style={{ padding: '28px 32px' }}>
|
|
<div
|
|
style={{
|
|
background: 'var(--bg-elev)',
|
|
border: '1px solid var(--border)',
|
|
borderRadius: 'var(--r-md)',
|
|
padding: 24,
|
|
}}
|
|
>
|
|
<p style={{ color: 'var(--ink-muted)', fontSize: 14 }}>
|
|
You need to{' '}
|
|
<Link href="/settings" style={{ color: 'var(--brand)', textDecoration: 'underline' }}>
|
|
create or join a household
|
|
</Link>{' '}
|
|
before managing refills.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<SetPageHeader title="Refills" subtitle="Running-low alerts and shopping lists" />
|
|
<div className="mt-page">
|
|
<RefillsContent householdId={householdId} />
|
|
</div>
|
|
</>
|
|
);
|
|
}
|