Add InventoryList component for improved inventory display and search functionality

This commit is contained in:
Nils-Johan Gynther
2026-04-10 18:57:21 +02:00
parent dd17656e4c
commit 33cb4e5328
2 changed files with 181 additions and 106 deletions
+179
View File
@@ -0,0 +1,179 @@
'use client';
import { useState } from 'react';
import type { InventoryItem } from '../../features/inventory/types';
import InventoryEditForm from './InventoryEditForm';
import InventoryConsumeForm from './InventoryConsumeForm';
import InventoryConsumptionHistory from './InventoryConsumptionHistory';
function formatDate(value: string | null) {
if (!value) return null;
return new Date(value).toLocaleDateString('sv-SE');
}
function getBestBeforeStatus(bestBeforeDate: string | null) {
if (!bestBeforeDate) {
return { label: 'Ingen bäst före angiven', color: '#666', background: '#f5f5f5', border: '#ddd' };
}
const today = new Date();
const bestBefore = new Date(bestBeforeDate);
today.setHours(0, 0, 0, 0);
bestBefore.setHours(0, 0, 0, 0);
const diffDays = Math.round((bestBefore.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
if (diffDays < 0) return { label: 'Utgången', color: '#8b0000', background: '#ffeaea', border: '#f1b5b5' };
if (diffDays <= 3) return { label: 'Snart utgången', color: '#8a4b00', background: '#fff4e5', border: '#f0cf9b' };
return { label: 'OK', color: '#1f5f2c', background: '#ecf8ee', border: '#b9e0bf' };
}
type Props = {
inventory: InventoryItem[];
};
export default function InventoryList({ inventory }: Props) {
const [search, setSearch] = useState('');
// Unika produktnamn för autocomplete
const autocompleteNames = Array.from(
new Set(
inventory.map((item) => item.product.canonicalName || item.product.name)
)
).sort();
// Filtrera baserat på söktext
const filtered = search.trim()
? inventory.filter((item) => {
const q = search.trim().toLowerCase();
const name = (item.product.canonicalName || item.product.name).toLowerCase();
const brand = (item.brand || '').toLowerCase();
const loc = (item.location || '').toLowerCase();
const comment = (item.comment || '').toLowerCase();
const suitable = (item.suitableFor || '').toLowerCase();
return (
name.includes(q) ||
brand.includes(q) ||
loc.includes(q) ||
comment.includes(q) ||
suitable.includes(q)
);
})
: inventory;
return (
<section>
<h2>Aktuella hemmavaror (inventory)</h2>
{/* Sökfält */}
<div style={{ marginBottom: '1rem' }}>
<input
type="search"
list="inventory-autocomplete"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Sök vara, varumärke, plats..."
style={{
width: '100%',
padding: '0.6rem 0.75rem',
border: '1px solid #ccc',
borderRadius: '6px',
fontSize: '1rem',
}}
/>
<datalist id="inventory-autocomplete">
{autocompleteNames.map((name) => (
<option key={name} value={name} />
))}
</datalist>
{search && (
<div style={{ marginTop: '0.4rem', fontSize: '0.9rem', color: '#555' }}>
{filtered.length} av {inventory.length} varor visas
</div>
)}
</div>
{filtered.length === 0 ? (
<p>Inga hemmavaror matchar sökningen.</p>
) : (
<div style={{ display: 'grid', gap: '0.75rem' }}>
{filtered.map((item) => {
const bestBeforeStatus = getBestBeforeStatus(item.bestBeforeDate);
return (
<article
key={item.id}
style={{
border: `1px solid ${bestBeforeStatus.border}`,
borderRadius: '10px',
padding: '1rem',
display: 'flex',
flexDirection: 'column',
gap: '0.6rem',
background: '#fff',
boxShadow: '0 1px 2px rgba(0,0,0,0.03)',
}}
>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'flex-start',
gap: '1rem',
flexWrap: 'wrap',
}}
>
<div>
<strong style={{ fontSize: '1rem' }}>
{item.product.canonicalName || item.product.name}
</strong>
<div style={{ marginTop: '0.2rem', color: '#444' }}>
{item.quantity} {item.unit}
</div>
</div>
<div
style={{
padding: '0.3rem 0.6rem',
borderRadius: '999px',
background: bestBeforeStatus.background,
color: bestBeforeStatus.color,
border: `1px solid ${bestBeforeStatus.border}`,
fontSize: '0.85rem',
fontWeight: 600,
whiteSpace: 'nowrap',
}}
>
{bestBeforeStatus.label}
</div>
</div>
<div style={{ display: 'grid', gap: '0.35rem', color: '#333' }}>
{item.location ? <div>Plats: {item.location}</div> : null}
{item.brand ? <div>Varumärke: {item.brand}</div> : null}
<div>Öppnad: {item.opened ? 'Ja' : 'Nej'}</div>
{item.suitableFor ? <div>Passar till: {item.suitableFor}</div> : null}
{item.bestBeforeDate ? <div>Bäst före: {formatDate(item.bestBeforeDate)}</div> : null}
{item.comment ? <div>Kommentar: {item.comment}</div> : null}
</div>
<div
style={{
marginTop: '0.75rem',
paddingTop: '0.75rem',
borderTop: '1px solid #eee',
display: 'flex',
gap: '0.75rem',
flexWrap: 'wrap',
alignItems: 'center',
justifyContent: 'flex-start',
}}
>
<InventoryEditForm item={item} />
<InventoryConsumeForm id={item.id} unit={item.unit} />
<InventoryConsumptionHistory id={item.id} />
</div>
</article>
);
})}
</div>
)}
</section>
);
}
+2 -106
View File
@@ -1,11 +1,9 @@
import InventoryForm from './InventoryForm';
import InventoryEditForm from './InventoryEditForm';
import InventoryConsumeForm from './InventoryConsumeForm';
import ProductForm from './ProductForm';
import Link from 'next/link';
import { fetchJson } from '../../lib/api';
import type { InventoryItem, Product } from '../../features/inventory/types';
import InventoryConsumptionHistory from './InventoryConsumptionHistory';
import InventoryList from './InventoryList';
function formatDate(value: string | null) {
if (!value) return null;
@@ -179,109 +177,7 @@ export default async function InventoryPage({ searchParams }: InventoryPageProps
</div>
</section>
<section>
<h2>Aktuella hemmavaror (inventory)</h2>
{inventory.length === 0 ? (
<p>Inga hemmavaror för det valda filtret.</p>
) : (
<div style={{ display: 'grid', gap: '0.75rem' }}>
{inventory.map((item) => {
const bestBeforeStatus = getBestBeforeStatus(item.bestBeforeDate);
return (
<article
key={item.id}
style={{
border: `1px solid ${bestBeforeStatus.border}`,
borderRadius: '10px',
padding: '1rem',
display: 'flex',
flexDirection: 'column',
gap: '0.6rem',
background: '#fff',
boxShadow: '0 1px 2px rgba(0,0,0,0.03)',
}}
>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'flex-start',
gap: '1rem',
flexWrap: 'wrap',
}}
>
<div>
<strong style={{ fontSize: '1rem' }}>
{item.product.canonicalName || item.product.name}
</strong>
<div style={{ marginTop: '0.2rem', color: '#444' }}>
{item.quantity} {item.unit}
</div>
</div>
<div
style={{
padding: '0.3rem 0.6rem',
borderRadius: '999px',
background: bestBeforeStatus.background,
color: bestBeforeStatus.color,
border: `1px solid ${bestBeforeStatus.border}`,
fontSize: '0.85rem',
fontWeight: 600,
whiteSpace: 'nowrap',
}}
>
{bestBeforeStatus.label}
</div>
</div>
<div
style={{
display: 'grid',
gap: '0.35rem',
color: '#333',
}}
>
{item.location ? <div>Plats: {item.location}</div> : null}
{item.brand ? <div>Varumärke: {item.brand}</div> : null}
<div>Öppnad: {item.opened ? 'Ja' : 'Nej'}</div>
{item.suitableFor ? (
<div>Passar till: {item.suitableFor}</div>
) : null}
{item.bestBeforeDate ? (
<div>Bäst före: {formatDate(item.bestBeforeDate)}</div>
) : null}
{item.comment ? <div>Kommentar: {item.comment}</div> : null}</div>
<div
style={{
marginTop: '0.75rem',
paddingTop: '0.75rem',
borderTop: '1px solid #eee',
display: 'flex',
gap: '0.75rem',
flexWrap: 'wrap',
alignItems: 'center',
justifyContent: 'flex-start',
}}
>
<InventoryEditForm item={item} />
<InventoryConsumeForm id={item.id} unit={item.unit} />
<InventoryConsumptionHistory id={item.id} />
</div>
</article>
);
})}
</div>
)}
</section>
<InventoryList inventory={inventory} />
</main>
);
}