677 lines
24 KiB
Markdown
677 lines
24 KiB
Markdown
# Teknisk beskrivning av Recipe App
|
||
|
||
> Se [README.md](README.md) för användarinformation och kom-igång-guide.
|
||
|
||
## Översikt
|
||
|
||
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.
|
||
|
||
---
|
||
|
||
## Versionsinformation
|
||
|
||
| Delsystem | Teknik | Version |
|
||
|-------------|----------------|-----------------|
|
||
| Frontend | Next.js | 16.2 |
|
||
| | React | 19.2 |
|
||
| | TypeScript | 5.4.5 |
|
||
| | Node | 22.x (@types/node 22.15.29) |
|
||
| Backend | NestJS | 10.3 |
|
||
| | Prisma | 6.12.0 |
|
||
| | TypeScript | 5.4.5 |
|
||
| | Node | 22.x (@types/node 22.15.29) |
|
||
| Databas | MariaDB | 11 |
|
||
| Proxy | Caddy | 2.x |
|
||
| Container | Docker | 24+ |
|
||
| Converter | Node.js (TypeScript) | Noll externa beroenden |
|
||
|
||
---
|
||
|
||
## Frontend
|
||
|
||
- **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)
|
||
- **Bygg:** Standalone output, körs i Docker-container
|
||
- **API-anrop:** Fetch mot backend och Next.js API routes
|
||
- **Felhantering:** Global parseErrorResponse utility, svenska felmeddelanden
|
||
|
||
### Frontend-sidor och komponenter
|
||
|
||
| Sida | Fil | Funktionalitet |
|
||
|------|-----|---|
|
||
| **Hem** | `app/page.tsx` | Startsida |
|
||
| **Navigering** | `app/Navigation.tsx` | Huvudmeny |
|
||
| **Inventorie** | `app/inventory/page.tsx` | Lista, filtrera, sortera varor |
|
||
| | `InventoryList.tsx` | Ritning av inventarieföremål |
|
||
| | `InventoryForm.tsx` | Skapa nytt inventarieföremål |
|
||
| | `InventoryEditForm.tsx` | Redigera inventarieföremål |
|
||
| | `InventoryConsumeForm.tsx` | Konsumera (brukat) inventarieföremål |
|
||
| | `InventoryConsumptionHistory.tsx` | Visa konsumtionshistorik |
|
||
| | `ProductForm.tsx` | Välja produkt för inventarieföremål |
|
||
| | `actions.ts` | Server actions för inventarie |
|
||
| **Recept** | `app/recipes/page.tsx` | Lista recept |
|
||
| | `RecipePreview.tsx` | Receptförhandsvisning med inventariestatus |
|
||
| **Skapa recept** | `app/recipes/create/page.tsx` | Receptkreation (manual form) |
|
||
| | `app/recipes/create/CreateRecipePage.tsx` | Komponenter för receptskapande |
|
||
| **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 (NestJS)
|
||
|
||
- **Framework:** NestJS 10.3
|
||
- **Språk:** TypeScript 5.4.5
|
||
- **Databas:** MariaDB 11 (via Prisma 6.12.0 ORM)
|
||
- **API:** REST, validering med class-validator
|
||
- **Felhantering:** GlobalExceptionFilter (svenska felmeddelanden)
|
||
- **Hälsokontroll:** /health endpoints
|
||
- **Bygg:** `nest build`, körs i Docker-container
|
||
|
||
### Backend-moduler och strukturen läsa
|
||
|
||
```
|
||
backend/src/
|
||
├── app.module.ts # Root module
|
||
├── main.ts # Startpunkt (port 8080)
|
||
├── 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
|
||
- **Parse-markdown endpoint:** Tolkar Markdown-format, matchar ingredienser mot databas
|
||
- **Matchningsalgoritm (3 nivåer):**
|
||
1. Exakt match (normalizedName eller canonicalName efter normalisering): **100 poäng**
|
||
2. Delsträng-match (ingrediens i produktnamn eller vice versa): **70 poäng**
|
||
3. Levenshtein-distans-baserad likhet: **40–100 poäng** (under 40 filtreras bort)
|
||
- Top 5 förslag per ingrediens
|
||
- Sortering: Högsta poäng först
|
||
- **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
|
||
|
||
---
|
||
|
||
## API-endpoints (fullständig lista)
|
||
|
||
### 🏥 Health endpoints
|
||
```
|
||
GET /health Övergripande hälsakontroll (200/503)
|
||
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)
|
||
```
|
||
|
||
---
|
||
|
||
## Datamodell (Prisma ORM)
|
||
|
||
### Product
|
||
```prisma
|
||
model Product {
|
||
id Int @id @default(autoincrement())
|
||
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])
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Receptimport via Markdown — Detaljerad arkitektur
|
||
|
||
### Syfte
|
||
|
||
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 och parsningsregler
|
||
|
||
**Format:**
|
||
```markdown
|
||
# Receptnamn
|
||
|
||
Valfri beskrivning av receptet.
|
||
|
||
## Ingredienser
|
||
- 500 g köttfärs
|
||
- 1 st lök
|
||
- 2.5 msk tomatpuré
|
||
- 1 dl grädde (vispgrädde)
|
||
- salt
|
||
|
||
## Tillvägagångssätt
|
||
Stek löken i lite smör. Tillsätt köttfärsen…
|
||
```
|
||
|
||
**Parsningsregler i detalj:**
|
||
| Element | Tolkning |
|
||
|---------|----------|
|
||
| `# Rubrik` | Receptnamn (första H1) |
|
||
| Text mellan H1 och `## Ingredienser` | Receptbeskrivning (flera rader OK, valfritt) |
|
||
| `## Ingredienser` | Ingred markerare (case-insensitive) |
|
||
| `- ANTAL ENHET NAMN` | Ingrediens: quantity=ANTAL, unit=ENHET, name=NAMN |
|
||
| `- 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:**
|
||
```
|
||
"- 500 g köttfärs" → {quantity: 500, unit: "g", rawName: "köttfärs"}
|
||
"- 1,5 dl grädde (vispgrädde)" → {quantity: 1.5, unit: "dl", rawName: "grädde", note: "vispgrädde"}
|
||
"- 3 ägg" → {quantity: 3, unit: "st", rawName: "ägg"}
|
||
"- salt" → {quantity: 0, unit: "", rawName: "salt"}
|
||
```
|
||
|
||
### Komponenter och dataflöde
|
||
|
||
#### 1. `recipe-document-converter/` bibliotek
|
||
|
||
**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
|
||
export function parseRecipeMarkdown(markdown: string): ParsedRecipe
|
||
|
||
interface ParsedRecipe {
|
||
name: string;
|
||
description: string; # Kan vara tom
|
||
instructions: string; # Kan vara tom
|
||
ingredients: ParsedIngredient[];
|
||
}
|
||
|
||
interface ParsedIngredient {
|
||
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)
|
||
}
|
||
```
|
||
|
||
**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
|
||
|
||
**Byggning:**
|
||
- TypeScript → JavaScript via `tsc`
|
||
- Separerad npm-modul
|
||
- Kompileras i Docker-build steg 1 (converter-build)
|
||
|
||
#### 2. Backend: `POST /api/recipes/parse-markdown` endpoint
|
||
|
||
**Klassbank:**
|
||
- `recipe-document-converter` — Markdown-parser
|
||
- `@prisma/client` — Databasaccess
|
||
- Common utils: `normalize-name.ts`
|
||
|
||
**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
|
||
{
|
||
"name": "Köttfärssås",
|
||
"description": "En klassisk…",
|
||
"instructions": "Stek löken…",
|
||
"ingredients": [
|
||
{
|
||
"rawName": "köttfärs",
|
||
"quantity": 500,
|
||
"unit": "g",
|
||
"note": null,
|
||
"suggestions": [
|
||
{ "productId": 12, "productName": "Köttfärs", "score": 100 },
|
||
{ "productId": 34, "productName": "Blandfärs", "score": 65 },
|
||
{ "productId": 56, "productName": "Nötfärs", "score": 55 }
|
||
]
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
#### 3. Frontend: `/recipes/import` page
|
||
|
||
**Komponenter:**
|
||
- `ImportRecipePage.tsx` — Main client component (3-steps state machine)
|
||
- Använder `/api/parse-markdown-proxy` för backend-anrop (Next.js proxy)
|
||
|
||
**Steg 1: Klistra in Markdown**
|
||
- `<textarea>` för råtext
|
||
- Knapp: "Tolka recept" → POST /api/parse-markdown-proxy
|
||
- Error handling med svenska meddelanden
|
||
|
||
**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
|
||
|
||
**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 }
|
||
]
|
||
}
|
||
```
|
||
- Efter framgång: navigera till receptlistan
|
||
|
||
**Enhetsstöd i UI:**
|
||
- 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]
|
||
}
|
||
```
|
||
|
||
### Användning i `getInventoryPreview()`
|
||
|
||
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
|
||
```
|
||
|
||
---
|
||
|
||
## 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
|
||
FROM node:22-alpine AS converter-build
|
||
# Bygg recipe-document-converter biblioteket
|
||
```
|
||
|
||
**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)
|
||
```
|
||
|
||
**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
|
||
docker compose build recipe-api
|
||
docker compose up -d recipe-api
|
||
```
|
||
|
||
### Frontend-Dockerfile
|
||
|
||
Standard Next.js build → standalone output
|
||
|
||
### Miljövariabler
|
||
|
||
Konfigureras via `.env` eller `docker compose up`:
|
||
- `DATABASE_URL` — MariaDB-anslutning (backend)
|
||
- `PORT` — Backend port (default 8080)
|
||
- Ev. Caddy-konfiguration
|
||
|
||
---
|
||
|
||
## UTF-8 och lokalisering
|
||
|
||
- Database: utf8mb4
|
||
- Backend: Normaliseringsfunktion hanterar åäö
|
||
- Frontend: Svenska felmeddelanden och UI-text
|
||
- Endpoints: Svenska benämningar och kategori-namn
|
||
|
||
---
|
||
|
||
## Säkerhet & Utbyggbarhet
|
||
|
||
- **Ingen auth i grundutförande** (kan enkelt byggas på)
|
||
- **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)
|
||
- Multi-user support
|
||
- Shoppinglistor
|
||
- Recept-delning
|
||
- Nutrition facts
|
||
- Allergi-tracking
|