/* global React, window */ // Ruticomidas — modal system. // Components listen for window 'rc:modal' CustomEvent with detail = { name, props } // Dispatching with name: null closes the modal. const { useState, useEffect, useMemo, useRef } = React; const D = window.RC_DATA; // ========================================================= // Shell // ========================================================= function ModalShell({ kicker, title, size, children, foot, onClose }) { useEffect(() => { const onKey = (e) => { if (e.key === 'Escape') onClose(); }; document.addEventListener('keydown', onKey); document.body.style.overflow = 'hidden'; return () => { document.removeEventListener('keydown', onKey); document.body.style.overflow = ''; }; }, [onClose]); const onBackdrop = (e) => { if (e.target === e.currentTarget) onClose(); }; return (
{kicker &&
{kicker}
}

{title}

{children}
{foot &&
{foot}
}
); } const Field = ({ label, full, children }) => (
{children}
); const Toggle = ({ checked, onChange, label }) => ( ); // ========================================================= // MEAL — assign recipe + loose ingredients to a planning slot // ========================================================= function MealModal({ day, slot, recipes, onSave, onClose }) { const recipeList = recipes || []; const [selectedId, setSelectedId] = useState(''); const [busy, setBusy] = useState(false); const planDate = day?.date; const mealType = slot?.id; async function doSave() { if (!selectedId) { window.showToast('Elige una receta', 'error'); return; } if (!planDate || !mealType) { window.showToast('Datos de planificación inválidos', 'error'); return; } setBusy(true); try { await window.api(`/api/planning/${planDate}/${mealType}/recipes`, 'POST', { recipe_id: parseInt(selectedId, 10), raciones: 1, }); if (onSave) await onSave(); onClose(); } catch (e) { window.showToast(e.message, 'error'); } finally { setBusy(false); } } return ( } >
● EL STOCK SE DESCUENTA AUTOMÁTICAMENTE AL FINALIZAR EL DÍA
); } // ========================================================= // LOOSE — quick add of a single loose ingredient // ========================================================= function LooseModal({ day, slot, onSave, onClose }) { const { ingredients } = window.RC_STORE.useStore(); const sortedIngs = [...(ingredients || [])].sort((a, b) => a.name.localeCompare(b.name)); const [name, setName] = useState(''); const [qty, setQty] = useState(1); const [unit, setUnit] = useState('ud'); const [busy, setBusy] = useState(false); const planDate = day?.date; const mealType = slot?.id; async function doAdd() { if (!name.trim()) { window.showToast('Nombre requerido', 'error'); return; } if (!planDate || !mealType) { window.showToast('Datos inválidos', 'error'); return; } setBusy(true); try { await window.api(`/api/planning/${planDate}/${mealType}/loose`, 'POST', { ingredient_name: name.trim(), qty: parseFloat(qty) || 1, unit, }); if (onSave) await onSave(); onClose(); } catch (e) { window.showToast(e.message, 'error'); } finally { setBusy(false); } } return ( EXTRA FUERA DE RECETA
} >
setQty(+e.target.value)} />
); } // ========================================================= // RECIPE — view/edit a recipe // ========================================================= function RecipeModal({ recipe, edit: startEdit, onSave, onClose }) { const { planningWeek, ingredients, reload } = window.RC_STORE.useStore(); const [mode, setMode] = useState(startEdit ? 'edit' : 'view'); // 'view' | 'edit' | 'plan' const [busy, setBusy] = useState(false); const [newIngTarget, setNewIngTarget] = useState(null); // edit form state const [name, setName] = useState(recipe?.name || ''); const [kinds, setKinds] = useState(() => (recipe?.kinds || recipe?.meal_types || []).map(k => { const m = { desayuno: 'Desayuno', comida: 'Comida', merienda: 'Merienda', cena: 'Cena' }; return m[k] || (typeof k === 'string' ? k.charAt(0).toUpperCase() + k.slice(1) : k); })); const [notes, setNotes] = useState(recipe?.notes || ''); const [isPublic, setIsPublic] = useState(!!recipe?.is_public); const [ings, setIngs] = useState(() => (recipe?.ingredients || []).map(i => ({ ...i }))); // planning picker state const [planDate, setPlanDate] = useState(''); const [planSlot, setPlanSlot] = useState('desayuno'); const [planRaciones, setPlanRaciones] = useState(2); const sortedIngs = [...(ingredients || [])].sort((a, b) => a.name.localeCompare(b.name)); const toggleKind = (k) => setKinds(kk => kk.includes(k) ? kk.filter(x => x !== k) : [...kk, k]); const addIng = () => setIngs(l => [...l, { name: '', qty: '', unit: 'ud' }]); const updIng = (i, k, v) => setIngs(l => l.map((row, j) => j === i ? { ...row, [k]: v } : row)); const delIng = (i) => setIngs(l => l.filter((_, j) => j !== i)); function handleIngName(i, v) { if (v === '__new__') { setNewIngTarget(i); } else { updIng(i, 'name', v); } } async function doDelete() { if (!recipe?.id) return; setBusy(true); try { await window.api(`/api/recipes/${recipe.id}`, 'DELETE'); if (onSave) await onSave(); onClose(); } catch (e) { window.showToast(e.message, 'error'); } finally { setBusy(false); } } async function doSaveEdit() { if (!name.trim()) { window.showToast('Nombre requerido', 'error'); return; } const validNames = new Set(sortedIngs.map(ing => ing.name.toLowerCase())); const invalidIng = ings.filter(r => r.name?.trim()).find(r => !validNames.has(r.name.trim().toLowerCase())); if (invalidIng) { window.showToast(`Ingrediente no válido: ${invalidIng.name}`, 'error'); return; } setBusy(true); try { await window.api(`/api/recipes/${recipe.id}`, 'PUT', { name: name.trim(), notes, meal_types: kinds.map(k => k.toLowerCase()), ingredients: ings.filter(r => r.name?.trim()).map(r => ({ name: r.name.trim(), qty: parseFloat(r.qty) || 1, unit: r.unit, })), is_public: isPublic, }); window.showToast('Receta guardada'); if (onSave) await onSave(); onClose(); } catch (e) { window.showToast(e.message, 'error'); } finally { setBusy(false); } } async function doAddPlanning() { if (!planDate || !planSlot) { window.showToast('Elige día y comida', 'error'); return; } setBusy(true); try { await window.api(`/api/planning/${planDate}/${planSlot}/recipes`, 'POST', { recipe_id: recipe.id, raciones: planRaciones, }); window.showToast('Añadido al planning'); onClose(); } catch (e) { window.showToast(e.message, 'error'); } finally { setBusy(false); } } const viewIngs = recipe?.ingredients || []; const viewKinds = recipe?.kinds || recipe?.meal_types || []; if (mode === 'edit') { return ( <> {newIngTarget !== null && ( { updIng(newIngTarget, 'name', ingName); setNewIngTarget(null); }} onClose={() => setNewIngTarget(null)} /> )}
} >
setName(e.target.value)} />
{['Desayuno', 'Comida', 'Merienda', 'Cena'].map(k => ( ))}