feat: enhance error handling with user-friendly messages and improve response parsing
This commit is contained in:
+5
-2
@@ -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`
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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<string> {
|
||||
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();
|
||||
bodyText = await response.text();
|
||||
} catch {
|
||||
// Body kunde inte läsas
|
||||
}
|
||||
|
||||
// Om backend skickade ett felmeddelande
|
||||
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;
|
||||
}
|
||||
if (data.error) {
|
||||
return data.error;
|
||||
}
|
||||
if (data.details) {
|
||||
return data.details;
|
||||
}
|
||||
} catch {
|
||||
// Inte JSON, försök text
|
||||
// Försök tolka som JSON
|
||||
try {
|
||||
const text = await response.text();
|
||||
if (text && text.length < 200) {
|
||||
return text;
|
||||
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') {
|
||||
return translateMessage(data.message);
|
||||
}
|
||||
|
||||
if (typeof data.error === 'string') {
|
||||
return translateMessage(data.error);
|
||||
}
|
||||
|
||||
if (typeof data.details === 'string') {
|
||||
return translateMessage(data.details);
|
||||
}
|
||||
} 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<string> {
|
||||
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<string> {
|
||||
|
||||
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.';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user