Enhance README and TEKNISK_BESKRIVNING with detailed features, API endpoints, and Markdown import process for Recipe App
This commit is contained in:
@@ -8,11 +8,26 @@ En fullstack-applikation för hantering av hemmavaror och recept. Håll koll på
|
|||||||
|
|
||||||
## Funktioner
|
## Funktioner
|
||||||
|
|
||||||
- **Hemmavaror** — lägg till, redigera och konsumera varor. Filtrera på plats och bäst före-datum.
|
### Inventorie (Hemmavaror)
|
||||||
- **Recept** — skapa och redigera recept med ingredienser och tillagningsinstruktioner (Markdown-stöd).
|
- **Lägg till, redigera och ta bort varor** — hantera produkt, kvantitet, enhet, plats, märke och bäst före-datum
|
||||||
- **Receptjämförelse** — se direkt vilka ingredienser du har hemma och vad som saknas.
|
- **Filtrera och sortera** — efter plats (kyl, frys, skafferi), bäst före-datum, och namn (A–Ö)
|
||||||
- **Importera recept från Markdown** — klistra in ett recept i ett enkelt textformat, granska matchade produkter och spara med ett klick.
|
- **Konsumera varor** — registrera förbrukad mängd med eventuell kommentar
|
||||||
- **Admin: Produkter** — hantera produktnamn och slå ihop dubbletter.
|
- **Konsumtionshistorik** — spåra vad som använts när och i vilken mängd
|
||||||
|
- **Utförlig information** — stöd för varumärke, lagringsnot, tillkomsttid, mera detaljer
|
||||||
|
|
||||||
|
### Recept
|
||||||
|
- **Skapa och redigera recept** — med ingredienser, kvantiteter, enheter och instruktioner (Markdown-stöd)
|
||||||
|
- **Receptjämförelse mot inventorie** — se direkt vilka ingredienser du har hemma, vad som saknas och enhetskonflikt
|
||||||
|
- **Importera recept från Markdown** — klistra in ett recept i enkelt format, låt systemet matcha ingredienser, granska och spara med ett klick
|
||||||
|
- **Intelligenta matchningar** — Levenshtein-baserad likhetsbedömning hittar rätt produkt även på osäker stavning
|
||||||
|
- **Enhetskonvertering** — automatisk konvertering mellan viktenheter (g/kg), volymenheter (ml/dl) och portionsenheter (tsk/msk)
|
||||||
|
|
||||||
|
### Admin: Produkter
|
||||||
|
- **Hantera produktnamn** — uppdatera canonical name för varje produkt (användes vid receptmatchning)
|
||||||
|
- **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
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -21,28 +36,54 @@ En fullstack-applikation för hantering av hemmavaror och recept. Håll koll på
|
|||||||
### Förutsättningar
|
### Förutsättningar
|
||||||
|
|
||||||
- Docker och Docker Compose
|
- Docker och Docker Compose
|
||||||
- En `proxy`-nätverk i Docker (extern, hanteras av Caddy eller liknande)
|
- En extern `proxy`-nätverk i Docker för Caddy (rekommenderat) eller localhost-konfiguration
|
||||||
|
|
||||||
### Starta applikationen
|
### Starta applikationen
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Bygg och starta alla tjänster
|
# Bygg alla images (första gånger)
|
||||||
docker compose build
|
docker compose build
|
||||||
|
|
||||||
|
# Starta alla tjänster i bakgrunden
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
### Bygg om enbart backend (t.ex. efter kodändringar)
|
Frontend är tillgänglig på `http://localhost:3000` (eller via Caddy proxy)
|
||||||
|
Backend API är tillgänglig på `http://localhost:8080` (eller via Caddy proxy)
|
||||||
|
|
||||||
|
### Bygg bara backend eller frontend om behövligt
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Bygg enbart backend (t.ex. efter kodändringar)
|
||||||
docker compose build recipe-api
|
docker compose build recipe-api
|
||||||
|
|
||||||
|
# Starta bara backend (övriga tjänster fortsätter)
|
||||||
docker compose up -d recipe-api
|
docker compose up -d recipe-api
|
||||||
|
|
||||||
|
# Liknande för frontend
|
||||||
|
docker compose build recipe-frontend
|
||||||
|
docker compose up -d recipe-frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
### Kontrollera hälsa
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Hälsokontroll via HTTP (backend måste köra)
|
||||||
|
curl http://localhost:8080/health
|
||||||
|
|
||||||
|
# Databasspecifik hälsokontroll
|
||||||
|
curl http://localhost:8080/health/db
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Importera recept från Markdown
|
## Importera recept från Markdown
|
||||||
|
|
||||||
Gå till **Recept → Lägg till nytt recept → Importera från Markdown** och klistra in ett recept i följande format:
|
### Steg 1: Gå till receptsidan
|
||||||
|
Navigera till **Recept** och välj **Lägg till nytt recept → Importera från Markdown**
|
||||||
|
|
||||||
|
### Steg 2: Klistra in receptet
|
||||||
|
Använd följande format:
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
# Köttfärssås
|
# Köttfärssås
|
||||||
@@ -54,12 +95,54 @@ En klassisk köttfärssås med massa smak.
|
|||||||
- 1 st lök
|
- 1 st lök
|
||||||
- 2 msk tomatpuré
|
- 2 msk tomatpuré
|
||||||
- 1 dl grädde (vispgrädde)
|
- 1 dl grädde (vispgrädde)
|
||||||
|
- salt och peppar
|
||||||
|
|
||||||
## Tillvägagångssätt
|
## Tillvägagångssätt
|
||||||
Hacka löken och stek den mjuk i lite olja. Tillsätt köttfärsen...
|
Hacka löken och stek den mjuk i lite olja. Tillsätt köttfärsen och bräsera tills den är genomstekt. Tillsätt tomatpuré och låt det småkoka ett par minuter innan du tillsätter grädde. Smaka av med salt och peppar.
|
||||||
```
|
```
|
||||||
|
|
||||||
Systemet tolkar texten, föreslår matchande produkter från databasen och låter dig granska och justera innan receptet sparas.
|
### Steg 3: Granska
|
||||||
|
Systemet:
|
||||||
|
- Tolkar receptnamn, beskrivning och instruktioner
|
||||||
|
- Försöker matcha varje ingrediens mot databasen (Levenshtein-likhet)
|
||||||
|
- Visar förslag för varje ingrediens i prioriteringsordning
|
||||||
|
|
||||||
|
Du kan:
|
||||||
|
- Redigera nombres, beskrivning och instruktioner
|
||||||
|
- Välj rätt produkt från förslagen för varje ingrediens
|
||||||
|
- Ta bort ingredienser som inte behövs
|
||||||
|
- Ändra kvantiteter och enheter
|
||||||
|
|
||||||
|
### Steg 4: Spara
|
||||||
|
Klicka "Spara recept" — basrecepet sparas med dina valida ingredienser
|
||||||
|
|
||||||
|
### Receptformat — regler
|
||||||
|
|
||||||
|
| Sektion | Beskrivning |
|
||||||
|
|---------|------------|
|
||||||
|
| **H1 (# titel)** | Receptnamn |
|
||||||
|
| **Text efter H1, före ## Ingredienser** | Receptbeskrivning (valfritt) |
|
||||||
|
| **## Ingredienser** | Rubrik för ingredienslistan |
|
||||||
|
| **Ingrediensrader** | Mönster: `- ANTAL ENHET NAMN` eller `- ANTAL NAMN` (standard: st) |
|
||||||
|
| **Parentes i ingrediens** | Text i `(parentes)` sparas som ingrediensnot, t.ex. `(vispgrädde)`, `(eller crème fraiche)` |
|
||||||
|
| **## Tillvägagångssätt** (eller `## Instruktioner`) | Rubrik för tillagningsinstruktioner |
|
||||||
|
| **Text under instruktioner** | Instruktionstexten (kan fortsätta över flera rader) |
|
||||||
|
|
||||||
|
### Matchningsalgoritm
|
||||||
|
|
||||||
|
Systemet använder tre metoder för att hitta rätt produkt:
|
||||||
|
|
||||||
|
1. **Exakt match** (100 poäng)
|
||||||
|
- Ingrediensnamn matchar exakt efter normalisering (lowercase, utan skiljetecken)
|
||||||
|
|
||||||
|
2. **Delsträng-match** (70 poäng)
|
||||||
|
- Ingrediensnamn förekommer som del av produktnamnet eller vice versa
|
||||||
|
|
||||||
|
3. **Levenshtein-likhet** (40–100 poäng)
|
||||||
|
- Likhetspoäng baserat på tecknenskillnad
|
||||||
|
- Mindre än 40 poäng filtreras bort
|
||||||
|
|
||||||
|
Systemet visar upp till 5 bästa förslag per ingrediens.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+539
-220
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
## Översikt
|
## Översikt
|
||||||
|
|
||||||
Recipe App är en fullstack-applikation för hantering av hemmavaror, recept och inköpsplanering. Systemet är byggt med Next.js (frontend), NestJS (backend), Prisma ORM och MariaDB. Applikationen är containeriserad med Docker och använder Caddy som reverse proxy.
|
Recipe App är en fullstack-applikation för hantering av hemmavaror, recept och matplanering. Systemet är byggt med Next.js (frontend), NestJS (backend), Prisma ORM och MariaDB. Applikationen är containeriserad med Docker och använder Caddy som reverse proxy.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -15,14 +15,15 @@ Recipe App är en fullstack-applikation för hantering av hemmavaror, recept och
|
|||||||
| Frontend | Next.js | 16.2 |
|
| Frontend | Next.js | 16.2 |
|
||||||
| | React | 19.2 |
|
| | React | 19.2 |
|
||||||
| | TypeScript | 5.4.5 |
|
| | TypeScript | 5.4.5 |
|
||||||
| | Node | 22.x (via @types/node 22.15.29) |
|
| | Node | 22.x (@types/node 22.15.29) |
|
||||||
| Backend | NestJS | 10.3 |
|
| Backend | NestJS | 10.3 |
|
||||||
| | Prisma | 6.12.0 |
|
| | Prisma | 6.12.0 |
|
||||||
| | TypeScript | 5.4.5 |
|
| | TypeScript | 5.4.5 |
|
||||||
| | Node | 22.x (via @types/node 22.15.29) |
|
| | Node | 22.x (@types/node 22.15.29) |
|
||||||
| Databas | MariaDB | 11 |
|
| Databas | MariaDB | 11 |
|
||||||
| Proxy | Caddy | 2.x |
|
| Proxy | Caddy | 2.x |
|
||||||
| Container | Docker | 24+ |
|
| Container | Docker | 24+ |
|
||||||
|
| Converter | Node.js (TypeScript) | Noll externa beroenden |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -35,126 +36,312 @@ Recipe App är en fullstack-applikation för hantering av hemmavaror, recept och
|
|||||||
- **API-anrop:** Fetch mot backend och Next.js API routes
|
- **API-anrop:** Fetch mot backend och Next.js API routes
|
||||||
- **Felhantering:** Global parseErrorResponse utility, svenska felmeddelanden
|
- **Felhantering:** Global parseErrorResponse utility, svenska felmeddelanden
|
||||||
|
|
||||||
### Funktioner (frontend)
|
### Frontend-sidor och komponenter
|
||||||
|
|
||||||
- **Inventarielista:**
|
| Sida | Fil | Funktionalitet |
|
||||||
- Sök, filtrera och sortera hemmavaror (namn, plats, bäst före, A–Ö)
|
|------|-----|---|
|
||||||
- Lägg till, redigera, konsumera och ta bort varor
|
| **Hem** | `app/page.tsx` | Startsida |
|
||||||
- Konsumtionshistorik med enheter
|
| **Navigering** | `app/Navigation.tsx` | Huvudmeny |
|
||||||
- **Recept:**
|
| **Inventorie** | `app/inventory/page.tsx` | Lista, filtrera, sortera varor |
|
||||||
- Lista, skapa, redigera och ta bort recept
|
| | `InventoryList.tsx` | Ritning av inventarieföremål |
|
||||||
- Jämför recept mot hemmavaror (räcker/saknas/enhetskonflikt)
|
| | `InventoryForm.tsx` | Skapa nytt inventarieföremål |
|
||||||
- Visar instruktioner och saknade ingredienser för valt recept
|
| | `InventoryEditForm.tsx` | Redigera inventarieföremål |
|
||||||
- Sidebar med snabblista över recept
|
| | `InventoryConsumeForm.tsx` | Konsumera (brukat) inventarieföremål |
|
||||||
- **Admin: Produkter:**
|
| | `InventoryConsumptionHistory.tsx` | Visa konsumtionshistorik |
|
||||||
- Sök och sortera produkter (A–Ö, senast tillagda)
|
| | `ProductForm.tsx` | Välja produkt för inventarieföremål |
|
||||||
- Redigera canonical name
|
| | `actions.ts` | Server actions för inventarie |
|
||||||
- Merge preview för produktnamn
|
| **Recept** | `app/recipes/page.tsx` | Lista recept |
|
||||||
- **Felhantering:**
|
| | `RecipePreview.tsx` | Receptförhandsvisning med inventariestatus |
|
||||||
- Svenska felmeddelanden, tydliga varningar vid valideringsfel
|
| **Skapa recept** | `app/recipes/create/page.tsx` | Receptkreation (manual form) |
|
||||||
- **Responsiv design:**
|
| | `app/recipes/create/CreateRecipePage.tsx` | Komponenter för receptskapande |
|
||||||
- Fungerar på mobil, surfplatta och desktop
|
| **Importera recept** | `app/recipes/import/page.tsx` | Startpunkt för Markdown-import |
|
||||||
|
| | `app/recipes/import/ImportRecipePage.tsx` | 3-stegsvyn för Markdown-import |
|
||||||
|
| **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 |
|
||||||
|
| | `NameForm.tsx` | Redigera produktnamn |
|
||||||
|
| | `CanonicalNameForm.tsx` | Redigera canonical name |
|
||||||
|
| | `MergePreviewForm.tsx` | Förhandsgranska merge |
|
||||||
|
|
||||||
|
### API-proxy routes (Next.js)
|
||||||
|
|
||||||
|
| Route | Metod | Syfte |
|
||||||
|
|-------|-------|-------|
|
||||||
|
| `/api/parse-markdown-proxy` | POST | Proxies `POST /api/recipes/parse-markdown` (Markdown-tolkning) |
|
||||||
|
| `/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 |
|
||||||
|
|
||||||
|
### Frontend utbyggbarhet
|
||||||
|
|
||||||
|
- Svenska felmeddelanden via `lib/error-handler.ts` (`parseErrorResponse`)
|
||||||
|
- Centraliserad API-access via `lib/api.ts` (`fetchJson`)
|
||||||
|
- Typade inventory/recipe data i `features/inventory/types.ts`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Backend
|
## Backend (NestJS)
|
||||||
|
|
||||||
- **Framework:** NestJS 10.3
|
- **Framework:** NestJS 10.3
|
||||||
- **Språk:** TypeScript 5.4.5
|
- **Språk:** TypeScript 5.4.5
|
||||||
- **Databas:** MariaDB 11 (via Prisma 6.12.0)
|
- **Databas:** MariaDB 11 (via Prisma 6.12.0 ORM)
|
||||||
- **API:** REST, validering med class-validator
|
- **API:** REST, validering med class-validator
|
||||||
- **Felhantering:** GlobalExceptionFilter (svenska felmeddelanden)
|
- **Felhantering:** GlobalExceptionFilter (svenska felmeddelanden)
|
||||||
- **Hälsokontroll:** /health endpoint med status, uptime, DB-latens
|
- **Hälsokontroll:** /health endpoints
|
||||||
- **Bygg:** Körs i Docker-container, byggs med nest build
|
- **Bygg:** `nest build`, körs i Docker-container
|
||||||
|
|
||||||
### Funktioner (backend)
|
### Backend-moduler och strukturen läsa
|
||||||
|
|
||||||
- **Inventarie-API:**
|
```
|
||||||
- CRUD för hemmavaror
|
backend/src/
|
||||||
- Konsumtionshistorik (med enheter)
|
├── app.module.ts # Root module
|
||||||
- Sortering och filtrering (plats, bäst före, namn)
|
├── main.ts # Startpunkt (port 8080)
|
||||||
- **Recept-API:**
|
├── 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.module.ts
|
||||||
|
├── inventory/
|
||||||
|
│ ├── inventory.controller.ts # CRUD endpoints
|
||||||
|
│ ├── inventory.service.ts # CRUD + konsumtion
|
||||||
|
│ ├── inventory.module.ts
|
||||||
|
│ └── dto/
|
||||||
|
│ ├── create-inventory.dto.ts
|
||||||
|
│ ├── update-inventory.dto.ts
|
||||||
|
│ └── consume-inventory.dto.ts
|
||||||
|
├── prisma/
|
||||||
|
│ ├── prisma.service.ts # PrismaClient wrapper
|
||||||
|
│ └── prisma.module.ts
|
||||||
|
├── products/
|
||||||
|
│ ├── products.controller.ts # CRUD, merge, duplicates
|
||||||
|
│ ├── products.service.ts # Produktlogik
|
||||||
|
│ ├── products.module.ts
|
||||||
|
│ └── dto/
|
||||||
|
│ ├── create-product.dto.ts
|
||||||
|
│ ├── update-product.dto.ts
|
||||||
|
│ ├── merge-products.dto.ts
|
||||||
|
│ └── update-canonical-name.dto.ts
|
||||||
|
└── recipes/
|
||||||
|
├── recipes.controller.ts # Recept endpoints
|
||||||
|
├── recipes.service.ts # Recept + Markdown-parsing
|
||||||
|
├── recipes.module.ts
|
||||||
|
└── dto/
|
||||||
|
├── create-recipe.dto.ts
|
||||||
|
├── parse-markdown.dto.ts
|
||||||
|
└── create-recipe-ingredient.dto.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backend-funktioner
|
||||||
|
|
||||||
|
**Health API:**
|
||||||
|
- Övergripande systemstatus (uptime, service info)
|
||||||
|
- Databasspecifik hälsokontroll (responseTime, connection test)
|
||||||
|
- Returnerar statusCode 200 eller 503
|
||||||
|
|
||||||
|
**Inventarie-API:**
|
||||||
|
- CRUD för inventarieföremål (produktreferens, kvantitet, enhet, plats, märke, bäst före, mm)
|
||||||
|
- Konsumtionshistorik-tracking (registrera brukat amount och kommentar)
|
||||||
|
- Sortering: efter plats, bäst före-datum, namn (A–Ö)
|
||||||
|
- Filtrera utgående varor
|
||||||
|
- **Enhetskonvertering:** Stöd för viktenheter (g/kg), volymenheter (ml/dl), portionsenheter (tsk/msk)
|
||||||
|
- Normalisering av enheter (t.ex. "tesked" → "tsk", "gram" → "g")
|
||||||
|
- Konverteringsregler per enhet-typ
|
||||||
|
- Kan endast konvertera inom samma enhet-typ (error om blandning)
|
||||||
|
|
||||||
|
**Recept-API:**
|
||||||
- CRUD för recept och ingredienser
|
- CRUD för recept och ingredienser
|
||||||
- Preview mot hemmavaror (räcker/saknas/enhetskonflikt)
|
- **Parse-markdown endpoint:** Tolkar Markdown-format, matchar ingredienser mot databas
|
||||||
- **Produkt-API:**
|
- **Matchningsalgoritm (3 nivåer):**
|
||||||
- CRUD för produkter
|
1. Exakt match (normalizedName eller canonicalName efter normalisering): **100 poäng**
|
||||||
- Merge preview och canonical name
|
2. Delsträng-match (ingrediens i produktnamn eller vice versa): **70 poäng**
|
||||||
- **Felhantering:**
|
3. Levenshtein-distans-baserad likhet: **40–100 poäng** (under 40 filtreras bort)
|
||||||
- Svenska felmeddelanden, 400/503 status
|
- Top 5 förslag per ingrediens
|
||||||
- **Hälsokontroll:**
|
- Sortering: Högsta poäng först
|
||||||
- /health endpoint (200/503, DB-status, uptime)
|
- **Inventory-preview:** Jämför recept mot inventarie
|
||||||
|
- Returnerar status för varje ingrediens: räcker | saknas | enhetskonflikt
|
||||||
|
- Automatisk enhetskonvertering vid jämförelse
|
||||||
|
- **Normalisering:** `normalize-name()` utility för consistent namn-matching
|
||||||
|
|
||||||
|
**Produkt-API:**
|
||||||
|
- CRUD för produkter (create, read, update, delete)
|
||||||
|
- **Duplicate detection:** `findDuplicates()` - Hitta produkter med samma normalizedName
|
||||||
|
- **Merge-preview:** `previewMerge()` - Förhandsgranska merge operation (visa inventory-counts, outcome)
|
||||||
|
- **Merge operation:** `merge()` - Slå ihop två produkter
|
||||||
|
- Flytta alla inventarieföremål från källa till mål
|
||||||
|
- Soft-delete källan (isActive = false, deletedAt = nu)
|
||||||
|
- Uppdatera recept-ingredienser från källa till mål
|
||||||
|
- **Canonical name management:**
|
||||||
|
- `updateCanonicalName()` - Uppdatera canonical name för ett produktnamn
|
||||||
|
- `backfillCanonical()` - Fylla på canonical names för alla produkter (admin-funktion)
|
||||||
|
- **Soft delete & restore:**
|
||||||
|
- `remove()` - Soft-delete produkt (isActive = false)
|
||||||
|
- `restore()` - Återställ borttagen produkt
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Infrastruktur & DevOps
|
## API-endpoints (fullständig lista)
|
||||||
|
|
||||||
- **Docker Compose:** Orkestrerar frontend, backend, databas och proxy
|
### 🏥 Health endpoints
|
||||||
- **Caddy:** Reverse proxy, hanterar Next.js API routes och backend
|
```
|
||||||
- **Miljövariabler:** Hanterar DB-url, ports etc
|
GET /health Övergripande hälsakontroll (200/503)
|
||||||
- **Backup-script:** backup_recipe_app.sh
|
GET /health/db Databasspecifik hälsa + responseTime
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📦 Inventarie-endpoints
|
||||||
|
```
|
||||||
|
GET /api/inventory Lista inventarieföremål
|
||||||
|
Params: ?location=... &sort=...
|
||||||
|
GET /api/inventory/expiring Utgångna/snart utgångna varor
|
||||||
|
POST /api/inventory Skapa nytt inventarieföremål
|
||||||
|
PATCH /api/inventory/:id Uppdatera inventarieföremål
|
||||||
|
POST /api/inventory/:id/consume Konsumera (registrera brukat amount)
|
||||||
|
GET /api/inventory/:id/consumption-history Konsumtionshistorik
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🍽️ Recept-endpoints
|
||||||
|
```
|
||||||
|
POST /api/recipes/parse-markdown Tolka Markdown-recept (matchningslogik)
|
||||||
|
GET /api/recipes Lista alla recept
|
||||||
|
POST /api/recipes Skapa nytt recept
|
||||||
|
GET /api/recipes/:id Hämta specifikt recept
|
||||||
|
PATCH /api/recipes/:id Uppdatera recept
|
||||||
|
DELETE /api/recipes/:id Ta bort recept (204 No Content)
|
||||||
|
GET /api/recipes/:id/inventory-preview Jämför recept mot inventarie
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🏷️ Produkt-endpoints
|
||||||
|
```
|
||||||
|
GET /api/products Lista alla aktiva produkter
|
||||||
|
POST /api/products Skapa ny produkt
|
||||||
|
GET /api/products/:id Hämta specifik produkt
|
||||||
|
PATCH /api/products/:id Uppdatera produktens namn
|
||||||
|
DELETE /api/products/:id Soft-delete produkt
|
||||||
|
POST /api/products/:id/restore Återställ raderad produkt
|
||||||
|
|
||||||
|
GET /api/products/duplicates Lista duplicerade namn (grupperade)
|
||||||
|
GET /api/products/merge-preview Förhandsgranska merge
|
||||||
|
?sourceProductId=X &targetProductId=Y
|
||||||
|
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)
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Viktiga filer & mappar
|
## Datamodell (Prisma ORM)
|
||||||
|
|
||||||
- `frontend/app/` – Next.js app directory (pages, komponenter)
|
### Product
|
||||||
- `backend/src/` – NestJS API (controllers, services, modules)
|
```prisma
|
||||||
- `backend/prisma/schema.prisma` – Prisma datamodell
|
model Product {
|
||||||
- `compose.yml` – Docker Compose setup
|
id Int @id @default(autoincrement())
|
||||||
- `Caddyfile` – Proxyregler
|
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
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
inventoryItems InventoryItem[]
|
||||||
|
recipeIngredients RecipeIngredient[]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### InventoryItem
|
||||||
|
```prisma
|
||||||
|
model InventoryItem {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
productId Int # Foreign key till Product
|
||||||
|
quantity Decimal @db.Decimal(10, 2) # Kvantitet (decimal för precision)
|
||||||
|
unit String # Enhet (g, kg, ml, dl, st, tsk, msk, etc)
|
||||||
|
brand String? # Varumärke
|
||||||
|
location String? # Lagerplats (Kyl, Frys, Skafferi, etc)
|
||||||
|
priority Int? # Prioritetsordning
|
||||||
|
purchaseDate DateTime? # Köpdatum
|
||||||
|
opened Boolean? # Markering för öppnad produkt
|
||||||
|
shelfNote String? # Lagringsnot
|
||||||
|
suitableFor String? # Lämplighetsmärkning (t.ex. "vegetarian")
|
||||||
|
isOnSale Boolean? # Är på rea
|
||||||
|
priceLevel Int? # Priskategori (1–5)
|
||||||
|
bestBeforeDate DateTime? # Bäst före-datum
|
||||||
|
proteinType String? # Proteintyp (t.ex. "beef", "chicken")
|
||||||
|
isLeftover Boolean? # Är från tidigare lagnning
|
||||||
|
comment String? # Fri kommentar
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
|
||||||
|
consumptions InventoryConsumption[]
|
||||||
|
|
||||||
|
@@index([productId])
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### InventoryConsumption
|
||||||
|
```prisma
|
||||||
|
model InventoryConsumption {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
inventoryItemId Int # Foreign key till InventoryItem
|
||||||
|
amountUsed Decimal @db.Decimal(10, 2) # Konsumerad kvantitet
|
||||||
|
comment String? # Kommentar
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
inventoryItem InventoryItem @relation(fields: [inventoryItemId], references: [id])
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Recipe
|
||||||
|
```prisma
|
||||||
|
model Recipe {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
name String # Receptnamn
|
||||||
|
description String? # Receptbeskrivning
|
||||||
|
instructions String? @db.Text # Tillagningsinstruktioner (kan vara långt, stöder Markdown)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
ingredients RecipeIngredient[]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### RecipeIngredient
|
||||||
|
```prisma
|
||||||
|
model RecipeIngredient {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
recipeId Int # Foreign key till Recipe
|
||||||
|
productId Int # Foreign key till Product
|
||||||
|
quantity Decimal @db.Decimal(10, 2) # Receptkvantitet
|
||||||
|
unit String # Enhet enligt recept
|
||||||
|
note String? # Ingrediensnot (t.ex. variation)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
recipe Recipe @relation(fields: [recipeId], references: [id])
|
||||||
|
product Product @relation(fields: [productId], references: [id])
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Funktionell översikt
|
## Receptimport via Markdown — Detaljerad arkitektur
|
||||||
|
|
||||||
### Hemmavaror
|
|
||||||
- Lägg till, redigera, ta bort och konsumera varor
|
|
||||||
- Sök, filtrera (plats), sortera (bäst före, namn)
|
|
||||||
- Konsumtionshistorik med enheter
|
|
||||||
|
|
||||||
### Recept
|
|
||||||
- Skapa, redigera, ta bort recept
|
|
||||||
- Jämför mot hemmavaror (räcker/saknas/enhetskonflikt)
|
|
||||||
- Visar instruktioner och saknade ingredienser
|
|
||||||
- **Importera recept från Markdown** (se nedan)
|
|
||||||
|
|
||||||
### Produkter (Admin)
|
|
||||||
- Sök, sortera, redigera canonical name
|
|
||||||
- Merge preview
|
|
||||||
|
|
||||||
### Hälsa & Fel
|
|
||||||
- /health endpoint (status, uptime, DB)
|
|
||||||
- Svenska felmeddelanden i hela systemet
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Exempel på API-endpoints
|
|
||||||
|
|
||||||
- `GET /api/inventory` – Lista hemmavaror
|
|
||||||
- `POST /api/inventory` – Lägg till vara
|
|
||||||
- `PATCH /api/inventory/:id` – Uppdatera vara
|
|
||||||
- `DELETE /api/inventory/:id` – Ta bort vara
|
|
||||||
- `GET /api/recipes` – Lista recept
|
|
||||||
- `POST /api/recipes` – Skapa recept
|
|
||||||
- `PATCH /api/recipes/:id` – Uppdatera recept
|
|
||||||
- `DELETE /api/recipes/:id` – Ta bort recept
|
|
||||||
- `POST /api/recipes/parse-markdown` – Tolka Markdown-recept (se nedan)
|
|
||||||
- `GET /api/products` – Lista produkter
|
|
||||||
- `PATCH /api/products/:id` – Uppdatera produkt
|
|
||||||
- `GET /health` – Hälsokontroll
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Receptimport via Markdown
|
|
||||||
|
|
||||||
### Syfte
|
### Syfte
|
||||||
|
|
||||||
Användaren kan importera ett recept skrivet i ett enkelt Markdown-format istället för att fylla i formularet manuellt. Systemet tolkar texten, föreslår matchande produkter från databasen och låter användaren granska och bekräfta innan receptet sparas.
|
Användaren kan importera ett recept skrivet i Markdown-format istället för att fylla i formularet manuellt. Systemet:
|
||||||
|
1. Tolkar Markdown-format (namn, beskrivning, ingredienser, instruktioner)
|
||||||
|
2. Matchar varje ingrediens mot produktdatabasen (intelligenta matchningar)
|
||||||
|
3. Låter användaren granska förslag och välja rätt produkt
|
||||||
|
4. Sparar receptet med valida ingredienser
|
||||||
|
|
||||||
### Markdown-format
|
### Markdown-format och parsningsregler
|
||||||
|
|
||||||
|
**Format:**
|
||||||
```markdown
|
```markdown
|
||||||
# Receptnamn
|
# Receptnamn
|
||||||
|
|
||||||
@@ -163,195 +350,327 @@ Valfri beskrivning av receptet.
|
|||||||
## Ingredienser
|
## Ingredienser
|
||||||
- 500 g köttfärs
|
- 500 g köttfärs
|
||||||
- 1 st lök
|
- 1 st lök
|
||||||
- 2 msk tomatpuré
|
- 2.5 msk tomatpuré
|
||||||
- 1 dl grädde (vispgrädde)
|
- 1 dl grädde (vispgrädde)
|
||||||
|
- salt
|
||||||
|
|
||||||
## Tillvägagångssätt
|
## Tillvägagångssätt
|
||||||
Stek löken i lite smör. Tillsats köttfärsen...
|
Stek löken i lite smör. Tillsätt köttfärsen…
|
||||||
```
|
```
|
||||||
|
|
||||||
Regler:
|
**Parsningsregler i detalj:**
|
||||||
- Rad med `#` tolkas som receptnamn
|
| Element | Tolkning |
|
||||||
- Text mellan `#`-rubriken och `## Ingredienser` tolkas som beskrivning
|
|---------|----------|
|
||||||
- Rader under `## Ingredienser` med mönstret `- ANTAL ENHET NAMN` tolkas som ingredienser
|
| `# Rubrik` | Receptnamn (första H1) |
|
||||||
- Text i parentes efter ingrediensnamnet (`(vispgrädde)`) sparas som anteckning
|
| Text mellan H1 och `## Ingredienser` | Receptbeskrivning (flera rader OK, valfritt) |
|
||||||
- Text under `## Tillvägagångssätt` (eller `## Instruktioner`) tolkas som instruktioner
|
| `## Ingredienser` | Ingred markerare (case-insensitive) |
|
||||||
|
| `- ANTAL ENHET NAMN` | Ingrediens: quantity=ANTAL, unit=ENHET, name=NAMN |
|
||||||
### Arkitektur
|
| `- ANTAL NAMN` | Ingrediens utan enhet: unit sätts till "st" |
|
||||||
|
| `- NAMN` | Ingrediens utan kvantitet: quantity=0, unit="" |
|
||||||
|
| `(text i parentes)` | Ingrediensnot (sparas, t.ex. "vispgrädde") |
|
||||||
|
| `## Tillvägagångssätt` / `## Instruktioner` | Instruktionssektion markerare |
|
||||||
|
| Text under instruktioner | Tillagningsinstruktioner (flera rader OK) |
|
||||||
|
|
||||||
|
**Exempel ingrediensparsning:**
|
||||||
```
|
```
|
||||||
Användaren Frontend Backend Bibliotek
|
"- 500 g köttfärs" → {quantity: 500, unit: "g", rawName: "köttfärs"}
|
||||||
(klistrar in MD) → /recipes/import → POST /api/ → recipe-document-
|
"- 1,5 dl grädde (vispgrädde)" → {quantity: 1.5, unit: "dl", rawName: "grädde", note: "vispgrädde"}
|
||||||
ImportRecipePage recipes/ converter/
|
"- 3 ägg" → {quantity: 3, unit: "st", rawName: "ägg"}
|
||||||
parse-markdown parseRecipeMarkdown()
|
"- salt" → {quantity: 0, unit: "", rawName: "salt"}
|
||||||
↑
|
|
||||||
Granskar förslag
|
|
||||||
Väljer produkter
|
|
||||||
↓
|
|
||||||
POST /api/recipes (befintlig endpoint)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Komponenterna
|
### Komponenter och dataflöde
|
||||||
|
|
||||||
#### `recipe-document-converter/` (fristående TypeScript-bibliotek)
|
#### 1. `recipe-document-converter/` bibliotek
|
||||||
|
|
||||||
Ett eget npm-paket som inte har externa beroenden. Det enda som exporteras är:
|
**Modulstruktur:**
|
||||||
|
- `src/parser.ts` — Markdown-parser med ingrediensparsning
|
||||||
|
- `src/index.ts` — Biblioteksexport
|
||||||
|
- `package.json` — npm-paket (noll externa beroenden)
|
||||||
|
- `tsconfig.json` — TypeScript-konfiguration
|
||||||
|
|
||||||
|
**Huvudexport:**
|
||||||
```typescript
|
```typescript
|
||||||
parseRecipeMarkdown(markdown: string): ParsedRecipe
|
export function parseRecipeMarkdown(markdown: string): ParsedRecipe
|
||||||
```
|
|
||||||
|
|
||||||
Returnerar:
|
interface ParsedRecipe {
|
||||||
```typescript
|
|
||||||
type ParsedRecipe = {
|
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description: string; # Kan vara tom
|
||||||
instructions?: string;
|
instructions: string; # Kan vara tom
|
||||||
ingredients: Array<{
|
ingredients: ParsedIngredient[];
|
||||||
rawName: string; // fråntext, t.ex. "köttfärs"
|
}
|
||||||
quantity: number; // t.ex. 500
|
|
||||||
unit: string; // t.ex. "g"
|
interface ParsedIngredient {
|
||||||
note?: string; // text i parentes, t.ex. "nötfärs"
|
rawName: string; # Extraherat ingrediensnamn
|
||||||
}>;
|
quantity: number; # Numerisk kvantitet (eller 0 om ingen)
|
||||||
};
|
unit: string; # Enhet (eller "" om ingen)
|
||||||
|
note: string | null; # Text i parentes (eller null)
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Biblioteket kompileras i ett separat Docker-byggsteg och länkas till backend via `"recipe-document-converter": "file:../recipe-document-converter"` i `backend/package.json`.
|
**Ingrediensparsning i detalj:**
|
||||||
|
- Försöker matcha regex `(\d+(?:[.,]\d+)?)\s+(\S+)\s+(.+)` för "ANTAL ENHET NAMN"
|
||||||
|
- Fallback: `(\d+(?:[.,]\d+)?)\s+(.+)` för "ANTAL NAMN" (unit → "st")
|
||||||
|
- Fallback: bara namn, quantity=0, unit=""
|
||||||
|
- Kommatecken i tal: `,` → `.` (t.ex. "1,5" blir 1.5)
|
||||||
|
- Parentes-extraktion: Matchar sista `(text)` i raden
|
||||||
|
|
||||||
#### Backend — `POST /api/recipes/parse-markdown`
|
**Byggning:**
|
||||||
|
- TypeScript → JavaScript via `tsc`
|
||||||
|
- Separerad npm-modul
|
||||||
|
- Kompileras i Docker-build steg 1 (converter-build)
|
||||||
|
|
||||||
Endpoint som tar emot `{ markdown: string }` och returnerar det tolkade receptet åtsamman med produktmatchförslag för varje ingrediens.
|
#### 2. Backend: `POST /api/recipes/parse-markdown` endpoint
|
||||||
|
|
||||||
Matchningslogik:
|
**Klassbank:**
|
||||||
1. Anropar `parseRecipeMarkdown()` från biblioteket
|
- `recipe-document-converter` — Markdown-parser
|
||||||
2. Hämtar alla aktiva produkter ur databasen
|
- `@prisma/client` — Databasaccess
|
||||||
3. Jämför varje ingrediensnamn mot `product.canonicalName` / `product.normalizedName` med tre metoder i ordning:
|
- Common utils: `normalize-name.ts`
|
||||||
- **Exakt match** (efter normalisering) → 100 poäng
|
|
||||||
- **Delsträngsmatch** → 70 poäng
|
|
||||||
- **Levenshtein-likhet** → 0–100 poäng (filtreras under 40)
|
|
||||||
4. Returnerar upp till 5 förslag per ingrediens, sorterade efter poäng
|
|
||||||
|
|
||||||
Svar:
|
**Processflöde:**
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Motta: ParseMarkdownDto { markdown: string }
|
||||||
|
2. Anropa parseRecipeMarkdown() → ParsedRecipe
|
||||||
|
3. Hämta alla aktiva produkter från DB
|
||||||
|
4. För varje ingrediens:
|
||||||
|
a. Normalisera: lowercase + trim + remove accents/punctuation
|
||||||
|
b. Matchningsalgoritm (se nedan)
|
||||||
|
c. Top 5 förslag sortera efter score
|
||||||
|
5. Returnera ParsedRecipe + suggestions
|
||||||
|
```
|
||||||
|
|
||||||
|
**Matchningsalgoritm:**
|
||||||
|
```
|
||||||
|
Normalisering: lowercase + trim + åäö-handling + skilljetecken-borttagning
|
||||||
|
|
||||||
|
1. EXAKT MATCH (100 poäng)
|
||||||
|
IF (ingrediens == product.canonicalName_normalized) OR
|
||||||
|
(ingrediens == product.normalizedName_normalized)
|
||||||
|
THEN score = 100
|
||||||
|
|
||||||
|
2. DELSTRÄNG-MATCH (70 poäng)
|
||||||
|
IF (ingrediens IN product.name_normalized) OR
|
||||||
|
(product.name_normalized IN ingrediens)
|
||||||
|
THEN score = 70
|
||||||
|
|
||||||
|
3. LEVENSHTEIN-LIKHET (40–100 poäng)
|
||||||
|
Calculate Levenshtein distance between ingrediens and product.name_normalized
|
||||||
|
similarity% = (1 - (distance / max_length)) * 100
|
||||||
|
IF similarity% >= 40 THEN score = similarity%
|
||||||
|
ELSE filter out
|
||||||
|
|
||||||
|
Sortering: Högsta poäng först
|
||||||
|
Top 5: Max 5 förslag per ingrediens
|
||||||
|
```
|
||||||
|
|
||||||
|
**Svarsobjekt:**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"name": "Köttfärssås",
|
"name": "Köttfärssås",
|
||||||
"description": "En klassisk...",
|
"description": "En klassisk…",
|
||||||
"instructions": "Stek löken...",
|
"instructions": "Stek löken…",
|
||||||
"ingredients": [
|
"ingredients": [
|
||||||
{
|
{
|
||||||
"rawName": "köttfärs",
|
"rawName": "köttfärs",
|
||||||
"quantity": 500,
|
"quantity": 500,
|
||||||
"unit": "g",
|
"unit": "g",
|
||||||
|
"note": null,
|
||||||
"suggestions": [
|
"suggestions": [
|
||||||
{ "productId": 12, "productName": "Köttfärs", "score": 100 },
|
{ "productId": 12, "productName": "Köttfärs", "score": 100 },
|
||||||
{ "productId": 34, "productName": "Blandfärs", "score": 55 }
|
{ "productId": 34, "productName": "Blandfärs", "score": 65 },
|
||||||
|
{ "productId": 56, "productName": "Nötfärs", "score": 55 }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Frontend — `/recipes/import`
|
#### 3. Frontend: `/recipes/import` page
|
||||||
|
|
||||||
En 3-stegsvy (client component):
|
**Komponenter:**
|
||||||
|
- `ImportRecipePage.tsx` — Main client component (3-steps state machine)
|
||||||
|
- Använder `/api/parse-markdown-proxy` för backend-anrop (Next.js proxy)
|
||||||
|
|
||||||
| Steg | Innehåll |
|
**Steg 1: Klistra in Markdown**
|
||||||
|------|----------|
|
- `<textarea>` för råtext
|
||||||
| 1. Klistra in | Textarea för Markdown + "Tolka recept"-knapp |
|
- Knapp: "Tolka recept" → POST /api/parse-markdown-proxy
|
||||||
| 2. Granska | Redigerbara fält för namn/beskrivning/instruktioner; varje ingrediens har en dropdown med föreslagna produkter överst, sedan alla produkter |
|
- Error handling med svenska meddelanden
|
||||||
| 3. Spara | Knapp som POSTar till befintlig `POST /api/recipes` |
|
|
||||||
|
|
||||||
Ingrediensräder med ingen matchning markeras visuellt (gul ram) så att användaren ser att de behöver väljas manuellt. Receptet sparas inte förrän minst en ingrediens har en vald produkt.
|
**Steg 2: Granska och välj**
|
||||||
|
- Redigerbara fält: namn, beskrivning, instruktioner
|
||||||
|
- För varje ingrediens:
|
||||||
|
- Visar föreslagna produkter (top 5, prioriterad ordning)
|
||||||
|
- Fallback: Dropdown med alla produkter i DB
|
||||||
|
- Redigerbara fält: quantity, unit, note
|
||||||
|
- Visuell markering (gul ram) för ingredienser utan vald produkt
|
||||||
|
- Knapp: "Ta bort ingrediens"
|
||||||
|
- Validering: Minst 1 ingrediens måste ha vald produkt
|
||||||
|
|
||||||
Flöde i Next.js:
|
**Steg 3: Spara**
|
||||||
|
- POST /api/recipes med:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "...",
|
||||||
|
"description": "... " eller undefined,
|
||||||
|
"instructions": "..." eller undefined,
|
||||||
|
"ingredients": [
|
||||||
|
{ "productId": 12, "quantity": 500, "unit": "g", "note": "vispgrädde" },
|
||||||
|
{ "productId": 34, "quantity": 1, "unit": "st", "note": undefined }
|
||||||
|
]
|
||||||
|
}
|
||||||
```
|
```
|
||||||
/recipes/import
|
- Efter framgång: navigera till receptlistan
|
||||||
└─ ImportRecipePage.tsx (client component, 3-stegsflödet)
|
|
||||||
|
|
||||||
/api/parse-markdown-proxy
|
**Enhetsstöd i UI:**
|
||||||
└─ route.ts (POST-proxy till backend, omgår CORS)
|
- Dropdown-alternativ: g, kg, hg, ml, dl, l, st, tsk, msk
|
||||||
|
|
||||||
|
#### 4. API-proxy-route (Next.js)
|
||||||
|
|
||||||
|
**`/api/parse-markdown-proxy/route.ts`**
|
||||||
|
- POST-endpoint
|
||||||
|
- Proxies anrop till backend `POST /api/recipes/parse-markdown`
|
||||||
|
- Hanterar CORS, headers, error-svarsöversättning
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Enhetskonvertering (backendsida)
|
||||||
|
|
||||||
|
### Stödda enhetstyper
|
||||||
|
|
||||||
|
| Typ | Enheter | Bassystem |
|
||||||
|
|-----|---------|-----------|
|
||||||
|
| **Vikt** | g, kg | gram (g) |
|
||||||
|
| **Volym** | ml, dl | milliliter (ml) |
|
||||||
|
| **Portioner** | tsk, msk | tesked (tsk), där 1 msk = 3 tsk |
|
||||||
|
| **Stycken** | st | kan inte konverteras |
|
||||||
|
|
||||||
|
### Normalisering (inom `RecipesService.normalizeUnit()`)
|
||||||
|
|
||||||
|
| Input | Output | Typ |
|
||||||
|
|-------|--------|-----|
|
||||||
|
| "tesked", "test" | "tsk" | Portion |
|
||||||
|
| "matsled", "matsked" | "msk" | Portion |
|
||||||
|
| "gram" | "g" | Vikt |
|
||||||
|
| "kilogram", "kilo", "kg" | "kg" | Vikt |
|
||||||
|
| "milliliter" | "ml" | Volym |
|
||||||
|
| "deciliter" | "dl" | Volym |
|
||||||
|
| "stycke" | "st" | Styck |
|
||||||
|
| (other) | (as-is) | (as-is) |
|
||||||
|
|
||||||
|
### Konverteringslogik (`convertUnit()` metod)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
convertUnit(quantity: number, fromUnit: string, toUnit: string, productName: string): number {
|
||||||
|
// 1. Validering (quantity > 0, units inte tomma)
|
||||||
|
// 2. Normalisera båda enheter
|
||||||
|
// 3. Om identiska efter normalisering → return quantity
|
||||||
|
// 4. Bestäm enhetstyp för båda
|
||||||
|
// 5. Om olika typer → throw Error
|
||||||
|
// 6. Konvertera via basenhet:
|
||||||
|
// quantity * factor[fromUnit] / factor[toUnit]
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Docker-bygget
|
### Användning i `getInventoryPreview()`
|
||||||
|
|
||||||
Backend-Dockerfilen använder projektets rot (`.`) som byggkontext. Bygget sker i tre steg:
|
Då receptjämförelse jämförs mot inventarie:
|
||||||
|
```
|
||||||
|
För varje ingrediens:
|
||||||
|
1. Hämta alla inventarieföremål för denna produkt
|
||||||
|
2. Gruppera efter enhet:
|
||||||
|
- Samma enhet som ingrediens → summera direkt
|
||||||
|
- Annan enhet → försök konvertera
|
||||||
|
3. Om konvertering misslyckas (t.ex. st ↔ g) → hoppa över denna post
|
||||||
|
4. Summera totalt available (samma + konverterad)
|
||||||
|
5. Returnera status: räcker | saknas | enhetskonflikt
|
||||||
|
```
|
||||||
|
|
||||||
1. **converter-build** — Kompilerar `recipe-document-converter` till JavaScript + typdeklarationer
|
---
|
||||||
2. **builder** — Installerar backend-beroenden, kopierar in den kompilerade convertern till `node_modules/`, genererar Prisma-klient och bygger NestJS-appen
|
|
||||||
3. **runner** — Minimal produktionsimage med enbart `dist/`, `node_modules/` och `prisma/`
|
|
||||||
|
|
||||||
|
## Infrastruktur & DevOps
|
||||||
|
|
||||||
|
### Docker Compose setup
|
||||||
|
|
||||||
|
**Services:**
|
||||||
|
- `recipe-frontend` — Next.js container (port 3000)
|
||||||
|
- `recipe-api` — NestJS backend (port 8080)
|
||||||
|
- `db` — MariaDB database (port 3306)
|
||||||
|
- `proxy` — (valfritt) Caddy reverse proxy
|
||||||
|
|
||||||
|
**Volumer:**
|
||||||
|
- Database data persistence
|
||||||
|
- Build layers caching
|
||||||
|
|
||||||
|
**Networks:**
|
||||||
|
- External `proxy` network (för Caddy integrering, valfritt)
|
||||||
|
- Intern `recipe-network` mellan services
|
||||||
|
|
||||||
|
### Backend-Dockerfile (3-stage build)
|
||||||
|
|
||||||
|
**Stage 1: converter-build**
|
||||||
```dockerfile
|
```dockerfile
|
||||||
# Stage 1: Bygg converter-biblioteket
|
|
||||||
FROM node:22-alpine AS converter-build
|
FROM node:22-alpine AS converter-build
|
||||||
WORKDIR /converter
|
# Bygg recipe-document-converter biblioteket
|
||||||
COPY recipe-document-converter/package.json ./
|
|
||||||
RUN npm install
|
|
||||||
COPY recipe-document-converter/src ./src
|
|
||||||
COPY recipe-document-converter/tsconfig.json ./
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
# Stage 2: Bygg applikationen
|
|
||||||
FROM node:22-alpine AS builder
|
|
||||||
WORKDIR /app
|
|
||||||
COPY backend/package.json ./
|
|
||||||
COPY backend/prisma ./prisma
|
|
||||||
COPY backend/src ./src
|
|
||||||
COPY backend/tsconfig.json ./
|
|
||||||
COPY backend/nest-cli.json ./
|
|
||||||
RUN npm install
|
|
||||||
# Kopiera in det kompilerade converter-biblioteket efter npm install
|
|
||||||
COPY --from=converter-build /converter ./node_modules/recipe-document-converter
|
|
||||||
RUN npx prisma generate
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
# Stage 3: Kör applikationen
|
|
||||||
FROM node:22-alpine AS runner
|
|
||||||
WORKDIR /app
|
|
||||||
ENV NODE_ENV=production
|
|
||||||
COPY --from=builder /app/package.json ./package.json
|
|
||||||
COPY --from=builder /app/node_modules ./node_modules
|
|
||||||
COPY --from=builder /app/prisma ./prisma
|
|
||||||
COPY --from=builder /app/dist ./dist
|
|
||||||
EXPOSE 8080
|
|
||||||
CMD ["node", "dist/main"]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
> **OBS:** `backend/package.json` har `"recipe-document-converter": "file:../recipe-document-converter"` för lokal utveckling. I Docker-bygget ignoreras den file-referensen — convertern kopieras in manuellt från converter-build-steget.
|
**Stage 2: builder**
|
||||||
|
```dockerfile
|
||||||
|
FROM node:22-alpine AS builder
|
||||||
|
# Installera backend-deps
|
||||||
|
# Kopiera converter från stage 1
|
||||||
|
# Generera Prisma-klient
|
||||||
|
# Bygg NestJS-appen (nest build)
|
||||||
|
```
|
||||||
|
|
||||||
Bygga om backend efter ändringar:
|
**Stage 3: runner**
|
||||||
|
```dockerfile
|
||||||
|
FROM node:22-alpine AS runner
|
||||||
|
# Minimal production image
|
||||||
|
# Enbart dist/, node_modules/, prisma/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Viktigt:** `backend/package.json` har `"recipe-document-converter": "file:../recipe-document-converter"` för lokal dev. I Docker ignoreras denna; convertern kopieras in från stage 1.
|
||||||
|
|
||||||
|
Build-kommando:
|
||||||
```bash
|
```bash
|
||||||
docker compose build recipe-api
|
docker compose build recipe-api
|
||||||
|
docker compose up -d recipe-api
|
||||||
```
|
```
|
||||||
|
|
||||||
### Relevanta filer
|
### Frontend-Dockerfile
|
||||||
|
|
||||||
| Fil | Syfte |
|
Standard Next.js build → standalone output
|
||||||
|-----|-------|
|
|
||||||
| `recipe-document-converter/src/parser.ts` | Markdown-parser |
|
### Miljövariabler
|
||||||
| `recipe-document-converter/src/index.ts` | Biblioteksexport |
|
|
||||||
| `backend/src/recipes/dto/parse-markdown.dto.ts` | Inkommande DTO |
|
Konfigureras via `.env` eller `docker compose up`:
|
||||||
| `backend/src/recipes/recipes.controller.ts` | Nytt endpoint |
|
- `DATABASE_URL` — MariaDB-anslutning (backend)
|
||||||
| `backend/src/recipes/recipes.service.ts` | Matchningslogik |
|
- `PORT` — Backend port (default 8080)
|
||||||
| `frontend/app/recipes/import/ImportRecipePage.tsx` | 3-stegsvy |
|
- Ev. Caddy-konfiguration
|
||||||
| `frontend/app/api/parse-markdown-proxy/route.ts` | Proxy-route |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Säkerhet
|
## UTF-8 och lokalisering
|
||||||
|
|
||||||
- Ingen auth i grundutförande (kan enkelt byggas på)
|
- Database: utf8mb4
|
||||||
- Validering av all input (class-validator)
|
- Backend: Normaliseringsfunktion hanterar åäö
|
||||||
- Felmeddelanden på svenska
|
- Frontend: Svenska felmeddelanden och UI-text
|
||||||
|
- Endpoints: Svenska benämningar och kategori-namn
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Utbyggbarhet
|
## Säkerhet & Utbyggbarhet
|
||||||
|
|
||||||
- Lätt att lägga till fler fält, filter och funktioner
|
- **Ingen auth i grundutförande** (kan enkelt byggas på)
|
||||||
- Kan utökas med auth, shoppinglistor, delning m.m.
|
- **Validering:** Alla DTO:er valideras med class-validator
|
||||||
|
- **Felhantering:** GlobalExceptionFilter med svenska meddelanden
|
||||||
|
- **CORS:** Proxies hanteras via Next.js API routes
|
||||||
|
|
||||||
---
|
### Möjliga utbyggnader
|
||||||
|
- Authentication (JWT, OAuth)
|
||||||
## Kontakt
|
- Multi-user support
|
||||||
|
- Shoppinglistor
|
||||||
För frågor, kontakta utvecklaren.
|
- Recept-delning
|
||||||
|
- Nutrition facts
|
||||||
|
- Allergi-tracking
|
||||||
|
|||||||
Reference in New Issue
Block a user