/* ============================================================ RESERVAS — disponibilidad por franja horaria Combina cupo automático (máx. personas/hora) + bloqueos manuales del administrador (día completo o franja puntual). Todo se guarda en este dispositivo (localStorage). ============================================================ */ const { HOURS: RES_HOURS } = window; const RES_CAP_KEY = "do_res_capacity"; const RES_LIST_KEY = "do_reservations"; const RES_BLOCKS_KEY = "do_res_blocks"; const DEFAULT_CAPACITY = 15; // personas por franja horaria function loadCapacity() { const n = parseInt(localStorage.getItem(RES_CAP_KEY), 10); return Number.isFinite(n) && n > 0 ? n : DEFAULT_CAPACITY; } function saveCapacity(n) { localStorage.setItem(RES_CAP_KEY, String(n)); } function loadReservations() { try { return JSON.parse(localStorage.getItem(RES_LIST_KEY) || "[]"); } catch (e) { return []; } } function saveReservations(list) { localStorage.setItem(RES_LIST_KEY, JSON.stringify(list)); } function loadBlocks() { try { const b = JSON.parse(localStorage.getItem(RES_BLOCKS_KEY) || "null"); if (b && typeof b === "object") return { days: b.days || [], slots: b.slots || [] }; } catch (e) {} return { days: [], slots: [] }; } function saveBlocks(b) { localStorage.setItem(RES_BLOCKS_KEY, JSON.stringify(b)); } // Franjas horarias del día (cada hora, dentro del horario de atención) function getHourSlots() { const start = Math.ceil(RES_HOURS.open); const end = Math.floor(RES_HOURS.close); const out = []; for (let h = start; h < end; h++) { const period = h >= 12 ? "pm" : "am"; const h12 = h > 12 ? h - 12 : h; out.push({ value: String(h).padStart(2, "0") + ":00", label: h12 + ":00 " + period }); } return out; } function bookedCount(reservations, date, time) { return reservations .filter(r => r.date === date && r.time === time && r.status !== "cancelada") .reduce((s, r) => s + (r.people || 0), 0); } function slotStatus(date, time, reservations, blocks, capacity) { const dayBlocked = blocks.days.includes(date); const slotBlocked = blocks.slots.includes(date + "|" + time); const booked = bookedCount(reservations, date, time); const full = booked >= capacity; return { available: !dayBlocked && !slotBlocked && !full, dayBlocked, slotBlocked, full, booked, capacity }; } /* ---- Selector de franja horaria (para el cliente) ---- */ function SlotPicker({ date, time, onPick }) { const [, force] = useState(0); useEffect(() => { const onStorage = () => force(x => x + 1); window.addEventListener("storage", onStorage); return () => window.removeEventListener("storage", onStorage); }, []); if (!date) return

Elige primero una fecha para ver las horas disponibles.

; const reservations = loadReservations(); const blocks = loadBlocks(); const capacity = loadCapacity(); const slots = getHourSlots(); const dayFull = blocks.days.includes(date); return (
{dayFull &&

No hay disponibilidad este día. Elige otra fecha.

} {!dayFull && slots.map(s => { const st = slotStatus(date, s.value, reservations, blocks, capacity); return ( ); })}
); } /* ---- Panel de administración de reservas ---- */ function ReservationsAdmin() { const [list, setList] = useState(loadReservations); const [blocks, setBlocks] = useState(loadBlocks); const [capacity, setCapacity] = useState(loadCapacity); const [blockDate, setBlockDate] = useState(""); const [blockSlotDate, setBlockSlotDate] = useState(""); const [blockSlotTime, setBlockSlotTime] = useState(getHourSlots()[0].value); function updateList(next) { setList(next); saveReservations(next); } function updateBlocks(next) { setBlocks(next); saveBlocks(next); } function updateCapacity(n) { setCapacity(n); saveCapacity(n); } function setStatus(id, status) { updateList(list.map(r => r.id === id ? { ...r, status } : r)); } function addDayBlock() { if (!blockDate || blocks.days.includes(blockDate)) return; updateBlocks({ ...blocks, days: [...blocks.days, blockDate].sort() }); setBlockDate(""); } function removeDayBlock(d) { updateBlocks({ ...blocks, days: blocks.days.filter(x => x !== d) }); } function addSlotBlock() { const key = blockSlotDate + "|" + blockSlotTime; if (!blockSlotDate || blocks.slots.includes(key)) return; updateBlocks({ ...blocks, slots: [...blocks.slots, key].sort() }); setBlockSlotDate(""); } function removeSlotBlock(k) { updateBlocks({ ...blocks, slots: blocks.slots.filter(x => x !== k) }); } const upcoming = list .filter(r => r.status !== "cancelada") .sort((a, b) => (a.date + a.time).localeCompare(b.date + b.time)); const slots = getHourSlots(); return (
Cupo por franja horaria
updateCapacity(Math.max(1, parseInt(e.target.value, 10) || 1))} /> personas por hora, máximo
Bloquear un día completo
setBlockDate(e.target.value)} />
{blocks.days.length > 0 && (
{blocks.days.map(d => ( {d} ))}
)}
Bloquear una franja puntual
setBlockSlotDate(e.target.value)} />
{blocks.slots.length > 0 && (
{blocks.slots.map(k => ( {k.replace("|", " · ")} ))}
)}
Reservas de clientes {upcoming.length} activa(s)
{upcoming.length === 0 &&

Aún no hay reservas registradas desde la carta.

} {upcoming.map(r => (
{r.name} {r.date} · {r.time} · {r.people} pers. {r.notes && “{r.notes}”}
{r.status} {r.status !== "cumplida" && } {r.status !== "cancelada" && }
))}
); } function addReservation(rec) { const list = loadReservations(); const next = [...list, { ...rec, id: "R" + Date.now() + Math.random().toString(36).slice(2, 6), status: "pendiente" }]; saveReservations(next); return next; } Object.assign(window, { loadCapacity, saveCapacity, loadReservations, saveReservations, loadBlocks, saveBlocks, getHourSlots, bookedCount, slotStatus, SlotPicker, ReservationsAdmin, addReservation, });