diff --git a/.kilo/plans/1779550355555-hidden-mountain.md b/.kilo/plans/1779550355555-hidden-mountain.md
new file mode 100644
index 00000000..fbaf9322
--- /dev/null
+++ b/.kilo/plans/1779550355555-hidden-mountain.md
@@ -0,0 +1,146 @@
+# Plan: Projektanpassad Lighthouse-plan for Flutter-web
+
+## Mål
+Höja Lighthouse-resultaten för Flutter-webklienten i detta repo utan att bryta befintlig Docker/Caddy-deploy, med fokus på mätbar förbättring av prestanda, tillgänglighet och grundläggande SEO.
+
+## Kontext och nuläge (verifierat i projektet)
+- Flutter-web byggs redan i release-läge i `flutter/Dockerfile` via `flutter build web --release`.
+- Webb klient körs via Caddy i `flutter/Caddyfile` och har redan `encode gzip`.
+- `flutter/web/index.html` är minimal och innehåller redan korrekt viewport utan `user-scalable=no`.
+- `flutter/web/` innehåller idag endast `index.html` (ingen `robots.txt`/`sitemap.xml` i Flutter-mappen).
+- API-basurl injiceras redan korrekt med `--dart-define=API_BASE_URL=/api` via `compose.flutter.yml`.
+
+## Problem i befintlig flutter-lighthouse.md som inte passar repo exakt
+- Planen utgår delvis från Nginx/Apache, men projektet använder Caddy för Flutter-spåret.
+- Påståendet om `user-scalable=no` gäller inte nuvarande `flutter/web/index.html`.
+- Påståendet om ogiltig `robots.txt` kan inte verifieras i nuvarande Flutter-webmapp.
+- Förslag om tvingad HTML-renderer är för grovt; bör vara experiment med mätning och rollback-kriterier.
+
+## Strategi
+Arbeta i tre iterationer med baseline -> lågriskoptimeringar -> tyngre optimeringar, och låt varje steg vara datadrivet.
+
+---
+
+## Fas 1: Baseline och mätprotokoll (dag 1)
+1. Skapa en reproducerbar mätbaseline för både lokal container och produktionsdomän.
+2. Kör Lighthouse minst 3 gånger per miljö och ta median för:
+ - Performance score
+ - LCP
+ - TBT
+ - INP (om rapporteras)
+ - Transfer size / antal requests
+3. Dokumentera nuläge och tröskelvärden före ändringar.
+
+### Acceptanskriterier Fas 1
+- Baseline-tabell finns med mätvärden från 3 körningar per miljö.
+- Samma URL, samma nätprofil och samma emulering används konsekvent.
+
+---
+
+## Fas 2: Lågriskfixar med hög nytta (dag 1-2)
+
+### 2.1 Index och metadata (`flutter/web/index.html`)
+- Lägg till `lang="sv"` på ``.
+- Lägg till relevant `meta description` för appens huvudsakliga nytta.
+- Behåll `viewport` som den är (ingen ändring behövs kring zoom-blockering).
+
+### 2.2 Caddy-headerhygien (`flutter/Caddyfile`)
+- Behåll `encode gzip`.
+- Lägg till explicita cache-headers för hashade statiska assets (js/wasm/fonts) med lång TTL och immutable.
+- Lägg till kortare/konservativ cache för `index.html` så nya deploys slår igenom snabbt.
+- Lägg till säkerhetsheaders som är kompatibla med Flutter-web (minst grundnivå: `X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy`).
+
+### 2.3 Byggoptimering (`flutter/Dockerfile`)
+- Utvärdera `--no-source-maps` i produktionsbuild för mindre artifact-storlek.
+- Säkerställ att eventuella ändringar inte påverkar felsökning i miljö där sourcemaps behövs.
+
+### Acceptanskriterier Fas 2
+- Lighthouse visar förbättring i minst två nyckelmått (t.ex. LCP/TBT).
+- Ingen regress i appstart, routning eller API-proxy `/api/*`.
+- Build pipeline passerar oförändrat i Docker.
+
+---
+
+## Fas 3: Prestandaexperiment med tydlig rollback (dag 2-4)
+
+### 3.1 Rendering-strategi (CanvasKit vs HTML/Skwasm)
+- Kör A/B-test av rendererstrategi i en separat branch/buildvariant.
+- Mät skillnad i initial transfer size, LCP och renderingkvalitet på kritiska vyer.
+- Beslut endast baserat på mätdata + visuell/regressionskontroll.
+
+### 3.2 Deferred loading i tunga featureflöden
+- Identifiera kandidater för deferred imports (exempel: import/admin-vyer med hög kodvikt).
+- Introducera gradvis och validera att navigation inte blir ryckig.
+
+### 3.3 Bootstrap-laddning
+- Behåll asynkron laddning i `index.html` om mätning visar bäst resultat.
+- Undvik manuella hacks som injicerar canvaskit-script ad hoc utan evidens i mätning.
+
+### Acceptanskriterier Fas 3
+- Minst 15-25% förbättring i TBT eller tydlig minskning i JS-exekveringstid jämfört med baseline.
+- Ingen funktionell regress i kärnflöden (login, inventarie, recept, import).
+
+---
+
+## Fas 4: Tillgänglighet (A11y) med repo-fokus (dag 3-5)
+1. Inventera ikonknappar och actions i Flutter-kod och säkra `tooltip`/`semanticsLabel` där det saknas.
+2. Lägg till/justera `Semantics` för centrala actions (import, spara, ta bort, navigering).
+3. Verifiera tangentbordsnavigering i webbläsare för huvudflöden.
+
+### Acceptanskriterier Fas 4
+- Lighthouse Accessibility förbättras mätbart.
+- Inga nya fokusfällor eller förlorad keyboard-navigering introduceras.
+
+---
+
+## Fas 5: SEO-minimum för app-shell (dag 4-5)
+1. Säkerställ att titel och meta description är korrekta för startsidan.
+2. Besluta om `robots.txt` och `sitemap.xml` ska hanteras i Flutter-web, Caddy eller upstream-domänkonfiguration (inte antagande).
+3. Implementera endast den väg som matchar faktisk domänrouting i drift.
+
+### Acceptanskriterier Fas 5
+- Lighthouse SEO får förbättring från baseline.
+- Robots/sitemap-lösning är verifierad mot faktisk driftarkitektur.
+
+---
+
+## Fas 6: Säkerhet och CSP (dag 5)
+1. Introducera en pragmatisk CSP för Flutter-web i Caddy med minsta nödvändiga undantag.
+2. Testa särskilt att Flutter bootstrap, API-anrop och ev. externa resurser fungerar.
+3. Strama åt policyn iterativt istället för en aggressiv engångspolicy.
+
+### Acceptanskriterier Fas 6
+- Säkerhetsheaders levereras korrekt från Flutter-Caddy.
+- Ingen blockerad kärnfunktion pga CSP.
+
+---
+
+## Prioriterad genomförandeordning
+1. Fas 1 (baseline)
+2. Fas 2 (lågriskfixar)
+3. Re-mätning
+4. Fas 3 (prestandaexperiment)
+5. Fas 4 (tillgänglighet)
+6. Fas 5 (SEO-minimum)
+7. Fas 6 (CSP hardening)
+8. Slutlig Lighthouse-jämförelse och dokumentation
+
+## Definition of Done
+- Reproducerbar före/efter-mätning finns dokumenterad.
+- Performance, Accessibility och SEO har förbättrats jämfört med baseline.
+- Inga regressioner i Docker/Caddy-flödet eller appens kärnflöden.
+- Åtgärderna är anpassade till faktisk stack (Flutter + Caddy), inte generiska Nginx-råd.
+
+## Konkreta filer som sannolikt berörs vid implementation
+- `flutter/web/index.html`
+- `flutter/Caddyfile`
+- `flutter/Dockerfile`
+- ev. flera Flutter-vyer med knapp-/ikonsemantik under `flutter/lib/**`
+
+## Risker och motåtgärder
+- **Risk:** För aggressiv CSP bryter Flutter-bootstrap.
+ **Motåtgärd:** Iterativ policy + verifiering efter varje ändring.
+- **Risk:** Rendererbyte förbättrar vikt men försämrar visual fidelity.
+ **Motåtgärd:** A/B-test med tydliga rollback-kriterier.
+- **Risk:** Cache-policy gör deploys "stale".
+ **Motåtgärd:** Lång cache endast för fingerprintade assets, kort cache för `index.html`.
diff --git a/.kilo/plans/flutter-lighthouse.md b/.kilo/plans/flutter-lighthouse.md
new file mode 100644
index 00000000..c9ca0916
--- /dev/null
+++ b/.kilo/plans/flutter-lighthouse.md
@@ -0,0 +1,706 @@
+🚨 Kritiska Problem (Prioritet 1: Fixa Omedelbart)
+Dessa problem påverkar användarupplevelsen mest och bör åtgärdas först.
+
+1. Prestanda: Långsam inladdning & Stora Filer
+🔴 Problem:
+
+Total storlek: 2,978 KiB (för stor för en webbapp).
+
+/main.dart.js: 1,216 KiB (Flutter’s kompilade JavaScript).
+canvaskit.wasm: 1,592 KiB (Flutter’s CanvasKit-renderare).
+MaterialIcons-Regular.otf: 9.8 KiB (ikoner).
+
+Largest Contentful Paint (LCP) misslyckades (tidsgräns överskreds).
+Total Blocking Time (TBT) misslyckades (långa JavaScript-uppgifter blockerar huvudtråden).
+JavaScript-exekveringstid: 1.8s (för långt).
+🟢 Lösningar:
+A. Optimera Flutter för Web
+Flutter-webbappar är tyngre än traditionella webbappar på grund av CanvasKit. Här är hur du minskar storleken:
+
+
+Använd HTML-renderaren istället för CanvasKit
+
+CanvasKit ger bättre grafik men är mycket tyngre.
+Ändra i index.html:
+html
+Copy
+
+
+
+
+
+
+Fördel: Minskar storleken med ~1.5MB (WASM-filen laddas inte).
+Nackdel: Vissa avancerade animationer/efekter fungerar inte lika bra.
+
+
+Aktivera komprimering (GZIP/Brotli)
+
+Din server skickar okomprimerade filer.
+Lösning:
+
+Nginx: Lägg till i konfigurationen:
+nginx
+Copy
+
+gzip on;
+gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
+brotli on;
+brotli_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
+
+
+
+
+Apache: Använd mod_deflate eller mod_brotli.
+Cloudflare: Aktivera "Brotli Compression" i inställningarna.
+
+
+
+Cachea statiska resurser
+
+Problem: /main.dart.js och flutter_bootstrap.js har ingen cache-TTL.
+Lösning: Lägg till Cache-Control-headers:
+nginx
+Copy
+
+location ~* \.(js|css|png|jpg|jpeg|gif|ico|wasm|otf)$ {
+ expires 1y;
+ add_header Cache-Control "public, immutable";
+}
+
+
+
+
+Besparing: ~1.2MB vid upprepade besök.
+
+
+Dela upp JavaScript-koden (Code Splitting)
+
+Flutter laddar allt i en stor fil (main.dart.js).
+Lösning: Använd deferred imports för att ladda funktioner på begäran:
+dart
+Copy
+
+// I din Dart-kod:
+import 'package:flutter_web_plugins/flutter_web_plugins.dart';
+import 'package:my_app/receipt_import.dart' deferred as receipt_import;
+
+// Ladda endast när behövs:
+Future loadReceiptImport() async {
+ await receipt_import.loadLibrary();
+}
+
+
+
+
+Effekt: Minskar initial laddningstid.
+
+
+Optimera bilder
+
+Problem: Bilder laddas inte optimalt.
+Lösning:
+
+Använd flutter_image_compress för att komprimera bilder innan uppladdning.
+För webb: Använd med srcset för responsiva bilder.
+Exempel:
+html
+Copy
+
+
+
+
+
+
+
+
+
+
+
+
+
+B. Förbättra Laddningsordningen
+
+Fördröj laddning av icke-kritiska resurser
+
+Ladda canvaskit.wasm efter att sidan har renderats:
+html
+Copy
+
+
+
+
+
+
+
+Använd preload för kritiska resurser
+
+Lägg till i :
+html
+Copy
+
+
+
+
+
+
+
+
+
+2. Tillgänglighet: Grundläggande Problem
+🔴 Problem:
+
+[user-scalable="no"] i viewport-meta-taggen
+
+Varför det är dåligt: Användare med nedsatt syn kan inte zooma in.
+Lösning: Ta bort user-scalable="no":
+html
+Copy
+
+
+
+
+
+
+
+Saknas alt-texter på bilder
+
+Problem: Skärmläsare kan inte beskriva bilder.
+Lösning: Lägg till alt-texter i Flutter:
+dart
+Copy
+
+Image.network(
+ 'https://example.com/recept.jpg',
+ semanticLabel: 'Bild på lasagne med ost och tomatsås', // Alt-text
+),
+
+
+
+
+
+Saknas lang-attribut
+
+Problem: Skärmläsare vet inte vilket språk sidan använder.
+Lösning: Lägg till i :
+html
+Copy
+
+
+
+
+
+
+
+
+3. SEO: Grundläggande Problem
+🔴 Problem:
+
+Saknas meta description
+
+Varför det är dåligt: Sökmotorer visar ingen beskrivning i resultaten.
+Lösning: Lägg till i :
+html
+Copy
+
+
+
+
+
+
+
+Ogiltig robots.txt
+
+Problem: Din robots.txt innehåller HTML-kod istället för korrekt syntax.
+Lösning: Skapa en korrekt robots.txt:
+text
+Copy
+
+User-agent: *
+Allow: /
+Sitemap: https://recept.gynther.se/sitemap.xml
+
+
+
+
+
+
+⚠️ Viktiga Förbättringar (Prioritet 2: Fixa Inom 1-2 Veckor)
+Dessa problem påverkar användarupplevelsen och SEO, men är inte lika kritiska.
+
+1. Prestanda: JavaScript & Rendering
+🟡 Problem:
+
+Lång JavaScript-exekveringstid (1.8s)
+
+Orsak: Flutter’s main.dart.js tar 1.7s att parsas och exekveras.
+
+Minify CSS/JS misslyckades
+
+Flutter genererar redan minifierad kod, men du kan optimera vidare.
+
+🟢 Lösningar:
+
+
+Aktivera Flutter’s --release flagga
+
+Bygg appen med:
+bash
+Copy
+
+flutter build web --release
+
+
+
+
+Detta genererar optimerad och minifierad kod.
+
+
+Använd flutter build web --no-source-maps
+
+Source maps ökar filstorleken. Ta bort dem i produktion:
+bash
+Copy
+
+flutter build web --no-source-maps
+
+
+
+
+
+
+Fördröj laddning av icke-kritisk JavaScript
+
+Använd defer eller async för skript som inte behövs omedelbart:
+html
+Copy
+
+
+
+
+
+
+
+
+Reducera DOM-storlek
+
+Problem: Din sida har 21 element med en max-djup på 7 (acceptabelt, men kan optimeras).
+Lösning: Undvik onödiga Container-widgets i Flutter. Använd const widgets där möjligt.
+
+
+2. Tillgänglighet: Interaktiva Element
+🟡 Problem:
+
+Saknas focus-indikatorer
+
+Användare som navigerar med tangentbord ser inte vilka element som är fokuserade.
+
+Saknas aria-label på anpassade knappar
+
+Skärmläsare vet inte vad knapparna gör.
+
+🟢 Lösningar:
+
+Lägg till focus-stilar i CSS
+
+Exempel:
+css
+Copy
+
+button:focus, [tabindex="0"]:focus {
+ outline: 2px solid #4285F4;
+ outline-offset: 2px;
+}
+
+
+
+
+
+Använd semantics i Flutter för tillgänglighet
+
+Exempel för en knapp:
+dart
+Copy
+
+Semantics(
+ label: 'Importera kvitto',
+ button: true,
+ child: ElevatedButton(
+ onPressed: () => _importReceipt(),
+ child: Text('Importera'),
+ ),
+),
+
+
+
+
+
+Lägg till aria-label på ikonknappar
+
+Exempel:
+dart
+Copy
+
+IconButton(
+ icon: Icon(Icons.upload),
+ onPressed: () => _uploadFile(),
+ tooltip: 'Ladda upp kvitto', // Visas på hover
+ semanticsLabel: 'Ladda upp kvitto', // För skärmläsare
+),
+
+
+
+
+
+
+3. Säkerhet: Content Security Policy (CSP)
+🟡 Problem:
+
+Saknas CSP-header
+
+Din sida är sårbar för XSS-attacker (Cross-Site Scripting).
+
+🟢 Lösning:
+Lägg till en stark CSP i din serverkonfiguration:
+nginx
+Copy
+
+add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://www.gstatic.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://*.gstatic.com; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https://recept.gynther.se; frame-src 'none'; object-src 'none';";
+
+
+
+
+Förklaring:
+
+script-src: Tillåter skript från self och gstatic.com (för Flutter).
+unsafe-inline: Nödvändigt för Flutter (men försök minska användningen).
+img-src: Tillåter bilder från self och gstatic.com.
+
+
+📌 Mindre Problem (Prioritet 3: Fixa När Tid Finns)
+Dessa är förbättringar som inte är kritiska men kan förbättra UX och SEO.
+
+1. Prestanda: Bildoptimering
+
+Problem: Bilder laddas utan width och height (orsakar layout shifts).
+Lösning: Använd Image.network med explicit storlek:
+dart
+Copy
+
+Image.network(
+ 'https://example.com/recept.jpg',
+ width: 300,
+ height: 200,
+ fit: BoxFit.cover,
+),
+
+
+
+
+
+2. SEO: Structured Data
+
+Problem: Saknas schema.org-markup för recept (gör att Google kan visa "rich results").
+Lösning: Lägg till JSON-LD i :
+html
+Copy
+
+
+
+
+
+
+Effekt: Google kan visa receptkort i sökresultaten (ökad klickfrekvens).
+
+3. Tillgänglighet: Logisk Tab-Order
+
+Problem: Användare kan tabba till element som är dolda eller off-screen.
+Lösning: Använd FocusableAction i Flutter för att kontrollera tab-order:
+dart
+Copy
+
+FocusableActionDetector(
+ autofocus: true,
+ onFocusChange: (hasFocus) {
+ if (hasFocus) {
+ // Hantera focus
+ }
+ },
+ child: MyWidget(),
+),
+
+
+
+
+
+📊 Sammanfattning av Prioriteringar
+
+
+
+
+ Prioritet
+ Problem
+ Lösning
+ Tidsuppskattning
+ Impact
+
+
+
+
+ 1 (Kritisk)
+ Stora filer (2.9MB)
+ Byt till HTML-renderare, aktivera GZIP, cachea resurser
+ 1-2 timmar
+ ⭐⭐⭐⭐⭐ (Hög)
+
+
+ 1 (Kritisk)
+ Lång JavaScript-exekvering (1.8s)
+ Code splitting, defer non-critical JS
+ 2-4 timmar
+ ⭐⭐⭐⭐⭐ (Hög)
+
+
+ 1 (Kritisk)
+ user-scalable="no"
+ Ta bort från viewport-meta-taggen
+ 5 minuter
+ ⭐⭐⭐⭐ (Hög)
+
+
+ 1 (Kritisk)
+ Saknas meta description
+ Lägg till i
+ 5 minuter
+ ⭐⭐⭐ (Medel)
+
+
+ 1 (Kritisk)
+ Ogiltig robots.txt
+ Skapa korrekt robots.txt
+ 10 minuter
+ ⭐⭐⭐ (Medel)
+
+
+ 2 (Viktig)
+ Saknas CSP-header
+ Lägg till i serverkonfigurationen
+ 1 timme
+ ⭐⭐⭐⭐ (Hög)
+
+
+ 2 (Viktig)
+ Saknas alt-texter
+ Lägg till semanticLabel i Flutter
+ 1-2 timmar
+ ⭐⭐⭐ (Medel)
+
+
+ 2 (Viktig)
+ Lång DOM-parsning
+ Minska onödiga widgets
+ 1-2 timmar
+ ⭐⭐ (Låg)
+
+
+ 3 (Mindre)
+ Saknas structured data
+ Lägg till JSON-LD för recept
+ 1 timme
+ ⭐⭐ (Låg)
+
+
+
+
+
+🛠️ Konkreta Åtgärder (Steg-för-Steg)
+Steg 1: Fixa Prestanda (1-2 dagar)
+
+Byt till HTML-renderare (sparar ~1.5MB):
+
+Ändra index.html som visat ovan.
+
+Aktivera GZIP/Brotli på servern.
+Cachea statiska filer (1 år för JS/CSS).
+Bygg appen med --release:
+bash
+Copy
+
+flutter build web --release --no-source-maps
+
+
+
+
+Ladd upp den optimerade versionen till din server.
+
+Steg 2: Fixa Tillgänglighet (1 dag)
+
+Ta bort user-scalable="no" från viewport.
+Lägg till lang="sv" i .
+Lägg till alt-texter på alla bilder.
+Lägg till semanticsLabel på knappar och ikoner.
+
+Steg 3: Fixa SEO (1 dag)
+
+Lägg till meta description.
+Fixa robots.txt.
+Lägg till structured data för recept.
+
+Steg 4: Säkerhet (1 timme)
+
+Lägg till CSP-header i serverkonfigurationen.
+
+📈 Förväntade Resultat
+
+
+
+
+ Mätning
+ Nuvarande
+ Efter Fixar
+ Förbättring
+
+
+
+
+ Total storlek
+ 2,978 KiB
+ ~500-800 KiB
+ -70%
+
+
+ Laddningstid (LCP)
+ Timeout
+ < 2.5s
+ ✅ Fixad
+
+
+ JavaScript-exekvering
+ 1.8s
+ < 1s
+ -45%
+
+
+ Tillgänglighet
+ 82/100
+ 95+/100
+ +15 poäng
+
+
+ SEO
+ 6/10
+ 9/10
+ +50%
+
+
+ Säkerhet
+ Risk för XSS
+ Skyddad
+ ✅ Fixad
+
+
+
+
+
+🔍 Hur du Testar Förbättringarna
+
+Kör Lighthouse igen i Chrome DevTools (F12 → "Lighthouse").
+Testa på mobil (använd Chrome’s "Device Toolbar").
+Använd WebPageTest (webpagetest.org) för att mäta prestanda globalt.
+Testa tillgänglighet med:
+
+WAVE Evaluation Tool
+Skärmläsare (t.ex. NVDA eller VoiceOver).
+
+
+💡 Extra Tips för Flutter-Webbappar
+
+
+Använd flutter build web --target=lib/main_deferred.dart
+
+Förbättrar code splitting.
+
+
+Ladda Flutter asynkront
+
+Lägg till async i
+
+
+
+
+
+
+Använd flutter_tex för text-rendering
+
+Minskar beroendet av CanvasKit.
+
+
+Override Flutter’s default fonts
+
+Använd systemtypsnitt för snabbare laddning:
+css
+Copy
+
+@font-face {
+ font-family: 'Roboto';
+ src: local('Roboto'), local('Roboto-Regular');
+}
+
+
+
+
+
+
+🎯 Sammanfattning: Vad du ska göra nu
+
+Fixa de kritiska prestandaproblemen (HTML-renderare, GZIP, caching).
+Fixa tillgänglighetsproblemen (user-scalable, alt-texter, lang).
+Fixa SEO (meta description, robots.txt).
+Lägg till säkerhetsheaders (CSP).
+Testa och iterera med Lighthouse.
diff --git a/.kilo/plans/plan-multiplatform.md b/.kilo/plans/plan-multiplatform.md
new file mode 100644
index 00000000..abb43417
--- /dev/null
+++ b/.kilo/plans/plan-multiplatform.md
@@ -0,0 +1,118 @@
+# Plan: Anpassad multiplattformsplan for recipe-app
+
+## Mal
+Gora Flutter-klienten i `flutter/` till en riktig multiplattformsklient (web + Android + iOS) utan att bryta befintligt webbflode via Docker/Caddy, och med tydlig miljohantering for API-anrop pa mobil.
+
+## Nulagesanalys (projektanpassad)
+- Flutter-projektet ar i praktiken web-only just nu: `flutter/` saknar `android/` och `ios/`.
+- Webbbygget ar redan etablerat och stabilt via Docker:
+ - `flutter/Dockerfile` bygger `flutter build web --dart-define=API_BASE_URL=/api`.
+ - `compose.flutter.yml` och `flutter/Caddyfile` proxar `/api/*` till `recipe-api:8080`.
+- API-basurl hanteras redan med `--dart-define` (bra grund for multiplattform):
+ - `flutter/lib/core/api/api_client.dart`
+ - `flutter/lib/features/import/data/import_repository.dart`
+- Token-lagring ar forberedd for mobil men inte implementerad:
+ - `flutter/lib/core/platform/token_storage.dart`
+ - `flutter/lib/core/platform/platform_providers.dart` (har TODO om secure storage).
+
+## Viktig skillnad mot gamla planen
+- Ingen hardkodad `Config.apiUrl` med fasta domaner ska inforas som huvudlosning.
+- Projektet anvander redan `String.fromEnvironment('API_BASE_URL')`; vi behaller detta och utokar till mobil.
+- Befintlig Docker-setup for web ska inte ersattas, bara kompletteras med mobil-byggflode.
+
+## Foreslagen implementation
+
+### Fas 1: Aktivera plattformsstommar utan att rora webdeploy
+1. Skapa Android/iOS-mappar i `flutter/`:
+ - `flutter create --platforms android,ios .`
+2. Verifiera att webfiler och befintliga Dart-filer inte overskrivs pa ett destruktivt satt.
+3. Bekrafta att Docker-webbygget fortfarande fungerar oforandrat.
+
+**Leverabel:** Projektet innehaller `flutter/android/` och `flutter/ios/` samtidigt som webflodet ar intakt.
+
+### Fas 2: Plattformsaker konfiguration av API-basurl
+1. Standardisera API-konfiguration kring en enda kontraktspunkt:
+ - Behall `API_BASE_URL` via `--dart-define`.
+2. Satt tydliga miljoer:
+ - Web i Docker: `API_BASE_URL=/api` (som idag).
+ - Android emulator lokalt: t.ex. `http://10.0.2.2:8080/api` (vid lokal backend utan reverse proxy).
+ - Fysisk mobil/test/prod: publik HTTPS-url (doman som ar natbar utanfor Docker).
+3. Se over alla direkta API-basar i Flutter-koden sa att de gar via samma pattern (inga hardkodade hostnamn).
+
+**Leverabel:** Samma kodbas fungerar pa web och mobil genom miljoinjektering, inte forks av API-klient.
+
+### Fas 3: Mobilanpassad auth/tokenlagring
+1. Implementera `SecureTokenStorage` med `flutter_secure_storage` for mobil.
+2. Uppdatera `platform_providers.dart` till plattformsval:
+ - Web -> befintlig `WebTokenStorage`.
+ - Android/iOS -> `SecureTokenStorage`.
+3. Verifiera att inloggning/logout/session beter sig lika mellan web och mobil.
+
+**Leverabel:** JWT lagras sakert pa mobil, befintligt webbeteende bibehalls.
+
+### Fas 4: UI- och UX-hardning for mindre skarmar
+1. Identifiera skarmar med hog informationsdensitet (admin/import/tabeller).
+2. Lagg in responsiva brytpunkter med `LayoutBuilder`/`MediaQuery` dar det behovs.
+3. Prioritera funktionellt minimum for mobil i forsta iteration:
+ - Login
+ - Inventarie
+ - Receptlista
+ - Grundlaggande importfloden
+4. Markera admin-tunga vyer som sekundara om de inte ar mobilkritiska i fas 1.
+
+**Leverabel:** Nyckelfloden ar anvandbara pa telefon utan horisontell overflow eller blockerande layoutfel.
+
+### Fas 5: Build- och releaseflode (utan att blanda ihop med Docker-runtime)
+1. Dokumentera separata kommandon:
+ - Web (befintligt): Docker/compose.
+ - Android: `flutter build apk --release` (och ev. `appbundle`).
+ - iOS: `flutter build ios --release` (kraver macOS/Xcode).
+2. Behall principen: mobilappar kor inte i Docker; Docker far anvandas som byggmiljo dar det ar rimligt.
+3. Om CI ska byggas senare: separera web-jobb och mobil-jobb for tydlighet.
+
+**Leverabel:** Reproducerbar byggprocess for web och mobil med tydlig ansvarsskillnad.
+
+### Fas 6: Test- och verifieringsplan
+1. Statisk kvalitet:
+ - `flutter analyze`
+ - `flutter test`
+2. Plattformsverifiering:
+ - Web via befintlig container
+ - Android emulator + fysisk enhet
+ - iOS simulator (pa macOS)
+3. Natverksverifiering:
+ - Bekrafta att mobil kan na vald `API_BASE_URL` over HTTPS/CORS/proxyregler.
+4. Regression:
+ - Inloggning, token-refresh/logout
+ - CRUD i inventarie/recept
+ - Importendpoints med storre payloads
+
+**Leverabel:** Checklista med passerade verifieringspunkter innan distribution.
+
+## Konkreta filer som sannolikt berors
+- `flutter/pubspec.yaml` (nytt beroende for secure storage)
+- `flutter/lib/core/platform/platform_providers.dart`
+- `flutter/lib/core/platform/token_storage.dart` (ev. endast kontraktsjustering)
+- Ny fil: `flutter/lib/core/platform/secure_token_storage.dart`
+- Mobilplattformar som genereras: `flutter/android/**`, `flutter/ios/**`
+- Dokumentation: `README.md` och/eller `TEKNISK_BESKRIVNING.md` (kommandon och miljoexempel)
+
+## Risker och hantering
+- iOS-bygg kan inte verifieras i Windows/Linux-miljo -> hanteras med separat macOS-steg.
+- Hardkodade URL:er kan smyga sig in i featurekod -> hanteras med kodsok + central konfigpolicy.
+- UI-regression pa web vid responsiva andringar -> hanteras med web-regressionstest av kritiska vyer.
+
+## Prioriterad ordning for implementation
+1. Fas 1 (plattformsstommar)
+2. Fas 2 (API-konfiguration)
+3. Fas 3 (secure token storage)
+4. Fas 6 del 1 (analyze/test tidigt)
+5. Fas 4 (mobil UI-hardning)
+6. Fas 5 + Fas 6 slutlig verifiering och dokumentation
+
+## Definition of Done
+- Flutter-projektet bygger for web + android (och ios dar macOS finns).
+- Mobil och web anvander samma API-konfigmodell via `--dart-define`.
+- Mobil lagrar token sakert; webflodet ar oforandrat.
+- Minst nyckelfloden login/inventarie/recept/import ar verifierade pa mobil.
+- Dokumentationen beskriver exakt hur man bygger och kor respektive plattform i detta repo.
diff --git a/_archive/docs/SESSION_CHECKPOINT_2026-05-21.md b/_archive/docs/SESSION_CHECKPOINT_2026-05-21.md
new file mode 100644
index 00000000..34d9a828
--- /dev/null
+++ b/_archive/docs/SESSION_CHECKPOINT_2026-05-21.md
@@ -0,0 +1,87 @@
+# Session Checkpoint (2026-05-21)
+
+> Föregående checkpoint: [SESSION_CHECKPOINT_2026-05-12.md](SESSION_CHECKPOINT_2026-05-12.md)
+
+## Status
+
+- Arbetsytan är ren (`git status --short` gav ingen output).
+- Kritiska build-blockers för Flutter-l10n är åtgärdade.
+- Backend build + backend tester + Flutter tester verifierade gröna i denna session.
+
+## Klart i denna session
+
+### 1. Felsökning och fix av Docker-fel i Flutter `gen-l10n`
+
+**Problem:** Docker-bygg kraschade vid `flutter gen-l10n` p.g.a. ogiltig ARB-JSON och konflikt i locale-filer.
+
+**Åtgärder:**
+- `flutter/lib/l10n/app_en.arb` reparerad (felaktig JSON-struktur, saknade/utanförliggande nycklar).
+- Krock mellan engelska locale-filer hanterad (dubbla `en`-källor var en del av tidigare felsymptom).
+- `flutter gen-l10n` kördes om utan formatteringsfel.
+
+### 2. Fix av Flutter test-fel: saknad l10n-nyckel `required`
+
+**Problem:** `flutter test` föll på:
+- `The getter 'required' isn't defined for the type 'AppLocalizations'`
+- fel i `lib/features/admin/presentation/admin_users_panel.dart`.
+
+**Åtgärder:**
+- Återställde saknade nycklar i `flutter/lib/l10n/app_en.arb`:
+ - `required`
+ - `logoutAction`
+ - `adminAiDescription`
+ - `adminPagePrefix`
+- Synkade svenska ARB-filen och la till saknad nyckel:
+ - `profileDatabaseDescription`
+- Regenererade lokaliseringar med `flutter gen-l10n`.
+
+### 3. Kvalitetsverifiering
+
+Körda verifieringar:
+
+```bash
+# Backend
+cd backend
+npm run build
+npm run test
+
+# Flutter
+cd ../flutter
+flutter gen-l10n
+flutter test --reporter compact
+```
+
+**Resultat:**
+- Backend build: OK
+- Backend tests: OK (29/29 suites, 245/245 tester)
+- Flutter tests: OK (alla passerar)
+
+## Viktig kontext inför nästa session
+
+- Root-varningen från Flutter i Docker (`trying to run flutter as root`) är en varning och blockerar inte i sig.
+- Den blockerande orsaken var ARB/l10n-konsistens, inte root-varningen.
+- Nuvarande l10n-läge är stabilt efter regeneration.
+
+## Rekommenderad snabbstart imorgon
+
+```bash
+# 1) Verifiera ren arbetsyta
+git status --short
+
+# 2) Reprova hela lokala verifieringen
+cd backend
+npm run build && npm run test
+
+cd ../flutter
+flutter gen-l10n
+flutter test --reporter compact
+
+# 3) Om allt är grönt, kör deploy/build-pipeline igen
+```
+
+## Ändrade filer i denna session (huvudsakligen)
+
+- `flutter/lib/l10n/app_en.arb`
+- `flutter/lib/l10n/app_sv.arb`
+- genererade l10n-filer under `flutter/lib/l10n/generated/*`
+- mindre korrigeringar i backend-test/service under felsökningen, slutläge verifierat grönt.
diff --git a/compose.flutter.yml b/compose.flutter.yml
index eae2db8b..b38b3625 100644
--- a/compose.flutter.yml
+++ b/compose.flutter.yml
@@ -3,8 +3,10 @@ services:
build:
context: ./flutter
dockerfile: Dockerfile
- args:
- API_BASE_URL: "/api"
+ args:
+ API_BASE_URL: "/api"
+ SOURCE_MAPS: "false"
+ WEB_RENDERER: "auto"
image: recipe-flutter:local
container_name: recipe-flutter
restart: unless-stopped
diff --git a/flutter/Caddyfile b/flutter/Caddyfile
index 03a88ca3..d0cb9611 100644
--- a/flutter/Caddyfile
+++ b/flutter/Caddyfile
@@ -1,10 +1,30 @@
-:{$PORT:5000} {
- root * /usr/share/caddy
-
- # Proxy API calls to backend service on the internal Docker network.
- handle /api/* {
- reverse_proxy recipe-api:8080
- }
+:{$PORT:5000} {
+ root * /usr/share/caddy
+
+ header {
+ Content-Security-Policy "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self' https: http: ws: wss:; worker-src 'self' blob:"
+ }
+
+ @staticAssets {
+ path *.js *.wasm *.woff *.woff2 *.ttf *.otf
+ }
+ header @staticAssets Cache-Control "public, max-age=86400"
+
+ @hashedAssets {
+ path_regexp hashedAssets .*[._-][0-9a-fA-F]{8,}\.(js|css|wasm|woff2?|ttf|otf)$
+ }
+ header @hashedAssets Cache-Control "public, max-age=31536000, immutable"
+
+ @serviceWorker path /flutter_service_worker.js /version.json
+ header @serviceWorker Cache-Control "no-cache, must-revalidate"
+
+ @index path / /index.html
+ header @index Cache-Control "public, max-age=300, must-revalidate"
+
+ # Proxy API calls to backend service on the internal Docker network.
+ handle /api/* {
+ reverse_proxy recipe-api:8080
+ }
# SPA-routing – returnera alltid index.html för okända paths
handle {
diff --git a/flutter/Dockerfile b/flutter/Dockerfile
index aeff9756..8698b773 100644
--- a/flutter/Dockerfile
+++ b/flutter/Dockerfile
@@ -14,11 +14,20 @@ RUN flutter gen-l10n
# Run tests
RUN flutter test
-# Inject API base URL at build time via --dart-define.
-# Default to same-origin /api to avoid mixed-content in HTTPS deployments.
-ARG API_BASE_URL=/api
-RUN flutter build web --release \
- --dart-define=API_BASE_URL=${API_BASE_URL}
+# Inject API base URL at build time via --dart-define.
+# Default to same-origin /api to avoid mixed-content in HTTPS deployments.
+ARG API_BASE_URL=/api
+ARG SOURCE_MAPS=false
+ARG WEB_RENDERER=auto
+RUN set -eux; \
+ build_args="--release --dart-define=API_BASE_URL=${API_BASE_URL}"; \
+ if [ "${SOURCE_MAPS}" = "false" ]; then \
+ build_args="${build_args} --no-source-maps"; \
+ fi; \
+ if [ "${WEB_RENDERER}" != "auto" ]; then \
+ build_args="${build_args} --web-renderer=${WEB_RENDERER}"; \
+ fi; \
+ flutter build web ${build_args}
# Stage 2 – Serve with Caddy
FROM caddy:alpine AS runner
diff --git a/flutter/lib/core/router/app_router.dart b/flutter/lib/core/router/app_router.dart
index 8448d8ce..fdf0e940 100644
--- a/flutter/lib/core/router/app_router.dart
+++ b/flutter/lib/core/router/app_router.dart
@@ -21,22 +21,55 @@ import '../../features/inventory/presentation/consume_inventory_screen.dart';
import '../../features/inventory/presentation/consumption_history_screen.dart';
import '../../features/meal_plan/presentation/meal_plan_screen.dart';
import '../../features/pantry/presentation/pantry_screen.dart';
-import '../../features/import/presentation/import_screen.dart';
-import '../../features/shopping_list/presentation/shopping_list_screen.dart';
-import '../../features/admin/presentation/admin_screen.dart';
+import '../../features/import/presentation/import_screen.dart'
+ deferred as import_ui;
+import '../../features/shopping_list/presentation/shopping_list_screen.dart';
+import '../../features/admin/presentation/admin_screen.dart';
int? _shellBranchIndexForPath(String path) {
if (path.startsWith('/recipes')) return 0;
if (path.startsWith('/inventory')) return 1;
if (path.startsWith('/matsedel')) return 2;
if (path.startsWith('/baslager')) return 3;
- if (path.startsWith('/import')) return 4;
- if (path.startsWith('/inkopslista')) return 5;
- if (path.startsWith('/profile')) return 6;
- if (path.startsWith('/admin')) return 7;
+ if (path.startsWith('/import')) return 4;
+ if (path.startsWith('/inkopslista')) return 5;
+ if (path.startsWith('/profile')) return 6;
+ if (path.startsWith('/admin')) return 7;
return null;
}
+class _DeferredRouteLoader extends StatelessWidget {
+ const _DeferredRouteLoader({
+ required this.loadLibrary,
+ required this.builder,
+ });
+
+ final Future Function() loadLibrary;
+ final WidgetBuilder builder;
+
+ @override
+ Widget build(BuildContext context) {
+ return FutureBuilder(
+ future: loadLibrary(),
+ builder: (context, snapshot) {
+ if (snapshot.connectionState != ConnectionState.done) {
+ return const Scaffold(
+ body: LoadingStateView(label: 'Laddar vy...'),
+ );
+ }
+ if (snapshot.hasError) {
+ return Scaffold(
+ body: Center(
+ child: Text('Kunde inte ladda sidan: ${snapshot.error}'),
+ ),
+ );
+ }
+ return builder(context);
+ },
+ );
+ }
+}
+
final appRouterProvider = Provider((ref) {
final authState = ref.watch(authStateProvider);
@@ -244,26 +277,29 @@ final appRouterProvider = Provider((ref) {
),
],
),
- StatefulShellBranch(
- routes: [
- GoRoute(
- path: '/import',
- builder: (context, state) => const ImportScreen(),
- ),
- ],
- ),
- StatefulShellBranch(
- routes: [
- GoRoute(
- path: '/inkopslista',
- builder: (context, state) => const ShoppingListScreen(),
- ),
- ],
- ),
- StatefulShellBranch(
- routes: [
- GoRoute(
- path: '/profile',
+ StatefulShellBranch(
+ routes: [
+ GoRoute(
+ path: '/import',
+ builder: (context, state) => _DeferredRouteLoader(
+ loadLibrary: import_ui.loadLibrary,
+ builder: (_) => import_ui.ImportScreen(),
+ ),
+ ),
+ ],
+ ),
+ StatefulShellBranch(
+ routes: [
+ GoRoute(
+ path: '/inkopslista',
+ builder: (context, state) => const ShoppingListScreen(),
+ ),
+ ],
+ ),
+ StatefulShellBranch(
+ routes: [
+ GoRoute(
+ path: '/profile',
builder: (context, state) => const ProfileScreen(),
),
],
@@ -273,7 +309,8 @@ final appRouterProvider = Provider((ref) {
GoRoute(
path: '/admin',
redirect: (context, state) {
- final token = ref.read(authStateProvider)
+ final token = ref
+ .read(authStateProvider)
.maybeWhen(data: (t) => t, orElse: () => null);
return jwtIsAdmin(token) ? null : '/recipes';
},
diff --git a/flutter/lib/core/ui/app_shell.dart b/flutter/lib/core/ui/app_shell.dart
index 01fe23f3..6adafe44 100644
--- a/flutter/lib/core/ui/app_shell.dart
+++ b/flutter/lib/core/ui/app_shell.dart
@@ -181,9 +181,14 @@ class AppShell extends ConsumerWidget {
tooltip: view.mode == RecipesViewMode.grid
? 'Visa som lista'
: 'Visa som grid',
- icon: Icon(view.mode == RecipesViewMode.grid
- ? Icons.view_list
- : Icons.grid_view),
+ icon: Icon(
+ view.mode == RecipesViewMode.grid
+ ? Icons.view_list
+ : Icons.grid_view,
+ semanticLabel: view.mode == RecipesViewMode.grid
+ ? 'Visa som lista'
+ : 'Visa som grid',
+ ),
onPressed: () =>
ref.read(recipesViewProvider.notifier).toggleMode(),
),
@@ -207,7 +212,10 @@ class AppShell extends ConsumerWidget {
),
PopupMenuButton(
tooltip: 'Profil och konto',
- icon: const Icon(Icons.account_circle_outlined),
+ icon: const Icon(
+ Icons.account_circle_outlined,
+ semanticLabel: 'Profil och konto',
+ ),
onSelected: (value) async {
switch (value) {
case 'profile':
diff --git a/flutter/lib/features/import/presentation/flyer_import_tab.dart b/flutter/lib/features/import/presentation/flyer_import_tab.dart
index f834628f..30339368 100644
--- a/flutter/lib/features/import/presentation/flyer_import_tab.dart
+++ b/flutter/lib/features/import/presentation/flyer_import_tab.dart
@@ -44,14 +44,17 @@ class _FlyerImportTabState extends ConsumerState {
try {
final token = await ref.read(authStateProvider.future);
final api = ref.read(apiClientProvider);
- final categoryData = await api.getJson(CategoryApiPaths.tree, token: token);
+ final categoryData =
+ await api.getJson(CategoryApiPaths.tree, token: token);
final categoryList = categoryData is List
? categoryData
- : (categoryData is Map && categoryData['items'] is List)
+ : (categoryData is Map &&
+ categoryData['items'] is List)
? categoryData['items'] as List
: const [];
final tree = categoryList
- .map((e) => AdminCategoryNode.fromJson(Map.from(e as Map)))
+ .map((e) =>
+ AdminCategoryNode.fromJson(Map.from(e as Map)))
.toList();
if (!mounted) return;
setState(() => _categoryTree = tree);
@@ -142,12 +145,14 @@ class _FlyerImportTabState extends ConsumerState {
}
}
- Future _loadRestoredSourceIfNeeded(FlyerImportResult result, String? token) async {
+ Future _loadRestoredSourceIfNeeded(
+ FlyerImportResult result, String? token) async {
if (result.sessionId == null || result.sourceAvailable != true) return;
if (_pickedFile?.bytes != null) return;
try {
final repo = ref.read(importRepositoryProvider);
- final bytes = await repo.getFlyerSourceBytes(sessionId: result.sessionId!, token: token);
+ final bytes = await repo.getFlyerSourceBytes(
+ sessionId: result.sessionId!, token: token);
if (!mounted) return;
setState(() => _restoredSourceBytes = bytes);
} catch (_) {
@@ -250,7 +255,8 @@ class _FlyerImportTabState extends ConsumerState {
ref.invalidate(shoppingListItemsProvider);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
- content: Text('${saved.length} planerade. Inköpslista: $created tillagda, $updated uppdaterade.'),
+ content: Text(
+ '${saved.length} planerade. Inköpslista: $created tillagda, $updated uppdaterade.'),
action: SnackBarAction(
label: 'Öppna',
onPressed: () => context.go('/inkopslista'),
@@ -273,7 +279,8 @@ class _FlyerImportTabState extends ConsumerState {
int? selectedCategoryId = item.categoryId;
String? selectedCategoryPath = item.category;
- final payload = await showDialog<({String name, int? categoryId, String? categoryPath})>(
+ final payload = await showDialog<
+ ({String name, int? categoryId, String? categoryPath})>(
context: context,
builder: (context) {
return StatefulBuilder(
@@ -295,7 +302,8 @@ class _FlyerImportTabState extends ConsumerState {
),
const SizedBox(height: 12),
Text(
- selectedCategoryPath == null || selectedCategoryPath!.isEmpty
+ selectedCategoryPath == null ||
+ selectedCategoryPath!.isEmpty
? 'Ingen kategori vald'
: selectedCategoryPath!,
),
@@ -307,7 +315,9 @@ class _FlyerImportTabState extends ConsumerState {
onPressed: _categoryTree.isEmpty
? null
: () async {
- final selected = await CategoryThenProductPicker.showCategorySheet(
+ final selected =
+ await CategoryThenProductPicker
+ .showCategorySheet(
context,
categoryTree: _categoryTree,
preselectedCategoryId: selectedCategoryId,
@@ -402,7 +412,8 @@ class _FlyerImportTabState extends ConsumerState {
String _formatPrice(double? price, String? unit) {
if (price == null) return '';
final raw = price.toStringAsFixed(2).replaceAll('.', ',');
- final unitPart = (unit != null && unit.trim().isNotEmpty) ? '/${unit.trim()}' : '';
+ final unitPart =
+ (unit != null && unit.trim().isNotEmpty) ? '/${unit.trim()}' : '';
return '$raw kr$unitPart';
}
@@ -412,13 +423,20 @@ class _FlyerImportTabState extends ConsumerState {
if (trimmedOffer.isEmpty || trimmedLimit.isEmpty) return trimmedOffer;
final escaped = RegExp.escape(trimmedLimit);
- final withoutLimit = trimmedOffer.replaceAll(RegExp(escaped, caseSensitive: false), '').trim();
- final withoutLeadingPunctuation = withoutLimit.replaceAll(RegExp(r'^[,.;:\s-]+'), '').trim();
- return withoutLeadingPunctuation.replaceAll(RegExp(r'[,.;:\s-]+$'), '').trim();
+ final withoutLimit = trimmedOffer
+ .replaceAll(RegExp(escaped, caseSensitive: false), '')
+ .trim();
+ final withoutLeadingPunctuation =
+ withoutLimit.replaceAll(RegExp(r'^[,.;:\s-]+'), '').trim();
+ return withoutLeadingPunctuation
+ .replaceAll(RegExp(r'[,.;:\s-]+$'), '')
+ .trim();
}
Widget _buildOfferBadge(FlyerImportItem item, ThemeData theme) {
- final hasOffer = item.isOffer || (item.offerText?.trim().isNotEmpty ?? false) || item.price != null;
+ final hasOffer = item.isOffer ||
+ (item.offerText?.trim().isNotEmpty ?? false) ||
+ item.price != null;
if (!hasOffer) return const SizedBox.shrink();
return Container(
@@ -493,7 +511,8 @@ class _FlyerImportTabState extends ConsumerState {
children: [
Row(
children: [
- Icon(Icons.warning_amber_rounded, color: Colors.amber.shade800, size: 18),
+ Icon(Icons.warning_amber_rounded,
+ color: Colors.amber.shade800, size: 18),
const SizedBox(width: 8),
Text(
'Varningar (${warnings.length})',
@@ -546,14 +565,16 @@ class _FlyerImportTabState extends ConsumerState {
: OutlinedButton.icon(
icon: const Icon(Icons.open_in_new, size: 16),
label: const Text('Visa flyer'),
- style: OutlinedButton.styleFrom(visualDensity: VisualDensity.compact),
+ style: OutlinedButton.styleFrom(
+ visualDensity: VisualDensity.compact),
onPressed: () async {
final messenger = ScaffoldMessenger.of(context);
final opened = await openPdfBytes(bytes);
if (!context.mounted || opened) return;
messenger.showSnackBar(
const SnackBar(
- content: Text('PDF kan bara öppnas direkt i webbversionen just nu.'),
+ content: Text(
+ 'PDF kan bara öppnas direkt i webbversionen just nu.'),
),
);
},
@@ -600,25 +621,29 @@ class _FlyerImportTabState extends ConsumerState {
),
const SizedBox(height: 12),
FilledButton.icon(
- onPressed: (!_isLoading && _pickedFile?.bytes != null) ? _parseFlyer : null,
+ onPressed: (!_isLoading && _pickedFile?.bytes != null)
+ ? _parseFlyer
+ : null,
icon: const Icon(Icons.auto_awesome),
label: const Text('Importera flyer'),
),
const SizedBox(height: 12),
- _buildFlyerPreview(theme),
- if (_isLoading) ...[
- const SizedBox(height: 12),
- const LinearProgressIndicator(),
- ],
- if (items.isNotEmpty) ...[
- const SizedBox(height: 20),
- _buildWarningsPanel(theme),
- if ((_result?.warnings ?? const []).isNotEmpty) const SizedBox(height: 12),
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Text('${items.length} rader hittades', style: theme.textTheme.titleSmall),
- TextButton(
+ _buildFlyerPreview(theme),
+ if (_isLoading) ...[
+ const SizedBox(height: 12),
+ const LinearProgressIndicator(),
+ ],
+ if (items.isNotEmpty) ...[
+ const SizedBox(height: 20),
+ _buildWarningsPanel(theme),
+ if ((_result?.warnings ?? const []).isNotEmpty)
+ const SizedBox(height: 12),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text('${items.length} rader hittades',
+ style: theme.textTheme.titleSmall),
+ TextButton(
onPressed: () {
final target = selectedCount < items.length;
setState(() {
@@ -626,77 +651,92 @@ class _FlyerImportTabState extends ConsumerState {
_selected[i] = target;
}
});
- ref.read(flyerImportSessionProvider.notifier).setSelectedForAll(items.length, target);
+ ref
+ .read(flyerImportSessionProvider.notifier)
+ .setSelectedForAll(items.length, target);
},
- child: Text(selectedCount < items.length ? 'Välj alla' : 'Avmarkera alla'),
- ),
- ],
- ),
- const SizedBox(height: 8),
+ child: Text(selectedCount < items.length
+ ? 'Välj alla'
+ : 'Avmarkera alla'),
+ ),
+ ],
+ ),
+ const SizedBox(height: 8),
...items.asMap().entries.map((entry) {
final index = entry.key;
final item = entry.value;
final priceText = _formatPrice(item.price, item.priceUnit);
- final comparisonText = _formatPrice(item.comparisonPrice, item.comparisonUnit);
+ final comparisonText =
+ _formatPrice(item.comparisonPrice, item.comparisonUnit);
final limitText = item.offerLimitText?.trim();
final sanitizedOfferText = item.offerText == null
? ''
: _removeLimitTextFromOfferText(item.offerText!, limitText);
- return CheckboxListTile(
- value: _selected[index] ?? false,
- onChanged: (value) {
- final checked = value ?? false;
- setState(() => _selected[index] = checked);
- ref.read(flyerImportSessionProvider.notifier).setSelected(index, checked);
- },
- title: Row(
- children: [
- Expanded(child: Text(item.rawName)),
- IconButton(
- tooltip: 'Redigera',
- visualDensity: VisualDensity.compact,
- icon: const Icon(Icons.edit_outlined, size: 18),
- onPressed: () => _editItem(index, item),
+ return CheckboxListTile(
+ value: _selected[index] ?? false,
+ onChanged: (value) {
+ final checked = value ?? false;
+ setState(() => _selected[index] = checked);
+ ref
+ .read(flyerImportSessionProvider.notifier)
+ .setSelected(index, checked);
+ },
+ title: Row(
+ children: [
+ Expanded(child: Text(item.rawName)),
+ IconButton(
+ tooltip: 'Redigera',
+ visualDensity: VisualDensity.compact,
+ icon: const Icon(
+ Icons.edit_outlined,
+ size: 18,
+ semanticLabel: 'Redigera rad',
),
- const SizedBox(width: 8),
- _buildQualityBadge(item, theme),
- const SizedBox(width: 8),
- _buildOfferBadge(item, theme),
- ],
- ),
- subtitle: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- if (priceText.isNotEmpty) Text('Pris: $priceText'),
- if ((item.category ?? '').trim().isNotEmpty)
- Text('Kategori: ${item.category}'),
- if (comparisonText.isNotEmpty) Text('Jämförpris: $comparisonText'),
- if (limitText != null && limitText.isNotEmpty)
- Text(
- 'Begränsning: $limitText',
- style: theme.textTheme.bodyMedium?.copyWith(
- color: Colors.orange.shade900,
- fontWeight: FontWeight.w600,
- ),
- ),
- if (sanitizedOfferText.isNotEmpty) Text(sanitizedOfferText),
- if (item.matchedProductName != null) Text('Match: ${item.matchedProductName}'),
- ],
- ),
- controlAffinity: ListTileControlAffinity.leading,
- );
+ onPressed: () => _editItem(index, item),
+ ),
+ const SizedBox(width: 8),
+ _buildQualityBadge(item, theme),
+ const SizedBox(width: 8),
+ _buildOfferBadge(item, theme),
+ ],
+ ),
+ subtitle: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ if (priceText.isNotEmpty) Text('Pris: $priceText'),
+ if ((item.category ?? '').trim().isNotEmpty)
+ Text('Kategori: ${item.category}'),
+ if (comparisonText.isNotEmpty)
+ Text('Jämförpris: $comparisonText'),
+ if (limitText != null && limitText.isNotEmpty)
+ Text(
+ 'Begränsning: $limitText',
+ style: theme.textTheme.bodyMedium?.copyWith(
+ color: Colors.orange.shade900,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ if (sanitizedOfferText.isNotEmpty) Text(sanitizedOfferText),
+ if (item.matchedProductName != null)
+ Text('Match: ${item.matchedProductName}'),
+ ],
+ ),
+ controlAffinity: ListTileControlAffinity.leading,
+ );
}),
const SizedBox(height: 8),
SizedBox(
width: double.infinity,
child: FilledButton.icon(
- onPressed: (_isSaving || selectedCount == 0) ? null : _planSelected,
+ onPressed:
+ (_isSaving || selectedCount == 0) ? null : _planSelected,
icon: _isSaving
? const SizedBox(
width: 16,
height: 16,
- child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
+ child: CircularProgressIndicator(
+ strokeWidth: 2, color: Colors.white),
)
: const Icon(Icons.playlist_add_check),
label: Text('Planera $selectedCount markerade'),
diff --git a/flutter/lib/features/import/presentation/receipt_import_tab.dart b/flutter/lib/features/import/presentation/receipt_import_tab.dart
index 319cfad1..f349c33b 100644
--- a/flutter/lib/features/import/presentation/receipt_import_tab.dart
+++ b/flutter/lib/features/import/presentation/receipt_import_tab.dart
@@ -133,7 +133,8 @@ class _ReceiptImportTabState extends ConsumerState {
final token = await ref.read(authStateProvider.future);
try {
- final globalData = await api.getJson(ProductApiPaths.list, token: token);
+ final globalData =
+ await api.getJson(ProductApiPaths.list, token: token);
globalList = _extractItems(globalData);
} catch (e, st) {
globalFailed = true;
@@ -146,7 +147,8 @@ class _ReceiptImportTabState extends ConsumerState {
mineList = _extractItems(mineData);
} catch (e, st) {
privateFailed = true;
- debugPrint('ReceiptImportTab._loadProducts private products failed: $e');
+ debugPrint(
+ 'ReceiptImportTab._loadProducts private products failed: $e');
debugPrintStack(stackTrace: st);
}
} catch (e, st) {
@@ -158,12 +160,16 @@ class _ReceiptImportTabState extends ConsumerState {
if (!mounted) return;
final mergedProducts = [
- ...globalList
- .cast