270 lines
9.1 KiB
TypeScript
270 lines
9.1 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');
|
|
}
|
|
|
|
function isWeightUnit(unit: string): boolean {
|
|
return ['kg', 'g', 'mg', 'ml', 'l'].includes(unit.trim().toLowerCase());
|
|
}
|
|
|
|
function isPieceUnit(unit: string): boolean {
|
|
return ['st', 'stycke'].includes(unit.trim().toLowerCase());
|
|
}
|
|
|
|
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 && ingredient.otherInventoryItems.length > 0 ? (
|
|
<div style={{ display: 'grid', gap: '0.35rem' }}>
|
|
<strong>Andra enheter:</strong>
|
|
{ingredient.otherInventoryItems.map((item) => {
|
|
const weight = isWeightUnit(item.unit);
|
|
const pieces = isPieceUnit(ingredient.requiredUnit);
|
|
const pieces2 = isPieceUnit(item.unit);
|
|
const weight2 = isWeightUnit(ingredient.requiredUnit);
|
|
|
|
return (
|
|
<div key={item.id}>
|
|
#{item.id}: {item.quantity} {item.unit}
|
|
{item.canConvert ? (
|
|
<span> ≈ {(item.convertedQuantity || 0).toFixed(2)} {ingredient.requiredUnit}</span>
|
|
) : (
|
|
<span>
|
|
{weight && pieces
|
|
? ' (kan inte konvertera vikt till stycken)'
|
|
: pieces2 && weight2
|
|
? ' (kan inte konvertera stycken till vikt)'
|
|
: ' (kan inte konvertera)'}
|
|
</span>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
) : null}
|
|
</article>
|
|
);
|
|
})}
|
|
</div>
|
|
</section>
|
|
) : null}
|
|
</section>
|
|
);
|
|
} |