feat: add native meal cook selector

This commit is contained in:
OpenClaw Bot
2026-05-11 23:14:15 +02:00
parent 5099155c61
commit cf099bb353
5 changed files with 56 additions and 7 deletions
+3 -1
View File
@@ -319,7 +319,9 @@
"savedRecipePlaceholder": "Rezept auswählen",
"saveAsRecipe": "Als Rezept speichern",
"recipeScaleLabel": "Zutaten skalieren",
"deletedToast": "Mahlzeit gelöscht"
"deletedToast": "Mahlzeit gelöscht",
"cookLabel": "Koch/Köchin",
"cookNone": "Keine Koch-Zuweisung"
},
"calendar": {
"title": "Kalender",
+3 -1
View File
@@ -313,7 +313,9 @@
"savedRecipePlaceholder": "Select recipe",
"saveAsRecipe": "Save as recipe",
"recipeScaleLabel": "Scale ingredients",
"deletedToast": "Meal deleted"
"deletedToast": "Meal deleted",
"cookLabel": "Cook",
"cookNone": "No assigned cook"
},
"calendar": {
"title": "Calendar",
+32 -5
View File
@@ -38,6 +38,7 @@ let state = {
currentWeek: null, // YYYY-MM-DD (Montag)
meals: [],
recipes: [],
familyMembers: [], // Familienmitglieder für Koch-Zuweisung
lists: [], // Einkaufslisten für Transfer-Dropdown
categories: [], // Einkaufskategorien für Zutaten
modal: null,
@@ -126,6 +127,15 @@ async function loadRecipes() {
}
}
async function loadFamilyMembers() {
try {
const res = await api.get('/family/members');
state.familyMembers = res.data;
} catch {
state.familyMembers = [];
}
}
async function loadPreferences() {
try {
const res = await api.get('/preferences');
@@ -169,7 +179,7 @@ export async function render(container, { user }) {
const today = new Date().toISOString().slice(0, 10);
const monday = getMondayOf(today);
await Promise.all([loadWeek(monday), loadLists(), loadPreferences(), loadCategories(), loadRecipes()]);
await Promise.all([loadWeek(monday), loadLists(), loadPreferences(), loadCategories(), loadRecipes(), loadFamilyMembers()]);
renderWeekGrid();
wireNav();
@@ -252,6 +262,7 @@ function renderSlot(date, type, mealsForDay) {
const ingLabel = ingCount > 0 ? (ingCount !== 1 ? t('meals.ingredientCountPlural', { count: ingCount }) : t('meals.ingredientCount', { count: ingCount })) : '';
const ingDoneLabel = ingCount > 0 && ingDone === ingCount ? ' ✓' : '';
const canTransfer = ingCount > 0 && ingDone < ingCount;
const cookName = meal.cook_assignment?.cook_name;
return `
<div class="meal-slot meal-slot--has-meal" data-meal-id="${meal.id}" data-date="${meal.date}" data-type="${type.key}">
@@ -261,8 +272,9 @@ function renderSlot(date, type, mealsForDay) {
data-meal-id="${meal.id}"
role="button" tabindex="0">
<div class="meal-card__title">${esc(meal.title)}</div>
${ingLabel ? `<div class="meal-card__meta">
<span class="meal-card__ingredients-count">${ingLabel}${esc(ingDoneLabel)}</span>
${(ingLabel || cookName) ? `<div class="meal-card__meta">
${ingLabel ? `<span class="meal-card__ingredients-count">${ingLabel}${esc(ingDoneLabel)}</span>` : ''}
${cookName ? `<span class="meal-card__cook"><i data-lucide="chef-hat" style="width:13px;height:13px;" aria-hidden="true"></i>${esc(cookName)}</span>` : ''}
</div>` : ''}
<div class="meal-card__actions">
${meal.recipe_url ? `<a class="meal-card__action-btn meal-card__action-btn--recipe"
@@ -754,6 +766,12 @@ function buildModalContent({ mode, date, mealType, meal }) {
...state.recipes.map((r) => `<option value="${r.id}" ${isEdit && meal.recipe_id === r.id ? 'selected' : ''}>${esc(r.title)}</option>`),
].join('');
const selectedCookId = isEdit && meal.cook_assignment?.user_id ? String(meal.cook_assignment.user_id) : '';
const cookOptions = [
`<option value="">${t('meals.cookNone')}</option>`,
...state.familyMembers.map((member) => `<option value="${member.id}" ${selectedCookId === String(member.id) ? 'selected' : ''}>${esc(member.display_name)}</option>`),
].join('');
return `
<div class="modal-grid modal-grid--2">
<div class="form-group">
@@ -766,6 +784,11 @@ function buildModalContent({ mode, date, mealType, meal }) {
</div>
</div>
<div class="form-group">
<label class="form-label" for="modal-cook-user-id">${t('meals.cookLabel')}</label>
<select class="form-input" id="modal-cook-user-id">${cookOptions}</select>
</div>
<div class="form-group" style="position:relative;">
<label class="form-label" for="modal-title">${t('meals.titleLabel')}</label>
<input type="text" class="form-input" id="modal-title"
@@ -865,6 +888,8 @@ async function saveModal(overlay) {
const notes = overlay.querySelector('#modal-notes').value.trim() || null;
const recipe_url = overlay.querySelector('#modal-recipe-url').value.trim() || null;
const recipe_id = overlay.querySelector('#modal-recipe-id')?.value || null;
const cookSelect = overlay.querySelector('#modal-cook-user-id');
const cook_user_id = cookSelect?.value ? Number(cookSelect.value) : null;
if (!date || !isDateInputValid(dateRaw)) {
window.oikos?.showToast(t('calendar.invalidDate'), 'error');
@@ -884,12 +909,14 @@ async function saveModal(overlay) {
try {
const { mode, meal } = state.modal;
const mealPayload = { date, meal_type, title, notes, recipe_url, recipe_id, cook_user_id };
if (mode === 'create') {
const res = await api.post('/meals', { date, meal_type, title, notes, recipe_url, recipe_id, ingredients });
const res = await api.post('/meals', { ...mealPayload, ingredients });
state.meals.push(res.data);
} else {
// Update meal meta
await api.put(`/meals/${meal.id}`, { date, meal_type, title, notes, recipe_url, recipe_id });
await api.put(`/meals/${meal.id}`, mealPayload);
// Sync ingredients
const existingIds = new Set((meal.ingredients ?? []).map((i) => i.id));
+8
View File
@@ -461,3 +461,11 @@
transform: none;
}
}
.meal-card__cook {
display: inline-flex;
align-items: center;
gap: 0.25rem;
color: var(--color-text-secondary);
font-weight: 600;
}
+10
View File
@@ -87,6 +87,14 @@ function saveCookAssignment(meal, cookUserId, sourcePlanId, createdBy) {
`).run(meal.id, cookUserId, meal.date, meal.meal_type, sourcePlanId || null, createdBy);
}
function syncCookAssignmentSlot(meal) {
db.get().prepare(`
UPDATE planned_meal_cooks
SET planned_for_date = ?, meal_type = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
WHERE meal_id = ?
`).run(meal.date, meal.meal_type, meal.id);
}
// --------------------------------------------------------
// Routen - Mahlzeiten-Vorschläge (vor dynamischen Routen!)
// --------------------------------------------------------
@@ -321,6 +329,8 @@ router.put('/:id', (req, res) => {
if (vCookUserId.present) {
saveCookAssignment(updated, vCookUserId.value, vSourcePlanId.value, req.session.userId);
} else {
syncCookAssignmentSlot(updated);
}
const ings = db.get().prepare(