MAJOR UPPDATE: "First Ai"
feat: add AI categorization for products and enhance user management - Integrated AI service for category suggestions in receipt import and product management. - Added premium subscription feature for users with corresponding API endpoints. - Implemented admin interface for managing pending product suggestions. - Enhanced user management to include premium status and corresponding UI updates. - Updated database schema to support new fields for premium status and product status.
This commit is contained in:
+62
-26
@@ -66,13 +66,13 @@ docker exec recipe-db mariadb -uroot -p"LÖSENORD" recipe_app -e "SHOW TABLES;"
|
||||
| Sida | Fil | Funktionalitet |
|
||||
|------|-----|---|
|
||||
| **Hem** | `app/page.tsx` | Startsida |
|
||||
| **Navigering** | `app/Navigation.tsx` | Huvudmeny, inloggad användare, länk till profil; länkarna "⚙️ Admin" och "👥 Användare" visas enbart om sessionens roll är `admin` |
|
||||
| **Navigering** | `app/Navigation.tsx` | Huvudmeny, inloggad användare, länk till profil; länkarna "⚙️ Admin", "⏳ Förslag" och "👥 Användare" visas enbart om sessionens roll är `admin` |
|
||||
| **Inloggning** | `app/login/page.tsx` | Inloggningssida med Auth.js Credentials |
|
||||
| **Profil** | `app/profil/page.tsx` | Flikbaserad profil-/adminsida (server component): läser `?tab=`-param, kontrollerar admin-roll via `auth()`, laddar rätt flik dynamiskt |
|
||||
| | `app/profil/ProfileTabs.tsx` | Klientkomponent: fliknavigering med Link-basad URL-routing (`?tab=profil\|anvandare\|databas`) |
|
||||
| | `app/profil/tabs/MinProfilTab.tsx` | Profilformulär (förnamn, efternamn, e-post) |
|
||||
| | `app/profil/tabs/AnvandareTab.tsx` | Server component: hämtar användarlista, renderar AnvandareClient |
|
||||
| | `app/profil/tabs/AnvandareClient.tsx` | Klientkomponent: skapa/ta bort användare, rollbyte, e-postbyte, lösenordsåterställning med kopierings-modal |
|
||||
| | `app/profil/tabs/AnvandareClient.tsx` | Klientkomponent: skapa/ta bort användare, rollbyte, plan-byte (Free/Paid ✨ → isPremium), e-postbyte, lösenordsåterställning med kopierings-modal |
|
||||
| | `app/profil/tabs/DatabsTab.tsx` | Server component: produktdatabas (importerar admin/products-komponenter) |
|
||||
| **Inventorie** | `app/inventory/page.tsx` | Lista, filtrera, sortera varor |
|
||||
| | `InventoryList.tsx` | Ritning av inventarieföremål |
|
||||
@@ -96,11 +96,13 @@ docker exec recipe-db mariadb -uroot -p"LÖSENORD" recipe_app -e "SHOW TABLES;"
|
||||
| | `app/import/ImportTabsClient.tsx` | Klientkomponent: kvitto/recept-flikar |
|
||||
| **Admin: Användare** | `app/admin/users/page.tsx` | Redirect till `/profil?tab=anvandare` |
|
||||
| **Admin: Produkter** | `app/admin/products/page.tsx` | Produktadmin-panel |
|
||||
| | `AdminProductList.tsx` | Lista produkter, sök, sortera, filter okategoriserade, bulk-select + bulk-kategorisering |
|
||||
| | `EditProductForm.tsx` | Inline redigering: name, canonicalName, kategori (hierarkisk dropdown), brand, taggar |
|
||||
| | `AdminProductList.tsx` | Lista produkter, sök, sortera, filter okategoriserade, bulk-select + bulk-kategorisering; AI-bulk-knapp ("✨ AI-kategorisera") med bekräftelsemodal |
|
||||
| | `EditProductForm.tsx` | Inline redigering: name, canonicalName, kategori (hierarkisk dropdown), brand, taggar; "✨ Fråga AI"-knapp med suggestion-chip (grön = hög, gul = fallback) |
|
||||
| | `ResetProductsButton.tsx` | Knapp för att rensa all produktdata |
|
||||
| | `MergePreviewForm.tsx` | Förhandsgranska merge |
|
||||
| | `actions.ts` | Server actions: updateProduct, deleteProduct, resetAllProducts, bulkSetCategory |
|
||||
| | `actions.ts` | Server actions: updateProduct, deleteProduct, resetAllProducts, bulkSetCategory, suggestProductCategory, suggestBulkCategories, setProductStatus |
|
||||
| **Admin: Väntande produkter** | `app/admin/products/pending/page.tsx` | Server component, auth-skyddad, hämtar pending-produkter |
|
||||
| | `PendingProductsClient.tsx` | Tabell: Produkt / Kategori (AI) / Föreslagen av / Datum / Åtgärd; "✓ Godkänn" / "✕ Avvisa"-knappar |
|
||||
| **Baslager** | `app/baslager/page.tsx` | Visa och hantera baslager (server component) |
|
||||
| | `AddToPantryForm.tsx` | Lägg till produkt i baslager (dropdown) |
|
||||
| | `PantryList.tsx` | Visa baslager grupperat per kategori |
|
||||
@@ -113,7 +115,7 @@ Alla proxy-routes läser auth-token via `auth()` (Auth.js v5) och vidarebefordra
|
||||
| Route | Metod | Syfte |
|
||||
|-------|-------|-------|
|
||||
| `/api/admin-users` | GET, POST | Hämtar alla användare / skapar ny användare (kräver admin-roll i session) |
|
||||
| `/api/admin-users/[id]` | PATCH, DELETE, PUT | Ändrar roll / tar bort användare / byter e-post (kräver admin-roll i session) |
|
||||
| `/api/admin-users/[id]` | PATCH, DELETE, PUT | Ändrar roll / tar bort användare / byter e-post; om body innehåller `isPremium` → anropar `PATCH /api/users/:id/premium` (kräver admin-roll i session) |
|
||||
| `/api/admin-users/[id]/reset-password` | POST | Återställer lösenord och returnerar tillfälligt lösenord + meddelandetext (kräver admin-roll) |
|
||||
| `/api/auth/[...nextauth]` | GET, POST | Auth.js handlers (login, logout, session) |
|
||||
| `/api/products` | GET | Produktlista (auth-wrappat med `auth(req)`) |
|
||||
@@ -166,27 +168,31 @@ backend/src/
|
||||
├── main.ts # Startpunkt (port 8080, global prefix "api")
|
||||
├── auth/
|
||||
│ ├── auth.controller.ts # POST /api/auth/login, POST /api/auth/register
|
||||
│ ├── auth.service.ts # validateUser, login (JWT-signering inkl. role)
|
||||
│ ├── auth.service.ts # validateUser, login (JWT-signering inkl. role + isPremium)
|
||||
│ ├── auth.module.ts
|
||||
│ ├── jwt.strategy.ts # Passport JWT-strategi (returnerar userId, username, role)
|
||||
│ ├── jwt.strategy.ts # Passport JWT-strategi (returnerar userId, username, role, isPremium)
|
||||
│ ├── jwt-auth.guard.ts # Global guard (skyddar allt utom @Public)
|
||||
│ ├── roles.guard.ts # Guard som kontrollerar @Roles() metadata; kastar 403
|
||||
│ └── decorators/
|
||||
│ ├── public.decorator.ts # @Public() – markerar öppen endpoint
|
||||
│ ├── current-user.decorator.ts # @CurrentUser() – extraherar {userId, username, role}
|
||||
│ ├── current-user.decorator.ts # @CurrentUser() – extraherar {userId, username, role, isPremium}
|
||||
│ └── roles.decorator.ts # @Roles('admin') – sätter rollkrav via SetMetadata
|
||||
├── ai/
|
||||
│ ├── ai.service.ts # AiService: suggestCategory() — anropar Mistral API med kategorikontext
|
||||
│ └── ai.module.ts # Exporterar AiService (importeras av ProductsModule, ReceiptImportModule)
|
||||
├── users/
|
||||
│ ├── users.controller.ts # GET /api/users/me, PATCH /api/users/me
|
||||
│ │ # GET /api/users (admin), PATCH /api/users/:id/role (admin)
|
||||
│ │ # POST /api/users (admin), DELETE /api/users/:id (admin)
|
||||
│ │ # POST /api/users/:id/reset-password (admin), PATCH /api/users/:id/email (admin)
|
||||
│ │ # PATCH /api/users/:id/premium (admin)
|
||||
│ ├── users.service.ts # findByUsername, findById, create, updateProfile
|
||||
│ │ # findAll, setRole, adminCreate, deleteUser, resetPassword, updateEmail
|
||||
│ │ # findAll, setRole, adminCreate, deleteUser, resetPassword, updateEmail, setPremium
|
||||
│ ├── admin-bootstrap.service.ts # OnApplicationBootstrap: skapar/uppdaterar 4 seed-användare
|
||||
│ └── users.module.ts
|
||||
├── categories/
|
||||
│ ├── categories.controller.ts # GET /api/categories, GET /api/categories/tree (@Public)
|
||||
│ ├── categories.service.ts # findAll (flat), findTree (hierarkisk)
|
||||
│ ├── categories.service.ts # findAll (flat), findTree (hierarkisk), findFlattened (med full path)
|
||||
│ └── categories.module.ts
|
||||
├── common/
|
||||
│ ├── filters/
|
||||
@@ -210,8 +216,12 @@ backend/src/
|
||||
│ └── prisma.module.ts
|
||||
├── products/
|
||||
│ ├── products.controller.ts # CRUD, merge, duplicates, reset-all
|
||||
│ ├── products.service.ts # Produktlogik inkl. resetAll()
|
||||
│ ├── products.module.ts
|
||||
│ │ # GET /products/pending (admin)
|
||||
│ │ # GET /products/:id/suggest-category (premium/admin)
|
||||
│ │ # POST /products/ai-categorize-bulk (admin)
|
||||
│ │ # PATCH /products/:id/status (admin)
|
||||
│ ├── products.service.ts # Produktlogik inkl. resetAll(), findPending(), setStatus()
|
||||
│ ├── products.module.ts # Importerar AiModule + CategoriesModule
|
||||
│ └── dto/
|
||||
│ ├── create-product.dto.ts
|
||||
│ ├── update-product.dto.ts
|
||||
@@ -232,10 +242,12 @@ backend/src/
|
||||
│ └── dto/
|
||||
│ └── create-meal-plan-entry.dto.ts # { date, recipeId, servings? }
|
||||
├── receipt-import/
|
||||
│ ├── receipt-import.controller.ts # POST /api/receipt-import (multipart)
|
||||
│ ├── receipt-import.service.ts # Mistral AI-anrop, bildtolkning
|
||||
│ ├── receipt-import.controller.ts # POST /api/receipt-import (multipart, kräver JWT)
|
||||
│ │ # Skickar isPremium till service (premium/admin → AI-enrichment)
|
||||
│ ├── receipt-import.service.ts # Mistral AI-anrop, bildtolkning, enrichWithAiCategories()
|
||||
│ ├── receipt-import.module.ts # Importerar AiModule + CategoriesModule
|
||||
│ └── dto/
|
||||
│ └── parsed-receipt-item.dto.ts
|
||||
│ └── parsed-receipt-item.dto.ts # Inkl. categorySuggestion?: CategorySuggestion
|
||||
├── receipt-alias/
|
||||
│ ├── receipt-alias.controller.ts # CRUD /api/receipt-alias
|
||||
│ ├── receipt-alias.service.ts
|
||||
@@ -331,10 +343,18 @@ backend/src/
|
||||
- **`inventoryCompare(from, to)`** — Kör samma aggregering som `shoppingList` men jämför sedan varje ingrediens mot aktuellt inventarielager. Returnerar status per ingrediens: `räcker | saknas | enhetskonflikt`.
|
||||
|
||||
**Kvittoimport-API:**
|
||||
- **`parseReceipt(file)`** — Tar emot en bildel eller PDF (max 15 MB), skickar den till Mistral AI för tolkning och returnerar en lista av kandidatprodukter med namn, kvantitet och enhet.
|
||||
- **`parseReceipt(file, isPremium)`** — Tar emot en bild eller PDF (max 15 MB), skickar den till Mistral AI för tolkning och returnerar en lista av kandidatprodukter med namn, kvantitet och enhet.
|
||||
- Alias-matchning: före returneringen slås varje rånamn upp mot `ReceiptAlias`-tabellen och mot `Product.normalizedName`. Träffar kopplas automatiskt till rätt produkt-ID.
|
||||
- **AI-kategorisuggestion (premium):** Om `isPremium = true` (eller admin) och en vara varken alias-matchas eller ordbaserat matchas anropas `AiService.suggestCategory()`. Svaret inkluderas som `categorySuggestion` i retur-DTO:n och visas som ett lila "✨"-chip i frontend.
|
||||
- Stödda MIME-typer: `image/jpeg`, `image/png`, `image/webp`, `image/heic`, `image/heif`, `application/pdf`
|
||||
|
||||
**AI-API (`AiService`):**
|
||||
- **`suggestCategory(productName, categories: FlatCategory[])`** — Skickar produktnamnet och en flat lista av alla kategorier (med full sökväg, t.ex. "Mejeri och ägg > Mjölk och grädde") till Mistral API (`mistral-small-2603`). Returnerar `CategorySuggestion`.
|
||||
- **Svar-typ `CategorySuggestion`:** `{ categoryId, categoryName, path, confidence: 'high'|'medium'|'low', usedFallback: boolean }`
|
||||
- **Fallback-strategi:** Om AI returnerar ett ogiltigt kategori-ID eller om anropet misslyckas: försök hitta "Övrigt"-underkategori → fallback till rot-"Övrigt" (id 221) med `confidence: 'low'` och `usedFallback: true`
|
||||
- **Integration:** `AiModule` exporterar `AiService` och importeras av `ProductsModule` och `ReceiptImportModule`
|
||||
- **CategoriesService tillägg:** `findFlattened()` bygger en flat lista med `{ id, name, path }` där `path` är den fullständiga kategorivägen (används som kontext till Mistral)
|
||||
|
||||
---
|
||||
|
||||
## API-endpoints (fullständig lista)
|
||||
@@ -387,6 +407,13 @@ POST /api/products/backfill-canonical Backfill canonical names (admin)
|
||||
POST /api/products/reset-all Rensa all produktdata (admin)
|
||||
POST /api/products/bulk-update Uppdatera flera produkter (t.ex. sätt kategori)
|
||||
Body: { ids: number[], categoryId?: number | null }
|
||||
|
||||
GET /api/products/pending Lista pending-produkter (admin)
|
||||
GET /api/products/:id/suggest-category AI-förslag på kategori för produkt (premium/admin)
|
||||
POST /api/products/ai-categorize-bulk Kör AI-kategorisering på alla okategoriserade (admin)
|
||||
Returnerar lista av { productId, productName, suggestion: CategorySuggestion }
|
||||
PATCH /api/products/:id/status Sätt produktstatus active/rejected (admin)
|
||||
Body: { status: 'active' | 'rejected' }
|
||||
```
|
||||
|
||||
### Kategori-endpoints
|
||||
@@ -397,11 +424,13 @@ GET /api/categories/tree Hierarkiskt träd (@Public)
|
||||
|
||||
### Användar-endpoints
|
||||
```
|
||||
POST /api/auth/login Logga in, returnerar JWT inkl. role (@Public)
|
||||
GET /api/users/me Hämta inloggad användares profil (inkl. role)
|
||||
POST /api/auth/login Logga in, returnerar JWT inkl. role + isPremium (@Public)
|
||||
GET /api/users/me Hämta inloggad användares profil (inkl. role, isPremium)
|
||||
PATCH /api/users/me Uppdatera firstName, lastName, email
|
||||
GET /api/users Lista alla användare (kräver admin-roll)
|
||||
PATCH /api/users/:id/role Ändra roll för användare (kräver admin-roll)
|
||||
PATCH /api/users/:id/premium Sätt isPremium true/false (kräver admin-roll)
|
||||
Body: { isPremium: boolean }
|
||||
```
|
||||
|
||||
### Baslager-endpoints
|
||||
@@ -448,7 +477,8 @@ model User {
|
||||
firstName String?
|
||||
lastName String?
|
||||
passwordHash String
|
||||
role String @default("user") # "user" eller "admin"
|
||||
role String @default("user") # "user" eller "admin"
|
||||
isPremium Boolean @default(false) # Styr tillgång till AI-premium-funktioner
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
@@ -456,12 +486,12 @@ model User {
|
||||
|
||||
**Bootstrap-användare:** När backend startar kör `AdminBootstrapService.onApplicationBootstrap()` och skapar eller uppdaterar fyra användare baserade på miljövariabler:
|
||||
|
||||
| Användarnamn | Roll | E-post | Miljövariabel |
|
||||
|---|---|---|---|
|
||||
| Nadmin | admin | nadmin@localhost | `ADMIN_NADMIN_PASSWORD` |
|
||||
| Padmin | admin | padmin@localhost | `ADMIN_PADMIN_PASSWORD` |
|
||||
| user1 | user | user1@localhost | `SEED_USER1_PASSWORD` |
|
||||
| user2 | user | user2@localhost | `SEED_USER2_PASSWORD` |
|
||||
| Användarnamn | Roll | isPremium | E-post | Miljövariabel |
|
||||
|---|---|---|---|---|
|
||||
| Nadmin | admin | false | nadmin@localhost | `ADMIN_NADMIN_PASSWORD` |
|
||||
| Padmin | admin | false | padmin@localhost | `ADMIN_PADMIN_PASSWORD` |
|
||||
| user1 | user | false | user1@localhost | `SEED_USER1_PASSWORD` |
|
||||
| user2 | user | false | user2@localhost | `SEED_USER2_PASSWORD` |
|
||||
|
||||
Om en användare redan finns men har fel roll rättas rollen automatiskt. Om miljövariabeln saknas hoppas den användaren över med en varning i loggen.
|
||||
|
||||
@@ -506,6 +536,7 @@ model Product {
|
||||
brand String? # Varumärke
|
||||
categoryId Int? # FK till Category (ny hierarki)
|
||||
categoryRef Category? # Relation till Category
|
||||
status String @default("active") # "active" | "pending" | "rejected"
|
||||
isActive Boolean @default(true)
|
||||
deletedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
@@ -518,6 +549,11 @@ model Product {
|
||||
}
|
||||
```
|
||||
|
||||
**Produktstatus:**
|
||||
- `active` — normal produkt synlig i alla listor (default)
|
||||
- `pending` — föreslagen produkt som väntar på admin-godkännande; visas på `/admin/products/pending`
|
||||
- `rejected` — avvisad produkt; visas inte i vanliga listor
|
||||
|
||||
### InventoryItem
|
||||
```prisma
|
||||
model InventoryItem {
|
||||
|
||||
Reference in New Issue
Block a user