feat(admin): refactor product management components for improved state handling and data fetching
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user