feat(meal-plan): add servings field to MealPlanEntry and update related functionality

feat(products): implement bulk update for product categories

feat(recipes): add servings input to WriteRecipePage and update MealPlanClient for servings management

refactor(types): enhance Product and Category types with additional properties
This commit is contained in:
Nils-Johan Gynther
2026-04-17 22:50:41 +02:00
parent a81bd6b460
commit 21dc06829a
12 changed files with 323 additions and 52 deletions
+38 -1
View File
@@ -9,7 +9,9 @@ const DAYS_SV = ['Måndag', 'Tisdag', 'Onsdag', 'Torsdag', 'Fredag', 'Lördag',
type MealPlanEntry = {
id: number;
date: string;
servings: number | null;
recipe: Pick<Recipe, 'id' | 'name' | 'imageUrl'> & {
servings: number | null;
ingredients: { quantity: string; unit: string; note: string | null; product: { id: number; name: string; canonicalName: string | null } }[];
};
};
@@ -89,10 +91,11 @@ export default function MealPlanClient({ recipes }: { recipes: Recipe[] }) {
if (!recipeId) {
await fetch(`/api/meal-plan-proxy?date=${date}`, { method: 'DELETE' });
} else {
const existing = entryForDate(date);
await fetch('/api/meal-plan-proxy', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ date, recipeId: Number(recipeId) }),
body: JSON.stringify({ date, recipeId: Number(recipeId), servings: existing?.servings ?? null }),
});
}
await load();
@@ -103,6 +106,17 @@ export default function MealPlanClient({ recipes }: { recipes: Recipe[] }) {
const plannedCount = weekDates.filter((d) => entryForDate(d)).length;
const handleServingsChange = async (date: string, servings: number | null) => {
const entry = entryForDate(date);
if (!entry) return;
await fetch('/api/meal-plan-proxy', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ date, recipeId: entry.recipe.id, servings }),
});
await load();
};
return (
<div>
{/* Veckonavigering */}
@@ -173,6 +187,29 @@ export default function MealPlanClient({ recipes }: { recipes: Recipe[] }) {
Visa recept
</Link>
)}
{entry && entry.recipe.servings && (
<div style={{ display: 'flex', alignItems: 'center', gap: '0.35rem', fontSize: '0.82rem', color: '#555' }}>
<span>Port.:</span>
<input
type="number"
min={1}
step={1}
value={entry.servings ?? entry.recipe.servings}
onChange={(e) => handleServingsChange(date, e.target.value ? Number(e.target.value) : null)}
style={{ width: '52px', padding: '0.25rem 0.4rem', border: '1px solid #ced4da', borderRadius: '4px', fontSize: '0.82rem' }}
/>
{entry.servings && entry.servings !== entry.recipe.servings && (
<button
type="button"
onClick={() => handleServingsChange(date, null)}
title={`Återställ till ${entry.recipe.servings} portioner`}
style={{ fontSize: '0.75rem', color: '#888', background: 'none', border: 'none', cursor: 'pointer', padding: '0 0.2rem' }}
>
{entry.recipe.servings}
</button>
)}
</div>
)}
{isSaving && <span style={{ fontSize: '0.8rem', color: '#888' }}>Sparar...</span>}
</div>
</div>