From 20330f6410762608268e6320b4888c08b4add47f Mon Sep 17 00:00:00 2001 From: Nils-Johan Gynther Date: Sat, 18 Apr 2026 09:13:35 +0200 Subject: [PATCH] feat(matplan): enhance shopping list with inventory status indicators and summary --- NEXT_STEPS.md | 16 ++- frontend/app/matplan/MealPlanClient.tsx | 167 +++++++++++++----------- 2 files changed, 103 insertions(+), 80 deletions(-) diff --git a/NEXT_STEPS.md b/NEXT_STEPS.md index bfb2a63b..77030d69 100644 --- a/NEXT_STEPS.md +++ b/NEXT_STEPS.md @@ -16,7 +16,7 @@ | Matplanering (veckovy, inköpslista) | ✅ Klart | | Matplan — portionsjustering per dag | ✅ Klart | | Matplan — inventariejämförelse (backend) | ✅ Klart | -| Matplan — inventariejämförelse (frontend-vy) | ⚠️ Grundläggande, saknar ✅/⚠️/❌-status | +| Matplan — inventariejämförelse (frontend-vy) | ✅ Klart (✅/⚠️/❌ integrerat i inköpslistan) | | Baslager (lista, lägg till, ta bort) | ✅ Klart | | Admin: Produkter (edit, merge, duplicate, restore, reset) | ✅ Klart | | Admin: Bulk-kategorisering | ✅ Klart | @@ -61,13 +61,15 @@ Idag har alla inloggade användare samma behörighetsnivå — ett säkerhetspro - **Frontend — admin-UI (`/admin/users/`):** Lista användare, skapa nya konton (namn, e-post, lösenord, roll), ändra roll, avaktivera konto - **Frontend — skyddade routes:** `/admin/*` kräver admin-roll; omdirigerar annars till startsidan -### 3. Matplan-vy (frontend-polish) -**Mål:** Ge användare tydlig feedback på lagerstatus och underlätta inköp. +### 3. Matplan-vy (frontend-polish) ✅ +**Klart.** -Backend-endpointen `GET /api/meal-plan/inventory-compare?from=...&to=...` finns och fungerar. Det som saknas är en tydlig frontend-vy: -- Visa inköpslistan med statusindikatorer: ✅ Finns hemma / ⚠️ Delvis / ❌ Saknas -- Aggregera `inventory-compare`-svaret per ingrediens över hela veckan -- Möjlig placering: ny flik i matplanen eller sidopanel i veckovy +Inköpslistan och inventariejämförelsen är sammanslagna till en enhetlig vy med tre statusnivåer: +- ❌ Saknas helt — visar hur mycket som behövs köpas +- ⚠️ Delvis hemma — visar hur mycket mer som behövs + vad som finns +- ✅ Finns hemma — markeras nedtonat, ingen köpindikering + +Listan sorteras automatiskt: saknade ingredienser överst, hemma-ingredienser underst. En sammanfattningsrad visar totalt antal per statuskategori. ### 4. Teknisk skuld (underhåll) **Mål:** Minska komplexitet och risk för buggar. diff --git a/frontend/app/matplan/MealPlanClient.tsx b/frontend/app/matplan/MealPlanClient.tsx index 82d7d7ce..ddbf407c 100644 --- a/frontend/app/matplan/MealPlanClient.tsx +++ b/frontend/app/matplan/MealPlanClient.tsx @@ -217,7 +217,7 @@ export default function MealPlanClient({ recipes }: { recipes: Recipe[] }) { })} - {/* Samlad ingredienslista */} + {/* Inköpslista med lagerstatus */}

Inköpslista ({plannedCount} {plannedCount === 1 ? 'recept' : 'recept'} planerade) @@ -226,80 +226,101 @@ export default function MealPlanClient({ recipes }: { recipes: Recipe[] }) {

Välj recept ovan för att se en samlad ingredienslista.

) : shopping.length === 0 ? (

Laddar ingredienser...

- ) : ( -
    - {shopping.map((item) => ( -
  • - - {item.quantity % 1 === 0 ? item.quantity : item.quantity.toFixed(1)} {item.unit} - - {item.name} -
  • - ))} -
- )} -

+ ) : (() => { + // Berika varje rad med inventariestatus + type DisplayStatus = 'enough' | 'partial' | 'missing'; + const enriched = shopping.map((item) => { + const cmp = inventoryCompare.find( + (c) => c.productId === item.productId && c.unit === item.unit, + ); + let displayStatus: DisplayStatus = 'missing'; + let buyQty = item.quantity; + if (cmp) { + if (cmp.available >= cmp.required) { + displayStatus = 'enough'; + buyQty = 0; + } else if (cmp.available > 0) { + displayStatus = 'partial'; + buyQty = cmp.missing; + } + } + return { ...item, cmp, displayStatus, buyQty }; + }); - {/* Inventariejämförelse */} - {plannedCount > 0 && inventoryCompare.length > 0 && ( -
-

Inventariegranskning

-

- Vad du har hemma vs. vad veckans recept kräver. -

- {(() => { - const missingCount = inventoryCompare.filter((i) => i.status === 'missing').length; - return missingCount === 0 ? ( -

- ✓ Du har allt hemma! -

- ) : ( -

- {missingCount} ingrediens{missingCount !== 1 ? 'er' : ''} saknas eller räcker inte -

+ const order: Record = { missing: 0, partial: 1, enough: 2 }; + enriched.sort((a, b) => order[a.displayStatus] - order[b.displayStatus] || a.name.localeCompare(b.name, 'sv')); + + const missingCount = enriched.filter((e) => e.displayStatus === 'missing').length; + const partialCount = enriched.filter((e) => e.displayStatus === 'partial').length; + const enoughCount = enriched.filter((e) => e.displayStatus === 'enough').length; + const hasCompare = inventoryCompare.length > 0; + + const fmtQty = (n: number) => (n % 1 === 0 ? String(n) : n.toFixed(1)); + + return ( + <> + {/* Sammanfattning */} + {hasCompare && ( +
+ {missingCount > 0 && ❌ {missingCount} saknas} + {partialCount > 0 && ⚠️ {partialCount} delvis hemma} + {enoughCount > 0 && ✅ {enoughCount} hemma} + {missingCount === 0 && partialCount === 0 && ( + ✅ Du har allt hemma! + )} +
+ )} + +
    + {enriched.map((item) => { + const isMissing = item.displayStatus === 'missing'; + const isPartial = item.displayStatus === 'partial'; + const isEnough = item.displayStatus === 'enough'; + const bg = isMissing ? '#ffeaea' : isPartial ? '#fff8e6' : '#ecf8ee'; + const icon = isMissing ? '❌' : isPartial ? '⚠️' : '✅'; + + return ( +
  • + {hasCompare && {icon}} + + {item.name} + {isPartial && item.cmp && ( + + ({fmtQty(item.cmp.available)} av {fmtQty(item.cmp.required)} {item.unit} hemma) + + )} + {isEnough && ( + + (finns hemma) + + )} + + + {isEnough + ? '—' + : `${fmtQty(item.buyQty)} ${item.unit}`} + +
  • + ); + })} +
+ ); - })()} -
    - {inventoryCompare.map((item) => ( -
  • - - {item.name} - {' '} - - {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 - - - {item.status === 'missing' && item.missing > 0 && ( - - Saknar {item.missing % 1 === 0 ? item.missing : item.missing.toFixed(1)} {item.unit} - - )} - {item.status === 'enough' && ( - - )} -
  • - ))} -
-
- )} + })() + } + )}