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", "savedRecipePlaceholder": "Rezept auswählen",
"saveAsRecipe": "Als Rezept speichern", "saveAsRecipe": "Als Rezept speichern",
"recipeScaleLabel": "Zutaten skalieren", "recipeScaleLabel": "Zutaten skalieren",
"deletedToast": "Mahlzeit gelöscht" "deletedToast": "Mahlzeit gelöscht",
"cookLabel": "Koch/Köchin",
"cookNone": "Keine Koch-Zuweisung"
}, },
"calendar": { "calendar": {
"title": "Kalender", "title": "Kalender",
+3 -1
View File
@@ -313,7 +313,9 @@
"savedRecipePlaceholder": "Select recipe", "savedRecipePlaceholder": "Select recipe",
"saveAsRecipe": "Save as recipe", "saveAsRecipe": "Save as recipe",
"recipeScaleLabel": "Scale ingredients", "recipeScaleLabel": "Scale ingredients",
"deletedToast": "Meal deleted" "deletedToast": "Meal deleted",
"cookLabel": "Cook",
"cookNone": "No assigned cook"
}, },
"calendar": { "calendar": {
"title": "Calendar", "title": "Calendar",
+32 -5
View File
@@ -38,6 +38,7 @@ let state = {
currentWeek: null, // YYYY-MM-DD (Montag) currentWeek: null, // YYYY-MM-DD (Montag)
meals: [], meals: [],
recipes: [], recipes: [],
familyMembers: [], // Familienmitglieder für Koch-Zuweisung
lists: [], // Einkaufslisten für Transfer-Dropdown lists: [], // Einkaufslisten für Transfer-Dropdown
categories: [], // Einkaufskategorien für Zutaten categories: [], // Einkaufskategorien für Zutaten
modal: null, 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() { async function loadPreferences() {
try { try {
const res = await api.get('/preferences'); 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 today = new Date().toISOString().slice(0, 10);
const monday = getMondayOf(today); const monday = getMondayOf(today);
await Promise.all([loadWeek(monday), loadLists(), loadPreferences(), loadCategories(), loadRecipes()]); await Promise.all([loadWeek(monday), loadLists(), loadPreferences(), loadCategories(), loadRecipes(), loadFamilyMembers()]);
renderWeekGrid(); renderWeekGrid();
wireNav(); 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 ingLabel = ingCount > 0 ? (ingCount !== 1 ? t('meals.ingredientCountPlural', { count: ingCount }) : t('meals.ingredientCount', { count: ingCount })) : '';
const ingDoneLabel = ingCount > 0 && ingDone === ingCount ? ' ✓' : ''; const ingDoneLabel = ingCount > 0 && ingDone === ingCount ? ' ✓' : '';
const canTransfer = ingCount > 0 && ingDone < ingCount; const canTransfer = ingCount > 0 && ingDone < ingCount;
const cookName = meal.cook_assignment?.cook_name;
return ` return `
<div class="meal-slot meal-slot--has-meal" data-meal-id="${meal.id}" data-date="${meal.date}" data-type="${type.key}"> <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}" data-meal-id="${meal.id}"
role="button" tabindex="0"> role="button" tabindex="0">
<div class="meal-card__title">${esc(meal.title)}</div> <div class="meal-card__title">${esc(meal.title)}</div>
${ingLabel ? `<div class="meal-card__meta"> ${(ingLabel || cookName) ? `<div class="meal-card__meta">
<span class="meal-card__ingredients-count">${ingLabel}${esc(ingDoneLabel)}</span> ${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>` : ''}
<div class="meal-card__actions"> <div class="meal-card__actions">
${meal.recipe_url ? `<a class="meal-card__action-btn meal-card__action-btn--recipe" ${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>`), ...state.recipes.map((r) => `<option value="${r.id}" ${isEdit && meal.recipe_id === r.id ? 'selected' : ''}>${esc(r.title)}</option>`),
].join(''); ].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 ` return `
<div class="modal-grid modal-grid--2"> <div class="modal-grid modal-grid--2">
<div class="form-group"> <div class="form-group">
@@ -766,6 +784,11 @@ function buildModalContent({ mode, date, mealType, meal }) {
</div> </div>
</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;"> <div class="form-group" style="position:relative;">
<label class="form-label" for="modal-title">${t('meals.titleLabel')}</label> <label class="form-label" for="modal-title">${t('meals.titleLabel')}</label>
<input type="text" class="form-input" id="modal-title" <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 notes = overlay.querySelector('#modal-notes').value.trim() || null;
const recipe_url = overlay.querySelector('#modal-recipe-url').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 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)) { if (!date || !isDateInputValid(dateRaw)) {
window.oikos?.showToast(t('calendar.invalidDate'), 'error'); window.oikos?.showToast(t('calendar.invalidDate'), 'error');
@@ -884,12 +909,14 @@ async function saveModal(overlay) {
try { try {
const { mode, meal } = state.modal; const { mode, meal } = state.modal;
const mealPayload = { date, meal_type, title, notes, recipe_url, recipe_id, cook_user_id };
if (mode === 'create') { 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); state.meals.push(res.data);
} else { } else {
// Update meal meta // 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 // Sync ingredients
const existingIds = new Set((meal.ingredients ?? []).map((i) => i.id)); const existingIds = new Set((meal.ingredients ?? []).map((i) => i.id));
+8
View File
@@ -461,3 +461,11 @@
transform: none; 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); `).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!) // Routen - Mahlzeiten-Vorschläge (vor dynamischen Routen!)
// -------------------------------------------------------- // --------------------------------------------------------
@@ -321,6 +329,8 @@ router.put('/:id', (req, res) => {
if (vCookUserId.present) { if (vCookUserId.present) {
saveCookAssignment(updated, vCookUserId.value, vSourcePlanId.value, req.session.userId); saveCookAssignment(updated, vCookUserId.value, vSourcePlanId.value, req.session.userId);
} else {
syncCookAssignmentSlot(updated);
} }
const ings = db.get().prepare( const ings = db.get().prepare(