feat(api): implement new API routes for bulk category updates, inventory consumption, and product management with authentication
This commit is contained in:
@@ -1,9 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useMemo, useEffect, useTransition, useCallback } from 'react';
|
||||
import { useState, useMemo, useEffect, useCallback } from 'react';
|
||||
import type { Product, Category } from '../../../features/inventory/types';
|
||||
import EditProductForm from './EditProductForm';
|
||||
import { bulkSetCategory } from './actions';
|
||||
|
||||
type CategoryNode = Category & { children: CategoryNode[] };
|
||||
|
||||
@@ -43,7 +42,7 @@ export default function AdminProductList() {
|
||||
const [selectedIds, setSelectedIds] = useState<Set<number>>(new Set());
|
||||
const [bulkCategoryId, setBulkCategoryId] = useState<string>('');
|
||||
const [categoryTree, setCategoryTree] = useState<CategoryNode[]>([]);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [isBulkPending, setIsBulkPending] = useState(false);
|
||||
const [bulkError, setBulkError] = useState<string | null>(null);
|
||||
|
||||
// AI-kategorisering state
|
||||
@@ -132,21 +131,30 @@ export default function AdminProductList() {
|
||||
});
|
||||
};
|
||||
|
||||
const handleBulkApply = () => {
|
||||
const handleBulkApply = async () => {
|
||||
setBulkError(null);
|
||||
const ids = Array.from(selectedIds);
|
||||
if (ids.length === 0) return;
|
||||
const categoryId = bulkCategoryId === '' ? null : bulkCategoryId === '__remove__' ? null : Number(bulkCategoryId);
|
||||
startTransition(async () => {
|
||||
try {
|
||||
await bulkSetCategory(ids, categoryId);
|
||||
setSelectedIds(new Set());
|
||||
setBulkCategoryId('');
|
||||
refetchProducts();
|
||||
} catch (err) {
|
||||
setBulkError(err instanceof Error ? err.message : 'Fel vid uppdatering');
|
||||
setIsBulkPending(true);
|
||||
try {
|
||||
const res = await fetch('/api/admin/bulk-set-category', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ids, categoryId }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
throw new Error(data?.error || 'Fel vid uppdatering');
|
||||
}
|
||||
});
|
||||
setSelectedIds(new Set());
|
||||
setBulkCategoryId('');
|
||||
refetchProducts();
|
||||
} catch (err) {
|
||||
setBulkError(err instanceof Error ? err.message : 'Fel vid uppdatering');
|
||||
} finally {
|
||||
setIsBulkPending(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAiCategorize = async () => {
|
||||
@@ -178,7 +186,15 @@ export default function AdminProductList() {
|
||||
grouped.get(cid)!.push(s.productId);
|
||||
}
|
||||
for (const [categoryId, ids] of grouped.entries()) {
|
||||
await bulkSetCategory(ids, categoryId);
|
||||
const res = await fetch('/api/admin/bulk-set-category', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ids, categoryId }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
throw new Error(data?.error || 'Fel vid tillämpning');
|
||||
}
|
||||
}
|
||||
setAiSuggestions(null);
|
||||
setAiApproved(new Set());
|
||||
@@ -287,10 +303,10 @@ export default function AdminProductList() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleBulkApply}
|
||||
disabled={isPending}
|
||||
disabled={isBulkPending}
|
||||
style={{ padding: '0.4rem 0.9rem', background: '#0070f3', color: '#fff', border: 'none', borderRadius: '6px', cursor: 'pointer', fontWeight: 600, fontSize: '0.9rem' }}
|
||||
>
|
||||
{isPending ? 'Sparar…' : 'Sätt kategori'}
|
||||
{isBulkPending ? 'Sparar…' : 'Sätt kategori'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useTransition, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import type { MergePreview, Product } from '../../../features/inventory/types';
|
||||
import { mergeProducts } from '../../inventory/actions';
|
||||
|
||||
export default function MergePreviewForm() {
|
||||
const [products, setProducts] = useState<Product[]>([]);
|
||||
@@ -14,6 +14,7 @@ export default function MergePreviewForm() {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [isConfirming, setIsConfirming] = useState(false);
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (isExpanded && products.length === 0) {
|
||||
@@ -74,11 +75,19 @@ export default function MergePreviewForm() {
|
||||
|
||||
startTransition(async () => {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.set('sourceProductId', String(preview.source.id));
|
||||
formData.set('targetProductId', String(preview.target.id));
|
||||
const res = await fetch('/api/admin/merge-products', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
sourceProductId: preview.source.id,
|
||||
targetProductId: preview.target.id,
|
||||
}),
|
||||
});
|
||||
|
||||
await mergeProducts(formData);
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
throw new Error(data?.error || 'Sammanslagning misslyckades');
|
||||
}
|
||||
|
||||
setSuccessMessage(
|
||||
`Produkten "${preview.source.canonicalName || preview.source.name}" slogs ihop med "${preview.target.canonicalName || preview.target.name}".`,
|
||||
@@ -87,6 +96,7 @@ export default function MergePreviewForm() {
|
||||
setIsConfirming(false);
|
||||
setSourceProductId('');
|
||||
setTargetProductId('');
|
||||
router.refresh();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Okänt fel');
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useTransition } from 'react';
|
||||
import { resetAllProducts } from './actions';
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export default function ResetProductsButton() {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const router = useRouter();
|
||||
|
||||
function handleClick() {
|
||||
async function handleClick() {
|
||||
if (
|
||||
!confirm(
|
||||
'⚠️ Detta raderar ALLA produkter, inventory, taggar, kvitto-alias och pantry.\n\nKategorier och användare behålls.\n\nÄr du säker?',
|
||||
@@ -16,13 +17,19 @@ export default function ResetProductsButton() {
|
||||
return;
|
||||
|
||||
setError(null);
|
||||
startTransition(async () => {
|
||||
try {
|
||||
await resetAllProducts();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Okänt fel');
|
||||
setIsPending(true);
|
||||
try {
|
||||
const res = await fetch('/api/admin/reset-products', { method: 'POST' });
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
throw new Error(data?.error || 'Återställning misslyckades');
|
||||
}
|
||||
});
|
||||
router.refresh();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Okänt fel');
|
||||
} finally {
|
||||
setIsPending(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user