diff --git a/backend/src/products/dto/update-product.dto.ts b/backend/src/products/dto/update-product.dto.ts
index 8c3fb82a..974732b4 100644
--- a/backend/src/products/dto/update-product.dto.ts
+++ b/backend/src/products/dto/update-product.dto.ts
@@ -6,4 +6,14 @@ export class UpdateProductDto {
@IsNotEmpty()
@MaxLength(191)
name?: string;
+
+ @IsOptional()
+ @IsString()
+ @MaxLength(191)
+ canonicalName?: string;
+
+ @IsOptional()
+ @IsString()
+ @MaxLength(191)
+ category?: string;
}
diff --git a/backend/src/products/products.service.ts b/backend/src/products/products.service.ts
index e7940fd1..3dd3244e 100644
--- a/backend/src/products/products.service.ts
+++ b/backend/src/products/products.service.ts
@@ -104,6 +104,7 @@ export class ProductsService {
name?: string;
normalizedName?: string;
canonicalName?: string;
+ category?: string | null;
} = {};
if (typeof data.name === 'string') {
@@ -132,7 +133,14 @@ export class ProductsService {
updateData.name = name;
updateData.normalizedName = normalizedName;
- updateData.canonicalName = name;
+ }
+
+ if (typeof data.canonicalName === 'string') {
+ updateData.canonicalName = data.canonicalName.trim() || null;
+ }
+
+ if (typeof data.category === 'string') {
+ updateData.category = data.category.trim() || null;
}
return this.prisma.product.update({
diff --git a/frontend/app/admin/products/AdminProductList.tsx b/frontend/app/admin/products/AdminProductList.tsx
index b4d6b57a..a834bc18 100644
--- a/frontend/app/admin/products/AdminProductList.tsx
+++ b/frontend/app/admin/products/AdminProductList.tsx
@@ -2,7 +2,7 @@
import { useState, useMemo } from 'react';
import type { Product } from '../../../features/inventory/types';
-import CanonicalNameForm from './CanonicalNameForm';
+import EditProductForm from './EditProductForm';
type Props = {
products: Product[];
@@ -105,23 +105,24 @@ export default function AdminProductList({ products }: Props) {
gap: '0.5rem',
}}
>
-
-
ID: {product.id}
+
+
+ {product.canonicalName || product.name}
+ {product.canonicalName && product.canonicalName !== product.name && (
+ ({product.name})
+ )}
+ {product.category && (
+
+ {product.category}
+
+ )}
+
+
ID: {product.id}
-
-
Namn: {product.name}
+
+ Normalized: {product.normalizedName}
-
- Canonical name: {product.canonicalName || 'Saknas'}
-
-
- Normalized: {product.normalizedName}
-
-
-
+
))}
diff --git a/frontend/app/admin/products/EditProductForm.tsx b/frontend/app/admin/products/EditProductForm.tsx
new file mode 100644
index 00000000..354afa20
--- /dev/null
+++ b/frontend/app/admin/products/EditProductForm.tsx
@@ -0,0 +1,169 @@
+'use client';
+
+import { useState, useTransition } from 'react';
+import type { Product } from '../../../features/inventory/types';
+import { updateProduct, deleteProduct } from './actions';
+
+type Props = {
+ product: Product;
+};
+
+const inputStyle: React.CSSProperties = {
+ padding: '0.5rem 0.75rem',
+ border: '1px solid #ddd',
+ borderRadius: '4px',
+ fontSize: '1rem',
+ width: '100%',
+ boxSizing: 'border-box',
+};
+
+export default function EditProductForm({ product }: Props) {
+ const [isOpen, setIsOpen] = useState(false);
+ const [isPending, startTransition] = useTransition();
+ const [error, setError] = useState
(null);
+ const [success, setSuccess] = useState(false);
+
+ function handleSubmit(e: React.FormEvent) {
+ e.preventDefault();
+ setError(null);
+ setSuccess(false);
+ const formData = new FormData(e.currentTarget);
+ startTransition(async () => {
+ try {
+ await updateProduct(formData);
+ setSuccess(true);
+ setIsOpen(false);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Okänt fel');
+ }
+ });
+ }
+
+ function handleDelete() {
+ if (!confirm(`Ta bort "${product.name}"? Detta är en mjukradering och kan återställas.`)) return;
+ setError(null);
+ setSuccess(false);
+ startTransition(async () => {
+ try {
+ await deleteProduct(product.id);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Okänt fel');
+ }
+ });
+ }
+
+ return (
+
+
+
+ {success && ✓ Sparat!}
+
+
+ {error &&
{error}
}
+
+ {isOpen && (
+
+ )}
+
+ );
+}
diff --git a/frontend/app/admin/products/actions.ts b/frontend/app/admin/products/actions.ts
new file mode 100644
index 00000000..20a1e0a1
--- /dev/null
+++ b/frontend/app/admin/products/actions.ts
@@ -0,0 +1,43 @@
+'use server';
+
+import { revalidatePath } from 'next/cache';
+import { API_BASE } from '../../../lib/api';
+
+export async function updateProduct(formData: FormData) {
+ const id = Number(formData.get('id'));
+ const name = String(formData.get('name') || '').trim();
+ const canonicalName = String(formData.get('canonicalName') || '').trim();
+ const category = String(formData.get('category') || '').trim();
+
+ const res = await fetch(`${API_BASE}/api/products/${id}`, {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ name: name || undefined,
+ canonicalName: canonicalName || undefined,
+ category: category || null,
+ }),
+ cache: 'no-store',
+ });
+
+ if (!res.ok) {
+ const text = await res.text();
+ throw new Error(`Kunde inte uppdatera produkt: ${text}`);
+ }
+
+ revalidatePath('/admin/products');
+}
+
+export async function deleteProduct(id: number) {
+ const res = await fetch(`${API_BASE}/api/products/${id}`, {
+ method: 'DELETE',
+ cache: 'no-store',
+ });
+
+ if (!res.ok) {
+ const text = await res.text();
+ throw new Error(`Kunde inte ta bort produkt: ${text}`);
+ }
+
+ revalidatePath('/admin/products');
+}