Praxissofttware/views/calendar/index.ejs

286 lines
11 KiB
Plaintext

<%# views/calendar/index.ejs %>
<%# Eingebettet in das bestehende layout.ejs via express-ejs-layouts %>
<style>
/* ── Kalender-Variablen ──────────────────────────────────────────────── */
:root {
--cal-slot-h: 40px;
--cal-time-w: 60px;
--cal-min-col: 160px;
--cal-border: #dee2e6;
--cal-hover: #e8f0fe;
--cal-now: #dc3545;
--cal-holiday: #fff3cd;
--cal-weekend: #f8f9fa;
}
/* ── Layout ─────────────────────────────────────────────────────────── */
#calendarPage { display:flex; flex-direction:column; height:calc(100vh - 70px); overflow:hidden; }
#calToolbar { flex-shrink:0; }
#calHolidayBanner { flex-shrink:0; display:none; }
#calBody { flex:1; display:flex; overflow:hidden; }
/* ── Sidebar ─────────────────────────────────────────────────────────── */
#calSidebar {
width: 220px;
flex-shrink: 0;
border-right: 1px solid var(--cal-border);
overflow-y: auto;
background: #fff;
padding: 12px;
}
.mini-cal-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 2px;
text-align: center;
font-size: 12px;
}
.mini-wd { color:#6c757d; font-weight:600; padding-bottom:3px; }
.mini-day { padding:3px 1px; border-radius:4px; cursor:pointer; line-height:1.6; }
.mini-day:hover { background:#e9ecef; }
.mini-day.today { background:#cfe2ff; color:#0d6efd; font-weight:600; }
.mini-day.selected { background:#0d6efd; color:#fff; font-weight:600; }
.mini-day.holiday { color:#dc3545; }
.mini-day.other-month { color:#ced4da; }
.doc-item {
display:flex; align-items:center; gap:8px;
padding:6px 4px; border-radius:6px; cursor:pointer;
font-size:13px; transition:background .12s;
user-select:none;
}
.doc-item:hover { background:#f8f9fa; }
.doc-dot { width:10px; height:10px; border-radius:50%; flex-shrink:0; }
.doc-check {
width:16px; height:16px; border-radius:3px; flex-shrink:0;
border:1.5px solid #ced4da; margin-left:auto;
display:flex; align-items:center; justify-content:center;
}
.doc-item.active .doc-check { background:#0d6efd; border-color:#0d6efd; }
/* ── Kalender-Bereich ────────────────────────────────────────────────── */
#calMain { flex:1; display:flex; flex-direction:column; overflow:hidden; }
#calColHeaders { display:flex; background:#fff; border-bottom:2px solid var(--cal-border); flex-shrink:0; z-index:10; }
#calScroll { flex:1; overflow-y:auto; overflow-x:hidden; }
#calGrid { display:flex; }
/* Zeitachse */
.cal-time-axis { width:var(--cal-time-w); flex-shrink:0; background:#f8f9fa; }
.cal-time-label {
height:var(--cal-slot-h); display:flex; align-items:flex-start; justify-content:flex-end;
padding:0 6px; font-size:11px; color:#6c757d; transform:translateY(-6px);
font-variant-numeric:tabular-nums;
}
.cal-time-label.hour { font-weight:600; color:#495057; }
/* Spalten */
#calColHeadersInner { display:flex; flex:1; overflow:hidden; }
#calColumnsInner { display:flex; flex:1; }
.col-header {
flex:1; min-width:var(--cal-min-col);
padding:8px 10px; border-left:1px solid var(--cal-border);
display:flex; align-items:center; gap:6px;
}
.col-header-name { font-weight:600; font-size:13px; }
.col-header-spec { font-size:11px; color:#6c757d; }
.col-header-count {
margin-left:auto; font-size:11px; background:#e9ecef;
padding:1px 7px; border-radius:20px; color:#6c757d; white-space:nowrap;
}
.col-header-color {
width:32px; height:20px; border-radius:4px; border:none;
cursor:pointer; padding:0 2px;
}
.doc-col { flex:1; min-width:var(--cal-min-col); border-left:1px solid var(--cal-border); position:relative; }
.slot-row {
height:var(--cal-slot-h); border-bottom:1px solid #f0f0f0;
cursor:pointer; position:relative; transition:background .08s;
}
.slot-row.hour-start { border-bottom-color:#dee2e6; }
.slot-row.weekend { background:var(--cal-weekend); }
.slot-row:hover { background:var(--cal-hover); }
/* Terminblock */
.appt-block {
position:absolute; left:3px; right:3px;
border-radius:5px; padding:3px 6px;
cursor:pointer; z-index:3; overflow:hidden;
border-left:3px solid; font-size:12px;
transition:filter .12s;
}
.appt-block:hover { filter:brightness(.9); }
.appt-block.cancelled { opacity:.45; text-decoration:line-through; }
.appt-block.completed { opacity:.65; }
.appt-patient { font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.appt-time { font-size:10px; opacity:.75; font-variant-numeric:tabular-nums; }
/* Jetzt-Linie */
.now-line { position:absolute; left:0; right:0; height:2px; background:var(--cal-now); z-index:4; pointer-events:none; }
.now-dot { position:absolute; left:-4px; top:-4px; width:10px; height:10px; border-radius:50%; background:var(--cal-now); }
/* Scrollbar */
#calScroll::-webkit-scrollbar { width:5px; }
#calScroll::-webkit-scrollbar-thumb { background:#ced4da; border-radius:3px; }
</style>
<div id="calendarPage">
<%# ── Toolbar ── %>
<div id="calToolbar" class="d-flex align-items-center gap-2 p-2 border-bottom bg-white">
<i class="bi bi-calendar3 text-primary fs-5"></i>
<strong class="me-2">Kalender</strong>
<button class="btn btn-sm btn-outline-secondary" id="btnPrev">
<i class="bi bi-chevron-left"></i>
</button>
<button class="btn btn-sm btn-outline-secondary fw-semibold" id="btnDateDisplay" style="min-width:220px">
Lädt …
</button>
<button class="btn btn-sm btn-outline-secondary" id="btnNext">
<i class="bi bi-chevron-right"></i>
</button>
<button class="btn btn-sm btn-outline-primary" id="btnToday">Heute</button>
<div class="ms-auto d-flex gap-2">
<button class="btn btn-sm btn-primary" id="btnNewAppt">
<i class="bi bi-plus-lg me-1"></i>Neuer Termin
</button>
</div>
</div>
<%# ── Feiertagsbanner ── %>
<div id="calHolidayBanner" class="alert alert-warning d-flex align-items-center gap-2 mb-0 rounded-0 py-2 px-3">
<i class="bi bi-star-fill text-warning"></i>
<span id="calHolidayText"></span>
</div>
<%# ── Haupt-Body ── %>
<div id="calBody">
<%# ── Sidebar ── %>
<div id="calSidebar">
<%# Mini-Kalender %>
<div class="mb-3">
<div id="miniCal"></div>
</div>
<hr class="my-2">
<div class="text-uppercase fw-bold" style="font-size:11px;color:#6c757d;letter-spacing:.06em;">Ärzte</div>
<div id="docList" class="mt-2"></div>
</div>
<%# ── Kalender-Spalten ── %>
<div id="calMain">
<div id="calColHeaders">
<div style="width:var(--cal-time-w);flex-shrink:0;"></div>
<div id="calColHeadersInner"></div>
</div>
<div id="calScroll">
<div id="calGrid">
<div class="cal-time-axis" id="calTimeAxis"></div>
<div id="calColumnsInner"></div>
</div>
</div>
</div>
</div><%# /calBody %>
</div><%# /calendarPage %>
<%# ── Modal: Termin ── %>
<div class="modal fade" id="apptModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="apptModalTitle">Neuer Termin</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="row g-2">
<div class="col-7">
<label class="form-label small fw-semibold">Arzt</label>
<select class="form-select form-select-sm" id="fDoctor"></select>
</div>
<div class="col-5">
<label class="form-label small fw-semibold">Status</label>
<select class="form-select form-select-sm" id="fStatus">
<option value="scheduled">Geplant</option>
<option value="completed">Abgeschlossen</option>
<option value="cancelled">Abgesagt</option>
</select>
</div>
<div class="col-12">
<label class="form-label small fw-semibold">Patient</label>
<div class="position-relative">
<input type="text"
class="form-control form-control-sm"
id="fPatient"
placeholder="Name eingeben …"
autocomplete="off">
<input type="hidden" id="fPatientId">
<div id="patientDropdown"
class="position-absolute w-100 bg-white border rounded shadow-sm"
style="display:none; z-index:1060; top:100%; max-height:200px; overflow-y:auto;">
</div>
</div>
</div>
<div class="col-4">
<label class="form-label small fw-semibold">Datum</label>
<input type="date" class="form-control form-control-sm" id="fDate">
</div>
<div class="col-4">
<label class="form-label small fw-semibold">Uhrzeit</label>
<select class="form-select form-select-sm" id="fTime"></select>
</div>
<div class="col-4">
<label class="form-label small fw-semibold">Dauer</label>
<select class="form-select form-select-sm" id="fDuration">
<option value="15">15 min</option>
<option value="30">30 min</option>
<option value="45">45 min</option>
<option value="60">60 min</option>
<option value="90">90 min</option>
</select>
</div>
<div class="col-12">
<label class="form-label small fw-semibold">Notizen</label>
<textarea class="form-control form-control-sm" id="fNotes" rows="2" placeholder="Optional …"></textarea>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-sm btn-outline-danger me-auto" id="btnApptDelete" style="display:none">
<i class="bi bi-trash me-1"></i>Löschen
</button>
<button type="button" class="btn btn-sm btn-outline-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="button" class="btn btn-sm btn-primary" id="btnApptSave">
<i class="bi bi-check-lg me-1"></i>Speichern
</button>
</div>
</div>
</div>
</div>
<%# ── Toast ── %>
<div class="position-fixed bottom-0 start-50 translate-middle-x p-3" style="z-index:9999">
<div id="calToast" class="toast align-items-center text-bg-dark border-0" role="alert">
<div class="d-flex">
<div class="toast-body" id="calToastMsg"></div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
</div>
</div>
</div>
<%# ── Ärzte-Daten CSP-sicher übergeben (type="application/json" wird NICHT geblockt) ── %>
<script type="application/json" id="calDoctorsData"><%- JSON.stringify(doctors) %></script>
<%# ── Externes Script (script-src 'self' erlaubt dies) ── %>
<script src="/js/calendar.js"></script>