From e9b5de44077ddaa164be903bc471352b98a6912f Mon Sep 17 00:00:00 2001 From: Nils-Johan Gynther Date: Sun, 19 Apr 2026 18:22:43 +0200 Subject: [PATCH] feat(admin): refactor product management components for improved state handling and data fetching --- .../app/admin/products/AdminProductList.tsx | 37 ++++++++++++++----- .../app/admin/products/EditProductForm.tsx | 12 +++--- .../app/admin/products/MergePreviewForm.tsx | 18 ++++++--- frontend/app/admin/products/actions.ts | 10 +++++ frontend/app/admin/products/page.tsx | 8 +--- 5 files changed, 57 insertions(+), 28 deletions(-) diff --git a/frontend/app/admin/products/AdminProductList.tsx b/frontend/app/admin/products/AdminProductList.tsx index 84daa7d3..99ed91d3 100644 --- a/frontend/app/admin/products/AdminProductList.tsx +++ b/frontend/app/admin/products/AdminProductList.tsx @@ -1,7 +1,6 @@ 'use client'; -import { useState, useMemo, useEffect, useTransition } from 'react'; -import { useRouter } from 'next/navigation'; +import { useState, useMemo, useEffect, useTransition, useCallback } from 'react'; import type { Product, Category } from '../../../features/inventory/types'; import EditProductForm from './EditProductForm'; import { bulkSetCategory, suggestBulkCategories } from './actions'; @@ -20,10 +19,6 @@ type AiSuggestion = { }; }; -type Props = { - products: Product[]; -}; - const sortOptions = [ { value: 'createdDesc', label: 'Senast tillagda' }, { value: 'nameAsc', label: 'Namn A–Ö' }, @@ -39,8 +34,9 @@ function flattenTree(nodes: CategoryNode[], depth = 0): { id: number; label: str return result; } -export default function AdminProductList({ products }: Props) { - const router = useRouter(); +export default function AdminProductList() { + const [products, setProducts] = useState([]); + const [productsLoading, setProductsLoading] = useState(true); const [search, setSearch] = useState(''); const [sort, setSort] = useState('createdDesc'); const [showUncategorizedOnly, setShowUncategorizedOnly] = useState(false); @@ -57,6 +53,18 @@ export default function AdminProductList({ products }: Props) { const [aiApproved, setAiApproved] = useState>(new Set()); const [aiApplying, setAiApplying] = useState(false); + const refetchProducts = useCallback(() => { + fetch('/api/products') + .then((r) => r.json()) + .then((data) => { if (Array.isArray(data)) setProducts(data); }) + .catch(() => {}) + .finally(() => setProductsLoading(false)); + }, []); + + useEffect(() => { + refetchProducts(); + }, [refetchProducts]); + useEffect(() => { fetch('/api/categories') .then((r) => r.json()) @@ -129,7 +137,7 @@ export default function AdminProductList({ products }: Props) { await bulkSetCategory(ids, categoryId); setSelectedIds(new Set()); setBulkCategoryId(''); - router.refresh(); + refetchProducts(); } catch (err) { setBulkError(err instanceof Error ? err.message : 'Fel vid uppdatering'); } @@ -167,6 +175,7 @@ export default function AdminProductList({ products }: Props) { } setAiSuggestions(null); setAiApproved(new Set()); + refetchProducts(); } catch (err) { setAiError(err instanceof Error ? err.message : 'Fel vid tillämpning'); } finally { @@ -174,6 +183,10 @@ export default function AdminProductList({ products }: Props) { } }; + if (productsLoading) { + return

Laddar produkter…

; + } + return ( <> {/* Sök + sortering + filter */} @@ -337,7 +350,11 @@ export default function AdminProductList({ products }: Props) {
Normalized: {product.normalizedName}
- + setProducts((prev) => prev.map((p) => p.id === updated.id ? updated : p))} + onDeleted={(id) => setProducts((prev) => prev.filter((p) => p.id !== id))} + /> ))} diff --git a/frontend/app/admin/products/EditProductForm.tsx b/frontend/app/admin/products/EditProductForm.tsx index 31e630ed..728ce0eb 100644 --- a/frontend/app/admin/products/EditProductForm.tsx +++ b/frontend/app/admin/products/EditProductForm.tsx @@ -1,7 +1,6 @@ 'use client'; import { useState, useTransition, useEffect } from 'react'; -import { useRouter } from 'next/navigation'; import type { Product } from '../../../features/inventory/types'; import { updateProductWithTags, deleteProduct, suggestProductCategory } from './actions'; @@ -22,6 +21,8 @@ type AiSuggestion = { type Props = { product: Product; + onSaved: (updated: Product) => void; + onDeleted: (id: number) => void; }; const inputStyle: React.CSSProperties = { @@ -33,8 +34,7 @@ const inputStyle: React.CSSProperties = { boxSizing: 'border-box', }; -export default function EditProductForm({ product }: Props) { - const router = useRouter(); +export default function EditProductForm({ product, onSaved, onDeleted }: Props) { const [isOpen, setIsOpen] = useState(false); const [isPending, startTransition] = useTransition(); const [error, setError] = useState(null); @@ -106,10 +106,10 @@ export default function EditProductForm({ product }: Props) { const rawTags = tagInput.split(',').map((t) => t.trim().toLowerCase()).filter(Boolean); startTransition(async () => { try { - await updateProductWithTags(formData, rawTags); + const updated = await updateProductWithTags(formData, rawTags); setSuccess(true); setIsOpen(false); - router.refresh(); + onSaved(updated as Product); } catch (err) { setError(err instanceof Error ? err.message : 'Okänt fel'); } @@ -123,7 +123,7 @@ export default function EditProductForm({ product }: Props) { startTransition(async () => { try { await deleteProduct(product.id); - router.refresh(); + onDeleted(product.id); } catch (err) { setError(err instanceof Error ? err.message : 'Okänt fel'); } diff --git a/frontend/app/admin/products/MergePreviewForm.tsx b/frontend/app/admin/products/MergePreviewForm.tsx index 0a4f0f54..1ef39571 100644 --- a/frontend/app/admin/products/MergePreviewForm.tsx +++ b/frontend/app/admin/products/MergePreviewForm.tsx @@ -1,14 +1,11 @@ 'use client'; -import { useState, useTransition } from 'react'; +import { useState, useTransition, useEffect } from 'react'; import type { MergePreview, Product } from '../../../features/inventory/types'; import { mergeProducts } from '../../inventory/actions'; -type Props = { - products: Product[]; -}; - -export default function MergePreviewForm({ products }: Props) { +export default function MergePreviewForm() { + const [products, setProducts] = useState([]); const [sourceProductId, setSourceProductId] = useState(''); const [targetProductId, setTargetProductId] = useState(''); const [preview, setPreview] = useState(null); @@ -18,6 +15,15 @@ export default function MergePreviewForm({ products }: Props) { const [isConfirming, setIsConfirming] = useState(false); const [isExpanded, setIsExpanded] = useState(false); + useEffect(() => { + if (isExpanded && products.length === 0) { + fetch('/api/products') + .then((r) => r.json()) + .then((data) => { if (Array.isArray(data)) setProducts(data); }) + .catch(() => {}); + } + }, [isExpanded]); + const fetchPreview = () => { setError(null); setSuccessMessage(null); diff --git a/frontend/app/admin/products/actions.ts b/frontend/app/admin/products/actions.ts index c184923e..8473a41d 100644 --- a/frontend/app/admin/products/actions.ts +++ b/frontend/app/admin/products/actions.ts @@ -97,6 +97,8 @@ export async function updateProductWithTags(formData: FormData, tags: string[]) throw new Error(`Kunde inte uppdatera produkt: ${text}`); } + const updatedProduct = await res.json(); + const tagsRes = await fetch(`${API_BASE}/api/products/${id}/tags`, { method: 'PUT', headers: { 'Content-Type': 'application/json', ...authHeaders }, @@ -108,6 +110,14 @@ export async function updateProductWithTags(formData: FormData, tags: string[]) const text = await tagsRes.text(); throw new Error(`Kunde inte uppdatera taggar: ${text}`); } + + // Fetch the complete product (includes tags, categoryRef) after both updates + const fullRes = await fetch(`${API_BASE}/api/products/${id}`, { + headers: authHeaders, + cache: 'no-store', + }); + if (!fullRes.ok) return updatedProduct; + return fullRes.json(); } export async function deleteProduct(id: number) { diff --git a/frontend/app/admin/products/page.tsx b/frontend/app/admin/products/page.tsx index f0ea4ddc..16cda293 100644 --- a/frontend/app/admin/products/page.tsx +++ b/frontend/app/admin/products/page.tsx @@ -1,5 +1,3 @@ -import { fetchJson } from '../../../lib/api'; -import type { Product } from '../../../features/inventory/types'; import MergePreviewForm from './MergePreviewForm'; import AdminProductList from './AdminProductList'; import Navigation from '../../Navigation'; @@ -7,8 +5,6 @@ import ExpandableCreateProductSection from './ExpandableCreateProductSection'; import ResetProductsButton from './ResetProductsButton'; export default async function AdminProductsPage() { - const products = await fetchJson('/api/products'); - return (
@@ -19,9 +15,9 @@ export default async function AdminProductsPage() { - + - +
); } \ No newline at end of file