From a81bd6b460149779d25357ce13b0792b2cbbd8a6 Mon Sep 17 00:00:00 2001 From: Nils-Johan Gynther Date: Fri, 17 Apr 2026 22:39:57 +0200 Subject: [PATCH] docs: update README, NEXT_STEPS, and technical description for category feature and auth --- NEXT_STEPS.md | 98 +++++++--------------------- README.md | 4 +- TEKNISK_BESKRIVNING.md | 143 +++++++++++++++++++++++++++++++++-------- 3 files changed, 145 insertions(+), 100 deletions(-) diff --git a/NEXT_STEPS.md b/NEXT_STEPS.md index b800467c..af714ac2 100644 --- a/NEXT_STEPS.md +++ b/NEXT_STEPS.md @@ -15,23 +15,31 @@ | Kvittoimport (Mistral AI, OCR, alias) | ✅ Klart | | Matplanering (veckovy, inköpslista) | ✅ Klart | | Baslager (lista, lägg till, ta bort) | ✅ Klart | -| Admin: Produkter (edit, merge, duplicate, restore) | ✅ Klart | +| Admin: Produkter (edit, merge, duplicate, restore, reset) | ✅ Klart | | Receptredigering (frontend UX) | ✅ Klart | | Receptbilder (upload URL) | ✅ Klart | +| Autentisering (JWT, Auth.js v5, User-modell) | ✅ Klart | +| Användarprofil (firstName, lastName, email) | ✅ Klart | +| Produktkategorier — hierarkisk struktur (3 nivåer) | ✅ Klart | +| Taggning av produkter | ✅ Klart | +| Näringsvärden på produkter | ✅ Klart (schema + API) | +| Kategoritilldelning i admin-UI | ✅ Klart | | Portionsjustering | ❌ Saknas | -| Produktkategorier — fast lista | ❌ Saknas | -| Receptlista — filtrering & kortvy | ✅ Klart | | Matplan — inventariejämförelse | ❌ Saknas | -| Taggning av produkter | ⚠️ Delvis — kräver migration | -| Näringsvärden på produkter | ⚠️ Delvis — kräver migration | -| Autentisering (User-modell) | ❌ Saknas | -| Användarspecifika produkter (UserProduct) | ❌ Saknas — kräver auth | +| Seed produktdata med kategoritilldelning | ❌ Saknas (002-seed-products.sql.disabled) | +| Användarspecifika produkter (UserProduct) | ⚠️ Schema klart, UI basic | --- ## Prioriterade förbättringar -### 1. Portionsjustering av recept +### 1. Seed produktdata med kategoritilldelning +`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. +- Gå igenom de ~190 produkterna och tilldela rätt kategori-ID från tabellen `Category` +- Aktivera filen igen genom att ta bort `.disabled`-suffixet +- Alternativt: bygg ett admin-verktyg för bulk-kategorisering + +### 2. Portionsjustering av recept Recept lagras utan portionsangivelse. Lägg till ett `servings`-fält och låt användaren justera antal portioner i receptvyn — ingrediensmängderna räknas om proportionellt (t.ex. 4 → 6 pers: × 1,5). - **Databas:** `servings Int?` på `Recipe` i Prisma + migration - **Backend:** `servings` exponeras i `RecipeDto`, sätts vid create/update @@ -42,67 +50,10 @@ Recept lagras utan portionsangivelse. Lägg till ett `servings`-fält och låt a ### 3. Matplanering — jämförelse mot inventariet Veckovy och inköpslista fungerar. Nästa steg är att visa vilka ingredienser på inköpslistan som redan finns hemma och i vilken mängd — liknande receptvyns inventory-preview. Implementeras via `GET /api/recipes/:id/inventory-preview` per recept, aggregerat på veckonivå. -### 4. Produktkategorier — definiera en fast lista -Kategorier skrivs in som fritext i admin. Byt till en dropdown med fördefinierade kategorier (t.ex. "Mejeri, ost & ägg", "Kött, chark & fågel", "Frukt & Grönt") för konsistent data och bättre gruppering i baslagervyn. - -### 5. Utökad databas med taggning -Lägg till stöd för taggar, underkategorier och varumärke direkt på produkter. Möjliggör filtrering, sökning och rekommendationer baserade på taggar. - -**Schemaändringar (Prisma):** -- **`Product`** — lägg till `subcategory String?` och `brand String?` (behåll `canonicalName`) -- **`Tag`** — ny modell: `id`, `name @unique` -- **`ProductTag`** — ny relationstabell (many-to-many: `Product ↔ Tag`) - -**Implementeringssteg:** -1. Uppdatera `backend/prisma/schema.prisma` med nya modeller och relationer -2. Kör migration: `docker exec recipe-api npm exec prisma migrate dev --name add_tags_subcategory_brand` -3. Skapa seed-fil (`data/seed_tags.sql`) med taggar och kopplingar -4. Kör seed-filen mot databasen -5. Exponera `tags`, `subcategory`, `brand` i produkt-DTOs och `GET /api/products` (lägg till `?tag=` och `?subcategory=` som filterparametrar) -6. Admin: lägg till tagg-hantering och underkategori-fält -7. Baslager/produktlista: filtrera per tagg eller underkategori - -**Rekommenderade taggar:** `ekologisk`, `svensk`, `laktosfri`, `glutenfri`, `vegan`, `nötfri`, `säsong`, `rökt`, `premium`, `lamm`, `korv`, `färs`, m.fl. - -### 6. Näringsvärden på produkter -Lägg till en `Nutrition`-modell kopplad till `Product` (one-to-one) med näringsvärden per 100g: kalorier, protein, fett, kolhydrater, salt, socker, fiber. Kan implementeras oberoende av autentisering. - -**Schemaändring:** -```prisma -model Nutrition { - id Int @id @default(autoincrement()) - calories Float? - protein Float? - fat Float? - carbohydrates Float? - salt Float? - sugar Float? - fiber Float? - product Product @relation(fields: [productId], references: [id]) - productId Int @unique -} -``` -- **Backend:** CRUD via produktendpoints, exponeras i `ProductDto` -- **Frontend:** Visa näringsvärden i produktdetalj och eventuellt i receptvyn (summerat per portion) - -### 7. Autentisering — User-modell -Förutsättning för användarspecifika produkter (punkt 10). Idag saknar hela appen autentisering — alla kan CRUD allt. - -**Scope:** JWT-baserad auth med `User`-modell (id, name, email, passwordHash). Berör: -- Backend: AuthModule med NestJS Guards, JWT-strategi, skyddade routes -- Frontend: Inloggningsflöde, token-hantering i API-anrop -- Databas: `User`-tabell + migration - -> ⚠️ Detta är ett stort projekt i sig. Överväg om appen verkligen behöver fler användare eller om enkel HTTP Basic Auth räcker som skydd. - -### 8. Användarspecifika produkter (UserProduct) -Låter en användare spara egna produktvarianter med eget namn (t.ex. "Mormors Prästost") kopplade till en standardprodukt — eller fristående utan koppling. Kräver att punkt 9 (auth) är på plats. - -> ⚠️ **Överlapp med InventoryItem:** `InventoryItem` lagrar redan productId, quantity, unit, brand, bestBeforeDate och är i princip en "användarens produkt i lager". Klargör skillnaden: -> - `InventoryItem` = vad som finns hemma just nu (lager) -> - `UserProduct` = ett eget produktkort/favorit som kan återanvändas utan att vara lager -> -> Om distinktionen inte är tydlig, riskerar `UserProduct` att duplicera `InventoryItem`-logiken. +### 4. Bulk-kategorisering av produkter i admin +Admin-UI:t tillåter idag att sätta kategori per produkt. För att effektivt kategorisera hundratals produkter behövs: +- Filtervy för okategoriserade produkter +- Möjlighet att sätta kategori på flera produkter samtidigt (bulk-select) --- @@ -137,7 +88,8 @@ Frontend-server-actions saknar validering på inkommande fält (tom sträng, fö ## Produktdatabasen -193 svenska produkter är inseedad. Nästa naturliga steg: -- Lägg till fler saknade produkter som dyker upp vid receptimport -- Gå igenom produkter utan `canonicalName` i admin och fyll i dem -- Kontrollera att `category` är ifyllt för alla produkter (för bättre gruppering i baslager) +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 diff --git a/README.md b/README.md index b7dc0bee..921d4514 100644 --- a/README.md +++ b/README.md @@ -30,12 +30,14 @@ En fullstack-applikation för hantering av hemmavaror och recept. Håll koll på - **Lägg till och ta bort** — välj från produktlistan via dropdown, ta bort med ett klick ### Admin: Produkter -- **Redigera produkter** — uppdatera visningsnamn (name), canonical name och kategori inline direkt i listan +- **Redigera produkter** — uppdatera visningsnamn (name), canonical name, kategori (hierarkisk dropdown) och varumärke inline direkt i listan +- **Kategoritilldelning** — välj kategori ur ett 3-nivåträd (huvudkategori → underkategori → typ) som laddas dynamiskt från API:et - **Ta bort produkter** — soft-delete enskilda produkter - **Hitta dubbletter** — identifiera produkter med samma normaliserade namn - **Slå ihop produkter** — merge två produktposter, flytta alla inventarieföremål till målprodukten (källan soft-deleteras) - **Förhandsvisning** — granska vad som kommer att hända innan merge genomförs - **Återställ produkter** — restore tidigare raderade produkter +- **Återställ all produktdata** — rensningsknapp som raderar alla produkter, inventorie, taggar och kvitto-alias (behåller användare och kategorier) --- diff --git a/TEKNISK_BESKRIVNING.md b/TEKNISK_BESKRIVNING.md index b9f412bb..d8843e76 100644 --- a/TEKNISK_BESKRIVNING.md +++ b/TEKNISK_BESKRIVNING.md @@ -53,16 +53,22 @@ docker exec recipe-db mariadb -uroot -p"LÖSENORD" recipe_app -e "SHOW TABLES;" - **Framework:** Next.js 16.2 (App Router, server + client components) - **Språk:** TypeScript 5.4.5 - **UI:** React 19.2, ingen CSS-ramverk (ren CSS-in-JS och inline-stilar) +- **Autentisering:** Auth.js v5 (next-auth beta), JWT-session, `auth()` i server components - **Bygg:** Standalone output, körs i Docker-container -- **API-anrop:** Fetch mot backend och Next.js API routes +- **API-anrop:** `fetchJson` (server-side med auth-headers) + Next.js API route-proxies (client-side) - **Felhantering:** Global parseErrorResponse utility, svenska felmeddelanden +> **Viktigt:** `Navigation.tsx` är en async server component som anropar `auth()`. Den får aldrig importeras av client components — rendera den alltid i `page.tsx` (server component). + ### Frontend-sidor och komponenter | Sida | Fil | Funktionalitet | |------|-----|---| | **Hem** | `app/page.tsx` | Startsida | -| **Navigering** | `app/Navigation.tsx` | Huvudmeny | +| **Navigering** | `app/Navigation.tsx` | Huvudmeny, inloggad användare, länk till profil | +| **Inloggning** | `app/login/page.tsx` | Inloggningssida med Auth.js Credentials | +| **Profil** | `app/profil/page.tsx` | Redigera firstName, lastName, email | +| | `app/profil/ProfileClient.tsx` | Klientkomponent för profilformulär | | **Inventorie** | `app/inventory/page.tsx` | Lista, filtrera, sortera varor | | | `InventoryList.tsx` | Ritning av inventarieföremål | | | `InventoryForm.tsx` | Skapa nytt inventarieföremål | @@ -73,17 +79,21 @@ docker exec recipe-db mariadb -uroot -p"LÖSENORD" recipe_app -e "SHOW TABLES;" | | `actions.ts` | Server actions för inventarie | | **Recept** | `app/recipes/page.tsx` | Lista recept | | | `RecipePreview.tsx` | Receptförhandsvisning med inventariestatus | -| **Lägg till recept** | `app/recipes/create/page.tsx` | Meny för receptskaping (val mellan två vägar) | -| **Skriv in recept** | `app/recipes/write/page.tsx` | Startpunkt för Markdown-inmatning | -| | `app/recipes/write/WriteRecipePage.tsx` | Komponenter för receptskapande (Markdown-baserat, 3-steg) | +| **Lägg till recept** | `app/recipes/create/page.tsx` | Server component med Navigation | +| | `app/recipes/create/CreateRecipeClient.tsx` | Klientkomponent: snabbimport + metodval | +| **Skriv in recept** | `app/recipes/write/page.tsx` | Server component med Navigation | +| | `app/recipes/write/WriteRecipePage.tsx` | Markdown-baserat receptskapande (3-steg) | | **Importera från fil** | `app/recipes/import/page.tsx` | Startpunkt för fil/länk-import | -| | `app/recipes/import/ImportFilePage.tsx` | Komponenter för fil-/länk-import (PDF, URL, etc) | +| | `app/recipes/import/ImportFilePage.tsx` | Fil-/länk-import (PDF, URL, etc) | +| **Import (flikar)** | `app/import/page.tsx` | Server component med Navigation + flikvy | +| | `app/import/ImportTabsClient.tsx` | Klientkomponent: kvitto/recept-flikar | | **Recipe detail** | `app/recipes/[id]/` | Enskilt recept (detaljer, redigering) | | **Admin: Produkter** | `app/admin/products/page.tsx` | Produktadmin-panel | | | `AdminProductList.tsx` | Lista produkter, sök, sortera | -| | `EditProductForm.tsx` | Inline redigering av name, canonicalName, category + soft-delete | +| | `EditProductForm.tsx` | Inline redigering: name, canonicalName, kategori (hierarkisk dropdown), brand, taggar | +| | `ResetProductsButton.tsx` | Knapp för att rensa all produktdata | | | `MergePreviewForm.tsx` | Förhandsgranska merge | -| | `actions.ts` | Server actions: updateProduct, deleteProduct | +| | `actions.ts` | Server actions: updateProduct, deleteProduct, resetAllProducts | | **Baslager** | `app/baslager/page.tsx` | Visa och hantera baslager (server component) | | | `AddToPantryForm.tsx` | Lägg till produkt i baslager (dropdown) | | | `PantryList.tsx` | Visa baslager grupperat per kategori | @@ -91,15 +101,29 @@ docker exec recipe-db mariadb -uroot -p"LÖSENORD" recipe_app -e "SHOW TABLES;" ### API-proxy routes (Next.js) +Alla proxy-routes läser auth-token via `auth()` (Auth.js v5) och vidarebefordrar `Authorization: Bearer ` till backend. + | Route | Metod | Syfte | |-------|-------|-------| -| `/api/quick-import-proxy` | POST | Proxies `POST /api/quick-import` för URL-, PDF- och bildimport | -| `/api/parse-markdown-proxy` | POST | Proxies `POST /api/recipes/parse-markdown` (Markdown-tolkning för skriv-in-recept) | -| `/api/inventory-history-proxy` | GET | Proxies konsumtionshistorik | -| `/api/recipe-preview-proxy` | GET | Proxies receptförhandsvisning | -| `/api/admin/merge-preview-proxy` | GET | Proxies produktmerge-preview | -| `/api/products` | GET | Lista/proxies produkter | -| `/api/recipes` | GET, POST | Lista recept + spara nytt recept (proxy till backend) | +| `/api/auth/[...nextauth]` | GET, POST | Auth.js handlers (login, logout, session) | +| `/api/products` | GET | Produktlista (auth-wrappat med `auth(req)`) | +| `/api/categories` | GET | Kategorihierarki (publik, proxies `/api/categories/tree`) | +| `/api/profile` | GET, PATCH | Hämta/uppdatera användarprofil | +| `/api/recipes` | GET, POST | Lista recept + spara nytt | +| `/api/quick-import-proxy` | POST | URL-, PDF- och bildimport | +| `/api/parse-markdown-proxy` | POST | Markdown-tolkning för skriv-in-recept | +| `/api/inventory-history-proxy` | GET | Konsumtionshistorik | +| `/api/recipe-preview-proxy` | GET | Receptförhandsvisning | +| `/api/admin/merge-preview-proxy` | GET | Produktmerge-preview | +| `/api/receipt-import-proxy` | POST | Kvittoimport via Mistral AI | +| `/api/user-products` | GET, POST, DELETE | Användarspecifika produkter | + +### Autentisering (Auth.js v5) + +- `auth.ts` — NextAuth-konfiguration med Credentials provider +- `middleware.ts` — Skyddar alla routes utom `/login`, `/register` och `/api/auth` +- `lib/auth-headers.ts` — `getAuthHeaders()` hämtar Bearer-token från session (server-side) +- `lib/api.ts` — `fetchJson()` lägger automatiskt till auth-headers server-side, redirectar till `/login` vid 401 ### Frontend utbyggbarhet @@ -115,24 +139,41 @@ docker exec recipe-db mariadb -uroot -p"LÖSENORD" recipe_app -e "SHOW TABLES;" - **Språk:** TypeScript 5.4.5 - **Databas:** MariaDB 11 (via Prisma 6.12.0 ORM) - **API:** REST, validering med class-validator +- **Autentisering:** JWT (7 dagars token), JwtAuthGuard skyddar alla routes, `@Public()` dekorator för öppna endpoints - **Felhantering:** GlobalExceptionFilter (svenska felmeddelanden) - **Hälsokontroll:** /health endpoints - **Bygg:** `nest build`, körs i Docker-container -### Backend-moduler och strukturen läsa +### Backend-moduler och strukturen ``` backend/src/ ├── app.module.ts # Root module -├── main.ts # Startpunkt (port 8080) +├── main.ts # Startpunkt (port 8080, global prefix "api") +├── auth/ +│ ├── auth.controller.ts # POST /api/auth/login +│ ├── auth.service.ts # validateUser, login (JWT-signering) +│ ├── auth.module.ts +│ ├── jwt.strategy.ts # Passport JWT-strategi +│ ├── jwt-auth.guard.ts # Global guard (skyddar allt utom @Public) +│ └── decorators/ +│ └── public.decorator.ts # @Public() – markerar öppen endpoint +├── users/ +│ ├── users.controller.ts # GET/PATCH /api/users/me +│ ├── users.service.ts # findByUsername, create, updateProfile +│ └── users.module.ts +├── categories/ +│ ├── categories.controller.ts # GET /api/categories, GET /api/categories/tree (@Public) +│ ├── categories.service.ts # findAll (flat), findTree (hierarkisk) +│ └── categories.module.ts ├── common/ │ ├── filters/ │ │ └── global-exception.filter.ts # Centraliserad felhantering │ └── utils/ │ └── normalize-name.ts # Namnormalisering ├── health/ -│ ├── health.controller.ts # GET /health, /health/db -│ ├── health.service.ts # Hälsotillstånd-logik +│ ├── health.controller.ts # GET /health, /health/db (@Public) +│ ├── health.service.ts │ └── health.module.ts ├── inventory/ │ ├── inventory.controller.ts # CRUD endpoints @@ -146,8 +187,8 @@ backend/src/ │ ├── prisma.service.ts # PrismaClient wrapper │ └── prisma.module.ts ├── products/ -│ ├── products.controller.ts # CRUD, merge, duplicates -│ ├── products.service.ts # Produktlogik +│ ├── products.controller.ts # CRUD, merge, duplicates, reset-all +│ ├── products.service.ts # Produktlogik inkl. resetAll() │ ├── products.module.ts │ └── dto/ │ ├── create-product.dto.ts @@ -291,9 +332,23 @@ GET /api/products/merge-preview Förhandsgranska merge POST /api/products/merge Slå ihop två produkter PATCH /api/products/:id/canonical-name Uppdatera canonical name POST /api/products/backfill-canonical Backfill canonical names (admin) +POST /api/products/reset-all Rensa all produktdata (admin) ``` -### 🛀 Baslager-endpoints +### Kategori-endpoints +``` +GET /api/categories Flat lista av alla kategorier (@Public) +GET /api/categories/tree Hierarkiskt träd (@Public) +``` + +### Användar-endpoints +``` +POST /api/auth/login Logga in, returnerar JWT (@Public) +GET /api/users/me Hämta inloggad användares profil +PATCH /api/users/me Uppdatera firstName, lastName, email +``` + +### Baslager-endpoints ``` GET /api/pantry Lista alla baslagerartiklar (inkl. produktinfo) POST /api/pantry Lägg till produkt i baslagret @@ -304,6 +359,36 @@ DELETE /api/pantry/:id Ta bort produkt från baslagret ## Datamodell (Prisma ORM) +### User +```prisma +model User { + id Int @id @default(autoincrement()) + username String @unique + email String @unique + firstName String? + lastName String? + passwordHash String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} +``` + +### Category +```prisma +model Category { + id Int @id @default(autoincrement()) + name String + parentId Int? + parent Category? @relation("CategoryTree", ...) # Förälder (null = toppnivå) + children Category[] @relation("CategoryTree") # Underkategorier + products Product[] + + @@unique([name, parentId]) +} +``` +Hierarkin har 3 nivåer: **Huvudkategori → Underkategori → Typ** +Exempelträd: `Mejeri, ost & ägg → Mjölk → Laktosfri mjölk` + ### Product ```prisma model Product { @@ -311,14 +396,20 @@ model Product { name String # Visningsnamn normalizedName String @unique # Normaliserat namn (lowercase, utan skiljetecken) canonicalName String? # Canonical namn för receptmatchning - category String? # Produktkategori - isActive Boolean @default(true) # Soft-delete flag - deletedAt DateTime? # Tidpunkt för soft-delete + category String? # Fritext-kategori (äldre fält, ersätts av categoryRef) + subcategory String? # Fritext-underkategori (äldre fält) + brand String? # Varumärke + categoryId Int? # FK till Category (ny hierarki) + categoryRef Category? # Relation till Category + isActive Boolean @default(true) + deletedAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - inventoryItems InventoryItem[] + inventoryItems InventoryItem[] recipeIngredients RecipeIngredient[] + tags ProductTag[] + nutrition Nutrition? } ```