1477 lines
75 KiB
HTML
1477 lines
75 KiB
HTML
<!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>
|