feat: add native meal cook selector
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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
@@ -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));
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user