feat: structure meal planning taxonomy and favorites
This commit is contained in:
+137
-6
@@ -29,6 +29,12 @@ const DAY_NAMES = () => [
|
||||
];
|
||||
|
||||
const EXCLUDED_MEAL_CATEGORY_NAMES = new Set(['Haushalt', 'Drogerie']);
|
||||
const MEAL_CATEGORY_OPTIONS = [
|
||||
['meat', 'Kød'], ['fish', 'Fisk'], ['pasta', 'Pasta'], ['rice', 'Ris'],
|
||||
['vegetarian', 'Grønt'], ['soup', 'Suppe'], ['leftovers', 'Rester'], ['cozy', 'Hygge'], ['other', 'Andet'],
|
||||
];
|
||||
const PROTEIN_OPTIONS = [['mixed', 'Blandet'], ['chicken', 'Kylling'], ['beef', 'Okse'], ['pork', 'Svin'], ['fish', 'Fisk'], ['vegetarian', 'Vegetar'], ['none', 'Ingen'], ['other', 'Andet']];
|
||||
const STYLE_OPTIONS = [['family', 'Familie'], ['quick', 'Hurtig'], ['cozy', 'Hygge'], ['grill', 'Grill'], ['vegetarian', 'Vegetar'], ['kids', 'Børnevenlig'], ['leftovers', 'Rester'], ['other', 'Andet']];
|
||||
|
||||
// --------------------------------------------------------
|
||||
// State
|
||||
@@ -38,6 +44,7 @@ let state = {
|
||||
currentWeek: null, // YYYY-MM-DD (Montag)
|
||||
meals: [],
|
||||
recipes: [],
|
||||
recipeSignals: [],
|
||||
familyMembers: [], // Familienmitglieder für Koch-Zuweisung
|
||||
lists: [], // Einkaufslisten für Transfer-Dropdown
|
||||
categories: [], // Einkaufskategorien für Zutaten
|
||||
@@ -83,6 +90,10 @@ function mealCategories() {
|
||||
return state.categories.filter((c) => !EXCLUDED_MEAL_CATEGORY_NAMES.has(c.name));
|
||||
}
|
||||
|
||||
function optionHtml(options, selected) {
|
||||
return options.map(([value, label]) => `<option value="${esc(value)}" ${value === selected ? 'selected' : ''}>${esc(label)}</option>`).join('');
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// API-Wrapper
|
||||
// --------------------------------------------------------
|
||||
@@ -136,6 +147,35 @@ async function loadFamilyMembers() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRecipeSignals() {
|
||||
try {
|
||||
const res = await api.get('/meal-planning/recipe-signals');
|
||||
state.recipeSignals = res.data;
|
||||
} catch {
|
||||
state.recipeSignals = [];
|
||||
}
|
||||
}
|
||||
|
||||
function signalFor(recipeId, userId) {
|
||||
return state.recipeSignals.find((signal) => Number(signal.recipe_id) === Number(recipeId) && Number(signal.user_id) === Number(userId));
|
||||
}
|
||||
|
||||
async function saveRecipeSignal(recipeId, userId, patch) {
|
||||
const current = signalFor(recipeId, userId) || {};
|
||||
const res = await api.put(`/meal-planning/recipe-signals/${recipeId}`, {
|
||||
user_id: userId,
|
||||
preference: current.preference || 'neutral',
|
||||
can_cook: !!current.can_cook,
|
||||
can_help_cook: !!current.can_help_cook,
|
||||
will_eat_modified: !!current.will_eat_modified,
|
||||
adult_only: !!current.adult_only,
|
||||
...patch,
|
||||
});
|
||||
state.recipeSignals = state.recipeSignals.filter((signal) => !(Number(signal.recipe_id) === Number(recipeId) && Number(signal.user_id) === Number(userId)));
|
||||
state.recipeSignals.push(res.data);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
async function loadPreferences() {
|
||||
try {
|
||||
const res = await api.get('/preferences');
|
||||
@@ -177,13 +217,15 @@ export async function render(container, { user }) {
|
||||
renderKitchenTabsBar(container, '/meals');
|
||||
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const monday = getMondayOf(today);
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const requestedWeek = params.get('week') || params.get('date') || today;
|
||||
const monday = getMondayOf(requestedWeek);
|
||||
|
||||
await Promise.all([loadWeek(monday), loadLists(), loadPreferences(), loadCategories(), loadRecipes(), loadFamilyMembers()]);
|
||||
await Promise.all([loadWeek(monday), loadLists(), loadPreferences(), loadCategories(), loadRecipes(), loadFamilyMembers(), loadRecipeSignals()]);
|
||||
renderWeekGrid();
|
||||
wireNav();
|
||||
|
||||
const selectedRecipeId = Number(new URLSearchParams(window.location.search).get('recipe'));
|
||||
const selectedRecipeId = Number(params.get('recipe'));
|
||||
if (selectedRecipeId) {
|
||||
const selectedRecipe = state.recipes.find((r) => r.id === selectedRecipeId);
|
||||
if (selectedRecipe) {
|
||||
@@ -263,6 +305,8 @@ function renderSlot(date, type, mealsForDay) {
|
||||
const ingDoneLabel = ingCount > 0 && ingDone === ingCount ? ' ✓' : '';
|
||||
const canTransfer = ingCount > 0 && ingDone < ingCount;
|
||||
const cookName = meal.cook_assignment?.cook_name;
|
||||
const mealCategoryLabel = meal.meal_category ? MEAL_CATEGORY_OPTIONS.find(([value]) => value === meal.meal_category)?.[1] : '';
|
||||
const leftoverLabel = meal.leftover_from_meal_id ? 'Rester' : '';
|
||||
|
||||
return `
|
||||
<div class="meal-slot meal-slot--has-meal" data-meal-id="${meal.id}" data-date="${meal.date}" data-type="${type.key}">
|
||||
@@ -272,7 +316,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 || cookName) ? `<div class="meal-card__meta">
|
||||
${(ingLabel || cookName || mealCategoryLabel || leftoverLabel) ? `<div class="meal-card__meta">
|
||||
${mealCategoryLabel ? `<span class="meal-card__ingredients-count">${esc(mealCategoryLabel)}</span>` : ''}
|
||||
${leftoverLabel ? `<span class="meal-card__ingredients-count">↻ ${esc(leftoverLabel)}</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>` : ''}
|
||||
@@ -619,6 +665,9 @@ function openMealModal(opts) {
|
||||
panel.querySelector('#modal-title').value = recipe.title || '';
|
||||
panel.querySelector('#modal-notes').value = recipe.notes || '';
|
||||
panel.querySelector('#modal-recipe-url').value = recipe.recipe_url || '';
|
||||
panel.querySelector('#modal-meal-category').value = recipe.meal_category || 'other';
|
||||
panel.querySelector('#modal-protein').value = recipe.protein || 'mixed';
|
||||
panel.querySelector('#modal-style').value = recipe.style || 'family';
|
||||
|
||||
ingList.innerHTML = (recipe.ingredients || [])
|
||||
.map((ing) => {
|
||||
@@ -654,6 +703,39 @@ function openMealModal(opts) {
|
||||
if (window.lucide) lucide.createIcons();
|
||||
});
|
||||
|
||||
panel.querySelector('#modal-leftover-from-meal-id')?.addEventListener('change', (event) => {
|
||||
if (!event.target.value) return;
|
||||
panel.querySelector('#modal-meal-category').value = 'leftovers';
|
||||
panel.querySelector('#modal-protein').value = 'none';
|
||||
panel.querySelector('#modal-style').value = 'leftovers';
|
||||
});
|
||||
|
||||
panel.querySelector('[data-meal-recipe-pref-actions]')?.addEventListener('click', async (e) => {
|
||||
const btn = e.target.closest('[data-meal-recipe-pref]');
|
||||
if (!btn) return;
|
||||
const recipeId = Number(panel.querySelector('#modal-recipe-id')?.value || 0);
|
||||
const userId = Number(panel.querySelector('#modal-recipe-pref-member')?.value || 0);
|
||||
if (!recipeId || !userId) {
|
||||
window.oikos?.showToast('Choose a saved recipe and family member first.', 'error');
|
||||
return;
|
||||
}
|
||||
const kind = btn.dataset.mealRecipePref;
|
||||
const patch = kind === 'favorite' ? { preference: 'favorite' }
|
||||
: kind === 'like' ? { preference: 'like' }
|
||||
: kind === 'dislike' ? { preference: 'dislike' }
|
||||
: kind === 'canCook' ? { can_cook: true }
|
||||
: {};
|
||||
btn.disabled = true;
|
||||
try {
|
||||
await saveRecipeSignal(recipeId, userId, patch);
|
||||
window.oikos?.showToast('Meal signal saved to profile', 'success');
|
||||
} catch (err) {
|
||||
window.oikos?.showToast(err.data?.error ?? t('common.errorGeneric'), 'error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
saveAsRecipeBtn?.addEventListener('click', async () => {
|
||||
const title = panel.querySelector('#modal-title').value.trim();
|
||||
if (!title) {
|
||||
@@ -668,10 +750,13 @@ function openMealModal(opts) {
|
||||
quantity: ing.quantity,
|
||||
category: ing.category,
|
||||
}));
|
||||
const meal_category = panel.querySelector('#modal-meal-category')?.value || 'other';
|
||||
const protein = panel.querySelector('#modal-protein')?.value || 'mixed';
|
||||
const style = panel.querySelector('#modal-style')?.value || 'family';
|
||||
|
||||
saveAsRecipeBtn.disabled = true;
|
||||
try {
|
||||
const created = await api.post('/recipes', { title, notes, recipe_url, ingredients });
|
||||
const created = await api.post('/recipes', { title, notes, recipe_url, meal_category, protein, style, ingredients });
|
||||
state.recipes.push(created.data);
|
||||
|
||||
if (recipeSelect) {
|
||||
@@ -772,6 +857,14 @@ function buildModalContent({ mode, date, mealType, meal }) {
|
||||
...state.familyMembers.map((member) => `<option value="${member.id}" ${selectedCookId === String(member.id) ? 'selected' : ''}>${esc(member.display_name)}</option>`),
|
||||
].join('');
|
||||
|
||||
const leftoverOptions = [
|
||||
`<option value="">Ingen rester</option>`,
|
||||
...state.meals
|
||||
.filter((candidate) => !isEdit || candidate.id !== meal.id)
|
||||
.slice(-20)
|
||||
.map((candidate) => `<option value="${candidate.id}" ${isEdit && meal.leftover_from_meal_id === candidate.id ? 'selected' : ''}>${esc(candidate.date)} · ${esc(candidate.title)}</option>`),
|
||||
].join('');
|
||||
|
||||
return `
|
||||
<div class="modal-grid modal-grid--2">
|
||||
<div class="form-group">
|
||||
@@ -803,6 +896,40 @@ function buildModalContent({ mode, date, mealType, meal }) {
|
||||
<select class="form-input" id="modal-recipe-id">${recipeOptions}</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" data-meal-recipe-pref-actions>
|
||||
<label class="form-label" for="modal-recipe-pref-member">Save this meal as a person-specific signal</label>
|
||||
<select class="form-input" id="modal-recipe-pref-member">
|
||||
<option value="">Choose family member</option>
|
||||
${state.familyMembers.map((member) => `<option value="${member.id}">${esc(member.display_name)}</option>`).join('')}
|
||||
</select>
|
||||
<div class="recipe-card__actions" style="margin-top:var(--space-2)">
|
||||
<button type="button" class="btn btn--ghost" data-meal-recipe-pref="favorite">⭐ Favorite</button>
|
||||
<button type="button" class="btn btn--ghost" data-meal-recipe-pref="like">👍 Likes</button>
|
||||
<button type="button" class="btn btn--ghost" data-meal-recipe-pref="dislike">👎 Dislikes</button>
|
||||
<button type="button" class="btn btn--ghost" data-meal-recipe-pref="canCook">👩🍳 Can cook</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-grid modal-grid--3">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="modal-meal-category">Meal category</label>
|
||||
<select class="form-input" id="modal-meal-category">${optionHtml(MEAL_CATEGORY_OPTIONS, isEdit ? (meal.meal_category || 'other') : 'other')}</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="modal-protein">Protein</label>
|
||||
<select class="form-input" id="modal-protein">${optionHtml(PROTEIN_OPTIONS, isEdit ? (meal.protein || 'mixed') : 'mixed')}</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="modal-style">Style</label>
|
||||
<select class="form-input" id="modal-style">${optionHtml(STYLE_OPTIONS, isEdit ? (meal.style || 'family') : 'family')}</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="modal-leftover-from-meal-id">Use leftovers from a specific dish</label>
|
||||
<select class="form-input" id="modal-leftover-from-meal-id">${leftoverOptions}</select>
|
||||
</div>
|
||||
|
||||
<div class="modal-grid modal-grid--2">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="modal-recipe-scale">${t('meals.recipeScaleLabel')}</label>
|
||||
@@ -888,6 +1015,10 @@ 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 meal_category = overlay.querySelector('#modal-meal-category')?.value || 'other';
|
||||
const protein = overlay.querySelector('#modal-protein')?.value || 'mixed';
|
||||
const style = overlay.querySelector('#modal-style')?.value || 'family';
|
||||
const leftover_from_meal_id = overlay.querySelector('#modal-leftover-from-meal-id')?.value || null;
|
||||
const cookSelect = overlay.querySelector('#modal-cook-user-id');
|
||||
const cook_user_id = cookSelect?.value ? Number(cookSelect.value) : null;
|
||||
|
||||
@@ -909,7 +1040,7 @@ async function saveModal(overlay) {
|
||||
try {
|
||||
const { mode, meal } = state.modal;
|
||||
|
||||
const mealPayload = { date, meal_type, title, notes, recipe_url, recipe_id, cook_user_id };
|
||||
const mealPayload = { date, meal_type, title, notes, recipe_url, recipe_id, meal_category, protein, style, leftover_from_meal_id, cook_user_id };
|
||||
|
||||
if (mode === 'create') {
|
||||
const res = await api.post('/meals', { ...mealPayload, ingredients });
|
||||
|
||||
+126
-3
@@ -8,14 +8,28 @@ import { t } from '/i18n.js';
|
||||
import { openModal as openSharedModal, closeModal as closeSharedModal } from '/components/modal.js';
|
||||
import { DEFAULT_CATEGORY_NAME, categoryLabel } from '/utils/shopping-categories.js';
|
||||
import { renderKitchenTabsBar } from '/utils/kitchen-tabs.js';
|
||||
import { esc } from '/utils/html.js';
|
||||
|
||||
let _container = null;
|
||||
|
||||
const state = {
|
||||
recipes: [],
|
||||
categories: [],
|
||||
familyMembers: [],
|
||||
recipeSignals: [],
|
||||
};
|
||||
|
||||
const MEAL_CATEGORY_OPTIONS = [
|
||||
['meat', 'Kød'], ['fish', 'Fisk'], ['pasta', 'Pasta'], ['rice', 'Ris'],
|
||||
['vegetarian', 'Grønt'], ['soup', 'Suppe'], ['leftovers', 'Rester'], ['cozy', 'Hygge'], ['other', 'Andet'],
|
||||
];
|
||||
const PROTEIN_OPTIONS = [['mixed', 'Blandet'], ['chicken', 'Kylling'], ['beef', 'Okse'], ['pork', 'Svin'], ['fish', 'Fisk'], ['vegetarian', 'Vegetar'], ['none', 'Ingen'], ['other', 'Andet']];
|
||||
const STYLE_OPTIONS = [['family', 'Familie'], ['quick', 'Hurtig'], ['cozy', 'Hygge'], ['grill', 'Grill'], ['vegetarian', 'Vegetar'], ['kids', 'Børnevenlig'], ['leftovers', 'Rester'], ['other', 'Andet']];
|
||||
|
||||
function optionHtml(options, selected) {
|
||||
return options.map(([value, label]) => `<option value="${value}" ${value === selected ? 'selected' : ''}>${label}</option>`).join('');
|
||||
}
|
||||
|
||||
function mealCategories() {
|
||||
return state.categories.filter((c) => c.name !== 'Haushalt' && c.name !== 'Drogerie');
|
||||
}
|
||||
@@ -34,6 +48,45 @@ async function loadCategories() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadFamilyMembers() {
|
||||
try {
|
||||
const res = await api.get('/family/members');
|
||||
state.familyMembers = res.data;
|
||||
} catch { state.familyMembers = []; }
|
||||
}
|
||||
|
||||
async function loadRecipeSignals() {
|
||||
try {
|
||||
const res = await api.get('/meal-planning/recipe-signals');
|
||||
state.recipeSignals = res.data;
|
||||
} catch { state.recipeSignals = []; }
|
||||
}
|
||||
|
||||
function signalsForRecipe(recipeId) {
|
||||
return state.recipeSignals.filter((signal) => Number(signal.recipe_id) === Number(recipeId));
|
||||
}
|
||||
|
||||
function signalFor(recipeId, userId) {
|
||||
return state.recipeSignals.find((signal) => Number(signal.recipe_id) === Number(recipeId) && Number(signal.user_id) === Number(userId));
|
||||
}
|
||||
|
||||
async function saveRecipeSignal(recipeId, userId, patch) {
|
||||
const current = signalFor(recipeId, userId) || {};
|
||||
const payload = {
|
||||
user_id: userId,
|
||||
preference: current.preference || 'neutral',
|
||||
can_cook: !!current.can_cook,
|
||||
can_help_cook: !!current.can_help_cook,
|
||||
will_eat_modified: !!current.will_eat_modified,
|
||||
adult_only: !!current.adult_only,
|
||||
...patch,
|
||||
};
|
||||
const res = await api.put(`/meal-planning/recipe-signals/${recipeId}`, payload);
|
||||
state.recipeSignals = state.recipeSignals.filter((signal) => !(Number(signal.recipe_id) === Number(recipeId) && Number(signal.user_id) === Number(userId)));
|
||||
state.recipeSignals.push(res.data);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function render(container) {
|
||||
_container = container;
|
||||
|
||||
@@ -75,7 +128,7 @@ export async function render(container) {
|
||||
|
||||
if (window.lucide) window.lucide.createIcons();
|
||||
|
||||
await Promise.all([loadRecipes(), loadCategories()]);
|
||||
await Promise.all([loadRecipes(), loadCategories(), loadFamilyMembers(), loadRecipeSignals()]);
|
||||
renderRecipeList();
|
||||
|
||||
addBtn.addEventListener('click', () => openRecipeModal('create'));
|
||||
@@ -107,6 +160,13 @@ export async function render(container) {
|
||||
if (actionBtn.dataset.action === 'add-to-meals') {
|
||||
window.oikos?.navigate(`/meals?recipe=${recipe.id}`);
|
||||
}
|
||||
|
||||
if (actionBtn.dataset.action === 'quick-favorite') {
|
||||
const memberId = Number(actionBtn.dataset.memberId);
|
||||
await saveRecipeSignal(recipe.id, memberId, { preference: 'favorite' });
|
||||
renderRecipeList();
|
||||
window.oikos?.showToast('Favorit gemt på profilen', 'success');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -155,6 +215,27 @@ function renderRecipeList() {
|
||||
|
||||
card.appendChild(h);
|
||||
|
||||
const taxonomy = [
|
||||
recipe.meal_category ? MEAL_CATEGORY_OPTIONS.find(([v]) => v === recipe.meal_category)?.[1] || recipe.meal_category : '',
|
||||
recipe.protein ? PROTEIN_OPTIONS.find(([v]) => v === recipe.protein)?.[1] || recipe.protein : '',
|
||||
recipe.style ? STYLE_OPTIONS.find(([v]) => v === recipe.style)?.[1] || recipe.style : '',
|
||||
].filter(Boolean);
|
||||
if (taxonomy.length) {
|
||||
const meta = document.createElement('p');
|
||||
meta.className = 'recipe-card__notes';
|
||||
meta.textContent = `Kategori: ${taxonomy.join(' · ')}`;
|
||||
card.appendChild(meta);
|
||||
}
|
||||
|
||||
const recipeSignals = signalsForRecipe(recipe.id);
|
||||
const favoriteSignals = recipeSignals.filter((signal) => signal.preference === 'favorite');
|
||||
if (favoriteSignals.length) {
|
||||
const fav = document.createElement('p');
|
||||
fav.className = 'recipe-card__notes';
|
||||
fav.textContent = `⭐ Favorit hos ${favoriteSignals.map((signal) => signal.user_name).filter(Boolean).join(', ')}`;
|
||||
card.appendChild(fav);
|
||||
}
|
||||
|
||||
if (recipe.notes) {
|
||||
const notes = document.createElement('p');
|
||||
notes.className = 'recipe-card__notes';
|
||||
@@ -220,6 +301,26 @@ function renderRecipeList() {
|
||||
actions.append(addToMeals, edit, duplicate, del);
|
||||
card.appendChild(actions);
|
||||
|
||||
if (state.familyMembers.length) {
|
||||
const pref = document.createElement('div');
|
||||
pref.className = 'recipe-card__actions';
|
||||
const label = document.createElement('span');
|
||||
label.className = 'recipe-card__notes';
|
||||
label.textContent = 'Gem som favorit for:';
|
||||
pref.appendChild(label);
|
||||
for (const member of state.familyMembers.slice(0, 6)) {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'btn btn--ghost';
|
||||
btn.type = 'button';
|
||||
btn.dataset.action = 'quick-favorite';
|
||||
btn.dataset.id = String(recipe.id);
|
||||
btn.dataset.memberId = String(member.id);
|
||||
btn.textContent = `⭐ ${member.display_name}`;
|
||||
pref.appendChild(btn);
|
||||
}
|
||||
card.appendChild(pref);
|
||||
}
|
||||
|
||||
list.appendChild(card);
|
||||
}
|
||||
}
|
||||
@@ -298,6 +399,24 @@ function openRecipeModal(mode, recipe = null) {
|
||||
<label class="form-label" for="recipe-url">${t('recipes.urlLabel')}</label>
|
||||
<input id="recipe-url" class="form-input" type="url" placeholder="${t('recipes.urlPlaceholder')}">
|
||||
</div>
|
||||
<div class="modal-grid modal-grid--3">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="recipe-meal-category">Meal category</label>
|
||||
<select id="recipe-meal-category" class="form-input">${optionHtml(MEAL_CATEGORY_OPTIONS, recipe?.meal_category || 'other')}</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="recipe-protein">Protein</label>
|
||||
<select id="recipe-protein" class="form-input">${optionHtml(PROTEIN_OPTIONS, recipe?.protein || 'mixed')}</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="recipe-style">Style</label>
|
||||
<select id="recipe-style" class="form-input">${optionHtml(STYLE_OPTIONS, recipe?.style || 'family')}</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="recipe-tags">Tags</label>
|
||||
<input id="recipe-tags" class="form-input" type="text" placeholder="hurtig, børnevenlig, fredag" value="${esc(Array.isArray(recipe?.tags) ? recipe.tags.join(', ') : '')}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">${t('recipes.ingredientsLabel')}</label>
|
||||
<div class="recipe-ingredient-list" id="recipe-ingredient-list"></div>
|
||||
@@ -348,6 +467,10 @@ async function saveRecipe(panel, mode, recipe) {
|
||||
const title = panel.querySelector('#recipe-title')?.value.trim() || '';
|
||||
const notes = panel.querySelector('#recipe-notes')?.value.trim() || null;
|
||||
const recipe_url = panel.querySelector('#recipe-url')?.value.trim() || null;
|
||||
const meal_category = panel.querySelector('#recipe-meal-category')?.value || 'other';
|
||||
const protein = panel.querySelector('#recipe-protein')?.value || 'mixed';
|
||||
const style = panel.querySelector('#recipe-style')?.value || 'family';
|
||||
const tags = (panel.querySelector('#recipe-tags')?.value || '').split(',').map((tag) => tag.trim()).filter(Boolean);
|
||||
|
||||
if (!title) {
|
||||
window.oikos?.showToast(t('recipes.titleRequired'), 'error');
|
||||
@@ -366,10 +489,10 @@ async function saveRecipe(panel, mode, recipe) {
|
||||
|
||||
try {
|
||||
if (mode === 'create') {
|
||||
const res = await api.post('/recipes', { title, notes, recipe_url, ingredients });
|
||||
const res = await api.post('/recipes', { title, notes, recipe_url, meal_category, protein, style, tags, ingredients });
|
||||
state.recipes.push(res.data);
|
||||
} else {
|
||||
const res = await api.put(`/recipes/${recipe.id}`, { title, notes, recipe_url, ingredients });
|
||||
const res = await api.put(`/recipes/${recipe.id}`, { title, notes, recipe_url, meal_category, protein, style, tags, ingredients });
|
||||
const idx = state.recipes.findIndex((r) => r.id === recipe.id);
|
||||
if (idx >= 0) state.recipes[idx] = res.data;
|
||||
}
|
||||
|
||||
@@ -2334,6 +2334,8 @@ function memberHtml(u) {
|
||||
u.phone ? t('settings.memberPhoneMeta', { value: u.phone }) : '',
|
||||
u.email || '',
|
||||
u.birth_date ? t('settings.memberBirthdayMeta', { date: formatDate(u.birth_date) }) : '',
|
||||
Number(u.favorite_meal_count || 0) ? `⭐ ${Number(u.favorite_meal_count)} favorite meals` : '',
|
||||
Number(u.can_cook_meal_count || 0) ? `👩🍳 ${Number(u.can_cook_meal_count)} can cook` : '',
|
||||
].filter(Boolean).map(esc).join(' · ');
|
||||
return `
|
||||
<li class="settings-member" data-id="${u.id}">
|
||||
|
||||
Reference in New Issue
Block a user