feat: implement inventory and pantry management views with CRUD functionality and user-friendly interfaces

This commit is contained in:
Nils-Johan Gynther
2026-04-21 14:43:18 +02:00
parent 82c3dc3fee
commit 81b63b3fdb
14 changed files with 352 additions and 59 deletions
@@ -0,0 +1,52 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import type { InventoryItem, Product } from '../../../../features/inventory/types';
import InventoryList from '../../../inventory/InventoryList';
import InventoryForm from '../../../inventory/InventoryForm';
export default function InventoryView() {
const [inventory, setInventory] = useState<InventoryItem[]>([]);
const [products, setProducts] = useState<Product[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const load = useCallback(async () => {
setLoading(true);
setError(null);
try {
const [invRes, prodRes] = await Promise.all([
fetch('/api/inventory'),
fetch('/api/products'),
]);
if (!invRes.ok) throw new Error('Kunde inte hämta inventarie');
if (!prodRes.ok) throw new Error('Kunde inte hämta produkter');
const [inv, prods] = await Promise.all([invRes.json(), prodRes.json()]);
setInventory(inv);
setProducts(prods);
} catch (e) {
setError(e instanceof Error ? e.message : 'Okänt fel');
} finally {
setLoading(false);
}
}, []);
useEffect(() => { load(); }, [load]);
if (loading) return <p style={{ color: '#888' }}>Laddar inventarie</p>;
if (error) return <p style={{ color: '#c00' }}>{error}</p>;
return (
<div>
<p style={{ color: '#555', marginBottom: '1rem' }}>
Lägg till, redigera och ta bort varor i ditt inventarie.
</p>
{/* Formulär för att lägga till vara — viker ut sig vid klick */}
<InventoryForm products={products} onCreated={load} />
{/* Lista med redigera/ta bort per rad */}
<InventoryList inventory={inventory} onDeleted={load} />
</div>
);
}
@@ -0,0 +1,83 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import type { Product } from '../../../../features/inventory/types';
import AddToPantryForm from '../../../baslager/AddToPantryForm';
import PantryList from '../../../baslager/PantryList';
type PantryItem = {
id: number;
productId: number;
createdAt: string;
updatedAt: string;
product: Product;
};
type InventoryItem = {
productId: number;
quantity: string;
unit: string;
};
export default function PantryView() {
const [pantryItems, setPantryItems] = useState<PantryItem[]>([]);
const [products, setProducts] = useState<Product[]>([]);
const [inventoryByProductId, setInventoryByProductId] = useState<Record<number, InventoryItem[]>>({});
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const load = useCallback(async () => {
setLoading(true);
setError(null);
try {
const [pantryRes, prodRes, invRes] = await Promise.all([
fetch('/api/pantry'),
fetch('/api/products'),
fetch('/api/inventory').catch(() => null),
]);
if (!pantryRes.ok) throw new Error('Kunde inte hämta baslager');
if (!prodRes.ok) throw new Error('Kunde inte hämta produkter');
const [pantry, prods] = await Promise.all([pantryRes.json(), prodRes.json()]);
const inv: InventoryItem[] = invRes?.ok ? await invRes.json() : [];
setPantryItems(pantry);
setProducts(prods);
const byProd = inv.reduce<Record<number, InventoryItem[]>>((acc, item) => {
if (!acc[item.productId]) acc[item.productId] = [];
acc[item.productId].push(item);
return acc;
}, {});
setInventoryByProductId(byProd);
} catch (e) {
setError(e instanceof Error ? e.message : 'Okänt fel');
} finally {
setLoading(false);
}
}, []);
useEffect(() => { load(); }, [load]);
if (loading) return <p style={{ color: '#888' }}>Laddar baslager</p>;
if (error) return <p style={{ color: '#c00' }}>{error}</p>;
const pantryProductIds = new Set(pantryItems.map((i) => i.productId));
return (
<div>
<p style={{ color: '#555', marginBottom: '1rem' }}>
Produkter du alltid räknar med att ha hemma. Lägg till och ta bort varor i ditt baslager.
</p>
<section style={{ marginBottom: '2rem' }}>
<h2 style={{ fontSize: '1.1rem', marginBottom: '0.75rem' }}>Lägg till produkt</h2>
<AddToPantryForm products={products} pantryProductIds={pantryProductIds} onCreated={load} />
</section>
<section>
<h2 style={{ fontSize: '1.1rem', marginBottom: '0.75rem' }}>
{pantryItems.length} {pantryItems.length === 1 ? 'produkt' : 'produkter'} i baslagret
</h2>
<PantryList items={pantryItems} inventoryByProductId={inventoryByProductId} onDeleted={load} />
</section>
</div>
);
}
@@ -0,0 +1,70 @@
'use client';
import { useState } from 'react';
import MergePreviewForm from '../../../admin/products/MergePreviewForm';
import AdminProductList from '../../../admin/products/AdminProductList';
import ExpandableCreateProductSection from '../../../admin/products/ExpandableCreateProductSection';
import ResetProductsButton from '../../../admin/products/ResetProductsButton';
import DeletedProductsView from '../../../admin/products/DeletedProductsView';
const subTabs = [
{ id: 'varor', label: '📦 Varor' },
{ id: 'skapa-merge', label: ' Skapa / Slå ihop' },
{ id: 'papperskorg', label: '🗑️ Papperskorg' },
] as const;
type SubTabId = typeof subTabs[number]['id'];
const tabStyle = (active: boolean): React.CSSProperties => ({
padding: '0.4rem 1rem',
border: 'none',
borderBottom: active ? '2.5px solid #333' : '2.5px solid transparent',
background: 'none',
cursor: 'pointer',
fontWeight: active ? 600 : 400,
color: active ? '#111' : '#666',
fontSize: '0.95rem',
transition: 'color 0.15s, border-color 0.15s',
});
export default function ProductsView() {
const [activeSubTab, setActiveSubTab] = useState<SubTabId>('varor');
return (
<div>
<div style={{ display: 'flex', gap: '0.25rem', borderBottom: '1px solid #ddd', marginBottom: '1.5rem' }}>
{subTabs.map((tab) => (
<button
key={tab.id}
style={tabStyle(activeSubTab === tab.id)}
onClick={() => setActiveSubTab(tab.id)}
>
{tab.label}
</button>
))}
</div>
{activeSubTab === 'varor' && (
<div>
<p style={{ color: '#555', marginBottom: '1rem' }}>
Granska och standardisera produktnamn samt hantera kategorier.
</p>
<AdminProductList />
</div>
)}
{activeSubTab === 'skapa-merge' && (
<div>
<p style={{ color: '#555', marginBottom: '1rem' }}>
Skapa ny produkt, återställ produktdatabas eller slå ihop dubbletter.
</p>
<ExpandableCreateProductSection />
<ResetProductsButton />
<MergePreviewForm />
</div>
)}
{activeSubTab === 'papperskorg' && <DeletedProductsView />}
</div>
);
}