feat: add servings field to Recipe model and implement inventory comparison functionality

This commit is contained in:
Nils-Johan Gynther
2026-04-17 18:48:08 +02:00
parent 8a86b0aebd
commit 8e0aed032c
10 changed files with 260 additions and 20 deletions
+74 -1
View File
@@ -16,6 +16,16 @@ type MealPlanEntry = {
type ShoppingItem = { productId: number; name: string; quantity: number; unit: string };
type InventoryCompareItem = {
productId: number;
name: string;
required: number;
unit: string;
available: number;
missing: number;
status: 'enough' | 'missing';
};
function getWeekDates(offset = 0): string[] {
const now = new Date();
const day = now.getDay();
@@ -32,6 +42,7 @@ export default function MealPlanClient({ recipes }: { recipes: Recipe[] }) {
const [weekOffset, setWeekOffset] = useState(0);
const [entries, setEntries] = useState<MealPlanEntry[]>([]);
const [shopping, setShopping] = useState<ShoppingItem[]>([]);
const [inventoryCompare, setInventoryCompare] = useState<InventoryCompareItem[]>([]);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState<string | null>(null); // date being saved
@@ -48,17 +59,21 @@ export default function MealPlanClient({ recipes }: { recipes: Recipe[] }) {
const load = useCallback(async () => {
setLoading(true);
try {
const [entriesRes, shoppingRes] = await Promise.all([
const [entriesRes, shoppingRes, compareRes] = await Promise.all([
fetch(`/api/meal-plan-proxy?from=${from}&to=${to}`),
fetch(`/api/meal-plan-proxy/shopping?from=${from}&to=${to}`),
fetch(`/api/meal-plan-proxy/inventory-compare?from=${from}&to=${to}`),
]);
const entriesData = await entriesRes.json();
setEntries(Array.isArray(entriesData) ? entriesData : []);
if (shoppingRes.ok) setShopping(await shoppingRes.json());
else setShopping([]);
if (compareRes.ok) setInventoryCompare(await compareRes.json());
else setInventoryCompare([]);
} catch {
setEntries([]);
setShopping([]);
setInventoryCompare([]);
} finally {
setLoading(false);
}
@@ -190,6 +205,64 @@ export default function MealPlanClient({ recipes }: { recipes: Recipe[] }) {
</ul>
)}
</section>
{/* Inventariejämförelse */}
{plannedCount > 0 && inventoryCompare.length > 0 && (
<section style={{ border: '1px solid #dee2e6', borderRadius: '8px', padding: '1rem' }}>
<h2 style={{ margin: '0 0 0.5rem', fontSize: '1.1rem' }}>Inventariegranskning</h2>
<p style={{ margin: '0 0 0.75rem', fontSize: '0.85rem', color: '#666' }}>
Vad du har hemma vs. vad veckans recept kräver.
</p>
{(() => {
const missingCount = inventoryCompare.filter((i) => i.status === 'missing').length;
return missingCount === 0 ? (
<p style={{ color: '#1f5f2c', fontWeight: 600, margin: '0 0 0.75rem' }}>
Du har allt hemma!
</p>
) : (
<p style={{ color: '#8b0000', fontWeight: 600, margin: '0 0 0.75rem' }}>
{missingCount} ingrediens{missingCount !== 1 ? 'er' : ''} saknas eller räcker inte
</p>
);
})()}
<ul style={{ listStyle: 'none', padding: 0, margin: 0, display: 'grid', gap: '0.4rem' }}>
{inventoryCompare.map((item) => (
<li
key={`${item.productId}-${item.unit}`}
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '0.4rem 0.6rem',
borderRadius: '6px',
background: item.status === 'missing' ? '#ffeaea' : '#ecf8ee',
fontSize: '0.88rem',
flexWrap: 'wrap',
gap: '0.25rem',
}}
>
<span>
<strong>{item.name}</strong>
{' '}
<span style={{ color: '#555' }}>
{item.required % 1 === 0 ? item.required : item.required.toFixed(1)} {item.unit} behövs
{' · '}
{item.available % 1 === 0 ? item.available : item.available.toFixed(1)} {item.unit} hemma
</span>
</span>
{item.status === 'missing' && item.missing > 0 && (
<span style={{ color: '#8b0000', fontWeight: 600, whiteSpace: 'nowrap' }}>
Saknar {item.missing % 1 === 0 ? item.missing : item.missing.toFixed(1)} {item.unit}
</span>
)}
{item.status === 'enough' && (
<span style={{ color: '#1f5f2c', fontWeight: 600 }}></span>
)}
</li>
))}
</ul>
</section>
)}
</>
)}
</div>