feat: implement two-step category and product picker with private product creation support

This commit is contained in:
Nils-Johan Gynther
2026-05-01 02:44:30 +02:00
parent 4f387fe6eb
commit f983458ff0
3 changed files with 116 additions and 4 deletions
+59 -1
View File
@@ -2,7 +2,65 @@
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.
## Senaste ändringar (2026-05-01)
## 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.