feat: enhance error handling with user-friendly messages and improve response parsing

This commit is contained in:
Nils-Johan Gynther
2026-04-21 13:38:59 +02:00
parent 87eab4d0ca
commit 83722123d2
4 changed files with 66 additions and 41 deletions
+5 -2
View File
@@ -37,7 +37,7 @@
| Användarspecifika produkter (UserProduct) | ⚠️ Schema klart, UI basic | | Användarspecifika produkter (UserProduct) | ⚠️ Schema klart, UI basic |
| Användarroller (user / admin) | ✅ Klart | | Användarroller (user / admin) | ✅ Klart |
| Användarhantering i admin-UI | ✅ 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 — oanvända InventoryItem-fält | ✅ Klart (migration 20260418) |
| Teknisk skuld — redirect-routes städade | ✅ Klart | | Teknisk skuld — redirect-routes städade | ✅ Klart |
| Premium-plan (isPremium på User, Free/Paid-dropdown) | ✅ 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`):** **Profilsidan med flikar (`/profil`):**
- `?tab=profil` — Min profil (alla användare) - `?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=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` - `/admin/users` omdirigerar till `/profil?tab=anvandare`
- Navigeringslänken "👥 Användare" går direkt till `/profil?tab=anvandare` - Navigeringslänken "👥 Användare" går direkt till `/profil?tab=anvandare`
+20 -11
View File
@@ -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 - **Lägg till och ta bort** — välj från produktlistan via sökbar dropdown, ta bort med ett klick
### Admin: Produkter ### 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. > 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 ### Väntande produktförslag
- **Produktförslags-kö** — produkter med status `pending` samlas på sidan `/admin/products/pending` (länk "⏳ Förslag" i navigeringen) - **Produktförslags-kö** — produkter med status `pending` samlas på sidan `/admin/products/pending` (länk "⏳ Förslag" i navigeringen)
@@ -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'; const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080';
+40 -27
View File
@@ -1,35 +1,53 @@
/** /**
* Utility för att parse HTTP-responses och extrahera tydliga felmeddelanden * 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<string> { export async function parseErrorResponse(response: Response): Promise<string> {
const status = response.status; const status = response.status;
// Läs body som text en gång — Response.body kan bara konsumeras en gång
let bodyText = '';
try { try {
const data = await response.json(); bodyText = await response.text();
} catch {
// Om backend skickade ett felmeddelande // 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') { if (typeof data.message === 'string') {
// Produktnamns-dubblett return translateMessage(data.message);
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;
} }
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 { } catch {
// Inte JSON, försök text // Inte JSON — använd råtexten om den är kortfattad
try { if (bodyText && bodyText.length < 200) {
const text = await response.text(); return bodyText;
if (text && text.length < 200) {
return text;
}
} catch {
// Inget text-innehål
} }
} }
@@ -39,7 +57,7 @@ export async function parseErrorResponse(response: Response): Promise<string> {
401: 'Du är inte autentiserad. Logga in.', 401: 'Du är inte autentiserad. Logga in.',
403: 'Du har inte behörighet till detta.', 403: 'Du har inte behörighet till detta.',
404: 'Resursen hittades inte.', 404: 'Resursen hittades inte.',
409: 'Konflikten med befintlig data.', 409: 'Konflikt med befintlig data.',
422: 'Valideringen misslyckades. Kontrollera dina inmatningar.', 422: 'Valideringen misslyckades. Kontrollera dina inmatningar.',
500: 'Serverfel. Försök igen senare.', 500: 'Serverfel. Försök igen senare.',
503: 'Tjänsten är inte tillgänglig.', 503: 'Tjänsten är inte tillgänglig.',
@@ -47,8 +65,3 @@ export async function parseErrorResponse(response: Response): Promise<string> {
return defaultMessages[status] || `Fel (${status}). Försök igen senare.`; 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.';
}