chu-iptv/apps/mobile-web/index.html

1477 lines
75 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="theme-color" content="#0a0e1a">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<title>CHU-IPTV — Patient</title>
<link rel="manifest" href="manifest.json">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
<style>
:root {
--accent: #00d4aa; --accent-dim: rgba(0,212,170,0.15);
--bg: #0a0e1a; --bg-card: #141a2e; --bg-elevated: #1a2240;
--text: #f0f4ff; --text-dim: #8892b0;
--danger: #ff4466; --warning: #ffaa33; --success: #00d4aa;
--radius: 16px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Inter', system-ui, sans-serif; background: var(--bg); color: var(--text); min-height: 100vh; overflow-x: hidden; -webkit-tap-highlight-color: transparent; }
/* ═══ PAIRING ═══ */
#pairingScreen { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100vh; padding: 32px 24px; text-align: center; }
#pairingScreen.hidden { display: none; }
.pair-logo { width: 64px; height: 64px; background: var(--accent); border-radius: 16px; display: flex; align-items: center; justify-content: center; font-size: 28px; font-weight: 900; color: var(--bg); margin-bottom: 16px; }
.pair-title { font-size: 22px; font-weight: 700; margin-bottom: 6px; }
.pair-sub { color: var(--text-dim); font-size: 14px; margin-bottom: 32px; }
.code-input { display: flex; gap: 6px; margin-bottom: 20px; }
.code-input input { width: 38px; height: 48px; text-align: center; font-size: 20px; font-weight: 700; border: 2px solid rgba(255,255,255,0.1); border-radius: 10px; background: var(--bg-card); color: var(--text); outline: none; transition: border-color 0.3s; }
.code-input input:focus { border-color: var(--accent); box-shadow: 0 0 12px var(--accent-dim); }
.pair-btn { width: 100%; max-width: 320px; padding: 16px; background: var(--accent); color: var(--bg); border: none; border-radius: var(--radius); font-size: 16px; font-weight: 700; cursor: pointer; transition: all 0.2s; }
.pair-btn:active { transform: scale(0.97); }
.pair-btn:disabled { opacity: 0.4; }
.pair-error { color: var(--danger); font-size: 13px; margin-top: 12px; min-height: 18px; }
.pair-hint { color: var(--text-dim); font-size: 12px; margin-top: 24px; padding: 12px 20px; background: var(--bg-card); border-radius: 12px; }
/* ═══ APP ═══ */
#appScreen { display: none; min-height: 100vh; padding-bottom: 76px; }
#appScreen.active { display: block; }
.app-header { position: sticky; top: 0; z-index: 50; background: rgba(10,14,26,0.94); backdrop-filter: blur(16px); padding: 14px 20px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid rgba(255,255,255,0.05); }
.hdr-left { display: flex; align-items: center; gap: 10px; }
.hdr-logo { font-size: 15px; font-weight: 800; color: var(--accent); }
.hdr-room { font-size: 11px; color: var(--text-dim); background: var(--bg-card); padding: 4px 10px; border-radius: 10px; }
.hdr-status { display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--success); font-weight: 500; }
.hdr-dot { width: 7px; height: 7px; background: var(--success); border-radius: 50%; animation: pulse 2s infinite; }
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.3; } }
/* Tabs */
.tab-nav { position: fixed; bottom: 0; left: 0; right: 0; z-index: 50; background: rgba(10,14,26,0.96); backdrop-filter: blur(16px); border-top: 1px solid rgba(255,255,255,0.05); display: flex; padding: 6px 0 max(6px, env(safe-area-inset-bottom)); }
.tab-item { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 3px; padding: 8px 4px; cursor: pointer; color: var(--text-dim); transition: color 0.2s; position: relative; }
.tab-item.active { color: var(--accent); }
.tab-item.active::before { content: ''; position: absolute; top: 0; width: 20px; height: 2px; background: var(--accent); border-radius: 1px; }
.tab-item svg { width: 20px; height: 20px; }
.tab-item span { font-size: 9px; font-weight: 600; }
.tab-badge { position: absolute; top: 4px; right: calc(50% - 16px); width: 14px; height: 14px; background: var(--danger); border-radius: 50%; font-size: 8px; display: none; align-items: center; justify-content: center; font-weight: 700; color: #fff; }
.tab-badge.visible { display: flex; }
/* Pages */
.page { display: none; padding: 20px; animation: pageIn 0.25s ease; }
.page.active { display: block; }
@keyframes pageIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
.section-title { font-size: 12px; color: var(--text-dim); text-transform: uppercase; letter-spacing: 1px; font-weight: 600; margin-bottom: 12px; }
/* ═══ PAGE TV ═══ */
.now-playing { background: var(--bg-card); border: 1px solid rgba(255,255,255,0.06); border-radius: var(--radius); padding: 16px; margin-bottom: 20px; display: flex; align-items: center; gap: 14px; }
.np-num { width: 44px; height: 44px; background: var(--accent); border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 20px; font-weight: 800; color: var(--bg); }
.np-info .np-name { font-size: 16px; font-weight: 600; }
.np-info .np-cat { font-size: 11px; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.5px; }
.np-live { margin-left: auto; display: flex; align-items: center; gap: 5px; font-size: 10px; color: var(--danger); font-weight: 600; }
.np-live::before { content: ''; width: 6px; height: 6px; background: var(--danger); border-radius: 50%; animation: pulse 1.5s infinite; }
.channel-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 24px; }
.ch-btn { background: var(--bg-card); border: 1px solid rgba(255,255,255,0.06); border-radius: 12px; padding: 12px 8px; text-align: center; cursor: pointer; transition: all 0.15s; }
.ch-btn:active { transform: scale(0.93); background: var(--bg-elevated); }
.ch-btn.active { border-color: var(--accent); background: var(--accent-dim); }
.ch-btn .ch-n { font-size: 18px; font-weight: 700; }
.ch-btn .ch-l { font-size: 9px; color: var(--text-dim); margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.controls-row { display: flex; gap: 10px; justify-content: center; }
.ctrl-btn { flex: 1; max-width: 80px; background: var(--bg-card); border: 1px solid rgba(255,255,255,0.06); border-radius: 14px; padding: 14px 8px; text-align: center; cursor: pointer; transition: all 0.15s; }
.ctrl-btn:active { transform: scale(0.92); background: var(--bg-elevated); }
.ctrl-btn svg { width: 22px; height: 22px; margin: 0 auto; display: block; color: var(--text); }
.ctrl-btn span { font-size: 9px; color: var(--text-dim); margin-top: 4px; display: block; }
.ctrl-btn.danger { border-color: rgba(255,68,102,0.2); }
.ctrl-btn.danger svg { color: var(--danger); }
/* ═══ PAGE DEMANDES ═══ */
.req-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; }
.req-card { background: var(--bg-card); border: 1px solid rgba(255,255,255,0.06); border-radius: var(--radius); padding: 18px 14px; text-align: center; cursor: pointer; transition: all 0.2s; }
.req-card:active { transform: scale(0.95); border-color: var(--accent); }
.req-card .req-icon { font-size: 28px; margin-bottom: 6px; display: block; }
.req-card .req-label { font-size: 13px; font-weight: 600; }
.req-card .req-desc { font-size: 10px; color: var(--text-dim); margin-top: 3px; }
.req-card.sent { border-color: var(--success); background: rgba(0,212,170,0.06); pointer-events: none; }
.req-card.sent::after { content: '✓ Envoyé'; display: block; font-size: 10px; color: var(--success); margin-top: 6px; font-weight: 600; }
.history-section { margin-top: 24px; }
.hist-item { background: var(--bg-card); border-radius: 12px; padding: 12px 14px; margin-bottom: 8px; display: flex; align-items: center; gap: 10px; }
.hist-item .hi-icon { font-size: 18px; }
.hist-item .hi-text { flex: 1; }
.hist-item .hi-label { font-size: 12px; font-weight: 500; }
.hist-item .hi-time { font-size: 10px; color: var(--text-dim); }
.hist-item .hi-status { font-size: 10px; font-weight: 600; padding: 3px 8px; border-radius: 6px; }
.hi-status.pending { background: rgba(255,170,51,0.12); color: var(--warning); }
.hi-status.ack { background: rgba(0,212,170,0.12); color: var(--success); }
.hi-status.done { background: rgba(100,100,100,0.12); color: #888; }
/* ═══ PAGE CHAT ═══ */
.chat-wrap { display: flex; flex-direction: column; height: calc(100vh - 150px); }
.chat-msgs { flex: 1; overflow-y: auto; padding-bottom: 12px; }
.msg { max-width: 82%; margin-bottom: 10px; padding: 10px 14px; border-radius: 16px; font-size: 14px; line-height: 1.4; animation: msgIn 0.2s ease; }
@keyframes msgIn { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } }
.msg.patient { background: var(--accent); color: var(--bg); margin-left: auto; border-bottom-right-radius: 4px; }
.msg.staff { background: var(--bg-card); border: 1px solid rgba(255,255,255,0.06); border-bottom-left-radius: 4px; }
.msg .msg-sender { font-size: 10px; font-weight: 600; margin-bottom: 3px; opacity: 0.7; }
.msg .msg-time { font-size: 9px; margin-top: 3px; opacity: 0.5; text-align: right; }
.chat-bar { display: flex; gap: 8px; padding-top: 10px; border-top: 1px solid rgba(255,255,255,0.05); }
.chat-bar input { flex: 1; background: var(--bg-card); border: 1px solid rgba(255,255,255,0.08); border-radius: 22px; padding: 12px 18px; font-size: 14px; color: var(--text); outline: none; transition: border-color 0.3s; }
.chat-bar input:focus { border-color: var(--accent); }
.chat-bar button { width: 42px; height: 42px; background: var(--accent); border: none; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; }
.chat-bar button:active { transform: scale(0.9); }
.chat-bar button svg { width: 18px; height: 18px; color: var(--bg); }
/* ═══ PAGE CHAMBRE ═══ */
.domo-card { background: var(--bg-card); border: 1px solid rgba(255,255,255,0.06); border-radius: var(--radius); padding: 16px 18px; margin-bottom: 10px; display: flex; align-items: center; justify-content: space-between; }
.domo-left { display: flex; align-items: center; gap: 12px; }
.domo-icon { font-size: 24px; }
.domo-label { font-size: 14px; font-weight: 500; }
.domo-status { font-size: 11px; color: var(--text-dim); }
.toggle { width: 48px; height: 26px; background: rgba(255,255,255,0.1); border-radius: 13px; position: relative; cursor: pointer; transition: background 0.3s; }
.toggle.on { background: var(--accent); }
.toggle::after { content: ''; position: absolute; top: 3px; left: 3px; width: 20px; height: 20px; background: #fff; border-radius: 50%; transition: transform 0.3s; }
.toggle.on::after { transform: translateX(22px); }
/* ═══ MENU PLUS (GRILLE) ═══ */
.more-overlay { display: none; position: fixed; inset: 0; z-index: 100; background: rgba(10,14,26,0.97); backdrop-filter: blur(20px); animation: fadeIn 0.2s ease; }
.more-overlay.active { display: flex; flex-direction: column; }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
.more-header { display: flex; justify-content: space-between; align-items: center; padding: 20px 24px; }
.more-title { font-size: 20px; font-weight: 700; }
.more-close { width: 36px; height: 36px; background: var(--bg-card); border: 1px solid rgba(255,255,255,0.1); border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; font-size: 18px; color: var(--text-dim); }
.more-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; padding: 10px 24px; flex: 1; align-content: start; }
.more-item { background: var(--bg-card); border: 1px solid rgba(255,255,255,0.06); border-radius: 18px; padding: 22px 12px; text-align: center; cursor: pointer; transition: all 0.2s; }
.more-item:active { transform: scale(0.93); border-color: var(--accent); background: var(--accent-dim); }
.more-item .mi-icon { font-size: 32px; margin-bottom: 8px; }
.more-item .mi-label { font-size: 12px; font-weight: 600; }
.more-item .mi-desc { font-size: 9px; color: var(--text-dim); margin-top: 3px; }
/* ═══ PAGE RADIO ═══ */
.radio-card { background: var(--bg-card); border: 1px solid rgba(255,255,255,0.06); border-radius: 14px; padding: 14px 16px; margin-bottom: 10px; display: flex; align-items: center; gap: 14px; cursor: pointer; transition: all 0.2s; }
.radio-card:active { transform: scale(0.97); }
.radio-card.playing { border-color: var(--accent); background: var(--accent-dim); }
.radio-icon { font-size: 28px; width: 44px; height: 44px; display: flex; align-items: center; justify-content: center; background: var(--bg-elevated); border-radius: 12px; }
.radio-info { flex: 1; }
.radio-name { font-size: 14px; font-weight: 600; }
.radio-genre { font-size: 11px; color: var(--text-dim); text-transform: capitalize; }
.radio-play { width: 36px; height: 36px; background: var(--accent); border-radius: 50%; display: flex; align-items: center; justify-content: center; color: var(--bg); font-size: 14px; font-weight: 900; }
.radio-stop { background: var(--danger); }
.genre-pills { display: flex; gap: 8px; overflow-x: auto; padding-bottom: 12px; margin-bottom: 14px; }
.genre-pill { padding: 6px 14px; border-radius: 20px; font-size: 11px; font-weight: 600; cursor: pointer; white-space: nowrap; background: var(--bg-card); color: var(--text-dim); border: 1px solid rgba(255,255,255,0.08); transition: all 0.2s; }
.genre-pill.active { background: var(--accent); color: var(--bg); border-color: var(--accent); }
/* ═══ PAGE REPAS ═══ */
.meal-section { margin-bottom: 20px; }
.meal-type-title { font-size: 14px; font-weight: 700; margin-bottom: 10px; display: flex; align-items: center; gap: 8px; }
.meal-card { background: var(--bg-card); border: 1px solid rgba(255,255,255,0.06); border-radius: 14px; padding: 14px 16px; margin-bottom: 8px; cursor: pointer; transition: all 0.2s; }
.meal-card:active { transform: scale(0.97); }
.meal-card.selected { border-color: var(--accent); background: var(--accent-dim); }
.meal-card.selected::after { content: '✓ Choisi'; float: right; font-size: 10px; color: var(--accent); font-weight: 700; }
.meal-name { font-size: 14px; font-weight: 600; margin-bottom: 4px; }
.meal-desc { font-size: 11px; color: var(--text-dim); line-height: 1.4; }
.meal-meta { display: flex; gap: 8px; margin-top: 6px; }
.meal-tag { font-size: 9px; padding: 2px 8px; border-radius: 6px; background: rgba(255,255,255,0.06); color: var(--text-dim); }
.meal-tag.veg { background: rgba(0,200,100,0.12); color: #4caf50; }
.meal-tag.allergen { background: rgba(255,170,51,0.12); color: var(--warning); }
/* ═══ PAGE JEUX ═══ */
.game-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; }
.game-card { background: var(--bg-card); border: 1px solid rgba(255,255,255,0.06); border-radius: 16px; padding: 20px 14px; text-align: center; cursor: pointer; transition: all 0.2s; }
.game-card:active { transform: scale(0.95); border-color: var(--accent); }
.game-icon { font-size: 36px; margin-bottom: 8px; }
.game-name { font-size: 13px; font-weight: 600; margin-bottom: 4px; }
.game-desc { font-size: 10px; color: var(--text-dim); line-height: 1.3; }
.game-diff { font-size: 9px; color: var(--accent); margin-top: 6px; font-weight: 600; }
/* ═══ PAGE INFOS ═══ */
.info-card { background: var(--bg-card); border: 1px solid rgba(255,255,255,0.06); border-radius: 14px; padding: 16px; margin-bottom: 12px; }
.info-card-title { font-size: 14px; font-weight: 600; margin-bottom: 8px; display: flex; align-items: center; gap: 8px; }
.info-card-body { font-size: 12px; color: var(--text-dim); line-height: 1.6; }
.info-row { display: flex; justify-content: space-between; align-items: center; padding: 8px 0; border-bottom: 1px solid rgba(255,255,255,0.04); }
.info-row:last-child { border-bottom: none; }
.weather-big { text-align: center; padding: 20px; }
.weather-icon { font-size: 48px; margin-bottom: 8px; }
.weather-temp { font-size: 32px; font-weight: 800; }
.weather-cond { font-size: 13px; color: var(--text-dim); margin-top: 4px; }
/* ═══ PAGE SATISFACTION ═══ */
.stars-row { display: flex; gap: 8px; justify-content: center; margin: 16px 0; }
.star { font-size: 36px; cursor: pointer; opacity: 0.3; transition: all 0.2s; }
.star.active { opacity: 1; transform: scale(1.1); }
.sat-category { margin-bottom: 16px; }
.sat-label { font-size: 12px; font-weight: 600; margin-bottom: 6px; }
.sat-stars { display: flex; gap: 6px; }
.sat-star { font-size: 22px; cursor: pointer; opacity: 0.3; transition: all 0.15s; }
.sat-star.active { opacity: 1; }
.sat-textarea { width: 100%; background: var(--bg-card); border: 1px solid rgba(255,255,255,0.08); border-radius: 12px; padding: 14px; font-size: 14px; color: var(--text); resize: vertical; min-height: 80px; outline: none; font-family: inherit; }
.sat-textarea:focus { border-color: var(--accent); }
.sat-submit { width: 100%; padding: 16px; background: var(--accent); color: var(--bg); border: none; border-radius: var(--radius); font-size: 16px; font-weight: 700; cursor: pointer; margin-top: 16px; transition: all 0.2s; }
.sat-submit:active { transform: scale(0.97); }
.sat-submit:disabled { opacity: 0.4; }
.sat-success { text-align: center; padding: 40px 20px; }
.sat-success-icon { font-size: 48px; margin-bottom: 12px; }
.sat-success-title { font-size: 18px; font-weight: 700; margin-bottom: 6px; }
.sat-success-sub { font-size: 13px; color: var(--text-dim); }
/* ═══ GAME BOARD (Memory) ═══ */
.game-board { padding: 20px; }
.game-back-btn { display: flex; align-items: center; gap: 6px; color: var(--accent); font-size: 13px; font-weight: 600; cursor: pointer; margin-bottom: 16px; }
.memory-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; }
.memory-card { aspect-ratio: 1; background: var(--bg-card); border: 2px solid rgba(255,255,255,0.08); border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 24px; cursor: pointer; transition: all 0.3s; }
.memory-card.flipped { background: var(--accent-dim); border-color: var(--accent); }
.memory-card.matched { background: rgba(0,212,170,0.2); border-color: var(--success); opacity: 0.7; }
.game-score { text-align: center; font-size: 13px; color: var(--text-dim); margin-bottom: 12px; }
/* Toast */
.toast { position: fixed; top: 70px; left: 50%; transform: translateX(-50%); z-index: 200; background: var(--bg-card); border: 1px solid rgba(255,255,255,0.1); border-radius: 12px; padding: 10px 18px; font-size: 13px; font-weight: 500; box-shadow: 0 8px 24px rgba(0,0,0,0.4); animation: toastIn 0.3s ease; white-space: nowrap; max-width: 90vw; overflow: hidden; text-overflow: ellipsis; }
@keyframes toastIn { from { opacity: 0; transform: translate(-50%, -8px); } to { opacity: 1; transform: translate(-50%, 0); } }
.toast.success { border-color: var(--success); color: var(--success); }
.toast.error { border-color: var(--danger); color: var(--danger); }
.toast.info { border-color: var(--accent); color: var(--accent); }
</style>
</head>
<body>
<!-- ═══ PAIRING ═══ -->
<div id="pairingScreen">
<div class="pair-logo">+</div>
<div class="pair-title">CHU-IPTV</div>
<div class="pair-sub">Entrez le code affiché sur la TV</div>
<div class="code-input" id="codeInput">
<input type="text" maxlength="1" data-idx="0" inputmode="text" autocomplete="off">
<input type="text" maxlength="1" data-idx="1" inputmode="text" autocomplete="off">
<input type="text" maxlength="1" data-idx="2" inputmode="text" autocomplete="off">
<input type="text" maxlength="1" data-idx="3" inputmode="text" autocomplete="off">
<input type="text" maxlength="1" data-idx="4" inputmode="text" autocomplete="off">
<input type="text" maxlength="1" data-idx="5" inputmode="text" autocomplete="off">
<input type="text" maxlength="1" data-idx="6" inputmode="text" autocomplete="off">
<input type="text" maxlength="1" data-idx="7" inputmode="text" autocomplete="off">
</div>
<button class="pair-btn" id="pairBtn" disabled>Se connecter</button>
<div class="pair-error" id="pairError"></div>
<div class="pair-hint">Scannez le QR code sur la TV pour vous connecter automatiquement</div>
</div>
<!-- ═══ APP ═══ -->
<div id="appScreen">
<header class="app-header">
<div class="hdr-left">
<div class="hdr-logo">CHU-IPTV</div>
<div class="hdr-room" id="appRoom">---</div>
</div>
<div style="display:flex;align-items:center;gap:8px;">
<select id="langSelect" onchange="setLang(this.value)" style="background:var(--bg-card);color:var(--text);border:1px solid rgba(255,255,255,0.1);border-radius:8px;padding:4px 8px;font-size:11px;cursor:pointer;">
<option value="fr">🇫🇷 FR</option>
<option value="en">🇬🇧 EN</option>
<option value="ar">🇸🇦 AR</option>
<option value="tr">🇹🇷 TR</option>
</select>
<div class="hdr-status" id="connStatus">
<div class="hdr-dot"></div>
<span data-i18n="connected">Connecté</span>
</div>
</div>
</header>
<!-- TV -->
<div class="page active" id="pageTv">
<div class="now-playing" id="nowPlaying">
<div class="np-num" id="npNum">-</div>
<div class="np-info">
<div class="np-name" id="npName">Aucune chaîne</div>
<div class="np-cat" id="npCat">---</div>
</div>
<div class="np-live">EN DIRECT</div>
</div>
<div class="section-title">Chaînes</div>
<div class="channel-grid" id="channelGrid"></div>
<div class="section-title">Contrôles</div>
<div class="controls-row">
<div class="ctrl-btn" onclick="volumeDown()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 5L6 9H2v6h4l5 4V5z"/></svg>
<span>Vol -</span>
</div>
<div class="ctrl-btn" onclick="channelUp()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 15l-6-6-6 6"/></svg>
<span>CH +</span>
</div>
<div class="ctrl-btn" onclick="toggleMute()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 5L6 9H2v6h4l5 4V5z"/><line x1="23" y1="9" x2="17" y2="15"/><line x1="17" y1="9" x2="23" y2="15"/></svg>
<span>Muet</span>
</div>
<div class="ctrl-btn" onclick="channelDown()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9l6 6 6-6"/></svg>
<span>CH -</span>
</div>
<div class="ctrl-btn" onclick="volumeUp()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 5L6 9H2v6h4l5 4V5z"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14"/></svg>
<span>Vol +</span>
</div>
<div class="ctrl-btn danger" onclick="togglePower()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18.36 6.64a9 9 0 1 1-12.73 0"/><line x1="12" y1="2" x2="12" y2="12"/></svg>
<span>Power</span>
</div>
</div>
</div>
<!-- Demandes -->
<div class="page" id="pageRequests">
<div id="sosBtn" onclick="sendSOS()" style="background:linear-gradient(135deg,#ff2244,#cc0022);border:2px solid #ff4466;border-radius:16px;padding:20px;text-align:center;margin-bottom:16px;cursor:pointer;transition:all 0.15s;position:relative;overflow:hidden;">
<div style="font-size:32px;margin-bottom:4px;">🚨</div>
<div style="font-size:18px;font-weight:800;color:#fff;">APPEL D'URGENCE</div>
<div style="font-size:11px;color:rgba(255,255,255,0.7);margin-top:4px;">Maintenir 2 secondes pour appeler</div>
</div>
<div class="section-title">Que puis-je faire pour vous ?</div>
<div class="req-grid" id="requestGrid"></div>
<div class="history-section">
<div class="section-title">Historique</div>
<div id="historyList"></div>
</div>
</div>
<!-- Chat -->
<div class="page" id="pageChat">
<div class="chat-wrap">
<div class="chat-msgs" id="chatMessages"></div>
<div class="chat-bar">
<input type="text" id="chatInput" placeholder="Votre message...">
<button onclick="sendChat()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
</button>
</div>
</div>
</div>
<!-- Éducation / VOD -->
<div class="page" id="pageEdu">
<div class="section-title">Vidéos éducatives</div>
<div id="eduCategories" style="display:flex;gap:8px;margin-bottom:14px;overflow-x:auto;padding-bottom:6px;"></div>
<div id="eduList"></div>
</div>
<!-- Radio -->
<div class="page" id="pageRadio">
<div class="section-title">Radio & Musique</div>
<div class="genre-pills" id="genrePills"></div>
<div id="radioNowPlaying" style="display:none;background:var(--accent-dim);border:1px solid var(--accent);border-radius:14px;padding:14px 16px;margin-bottom:14px;align-items:center;gap:12px;">
<div style="font-size:24px;">🎵</div>
<div style="flex:1;"><div style="font-size:12px;font-weight:600;" id="radioNpName">---</div><div style="font-size:10px;color:var(--text-dim);">En écoute sur la TV</div></div>
<div class="radio-play radio-stop" onclick="stopRadio()" style="cursor:pointer;"></div>
</div>
<div id="radioList"></div>
</div>
<!-- Repas -->
<div class="page" id="pageMeals">
<div class="section-title">Menu du jour</div>
<div id="mealsList"></div>
</div>
<!-- Jeux -->
<div class="page" id="pageGames">
<div class="section-title">Mini-jeux</div>
<div class="game-grid" id="gameGrid"></div>
</div>
<!-- Game Board (in-page game) -->
<div class="page" id="pageGameBoard">
<div class="game-board">
<div class="game-back-btn" onclick="switchTab('games')">← Retour aux jeux</div>
<div id="gameBoardContent"></div>
</div>
</div>
<!-- Infos -->
<div class="page" id="pageInfo">
<div class="section-title">Informations pratiques</div>
<div id="infoContent"></div>
</div>
<!-- Satisfaction -->
<div class="page" id="pageSatisfaction">
<div class="section-title">Votre avis compte</div>
<div id="satisfactionContent"></div>
</div>
<!-- Chambre -->
<div class="page" id="pageRoom">
<div class="section-title">Contrôle de la chambre</div>
<div id="domoticList"></div>
</div>
<!-- Nav -->
<nav class="tab-nav">
<div class="tab-item active" data-tab="tv" onclick="switchTab('tv')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="7" width="20" height="15" rx="2"/><polyline points="17 2 12 7 7 2"/></svg>
<span>TV</span>
</div>
<div class="tab-item" data-tab="requests" onclick="switchTab('requests')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>
<span>Demandes</span>
<div class="tab-badge" id="reqBadge">0</div>
</div>
<div class="tab-item" data-tab="chat" onclick="switchTab('chat')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
<span>Chat</span>
<div class="tab-badge" id="chatBadge">0</div>
</div>
<div class="tab-item" data-tab="room" onclick="switchTab('room')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
<span>Chambre</span>
</div>
<div class="tab-item" data-tab="more" onclick="openMoreMenu()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/></svg>
<span>Plus</span>
</div>
</nav>
</div>
<!-- ═══ MENU PLUS OVERLAY ═══ -->
<div class="more-overlay" id="moreOverlay">
<div class="more-header">
<div class="more-title">Services</div>
<div class="more-close" onclick="closeMoreMenu()"></div>
</div>
<div class="more-grid">
<div class="more-item" onclick="closeMoreMenu();switchTab('radio')">
<div class="mi-icon">🎵</div>
<div class="mi-label">Radio</div>
<div class="mi-desc">Musique & détente</div>
</div>
<div class="more-item" onclick="closeMoreMenu();switchTab('meals')">
<div class="mi-icon">🍽️</div>
<div class="mi-label">Repas</div>
<div class="mi-desc">Menu du jour</div>
</div>
<div class="more-item" onclick="closeMoreMenu();switchTab('games')">
<div class="mi-icon">🎮</div>
<div class="mi-label">Jeux</div>
<div class="mi-desc">Divertissement</div>
</div>
<div class="more-item" onclick="closeMoreMenu();switchTab('info')">
<div class="mi-icon"></div>
<div class="mi-label">Infos</div>
<div class="mi-desc">Infos pratiques</div>
</div>
<div class="more-item" onclick="closeMoreMenu();switchTab('satisfaction')">
<div class="mi-icon"></div>
<div class="mi-label">Avis</div>
<div class="mi-desc">Satisfaction</div>
</div>
<div class="more-item" onclick="closeMoreMenu();switchTab('edu')">
<div class="mi-icon">🎓</div>
<div class="mi-label">Éducation</div>
<div class="mi-desc">Vidéos santé</div>
</div>
</div>
</div>
<script>
// ═══ MULTI-LANGUE ═══
const TRANSLATIONS = {
fr: {
title: 'CHU-IPTV — Patient', pairing: 'Connexion', pairSub: 'Scannez le QR code sur la TV ou entrez le code',
pairBtn: 'Se connecter', tv: 'TV', requests: 'Demandes', chat: 'Chat', education: 'Éducation', room: 'Chambre', more: 'Plus',
nowPlaying: 'En cours', channels: 'Chaînes', chUp: 'CH+', chDown: 'CH-',
requestTitle: 'Demandes', sos: 'SOS URGENCE', water: 'Eau', pain: 'Douleur', toilet: 'Toilette', meal: 'Repas', blanket: 'Couverture', help: 'Aide',
chatTitle: 'Chat soignant', chatPlaceholder: 'Votre message...', send: 'Envoyer',
eduTitle: 'Vidéos éducatives', all: 'Tout',
roomTitle: 'Contrôle de la chambre', mainLight: 'Lumière principale', nightLight: 'Veilleuse', blinds: 'Stores',
on: 'Allumée', off: 'Éteinte', connected: 'Connecté', disconnected: 'Déconnecté',
radio: 'Radio', meals: 'Repas', games: 'Jeux', info: 'Infos', satisfaction: 'Avis',
langLabel: '🇫🇷 Français'
},
en: {
title: 'CHU-IPTV — Patient', pairing: 'Connection', pairSub: 'Scan the QR code on the TV or enter the code',
pairBtn: 'Connect', tv: 'TV', requests: 'Requests', chat: 'Chat', education: 'Education', room: 'Room', more: 'More',
nowPlaying: 'Now playing', channels: 'Channels', chUp: 'CH+', chDown: 'CH-',
requestTitle: 'Requests', sos: 'SOS EMERGENCY', water: 'Water', pain: 'Pain', toilet: 'Toilet', meal: 'Meal', blanket: 'Blanket', help: 'Help',
chatTitle: 'Staff chat', chatPlaceholder: 'Your message...', send: 'Send',
eduTitle: 'Educational videos', all: 'All',
roomTitle: 'Room control', mainLight: 'Main light', nightLight: 'Night light', blinds: 'Blinds',
on: 'On', off: 'Off', connected: 'Connected', disconnected: 'Disconnected',
radio: 'Radio', meals: 'Meals', games: 'Games', info: 'Info', satisfaction: 'Feedback',
langLabel: '🇬🇧 English'
},
ar: {
title: 'CHU-IPTV — المريض', pairing: 'الاتصال', pairSub: 'امسح رمز QR على التلفزيون أو أدخل الرمز',
pairBtn: 'اتصال', tv: 'تلفزيون', requests: 'طلبات', chat: 'محادثة', education: 'تعليم', room: 'غرفة', more: 'المزيد',
nowPlaying: 'يعرض الآن', channels: 'القنوات', chUp: '+', chDown: '-',
requestTitle: 'الطلبات', sos: 'SOS طوارئ', water: 'ماء', pain: 'ألم', toilet: 'مرحاض', meal: 'وجبة', blanket: 'بطانية', help: 'مساعدة',
chatTitle: 'محادثة الممرض', chatPlaceholder: 'رسالتك...', send: 'إرسال',
eduTitle: 'فيديوهات تعليمية', all: 'الكل',
roomTitle: 'تحكم في الغرفة', mainLight: 'الإضاءة الرئيسية', nightLight: 'ضوء الليل', blinds: 'الستائر',
on: 'مشغل', off: 'مطفأ', connected: 'متصل', disconnected: 'غير متصل',
radio: 'راديو', meals: 'وجبات', games: 'ألعاب', info: 'معلومات', satisfaction: 'رأي',
langLabel: '🇸🇦 العربية'
},
tr: {
title: 'CHU-IPTV — Hasta', pairing: 'Bağlantı', pairSub: 'TV\'deki QR kodu tarayın veya kodu girin',
pairBtn: 'Bağlan', tv: 'TV', requests: 'Talepler', chat: 'Sohbet', education: 'Eğitim', room: 'Oda', more: 'Daha',
nowPlaying: 'Şu an', channels: 'Kanallar', chUp: 'K+', chDown: 'K-',
requestTitle: 'Talepler', sos: 'SOS ACİL', water: 'Su', pain: 'Ağrı', toilet: 'Tuvalet', meal: 'Yemek', blanket: 'Battaniye', help: 'Yardım',
chatTitle: 'Hemşire sohbeti', chatPlaceholder: 'Mesajınız...', send: 'Gönder',
eduTitle: 'Eğitim videoları', all: 'Tümü',
roomTitle: 'Oda kontrolü', mainLight: 'Ana ışık', nightLight: 'Gece lambası', blinds: 'Panjurlar',
on: 'Açık', off: 'Kapalı', connected: 'Bağlı', disconnected: 'Bağlı değil',
radio: 'Radyo', meals: 'Yemekler', games: 'Oyunlar', info: 'Bilgi', satisfaction: 'Görüş',
langLabel: '🇹🇷 Türkçe'
}
};
let currentLang = localStorage.getItem('chu-iptv-lang') || 'fr';
function t(key) { return TRANSLATIONS[currentLang]?.[key] || TRANSLATIONS.fr[key] || key; }
function setLang(lang) {
currentLang = lang;
localStorage.setItem('chu-iptv-lang', lang);
document.documentElement.lang = lang;
if (lang === 'ar') document.documentElement.dir = 'rtl'; else document.documentElement.dir = 'ltr';
applyTranslations();
}
function applyTranslations() {
document.querySelectorAll('[data-i18n]').forEach(el => { el.textContent = t(el.dataset.i18n); });
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => { el.placeholder = t(el.dataset.i18nPlaceholder); });
document.querySelectorAll('.tab-item').forEach(tab => {
const key = tab.dataset.tab;
const span = tab.querySelector('span');
if (span && key) span.textContent = t(key === 'requests' ? 'requests' : key === 'edu' ? 'education' : key);
});
}
// ═══ CONFIG ═══
const API_URL = 'https://chu-iptv-api.cosmolan.fr';
let token = localStorage.getItem('chu-iptv-patient-token');
let roomData = null;
let socket = null;
let channels = [];
let currentChIdx = 0;
let requests = [];
let domoticState = { mainLight: false, nightLight: false, blinds: 100 };
let currentRadioStation = null;
// ═══ INIT ═══
async function init() {
const params = new URLSearchParams(window.location.search);
const pairCode = params.get('pair');
if (pairCode) {
await doPairing(pairCode);
} else if (token) {
try {
const res = await fetch(`${API_URL}/api/auth/verify`, { headers: { Authorization: `Bearer ${token}` } });
if (res.ok) {
const data = await res.json();
if (data.valid && data.type === 'patient') { roomData = data.room; showApp(); return; }
}
} catch {}
localStorage.removeItem('chu-iptv-patient-token');
token = null;
}
}
// ═══ PAIRING ═══
const codeInputs = document.querySelectorAll('#codeInput input');
codeInputs.forEach((input, idx) => {
input.addEventListener('input', (e) => {
e.target.value = e.target.value.toUpperCase();
if (e.target.value && idx < 7) codeInputs[idx + 1].focus();
document.getElementById('pairBtn').disabled = Array.from(codeInputs).some(i => !i.value);
});
input.addEventListener('keydown', (e) => {
if (e.key === 'Backspace' && !e.target.value && idx > 0) codeInputs[idx - 1].focus();
});
});
document.getElementById('pairBtn').addEventListener('click', () => {
const code = Array.from(codeInputs).map(i => i.value).join('');
doPairing(code);
});
async function doPairing(code) {
document.getElementById('pairError').textContent = '';
try {
const res = await fetch(`${API_URL}/api/auth/qr/pair`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pairingCode: code.toUpperCase() }),
});
if (!res.ok) {
const err = await res.json();
document.getElementById('pairError').textContent = err.error || 'Code invalide ou expiré';
return;
}
const data = await res.json();
token = data.token;
roomData = data.room;
localStorage.setItem('chu-iptv-patient-token', token);
window.history.replaceState({}, '', window.location.pathname);
showApp();
} catch (e) {
document.getElementById('pairError').textContent = 'Erreur de connexion';
}
}
// ═══ APP ═══
function showApp() {
document.getElementById('pairingScreen').classList.add('hidden');
document.getElementById('appScreen').classList.add('active');
document.getElementById('appRoom').textContent = `Ch. ${roomData.number}`;
document.getElementById('langSelect').value = currentLang;
applyTranslations();
loadChannels();
loadRequests();
loadDomotics();
connectSocket();
}
// ═══ CHANNELS ═══
async function loadChannels() {
try {
const res = await fetch(`${API_URL}/api/iptv/channels`);
if (res.ok) channels = await res.json();
} catch {}
renderChannels();
}
function renderChannels() {
const grid = document.getElementById('channelGrid');
grid.innerHTML = channels.map((ch, idx) => `
<div class="ch-btn${idx === currentChIdx ? ' active' : ''}" onclick="tuneChannel(${idx})">
<div class="ch-n">${ch.number}</div>
<div class="ch-l">${ch.name}</div>
</div>
`).join('');
}
function tuneChannel(idx) {
currentChIdx = idx;
const ch = channels[idx];
socket.emit('tv:tune', { roomId: roomData.id, channelNumber: ch.number, streamUrl: ch.streamUrl, name: ch.name, category: ch.category });
document.getElementById('npNum').textContent = ch.number;
document.getElementById('npName').textContent = ch.name;
document.getElementById('npCat').textContent = ch.category || 'TV';
renderChannels();
showToast('success', `${ch.name}`);
// Stop radio if playing
if (currentRadioStation) stopRadio();
fetch(`${API_URL}/api/iptv/tune`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, body: JSON.stringify({ roomId: roomData.id, channelId: ch.id }) }).catch(() => {});
}
function channelUp() { tuneChannel((currentChIdx + 1) % channels.length); }
function channelDown() { tuneChannel((currentChIdx - 1 + channels.length) % channels.length); }
function volumeUp() { socket?.emit('tv:volume', { roomId: roomData.id, action: 'up' }); showToast('info', 'Volume +'); }
function volumeDown() { socket?.emit('tv:volume', { roomId: roomData.id, action: 'down' }); showToast('info', 'Volume -'); }
function toggleMute() { socket?.emit('tv:volume', { roomId: roomData.id, action: 'mute' }); showToast('info', 'Muet'); }
function togglePower() { socket?.emit('tv:power', { roomId: roomData.id, state: 'off' }); showToast('info', 'TV éteinte'); }
// ═══ REQUESTS ═══
const REQ_TYPES = [
{ type: 'WATER', icon: '💧', label: 'Eau', desc: 'Verre d\'eau' },
{ type: 'PAIN', icon: '🩹', label: 'Douleur', desc: 'Signaler' },
{ type: 'TOILET', icon: '🚽', label: 'Toilettes', desc: 'Aide WC' },
{ type: 'MEAL', icon: '🍽️', label: 'Repas', desc: 'Question' },
{ type: 'BLANKET', icon: '🛏️', label: 'Couverture', desc: 'Supplémentaire' },
{ type: 'HELP', icon: '🆘', label: 'Aide', desc: 'Assistance' },
];
async function loadRequests() {
try {
const res = await fetch(`${API_URL}/api/requests/room/${roomData.id}`, { headers: { Authorization: `Bearer ${token}` } });
if (res.ok) requests = await res.json();
} catch {}
renderRequests();
}
function renderRequests() {
const grid = document.getElementById('requestGrid');
grid.innerHTML = REQ_TYPES.map(rt => {
const pending = requests.find(r => r.type === rt.type && r.status === 'PENDING');
return `<div class="req-card${pending ? ' sent' : ''}" onclick="sendRequest(this,'${rt.type}')">
<span class="req-icon">${rt.icon}</span>
<div class="req-label">${rt.label}</div>
<div class="req-desc">${rt.desc}</div>
</div>`;
}).join('');
const hist = document.getElementById('historyList');
hist.innerHTML = requests.slice(0, 5).map(r => {
const rt = REQ_TYPES.find(t => t.type === r.type) || { icon: '❓', label: r.type };
const sc = r.status === 'PENDING' ? 'pending' : r.status === 'ACKNOWLEDGED' ? 'ack' : 'done';
const st = r.status === 'PENDING' ? 'En attente' : r.status === 'ACKNOWLEDGED' ? 'Pris en charge' : 'Terminé';
const time = new Date(r.createdAt).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' });
return `<div class="hist-item"><div class="hi-icon">${rt.icon}</div><div class="hi-text"><div class="hi-label">${rt.label}</div><div class="hi-time">${time}</div></div><div class="hi-status ${sc}">${st}</div></div>`;
}).join('') || '<p style="color:var(--text-dim);font-size:12px;padding:8px;">Aucune demande</p>';
}
// ═══ SOS ═══
let sosTimer = null;
const sosBtn = document.getElementById('sosBtn');
sosBtn.addEventListener('touchstart', startSOS);
sosBtn.addEventListener('mousedown', startSOS);
sosBtn.addEventListener('touchend', cancelSOS);
sosBtn.addEventListener('mouseup', cancelSOS);
sosBtn.addEventListener('mouseleave', cancelSOS);
function startSOS(e) {
e.preventDefault();
sosBtn.style.transform = 'scale(0.95)';
sosBtn.style.boxShadow = '0 0 30px rgba(255,34,68,0.6)';
sosTimer = setTimeout(() => { sendSOS(); }, 2000);
}
function cancelSOS() {
sosBtn.style.transform = '';
sosBtn.style.boxShadow = '';
if (sosTimer) { clearTimeout(sosTimer); sosTimer = null; }
}
async function sendSOS() {
if (!token || !roomData) return;
sosBtn.style.transform = '';
sosBtn.style.boxShadow = '';
try {
const res = await fetch(`${API_URL}/api/requests/sos`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
});
if (res.ok) {
showToast('success', 'Appel d\'urgence envoyé !');
sosBtn.innerHTML = '<div style="font-size:32px;">✅</div><div style="font-size:18px;font-weight:800;color:#fff;">Appel envoyé</div><div style="font-size:11px;color:rgba(255,255,255,0.7);margin-top:4px;">L\'équipe soignante arrive</div>';
setTimeout(() => {
sosBtn.innerHTML = '<div style="font-size:32px;">🚨</div><div style="font-size:18px;font-weight:800;color:#fff;">APPEL D\'URGENCE</div><div style="font-size:11px;color:rgba(255,255,255,0.7);margin-top:4px;">Maintenir 2 secondes pour appeler</div>';
}, 30000);
} else {
showToast('error', 'Erreur lors de l\'appel');
}
} catch {
showToast('error', 'Connexion impossible');
}
}
function sendRequest(el, type) {
if (!socket || !roomData) return;
socket.emit('request:create', { roomId: roomData.id, type, priority: (type === 'PAIN' || type === 'HELP') ? 'HIGH' : 'NORMAL' });
el.classList.add('sent');
showToast('success', 'Demande envoyée');
setTimeout(() => { el.classList.remove('sent'); }, 30000);
requests.unshift({ type, status: 'PENDING', createdAt: new Date().toISOString() });
renderRequests();
}
// ═══ CHAT ═══
let chatMsgs = [];
async function loadChat() {
try {
const res = await fetch(`${API_URL}/api/chat/room/${roomData.id}`, { headers: { Authorization: `Bearer ${token}` } });
if (res.ok) { chatMsgs = await res.json(); renderChat(); }
} catch {}
}
function renderChat() {
const c = document.getElementById('chatMessages');
c.innerHTML = chatMsgs.map(m => `
<div class="msg ${m.sender === 'PATIENT' ? 'patient' : 'staff'}">
${m.sender === 'STAFF' ? `<div class="msg-sender">${m.staffName || 'Soignant'}</div>` : ''}
${m.content}
<div class="msg-time">${new Date(m.createdAt).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}</div>
</div>
`).join('');
c.scrollTop = c.scrollHeight;
}
function sendChat() {
const input = document.getElementById('chatInput');
const content = input.value.trim();
if (!content || !socket) return;
socket.emit('chat:send', { roomId: roomData.id, content, sender: 'PATIENT' });
chatMsgs.push({ content, sender: 'PATIENT', createdAt: new Date().toISOString() });
renderChat();
input.value = '';
fetch(`${API_URL}/api/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, body: JSON.stringify({ roomId: roomData.id, content, sender: 'PATIENT' }) }).catch(() => {});
}
document.getElementById('chatInput').addEventListener('keydown', (e) => { if (e.key === 'Enter') sendChat(); });
// ═══ ÉDUCATION / VOD ═══
let vodCatalog = [];
let vodCategory = 'all';
async function loadVOD() {
try {
const res = await fetch(`${API_URL}/api/vod/catalog`, { headers: { Authorization: `Bearer ${token}` } });
if (res.ok) vodCatalog = await res.json();
} catch {}
renderVOD();
}
function renderVOD() {
const cats = ['all', ...new Set(vodCatalog.map(v => v.category))];
const catLabels = { all: 'Tout', education: '🎓 Éducation', wellbeing: '🧘 Bien-être', entertainment: '🎬 Divertissement', info: '📰 Info' };
document.getElementById('eduCategories').innerHTML = cats.map(c =>
`<div onclick="filterVOD('${c}')" style="padding:6px 14px;border-radius:20px;font-size:12px;font-weight:600;cursor:pointer;white-space:nowrap;${vodCategory===c?'background:var(--accent);color:var(--bg);':'background:var(--bg-card);color:var(--text-dim);border:1px solid rgba(255,255,255,0.08);'}">${catLabels[c]||c}</div>`
).join('');
const filtered = vodCategory === 'all' ? vodCatalog : vodCatalog.filter(v => v.category === vodCategory);
document.getElementById('eduList').innerHTML = filtered.map(v => `
<div style="background:var(--bg-card);border:1px solid rgba(255,255,255,0.06);border-radius:14px;padding:14px;margin-bottom:10px;cursor:pointer;" onclick="playVOD('${v.id}')">
<div style="display:flex;align-items:center;gap:12px;">
<div style="width:48px;height:48px;background:var(--accent-dim);border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:22px;">🎬</div>
<div style="flex:1;">
<div style="font-size:14px;font-weight:600;">${v.title}</div>
<div style="font-size:11px;color:var(--text-dim);margin-top:2px;">${v.description || ''}</div>
<div style="font-size:10px;color:var(--accent);margin-top:4px;">${v.duration ? Math.round(v.duration/60)+'min' : ''}${catLabels[v.category]||v.category}</div>
</div>
<div style="color:var(--accent);font-size:20px;">▶</div>
</div>
</div>
`).join('') || '<p style="color:var(--text-dim);font-size:13px;text-align:center;padding:20px;">Aucune vidéo disponible</p>';
}
function filterVOD(cat) { vodCategory = cat; renderVOD(); }
async function playVOD(id) {
try {
await fetch(`${API_URL}/api/vod/play`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, body: JSON.stringify({ videoId: id, roomId: roomData.id }) });
showToast('success', 'Lecture lancée sur la TV');
} catch {
showToast('error', 'Erreur de lecture');
}
}
// ═══ RADIO ═══
let radioStations = [];
let radioGenre = 'all';
async function loadRadio() {
try {
const res = await fetch(`${API_URL}/api/radio/stations`);
if (res.ok) radioStations = await res.json();
} catch {}
try {
const res2 = await fetch(`${API_URL}/api/radio/genres`);
if (res2.ok) {
const genres = await res2.json();
renderGenrePills(genres);
}
} catch {}
renderRadio();
}
function renderGenrePills(genres) {
const pills = document.getElementById('genrePills');
pills.innerHTML = `<div class="genre-pill${radioGenre==='all'?' active':''}" onclick="filterRadio('all')">Tout</div>` +
genres.map(g => `<div class="genre-pill${radioGenre===g.id?' active':''}" onclick="filterRadio('${g.id}')">${g.icon} ${g.name}</div>`).join('');
}
function filterRadio(genre) {
radioGenre = genre;
document.querySelectorAll('.genre-pill').forEach(p => p.classList.remove('active'));
event.target.classList.add('active');
renderRadio();
}
function renderRadio() {
const filtered = radioGenre === 'all' ? radioStations : radioStations.filter(s => s.genre === radioGenre);
const list = document.getElementById('radioList');
list.innerHTML = filtered.map(s => `
<div class="radio-card${currentRadioStation && currentRadioStation.name === s.name ? ' playing' : ''}" onclick="playRadio('${s.name}','${s.streamUrl}','${s.genre}','${s.logoUrl || '🎵'}')">
<div class="radio-icon">${s.logoUrl || '🎵'}</div>
<div class="radio-info">
<div class="radio-name">${s.name}</div>
<div class="radio-genre">${s.genre}</div>
</div>
<div class="radio-play${currentRadioStation && currentRadioStation.name === s.name ? ' radio-stop' : ''}">
${currentRadioStation && currentRadioStation.name === s.name ? '⏹' : '▶'}
</div>
</div>
`).join('') || '<p style="color:var(--text-dim);font-size:13px;text-align:center;padding:20px;">Aucune station disponible</p>';
// Update now playing
const np = document.getElementById('radioNowPlaying');
if (currentRadioStation) {
np.style.display = 'flex';
document.getElementById('radioNpName').textContent = currentRadioStation.name;
} else {
np.style.display = 'none';
}
}
async function playRadio(name, url, genre, icon) {
if (currentRadioStation && currentRadioStation.name === name) {
stopRadio();
return;
}
try {
await fetch(`${API_URL}/api/radio/play`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify({ roomId: roomData.id, stationUrl: url, stationName: name })
});
currentRadioStation = { name, url, genre, icon };
showToast('success', `${name} sur la TV`);
renderRadio();
} catch {
showToast('error', 'Erreur radio');
}
}
async function stopRadio() {
try {
await fetch(`${API_URL}/api/radio/stop`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify({ roomId: roomData.id })
});
currentRadioStation = null;
showToast('info', 'Radio arrêtée');
renderRadio();
} catch {
showToast('error', 'Erreur');
}
}
// ═══ REPAS ═══
let mealsData = null;
let mealChoices = {};
async function loadMeals() {
try {
const res = await fetch(`${API_URL}/api/meals/today`);
if (res.ok) mealsData = await res.json();
} catch {}
try {
const res2 = await fetch(`${API_URL}/api/meals/choices/${roomData.id}`, { headers: { Authorization: `Bearer ${token}` } });
if (res2.ok) {
const choices = await res2.json();
choices.forEach(c => { mealChoices[c.mealType] = c.mealId; });
}
} catch {}
renderMeals();
}
function renderMeals() {
if (!mealsData) {
document.getElementById('mealsList').innerHTML = '<p style="color:var(--text-dim);font-size:13px;text-align:center;padding:20px;">Chargement du menu...</p>';
return;
}
const types = [
{ key: 'breakfast', label: '☀️ Petit-déjeuner', type: 'BREAKFAST' },
{ key: 'lunch', label: '🍽️ Déjeuner', type: 'LUNCH' },
{ key: 'dinner', label: '🌙 Dîner', type: 'DINNER' },
{ key: 'snack', label: '🍎 Collation', type: 'SNACK' },
];
document.getElementById('mealsList').innerHTML = types.map(t => {
const items = mealsData.meals[t.key] || [];
if (items.length === 0) return '';
return `<div class="meal-section">
<div class="meal-type-title">${t.label}</div>
${items.map(m => `
<div class="meal-card${mealChoices[t.type] === m.id ? ' selected' : ''}" onclick="chooseMeal('${m.id}','${t.type}')">
<div class="meal-name">${m.name}</div>
<div class="meal-desc">${m.description || ''}</div>
<div class="meal-meta">
${m.vegetarian ? '<span class="meal-tag veg">🌱 Végétarien</span>' : ''}
${m.allergens ? `<span class="meal-tag allergen">⚠️ ${m.allergens}</span>` : ''}
</div>
</div>
`).join('')}
</div>`;
}).join('') || '<p style="color:var(--text-dim);font-size:13px;text-align:center;padding:20px;">Aucun menu disponible</p>';
}
async function chooseMeal(mealId, mealType) {
try {
const today = new Date().toISOString().split('T')[0];
await fetch(`${API_URL}/api/meals/choose`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify({ roomId: roomData.id, mealId, mealType, date: today })
});
mealChoices[mealType] = mealId;
renderMeals();
showToast('success', 'Choix enregistré');
} catch {
showToast('error', 'Erreur');
}
}
// ═══ JEUX ═══
let gamesList = [];
async function loadGames() {
try {
const res = await fetch(`${API_URL}/api/games/list`);
if (res.ok) gamesList = await res.json();
} catch {}
renderGames();
}
function renderGames() {
const grid = document.getElementById('gameGrid');
grid.innerHTML = gamesList.map(g => `
<div class="game-card" onclick="openGame('${g.id}')">
<div class="game-icon">${g.icon}</div>
<div class="game-name">${g.name}</div>
<div class="game-desc">${g.description}</div>
<div class="game-diff">${g.estimatedTime || ''}</div>
</div>
`).join('') || '<p style="color:var(--text-dim);font-size:13px;text-align:center;padding:40px;">Chargement...</p>';
}
async function openGame(gameId) {
if (gameId === 'memory') {
await startMemory();
} else if (gameId === 'quiz') {
await startQuiz();
} else {
showToast('info', 'Jeu bientôt disponible');
}
}
// Memory Game
let memoryCards = [];
let memoryFlipped = [];
let memoryMatched = 0;
let memoryMoves = 0;
let memoryLocked = false;
async function startMemory() {
try {
const res = await fetch(`${API_URL}/api/games/memory/easy`);
if (res.ok) {
const data = await res.json();
memoryCards = data.cards;
memoryFlipped = [];
memoryMatched = 0;
memoryMoves = 0;
memoryLocked = false;
switchTab('gameboard');
renderMemory();
}
} catch {
showToast('error', 'Erreur chargement jeu');
}
}
function renderMemory() {
const content = document.getElementById('gameBoardContent');
content.innerHTML = `
<div style="text-align:center;margin-bottom:12px;font-size:16px;font-weight:700;">🧠 Memory</div>
<div class="game-score">Coups : ${memoryMoves} | Paires : ${memoryMatched}/${memoryCards.length/2}</div>
<div class="memory-grid">
${memoryCards.map((card, idx) => `
<div class="memory-card${card.flipped || card.matched ? ' flipped' : ''}${card.matched ? ' matched' : ''}" onclick="flipMemoryCard(${idx})">
${card.flipped || card.matched ? card.emoji : '?'}
</div>
`).join('')}
</div>
`;
if (memoryMatched === memoryCards.length / 2) {
content.innerHTML += `<div style="text-align:center;margin-top:20px;padding:20px;background:var(--accent-dim);border-radius:14px;"><div style="font-size:32px;">🎉</div><div style="font-size:16px;font-weight:700;margin-top:8px;">Bravo !</div><div style="font-size:12px;color:var(--text-dim);margin-top:4px;">Terminé en ${memoryMoves} coups</div><div style="margin-top:12px;color:var(--accent);font-weight:600;cursor:pointer;" onclick="startMemory()">Rejouer</div></div>`;
}
}
function flipMemoryCard(idx) {
if (memoryLocked || memoryCards[idx].flipped || memoryCards[idx].matched) return;
memoryCards[idx].flipped = true;
memoryFlipped.push(idx);
renderMemory();
if (memoryFlipped.length === 2) {
memoryMoves++;
memoryLocked = true;
const [a, b] = memoryFlipped;
if (memoryCards[a].emoji === memoryCards[b].emoji) {
memoryCards[a].matched = true;
memoryCards[b].matched = true;
memoryMatched++;
memoryFlipped = [];
memoryLocked = false;
renderMemory();
} else {
setTimeout(() => {
memoryCards[a].flipped = false;
memoryCards[b].flipped = false;
memoryFlipped = [];
memoryLocked = false;
renderMemory();
}, 800);
}
}
}
// Quiz Game
let quizQuestions = [];
let quizIdx = 0;
let quizScore = 0;
async function startQuiz() {
try {
const res = await fetch(`${API_URL}/api/games/quiz/easy`);
if (res.ok) {
const data = await res.json();
quizQuestions = data.questions;
quizIdx = 0;
quizScore = 0;
switchTab('gameboard');
renderQuiz();
}
} catch {
showToast('error', 'Erreur chargement quiz');
}
}
function renderQuiz() {
const content = document.getElementById('gameBoardContent');
if (quizIdx >= quizQuestions.length) {
content.innerHTML = `
<div style="text-align:center;padding:30px;">
<div style="font-size:48px;margin-bottom:12px;">🏆</div>
<div style="font-size:20px;font-weight:700;">Quiz terminé !</div>
<div style="font-size:14px;color:var(--text-dim);margin-top:8px;">Score : ${quizScore}/${quizQuestions.length}</div>
<div style="margin-top:16px;color:var(--accent);font-weight:600;cursor:pointer;" onclick="startQuiz()">Rejouer</div>
</div>`;
return;
}
const q = quizQuestions[quizIdx];
content.innerHTML = `
<div style="text-align:center;margin-bottom:12px;font-size:16px;font-weight:700;">❓ Quiz Santé</div>
<div class="game-score">Question ${quizIdx + 1}/${quizQuestions.length} | Score : ${quizScore}</div>
<div style="background:var(--bg-card);border-radius:14px;padding:18px;margin-bottom:14px;">
<div style="font-size:14px;font-weight:600;line-height:1.5;">${q.q}</div>
</div>
<div style="display:flex;flex-direction:column;gap:8px;">
${q.options.map((opt, i) => `
<div onclick="answerQuiz(${i})" style="background:var(--bg-card);border:1px solid rgba(255,255,255,0.08);border-radius:12px;padding:14px 16px;cursor:pointer;font-size:13px;font-weight:500;transition:all 0.2s;">${opt}</div>
`).join('')}
</div>
`;
}
function answerQuiz(optIdx) {
const q = quizQuestions[quizIdx];
if (optIdx === q.answer) {
quizScore++;
showToast('success', 'Bonne réponse !');
} else {
showToast('error', 'Mauvaise réponse');
}
quizIdx++;
setTimeout(renderQuiz, 600);
}
// ═══ INFOS PRATIQUES ═══
let hospitalInfo = null;
let weatherInfo = null;
let tipsInfo = [];
async function loadInfo() {
try {
const [r1, r2, r3] = await Promise.all([
fetch(`${API_URL}/api/info/hospital`),
fetch(`${API_URL}/api/info/weather`),
fetch(`${API_URL}/api/info/tips`)
]);
if (r1.ok) hospitalInfo = await r1.json();
if (r2.ok) weatherInfo = await r2.json();
if (r3.ok) tipsInfo = await r3.json();
} catch {}
renderInfo();
}
function renderInfo() {
const container = document.getElementById('infoContent');
let html = '';
// Météo
if (weatherInfo) {
html += `<div class="info-card">
<div class="weather-big">
<div class="weather-icon">${weatherInfo.icon}</div>
<div class="weather-temp">${weatherInfo.temperature}°C</div>
<div class="weather-cond">${weatherInfo.condition}</div>
<div style="display:flex;justify-content:center;gap:16px;margin-top:10px;font-size:11px;color:var(--text-dim);">
<span>💧 ${weatherInfo.humidity}%</span>
<span>💨 ${weatherInfo.wind} km/h</span>
</div>
</div>
</div>`;
}
// Conseils du jour
if (tipsInfo.length > 0) {
html += `<div class="info-card">
<div class="info-card-title">💡 Conseils du jour</div>
<div class="info-card-body">
${tipsInfo.map(tip => `<div style="padding:8px 0;border-bottom:1px solid rgba(255,255,255,0.04);">${tip.icon} ${tip.text}</div>`).join('')}
</div>
</div>`;
}
// Horaires de visite
if (hospitalInfo && hospitalInfo.visitingHours) {
html += `<div class="info-card">
<div class="info-card-title">🕐 Horaires de visite</div>
<div class="info-card-body">
${Object.entries(hospitalInfo.visitingHours).map(([unit, hours]) => `
<div class="info-row"><span style="font-weight:500;">${unit}</span><span>${hours}</span></div>
`).join('')}
</div>
</div>`;
}
// Numéros utiles
if (hospitalInfo && hospitalInfo.usefulNumbers) {
html += `<div class="info-card">
<div class="info-card-title">📞 Numéros utiles</div>
<div class="info-card-body">
${hospitalInfo.usefulNumbers.map(n => `
<div class="info-row"><span>${n.icon || '📞'} ${n.name}</span><span style="color:var(--accent);font-weight:600;">${n.number}</span></div>
`).join('')}
</div>
</div>`;
}
// Infos hôpital
if (hospitalInfo) {
html += `<div class="info-card">
<div class="info-card-title">🏥 ${hospitalInfo.name || 'Hôpital'}</div>
<div class="info-card-body">
${hospitalInfo.address ? `<div class="info-row"><span>📍 Adresse</span><span>${hospitalInfo.address}</span></div>` : ''}
${hospitalInfo.phone ? `<div class="info-row"><span>📞 Téléphone</span><span>${hospitalInfo.phone}</span></div>` : ''}
${hospitalInfo.emergency ? `<div class="info-row"><span>🚨 Urgences</span><span style="color:var(--danger);font-weight:600;">${hospitalInfo.emergency}</span></div>` : ''}
</div>
</div>`;
}
container.innerHTML = html || '<p style="color:var(--text-dim);font-size:13px;text-align:center;padding:20px;">Chargement...</p>';
}
// ═══ SATISFACTION ═══
let satRating = 0;
let satComfort = 0;
let satFood = 0;
let satStaff = 0;
let satCleanliness = 0;
let satSubmitted = false;
function loadSatisfaction() {
satRating = 0; satComfort = 0; satFood = 0; satStaff = 0; satCleanliness = 0; satSubmitted = false;
renderSatisfaction();
}
function renderSatisfaction() {
const container = document.getElementById('satisfactionContent');
if (satSubmitted) {
container.innerHTML = `
<div class="sat-success">
<div class="sat-success-icon">🙏</div>
<div class="sat-success-title">Merci pour votre avis !</div>
<div class="sat-success-sub">Votre retour nous aide à améliorer votre séjour</div>
</div>`;
return;
}
container.innerHTML = `
<div style="text-align:center;margin-bottom:20px;">
<div style="font-size:14px;font-weight:600;margin-bottom:8px;">Note globale</div>
<div class="stars-row">
${[1,2,3,4,5].map(i => `<span class="star${satRating>=i?' active':''}" onclick="setSatRating(${i})">⭐</span>`).join('')}
</div>
</div>
<div class="sat-category">
<div class="sat-label">🛏️ Confort</div>
<div class="sat-stars">${[1,2,3,4,5].map(i => `<span class="sat-star${satComfort>=i?' active':''}" onclick="satComfort=${i};renderSatisfaction()">⭐</span>`).join('')}</div>
</div>
<div class="sat-category">
<div class="sat-label">🍽️ Repas</div>
<div class="sat-stars">${[1,2,3,4,5].map(i => `<span class="sat-star${satFood>=i?' active':''}" onclick="satFood=${i};renderSatisfaction()">⭐</span>`).join('')}</div>
</div>
<div class="sat-category">
<div class="sat-label">👩‍⚕️ Personnel</div>
<div class="sat-stars">${[1,2,3,4,5].map(i => `<span class="sat-star${satStaff>=i?' active':''}" onclick="satStaff=${i};renderSatisfaction()">⭐</span>`).join('')}</div>
</div>
<div class="sat-category">
<div class="sat-label">✨ Propreté</div>
<div class="sat-stars">${[1,2,3,4,5].map(i => `<span class="sat-star${satCleanliness>=i?' active':''}" onclick="satCleanliness=${i};renderSatisfaction()">⭐</span>`).join('')}</div>
</div>
<div style="margin-top:16px;">
<div class="sat-label">💬 Commentaire (optionnel)</div>
<textarea class="sat-textarea" id="satComment" placeholder="Partagez votre expérience..."></textarea>
</div>
<button class="sat-submit" onclick="submitSatisfaction()" ${satRating === 0 ? 'disabled' : ''}>Envoyer mon avis</button>
`;
}
function setSatRating(n) { satRating = n; renderSatisfaction(); }
async function submitSatisfaction() {
if (satRating === 0) return;
try {
const comment = document.getElementById('satComment')?.value || '';
await fetch(`${API_URL}/api/satisfaction/submit`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify({
roomId: roomData.id,
rating: satRating,
comfort: satComfort || undefined,
food: satFood || undefined,
staff: satStaff || undefined,
cleanliness: satCleanliness || undefined,
comment: comment || undefined
})
});
satSubmitted = true;
renderSatisfaction();
showToast('success', 'Merci pour votre avis !');
} catch {
showToast('error', 'Erreur d\'envoi');
}
}
// ═══ DOMOTIQUE ═══
async function loadDomotics() {
try {
const res = await fetch(`${API_URL}/api/domotic/${roomData.id}`, { headers: { Authorization: `Bearer ${token}` } });
if (res.ok) domoticState = await res.json();
} catch {}
renderDomotics();
}
function renderDomotics() {
const list = document.getElementById('domoticList');
const items = [
{ key: 'mainLight', icon: '💡', label: 'Lumière principale', status: domoticState.mainLight ? 'Allumée' : 'Éteinte' },
{ key: 'nightLight', icon: '🌙', label: 'Veilleuse', status: domoticState.nightLight ? 'Allumée' : 'Éteinte' },
{ key: 'blinds', icon: '🪟', label: 'Stores', status: `${domoticState.blinds || 0}%` },
];
list.innerHTML = items.map(i => `
<div class="domo-card">
<div class="domo-left">
<div class="domo-icon">${i.icon}</div>
<div><div class="domo-label">${i.label}</div><div class="domo-status">${i.status}</div></div>
</div>
<div class="toggle${domoticState[i.key] ? ' on' : ''}" onclick="toggleDomo('${i.key}')"></div>
</div>
`).join('');
}
function toggleDomo(key) {
if (key === 'blinds') {
domoticState.blinds = domoticState.blinds > 50 ? 0 : 100;
} else {
domoticState[key] = !domoticState[key];
}
renderDomotics();
socket?.emit('domotic:set', { roomId: roomData.id, [key]: domoticState[key] });
fetch(`${API_URL}/api/domotic/${roomData.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, body: JSON.stringify({ [key]: domoticState[key] }) }).catch(() => {});
}
// ═══ WEBSOCKET ═══
function connectSocket() {
socket = io(API_URL, { auth: { token }, reconnection: true, reconnectionDelay: 2000, reconnectionAttempts: Infinity });
socket.on('connect', () => {
socket.emit('join:room', { roomId: roomData.id });
document.getElementById('connStatus').innerHTML = '<div class="hdr-dot"></div><span>Connecté</span>';
});
socket.on('disconnect', () => {
document.getElementById('connStatus').innerHTML = '<span style="color:var(--danger);">Déconnecté</span>';
});
socket.on('tv:tune', (data) => {
document.getElementById('npNum').textContent = data.channelNumber;
document.getElementById('npName').textContent = data.name;
document.getElementById('npCat').textContent = data.category || 'TV';
const idx = channels.findIndex(c => c.number === data.channelNumber);
if (idx >= 0) { currentChIdx = idx; renderChannels(); }
// If TV tuned, radio is stopped
currentRadioStation = null;
if (document.getElementById('pageRadio').classList.contains('active')) renderRadio();
});
socket.on('request:update', (data) => {
const idx = requests.findIndex(r => r.id === data.id);
if (idx >= 0) requests[idx] = { ...requests[idx], ...data };
renderRequests();
showToast('success', data.status === 'ACKNOWLEDGED' ? 'Demande prise en charge' : 'Demande terminée');
});
socket.on('chat:message', (data) => {
if (data.sender === 'STAFF') {
chatMsgs.push(data);
if (document.getElementById('pageChat').classList.contains('active')) renderChat();
else { const b = document.getElementById('chatBadge'); b.textContent = parseInt(b.textContent || 0) + 1; b.classList.add('visible'); }
showToast('info', `${data.staffName || 'Soignant'}: ${data.content}`);
}
});
socket.on('domotic:update', (state) => { Object.assign(domoticState, state); renderDomotics(); });
socket.on('session:wiped', () => { localStorage.removeItem('chu-iptv-patient-token'); showToast('info', 'Session terminée'); setTimeout(() => location.reload(), 2000); });
socket.on('radio:play', (data) => {
currentRadioStation = { name: data.stationName, url: data.stationUrl };
if (document.getElementById('pageRadio').classList.contains('active')) renderRadio();
});
socket.on('radio:stop', () => {
currentRadioStation = null;
if (document.getElementById('pageRadio').classList.contains('active')) renderRadio();
});
}
// ═══ NAVIGATION ═══
function switchTab(tab) {
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
document.querySelectorAll('.tab-item').forEach(t => t.classList.remove('active'));
const pages = {
tv: 'pageTv', requests: 'pageRequests', chat: 'pageChat', edu: 'pageEdu', room: 'pageRoom',
radio: 'pageRadio', meals: 'pageMeals', games: 'pageGames', info: 'pageInfo',
satisfaction: 'pageSatisfaction', gameboard: 'pageGameBoard'
};
const pageId = pages[tab];
if (pageId) document.getElementById(pageId).classList.add('active');
// Highlight tab
const tabEl = document.querySelector(`.tab-item[data-tab="${tab}"]`);
if (tabEl) tabEl.classList.add('active');
// Load data on tab switch
if (tab === 'chat') { loadChat(); document.getElementById('chatBadge').textContent = '0'; document.getElementById('chatBadge').classList.remove('visible'); }
if (tab === 'requests') loadRequests();
if (tab === 'edu') loadVOD();
if (tab === 'radio') loadRadio();
if (tab === 'meals') loadMeals();
if (tab === 'games') loadGames();
if (tab === 'info') loadInfo();
if (tab === 'satisfaction') loadSatisfaction();
}
// ═══ MENU PLUS ═══
function openMoreMenu() {
document.getElementById('moreOverlay').classList.add('active');
}
function closeMoreMenu() {
document.getElementById('moreOverlay').classList.remove('active');
}
// ═══ TOAST ═══
function showToast(type, msg) {
const existing = document.querySelector('.toast');
if (existing) existing.remove();
const t = document.createElement('div');
t.className = `toast ${type}`;
t.textContent = msg;
document.body.appendChild(t);
setTimeout(() => t.remove(), 3000);
}
// ═══ SERVICE WORKER ═══
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('sw.js').catch(() => {});
}
// ═══ START ═══
init();
</script>
</body>
</html>