feat(admin): refactor product management components for improved state handling and data fetching

This commit is contained in:
Nils-Johan Gynther
2026-04-19 18:22:43 +02:00
parent f12d881395
commit e9b5de4407
5 changed files with 57 additions and 28 deletions
@@ -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<Product[]>([]);
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<Set<number>>(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 <p style={{ color: '#888', marginTop: '1rem' }}>Laddar produkter</p>;
}
return (
<>
{/* Sök + sortering + filter */}
@@ -337,7 +350,11 @@ export default function AdminProductList({ products }: Props) {
<div style={{ fontSize: '0.8rem', color: '#888' }}>
Normalized: {product.normalizedName}
</div>
<EditProductForm product={product} />
<EditProductForm
product={product}
onSaved={(updated) => setProducts((prev) => prev.map((p) => p.id === updated.id ? updated : p))}
onDeleted={(id) => setProducts((prev) => prev.filter((p) => p.id !== id))}
/>
</article>
))}
</div>
@@ -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<string | null>(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');
}
@@ -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<Product[]>([]);
const [sourceProductId, setSourceProductId] = useState('');
const [targetProductId, setTargetProductId] = useState('');
const [preview, setPreview] = useState<MergePreview | null>(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);
+10
View File
@@ -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) {
+2 -6
View File
@@ -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<Product[]>('/api/products');
return (
<main style={{ padding: '1rem', maxWidth: '1100px', margin: '0 auto' }}>
<Navigation />
@@ -19,9 +15,9 @@ export default async function AdminProductsPage() {
<ResetProductsButton />
<MergePreviewForm products={products} />
<MergePreviewForm />
<AdminProductList products={products} />
<AdminProductList />
</main>
);
}