feat: add servings field to Recipe model and implement inventory comparison functionality
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user