/* ============================================================ HÔTEL OMÉGA — Admin Login + dashboard (stats, état chambres, calendrier, table) ============================================================ */ const { useState: useStateA, useEffect: useEffectA, useMemo: useMemoA } = React; /* ---------- STORE (rooms + bookings) ---------- */ const ROOM_STATUS = ["available", "occupied", "cleaning"]; const STATUS_LABEL = { available: "Disponible", occupied: "Occupée", cleaning: "Nettoyage", }; // Inventaire complet : 4 étages × (4 doubles + 1 triple) = 20 chambres // Doubles : 250 R$/nuit · Triples : 300 R$/nuit const ADMIN_ROOMS_DEF = (() => { const rooms = []; for (let floor = 1; floor <= 4; floor++) { // 4 chambres doubles par étage : x01, x02, x03, x04 for (let n = 1; n <= 4; n++) { const num = `${floor}0${n}`; rooms.push({ id: `D${num}`, num, floor, type: "Double", name: `Chambre Double — ${num}`, capacity: 2, price: 250, status: "available", }); } // 1 chambre triple par étage : x05 const tnum = `${floor}05`; rooms.push({ id: `T${tnum}`, num: tnum, floor, type: "Triple", name: `Chambre Triple — ${tnum}`, capacity: 3, price: 300, status: "available", }); } return rooms; })(); function loadRooms() { try { const raw = localStorage.getItem("omega_admin_rooms"); if (raw) { const stored = JSON.parse(raw); // Migration auto : si moins de 20 chambres en storage, on regénère // tout en préservant le statut des chambres qui existaient déjà if (Array.isArray(stored) && stored.length < ADMIN_ROOMS_DEF.length) { const merged = ADMIN_ROOMS_DEF.map(def => { const old = stored.find(s => s.num === def.num); return old ? { ...def, status: old.status || "available" } : { ...def }; }); saveRooms(merged); return merged; } return stored; } } catch (e) {} return ADMIN_ROOMS_DEF.map(r => ({ ...r })); } function saveRooms(rs) { try { localStorage.setItem("omega_admin_rooms", JSON.stringify(rs)); } catch (e) {} } // ─── Bookings: routed through OmegaDB (Firebase or localStorage fallback) ─── // We keep a synchronous in-memory cache so legacy code that calls loadBookings() // without await still works. The cache is hydrated by a subscription on init. let __bookingsCache = (() => { try { const raw = localStorage.getItem("omega_admin_bookings"); if (raw) return JSON.parse(raw); } catch (e) {} return []; })(); let __dbSubscribed = false; function ensureDbSubscription() { if (__dbSubscribed) return; if (!window.OmegaDB) { // OmegaDB might load slightly later — retry shortly setTimeout(ensureDbSubscription, 200); return; } __dbSubscribed = true; window.OmegaDB.subscribeBookings((list) => { __bookingsCache = list; window.dispatchEvent(new CustomEvent("omega-store-changed")); }); } ensureDbSubscription(); function loadBookings() { return __bookingsCache.slice(); } // Public API: pushed by the booking drawer window.OmegaStore = { async pushBooking(b) { // ── Attribution automatique de la chambre ── // On cherche la 1ère chambre du bon type qui est encore "available". // Le numéro est ajouté à la réservation AVANT envoi (Firebase + email). const rooms = loadRooms(); const target = rooms.find(r => r.type === b.roomType && r.status === "available"); const assignedRoomNum = target ? target.num : null; const booking = { createdAt: new Date().toISOString(), status: "Confirmée", roomNum: assignedRoomNum, // ← numéro attribué (ex: "203") roomLabel: target ? target.name : null, // ← libellé complet (ex: "Chambre Double — 203") ...b, }; if (window.OmegaDB) { try { const id = await window.OmegaDB.pushBooking(booking); booking.id = id; } catch (e) { console.warn("[OmegaStore] DB push failed:", e); booking.id = "L_" + Date.now(); } } else { booking.id = "L_" + Date.now(); } // Marquer la chambre attribuée comme occupée if (target) { target.status = "occupied"; saveRooms(rooms); } // Email notifications (best-effort, non-blocking) if (window.OmegaMail && window.OmegaMail.isConfigured()) { Promise.all([ window.OmegaMail.notifyManager(booking).catch(() => {}), window.OmegaMail.confirmClient(booking).catch(() => {}), ]); } window.dispatchEvent(new CustomEvent("omega-store-changed")); return booking; }, async checkAvailability(roomType, checkin, checkout) { if (!window.OmegaDB) return { ok: true }; return window.OmegaDB.isRoomAvailable(roomType, checkin, checkout); }, }; /* ---------- ADMIN LOGIN ---------- */ function AdminLogin({ onLogin }) { const [user, setUser] = useStateA(""); const [pass, setPass] = useStateA(""); const [err, setErr] = useStateA(false); const submit = () => { if (user === "admin" && pass === "omega2024") { setErr(false); onLogin(); } else { setErr(true); } }; return (
Hôtel Oméga
Ω

