refactor(inventory): remove unused fields from InventoryItem and update related DTOs
This commit is contained in:
+60
-46
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Status — senast genomgånget: 2026-04-17
|
## Status — senast genomgånget: 2026-04-18
|
||||||
|
|
||||||
| Funktion | Status |
|
| Funktion | Status |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -15,7 +15,8 @@
|
|||||||
| Kvittoimport (Mistral AI, OCR, alias) | ✅ Klart |
|
| Kvittoimport (Mistral AI, OCR, alias) | ✅ Klart |
|
||||||
| Matplanering (veckovy, inköpslista) | ✅ Klart |
|
| Matplanering (veckovy, inköpslista) | ✅ Klart |
|
||||||
| Matplan — portionsjustering per dag | ✅ Klart |
|
| Matplan — portionsjustering per dag | ✅ Klart |
|
||||||
| Matplan — inventariejämförelse | ✅ Klart |
|
| Matplan — inventariejämförelse (backend) | ✅ Klart |
|
||||||
|
| Matplan — inventariejämförelse (frontend-vy) | ⚠️ Grundläggande, saknar ✅/⚠️/❌-status |
|
||||||
| Baslager (lista, lägg till, ta bort) | ✅ Klart |
|
| Baslager (lista, lägg till, ta bort) | ✅ Klart |
|
||||||
| Admin: Produkter (edit, merge, duplicate, restore, reset) | ✅ Klart |
|
| Admin: Produkter (edit, merge, duplicate, restore, reset) | ✅ Klart |
|
||||||
| Admin: Bulk-kategorisering | ✅ Klart |
|
| Admin: Bulk-kategorisering | ✅ Klart |
|
||||||
@@ -24,74 +25,87 @@
|
|||||||
| Autentisering (JWT, Auth.js v5, User-modell) | ✅ Klart |
|
| Autentisering (JWT, Auth.js v5, User-modell) | ✅ Klart |
|
||||||
| Användarprofil (firstName, lastName, email) | ✅ Klart |
|
| Användarprofil (firstName, lastName, email) | ✅ Klart |
|
||||||
| Produktkategorier — hierarkisk struktur (3 nivåer) | ✅ Klart |
|
| Produktkategorier — hierarkisk struktur (3 nivåer) | ✅ Klart |
|
||||||
|
| Kategori-seed (supplement, idempotent) | ✅ Klart |
|
||||||
|
| Kategoritilldelning i admin-UI | ✅ Klart |
|
||||||
| Taggning av produkter | ✅ Klart |
|
| Taggning av produkter | ✅ Klart |
|
||||||
| Näringsvärden på produkter | ✅ Klart (schema + API) |
|
| Näringsvärden på produkter | ✅ Klart (schema + API) |
|
||||||
| Kategoritilldelning i admin-UI | ✅ Klart |
|
| Seed produktdata med kategoritilldelning | ⚠️ Script klart, ej aktiverat i init |
|
||||||
| Kategori-seed (supplement, idempotent) | ✅ Klart |
|
|
||||||
| Seed produktdata med kategoritilldelning | ❌ Saknas (002-seed-products.sql.disabled) |
|
|
||||||
| Användarspecifika produkter (UserProduct) | ⚠️ Schema klart, UI basic |
|
| Användarspecifika produkter (UserProduct) | ⚠️ Schema klart, UI basic |
|
||||||
| Användarroller (user / admin) | ❌ Saknas |
|
| Användarroller (user / admin) | ❌ Saknas |
|
||||||
| Användarhantering i admin-UI | ❌ Saknas |
|
| Användarhantering i admin-UI | ❌ Saknas |
|
||||||
|
| Teknisk skuld — oanvända InventoryItem-fält | ✅ Klart (migration 20260418) |
|
||||||
|
| Teknisk skuld — redirect-routes städade | ✅ Klart |
|
||||||
|
| Avancerad AI-integration (veckoplanering, kampanjdata) | ❌ Planerad |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Prioriterade förbättringar
|
## Prioriterade förbättringar
|
||||||
|
|
||||||
### 1. Seed produktdata med kategoritilldelning
|
### 1. Seed-data (002-seed-products.sql)
|
||||||
`db/init/002-seed-products.sql` är inaktiverad (`.disabled`) tills den uppdateras med rätt `categoryId` för varje produkt. Utan detta är produktdatabasen tom vid fresh install.
|
**Mål:** Möjliggöra demo och ny server utan manuell datainmatning.
|
||||||
- Gå igenom de ~190 produkterna och tilldela rätt kategori-ID från tabellen `Category` (nu inklusive supplement-kategorier)
|
|
||||||
- Aktivera filen igen genom att ta bort `.disabled`-suffixet
|
|
||||||
- Kontrollera att varje produkts `categoryId` matchar mot det ID som genereras i databasen (auto-increment — kör ett SELECT för att verifiera)
|
|
||||||
|
|
||||||
### 2. Matplan — djupare inventariejämförelse i frontend
|
`db/init/002-seed-products.sql` är inaktiverad (`.disabled`) tills produkterna har rätt `categoryId`. Skript för kategorimappning finns i `db/seeds/`:
|
||||||
Backend-endpointen `GET /api/meal-plan/inventory-compare?from=...&to=...` returnerar ingrediensstatus per dag. Funktionen saknar dock en frontend-vy som tydligt visar "vad behöver jag handla — och vad har jag redan hemma?" aggregerat för hela veckan.
|
- Kör `db/seeds/categories_supplement.sql` → lägger till saknade kategorier
|
||||||
- Visa inköpslistan med tydliga statusindikatorer: ✅ Finns hemma / ⚠️ Delvis / ❌ Saknas
|
- Kör `db/seeds/seed_product_categories.sql` → kopplar ~190 produkter till rätt kategori
|
||||||
- Möjlig placering: ny flik i matplanen eller sidopanel i veckovy
|
- Verifiera att mappningarna ser rätt ut i admin-UI (filtrera på okategoriserade)
|
||||||
- Kräver: aggregering av `inventory-compare`-svaret per ingrediens över hela veckan
|
- Generera en ny `002-seed-products.sql` med korrekt `categoryId` per rad (via `SELECT` mot live-db)
|
||||||
|
- Ta bort `.disabled`-suffixet och testa fresh install
|
||||||
|
|
||||||
### 3. Användarroller och användarhantering i admin
|
### 2. Användarroller
|
||||||
Idag har alla inloggade användare samma behörighetsnivå. Behövs:
|
**Mål:** Säkerställa att endast behöriga användare har admin-rättigheter.
|
||||||
|
|
||||||
|
Idag har alla inloggade användare samma behörighetsnivå — ett säkerhetsproblem inför lansering.
|
||||||
- **Databas:** Lägg till `role` (enum `user` | `admin`) på `User`-modellen i Prisma + migration
|
- **Databas:** Lägg till `role` (enum `user` | `admin`) på `User`-modellen i Prisma + migration
|
||||||
- **Backend:** Rollbaserad guard (`@Roles('admin')`) — skyddar admin-endpoints; vanliga användare nekas med 403
|
- **Backend:** Rollbaserad guard (`@Roles('admin')`) — skyddar admin-endpoints; vanliga användare nekas med 403
|
||||||
- **Auth:** Rollen inkluderas i JWT-tokenen och i Auth.js session-objektet
|
- **Auth:** Rollen inkluderas i JWT-tokenen och i Auth.js session-objektet
|
||||||
- **Frontend — admin-UI (`/admin/users/`):** Lista användare, skapa nya konton (namn, e-post, lösenord, roll), ändra roll, avaktivera konto
|
- **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
|
- **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.
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
### 4. Teknisk skuld (underhåll)
|
||||||
|
**Mål:** Minska komplexitet och risk för buggar.
|
||||||
|
|
||||||
|
#### A. CanonicalNameForm och NameForm ✅
|
||||||
|
Filerna var redan borttagna — inga aktiva imports hittades. Inget att göra.
|
||||||
|
|
||||||
|
#### B. Oanvända fält på InventoryItem ✅
|
||||||
|
Följande 6 fält togs bort via Prisma-migration (`20260418000000_remove_unused_inventory_fields`):
|
||||||
|
`priority`, `shelfNote`, `isOnSale`, `priceLevel`, `proteinType`, `isLeftover`
|
||||||
|
- Schema, DTOs (create + update), service och frontend-typen är städade.
|
||||||
|
- `opened` och `suitableFor` behölls — de används i UI.
|
||||||
|
|
||||||
|
#### C. Validering av DTO:er i admin-actions ✅
|
||||||
|
Redan implementerat — `trim()` + max 100 tecken på alla fält i `actions.ts`. Inget att göra.
|
||||||
|
|
||||||
|
#### D. Routing-städning för kvitto och import ✅
|
||||||
|
`frontend/app/kvitto/page.tsx` och `frontend/app/recipes/import/page.tsx` är borttagna.
|
||||||
|
`/import` täcker båda use-cases via flikar.
|
||||||
|
|
||||||
|
#### E. Seed-data i versionshantering ✅
|
||||||
|
`data/matvaror_sverige.csv` och `data/seed_products.sql` behålls i git för reproducerbarhet. Inga ändringar i `.gitignore`.
|
||||||
|
|
||||||
|
### 5. Avancerad AI-integration
|
||||||
|
**Mål:** Smarta receptförslag och veckoplanering baserat på inventarie och kampanjdata.
|
||||||
|
|
||||||
|
Nuvarande AI-funktionalitet (Mistral för kvittotolkning) är ett bra fundament. Nästa steg:
|
||||||
|
- Receptförslag utifrån vad som finns hemma ("Vad ska jag laga idag?")
|
||||||
|
- Veckoplanering med hänsyn till kampanjpriser (kräver extern datakälla)
|
||||||
|
- Kräver: tydlig API-design, kostnadskontroll och eventuellt modellval per use-case
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Teknisk skuld och städning
|
## Enhetstester
|
||||||
|
|
||||||
### A. CanonicalNameForm och NameForm — ta bort gamla filer
|
|
||||||
`frontend/app/admin/products/NameForm.tsx` och `CanonicalNameForm.tsx` ersattes av `EditProductForm.tsx`. Kontrollera om de gamla filerna fortfarande importeras och radera dem om inte.
|
|
||||||
|
|
||||||
### B. Oanvända fält på InventoryItem
|
|
||||||
Följande fält finns i Prisma-schemat men används varken i backend-endpoints eller frontend-UI:
|
|
||||||
`priority`, `opened`, `shelfNote`, `suitableFor`, `isOnSale`, `priceLevel`, `proteinType`, `isLeftover`
|
|
||||||
Besluta: implementera stöd för dem eller ta bort via migration.
|
|
||||||
|
|
||||||
### C. Seed-data i versionshantering
|
|
||||||
`data/matvaror_sverige.csv` och `data/seed_products.sql` ligger lokalt men är inte committade. Bestäm om de ska in i repot (för reproducerbarhet) eller hållas utanför.
|
|
||||||
|
|
||||||
### D. Enhetstester ✅
|
|
||||||
Jest + ts-jest är uppsatt. Tester finns för:
|
Jest + ts-jest är uppsatt. Tester finns för:
|
||||||
- `normalize-name.ts` — 10 tester
|
- `normalize-name.ts` — 10 tester
|
||||||
- `base.parser.ts` (`parseIngredientLine`) — 12 tester
|
- `base.parser.ts` (`parseIngredientLine`) — 12 tester
|
||||||
- `recipes.service.ts` (`normalizeUnit`, `convertUnit`) — 17 tester
|
- `recipes.service.ts` (`normalizeUnit`, `convertUnit`) — 17 tester
|
||||||
|
|
||||||
Kör med `npm test` i `backend/`.
|
Kör med `npm test` i `backend/`.
|
||||||
|
|
||||||
### E. Validering av DTO:er i admin-actions
|
|
||||||
Frontend-server-actions saknar validering på inkommande fält (tom sträng, för lång sträng, osv.). Lägg till enkel `trim()` + max-längd-kontroll i `frontend/app/admin/products/actions.ts`.
|
|
||||||
|
|
||||||
### F. Routing-städning för kvitto och import
|
|
||||||
`/kvitto/page.tsx` redirectar till `/import?tab=kvitto`. `/recipes/import/page.tsx` redirectar till `/import?tab=recept`. Om dessa routes inte exponeras i navigeringen kan de tas bort; om de behövs som deep links är redirectarna ok men bör dokumenteras.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Produktdatabasen
|
|
||||||
|
|
||||||
Produktdatabasen är just nu tom — seedfilen `db/init/002-seed-products.sql.disabled` innehåller ~190 svenska baslivsmedel men är inaktiverad tills produkterna har tilldelats rätt `categoryId`. Nästa naturliga steg:
|
|
||||||
- Gå igenom produkterna och tilldela kategorier via admin-UI eller uppdatera seed-filen direkt
|
|
||||||
- Aktivera seed-filen igen (`002-seed-products.sql`) för reproducerbarhet vid fresh install
|
|
||||||
- Lägg till fler produkter som dyker upp vid receptimport
|
|
||||||
- Kontrollera att `canonicalName` är ifyllt för alla produkter
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
-- Remove unused fields from InventoryItem
|
||||||
|
ALTER TABLE `InventoryItem`
|
||||||
|
DROP COLUMN `priority`,
|
||||||
|
DROP COLUMN `shelfNote`,
|
||||||
|
DROP COLUMN `isOnSale`,
|
||||||
|
DROP COLUMN `priceLevel`,
|
||||||
|
DROP COLUMN `proteinType`,
|
||||||
|
DROP COLUMN `isLeftover`;
|
||||||
@@ -88,16 +88,10 @@ model InventoryItem {
|
|||||||
brand String?
|
brand String?
|
||||||
receiptName String?
|
receiptName String?
|
||||||
location String?
|
location String?
|
||||||
priority Int?
|
|
||||||
purchaseDate DateTime?
|
purchaseDate DateTime?
|
||||||
opened Boolean?
|
opened Boolean?
|
||||||
shelfNote String?
|
|
||||||
suitableFor String?
|
suitableFor String?
|
||||||
isOnSale Boolean?
|
|
||||||
priceLevel Int?
|
|
||||||
bestBeforeDate DateTime?
|
bestBeforeDate DateTime?
|
||||||
proteinType String?
|
|
||||||
isLeftover Boolean?
|
|
||||||
comment String?
|
comment String?
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
IsBoolean,
|
IsBoolean,
|
||||||
IsInt,
|
|
||||||
IsNumber,
|
IsNumber,
|
||||||
IsOptional,
|
IsOptional,
|
||||||
IsString,
|
IsString,
|
||||||
@@ -22,10 +21,6 @@ export class CreateInventoryDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
location?: string;
|
location?: string;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsInt()
|
|
||||||
priority?: number;
|
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
purchaseDate?: string;
|
purchaseDate?: string;
|
||||||
|
|
||||||
@@ -44,30 +39,10 @@ export class CreateInventoryDto {
|
|||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
opened?: boolean;
|
opened?: boolean;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
shelfNote?: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
suitableFor?: string;
|
suitableFor?: string;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsBoolean()
|
|
||||||
isOnSale?: boolean;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsInt()
|
|
||||||
priceLevel?: number;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
proteinType?: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsBoolean()
|
|
||||||
isLeftover?: boolean;
|
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
comment?: string;
|
comment?: string;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
IsBoolean,
|
IsBoolean,
|
||||||
IsInt,
|
|
||||||
IsNumber,
|
IsNumber,
|
||||||
IsOptional,
|
IsOptional,
|
||||||
IsString,
|
IsString,
|
||||||
@@ -25,10 +24,6 @@ export class UpdateInventoryDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
location?: string;
|
location?: string;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsInt()
|
|
||||||
priority?: number;
|
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
purchaseDate?: string;
|
purchaseDate?: string;
|
||||||
|
|
||||||
@@ -47,30 +42,10 @@ export class UpdateInventoryDto {
|
|||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
opened?: boolean;
|
opened?: boolean;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
shelfNote?: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
suitableFor?: string;
|
suitableFor?: string;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsBoolean()
|
|
||||||
isOnSale?: boolean;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsInt()
|
|
||||||
priceLevel?: number;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
proteinType?: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsBoolean()
|
|
||||||
isLeftover?: boolean;
|
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
comment?: string;
|
comment?: string;
|
||||||
|
|||||||
@@ -206,10 +206,6 @@ export class InventoryService {
|
|||||||
updateData.receiptName = data.receiptName.trim();
|
updateData.receiptName = data.receiptName.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof data.priority === 'number') {
|
|
||||||
updateData.priority = data.priority;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof data.purchaseDate === 'string') {
|
if (typeof data.purchaseDate === 'string') {
|
||||||
updateData.purchaseDate = data.purchaseDate
|
updateData.purchaseDate = data.purchaseDate
|
||||||
? new Date(data.purchaseDate)
|
? new Date(data.purchaseDate)
|
||||||
@@ -226,30 +222,10 @@ export class InventoryService {
|
|||||||
updateData.opened = data.opened;
|
updateData.opened = data.opened;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof data.shelfNote === 'string') {
|
|
||||||
updateData.shelfNote = data.shelfNote.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof data.suitableFor === 'string') {
|
if (typeof data.suitableFor === 'string') {
|
||||||
updateData.suitableFor = data.suitableFor.trim();
|
updateData.suitableFor = data.suitableFor.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof data.isOnSale === 'boolean') {
|
|
||||||
updateData.isOnSale = data.isOnSale;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof data.priceLevel === 'number') {
|
|
||||||
updateData.priceLevel = data.priceLevel;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof data.proteinType === 'string') {
|
|
||||||
updateData.proteinType = data.proteinType.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof data.isLeftover === 'boolean') {
|
|
||||||
updateData.isLeftover = data.isLeftover;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof data.comment === 'string') {
|
if (typeof data.comment === 'string') {
|
||||||
updateData.comment = data.comment.trim();
|
updateData.comment = data.comment.trim();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
import { redirect } from 'next/navigation';
|
|
||||||
|
|
||||||
export default function KvittoPage() {
|
|
||||||
redirect('/import?tab=kvitto');
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { redirect } from 'next/navigation';
|
|
||||||
|
|
||||||
export default function Page() {
|
|
||||||
redirect('/import?tab=recept');
|
|
||||||
}
|
|
||||||
@@ -52,17 +52,11 @@ export type InventoryItem = {
|
|||||||
quantity: string;
|
quantity: string;
|
||||||
unit: string;
|
unit: string;
|
||||||
location: string | null;
|
location: string | null;
|
||||||
priority: number | null;
|
|
||||||
purchaseDate: string | null;
|
purchaseDate: string | null;
|
||||||
opened: boolean | null;
|
opened: boolean | null;
|
||||||
shelfNote: string | null;
|
|
||||||
suitableFor: string | null;
|
suitableFor: string | null;
|
||||||
isOnSale: boolean | null;
|
|
||||||
priceLevel: number | null;
|
|
||||||
bestBeforeDate: string | null;
|
bestBeforeDate: string | null;
|
||||||
brand: string | null;
|
brand: string | null;
|
||||||
proteinType: string | null;
|
|
||||||
isLeftover: boolean | null;
|
|
||||||
comment: string | null;
|
comment: string | null;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user