From 556a0fdc306e279ce4bf52ec4178914dddbdf448 Mon Sep 17 00:00:00 2001 From: Nils-Johan Gynther Date: Fri, 10 Apr 2026 19:10:50 +0200 Subject: [PATCH] Add sorting by name functionality and implement AdminProductList component for product management --- backend/src/inventory/inventory.service.ts | 2 + .../app/admin/products/AdminProductList.tsx | 130 ++++++++++++++++++ frontend/app/admin/products/page.tsx | 35 +---- frontend/app/inventory/InventoryForm.tsx | 38 ++++- frontend/app/inventory/page.tsx | 7 +- 5 files changed, 172 insertions(+), 40 deletions(-) create mode 100644 frontend/app/admin/products/AdminProductList.tsx diff --git a/backend/src/inventory/inventory.service.ts b/backend/src/inventory/inventory.service.ts index f490cbdd..12a11a68 100644 --- a/backend/src/inventory/inventory.service.ts +++ b/backend/src/inventory/inventory.service.ts @@ -26,6 +26,8 @@ export class InventoryService { orderBy.push({ bestBeforeDate: 'asc' }); } else if (query?.sort === 'bestBeforeDesc') { orderBy.push({ bestBeforeDate: 'desc' }); + } else if (query?.sort === 'nameAsc') { + orderBy.push({ product: { name: 'asc' } } as any); } else if (query?.sort === 'purchaseDateAsc') { orderBy.push({ purchaseDate: 'asc' }); } else if (query?.sort === 'purchaseDateDesc') { diff --git a/frontend/app/admin/products/AdminProductList.tsx b/frontend/app/admin/products/AdminProductList.tsx new file mode 100644 index 00000000..b4d6b57a --- /dev/null +++ b/frontend/app/admin/products/AdminProductList.tsx @@ -0,0 +1,130 @@ +'use client'; + +import { useState, useMemo } from 'react'; +import type { Product } from '../../../features/inventory/types'; +import CanonicalNameForm from './CanonicalNameForm'; + +type Props = { + products: Product[]; +}; + +const sortOptions = [ + { value: 'createdDesc', label: 'Senast tillagda' }, + { value: 'nameAsc', label: 'Namn A–Ö' }, +]; + +export default function AdminProductList({ products }: Props) { + const [search, setSearch] = useState(''); + const [sort, setSort] = useState('createdDesc'); + + const filtered = useMemo(() => { + const q = search.trim().toLowerCase(); + + let result = q + ? products.filter( + (p) => + p.name.toLowerCase().includes(q) || + (p.canonicalName ?? '').toLowerCase().includes(q) || + (p.normalizedName ?? '').toLowerCase().includes(q), + ) + : [...products]; + + if (sort === 'nameAsc') { + result.sort((a, b) => + (a.canonicalName || a.name).localeCompare(b.canonicalName || b.name, 'sv'), + ); + } else { + result.sort((a, b) => b.id - a.id); + } + + return result; + }, [products, search, sort]); + + return ( + <> +
+ setSearch(e.target.value)} + style={{ + flex: '1 1 200px', + padding: '0.5rem 0.75rem', + border: '1px solid #ddd', + borderRadius: '6px', + fontSize: '1rem', + }} + /> + +
+ {sortOptions.map((opt) => ( + + ))} +
+ + {search && ( + + {filtered.length} av {products.length} produkter + + )} +
+ +
+ {filtered.map((product) => ( +
+
+ ID: {product.id} +
+
+ Namn: {product.name} +
+
+ Canonical name: {product.canonicalName || 'Saknas'} +
+
+ Normalized: {product.normalizedName} +
+ + +
+ ))} +
+ + ); +} diff --git a/frontend/app/admin/products/page.tsx b/frontend/app/admin/products/page.tsx index 6caaa628..93465bb9 100644 --- a/frontend/app/admin/products/page.tsx +++ b/frontend/app/admin/products/page.tsx @@ -1,7 +1,7 @@ import { fetchJson } from '../../../lib/api'; import type { Product } from '../../../features/inventory/types'; -import CanonicalNameForm from './CanonicalNameForm'; import MergePreviewForm from './MergePreviewForm'; +import AdminProductList from './AdminProductList'; export default async function AdminProductsPage() { const products = await fetchJson('/api/products'); @@ -13,38 +13,7 @@ export default async function AdminProductsPage() { -
- {products.map((product) => ( -
-
- ID: {product.id} -
-
- Namn: {product.name} -
-
- Canonical name: {product.canonicalName || 'Saknas'} -
-
- Normalized: {product.normalizedName} -
- - -
- ))} -
+ ); } \ No newline at end of file diff --git a/frontend/app/inventory/InventoryForm.tsx b/frontend/app/inventory/InventoryForm.tsx index 9a537aa1..d7cf1748 100644 --- a/frontend/app/inventory/InventoryForm.tsx +++ b/frontend/app/inventory/InventoryForm.tsx @@ -11,6 +11,7 @@ type Props = { export default function InventoryForm({ products }: Props) { const [isPending, setIsPending] = useState(false); const [error, setError] = useState(null); + const [isOpen, setIsOpen] = useState(false); const UNIT_OPTIONS = [ { value: '', label: 'Välj enhet' }, @@ -56,8 +57,32 @@ export default function InventoryForm({ products }: Props) { } return ( -
{ +
+ + + {isOpen && ( + { e.preventDefault(); setError(null); setIsPending(true); @@ -82,11 +107,12 @@ export default function InventoryForm({ products }: Props) { gap: '0.75rem', padding: '1rem', border: '1px solid #ddd', - borderRadius: '8px', - marginBottom: '1.5rem', + borderTop: 'none', + borderRadius: '0 0 8px 8px', + marginBottom: '0', }} > -

Lägg till hemmavara

+

Lägg till hemmavara

); } \ No newline at end of file diff --git a/frontend/app/inventory/page.tsx b/frontend/app/inventory/page.tsx index 1e0bd42f..7ee2c490 100644 --- a/frontend/app/inventory/page.tsx +++ b/frontend/app/inventory/page.tsx @@ -98,8 +98,9 @@ export default async function InventoryPage({ searchParams }: InventoryPageProps const locationOptions = ['', 'Kyl', 'Frys', 'Skafferi']; const sortOptions = [ { value: '', label: 'Senast tillagda' }, - { value: 'bestBeforeAsc', label: 'Bäst före Stigande' }, - { value: 'bestBeforeDesc', label: 'Bäst före Fallande' }, + { value: 'nameAsc', label: 'Namn A\u2013\u00d6' }, + { value: 'bestBeforeAsc', label: 'B\u00e4st f\u00f6re Stigande' }, + { value: 'bestBeforeDesc', label: 'B\u00e4st f\u00f6re Fallande' }, ]; return ( @@ -131,6 +132,7 @@ export default async function InventoryPage({ searchParams }: InventoryPageProps