Espace administrateur

Accès réservé. Tableau de bord, réservations & gestion des chambres.

{err &&
Identifiants incorrects.
}
Démo : admin / omega2024
); } /* ---------- DASHBOARD ---------- */ function fmtDate(ds) { if (!ds) return "—"; const [y, m, d] = ds.split("-"); return `${d}/${m}/${y}`; } const MONTHS = ["Janvier","Février","Mars","Avril","Mai","Juin","Juillet","Août","Septembre","Octobre","Novembre","Décembre"]; const DAYS = ["Lu","Ma","Me","Je","Ve","Sa","Di"]; function StatsRow({ rooms, bookings }) { const conf = bookings.filter(b => b.status === "Confirmée"); const rev = conf.reduce((s, b) => s + (b.total || 0), 0); const av = rooms.filter(r => r.status === "available").length; const oc = rooms.filter(r => r.status === "occupied").length; const cl = rooms.filter(r => r.status === "cleaning").length; return (
Réservations
{bookings.length}
{conf.length} confirmées
Revenus
{rev.toLocaleString("fr-FR")} R$
Total encaissé
Disponibles
{av}
sur {rooms.length} chambres
Occupées
{oc}
{cl} en nettoyage
); } function RoomsPanel({ rooms, bookings, onOpen }) { // Pour chaque chambre, trouver le client qui l'occupe (réservation confirmée // dont le séjour englobe aujourd'hui) const today = (() => { const d = new Date(); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; })(); const findGuest = (room) => { return bookings.find(b => b.status === "Confirmée" && b.roomNum === room.num && b.checkin <= today && b.checkout > today ); }; // Grouper par étage const floors = [1, 2, 3, 4]; const byFloor = floors.map(f => ({ floor: f, rooms: rooms.filter(r => r.floor === f), })); return (
État des chambres
{byFloor.map(({ floor, rooms: floorRooms }) => (
Étage {floor}
{floorRooms.map(r => { const guest = findGuest(r); return ( ); })}
))}
); } function CalendarPanel({ rooms, bookings }) { const now = new Date(); const [y, setY] = useStateA(now.getFullYear()); const [m, setM] = useStateA(now.getMonth()); const first = new Date(y, m, 1).getDay(); const offset = first === 0 ? 6 : first - 1; const days = new Date(y, m + 1, 0).getDate(); const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`; const total = rooms.length || 1; const cells = []; for (let i = 0; i < offset; i++) cells.push(null); for (let d = 1; d <= days; d++) { const ds = `${y}-${String(m + 1).padStart(2, "0")}-${String(d).padStart(2, "0")}`; const c = bookings.filter(b => b.status === "Confirmée" && b.checkin <= ds && b.checkout > ds).length; cells.push({ d, ds, c, today: ds === today }); } const prev = () => { let nm = m - 1, ny = y; if (nm < 0) { nm = 11; ny--; } setM(nm); setY(ny); }; const next = () => { let nm = m + 1, ny = y; if (nm > 11) { nm = 0; ny++; } setM(nm); setY(ny); }; // Niveau d'occupation → couleur // vide : rien · vert : 1 à 33% · jaune : 34 à 66% · rouge : 67%+ const levelClass = (count) => { if (count === 0) return ""; const ratio = count / total; if (ratio >= 1) return " is-full"; // complet → rouge foncé if (ratio > 0.66) return " is-full"; // ≥ 67% → rouge if (ratio > 0.33) return " is-mid"; // 34-66% → jaune return " is-low"; // 1-33% → vert }; return (
Calendrier
{MONTHS[m]} {y}
{DAYS.map(d =>
{d}
)} {cells.map((c, i) => { if (!c) return
; const cls = "ad-cal-day" + (c.today ? " is-today" : "") + levelClass(c.c); const tip = c.c === 0 ? "Aucune réservation" : `${c.c}/${total} chambres occupées`; return (
{c.d} {c.c > 0 && {c.c}}
); })}
Peu d'occupation Moitié Complet Aujourd'hui
); } function BookingsTable({ bookings, onCancel }) { const [search, setSearch] = useStateA(""); const [stat, setStat] = useStateA(""); const [type, setType] = useStateA(""); const filtered = bookings.filter(b => (!search || ((b.firstName + " " + b.lastName + " " + b.email).toLowerCase().includes(search.toLowerCase()))) && (!stat || b.status === stat) && (!type || b.roomType === type) ); return (
Réservations
setSearch(e.target.value)} />
{filtered.length === 0 && ( )} {filtered.map(b => ( ))}
#ClientChambreArrivéeDépart NuitsTotalPaiementStatut
Aucune réservation pour le moment.
#{String(b.id).slice(-4)}
{b.firstName} {b.lastName}
{b.email}
N°{b.roomNum || "—"} · {b.roomType}
{fmtDate(b.checkin)} {fmtDate(b.checkout)} {b.nights} {b.total} R$ {b.method || "card"} {b.status} {b.status === "Confirmée" ? ( ) : "—"}
); } function RoomEditModal({ room, onClose, onSave, bookings }) { const [status, setStatus] = useStateA(room.status); const list = bookings.filter(b => b.roomType === room.type && b.status === "Confirmée"); return (
e.target === e.currentTarget && onClose()}>
Chambre N°{room.num}
{room.name}
Changer le statut
{ROOM_STATUS.map(s => ( ))}
Réservations actives
{list.length === 0 ? (
Aucune réservation active.
) : list.map(b => (
{b.firstName} {b.lastName}
{fmtDate(b.checkin)} → {fmtDate(b.checkout)} · {b.total} R$
))}
); } function AdminDashboard({ onLogout, hideLogout }) { const [rooms, setRooms] = useStateA(loadRooms()); const [bookings, setBookings] = useStateA(loadBookings()); const [edit, setEdit] = useStateA(null); // Listen to store changes (new bookings from drawer) useEffectA(() => { const handler = () => { setRooms(loadRooms()); setBookings(loadBookings()); }; window.addEventListener("omega-store-changed", handler); return () => window.removeEventListener("omega-store-changed", handler); }, []); const cancelBooking = (id) => { if (!window.confirm("Annuler cette réservation ?")) return; const cancelled = bookings.find(b => b.id === id); if (window.OmegaDB) { window.OmegaDB.updateBooking(id, { status: "Annulée" }).catch(() => {}); } // Free a matching occupied room if no other confirmed for that type if (cancelled) { const next = bookings.map(b => b.id === id ? { ...b, status: "Annulée" } : b); const stillBooked = next.some(b => b.id !== id && b.roomType === cancelled.roomType && b.status === "Confirmée"); if (!stillBooked) { // Flip ONE matching occupied room back to available let flipped = false; const nr2 = rooms.map(r => { if (!flipped && r.type === cancelled.roomType && r.status === "occupied") { flipped = true; return { ...r, status: "available" }; } return r; }); setRooms(nr2); saveRooms(nr2); } } }; const saveRoomStatus = (newStatus) => { const nr = rooms.map(r => r.id === edit.id ? { ...r, status: newStatus } : r); setRooms(nr); saveRooms(nr); setEdit(null); }; return (
Administration

Tableau de bord — Hôtel Oméga

{edit && ( setEdit(null)} onSave={saveRoomStatus} /> )}
); } function AdminPage() { const [auth, setAuth] = useStateA(false); const [tab, setTab] = useStateA("dashboard"); if (!auth) return setAuth(true)} />; return (
{tab === "dashboard" && setAuth(false)} hideLogout />} {tab === "config" && (window.ConfigPanel ? :
Chargement…
)}
); } window.AdminPage = AdminPage;