diff --git a/NEXT_STEPS.md b/NEXT_STEPS.md index f573f614..c488d7b9 100644 --- a/NEXT_STEPS.md +++ b/NEXT_STEPS.md @@ -37,7 +37,7 @@ | Användarspecifika produkter (UserProduct) | ⚠️ Schema klart, UI basic | | Användarroller (user / admin) | ✅ Klart | | Användarhantering i admin-UI | ✅ Klart | -| Profilsida med flikar (Min profil / Användare / Databas) | ✅ Klart | +| Profilsida med flikar (Min profil / Användare / Databas med undertabbar) | ✅ Klart | | Teknisk skuld — oanvända InventoryItem-fält | ✅ Klart (migration 20260418) | | Teknisk skuld — redirect-routes städade | ✅ Klart | | Premium-plan (isPremium på User, Free/Paid-dropdown) | ✅ Klart | @@ -145,7 +145,10 @@ Systemet har nu fullständig rollbaserad åtkomstkontroll och ett komplett anvä **Profilsidan med flikar (`/profil`):** - `?tab=profil` — Min profil (alla användare) - `?tab=anvandare` — Användare (enbart admin): skapa, ta bort, rollbyte, e-postbyte, lösenordsåterställning med kopierings-modal -- `?tab=databas` — Databas (enbart admin): produktadmin (samma innehåll som `/admin/products`) +- `?tab=databas` — Databas (enbart admin): produktadmin, nu med undertabbar: + - **Varor:** lista och redigera aktiva produkter + - **Skapa / Slå ihop:** skapa ny produkt, återställ produktdatabas, slå ihop dubbletter + - **Papperskorg:** visa mjukraderade produkter, återställ eller radera permanent - `/admin/users` omdirigerar till `/profil?tab=anvandare` - Navigeringslänken "👥 Användare" går direkt till `/profil?tab=anvandare` diff --git a/README.md b/README.md index 4c23962e..89ad1878 100644 --- a/README.md +++ b/README.md @@ -45,18 +45,27 @@ En fullstack-applikation för hantering av hemmavaror och recept. Håll koll på - **Lägg till och ta bort** — välj från produktlistan via sökbar dropdown, ta bort med ett klick ### Admin: Produkter -- **Redigera produkter** — uppdatera visningsnamn, canonical name, kategori (hierarkisk dropdown) och varumärke inline direkt i listan -- **Kategoritilldelning** — välj kategori ur ett 3-nivåträd (huvudkategori → underkategori → typ) som laddas dynamiskt från API:et -- **Bulk-kategorisering** — filtrera fram okategoriserade produkter, markera flera (eller "välj alla synliga") och sätt kategori på alla markerade på en gång -- **AI-kategorisering per produkt** — klicka "✨ Fråga AI" bredvid kategori-dropdown för att få ett AI-förslag med säkerhetsindikation (hög/medel/låg); godkänn eller avfärda med ett klick -- **AI-bulk-kategorisering** — knappen "✨ AI-kategorisera okategoriserade" analyserar alla produkter utan kategori via Mistral AI och presenterar ett bekräftelsemodal; admin väljer vilka förslag som ska tillämpas -- **Hitta dubbletter** — identifiera produkter med samma normaliserade namn -- **Slå ihop produkter** — merge av två produktposter: alla inventarieföremål och receptreferenser flyttas till målprodukten, källan soft-deleteras -- **Förhandsvisning** — granska vad som händer (inventarieräkningar, utfall) innan merge genomförs -- **Ta bort och återställ** — soft-delete enskilda produkter, återställ med ett klick -- **Återställ all produktdata** — rensningsknapp som raderar alla produkter, inventarie, taggar och kvitto-alias (behåller användare och kategorier) - > Obs: Destruktiva åtgärder (merge, ta bort, återställ, bulk-uppdatera, återställ all data) kräver admin-roll. +**Admin: Produkter (fliken Databas i /profil)** + +Produktadmin är nu uppdelad i tre undertabbar: + +- **📦 Varor** — lista och redigera aktiva produkter +- **➕ Skapa / Slå ihop** — skapa ny produkt, återställ produktdatabas, slå ihop dubbletter +- **🗑️ Papperskorg** — visa mjukraderade produkter, återställ eller radera permanent + +Funktioner: +- Redigera produkter — uppdatera namn, canonical name, kategori (hierarkisk dropdown) och varumärke inline +- Kategoritilldelning — välj kategori ur ett 3-nivåträd (huvudkategori → underkategori → typ) +- Bulk-kategorisering — filtrera fram okategoriserade produkter, markera flera och sätt kategori på alla markerade +- AI-kategorisering per produkt och bulk — "✨ Fråga AI" för kategori +- Hitta dubbletter och slå ihop produkter (merge) +- Förhandsvisning av merge +- Ta bort (mjukradera) och återställ produkter +- **Papperskorg:** Återställ eller radera produkter permanent +- Återställ all produktdata (reset) + +> Obs: Destruktiva åtgärder (merge, ta bort, återställ, permanent radering, bulk-uppdatera, återställ all data) kräver admin-roll. ### Väntande produktförslag - **Produktförslags-kö** — produkter med status `pending` samlas på sidan `/admin/products/pending` (länk "⏳ Förslag" i navigeringen) diff --git a/frontend/app/api/admin/deleted-products/[id]/route.ts b/frontend/app/api/admin/deleted-products/[id]/route.ts index 90836740..5cf9b633 100644 --- a/frontend/app/api/admin/deleted-products/[id]/route.ts +++ b/frontend/app/api/admin/deleted-products/[id]/route.ts @@ -1,4 +1,4 @@ -import { withAuth } from '../../../../../../lib/with-auth'; +import { withAuth } from '../../../../../lib/with-auth'; const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080'; diff --git a/frontend/lib/error-handler.ts b/frontend/lib/error-handler.ts index e84679e1..53ac80db 100644 --- a/frontend/lib/error-handler.ts +++ b/frontend/lib/error-handler.ts @@ -1,35 +1,53 @@ /** * Utility för att parse HTTP-responses och extrahera tydliga felmeddelanden */ + +// Deklarativ mappning av kända tekniska felsträngar → svenska meddelanden +const MESSAGE_MAP: Array<{ match: string; label: string }> = [ + { match: 'User_email_key', label: 'E-postadressen används redan av en annan användare.' }, + { match: 'Det finns redan en annan produkt med detta namn', label: 'Det finns redan en annan produkt med detta namn. Välj ett unikt namn.' }, +]; + +function translateMessage(msg: string): string { + const found = MESSAGE_MAP.find((entry) => msg.includes(entry.match)); + return found ? found.label : msg; +} + export async function parseErrorResponse(response: Response): Promise { const status = response.status; - + + // Läs body som text en gång — Response.body kan bara konsumeras en gång + let bodyText = ''; try { - const data = await response.json(); - - // Om backend skickade ett felmeddelande + bodyText = await response.text(); + } catch { + // Body kunde inte läsas + } + + // Försök tolka som JSON + try { + const data = JSON.parse(bodyText); + + // NestJS class-validator kan returnera message som array + if (Array.isArray(data.message) && data.message.length > 0) { + return translateMessage(String(data.message[0])); + } + if (typeof data.message === 'string') { - // Produktnamns-dubblett - if (data.message.includes('Det finns redan en annan produkt med detta namn')) { - return 'Det finns redan en annan produkt med detta namn. Välj ett unikt namn.'; - } - return data.message; + return translateMessage(data.message); } - if (data.error) { - return data.error; + + if (typeof data.error === 'string') { + return translateMessage(data.error); } - if (data.details) { - return data.details; + + if (typeof data.details === 'string') { + return translateMessage(data.details); } } catch { - // Inte JSON, försök text - try { - const text = await response.text(); - if (text && text.length < 200) { - return text; - } - } catch { - // Inget text-innehål + // Inte JSON — använd råtexten om den är kortfattad + if (bodyText && bodyText.length < 200) { + return bodyText; } } @@ -39,7 +57,7 @@ export async function parseErrorResponse(response: Response): Promise { 401: 'Du är inte autentiserad. Logga in.', 403: 'Du har inte behörighet till detta.', 404: 'Resursen hittades inte.', - 409: 'Konflikten med befintlig data.', + 409: 'Konflikt med befintlig data.', 422: 'Valideringen misslyckades. Kontrollera dina inmatningar.', 500: 'Serverfel. Försök igen senare.', 503: 'Tjänsten är inte tillgänglig.', @@ -47,8 +65,3 @@ export async function parseErrorResponse(response: Response): Promise { return defaultMessages[status] || `Fel (${status}). Försök igen senare.`; } - -// Prisma unique constraint: email -if (typeof data.message === 'string' && data.message.includes('User_email_key')) { - return 'E-postadressen används redan av en annan användare.'; -}