Refactor technical documentation for clarity and updates
- Updated "teknisk_beskrivning_flutter.md" to streamline content and remove outdated sections, focusing on architecture, environment, and recent technical additions. - Enhanced "migrering-MSI.md" with post-migration updates and clarifications for target audience. - Revised "produktlansering.md" to serve as a release checklist, ensuring it complements existing documentation without duplication. Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -1,456 +1,78 @@
|
||||
# Teknisk Beskrivning - Flutter Frontend
|
||||
|
||||
Viktigt att komma ihåg vid implementering av nya funktioner och kodning är att inte använda Windows-sökvägar. Använd inte `c:/dev/recipe-app/...` eftersom bygg- och testmiljön är på en remote Ubuntu-server. Utveckling sker lokalt och test samt drift sker på remote server. Säkerställ att inga absoluta Windows-sökvägar används i koden, för att stödja bygg och drift på Linux/Ubuntu.
|
||||
Detta dokument ar teknisk referens for systemadministratorer och programmerare.
|
||||
Mallet ar en sammanhallen teknisk sanning utan duplicerade roadmap- eller anvandarsektioner.
|
||||
|
||||
## Senaste ändringar (2026-05-01, session 3)
|
||||
## Dokumentstatus (2026-05-03)
|
||||
|
||||
### Separering av AI-chip och produktsuggestions-chip
|
||||
- Fokus: arkitektur, drift och kanda gotchas.
|
||||
- User guide finns i `README.md`.
|
||||
- Planering finns i `next_steps_flutter.md`.
|
||||
|
||||
**Problem:** AI-chipet visade felaktigt produktnamnsförslag som om de vore kategoriförslag, vilket skapade förvirring när användaren såg "AI-förslag: Dryck Multivitamin" (ett produktnamn) istället för en kategori.
|
||||
## Viktigt om miljo
|
||||
|
||||
**Lösning:**
|
||||
- **Blå chip** "Förslag: [produktnamn]" — när systemet hittat en trolig produkt via ordmatchning (inga AI-anrop inblandade). Klick väljer produkten.
|
||||
- **Grön chip** "AI-kategori: [kategoriväg]" — när AI:n föreslagit en kategori från databasen. Klick öppnar produktpickern filtrerad på den kategorin.
|
||||
Anvand inte absoluta Windows-sokvagar i kod eller scripts.
|
||||
Bygg och drift sker pa Linux/Ubuntu i containeriserad miljo.
|
||||
|
||||
**Kodändringar:**
|
||||
- `aiLabel` beräknas enbart från `categorySuggestionName`/`categorySuggestionPath` (kategoriförslag).
|
||||
- Nytt fält `suggestedProductLabel` för produktsuggestions-chip.
|
||||
- Separata villkor och UI-block för de två chipen i `_EditDialogState.build()`.
|
||||
## Senaste tekniska tillagg
|
||||
|
||||
### Produktnamns-normalisering
|
||||
|
||||
**Problem:** Kvittonamn i VERSALER (t.ex. "APRIKOSMARMELAD 284G") såg oprofessionella ut i UI:n.
|
||||
|
||||
**Lösning:** Ny funktion `_normalizeProductName()` som tillämpar smarta regler:
|
||||
- Token med `/` (förkortningar) lämnas i versaler: `KY/KAL/LE/TO`
|
||||
- Token som börjar med siffra (mängd/storlek) görs till gemener: `284G` → `284g`, `12X85G` → `12x85g`
|
||||
- Övriga token: första bokstav versal, resten gemen: `APRIKOSMARMELAD` → `Aprikosmarmelad`, `JUICE TROPISK` → `Juice Tropisk`
|
||||
|
||||
**Implementering:**
|
||||
- Top-level-funktion i `receipt_import_tab.dart`
|
||||
- Tillämpas när "Ny produkt"-fältet prefylls: `_newProductNameCtrl.text = _normalizeProductName(widget.current.productName ?? widget.item.rawName)`
|
||||
|
||||
### AI-kategorisering — validering i backend
|
||||
|
||||
**Problem:** Användaren rapporterade att AI föreslog kategorin "Dryck Multivitamin", som inte fanns i databasen.
|
||||
|
||||
**Undersökning:**
|
||||
- Backend-AI:n (`ai.service.ts`) validerar redan att `categoryId` finns i `categories`-listan och faller tillbaka på "Övrigt" om inte.
|
||||
- Problemet var att frontend visade produktnamnsförslag som om de vore kategoriförslag.
|
||||
|
||||
**Lösning:**
|
||||
- Separering av chipen (se ovan) gör det tydligt att AI-kategoriförslag alltid kommer från databasen.
|
||||
|
||||
---
|
||||
|
||||
## Senaste ändringar (2026-05-01, session 2)
|
||||
|
||||
### Tvåstegs-picker: Kategori → Produkt
|
||||
|
||||
Problembeskrivning: AI:n kan föreslå en kategori men produktpickern sökte bara på produktnamn, vilket gav nollresultat när kategorinamnet matades in som söktext. Lösningen är ett nytt separat arbetsflöde för produktval via kategoriträdet.
|
||||
|
||||
**Ny widget: `lib/core/ui/category_then_product_picker.dart`**
|
||||
|
||||
`CategoryThenProductPicker.show()` är en statisk metod som orkestrerar hela flödet:
|
||||
|
||||
1. **Steg 1 — Välj kategori:** Öppnar ett bottenark (`_CategoryPickerSheet`) med hela kategoriträdet (L1 → L2 → L3). Trädet är sökbart — sökfältet filtrerar till matchande lövnoder och visar hela sökvägen som brödsmula (t.ex. *Mat > Frukt & Grönt > Äpplen*).
|
||||
2. **Steg 2 — Välj produkt:** Öppnar `ProductPickerField.showSheet()` filtrerad på alla produkter som tillhör den valda kategorin **eller någon av dess ättlingar** (L1 samlar alltså in L2- och L3-produkter rekursivt via `_collectIds()`).
|
||||
|
||||
**Inbyggt AI-stöd:** Om `preselectedCategoryId` skickas in (från AI-förslaget) hoppar `show()` direkt till steg 2 — kategoriträdet visas aldrig. Om kategorin inte hittas i trädet faller den tillbaka till att visa trädet.
|
||||
|
||||
**Fallback för L1/L2-noder:** Mellanliggande noder (L1, L2) som inte är löv har en liten "Välj"-knapp till höger i raden. Klick på kategorinamnet/pilen expanderar/kollapsar som vanligt; klick på "Välj" väljer kategorin och öppnar produktpickern direkt.
|
||||
|
||||
**Trädknapp i redigeringsdialogen:** Bredvid det vanliga produktsökfältet finns nu en `OutlinedButton` med trädikon (🌳). Klick öppnar tvåstegs-pickern utan AI-förval.
|
||||
|
||||
### Skapa ny privat produkt i importflödet
|
||||
|
||||
Om inget i produktlistan matchar kvittoradens vara kan användaren skapa en egen produkt direkt från produktpickern.
|
||||
|
||||
**Flöde:**
|
||||
1. Välj kategori (via trädet eller AI-direkthopp).
|
||||
2. Produktpickern visas med en **"Skapa ny"**-knapp i rubrikraden.
|
||||
3. En enkel dialog öppnas med ett namnfält (förfyllt med söksträngen om sådan finns).
|
||||
4. Vid bekräftelse anropas `POST /products/private` — produkten skapas som privat och user-scopad.
|
||||
5. Den nya produkten läggs till i den lokala produktlistan och väljs direkt.
|
||||
|
||||
**Privata produkter — arkitektur:**
|
||||
|
||||
| | Globala produkter | Privata produkter |
|
||||
|---|---|---|
|
||||
| Endpoint (hämta) | `GET /products` | `GET /products/mine` |
|
||||
| Endpoint (skapa) | `POST /products` (admin) | `POST /products/private` (alla inloggade) |
|
||||
| Synlighet | Alla användare | Bara ägaren |
|
||||
| `isPrivate` | `false` | `true` |
|
||||
| `normalizedName` | `normalize(name)` | `private:{userId}:{normalize(name)}` |
|
||||
| Kategori | Global (admin-only) | Väljs vid skapandet, global kategori |
|
||||
|
||||
Importfliken laddar globala och privata produkter parallellt via `Future.wait` och slår ihop dem till en gemensam `_products`-lista. Den lokala `_localProducts`-listan i `_EditDialogState` utökas om en ny produkt skapas under dialogen, utan att en ny nätverksanrop krävs.
|
||||
|
||||
### Redigeringsdialog — sammanfattning av alla fält
|
||||
|
||||
`_EditDialog` i `receipt_import_tab.dart` innehåller nu:
|
||||
- **AI-chip (grön):** Klickbar. Om AI föreslog en matchad produkt direkt → väljer den. Om AI föreslog en kategori → öppnar produktpickern filtrerad på den kategorin (utan att visa trädet).
|
||||
- **Destinationsväljare:** `SegmentedButton` — Inventarie eller Baslager.
|
||||
- **Produktfält + trädknapp:** `ProductPickerField` (fritext-sökning) + knapp för att öppna tvåstegs-picker.
|
||||
- **Antal/Enhet:** Visas bara vid Inventarie-destination.
|
||||
|
||||
### Designregel bekräftad: User-scope
|
||||
|
||||
User-scope-principen dokumenterades formellt i båda tekniska beskrivningarna (2026-05-01). Privata produkter är det första exemplet på mönstret för resurser som är varken globala (alla ser dem) eller fullt user-owned (bara ägaren ser dem):
|
||||
- `Product.isPrivate = true` + `Product.ownerId = userId`
|
||||
- `normalizedName`-prefix undviker databaskollision med globala produkter
|
||||
- Migration: `20260501000000_add_product_is_private
|
||||
|
||||
## Senaste ändringar (2026-05-01, session 1)
|
||||
|
||||
**Kvittoimport Fas 6b — komplett:**
|
||||
- Granskningssteg i `receipt_import_tab.dart` med per-rad checkbox, redigeringsdialog och destinationsväljare.
|
||||
- Destinationsväljare: **Inventarie** eller **Baslager** via `SegmentedButton<_Destination>` i redigeringsdialogen.
|
||||
- Vid vald destination **Baslager** dolt antal/enhet-fält med informationstext.
|
||||
- Inventarie-flöde: skapar ny post eller slår ihop (PATCH quantity) med befintlig post. Förhandsvisning av ny mängd visas i listan.
|
||||
- Baslager-flöde: lägger till produkten om den inte redan finns. Befintliga baslagerprodukter visas med orange chip.
|
||||
- Snackbar visar separat räkning: skapade/sammanslagna i inventarie + tillagda/hoppa-över i baslager.
|
||||
- `_loadInventory()` laddar nu inventarie och baslager i parallell via `Future.wait`.
|
||||
- AI-kategorisuggestion chip (grön) visas för premium-användare i granskningslistan.
|
||||
|
||||
## Senaste ändringar (2026-04-25)
|
||||
|
||||
**Arkitektur- och UX-förbättringar:**
|
||||
- Grid-vy för recept: Kolumnval (2/4/6/8) via ikon i AppShell, med Riverpod-provider och SharedPreferences.
|
||||
- RecipesScreen är nu body-only, ingen egen Scaffold/AppBar.
|
||||
- AppShell visar grid-ikon endast på /recipes.
|
||||
- Buggfix: Produktväljaren i pantry/inventarie (ProductPickerField) — bottenark implementeras.
|
||||
- Kodkvalitet: Inga absoluta Windows-sökvägar.
|
||||
|
||||
**Tekniska förbättringar:**
|
||||
- Förbättrad bildhantering med normalisering av URL:er, fallback och diagnostikloggning.
|
||||
- Robustare importflöde för recept med stöd för PDF, bild och ICA-länkar.
|
||||
- User-scope för pantry och matplan: Alla baslager- och matplansdata är nu per användare.
|
||||
|
||||
## Syfte och mål
|
||||
- Isolerad Flutter-baserad frontend i separat Docker-service.
|
||||
- Web först, men med arkitektur som kan återanvändas för Android/iOS.
|
||||
- Stegvis migrering av funktioner från befintlig Next.js-frontend.
|
||||
- Sessionpersistens i klienten for pagande kvittoimport.
|
||||
- Forbattrad inferens for antal/forpackning (inklusive uttryck som `2st`).
|
||||
- Tydlig separation i UI mellan AI-kategoriforslag och produktforslag.
|
||||
- Ingen server-side persistens introducerad for kvittosession.
|
||||
|
||||
## Arkitektur
|
||||
|
||||
### Lager
|
||||
- **Presentation:** Skärmar och widgets i `flutter/lib/features/*/presentation`.
|
||||
- **State/Application:** Riverpod providers/notifiers i `flutter/lib/features/*/data`.
|
||||
- **Data/API:** `ApiClient` i `flutter/lib/core/api`.
|
||||
- **Platform abstraction:** Token storage interface i `flutter/lib/core/platform`.
|
||||
- Presentation: `flutter/lib/features/*/presentation`
|
||||
- State/Application: Riverpod providers/notifiers i `flutter/lib/features/*/data`
|
||||
- Data/API: `flutter/lib/core/api`
|
||||
- Platform abstraction: token storage i `flutter/lib/core/platform`
|
||||
|
||||
### Routing
|
||||
- GoRouter i [flutter/lib/core/router/app_router.dart](flutter/lib/core/router/app_router.dart).
|
||||
- Nuvarande routes:
|
||||
- `/login` — loginskärm
|
||||
- `/recipes` — receptlista (ShellRoute med AppShell)
|
||||
- `/recipes/create` — nytt recept, utanför ShellRoute
|
||||
- `/recipes/:id` — receptdetalj, utanför ShellRoute
|
||||
- `/recipes/:id/edit` — redigera recept, utanför ShellRoute
|
||||
- `/profile` — profil (ShellRoute med AppShell)
|
||||
- `/recipes/create` måste vara listad före `/recipes/:id` i routelistan för att undvika konflikt.
|
||||
- Detaljsidor (detalj, skapa, redigera) ligger utanför ShellRoute för att få full-screen med automatisk back-knapp.
|
||||
- GoRouter i `flutter/lib/core/router/app_router.dart`
|
||||
- ShellRoute for huvudnavigation
|
||||
- Fullscreen-routes for skapa/redigera/detalj
|
||||
|
||||
### Auth
|
||||
- Login endpoint: `POST /api/auth/login`.
|
||||
- Login via `POST /api/auth/login`
|
||||
- Token-falt: `accessToken`
|
||||
- Auth-gate i router med redirect logik
|
||||
- `guardedApiCall()` hanterar logout vid 401
|
||||
|
||||
## Kända fallgropar och API-gotchas
|
||||
## API- och kontraktsprinciper
|
||||
|
||||
### Designregel: User-scope vid ny funktionalitet
|
||||
- Flutter foljer backend-kontrakt, ingen lokal speciallogik for matchning.
|
||||
- 2xx-svar accepteras generellt i importanrop (inte hardkodning till enbart 200).
|
||||
- Kvittoimport och receptimport ar separata floden med olika svarstyper.
|
||||
|
||||
> **Regel:** Kontrollera alltid om ny data tillhör en specifik användare. Om ja — data **måste** ha user-scope i backend (`userId`-fält, filtrering i service) och Flutter-klienten får **inte** returnera/visa andra användares data.
|
||||
>
|
||||
> Resurser och deras scope:
|
||||
> | Resurs | Scope |
|
||||
> |---|---|
|
||||
> | Produkter (globala) | Publik — `GET /products` |
|
||||
> | Produkter (privata) | Per användare — `GET /products/mine`, `POST /products/private` |
|
||||
> | Kategorier | Global (admin-only att skapa) |
|
||||
> | Inventarie | Per användare — alltid filtrera på `userId` |
|
||||
> | Baslager | Per användare — alltid filtrera på `userId` |
|
||||
> | Matplan | Per användare — alltid filtrera på `userId` |
|
||||
> | Recept | Per användare (ägare) eller delat |
|
||||
>
|
||||
> I Flutter: ladda user-scopad data med token, kombinera globala och egna produktlistor vid behov.
|
||||
## Kvittoimport - tekniska noter
|
||||
|
||||
### Flutter Web och PDF MIME-typ
|
||||
- Flutter Web skickar PDF-filer med MIME-typ `application/octet-stream` istället för `application/pdf`.
|
||||
- Backend (`receipt-import.controller.ts`) måste tillåta båda: `application/pdf` och `application/octet-stream` i `ALLOWED_MIMES`.
|
||||
- Symptom om detta saknas: HTTP 400 "Otillåten filtyp. Använd JPEG, PNG, WebP eller PDF."
|
||||
|
||||
### NestJS @Post() returnerar HTTP 201 som standard
|
||||
- NestJS returnerar 201 (Created) för alla `@Post()`-endpoints som standard.
|
||||
- Flutter-klienten (`ImportRepository`) accepterar nu alla 2xx-svar (`statusCode < 200 || statusCode >= 300`).
|
||||
- Alternativt: lägg till `@HttpCode(200)` på backend-controllern för att returnera 200.
|
||||
- Detta gjordes i `receipt-import.controller.ts` (2026-04-30).
|
||||
|
||||
### Kvittoimport — backend och arkitektur
|
||||
- `/receipt-import` hanteras av en **dedikerad** `ReceiptImportController` och `ReceiptImportService`, **inte** av `QuickImportController`.
|
||||
- `ReceiptImportService` använder **Mistral AI** för att parsa kvitton till strukturerade `ParsedReceiptItem[]` direkt — inget markdown.
|
||||
- Svaret från `/receipt-import` är en array av `ParsedReceiptItem`, inte `{ markdown: "..." }`.
|
||||
- `{ markdown: "..." }` är svaret från `/quick-import` (receptimport) — inte kvittoimport.
|
||||
|
||||
## Övrigt
|
||||
- **Kvittoimport (Fas 6b):** ✅ Klar (2026-05-01) — granskningssteg, destination-väljare, merge och spara till inventarie/baslager.
|
||||
- **Bildimport:** Säkerställa att containrar är uppdaterade med senaste kod och att diagnostikloggar syns vid felsökning.
|
||||
- **Adminfunktioner:** Avancerad AI-integration och ytterligare adminfunktioner planeras men är ej migrerade.
|
||||
|
||||
## Syfte och mål
|
||||
- Isolerad Flutter-baserad frontend i separat Docker-service.
|
||||
- Web först, men med arkitektur som kan återanvändas för Android/iOS.
|
||||
- Stegvis migrering av funktioner från befintlig Next.js-frontend.
|
||||
|
||||
## Relaterade dokument
|
||||
- [next_steps_flutter.md](next_steps_flutter.md)
|
||||
- [README.md](README.md)
|
||||
- [teknisk_beskrivning.md](../TEKNISK_BESKRIVNING.md) (för backend-kontext)
|
||||
|
||||
## Arkitektur
|
||||
|
||||
### Lager
|
||||
- **Presentation:** Skärmar och widgets i `flutter/lib/features/*/presentation`.
|
||||
- **State/Application:** Riverpod providers/notifiers i `flutter/lib/features/*/data`.
|
||||
- **Data/API:** `ApiClient` i `flutter/lib/core/api`.
|
||||
- **Platform abstraction:** Token storage interface i `flutter/lib/core/platform`.
|
||||
|
||||
### Routing
|
||||
- GoRouter i [flutter/lib/core/router/app_router.dart](flutter/lib/core/router/app_router.dart).
|
||||
- Nuvarande routes:
|
||||
- `/login` — loginskärm
|
||||
- `/recipes` — receptlista (ShellRoute med AppShell)
|
||||
- `/recipes/create` — nytt recept, utanför ShellRoute
|
||||
- `/recipes/:id` — receptdetalj, utanför ShellRoute
|
||||
- `/recipes/:id/edit` — redigera recept, utanför ShellRoute
|
||||
- `/profile` — profil (ShellRoute med AppShell)
|
||||
- `/recipes/create` måste vara listad före `/recipes/:id` i routelistan för att undvika konflikt.
|
||||
- Detaljsidor (detalj, skapa, redigera) ligger utanför ShellRoute för att få full-screen med automatisk back-knapp.
|
||||
|
||||
### Auth
|
||||
- Login endpoint: `POST /api/auth/login`.
|
||||
- Backend kontrakt använder `username` + `password`.
|
||||
- Token-fält i svar: `accessToken`.
|
||||
- Token lagras via `ITokenStorage` (web implementation med SharedPreferences).
|
||||
- Auth-gate i router: utloggad användare redirectas till `/login`, inloggad redirectas från `/login` till `/recipes`.
|
||||
- Splash-skärm (`/`) visas medan auth-state läses från storage vid app-start.
|
||||
- `guardedApiCall()` i `flutter/lib/core/api/guarded_api_call.dart` hanterar automatisk logout vid 401.
|
||||
|
||||
### API-lager
|
||||
- `ApiClient` i `flutter/lib/core/api/api_client.dart` exponerar: `getJson`, `postJson`, `patchJson`, `putJson`, `deleteJson`.
|
||||
- Centraliserad HTTP-felklassning: 401 -> `ApiErrorType.unauthorized`, 403 -> `forbidden`, 5xx -> `server`, nätverksfel -> `network`.
|
||||
- `ApiException` i `flutter/lib/core/api/api_exception.dart` är den enda feltypen som propageras från repositories.
|
||||
- `mapErrorToUserMessage(error, context)` i `flutter/lib/core/api/api_error_mapper.dart` översätter fel till lokaliserade användarmeddelanden. Tar numera `BuildContext` som andra argument för att hämta korrekt språk från `AppLocalizations`.
|
||||
|
||||
### Lokalisering
|
||||
- Flutter `flutter_localizations` + `intl` tillagda i `pubspec.yaml` med `generate: true`.
|
||||
- Konfigurationsfil: [flutter/l10n.yaml](flutter/l10n.yaml) med `synthetic-package: false`.
|
||||
- Källsträngar i [flutter/lib/l10n/app_sv.arb](flutter/lib/l10n/app_sv.arb) och [flutter/lib/l10n/app_en.arb](flutter/lib/l10n/app_en.arb).
|
||||
- Genererade filer hamnar i `flutter/lib/l10n/generated/` och checkas inte in.
|
||||
- Hjälpare `context.l10n` i [flutter/lib/core/l10n/l10n.dart](flutter/lib/core/l10n/l10n.dart).
|
||||
- `MaterialApp.router` konfigurerad med `localizationsDelegates`, `supportedLocales` och `locale: const Locale('sv')`.
|
||||
- Dockerfilen kör `flutter gen-l10n` innan `flutter build web` för att generera Dart-koden i containerbygget.
|
||||
- Regressionstest i [flutter/test/core/swedish_strings_regression_test.dart](flutter/test/core/swedish_strings_regression_test.dart) misslyckas om vanliga ASCII-varianter (Välj, Lägg, Försök, etc.) dyker upp igen i `lib/`.
|
||||
|
||||
#### Användning av lokalisering
|
||||
För att lägga till en ny lokaliserad sträng:
|
||||
1. Lägg till nyckeln i `app_sv.arb` (och `app_en.arb`).
|
||||
2. Kör `flutter gen-l10n` lokalt (eller låt Docker-bygget göra det).
|
||||
3. Använd `context.l10n.dinNyckel` i widgetkoden.
|
||||
|
||||
För felsträngar från API: använd alltid `mapErrorToUserMessage(error, context)` — lägg inte in hårdkodade strängar.
|
||||
|
||||
## Vad som är gjort
|
||||
|
||||
### Infrastruktur
|
||||
- Ny compose override: [compose.flutter.yml](compose.flutter.yml).
|
||||
- Ny Flutter-service: `recipe-flutter`.
|
||||
- Service ansluten till nätverk:
|
||||
- `recipe-internal` (backend access)
|
||||
- `proxy` (external Caddy reachability)
|
||||
- Flutter-container serverar web build via intern Caddy.
|
||||
- Exponering via extern Caddy med site `test.gynther.se` -> `recipe-flutter:5000`.
|
||||
- API-anrop går same-origin via `/api` och proxas internt till `recipe-api:8080`.
|
||||
|
||||
### Inventarie (2026-04-22)
|
||||
- Filtrering per plats (alla/kyl/frys/skåfferi) via `inventoryLocationFilterProvider`.
|
||||
- Sortering (namn A-Ö, bäst före stigande/fallande) via `inventorySortFilterProvider`.
|
||||
- Riverpod-query (`InventoryQuery`) skickar `location` och `sort` som queryparametrar till backenden.
|
||||
- Alla felmeddelanden går via `mapErrorToUserMessage(error, context)`.
|
||||
|
||||
### Baslager/Pantry (2026-04-22)
|
||||
- Pantry-produkter grupperas nu på kategori utifrån backend-relationen `categoryRef` (rekursiv `parent`-kedja -> `categoryPath`), med fallback till legacy `product.category` och sist `Övrigt`.
|
||||
- `PantryProduct` har `categoryId` och `categoryPath` som parsas från API-svaret.
|
||||
|
||||
### Backend: User-scope för pantry och matplan (2026-04-22)
|
||||
- `PantryItem` och `MealPlanEntry` är nu user-scopade i Prisma-schemat.
|
||||
- `pantry`-controller/service filtrerar alltid per `userId` från JWT.
|
||||
- `meal-plan`-controller/service filtrerar alltid per `userId`; `inventoryCompare` använder inloggad användares pantry.
|
||||
- Migration: `20260422130000_user_scope_pantry_meal_plan` applicerad på server.
|
||||
- Next.js-frontenden krävde inga funktionella ändringar (förfrågningar går redan via auth-proxy).
|
||||
|
||||
## Arkitektur
|
||||
### Lager
|
||||
- Presentation: skarmar och widgets i `flutter/lib/features/*/presentation`.
|
||||
- State/Application: Riverpod providers/notifiers i `flutter/lib/features/*/data`.
|
||||
- Data/API: `ApiClient` i `flutter/lib/core/api`.
|
||||
- Platform abstraction: token storage interface i `flutter/lib/core/platform`.
|
||||
|
||||
### Routing
|
||||
- GoRouter i [flutter/lib/core/router/app_router.dart](flutter/lib/core/router/app_router.dart).
|
||||
- Nuvarande routes:
|
||||
- `/login` — loginskarm
|
||||
- `/recipes` — receptlista (ShellRoute med AppShell)
|
||||
- `/recipes/create` — nytt recept, utanfor ShellRoute
|
||||
- `/recipes/:id` — receptdetalj, utanfor ShellRoute
|
||||
- `/recipes/:id/edit` — redigera recept, utanfor ShellRoute
|
||||
- `/profile` — profil (ShellRoute med AppShell)
|
||||
- `/recipes/create` maste vara listad fore `/recipes/:id` i routelistan for att undvika konflikt.
|
||||
- Detaljsidor (detalj, skapa, redigera) ligger utanfor ShellRoute for att fa full-screen med automatisk back-knapp.
|
||||
|
||||
### Auth
|
||||
- Login endpoint: `POST /api/auth/login`.
|
||||
- Backend kontrakt anvander `username` + `password`.
|
||||
- Token-falt i svar: `accessToken`.
|
||||
- Token lagras via `ITokenStorage` (web implementation med SharedPreferences).
|
||||
- Auth-gate i router: utloggad anvandare redirectas till `/login`, inloggad redirectas fran `/login` till `/recipes`.
|
||||
- Splash-skarm (`/`) visas medan auth-state lases fran storage vid app-start.
|
||||
- `guardedApiCall()` i `flutter/lib/core/api/guarded_api_call.dart` hanterar automatisk logout vid 401.
|
||||
|
||||
### API-lager
|
||||
- `ApiClient` i `flutter/lib/core/api/api_client.dart` exponerar: `getJson`, `postJson`, `patchJson`, `putJson`, `deleteJson`.
|
||||
- Centralicerad HTTP-felklassning: 401 -> `ApiErrorType.unauthorized`, 403 -> `forbidden`, 5xx -> `server`, natverksfel -> `network`.
|
||||
- `ApiException` i `flutter/lib/core/api/api_exception.dart` ar den enda feltypen som propageras fran repositories.
|
||||
- `mapErrorToUserMessage(error, context)` i `flutter/lib/core/api/api_error_mapper.dart` oversatter fel till lokaliserade anvandarmedddelanden. Tar numera `BuildContext` som andra argument for att hämta korrekt sprak fran `AppLocalizations`.
|
||||
|
||||
### Lokalisering (2026-04-22)
|
||||
- Flutter `flutter_localizations` + `intl` tillagda i `pubspec.yaml` med `generate: true`.
|
||||
- Konfigurationsfil: [flutter/l10n.yaml](flutter/l10n.yaml) med `synthetic-package: false`.
|
||||
- Kallstrangar i [flutter/lib/l10n/app_sv.arb](flutter/lib/l10n/app_sv.arb) och [flutter/lib/l10n/app_en.arb](flutter/lib/l10n/app_en.arb).
|
||||
- Generade filer hamnar i `flutter/lib/l10n/generated/` och checkas inte in.
|
||||
- Hjalpare `context.l10n` i [flutter/lib/core/l10n/l10n.dart](flutter/lib/core/l10n/l10n.dart).
|
||||
- `MaterialApp.router` konfigurerad med `localizationsDelegates`, `supportedLocales` och `locale: const Locale('sv')`.
|
||||
- Dockerfilen kor `flutter gen-l10n` innan `flutter build web` for att generera Dart-koden i containerbygget.
|
||||
- Regressionstest i [flutter/test/core/swedish_strings_regression_test.dart](flutter/test/core/swedish_strings_regression_test.dart) failar om vanliga ASCII-varianter (Valj, Lagg, Forsok, etc.) dyker upp igen i `lib/`.
|
||||
|
||||
#### Anvandning av lokalisering
|
||||
For att lagga till en ny lokaliserad strang:
|
||||
1. Lagg till nyckeln i `app_sv.arb` (och `app_en.arb`).
|
||||
2. Kör `flutter gen-l10n` lokalt (eller lat Docker-bygget gora det).
|
||||
3. Anvand `context.l10n.dinNyckel` i widgetkoden.
|
||||
|
||||
For felstrangar fran API: anvand alltid `mapErrorToUserMessage(error, context)` — lagg inte in hardkodade strängar.
|
||||
|
||||
### Recipes
|
||||
- `GET /api/recipes` — receptlista.
|
||||
- `GET /api/recipes/:id` — receptdetalj inkl. ingredienser.
|
||||
- `POST /api/recipes` — skapa recept.
|
||||
- `PATCH /api/recipes/:id` — uppdatera recept (OBS: PATCH, inte PUT).
|
||||
- `DELETE /api/recipes/:id` — ta bort recept (returnerar 204, ingen body).
|
||||
- `POST /api/recipes/parse-markdown` — tolka Markdown, returnerar ParsedRecipe med ingrediensforslagslistor.
|
||||
- Flutter Recipe-model: `name`-fallback till `title`, null-safe parsing av Decimal-strang till double.
|
||||
|
||||
### Gemensamma UI-komponenter
|
||||
- `LoadingStateView`, `EmptyStateView`, `ErrorStateView` i `flutter/lib/core/ui/async_state_views.dart`.
|
||||
- `AppShell` i `flutter/lib/core/ui/app_shell.dart`: responsiv NavigationRail (bred) / NavigationBar (smal), delad AppBar med logout.
|
||||
|
||||
## Kända API-fallgropar (viktigt!)
|
||||
|
||||
> **Regel:** Kontrollera alltid HTTP-metod i [TEKNISK_BESKRIVNING.md i /recipe-app] innan en ny repository-metod implementeras.
|
||||
|
||||
| Operation | Korrekt metod | Fel som gjorts |
|
||||
|-------------------------|-----------------------------|------------------------------|
|
||||
| Uppdatera recept | `PATCH /api/recipes/:id` | `PUT` — ger 404 |
|
||||
| Uppdatera inventariepost| `PATCH /api/inventory/:id` | Använd PATCH |
|
||||
| Uppdatera matplan | `POST /api/meal-plan` (upsert) | Ingen PUT/PATCH |
|
||||
|
||||
Generell regel: NestJS-backenden använder `PATCH` för partiella uppdateringar, inte `PUT`. `PUT` exponeras inte på några resurser i detta projekt.
|
||||
- Granskningsflode med radredigering och destination (inventarie/baslager).
|
||||
- Produktval stoder tradbaserat kategori -> produkt-flode.
|
||||
- AI-kategorihint ar endast ett forslag; slutanvandaren validerar i granskningssteget.
|
||||
|
||||
## Drift och deploy
|
||||
### Build/run
|
||||
- Build argument i compose: `API_BASE_URL=/api`.
|
||||
- Build command:
|
||||
- `docker compose -f compose.yml -f compose.flutter.yml build recipe-flutter`
|
||||
- Run command:
|
||||
- `docker compose -f compose.yml -f compose.flutter.yml up -d --no-deps recipe-flutter`
|
||||
|
||||
### Rekommenderat kommandomönster
|
||||
|
||||
För att minska risken för fel startordning eller missförstånd mellan huvudappen och Flutter-spåret:
|
||||
|
||||
**När huvudappen ska vara uppe:**
|
||||
- `docker compose up -d recipe-db recipe-api recipe-frontend`
|
||||
|
||||
**När Flutter-klienten ska vara uppe:**
|
||||
- `docker compose -f compose.yml -f compose.flutter.yml up -d --no-deps recipe-flutter`
|
||||
|
||||
**När både huvudappen och Flutter testas parallellt:**
|
||||
1. Starta huvudappen med `compose.yml`.
|
||||
2. Starta sedan Flutter med override-filen `compose.flutter.yml`.
|
||||
|
||||
Viktigt:
|
||||
- `docker compose build ...` bygger bara image.
|
||||
- `docker compose up -d ...` krävs alltid för att containern faktiskt ska starta.
|
||||
### Bygg och start (Flutter)
|
||||
```bash
|
||||
docker compose -f compose.yml -f compose.flutter.yml build recipe-flutter
|
||||
docker compose -f compose.yml -f compose.flutter.yml up -d --no-deps recipe-flutter
|
||||
```
|
||||
|
||||
### Viktiga verifieringar
|
||||
- Compose merge valid:
|
||||
- `docker compose -f compose.yml -f compose.flutter.yml config`
|
||||
- Container reachable from external Caddy:
|
||||
- `docker exec caddy wget -S -O- http://recipe-flutter:5000`
|
||||
- Public endpoint:
|
||||
- `curl -I https://test.gynther.se`
|
||||
- Compose merge: `docker compose -f compose.yml -f compose.flutter.yml config`
|
||||
- Reachability via proxy/Caddy
|
||||
- Runtime-loggar for importfloden vid felsokning
|
||||
|
||||
## Viktiga beslut
|
||||
- `compose.yml` lämnas orörd; Flutter ligger i separat override-fil.
|
||||
- Flutter-web använder same-origin API (`/api`) för att undvika mixed-content.
|
||||
- Intern Caddy i Flutter-container hanterar static hosting + `/api` proxy.
|
||||
- Feature migration sker stegvis enligt [next_steps_flutter.md](next_steps_flutter.md).
|
||||
## Kanda fallgropar
|
||||
|
||||
## Kända fallgropar
|
||||
- Om `recipe-flutter` inte är i `proxy` nätverket blir det 502 från extern Caddy.
|
||||
- Om webbläsaren visar gammal JS kan gamla API-URL:er leva kvar i cache/service worker.
|
||||
- Login med e-post fungerar inte om backend förväntar sig användarnamn.
|
||||
- `recipe-flutter` kan stoppas av `docker compose down --remove-orphans` om kommandot körs utan override-filen och Flutter-spåret tidigare varit uppe.
|
||||
- En orphan-varning för `recipe-flutter` är normalt förväntad när man kör huvudappen med bara `compose.yml`; det betyder inte att backend eller Prisma är trasiga.
|
||||
- PDF-mime kan komma som `application/octet-stream` i Flutter Web.
|
||||
- Orphan-varning ar normal nar huvudstack och Flutter-stack kors separat.
|
||||
- Cache/service worker kan visa gammal frontend efter deploy.
|
||||
|
||||
### Orphan-varning i praktiken
|
||||
## Relaterade dokument
|
||||
|
||||
Om du ser en varning om orphan-containers under arbete med huvudappen betyder det oftast att `recipe-flutter` tidigare startats via:
|
||||
|
||||
- `docker compose -f compose.yml -f compose.flutter.yml up -d recipe-flutter`
|
||||
|
||||
och att du nu kör ett kommando som bara använder `compose.yml`.
|
||||
|
||||
Detta är normalt och ofarligt så länge du vet vilken stack du avser att köra.
|
||||
|
||||
Om `test.gynther.se` slutar svara efter städning med `--remove-orphans`, starta om Flutter-spåret med:
|
||||
|
||||
- `docker compose -f compose.yml -f compose.flutter.yml up -d --no-deps recipe-flutter`
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Nyheter och förbättringar (2026-04-22)
|
||||
|
||||
- **User-scope för pantry och matplan** — Flutter-klienten använder nu user-scopade endpoints för baslager och matplan. JWT används för filtrering i alla anrop.
|
||||
- **Robust bildimport** — Bild-URL normaliseras och skickas hela vägen till UI. Flutter hanterar både markdown och bild-url vid import.
|
||||
- **Importflöde** — Quick-import och receipt-import har förbättrats med robust multipart-hantering, timeout, och felhantering. Prefill av markdown och bild-url fungerar i Flutter.
|
||||
- **Flutter-parity** — Matplan, inventarie, baslager och receptflöden är nu fullt migrerade till Flutter med user-scope och robust felhantering.
|
||||
- **Felsökningslogg** — Se `../IMPORT_IMAGE_DEBUG_2026-04-22.md` för detaljerad felsökningshistorik kring bildimport och importflöde.
|
||||
|
||||
### Kända begränsningar
|
||||
- Kvittoimport (Fas 6b) är påbörjad men granskningssteg och bulk-spara återstår.
|
||||
- Bildimport kräver att containrar är uppdaterade med senaste kod — kontrollera att diagnostikloggar syns vid felsökning.
|
||||
- Vissa adminfunktioner och avancerad AI-integration är planerade men ej migrerade.
|
||||
|
||||
## Nyheter och tekniska förbättringar (2026-04-24)
|
||||
|
||||
- **Navigationsflöden:**
|
||||
- Navigationslänkar har lagts till mellan recept, inventarie, baslager och matplan för att förbättra användarupplevelsen och minska antalet klick.
|
||||
- Efter redigering av recept och konsumtion av inventariepost sker nu automatisk navigering till relevant vy.
|
||||
- Efter import av recept sker automatisk navigering till receptlistan.
|
||||
- **UX och UI:**
|
||||
- Receptdetaljvyn har fått förbättrad bakgrundsbildshantering och mjukare scrollning.
|
||||
- **Plattformsoberoende kod:**
|
||||
- Genomgång av kodbasen för att säkerställa att inga absoluta Windows-sökvägar används, vilket gör projektet robust för Linux/Ubuntu-baserad bygg- och driftmiljö.
|
||||
|
||||
Se även README.md och next_steps_flutter.md för mer detaljer om användarflöden och leveransplan.
|
||||
- `README.md` - anvandarguide.
|
||||
- `next_steps_flutter.md` - aktiv planering.
|
||||
- `../TEKNISK_BESKRIVNING.md` - backend/systemovergripande teknik.
|
||||
|
||||
Reference in New Issue
Block a user