/* ============================================================
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
{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)} />
| # | Client | Chambre | Arrivée | Départ |
Nuits | Total | Paiement | Statut | |
{filtered.length === 0 && (
| Aucune réservation pour le moment. |
)}
{filtered.map(b => (
| #{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;