304 lines
9.5 KiB
TypeScript
304 lines
9.5 KiB
TypeScript
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';
|
|
|
|
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 diffMs = bestBefore.getTime() - today.getTime();
|
|
const diffDays = Math.round(diffMs / (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 InventoryPageProps = {
|
|
searchParams?: Promise<{
|
|
location?: string;
|
|
sort?: string;
|
|
}>;
|
|
};
|
|
|
|
function buildInventoryUrl(location?: string, sort?: string) {
|
|
const params = new URLSearchParams();
|
|
|
|
if (location) {
|
|
params.set('location', location);
|
|
}
|
|
|
|
if (sort) {
|
|
params.set('sort', sort);
|
|
}
|
|
|
|
const query = params.toString();
|
|
return query ? `/inventory?${query}` : '/inventory';
|
|
}
|
|
|
|
export default async function InventoryPage({ searchParams }: InventoryPageProps) {
|
|
const resolvedSearchParams = searchParams ? await searchParams : {};
|
|
const location = resolvedSearchParams.location || '';
|
|
const sort = resolvedSearchParams.sort || '';
|
|
|
|
const inventoryPath = (() => {
|
|
const params = new URLSearchParams();
|
|
if (location) params.set('location', location);
|
|
if (sort) params.set('sort', sort);
|
|
const query = params.toString();
|
|
return query ? `/api/inventory?${query}` : '/api/inventory';
|
|
})();
|
|
|
|
const [inventory, products] = await Promise.all([
|
|
fetchJson<InventoryItem[]>(inventoryPath),
|
|
fetchJson<Product[]>('/api/products'),
|
|
]);
|
|
|
|
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' },
|
|
];
|
|
|
|
return (
|
|
<main style={{ padding: '1.5rem', maxWidth: '1000px', margin: '0 auto' }}>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1.5rem' }}>
|
|
<h1 style={{ margin: 0 }}>Varor hemma</h1>
|
|
<Link
|
|
href="/recipes/create"
|
|
style={{
|
|
padding: '0.5rem 1rem',
|
|
background: '#0070f3',
|
|
color: 'white',
|
|
borderRadius: '4px',
|
|
textDecoration: 'none',
|
|
fontWeight: 500,
|
|
fontSize: '1rem',
|
|
transition: 'background 0.2s',
|
|
}}
|
|
>
|
|
Lägg till nytt recept
|
|
</Link>
|
|
</div>
|
|
|
|
<ProductForm />
|
|
<InventoryForm products={products} />
|
|
|
|
<section style={{ marginBottom: '1.5rem' }}>
|
|
<h2>Filter och sortering</h2>
|
|
|
|
<div
|
|
style={{
|
|
display: 'grid',
|
|
gap: '1rem',
|
|
gridTemplateColumns: '1fr 1fr',
|
|
alignItems: 'start',
|
|
}}
|
|
>
|
|
<div>
|
|
<div style={{ fontWeight: 600, marginBottom: '0.5rem' }}>Plats</div>
|
|
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
|
|
{locationOptions.map((option) => {
|
|
const isActive = location === option;
|
|
const label = option === '' ? 'Alla' : option;
|
|
|
|
return (
|
|
<Link
|
|
key={option || 'alla'}
|
|
href={buildInventoryUrl(option || undefined, sort || undefined)}
|
|
style={{
|
|
padding: '0.45rem 0.75rem',
|
|
borderRadius: '999px',
|
|
border: '1px solid #ddd',
|
|
textDecoration: 'none',
|
|
color: '#111',
|
|
background: isActive ? '#efefef' : '#fff',
|
|
fontWeight: isActive ? 600 : 400,
|
|
}}
|
|
>
|
|
{label}
|
|
</Link>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<div style={{ fontWeight: 600, marginBottom: '0.5rem' }}>Sortering</div>
|
|
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
|
|
{sortOptions.map((option) => {
|
|
const isActive = sort === option.value;
|
|
|
|
return (
|
|
<Link
|
|
key={option.value || 'default'}
|
|
href={buildInventoryUrl(location || undefined, option.value || undefined)}
|
|
style={{
|
|
padding: '0.45rem 0.75rem',
|
|
borderRadius: '999px',
|
|
border: '1px solid #ddd',
|
|
textDecoration: 'none',
|
|
color: '#111',
|
|
background: isActive ? '#efefef' : '#fff',
|
|
fontWeight: isActive ? 600 : 400,
|
|
}}
|
|
>
|
|
{option.label}
|
|
</Link>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</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>
|
|
</main>
|
|
);
|
|
} |