205 lines
7.6 KiB
TypeScript
205 lines
7.6 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
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[];
|
|
onDeleted?: () => void;
|
|
};
|
|
|
|
export default function InventoryList({ inventory, onDeleted }: Props) {
|
|
const [search, setSearch] = useState('');
|
|
const router = useRouter();
|
|
|
|
// 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} onUpdated={onDeleted ?? (() => router.refresh())} />
|
|
<InventoryConsumeForm id={item.id} unit={item.unit} />
|
|
<InventoryConsumptionHistory id={item.id} />
|
|
<button
|
|
type="button"
|
|
onClick={async () => {
|
|
if (!confirm(`Ta bort "${item.product.canonicalName || item.product.name}" från inventariet?`)) return;
|
|
const res = await fetch(`/api/admin/inventory-item/${item.id}`, { method: 'DELETE' });
|
|
if (res.ok) {
|
|
if (onDeleted) onDeleted();
|
|
else router.refresh();
|
|
}
|
|
}}
|
|
style={{
|
|
padding: '0.5rem 0.75rem',
|
|
background: '#fff0f0',
|
|
border: '1px solid #f5b8b8',
|
|
borderRadius: '6px',
|
|
color: '#c00',
|
|
cursor: 'pointer',
|
|
fontWeight: 500,
|
|
}}
|
|
>
|
|
Ta bort
|
|
</button>
|
|
</div>
|
|
</article>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</section>
|
|
);
|
|
}
|