251 lines
8.2 KiB
TypeScript
251 lines
8.2 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useTransition } from 'react';
|
|
import type {
|
|
Recipe,
|
|
RecipeInventoryPreview,
|
|
} from '../../features/inventory/types';
|
|
|
|
type Props = {
|
|
recipes: Recipe[];
|
|
};
|
|
|
|
function getStatusStyle(status: 'enough' | 'missing' | 'unit_mismatch') {
|
|
if (status === 'enough') {
|
|
return {
|
|
label: 'Räcker',
|
|
color: '#1f5f2c',
|
|
background: '#ecf8ee',
|
|
border: '#b9e0bf',
|
|
};
|
|
}
|
|
|
|
if (status === 'missing') {
|
|
return {
|
|
label: 'Saknas',
|
|
color: '#8b0000',
|
|
background: '#ffeaea',
|
|
border: '#f1b5b5',
|
|
};
|
|
}
|
|
|
|
return {
|
|
label: 'Enhetskonflikt',
|
|
color: '#8a4b00',
|
|
background: '#fff4e5',
|
|
border: '#f0cf9b',
|
|
};
|
|
}
|
|
|
|
function formatDate(value: string | null) {
|
|
if (!value) return null;
|
|
return new Date(value).toLocaleDateString('sv-SE');
|
|
}
|
|
|
|
export default function RecipePreview({ recipes }: Props) {
|
|
const [selectedRecipeId, setSelectedRecipeId] = useState('');
|
|
const [preview, setPreview] = useState<RecipeInventoryPreview | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [isPending, startTransition] = useTransition();
|
|
|
|
const loadPreview = () => {
|
|
setError(null);
|
|
setPreview(null);
|
|
|
|
if (!selectedRecipeId) {
|
|
setError('Välj ett recept.');
|
|
return;
|
|
}
|
|
|
|
startTransition(async () => {
|
|
try {
|
|
const res = await fetch(`/api/recipe-preview-proxy?id=${selectedRecipeId}`, {
|
|
method: 'GET',
|
|
cache: 'no-store',
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const text = await res.text();
|
|
throw new Error(text || 'Kunde inte hämta recept-preview.');
|
|
}
|
|
|
|
const data: RecipeInventoryPreview = await res.json();
|
|
setPreview(data);
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Okänt fel');
|
|
}
|
|
});
|
|
};
|
|
|
|
return (
|
|
<section style={{ display: 'grid', gap: '1rem' }}>
|
|
<div
|
|
style={{
|
|
border: '1px solid #ddd',
|
|
borderRadius: '8px',
|
|
padding: '1rem',
|
|
display: 'grid',
|
|
gap: '0.75rem',
|
|
}}
|
|
>
|
|
<h2 style={{ margin: 0 }}>Recept mot hemmavaror</h2>
|
|
|
|
<label>
|
|
Recept
|
|
<br />
|
|
<select
|
|
value={selectedRecipeId}
|
|
onChange={(e) => setSelectedRecipeId(e.target.value)}
|
|
style={{ width: '100%', padding: '0.5rem' }}
|
|
>
|
|
<option value="">Välj recept</option>
|
|
{recipes.map((recipe) => (
|
|
<option key={recipe.id} value={recipe.id}>
|
|
{recipe.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
|
|
<div>
|
|
<button
|
|
type="button"
|
|
onClick={loadPreview}
|
|
disabled={isPending}
|
|
style={{ padding: '0.6rem 1rem' }}
|
|
>
|
|
{isPending ? 'Hämtar preview...' : 'Visa preview'}
|
|
</button>
|
|
</div>
|
|
|
|
{error ? <p style={{ color: 'crimson', margin: 0 }}>{error}</p> : null}
|
|
</div>
|
|
|
|
{preview ? (
|
|
<section style={{ display: 'grid', gap: '1rem' }}>
|
|
<article
|
|
style={{
|
|
border: '1px solid #ddd',
|
|
borderRadius: '8px',
|
|
padding: '1rem',
|
|
display: 'grid',
|
|
gap: '0.5rem',
|
|
}}
|
|
>
|
|
<h3 style={{ margin: 0 }}>{preview.recipe.name}</h3>
|
|
{preview.recipe.description ? <div>{preview.recipe.description}</div> : null}
|
|
|
|
<div style={{ display: 'flex', gap: '0.75rem', flexWrap: 'wrap' }}>
|
|
<span>Ingredienser: {preview.summary.totalIngredients}</span>
|
|
<span>Räcker: {preview.summary.enoughCount}</span>
|
|
<span>Saknas: {preview.summary.missingCount}</span>
|
|
<span>Enhetskonflikter: {preview.summary.unitMismatchCount}</span>
|
|
<strong>
|
|
{preview.summary.canCookExactly
|
|
? 'Kan lagas exakt'
|
|
: 'Kan inte lagas exakt ännu'}
|
|
</strong>
|
|
</div>
|
|
</article>
|
|
|
|
<div style={{ display: 'grid', gap: '0.75rem' }}>
|
|
{preview.ingredients.map((ingredient) => {
|
|
const statusStyle = getStatusStyle(ingredient.status);
|
|
|
|
return (
|
|
<article
|
|
key={ingredient.ingredientId}
|
|
style={{
|
|
border: `1px solid ${statusStyle.border}`,
|
|
borderRadius: '8px',
|
|
padding: '1rem',
|
|
display: 'grid',
|
|
gap: '0.75rem',
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'flex-start',
|
|
gap: '1rem',
|
|
flexWrap: 'wrap',
|
|
}}
|
|
>
|
|
<div>
|
|
<strong>{ingredient.productName}</strong>
|
|
<div>
|
|
Krävs: {ingredient.requiredQuantity} {ingredient.requiredUnit}
|
|
</div>
|
|
{ingredient.note ? <div>Notering: {ingredient.note}</div> : null}
|
|
</div>
|
|
|
|
<div
|
|
style={{
|
|
padding: '0.3rem 0.6rem',
|
|
borderRadius: '999px',
|
|
background: statusStyle.background,
|
|
color: statusStyle.color,
|
|
border: `1px solid ${statusStyle.border}`,
|
|
fontSize: '0.85rem',
|
|
fontWeight: 600,
|
|
whiteSpace: 'nowrap',
|
|
}}
|
|
>
|
|
{statusStyle.label}
|
|
</div>
|
|
</div>
|
|
|
|
<div style={{ display: 'grid', gap: '0.35rem' }}>
|
|
<div>
|
|
Tillgängligt i jämförbar enhet: {ingredient.availableQuantity}{' '}
|
|
{ingredient.availableUnit || ''}
|
|
</div>
|
|
|
|
{ingredient.status === 'missing' ? (
|
|
<div>
|
|
Saknas: {ingredient.missingQuantity} {ingredient.requiredUnit}
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
|
|
{ingredient.matchingInventoryItems.length > 0 ? (
|
|
<div style={{ display: 'grid', gap: '0.35rem' }}>
|
|
<strong>Matchande inventory</strong>
|
|
{ingredient.matchingInventoryItems.map((item) => (
|
|
<div key={item.id}>
|
|
#{item.id}: {item.quantity} {item.unit}
|
|
{item.brand ? `, ${item.brand}` : ''}
|
|
{item.location ? `, ${item.location}` : ''}
|
|
{item.bestBeforeDate
|
|
? `, bäst före ${formatDate(item.bestBeforeDate)}`
|
|
: ''}
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : null}
|
|
|
|
{ingredient.otherInventoryItems.length > 0 ? (
|
|
<div style={{ display: 'grid', gap: '0.35rem' }}>
|
|
<strong>Andra inventory-poster med annan enhet</strong>
|
|
{ingredient.otherInventoryItems.map((item) => (
|
|
<div key={item.id}>
|
|
#{item.id}: {item.quantity} {item.unit}
|
|
{item.brand ? `, ${item.brand}` : ''}
|
|
{item.location ? `, ${item.location}` : ''}
|
|
{item.bestBeforeDate
|
|
? `, bäst före ${formatDate(item.bestBeforeDate)}`
|
|
: ''}
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : null}
|
|
</article>
|
|
);
|
|
})}
|
|
</div>
|
|
</section>
|
|
) : null}
|
|
</section>
|
|
);
|
|
} |