diff --git a/frontend/app/admin/products/AdminProductList.tsx b/frontend/app/admin/products/AdminProductList.tsx index 6abec984..7003fa26 100644 --- a/frontend/app/admin/products/AdminProductList.tsx +++ b/frontend/app/admin/products/AdminProductList.tsx @@ -3,7 +3,7 @@ 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'; +import { bulkSetCategory } from './actions'; type CategoryNode = Category & { children: CategoryNode[] }; @@ -54,15 +54,12 @@ export default function AdminProductList() { const [aiApplying, setAiApplying] = useState(false); const refetchProducts = useCallback(() => { - console.log('[AdminProductList] refetchProducts: starting fetch /api/products'); fetch('/api/products') .then(async (r) => { - console.log('[AdminProductList] refetchProducts: HTTP', r.status); if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); }) .then((data) => { - console.log('[AdminProductList] refetchProducts: got', Array.isArray(data) ? data.length : 'non-array', 'products'); if (Array.isArray(data)) setProducts(data); }) .catch((e) => console.error('[AdminProductList] refetchProducts error:', e)) @@ -157,9 +154,11 @@ export default function AdminProductList() { setAiError(null); setAiSuggestions(null); try { - const results = await suggestBulkCategories(); - setAiSuggestions(results); - setAiApproved(new Set(results.map((r) => r.productId))); + const res = await fetch('/api/admin/bulk-categorize', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}) }); + const data = await res.json(); + if (!res.ok) throw new Error(data?.error ?? 'AI-kategorisering misslyckades'); + setAiSuggestions(data); + setAiApproved(new Set(data.map((r: AiSuggestion) => r.productId))); } catch (err) { setAiError(err instanceof Error ? err.message : 'AI-kategorisering misslyckades'); } finally { diff --git a/frontend/app/admin/products/EditProductForm.tsx b/frontend/app/admin/products/EditProductForm.tsx index d57806f3..bd38d6ce 100644 --- a/frontend/app/admin/products/EditProductForm.tsx +++ b/frontend/app/admin/products/EditProductForm.tsx @@ -2,7 +2,6 @@ import { useState, useEffect } from 'react'; import type { Product } from '../../../features/inventory/types'; -import { suggestProductCategory } from './actions'; type CategoryNode = { id: number; @@ -84,8 +83,10 @@ export default function EditProductForm({ product, onSaved, onDeleted }: Props) setAiError(null); setAiSuggestion(null); try { - const result = await suggestProductCategory(product.id); - setAiSuggestion(result); + const res = await fetch(`/api/admin/suggest-category/${product.id}`); + const data = await res.json(); + if (!res.ok) throw new Error(data?.error ?? 'AI-kategorisering misslyckades'); + setAiSuggestion(data); } catch (err) { setAiError(err instanceof Error ? err.message : 'AI-kategorisering misslyckades'); } finally { @@ -97,7 +98,6 @@ export default function EditProductForm({ product, onSaved, onDeleted }: Props) e.preventDefault(); const formData = new FormData(e.currentTarget); const rawTags = tagInput.split(',').map((t) => t.trim().toLowerCase()).filter(Boolean); - console.log('[EditProductForm] handleSubmit: PATCH /api/admin/product/', product.id); setIsPending(true); setError(null); setSuccess(false); @@ -116,7 +116,6 @@ export default function EditProductForm({ product, onSaved, onDeleted }: Props) }), }); const data = await res.json(); - console.log('[EditProductForm] handleSubmit: HTTP', res.status, data); if (!res.ok) { setError(data?.error ?? 'Okänt fel'); return; @@ -125,7 +124,6 @@ export default function EditProductForm({ product, onSaved, onDeleted }: Props) setIsOpen(false); onSaved(data as Product); } catch (err) { - console.error('[EditProductForm] handleSubmit error:', err); setError(err instanceof Error ? err.message : 'Okänt fel'); } finally { setIsPending(false); @@ -137,7 +135,6 @@ export default function EditProductForm({ product, onSaved, onDeleted }: Props) setError(null); setSuccess(false); setIsPending(true); - console.log('[EditProductForm] handleDelete: DELETE /api/admin/product/', product.id); try { const res = await fetch(`/api/admin/product/${product.id}`, { method: 'DELETE' }); console.log('[EditProductForm] handleDelete: HTTP', res.status); @@ -148,7 +145,6 @@ export default function EditProductForm({ product, onSaved, onDeleted }: Props) } onDeleted(product.id); } catch (err) { - console.error('[EditProductForm] handleDelete error:', err); setError(err instanceof Error ? err.message : 'Okänt fel'); } finally { setIsPending(false); diff --git a/frontend/app/admin/products/actions.ts b/frontend/app/admin/products/actions.ts index 288684d9..bf8d43c6 100644 --- a/frontend/app/admin/products/actions.ts +++ b/frontend/app/admin/products/actions.ts @@ -61,7 +61,6 @@ export async function setProductTags(productId: number, tags: string[]) { export async function updateProductWithTags(formData: FormData, tags: string[]) { const id = Number(formData.get('id')); - console.log('[actions:updateProductWithTags] called for product id:', id, 'tags:', tags); const name = String(formData.get('name') || '').trim(); const canonicalName = String(formData.get('canonicalName') || '').trim(); const category = String(formData.get('category') || '').trim(); @@ -78,7 +77,6 @@ export async function updateProductWithTags(formData: FormData, tags: string[]) if (brand.length > 100) throw new Error('Varumärke får inte vara längre än 100 tecken.'); const authHeaders = await getAuthHeaders(); - console.log('[actions:updateProductWithTags] auth headers present:', !!authHeaders.Authorization); const res = await fetch(`${API_BASE}/api/products/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', ...authHeaders }, @@ -99,7 +97,6 @@ export async function updateProductWithTags(formData: FormData, tags: string[]) } const updatedProduct = await res.json(); - console.log('[actions:updateProductWithTags] PATCH OK, product id:', (updatedProduct as any)?.id); const tagsRes = await fetch(`${API_BASE}/api/products/${id}/tags`, { method: 'PUT', @@ -113,16 +110,13 @@ export async function updateProductWithTags(formData: FormData, tags: string[]) throw new Error(`Kunde inte uppdatera taggar: ${text}`); } - console.log('[actions:updateProductWithTags] tags PUT OK, fetching full product...'); - // Fetch the complete product (includes tags, categoryRef) after both updates + // Fetch the complete product const fullRes = await fetch(`${API_BASE}/api/products/${id}`, { headers: authHeaders, cache: 'no-store', }); - console.log('[actions:updateProductWithTags] full product fetch HTTP', fullRes.status); if (!fullRes.ok) return updatedProduct; const result = await fullRes.json(); - console.log('[actions:updateProductWithTags] returning full product id:', (result as any)?.id); return result; } diff --git a/frontend/app/api/admin/bulk-categorize/route.ts b/frontend/app/api/admin/bulk-categorize/route.ts new file mode 100644 index 00000000..80c9122a --- /dev/null +++ b/frontend/app/api/admin/bulk-categorize/route.ts @@ -0,0 +1,43 @@ +import { auth } from '../../../../auth'; + +const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080'; + +async function getAuthHeaders(): Promise> { + const session = await auth(); + if (!session?.accessToken) return {}; + return { Authorization: `Bearer ${session.accessToken}` }; +} + +// POST /api/admin/bulk-categorize +// Body: { productIds?: number[] } +export async function POST(req: Request) { + try { + const body = await req.json().catch(() => ({})); + const { productIds } = body; + + const authHeaders = await getAuthHeaders(); + if (!authHeaders.Authorization) { + return Response.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const res = await fetch(`${API_BASE}/api/products/ai-categorize-bulk`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...authHeaders }, + body: JSON.stringify({ productIds }), + }); + + if (!res.ok) { + const text = await res.text(); + console.error('[api/admin/bulk-categorize] failed:', res.status, text); + return Response.json({ error: `Bulk-AI-kategorisering misslyckades: ${text}` }, { status: res.status }); + } + + return Response.json(await res.json()); + } catch (err) { + console.error('[api/admin/bulk-categorize] error:', err); + return Response.json( + { error: err instanceof Error ? err.message : 'Unknown error' }, + { status: 500 }, + ); + } +} diff --git a/frontend/app/api/admin/product/[id]/route.ts b/frontend/app/api/admin/product/[id]/route.ts index fb11c474..50a6f62d 100644 --- a/frontend/app/api/admin/product/[id]/route.ts +++ b/frontend/app/api/admin/product/[id]/route.ts @@ -33,8 +33,6 @@ export async function PATCH( return Response.json({ error: 'Unauthorized' }, { status: 401 }); } - console.log('[api/admin/product] PATCH product', productId); - // 1. Update product fields const patchRes = await fetch(`${API_BASE}/api/products/${productId}`, { method: 'PATCH', @@ -55,8 +53,6 @@ export async function PATCH( return Response.json({ error: `Kunde inte uppdatera produkt: ${text}` }, { status: patchRes.status }); } - console.log('[api/admin/product] PATCH OK'); - // 2. Update tags const tagsRes = await fetch(`${API_BASE}/api/products/${productId}/tags`, { method: 'PUT', @@ -70,8 +66,6 @@ export async function PATCH( return Response.json({ error: `Kunde inte uppdatera taggar: ${text}` }, { status: tagsRes.status }); } - console.log('[api/admin/product] tags PUT OK'); - // 3. Return the complete updated product const fullRes = await fetch(`${API_BASE}/api/products/${productId}`, { headers: authHeaders, @@ -82,7 +76,6 @@ export async function PATCH( } const product = await fullRes.json(); - console.log('[api/admin/product] returning full product id:', product?.id); return Response.json(product); } catch (err) { console.error('[api/admin/product] PATCH error:', err); @@ -108,8 +101,6 @@ export async function DELETE( return Response.json({ error: 'Unauthorized' }, { status: 401 }); } - console.log('[api/admin/product] DELETE product', productId); - const res = await fetch(`${API_BASE}/api/products/${productId}`, { method: 'DELETE', headers: authHeaders, @@ -121,7 +112,6 @@ export async function DELETE( return Response.json({ error: `Kunde inte ta bort produkt: ${text}` }, { status: res.status }); } - console.log('[api/admin/product] DELETE OK'); return new Response(null, { status: 204 }); } catch (err) { console.error('[api/admin/product] DELETE error:', err); diff --git a/frontend/app/api/admin/suggest-category/[id]/route.ts b/frontend/app/api/admin/suggest-category/[id]/route.ts new file mode 100644 index 00000000..077829eb --- /dev/null +++ b/frontend/app/api/admin/suggest-category/[id]/route.ts @@ -0,0 +1,44 @@ +import { auth } from '../../../../../auth'; + +const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080'; + +async function getAuthHeaders(): Promise> { + const session = await auth(); + if (!session?.accessToken) return {}; + return { Authorization: `Bearer ${session.accessToken}` }; +} + +// GET /api/admin/suggest-category/[id] +export async function GET( + _req: Request, + { params }: { params: Promise<{ id: string }> }, +) { + try { + const { id } = await params; + const productId = Number(id); + if (!productId) return Response.json({ error: 'Invalid id' }, { status: 400 }); + + const authHeaders = await getAuthHeaders(); + if (!authHeaders.Authorization) { + return Response.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const res = await fetch(`${API_BASE}/api/products/${productId}/suggest-category`, { + headers: authHeaders, + }); + + if (!res.ok) { + const text = await res.text(); + console.error('[api/admin/suggest-category] failed:', res.status, text); + return Response.json({ error: `AI-kategorisering misslyckades: ${text}` }, { status: res.status }); + } + + return Response.json(await res.json()); + } catch (err) { + console.error('[api/admin/suggest-category] error:', err); + return Response.json( + { error: err instanceof Error ? err.message : 'Unknown error' }, + { status: 500 }, + ); + } +}