| Server IP : 193.86.120.172 / Your IP : 216.73.216.67 Web Server : Apache/2.4.63 (Unix) System : Linux JServices 3.10.108 #86003 SMP Wed Oct 22 13:20:46 CST 2025 x86_64 User : kubec ( 1026) PHP Version : 8.2.28 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /volume1/web/hotel_jservices_cz/ |
Upload File : |
<!DOCTYPE html>
<html lang="cs">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Administrace — Hotel DEMO</title>
<style>
/* ============================================================
RESET & PROMĚNNÉ
============================================================ */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--blue-700:#1d4ed8; --blue-600:#2563eb; --blue-500:#3b82f6;
--blue-100:#dbeafe; --blue-50:#eff6ff;
--gray-900:#0f172a; --gray-800:#1e293b; --gray-700:#334155;
--gray-600:#475569; --gray-500:#64748b; --gray-400:#94a3b8;
--gray-300:#cbd5e1; --gray-200:#e2e8f0; --gray-100:#f1f5f9; --gray-50:#f8fafc;
--white:#ffffff;
--green-600:#16a34a; --green-100:#dcfce7; --green-50:#f0fdf4;
--red-600:#dc2626; --red-100:#fee2e2; --red-50:#fef2f2;
--amber-600:#d97706; --amber-100:#fef9c3;
--sidebar-w: 220px;
--topbar-h: 52px;
--radius: 10px;
--shadow: 0 1px 3px rgba(0,0,0,.08);
--shadow-md: 0 4px 12px rgba(0,0,0,.1);
}
html, body { height: 100%; font-family: Arial, sans-serif; font-size: 14px;
color: var(--gray-800); background: var(--gray-50); overflow: hidden; }
/* ============================================================
PŘIHLAŠOVACÍ OBRAZOVKA
============================================================ */
#loginScreen {
position: fixed; inset: 0; background: var(--gray-50);
display: flex; align-items: center; justify-content: center; z-index: 999;
}
.login-card {
background: var(--white); border: 1px solid var(--gray-200);
border-radius: 14px; padding: 36px 32px; width: 100%; max-width: 380px;
box-shadow: var(--shadow-md);
}
.login-logo { font-size: 16px; font-weight: 600; color: var(--blue-600); margin-bottom: 6px; }
.login-sub { font-size: 13px; color: var(--gray-500); margin-bottom: 28px; }
.login-field { margin-bottom: 14px; }
.login-field label { display: block; font-size: 12px; font-weight: 500; color: var(--gray-700); margin-bottom: 5px; }
.login-field input {
width: 100%; border: 1px solid var(--gray-200); border-radius: 8px;
padding: 10px 12px; font-size: 14px; color: var(--gray-900); font-family: inherit;
transition: border-color .15s;
}
.login-field input:focus { outline: none; border-color: var(--blue-500); box-shadow: 0 0 0 3px var(--blue-50); }
.login-zapamatovat { display: flex; align-items: center; gap: 8px; margin-bottom: 20px; font-size: 13px; color: var(--gray-600); }
.login-zapamatovat input[type=checkbox] { accent-color: var(--blue-600); width: 15px; height: 15px; }
.login-error { background: var(--red-100); color: var(--red-600); border-radius: 8px; padding: 10px 12px; font-size: 13px; margin-bottom: 14px; display: none; }
.btn-login {
width: 100%; padding: 11px; border: none; border-radius: 8px;
background: var(--blue-600); color: var(--white); font-size: 14px;
font-weight: 500; cursor: pointer; font-family: inherit;
}
.btn-login:hover { background: var(--blue-700); }
.btn-login:disabled { opacity: .6; cursor: not-allowed; }
/* ============================================================
SHELL — SIDEBAR + TOPBAR + OBSAH
============================================================ */
#appShell { display: none; height: 100vh; overflow: hidden; }
/* Sidebar */
.sidebar {
position: fixed; left: 0; top: 0; bottom: 0; width: var(--sidebar-w);
background: var(--gray-900); display: flex; flex-direction: column; z-index: 50;
}
.sb-logo {
padding: 0 16px; height: var(--topbar-h);
display: flex; align-items: center;
border-bottom: 1px solid rgba(255,255,255,.08);
font-size: 14px; font-weight: 600; color: var(--white); letter-spacing: .3px;
}
.sb-logo span { color: var(--blue-400, #60a5fa); }
.sb-nav { flex: 1; padding: 10px 8px; overflow-y: auto; }
.sb-section { font-size: 10px; font-weight: 600; letter-spacing: 1px;
color: rgba(255,255,255,.3); padding: 14px 8px 6px; text-transform: uppercase; }
.sb-item {
display: flex; align-items: center; gap: 9px;
padding: 9px 10px; border-radius: 8px; cursor: pointer;
font-size: 13px; color: rgba(255,255,255,.6);
transition: background .15s, color .15s; margin-bottom: 1px;
user-select: none; position: relative;
}
.sb-item:hover { background: rgba(255,255,255,.07); color: rgba(255,255,255,.9); }
.sb-item.active { background: var(--blue-600); color: var(--white); }
.sb-item .icon { font-size: 16px; flex-shrink: 0; }
.sb-badge {
position: absolute; right: 8px; top: 50%; transform: translateY(-50%);
background: var(--red-600); color: var(--white); font-size: 10px;
font-weight: 600; min-width: 18px; height: 18px; border-radius: 9px;
display: flex; align-items: center; justify-content: center; padding: 0 4px;
}
.sb-bottom {
padding: 10px 8px 12px;
border-top: 1px solid rgba(255,255,255,.08);
}
.sb-user {
display: flex; align-items: center; gap: 9px;
padding: 8px 10px; border-radius: 8px;
cursor: pointer; transition: background .15s;
}
.sb-user:hover { background: rgba(255,255,255,.07); }
.sb-avatar {
width: 30px; height: 30px; border-radius: 50%;
background: var(--blue-600); color: var(--white);
display: flex; align-items: center; justify-content: center;
font-size: 11px; font-weight: 600; flex-shrink: 0;
}
.sb-user-info { min-width: 0; }
.sb-user-name { font-size: 12px; font-weight: 500; color: var(--white);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sb-user-role { font-size: 11px; color: rgba(255,255,255,.4); }
.sb-logout {
display: flex; align-items: center; gap: 9px;
padding: 7px 10px; border-radius: 8px; cursor: pointer;
font-size: 12px; color: rgba(255,255,255,.4);
transition: color .15s, background .15s; margin-top: 2px;
}
.sb-logout:hover { color: var(--red-600); background: rgba(255,255,255,.05); }
/* Topbar */
.topbar {
position: fixed; left: var(--sidebar-w); right: 0; top: 0; height: var(--topbar-h);
background: var(--white); border-bottom: 1px solid var(--gray-200);
display: flex; align-items: center; justify-content: space-between;
padding: 0 24px; z-index: 40;
}
.topbar-title { font-size: 15px; font-weight: 600; color: var(--gray-900); }
.topbar-right { display: flex; align-items: center; gap: 10px; }
/* Obsah */
.content {
position: fixed;
left: var(--sidebar-w); right: 0;
top: var(--topbar-h); bottom: 0;
overflow-y: auto; padding: 24px;
}
/* ============================================================
SPOLEČNÉ KOMPONENTY
============================================================ */
.card {
background: var(--white); border: 1px solid var(--gray-200);
border-radius: var(--radius); box-shadow: var(--shadow);
}
.card-header {
padding: 14px 18px; border-bottom: 1px solid var(--gray-100);
display: flex; align-items: center; justify-content: space-between;
}
.card-title { font-size: 13px; font-weight: 600; color: var(--gray-900); }
.card-body { padding: 18px; }
/* Tlačítka */
.btn {
display: inline-flex; align-items: center; gap: 5px;
padding: 8px 14px; border-radius: 8px; font-size: 13px; font-weight: 500;
border: none; cursor: pointer; font-family: inherit; transition: opacity .15s;
text-decoration: none; white-space: nowrap;
}
.btn:active { opacity: .8; }
.btn-primary { background: var(--blue-600); color: var(--white); }
.btn-primary:hover { background: var(--blue-700); }
.btn-secondary { background: var(--white); color: var(--gray-700); border: 1px solid var(--gray-200); }
.btn-secondary:hover { background: var(--gray-50); }
.btn-danger { background: var(--red-100); color: var(--red-600); }
.btn-danger:hover { background: #fecaca; }
.btn-success { background: var(--green-100); color: var(--green-600); }
.btn-sm { padding: 5px 10px; font-size: 12px; border-radius: 6px; }
.btn:disabled { opacity: .45; cursor: not-allowed; }
/* Formulářové prvky */
.field { margin-bottom: 14px; }
.field label { display: block; font-size: 12px; font-weight: 500; color: var(--gray-700); margin-bottom: 5px; }
.field input, .field select, .field textarea {
width: 100%; border: 1px solid var(--gray-200); border-radius: 8px;
padding: 9px 11px; font-size: 13px; color: var(--gray-900);
background: var(--white); font-family: inherit; transition: border-color .15s;
}
.field input:focus, .field select:focus, .field textarea:focus {
outline: none; border-color: var(--blue-500); box-shadow: 0 0 0 3px var(--blue-50);
}
.field textarea { resize: vertical; min-height: 72px; }
.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.field-row-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; }
.field-hint { font-size: 11px; color: var(--gray-400); margin-top: 4px; }
/* Toggle switch */
.toggle-wrap { display: flex; align-items: center; gap: 10px; }
.toggle {
width: 40px; height: 22px; border-radius: 11px; background: var(--gray-200);
position: relative; cursor: pointer; flex-shrink: 0;
transition: background .2s; border: none;
}
.toggle.on { background: var(--blue-600); }
.toggle::after {
content: ''; position: absolute;
width: 18px; height: 18px; background: var(--white);
border-radius: 50%; top: 2px; left: 2px;
transition: left .2s; box-shadow: 0 1px 3px rgba(0,0,0,.2);
}
.toggle.on::after { left: 20px; }
.toggle-label { font-size: 13px; color: var(--gray-700); }
/* Badges */
.badge {
display: inline-flex; align-items: center; padding: 2px 8px;
border-radius: 20px; font-size: 11px; font-weight: 500; white-space: nowrap;
}
.badge-blue { background: var(--blue-100); color: var(--blue-700); }
.badge-green { background: var(--green-100); color: var(--green-600); }
.badge-amber { background: var(--amber-100); color: var(--amber-600); }
.badge-red { background: var(--red-100); color: var(--red-600); }
.badge-gray { background: var(--gray-100); color: var(--gray-500); }
/* Tabulka */
.table-wrap { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; font-size: 13px; }
th { text-align: left; font-size: 11px; font-weight: 600; letter-spacing: .5px;
color: var(--gray-500); padding: 10px 12px; border-bottom: 1px solid var(--gray-200);
background: var(--gray-50); white-space: nowrap; }
td { padding: 11px 12px; border-bottom: 1px solid var(--gray-100); vertical-align: middle; }
tr:last-child td { border-bottom: none; }
tr:hover td { background: var(--gray-50); }
.td-akce { display: flex; gap: 6px; }
/* Statistické karty */
.stat-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; margin-bottom: 20px; }
.stat-card { background: var(--white); border: 1px solid var(--gray-200); border-radius: var(--radius); padding: 16px 18px; }
.stat-label { font-size: 11px; font-weight: 600; letter-spacing: .5px; color: var(--gray-400); text-transform: uppercase; margin-bottom: 6px; }
.stat-val { font-size: 24px; font-weight: 600; color: var(--gray-900); line-height: 1; margin-bottom: 4px; }
.stat-val.blue { color: var(--blue-600); }
.stat-val.green { color: var(--green-600); }
.stat-val.amber { color: var(--amber-600); }
.stat-sub { font-size: 12px; color: var(--gray-400); }
.stat-sub.up { color: var(--green-600); }
.stat-sub.down { color: var(--red-600); }
/* Alert */
.alert { padding: 11px 14px; border-radius: 8px; font-size: 13px; margin-bottom: 14px; }
.alert-error { background: var(--red-100); color: #991b1b; }
.alert-info { background: var(--blue-50); color: var(--blue-700); }
.alert-ok { background: var(--green-100); color: #166534; }
.alert-warn { background: var(--amber-100); color: #92400e; }
/* Spinner */
.spinner { width: 18px; height: 18px; border: 2px solid var(--gray-200);
border-top-color: var(--blue-600); border-radius: 50%;
animation: spin .7s linear infinite; display: inline-block; }
@keyframes spin { to { transform: rotate(360deg); } }
.loading-row { text-align: center; padding: 32px; color: var(--gray-400); font-size: 13px; }
/* Modal */
.modal-overlay {
position: fixed; inset: 0; background: rgba(0,0,0,.4);
display: flex; align-items: center; justify-content: center;
z-index: 200; padding: 16px;
}
.modal {
background: var(--white); border-radius: 14px; width: 100%;
max-width: 520px; max-height: 90vh; overflow-y: auto;
box-shadow: var(--shadow-md);
}
.modal-header {
padding: 18px 22px; border-bottom: 1px solid var(--gray-200);
display: flex; align-items: center; justify-content: space-between;
}
.modal-title { font-size: 15px; font-weight: 600; color: var(--gray-900); }
.modal-close { background: none; border: none; font-size: 20px; cursor: pointer;
color: var(--gray-400); line-height: 1; padding: 2px 6px; border-radius: 4px; }
.modal-close:hover { background: var(--gray-100); color: var(--gray-700); }
.modal-body { padding: 22px; }
.modal-footer { padding: 14px 22px; border-top: 1px solid var(--gray-200);
display: flex; justify-content: flex-end; gap: 8px; }
/* Stránkování */
.pagination { display: flex; align-items: center; gap: 6px; padding: 12px 16px; border-top: 1px solid var(--gray-100); }
.page-btn { background: none; border: 1px solid var(--gray-200); border-radius: 6px;
padding: 5px 10px; font-size: 12px; cursor: pointer; color: var(--gray-600); }
.page-btn:hover { background: var(--gray-50); }
.page-btn.active { background: var(--blue-600); color: var(--white); border-color: var(--blue-600); }
.page-btn:disabled { opacity: .4; cursor: not-allowed; }
.page-info { font-size: 12px; color: var(--gray-400); margin-left: auto; }
/* Filtrační lišta */
.filter-bar { display: flex; gap: 8px; padding: 14px 18px; border-bottom: 1px solid var(--gray-100); flex-wrap: wrap; }
.filter-bar input, .filter-bar select {
border: 1px solid var(--gray-200); border-radius: 8px;
padding: 7px 11px; font-size: 13px; color: var(--gray-900);
background: var(--white); font-family: inherit;
}
.filter-bar input:focus, .filter-bar select:focus {
outline: none; border-color: var(--blue-500);
}
.filter-bar input[type=search] { flex: 1; min-width: 160px; }
/* Responsivita */
@media (max-width: 900px) {
.sidebar { transform: translateX(-100%); transition: transform .25s; }
.sidebar.open { transform: translateX(0); }
.topbar { left: 0; }
.content { left: 0; }
.stat-grid { grid-template-columns: repeat(2,1fr); }
}
@media (max-width: 600px) {
.stat-grid { grid-template-columns: 1fr 1fr; }
.field-row, .field-row-3 { grid-template-columns: 1fr; }
.content { padding: 16px; }
}
/* ============================================================
GANTT KALENDÁŘ
============================================================ */
.gantt-wrap { overflow-x: auto; }
.gantt-table { border-collapse: collapse; font-size: 12px; min-width: 100%; }
.gantt-room-col { width: 130px; min-width: 130px; }
.gantt-th-room {
position: sticky; left: 0; z-index: 3;
background: var(--gray-50); border-right: 1px solid var(--gray-200);
border-bottom: 1px solid var(--gray-200);
padding: 8px 12px; font-size: 10px; font-weight: 600;
letter-spacing: .5px; color: var(--gray-400); text-transform: uppercase;
}
.gantt-th-day {
min-width: 44px; width: 44px; text-align: center;
padding: 6px 0; border-right: 1px solid var(--gray-100);
border-bottom: 1px solid var(--gray-200); background: var(--gray-50);
}
.gantt-th-day .day-name { font-size: 9px; color: var(--gray-400); display: block; margin-bottom: 2px; }
.gantt-th-day .day-num { font-size: 12px; color: var(--gray-700); font-weight: 500; }
.gantt-th-day.today .day-num {
background: var(--blue-600); color: var(--white);
border-radius: 50%; width: 22px; height: 22px;
display: inline-flex; align-items: center; justify-content: center;
}
.gantt-th-day.weekend { background: #fafbff; }
.gantt-td-room {
position: sticky; left: 0; z-index: 2;
background: var(--white); border-right: 1px solid var(--gray-200);
border-bottom: 1px solid var(--gray-100);
padding: 8px 12px; white-space: nowrap;
}
.gantt-td-room:hover { background: var(--gray-50); }
.gantt-room-cislo { font-size: 13px; font-weight: 500; color: var(--gray-900); }
.gantt-room-typ { font-size: 11px; color: var(--gray-400); }
.gantt-td-day {
border-right: 1px solid var(--gray-100);
border-bottom: 1px solid var(--gray-100);
position: relative; height: 60px;
min-width: 44px; width: 44px;
}
.gantt-td-day.weekend { background: #fafbff; }
.gantt-bar {
position: absolute; top: 4px; height: 52px;
border-radius: 6px; display: flex; align-items: center;
padding: 0 6px; font-size: 11px; font-weight: 500;
white-space: nowrap; overflow: hidden; cursor: pointer;
z-index: 1; left: 2px;
transition: opacity .15s;
flex-direction: column; justify-content: center;
}
.gantt-bar:hover { opacity: .85; }
.gantt-bar.dragging { opacity: .6; cursor: grabbing; z-index: 10; }
.gantt-bar.drag-invalid { background: var(--red-100) !important; color: var(--red-600) !important; border-color: #fca5a5 !important; }
.gantt-bar .resize-handle {
position: absolute; top: 0; width: 8px; height: 100%;
cursor: col-resize; z-index: 2; opacity: 0;
transition: opacity .15s;
}
.gantt-bar:hover .resize-handle { opacity: 1; }
.gantt-bar .resize-handle-l { left: 0; border-radius: 4px 0 0 4px; background: rgba(0,0,0,.15); }
.gantt-bar .resize-handle-r { right: 0; border-radius: 0 4px 4px 0; background: rgba(0,0,0,.15); }
.gantt-td-day.drop-target { background: var(--blue-50); outline: 2px dashed var(--blue-400); }
.gantt-td-day.drop-invalid { background: var(--red-100) !important; }
.gantt-bar.potvrzena { background: var(--green-100); color: #166534; border: 1px solid #bbf7d0; }
.gantt-bar.ceka { background: var(--amber-100); color: var(--amber-600); border: 1px solid #fde68a; }
.gantt-bar.blokovano { background: var(--gray-100); color: var(--gray-500); border: 1px solid var(--gray-200); }
.gantt-bar.ceka-schvaleni { background: #ede9fe; color: #6d28d9; border: 1px solid #d8b4fe; }
/* Nav přes měsíce */
.gantt-nav { display: flex; align-items: center; gap: 10px; }
.gantt-nav-btn {
background: none; border: 1px solid var(--gray-200); border-radius: 7px;
width: 30px; height: 30px; cursor: pointer; font-size: 15px;
color: var(--gray-500); display: flex; align-items: center; justify-content: center;
}
.gantt-nav-btn:hover { background: var(--gray-100); }
.gantt-period { font-size: 14px; font-weight: 500; color: var(--gray-900); min-width: 180px; text-align: center; }
/* Legenda */
.gantt-legenda { display: flex; gap: 16px; padding: 10px 18px; border-top: 1px solid var(--gray-100); flex-wrap: wrap; }
.gantt-leg-item { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--gray-500); }
.gantt-leg-dot { width: 12px; height: 12px; border-radius: 3px; }
/* ============================================================
SEKCE — PŘEHLED (dashboard)
============================================================ */
.dnes-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 20px; }
.dnes-list { display: flex; flex-direction: column; gap: 8px; }
.dnes-item {
display: flex; align-items: center; gap: 10px;
background: var(--white); border: 1px solid var(--gray-200);
border-radius: 8px; padding: 10px 12px;
}
.dnes-pokoj { font-weight: 600; color: var(--blue-600); font-size: 13px; min-width: 36px; }
.dnes-host { flex: 1; font-size: 13px; color: var(--gray-900); }
.dnes-cas { font-size: 12px; color: var(--gray-500); }
/* ============================================================
HESLO — MODAL
============================================================ */
.heslo-strength { height: 4px; border-radius: 2px; background: var(--gray-200);
margin-top: 6px; overflow: hidden; }
.heslo-strength-bar { height: 100%; border-radius: 2px; transition: width .3s, background .3s; width: 0; }
.notif-item { display:flex; align-items:flex-start; gap:10px; padding:10px 16px; cursor:pointer; border-bottom:0.5px solid var(--gray-50); transition:background .1s; }
.notif-item:hover { background:var(--gray-50); }
.notif-item.nova { background:#eff6ff; }
.notif-item.nova:hover { background:#dbeafe; }
.notif-ikona { font-size:18px; flex-shrink:0; margin-top:1px; }
.notif-text { font-size:13px; color:var(--gray-700); line-height:1.4; }
.notif-cas { font-size:11px; color:var(--gray-400); margin-top:2px; }
</style>
</head>
<body>
<!-- ============================================================
PŘIHLÁŠENÍ
============================================================ -->
<div id="loginScreen">
<div class="login-card">
<div class="login-logo" id="loginLogo">Hotel DEMO</div>
<div class="login-sub">Administrace — přihlaste se</div>
<div class="login-error" id="loginError"></div>
<div class="login-field">
<label for="loginJmeno">Přihlašovací jméno</label>
<input type="text" id="loginJmeno" autocomplete="username" placeholder="Přihlašovací jméno">
</div>
<div class="login-field">
<label for="loginHeslo">Heslo</label>
<input type="password" id="loginHeslo" autocomplete="current-password">
</div>
<div class="login-zapamatovat">
<input type="checkbox" id="loginZapamatovat">
<label for="loginZapamatovat">Zapamatovat mě na 30 dní</label>
</div>
<button class="btn-login" id="btnLogin" onclick="prihlasit()">Přihlásit se</button>
</div>
</div>
<!-- ============================================================
APP SHELL
============================================================ -->
<div id="appShell">
<!-- SIDEBAR -->
<aside class="sidebar" id="sidebar">
<div class="sb-logo" id="sbLogo">Hotel <span>DEMO</span></div>
<nav class="sb-nav">
<div class="sb-section">Přehled</div>
<div class="sb-item active" onclick="naSecci('prehled')" id="nav-prehled">
<span class="icon">📊</span> Přehled
</div>
<div class="sb-item" onclick="naSecci('kalendar')" id="nav-kalendar">
<span class="icon">📅</span> Kalendář
</div>
<div class="sb-section" data-sekce="rezervace" id="sbSekceRezervace">Rezervace</div>
<div class="sb-item" onclick="naSecci('rezervace')" id="nav-rezervace">
<span class="icon">📋</span> Rezervace
<span class="sb-badge" id="badgeCekajici" style="display:none"></span>
</div>
<div class="sb-item" onclick="naSecci('zakaznici')" id="nav-zakaznici">
<span class="icon">👥</span> Zákazníci
</div>
<div class="sb-item" onclick="naSecci('hodnoceni')" id="nav-hodnoceni">
<span class="icon">⭐</span> Hodnocení
</div>
<div class="sb-item" onclick="naSecci('statistiky')" id="nav-statistiky">
<span class="icon">📊</span> Statistiky
</div>
<div class="sb-section" id="sbSekceHotel" data-sekce="hotel">Hotel</div>
<div class="sb-item" onclick="naSecci('pokoje')" id="nav-pokoje">
<span class="icon">🚪</span> Pokoje
</div>
<div class="sb-item" onclick="naSecci('doplnky')" id="nav-doplnky">
<span class="icon">➕</span> Doplňky
</div>
<div class="sb-section" id="sbSekceSystem">Systém</div>
<div class="sb-item" onclick="naSecci('uzivatele')" id="nav-uzivatele">
<span class="icon">🔐</span> Uživatelé
</div>
<div class="sb-item" onclick="naSecci('nastaveni')" id="nav-nastaveni">
<span class="icon">⚙️</span> Nastavení
</div>
</nav>
<div class="sb-bottom">
<div class="sb-user" onclick="otevritHesloModal()">
<div class="sb-avatar" id="sbAvatar">?</div>
<div class="sb-user-info">
<div class="sb-user-name" id="sbUserName">—</div>
<div class="sb-user-role" id="sbUserRole">—</div>
</div>
</div>
<div class="sb-logout" onclick="odhlasit()">↩ Odhlásit se</div>
</div>
</aside>
<!-- TOPBAR -->
<header class="topbar">
<div style="display:flex;align-items:center;gap:12px">
<button onclick="toggleSidebar()" style="display:none;background:none;border:none;cursor:pointer;font-size:20px;color:var(--gray-500)" id="btnMenu">☰</button>
<div class="topbar-title" id="topbarTitle">Přehled</div>
</div>
<div style="display:flex;align-items:center;gap:12px">
<div style="position:relative" id="notifWrap">
<button onclick="notifToggle()" id="notifBtn" style="background:none;border:none;cursor:pointer;font-size:20px;color:var(--gray-500);position:relative;width:36px;height:36px;display:flex;align-items:center;justify-content:center;border-radius:8px" title="Notifikace">
🔔
<span id="notifBadge" style="display:none;position:absolute;top:2px;right:2px;background:#dc2626;color:#fff;font-size:10px;font-weight:700;border-radius:10px;min-width:16px;height:16px;line-height:16px;text-align:center;padding:0 4px"></span>
</button>
<div id="notifDropdown" style="display:none;position:absolute;right:0;top:44px;width:340px;background:#fff;border-radius:10px;box-shadow:0 8px 32px rgba(0,0,0,.15);border:0.5px solid var(--gray-200);z-index:1000;max-height:420px;overflow-y:auto">
<div style="display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-bottom:1px solid var(--gray-100)">
<span style="font-size:13px;font-weight:600">Nevyřízené položky</span>
</div>
<div id="notifSeznam" style="padding:8px 0"><div style="padding:16px;text-align:center;color:var(--gray-400);font-size:13px">Načítám…</div></div>
</div>
</div>
<div class="topbar-right" id="topbarRight"></div>
</div>
</header>
<!-- OBSAH -->
<main class="content" id="mainContent">
<div class="loading-row"><div class="spinner"></div></div>
</main>
</div>
<!-- ============================================================
MODAL — ZMĚNA HESLA
============================================================ -->
<div class="modal-overlay" id="hesloModal" style="display:none" onclick="if(event.target===this)zavritModal('hesloModal')">
<div class="modal">
<div class="modal-header">
<div class="modal-title">Změna hesla</div>
<button class="modal-close" onclick="zavritModal('hesloModal')">×</button>
</div>
<div class="modal-body">
<div id="hesloAlert"></div>
<div class="field">
<label>Současné heslo</label>
<input type="password" id="hesloStare" autocomplete="current-password">
</div>
<div class="field">
<label>Nové heslo</label>
<input type="password" id="hesloNove" oninput="hesloSila(this.value)" autocomplete="new-password">
<div class="heslo-strength"><div class="heslo-strength-bar" id="hesloBar"></div></div>
</div>
<div class="field">
<label>Nové heslo znovu</label>
<input type="password" id="hesloZnovu" autocomplete="new-password">
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="zavritModal('hesloModal')">Zrušit</button>
<button class="btn btn-primary" onclick="ulozitHeslo()">Uložit heslo</button>
</div>
</div>
</div>
<script>
// ============================================================
// KONFIGURACE
// ============================================================
const API = '/api/index.php';
let APP = {
user: null,
token: null,
sekce: 'prehled',
nastaveni: {},
// Data pro Gantt
ganttOd: null,
ganttDo: null,
};
// Pomocné zkratky
const $ = id => document.getElementById(id);
const esc = s => String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
// ============================================================
// PŘIHLÁŠENÍ / SESSION
// ============================================================
async function init() {
// Zjistit zda máme uloženou session
const token = localStorage.getItem('hd_token') || sessionStorage.getItem('hd_token');
if (token) {
APP.token = token;
try {
const r = await apiFetch('auth', 'check');
if (r.ok) {
APP.user = r.data.user;
APP.opravneni = r.data.opravneni || {};
await spustitApp();
return;
}
} catch(e) {}
}
$('loginScreen').style.display = 'flex';
}
async function prihlasit() {
const login = $('loginJmeno').value.trim();
const heslo = $('loginHeslo').value;
const zapamatovat= $('loginZapamatovat').checked;
const errEl = $('loginError');
const btn = $('btnLogin');
errEl.style.display = 'none';
if (!login || !heslo) {
errEl.textContent = 'Vyplňte přihlašovací jméno a heslo.';
errEl.style.display = 'block';
return;
}
btn.disabled = true;
btn.textContent = 'Přihlašuji…';
try {
const r = await fetch(API + '/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ login, heslo, zapamatovat }),
});
const d = await r.json();
if (!d.ok) {
errEl.textContent = d.error || 'Chyba přihlášení.';
errEl.style.display = 'block';
btn.disabled = false;
btn.textContent = 'Přihlásit se';
return;
}
APP.token = d.data.token;
APP.user = d.data.user;
APP.opravneni = d.data.opravneni || {};
const store = zapamatovat ? localStorage : sessionStorage;
store.setItem('hd_token', APP.token);
await spustitApp();
} catch(e) {
errEl.textContent = 'Nepodařilo se připojit k serveru.';
errEl.style.display = 'block';
btn.disabled = false;
btn.textContent = 'Přihlásit se';
}
}
$('loginHeslo').addEventListener('keydown', e => { if (e.key === 'Enter') prihlasit(); });
async function odhlasit() {
await apiFetch('auth', 'logout');
localStorage.removeItem('hd_token');
sessionStorage.removeItem('hd_token');
APP.token = null;
APP.user = null;
$('appShell').style.display = 'none';
$('loginScreen').style.display = 'flex';
$('loginJmeno').value = '';
$('loginHeslo').value = '';
$('loginError').style.display = 'none';
$('btnLogin').disabled = false;
$('btnLogin').textContent = 'Přihlásit se';
}
async function spustitApp() {
$('loginScreen').style.display = 'none';
$('appShell').style.display = 'block';
// Nastavit UI dle uživatele
const u = APP.user;
$('sbUserName').textContent = u.jmeno + ' ' + u.prijmeni;
$('sbUserRole').textContent = { admin: 'Administrátor', manazer: 'Manažer', zamestnanec: 'Zaměstnanec' }[u.role] || u.role;
$('sbAvatar').textContent = (u.jmeno[0] + u.prijmeni[0]).toUpperCase();
// Načíst nastavení
try {
const r = await apiFetch('nastaveni', 'verejne');
if (r.ok) {
APP.nastaveni = r.data;
const nazev = r.data.hotel_nazev || 'Hotel DEMO';
$('sbLogo').innerHTML = nazev.split(' ').length > 1
? nazev.split(' ').slice(0,-1).join(' ') + ' <span>' + nazev.split(' ').slice(-1) + '</span>'
: '<span>' + nazev + '</span>';
$('loginLogo').textContent = nazev;
document.title = 'Admin — ' + nazev;
}
} catch(e) {}
// Skrýt sekce dle role a oprávnění
if (u.role === 'zamestnanec') {
const op = APP.opravneni || {};
// Vždy skryté pro zaměstnance
['nav-pokoje','nav-doplnky','nav-uzivatele','nav-nastaveni',
'sbSekceHotel','sbSekceSystem'].forEach(id => {
const el = $(id); if(el) el.style.display = 'none';
});
// Podmíněně dle oprávnění
if (!op.zmena_rezervace && !op.storno_rezervace && !op.potvrzeni_rezervace && !op.zmena_terminu) {
const el = $('nav-rezervace'); if(el) el.style.display = 'none';
}
if (!op.kartoteka) {
const el = $('nav-zakaznici'); if(el) el.style.display = 'none';
}
if (!op.hodnoceni) {
const el = $('nav-hodnoceni'); if(el) el.style.display = 'none';
}
if (!op.statistiky_modul) {
const el = $('nav-statistiky'); if(el) el.style.display = 'none';
}
// Skrýt sekci Rezervace v menu pokud jsou všechny pod ní skryté
const rezViditelna = $('nav-rezervace') && $('nav-rezervace').style.display !== 'none';
const zakViditelna = $('nav-zakaznici') && $('nav-zakaznici').style.display !== 'none';
const hodViditelna = $('nav-hodnoceni') && $('nav-hodnoceni').style.display !== 'none';
if (!rezViditelna && !zakViditelna && !hodViditelna) {
const sbRez = $('sbSekceRezervace');
if (sbRez) sbRez.style.display = 'none';
}
}
// Obsluha přímého odkazu z e-mailu (#rezervace/123)
const hash = window.location.hash;
if (hash.startsWith('#rezervace/')) {
const rid = parseInt(hash.split('/')[1]);
if (rid) {
naSecci('rezervace');
setTimeout(() => otevritRezervaci(rid), 600);
nactiStatsBadge();
return;
}
}
// Načíst dashboard
naSecci('prehled');
nactiStatsBadge();
startNotifPolling();
}
// ============================================================
// NAVIGACE MEZI SEKCEMI
// ============================================================
function naSecci(sekce) {
// Kontrola přístupu pro zaměstnance
if (APP.user?.role === 'zamestnanec') {
const op = APP.opravneni || {};
const zakazane = ['pokoje','doplnky','uzivatele','nastaveni'];
if (zakazane.includes(sekce)) { naSecci('prehled'); return; }
if (sekce === 'rezervace' && !op.zmena_rezervace && !op.storno_rezervace && !op.potvrzeni_rezervace && !op.zmena_terminu) { naSecci('prehled'); return; }
if (sekce === 'zakaznici' && !op.kartoteka) { naSecci('prehled'); return; }
if (sekce === 'hodnoceni' && !op.hodnoceni) { naSecci('prehled'); return; }
if (sekce === 'statistiky' && !op.statistiky_modul) { naSecci('prehled'); return; }
}
APP.sekce = sekce;
document.querySelectorAll('.sb-item').forEach(el => el.classList.remove('active'));
const navEl = $('nav-' + sekce);
if (navEl) navEl.classList.add('active');
const titulky = {
prehled:'Přehled', kalendar:'Kalendář obsazenosti',
rezervace:'Rezervace', zakaznici:'Zákazníci', hodnoceni:'Hodnocení',
pokoje:'Pokoje', doplnky:'Doplňkové služby',
uzivatele:'Uživatelé', nastaveni:'Nastavení',
};
$('topbarTitle').textContent = titulky[sekce] || sekce;
$('topbarRight').innerHTML = '';
const mc = $('mainContent');
mc.innerHTML = '<div class="loading-row"><div class="spinner"></div></div>';
switch(sekce) {
case 'prehled': renderPrehled(); break;
case 'kalendar': renderKalendar(); break;
case 'rezervace': renderRezervace(); break;
case 'zakaznici': renderZakaznici(); break;
case 'hodnoceni': renderHodnoceni(); break;
case 'statistiky': renderStatistiky(); break;
case 'pokoje': renderPokoje(); break;
case 'doplnky': renderDoplnky(); break;
case 'uzivatele': renderUzivatele(); break;
case 'nastaveni': renderNastaveni(); break;
}
}
// ============================================================
// PŘEHLED (DASHBOARD)
// ============================================================
function prepnoutObsazenost(perioda) {
const d = APP.obsazenostData;
if (!d) return;
const mapa = {
tyden: d.obsazenost_tyden,
pristy_tyden: d.obsazenost_pristy_tyden,
mesic: d.obsazenost_mesic,
kvartal: d.obsazenost_kvartal,
pulrok: d.obsazenost_pulrok,
rok: d.obsazenost_rok,
};
const el = document.getElementById('obsazenostHodnota');
if (el) el.textContent = (mapa[perioda] ?? '—') + ' %';
}
const KAP = { datumOd: null, pocetDni: 7 };
function kapFormatDen(datum) {
const dt = new Date(datum + 'T00:00:00');
const dnazvy = ['Ne','Po','Út','St','Čt','Pá','So'];
return `${dnazvy[dt.getDay()]} ${dt.getDate()}.${dt.getMonth()+1}.`;
}
function kapNastavRozsah(pocetDni) {
KAP.pocetDni = pocetDni;
nactiKapacituDnes();
}
function kapPosunOkno(smer) {
const dt = new Date(KAP.datumOd + 'T00:00:00');
dt.setDate(dt.getDate() + smer * KAP.pocetDni);
KAP.datumOd = dt.toISOString().split('T')[0];
nactiKapacituDnes();
}
async function nactiKapacituDnes() {
try {
if (!KAP.datumOd) KAP.datumOd = new Date().toISOString().split('T')[0];
const r = await apiFetch('dostupnost', 'kapacity_tyden', { datum_od: KAP.datumOd, pocet_dni: KAP.pocetDni });
if (!r.ok) return;
const dny = r.data.dny;
APP._kapDny = dny;
const dnesStr = new Date().toISOString().split('T')[0];
const dtOd = new Date(KAP.datumOd + 'T00:00:00');
const dtDo = new Date(dtOd); dtDo.setDate(dtDo.getDate() + KAP.pocetDni - 1);
const obdobiLabel = `${dtOd.getDate()}. – ${dtDo.getDate()}. ${['ledna','února','března','dubna','května','června','července','srpna','září','října','listopadu','prosince'][dtDo.getMonth()]} ${dtDo.getFullYear()}`;
const radek = (ikon, label, klic, getVal, getMax) => `
<div style="display:grid;grid-template-columns:90px repeat(${dny.length},1fr);gap:2px;margin-bottom:4px">
<div style="font-size:12px;color:var(--gray-500);display:flex;align-items:center;gap:4px">${ikon}${label}</div>
${dny.map(d => {
const val = getVal(d), max = getMax ? getMax(d) : null;
const plno = max !== null && val >= max;
const prazdno = val === 0;
const bg = plno ? 'var(--amber-100)' : prazdno ? 'var(--gray-50)' : 'var(--gray-100)';
const color = plno ? 'var(--amber-600)' : 'var(--gray-700)';
const label2 = max !== null ? `${val}/${max}` : `${val}`;
return `<div onclick="kapOtevritDen('${d.datum}','${klic}')" style="cursor:pointer;text-align:center;font-size:12px;font-weight:600;padding:6px 2px;background:${bg};border-radius:6px;color:${color}">${label2}</div>`;
}).join('')}
</div>`;
const el = document.getElementById('widgetKapacita');
if (!el) return;
el.innerHTML = `
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;flex-wrap:wrap;gap:8px">
<div style="display:flex;align-items:center;gap:6px">
<button onclick="kapPosunOkno(-1)" class="btn btn-secondary btn-sm" style="padding:3px 8px">‹</button>
<span style="font-size:13px;font-weight:500">${obdobiLabel}</span>
<button onclick="kapPosunOkno(1)" class="btn btn-secondary btn-sm" style="padding:3px 8px">›</button>
</div>
<div style="display:flex;gap:4px">
<button onclick="kapNastavRozsah(7)" class="btn btn-sm ${KAP.pocetDni===7?'btn-primary':'btn-secondary'}" style="padding:3px 10px;font-size:11px">Týden</button>
<button onclick="kapNastavRozsah(14)" class="btn btn-sm ${KAP.pocetDni===14?'btn-primary':'btn-secondary'}" style="padding:3px 10px;font-size:11px">14 dní</button>
</div>
</div>
<div style="display:grid;grid-template-columns:90px repeat(${dny.length},1fr);gap:2px;margin-bottom:6px">
<div></div>
${dny.map(d => `<div style="text-align:center;font-size:11px;padding:3px 0;border-radius:6px;${d.datum===dnesStr?'background:var(--blue-50);color:var(--blue-600);font-weight:600':'color:var(--gray-400)'}">${kapFormatDen(d.datum)}</div>`).join('')}
</div>
${radek('🚗', 'Parkování', 'parkovani', d => d.parkovani.obsazeno, d => d.parkovani.kapacita)}
${radek('⚡', 'Dobíjení', 'dobijeni', d => d.dobijeni.obsazeno, d => d.dobijeni.kapacita)}
${radek('☕', 'Snídaně', 'snidane', d => d.snidane.porci, null)}
<p style="font-size:11px;color:var(--gray-400);margin-top:10px;text-align:center">Kliknutím na den zobrazíte jmenný seznam hostů</p>
`;
} catch(e) { console.error(e); }
}
function kapOtevritDen(datum, klic) {
const den = (APP._kapDny || []).find(d => d.datum === datum);
if (!den) return;
const dt = new Date(datum + 'T00:00:00');
const dnazvyDlouhe = ['neděle','pondělí','úterý','středa','čtvrtek','pátek','sobota'];
const datumLabel = `${dnazvyDlouhe[dt.getDay()]} ${dt.getDate()}.${dt.getMonth()+1}.${dt.getFullYear()}`;
const sekce = (ikon, nazev, info, hoste) => `
<div style="margin-bottom:14px">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:6px">
<div style="font-size:13px;font-weight:600">${ikon} ${nazev}</div>
<div style="font-size:12px;color:var(--gray-500)">${info}</div>
</div>
${hoste.length
? hoste.map(h => `<div style="font-size:13px;color:var(--gray-700);padding:4px 0;border-bottom:1px solid var(--gray-50)">${h}</div>`).join('')
: '<div style="font-size:12px;color:var(--gray-400)">Žádní hosté</div>'}
</div>`;
const obsahy = {
parkovani: sekce('🚗', 'Parkování', `${den.parkovani.obsazeno} / ${den.parkovani.kapacita} míst`, den.parkovani.hoste),
dobijeni: sekce('⚡', 'Dobíjení', `${den.dobijeni.obsazeno} / ${den.dobijeni.kapacita} míst`, den.dobijeni.hoste),
snidane: sekce('☕', 'Snídaně', `${den.snidane.porci} porcí`, den.snidane.hoste.map(h => `${h.jmeno} <span style="color:var(--gray-400)">(${h.pocet} ${h.pocet===1?'osoba':h.pocet<5?'osoby':'osob'})</span>`)),
};
const modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.id = 'kapDenModal';
modal.onclick = e => { if (e.target === modal) modal.remove(); };
modal.innerHTML = `<div class="modal" style="max-width:380px">
<div class="modal-header">
<div class="modal-title">${datumLabel}</div>
<button class="modal-close" onclick="document.getElementById('kapDenModal').remove()">×</button>
</div>
<div class="modal-body">
${obsahy.parkovani}
${obsahy.dobijeni}
${obsahy.snidane}
</div>
</div>`;
document.body.appendChild(modal);
}
async function renderPrehled() {
const mc = $('mainContent');
try {
const r = await apiFetch('statistiky', 'prehled');
if (!r.ok) throw new Error(r.error);
const d = r.data;
APP.obsazenostData = d;
const zmenaStr = d.trzby_zmena_proc !== null
? (d.trzby_zmena_proc >= 0 ? '+' : '') + d.trzby_zmena_proc + ' % vs. minulý měsíc'
: '';
const zmenaCls = d.trzby_zmena_proc >= 0 ? 'up' : 'down';
mc.innerHTML = `
<div class="stat-grid">
<div class="stat-card">
<div class="stat-label">Příjezdy dnes</div>
<div class="stat-val blue">${d.prijezdy_dnes}</div>
<div class="stat-sub">check-in od ${APP.nastaveni.checkin_cas||'14:00'}</div>
</div>
<div class="stat-card">
<div class="stat-label">Odjezdy dnes</div>
<div class="stat-val">${d.odjezdy_dnes}</div>
<div class="stat-sub">check-out do ${APP.nastaveni.checkout_cas||'10:00'}</div>
</div>
<div class="stat-card" style="cursor:default">
<div class="stat-label" style="display:flex;align-items:center;justify-content:space-between">
<span>Obsazenost</span>
<select id="obsazenostPerioda" onchange="prepnoutObsazenost(this.value)"
style="font-size:10px;padding:2px 4px;border:1px solid var(--gray-200);border-radius:4px;background:var(--white);color:var(--gray-600)">
<option value="tyden">Týden</option>
<option value="pristy_tyden">Příští týden</option>
<option value="mesic">Měsíc</option>
<option value="kvartal">Kvartál</option>
<option value="pulrok">Půlrok</option>
<option value="rok">Rok</option>
</select>
</div>
<div class="stat-val blue" id="obsazenostHodnota">${d.obsazenost_tyden} %</div>
<div class="stat-sub">${d.ubytovani} hostů aktuálně</div>
</div>
${['admin','manazer'].includes(APP.user.role) ? `
<div class="stat-card">
<div class="stat-label">Tržby tento měsíc</div>
<div class="stat-val green">${formatCena(d.trzby_mesic)}</div>
<div class="stat-sub ${zmenaCls}">${zmenaStr}</div>
</div>` : ''}
</div>
${d.cekajici_slevy > 0 ? `
<div class="alert alert-warn" style="margin-bottom:16px">
⚠️ Čekají <strong>${d.cekajici_slevy}</strong> návrh(y) slevy ke schválení.
<a href="#" onclick="naSecci('rezervace');return false" style="color:inherit;font-weight:600;margin-left:8px">Zobrazit →</a>
</div>` : ''}
${d.hodnoceni_prumer ? `
<div class="alert alert-info" style="margin-bottom:16px">
⭐ Průměrné hodnocení: <strong>${d.hodnoceni_prumer} / 5</strong>
(${d.hodnoceni_pocet} hodnocení)
</div>` : ''}
<div class="card" style="margin-bottom:16px">
<div class="card-header"><div class="card-title">🅿️ Kapacity doplňků</div></div>
<div class="card-body" id="widgetKapacita"><div class="loading-row"><div class="spinner"></div></div></div>
</div>
<div class="dnes-grid">
<div class="card">
<div class="card-header"><div class="card-title">📥 Dnešní příjezdy</div></div>
<div class="card-body" id="dnesPrijezdy"><div class="loading-row"><div class="spinner"></div></div></div>
</div>
<div class="card">
<div class="card-header"><div class="card-title">📤 Dnešní odjezdy</div></div>
<div class="card-body" id="dnesOdjezdy"><div class="loading-row"><div class="spinner"></div></div></div>
</div>
</div>`;
// Načíst příjezdy/odjezdy
const rez = await apiFetch('guest', 'today');
if (rez.ok) {
renderDnesniSeznam('dnesPrijezdy', rez.data.prijezdy, 'prijezd');
renderDnesniSeznam('dnesOdjezdy', rez.data.odjezdy, 'odjezd');
}
nactiKapacituDnes();
} catch(e) {
console.error('renderPrehled chyba:', e);
mc.innerHTML = `<div class="alert alert-error">Nepodařilo se načíst přehled: ${esc(e.message)}</div>`;
}
}
function renderDnesniSeznam(containerId, seznam, typ) {
const el = $(containerId);
if (!seznam.length) {
el.innerHTML = '<div style="color:var(--gray-400);font-size:13px">Žádné ' + (typ==='prijezd'?'příjezdy':'odjezdy') + ' dnes</div>';
return;
}
// Widget kapacity parkovišť
el.innerHTML = '<div class="dnes-list">' +
seznam.map(r => `
<div class="dnes-item" onclick="otevritRezervaci(${r.id})" style="cursor:pointer">
<div class="dnes-pokoj">${esc(r.pokoj_cislo)}</div>
<div class="dnes-host">${esc(r.jmeno + ' ' + r.prijmeni)}</div>
<div class="dnes-cas">${r[typ==='prijezd'?'checkin_cas':'checkout_cas'] || (typ==='prijezd'?APP.nastaveni.checkin_cas:'10:00') || ''}</div>
</div>`).join('') +
'</div>';
}
// ============================================================
// GANTT KALENDÁŘ
// ============================================================
async function renderKalendar() {
const mc = $('mainContent');
const dnes = new Date();
if (!APP.ganttOd) {
APP.ganttOd = new Date(dnes.getFullYear(), dnes.getMonth(), 1);
APP.ganttDo = new Date(dnes.getFullYear(), dnes.getMonth() + 1, 0);
}
// Topbar tlačítko
$('topbarRight').innerHTML = `
<button class="btn btn-primary" onclick="otevritNovouRezervaci()">+ Nová rezervace</button>`;
mc.innerHTML = `
<div class="card">
<div class="card-header">
<div class="gantt-nav">
<button class="gantt-nav-btn" onclick="ganttPrev()">‹</button>
<div class="gantt-period" id="ganttPeriod"></div>
<button class="gantt-nav-btn" onclick="ganttNext()">›</button>
</div>
<div style="display:flex;gap:8px">
<button class="btn btn-secondary btn-sm" onclick="nactiKalendar()" title="Obnovit kalendář">↻</button>
<button class="btn btn-secondary btn-sm" onclick="ganttDnes()">Dnes</button>
</div>
</div>
<div class="gantt-wrap">
<div id="ganttTable"><div class="loading-row"><div class="spinner"></div></div></div>
</div>
<div class="gantt-legenda">
<div class="gantt-leg-item"><div class="gantt-leg-dot" style="background:var(--green-100);border:1px solid #bbf7d0"></div>Potvrzená</div>
<div class="gantt-leg-item"><div class="gantt-leg-dot" style="background:var(--amber-100);border:1px solid #fde68a"></div>Čeká</div>
<div class="gantt-leg-item"><div class="gantt-leg-dot" style="background:#ede9fe;border:1px solid #d8b4fe"></div>Čeká na schválení</div>
<div class="gantt-leg-item"><div class="gantt-leg-dot" style="background:var(--gray-100);border:1px solid var(--gray-200)"></div>Blokováno</div>
</div>
</div>`;
await nactiGanttData();
}
async function nactiGanttData() {
const od = formatDatum(APP.ganttOd);
const doDate = APP.ganttDo;
const do_ = formatDatum(doDate);
const mesice = ['Leden','Únor','Březen','Duben','Květen','Červen',
'Červenec','Srpen','Září','Říjen','Listopad','Prosinec'];
const period = mesice[APP.ganttOd.getMonth()] + ' ' + APP.ganttOd.getFullYear();
const el = $('ganttPeriod');
if (el) el.textContent = period;
try {
const r = await apiFetch('dostupnost', 'kalendar', { datum_od: od, datum_do: do_ });
if (!r.ok) throw new Error(r.error);
renderGanttTable(r.data, od, do_);
} catch(e) {
const gt = $('ganttTable');
if (gt) gt.innerHTML = '<div class="alert alert-error" style="margin:16px">Nepodařilo se načíst kalendář.</div>';
}
}
function nactiKalendar() {
if (APP.ganttOd && APP.ganttDo) {
nactiGanttData();
} else {
renderKalendar();
}
}
function renderGanttTable(data, od, do_) {
const gt = $('ganttTable');
if (!gt) return;
const CELL_W = 44;
const ROW_H = 60;
const dnes = formatDatum(new Date());
const dny = [];
const dnazvy = ['Ne','Po','Út','St','Čt','Pá','So'];
const op = APP.opravneni || {};
const mozeEditTermin = APP.user.role === 'admin' || APP.user.role === 'manazer' ||
!!(op.zmena_terminu === true || op.zmena_terminu === 1);
let d = new Date(od);
const konec = new Date(do_);
konec.setDate(konec.getDate() + 1);
while (d < konec) { dny.push(formatDatum(d)); d.setDate(d.getDate() + 1); }
// === HEADER ===
let html = `<table class="gantt-table"><thead><tr><th class="gantt-th-room">POKOJ</th>`;
dny.forEach(datum => {
const dt = new Date(datum + 'T00:00:00');
const isWE = dt.getDay() === 0 || dt.getDay() === 6;
const today = datum === dnes;
html += `<th class="gantt-th-day${isWE?' weekend':''}${today?' today':''}">
<span class="day-name">${dnazvy[dt.getDay()]}</span>
<span class="day-num">${dt.getDate()}</span>
</th>`;
});
html += '</tr></thead><tbody>';
// === ŘÁDKY — jen mřížka, bez barů ===
const pokoje = data.pokoje;
pokoje.forEach((pokoj, ri) => {
const nazev = pokoj.nazev ? pokoj.cislo + ' — ' + pokoj.nazev : pokoj.cislo;
html += `<tr>
<td class="gantt-td-room" onclick="naSecci('pokoje')">
<div class="gantt-room-cislo">${esc(nazev)}</div>
<div class="gantt-room-typ">${esc(pokoj.typ_nazev)}</div>
</td>`;
dny.forEach(datum => {
const dt = new Date(datum + 'T00:00:00');
const isWE = dt.getDay() === 0 || dt.getDay() === 6;
html += `<td class="gantt-td-day${isWE?' weekend':''}" data-datum="${datum}" data-pokoj="${pokoj.id}"></td>`;
});
html += '</tr>';
});
html += '</tbody></table>';
gt.innerHTML = html;
// === OVERLAY BARY — absolutně přes celou tabulku ===
const table = gt.querySelector('table');
const tbody = table.querySelector('tbody');
const thead = table.querySelector('thead');
// Wrapper pro overlay
const wrap = document.createElement('div');
wrap.style.cssText = 'position:relative;';
gt.insertBefore(wrap, null);
wrap.appendChild(table);
const overlay = document.createElement('div');
overlay.id = 'ganttOverlay';
overlay.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:5;';
wrap.appendChild(overlay);
// Zjistit pozici hlavičky (výška thead)
const headerH = thead.offsetHeight || 44;
const roomW = tbody.rows[0]?.cells[0]?.offsetWidth || 120;
// Přidat bary
pokoje.forEach((pokoj, ri) => {
const top = headerH + ri * ROW_H;
// Rezervace
data.rezervace.filter(r => r.pokoj_id === pokoj.id).forEach(rez => {
const diOd = dny.indexOf(rez.datum_prijezdu);
const diDo = dny.indexOf(rez.datum_odjezdu);
const diStart = diOd >= 0 ? diOd : (new Date(rez.datum_prijezdu) < new Date(od) ? 0 : -1);
if (diStart < 0) return;
const diEnd = diDo >= 0 ? diDo : dny.length;
const pocetDni = diEnd - diStart;
if (pocetDni <= 0) return;
const left = roomW + diStart * CELL_W + 2;
const w = pocetDni * CELL_W - 4;
const ikonky = (rez.doplnky_typy||[]).map(t => ({
parking:'🚗', charging:'⚡', breakfast:'☕', late_checkout:'🕑', other:'•'
}[t]||'')).join('');
const bar = document.createElement('div');
const maZadost = rez.pokoj_zadost_stav === 'ceka' || rez.cas_zadost_stav === 'ceka' || rez.sleva_stav === 'navrzena';
bar.className = `gantt-bar ${maZadost ? 'ceka-schvaleni' : rez.stav}`;
bar.style.cssText = `position:absolute;top:${top+4}px;left:${left}px;width:${w}px;height:${ROW_H-8}px;pointer-events:all;cursor:${mozeEditTermin?'grab':'pointer'};display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;overflow:hidden;`;
bar.dataset.rezId = rez.id;
bar.dataset.datumOd = rez.datum_prijezdu;
bar.dataset.datumDo = rez.datum_odjezdu;
bar.dataset.pokojId = pokoj.id;
bar.dataset.diStart = diStart;
bar.dataset.origLeft = left;
bar.dataset.cellW = CELL_W;
bar.dataset.roomW = roomW;
bar.dataset.rowTop = top;
bar.title = `${rez.jmeno} ${rez.prijmeni} (${rez.datum_prijezdu} – ${rez.datum_odjezdu})`;
bar.innerHTML = `
${mozeEditTermin ? `<div class="resize-handle resize-handle-l" onmousedown="ganttResizeStart(event,'l',${rez.id})"></div>` : ''}
<span style="line-height:1.2;font-size:11px;font-weight:500">${esc(rez.jmeno)}</span>
<span style="line-height:1.2;font-size:11px">${esc(rez.prijmeni)}</span>
${ikonky ? `<span style="font-size:10px;opacity:.8">${ikonky}</span>` : ''}
${mozeEditTermin ? `<div class="resize-handle resize-handle-r" onmousedown="ganttResizeStart(event,'r',${rez.id})"></div>` : ''}
`;
bar.addEventListener('mousedown', ev => {
if (!ev.target.classList.contains('resize-handle')) ganttMoveStart(ev, bar);
});
bar.addEventListener('click', ev => ganttBarClick(ev, rez.id));
overlay.appendChild(bar);
});
// Blokace
data.blokovane.filter(b => b.pokoj_id === pokoj.id).forEach(bl => {
const diOd = dny.indexOf(bl.datum_od);
if (diOd < 0) return;
const diDo = dny.indexOf(bl.datum_do);
const diEnd = diDo >= 0 ? diDo : dny.length;
const pocetDni = diEnd - diOd;
if (pocetDni <= 0) return;
const left = roomW + diOd * CELL_W + 2;
const w = pocetDni * CELL_W - 4;
const bar = document.createElement('div');
bar.className = 'gantt-bar blokovano';
bar.style.cssText = `position:absolute;top:${top+4}px;left:${left}px;width:${w}px;height:${ROW_H-8}px;pointer-events:all;cursor:default;display:flex;align-items:center;justify-content:center;overflow:hidden;font-size:11px;`;
bar.title = `Blokováno: ${bl.duvod||'—'}`;
bar.textContent = '🔒 ' + (bl.duvod || 'Blokováno');
overlay.appendChild(bar);
});
});
// Uložit data pro DnD
GANTT_DND.dny = dny;
GANTT_DND.roomW = roomW;
GANTT_DND.CELL_W = CELL_W;
GANTT_DND.ROW_H = ROW_H;
GANTT_DND.headerH = headerH;
}
function ganttPrev() {
APP.ganttOd = new Date(APP.ganttOd.getFullYear(), APP.ganttOd.getMonth() - 1, 1);
APP.ganttDo = new Date(APP.ganttOd.getFullYear(), APP.ganttOd.getMonth() + 1, 0);
nactiGanttData();
}
function ganttNext() {
APP.ganttOd = new Date(APP.ganttOd.getFullYear(), APP.ganttOd.getMonth() + 1, 1);
APP.ganttDo = new Date(APP.ganttOd.getFullYear(), APP.ganttOd.getMonth() + 1, 0);
nactiGanttData();
}
function ganttDnes() {
const d = new Date();
APP.ganttOd = new Date(d.getFullYear(), d.getMonth(), 1);
APP.ganttDo = new Date(d.getFullYear(), d.getMonth() + 1, 0);
nactiGanttData();
}
// ============================================================
// SEZNAM REZERVACÍ
// ============================================================
let rezState = { offset: 0, limit: 25, search: '', stav: '', celkem: 0 };
async function renderRezervace() {
const mc = $('mainContent');
$('topbarRight').innerHTML = `<button class="btn btn-primary" onclick="otevritNovouRezervaci()">+ Nová rezervace</button>`;
mc.innerHTML = `
<div class="card">
<div class="filter-bar">
<input type="search" id="rezSearch" placeholder="Jméno, e-mail, telefon…"
oninput="rezFilter()" style="max-width:260px">
<select id="rezStav" onchange="rezFilter()">
<option value="">Všechny stavy</option>
<option value="potvrzena">Potvrzená</option>
<option value="ceka">Čeká</option>
<option value="zrusena">Zrušená</option>
</select>
<input type="date" id="rezOd" onchange="rezFilter()" title="Příjezd od">
<input type="date" id="rezDo" onchange="rezFilter()" title="Příjezd do">
</div>
<div class="table-wrap">
<table>
<thead><tr>
<th>#</th><th>Host</th><th>Pokoj</th>
<th>Příjezd</th><th>Odjezd</th><th>Nocí</th>
<th>Cena</th><th>Stav</th><th></th>
</tr></thead>
<tbody id="rezTbody"><tr><td colspan="9" class="loading-row"><div class="spinner"></div></td></tr></tbody>
</table>
</div>
<div class="pagination" id="rezPagination"></div>
</div>`;
await nactiRezervace();
}
let rezFilterTimeout;
function rezFilter() {
clearTimeout(rezFilterTimeout);
rezFilterTimeout = setTimeout(() => {
rezState.offset = 0;
nactiRezervace();
}, 350);
}
async function nactiRezervace() {
const params = new URLSearchParams({
search: $('rezSearch')?.value || '',
stav: $('rezStav')?.value || '',
datum_od: $('rezOd')?.value || '',
datum_do: $('rezDo')?.value || '',
limit: rezState.limit,
offset: rezState.offset,
});
const tbody = $('rezTbody');
if (!tbody) return; // Uživatel není v sekci Rezervace — není co obnovovat
const r = await apiFetch('guest', 'seznam', Object.fromEntries(params));
if (!r.ok) {
tbody.innerHTML = `<tr><td colspan="9"><div class="alert alert-error">${esc(r.error)}</div></td></tr>`;
return;
}
rezState.celkem = r.data.celkem;
const rezervace = r.data.rezervace;
if (!rezervace.length) {
tbody.innerHTML = '<tr><td colspan="9" class="loading-row" style="color:var(--gray-400)">Žádné rezervace</td></tr>';
renderRezPaginaci();
return;
}
tbody.innerHTML = rezervace.map(rez => {
const stavBadge = {
potvrzena: '<span class="badge badge-blue">Potvrzená</span>',
ceka: '<span class="badge badge-amber">Čeká</span>',
zrusena: '<span class="badge badge-gray">Zrušená</span>',
}[rez.stav] || rez.stav;
const cena = rez.cena_upravena !== null
? formatCena(rez.cena_upravena)
: formatCena(rez.cena_zaklad);
const slevaIco = rez.sleva_stav === 'navrzena'
? '<span title="Čeká sleva ke schválení" style="color:var(--amber-600)">💰</span> ' : '';
return `<tr>
<td style="color:var(--gray-400)">#${rez.id}</td>
<td><strong>${esc(rez.jmeno + ' ' + rez.prijmeni)}</strong> ${({cs:'🇨🇿',en:'🇬🇧',de:'🇩🇪'})[rez.jazyk] || '🇨🇿'}<br>
<span style="font-size:11px;color:var(--gray-400)">${esc(rez.email)}</span></td>
<td>${esc(rez.pokoj_cislo)} <span style="color:var(--gray-400);font-size:11px">${esc(rez.typ_nazev)}</span></td>
<td>${formatDatumCS(rez.datum_prijezdu)}</td>
<td>${formatDatumCS(rez.datum_odjezdu)}</td>
<td>${rez.pocet_noci}</td>
<td>${slevaIco}${cena}</td>
<td>${stavBadge}</td>
<td>
<div class="td-akce">
<button class="btn btn-secondary btn-sm" onclick="otevritRezervaci(${rez.id})">Detail</button>
</div>
</td>
</tr>`;
}).join('');
renderRezPaginaci();
nactiStatsBadge();
}
function renderRezPaginaci() {
const el = $('rezPagination');
if (!el) return;
const stranky = Math.ceil(rezState.celkem / rezState.limit);
const aktualni = Math.floor(rezState.offset / rezState.limit);
let html = `<button class="page-btn" onclick="rezStrana(${aktualni-1})" ${aktualni===0?'disabled':''}>‹</button>`;
for (let i = 0; i < Math.min(stranky, 7); i++) {
html += `<button class="page-btn ${i===aktualni?'active':''}" onclick="rezStrana(${i})">${i+1}</button>`;
}
html += `<button class="page-btn" onclick="rezStrana(${aktualni+1})" ${aktualni>=stranky-1?'disabled':''}>›</button>`;
html += `<span class="page-info">Celkem: ${rezState.celkem}</span>`;
el.innerHTML = html;
}
function rezStrana(n) {
rezState.offset = n * rezState.limit;
nactiRezervace();
}
// ============================================================
// DETAIL REZERVACE (modal)
// ============================================================
async function otevritRezervaci(id) {
const r = await apiFetch('guest', 'detail', { id });
if (!r.ok) { alert(r.error); return; }
const { rezervace: rez, doplnky, hodnoceni } = r.data;
window._rezDetail = {
typ_id: rez.typ_id, jmeno: rez.jmeno, prijmeni: rez.prijmeni,
email: rez.email, telefon: rez.telefon, pocet_hostu: rez.pocet_hostu,
poznamka: rez.poznamka, na_firmu: rez.na_firmu, firma: rez.firma,
ico: rez.ico, dic: rez.dic, faktura_ulice: rez.faktura_ulice,
faktura_psc: rez.faktura_psc, faktura_mesto: rez.faktura_mesto,
};
const checkin = rez.checkin_cas || APP.nastaveni.checkin_cas || '14:00';
// Zkontrolovat pozdní check-out v doplňcích
let checkout = rez.checkout_cas || APP.nastaveni.checkout_cas || '10:00';
doplnky.forEach(d => {
const n = d.nazev.toLowerCase();
if (n.includes('check-out') || n.includes('checkout') || n.includes('pozdní')) {
const m = d.nazev.match(/(\d{1,2}:\d{2})/);
if (m) checkout = m[1];
}
});
const cenaZaklad = parseFloat(rez.cena_zaklad);
const cenaDoplnky = doplnky.reduce((s,d) => s + parseFloat(d.cena_celkem), 0);
const cenaUpravena= rez.cena_upravena !== null ? parseFloat(rez.cena_upravena) : null;
const slevaSchval = rez.sleva_stav === 'schvalena' ? parseFloat(rez.sleva_castka||0) : 0;
const cenaCelkem = cenaUpravena !== null ? cenaUpravena : cenaZaklad + cenaDoplnky - slevaSchval;
const stavBadge = {
potvrzena: '<span class="badge badge-blue">Potvrzená</span>',
ceka: '<span class="badge badge-amber">Čeká</span>',
zrusena: '<span class="badge badge-gray">Zrušená</span>',
}[rez.stav] || rez.stav;
let slevaHtml = '';
if (rez.sleva_stav === 'navrzena') {
slevaHtml = `<div class="alert alert-warn" style="margin:0 22px 14px">
💰 Čeká sleva ${formatCena(rez.sleva_castka)} — důvod: ${esc(rez.sleva_poznamka)}
${['admin','manazer'].includes(APP.user.role) ? `
<div style="margin-top:8px;display:flex;gap:6px">
<button class="btn btn-success btn-sm" onclick="rozhodnoutSlevu(${id},true)">✓ Schválit</button>
<button class="btn btn-danger btn-sm" onclick="rozhodnoutSlevu(${id},false)">✗ Zamítnout</button>
</div>` : ''}
</div>`;
} else if (rez.sleva_stav === 'schvalena') {
slevaHtml = `<div class="alert alert-ok" style="margin:0 22px 14px">
Sleva ${formatCena(rez.sleva_castka)} schválena. Důvod: ${esc(rez.sleva_poznamka)}
</div>`;
} else if (rez.sleva_stav === 'zamitnuta') {
slevaHtml = `<div class="alert" style="margin:0 22px 14px;background:var(--gray-100);color:var(--gray-600)">
<div><strong>Sleva ${formatCena(rez.sleva_castka)} zamítnuta.</strong></div>
<div style="margin-top:4px">Důvod žádosti (zaměstnanec): "${esc(rez.sleva_poznamka || '—')}"</div>
${rez.sleva_manazer_poznamka ? `<div style="margin-top:4px;font-style:italic">Poznámka manažera: "${esc(rez.sleva_manazer_poznamka)}"</div>` : ''}
</div>`;
}
const mozeEditovat = APP.user.role !== 'zamestnanec' ||
!!(APP.opravneni?.zmena_rezervace);
const mozeRusit = APP.user.role !== 'zamestnanec' ||
!!(APP.opravneni?.storno_rezervace);
const mozeEditovatTermin = APP.user.role === 'admin' || APP.user.role === 'manazer' ||
!!(APP.opravneni?.zmena_terminu);
const doplnkyHtml = `<div id="doplnkyDetail_${rez.id}">
${doplnky.length
? doplnky.map(d => `
<div style="display:flex;align-items:center;justify-content:space-between;padding:8px 10px;margin-bottom:4px;background:var(--green-100);border-radius:6px;border:0.5px solid #bbf7d0">
<div style="display:flex;align-items:center;gap:6px">
<span style="font-size:13px">${
d.nazev.toLowerCase().includes('park') ? '🚗' :
d.nazev.toLowerCase().includes('dobíj') || d.nazev.toLowerCase().includes('elektro') ? '⚡' :
d.nazev.toLowerCase().includes('snídaně') || d.nazev.toLowerCase().includes('snidane') ? '☕' :
d.nazev.toLowerCase().includes('check-out') || d.nazev.toLowerCase().includes('pozdní') ? '🕑' : '•'
}</span>
<div>
<div style="font-size:13px;font-weight:500;color:var(--gray-900)">${esc(d.nazev)}</div>
<div style="font-size:11px;color:var(--gray-500)">${formatCena(d.cena_celkem)}</div>
</div>
</div>
${mozeEditovat ? `<button class="btn btn-sm" style="color:var(--red-600);font-size:11px" onclick="odebratrDoplnek(${rez.id}, ${d.id||0}, '${esc(d.nazev)}')">✕ odebrat</button>` : ''}
</div>`).join('')
: '<div style="font-size:12px;color:var(--gray-400);margin-bottom:8px">Žádné doplňky</div>'}
${mozeEditovat ? `<button class="btn btn-sm btn-secondary" style="width:100%;margin-top:4px" onclick="pridatDoplnek(${rez.id}, '${rez.datum_prijezdu}', '${rez.datum_odjezdu}')">+ Přidat doplněk</button>` : ''}
</div>`;
const hodnoceniHtml = hodnoceni
? '<span style="color:#f59e0b">' + '★'.repeat(hodnoceni.hvezdicky) + '☆'.repeat(5-hodnoceni.hvezdicky) + '</span>'
: '<span style="color:var(--gray-400)">Nezhodnoceno</span>';
const html = `
<div class="modal-overlay" id="rezModal" onclick="if(event.target===this)zavritModal('rezModal')">
<div class="modal" style="max-width:600px">
<div class="modal-header">
<div class="modal-title">Rezervace #${rez.id} ${stavBadge}</div>
<button class="modal-close" onclick="zavritModal('rezModal')">×</button>
</div>
${slevaHtml}
${rez.cas_zadost_stav === 'ceka' ? `
<div class="alert alert-warn" style="margin:0 22px 14px">
🕑 Zaměstnanec žádá o změnu <strong>${rez.cas_zadost_typ === 'checkin' ? 'check-in' : 'check-out'}</strong>
na <strong>${rez.cas_zadost_hodnota}</strong> zdarma.
${['admin','manazer'].includes(APP.user.role) ? `
<div style="margin-top:8px;display:flex;gap:6px">
<button class="btn btn-success btn-sm" onclick="schvalitCas(${id})">✓ Schválit</button>
<button class="btn btn-danger btn-sm" onclick="zamitnoutCas(${id})">✗ Zamítnout</button>
</div>` : '<div style="margin-top:6px;font-size:12px;color:#92400e">Čeká na schválení manažerem.</div>'}
</div>` : ''}
${rez.cas_zadost_stav === 'zamitnuto' ? `
<div class="alert" style="margin:0 22px 14px;background:var(--gray-100);color:var(--gray-600)">
<div><strong>Žádost o změnu ${rez.cas_zadost_typ === 'checkin' ? 'check-in' : 'check-out'} na ${rez.cas_zadost_hodnota} zdarma — zamítnuta.</strong></div>
${rez.cas_zadost_poznamka ? `<div style="margin-top:4px;font-style:italic">Poznámka manažera: "${esc(rez.cas_zadost_poznamka)}"</div>` : ''}
</div>` : ''}
${rez.pokoj_zadost_stav === 'ceka' ? `
<div class="alert alert-warn" style="margin:0 22px 14px">
🔁 Zaměstnanec žádá o přiřazení dražšího pokoje za <strong>${formatCena(rez.cena_upravena !== null ? rez.cena_upravena : rez.cena_zaklad)}</strong>
(odpovídající cena nového typu: <strong>${formatCena(rez.pokoj_zadost_cena_typu)}</strong>).
${['admin','manazer'].includes(APP.user.role) ? `
<div style="margin-top:8px;display:flex;gap:6px">
<button class="btn btn-success btn-sm" onclick="schvalitZmenuPokoje(${id})">✓ Schválit</button>
<button class="btn btn-danger btn-sm" onclick="zamitnoutZmenuPokoje(${id})">✗ Zamítnout</button>
</div>` : '<div style="margin-top:6px;font-size:12px;color:#92400e">Čeká na schválení manažerem.</div>'}
</div>` : ''}
${rez.pokoj_zadost_stav === 'zamitnuto' ? `
<div class="alert" style="margin:0 22px 14px;background:var(--gray-100);color:var(--gray-600)">
<div><strong>Žádost o přiřazení dražšího pokoje za ${formatCena(rez.cena_upravena !== null ? rez.cena_upravena : rez.cena_zaklad)} (cena nového typu: ${formatCena(rez.pokoj_zadost_cena_typu)}) — zamítnuta.</strong></div>
${rez.pokoj_zadost_poznamka ? `<div style="margin-top:4px;font-style:italic">Poznámka manažera: "${esc(rez.pokoj_zadost_poznamka)}"</div>` : ''}
</div>` : ''}
<div class="modal-body" style="display:grid;grid-template-columns:1fr 1fr;gap:20px">
<div>
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px">
<div style="font-size:11px;font-weight:600;letter-spacing:.5px;color:var(--gray-400)">HOST</div>
${mozeEditovat ? `<button class="btn btn-sm btn-secondary" style="font-size:11px;padding:3px 8px" onclick="toggleEditHost(${id})">✏️ Upravit</button>` : ''}
</div>
<div id="hostView_${id}">
<div style="font-size:14px;font-weight:700;margin-bottom:4px;display:flex;align-items:center;gap:6px">
<span>${esc(rez.jmeno + ' ' + rez.prijmeni)}</span>
<span title="Vyplněno v jazyce ${({cs:'CZ',en:'EN',de:'DE'})[rez.jazyk] || 'CZ'}" style="font-size:15px">${({cs:'🇨🇿',en:'🇬🇧',de:'🇩🇪'})[rez.jazyk] || '🇨🇿'}</span>
</div>
<div style="font-size:13px;color:var(--gray-600);">${esc(rez.email)}</div>
<div style="font-size:13px;color:var(--gray-600);">${esc(rez.telefon||'—')}</div>
<div style="font-size:13px;color:var(--gray-400);margin-top:6px">${rez.pocet_hostu} host(é)</div>
${rez.poznamka ? `<div style="font-size:12px;color:var(--gray-500);margin-top:6px;font-style:italic">"${esc(rez.poznamka)}"</div>` : ''}
${rez.faktura_ulice || rez.na_firmu ? `
<div style="margin-top:10px;padding-top:10px;border-top:1px solid var(--gray-100)">
${rez.na_firmu ? `
<div style="font-size:11px;font-weight:600;letter-spacing:.5px;color:var(--gray-400);margin-bottom:6px">FIREMNÍ FAKTURACE</div>
${rez.firma ? `<div style="font-size:13px;font-weight:500">${esc(rez.firma)}</div>` : ''}
<div style="font-size:12px;color:var(--gray-500);margin-top:3px">
${rez.ico ? `IČO: ${esc(rez.ico)}` : ''}
${rez.ico && rez.dic ? ' · ' : ''}
${rez.dic ? `DIČ: ${esc(rez.dic)}` : ''}
</div>
` : `<div style="font-size:11px;font-weight:600;letter-spacing:.5px;color:var(--gray-400);margin-bottom:6px">ADRESA</div>`}
${rez.faktura_ulice ? `<div style="font-size:12px;color:var(--gray-500);margin-top:2px">${esc(rez.faktura_ulice)}, ${esc(rez.faktura_psc||'')} ${esc(rez.faktura_mesto||'')}</div>` : ''}
</div>` : ''}
</div>
<div id="hostEdit_${id}" style="display:none">
<div id="hostEditAlert" style="font-size:12px;margin-bottom:6px"></div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:6px">
<div>
<div style="font-size:10px;color:var(--gray-400);margin-bottom:2px">JMÉNO</div>
<input type="text" id="heJmeno" value="${esc(rez.jmeno)}" style="width:100%;border:1px solid var(--gray-200);border-radius:6px;padding:5px 8px;font-size:13px">
</div>
<div>
<div style="font-size:10px;color:var(--gray-400);margin-bottom:2px">PŘÍJMENÍ</div>
<input type="text" id="hePrijmeni" value="${esc(rez.prijmeni)}" style="width:100%;border:1px solid var(--gray-200);border-radius:6px;padding:5px 8px;font-size:13px">
</div>
</div>
<div style="margin-bottom:6px">
<div style="font-size:10px;color:var(--gray-400);margin-bottom:2px">E-MAIL</div>
<input type="email" id="heEmail" value="${esc(rez.email)}" style="width:100%;border:1px solid var(--gray-200);border-radius:6px;padding:5px 8px;font-size:13px">
</div>
<div style="margin-bottom:6px">
<div style="font-size:10px;color:var(--gray-400);margin-bottom:2px">TELEFON</div>
<input type="tel" id="heTelefon" value="${esc(rez.telefon||'')}" style="width:100%;border:1px solid var(--gray-200);border-radius:6px;padding:5px 8px;font-size:13px">
</div>
<div style="margin-bottom:6px">
<div style="font-size:10px;color:var(--gray-400);margin-bottom:2px">ULICE</div>
<input type="text" id="heUlice" value="${esc(rez.faktura_ulice||'')}" style="width:100%;border:1px solid var(--gray-200);border-radius:6px;padding:5px 8px;font-size:13px">
</div>
<div style="display:grid;grid-template-columns:1fr 2fr;gap:6px;margin-bottom:6px">
<div>
<div style="font-size:10px;color:var(--gray-400);margin-bottom:2px">PSČ</div>
<input type="text" id="hePsc" value="${esc(rez.faktura_psc||'')}" maxlength="6" oninput="formatPsc(this)" style="width:100%;border:1px solid var(--gray-200);border-radius:6px;padding:5px 8px;font-size:13px">
</div>
<div>
<div style="font-size:10px;color:var(--gray-400);margin-bottom:2px">MĚSTO</div>
<input type="text" id="heMesto" value="${esc(rez.faktura_mesto||'')}" style="width:100%;border:1px solid var(--gray-200);border-radius:6px;padding:5px 8px;font-size:13px">
</div>
</div>
<div style="margin-bottom:8px">
<div style="font-size:10px;color:var(--gray-400);margin-bottom:2px">POZNÁMKA</div>
<textarea id="hePoznamka" rows="2" style="width:100%;border:1px solid var(--gray-200);border-radius:6px;padding:5px 8px;font-size:13px;resize:vertical">${esc(rez.poznamka||'')}</textarea>
</div>
<div style="margin-bottom:10px;padding-top:10px;border-top:1px solid var(--gray-100)">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;font-size:13px;margin-bottom:8px">
<input type="checkbox" id="heNaFirmu" ${rez.na_firmu ? 'checked' : ''}
onchange="toggleHeFirma(this.checked)"
style="width:16px;height:16px;cursor:pointer">
<span>Fakturace na firmu</span>
</label>
<div id="heFirmaFields" style="display:${rez.na_firmu ? '' : 'none'}">
<div style="margin-bottom:6px">
<div style="font-size:10px;color:var(--gray-400);margin-bottom:2px">NÁZEV FIRMY</div>
<input type="text" id="heFirma" value="${esc(rez.firma||'')}" style="width:100%;border:1px solid var(--gray-200);border-radius:6px;padding:5px 8px;font-size:13px">
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:6px">
<div>
<div style="font-size:10px;color:var(--gray-400);margin-bottom:2px">IČO</div>
<div style="display:flex;gap:4px">
<input type="text" id="heIco" value="${esc(rez.ico||'')}" maxlength="8"
onblur="nacistAres()"
style="flex:1;border:1px solid var(--gray-200);border-radius:6px;padding:5px 8px;font-size:13px">
</div>
<div id="aresAlert" style="font-size:11px;margin-top:3px"></div>
</div>
<div>
<div style="font-size:10px;color:var(--gray-400);margin-bottom:2px">DIČ</div>
<input type="text" id="heDic" value="${esc(rez.dic||'')}" style="width:100%;border:1px solid var(--gray-200);border-radius:6px;padding:5px 8px;font-size:13px">
</div>
</div>
</div>
</div>
<div style="display:flex;gap:6px">
<button class="btn btn-primary btn-sm" onclick="ulozitHosta(${id})">Uložit</button>
<button class="btn btn-secondary btn-sm" onclick="toggleEditHost(${id})">Zrušit</button>
</div>
</div>
</div>
<div>
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px">
<div style="font-size:11px;font-weight:600;letter-spacing:.5px;color:var(--gray-400)">POBYT</div>
${mozeEditovatTermin && rez.stav !== 'zrusena' ? `<button class="btn btn-sm btn-secondary" style="font-size:11px;padding:3px 8px" onclick="zmenitPokoj(${id})">🔁 Změnit pokoj</button>` : ''}
</div>
<div style="font-size:14px;font-weight:700;margin-bottom:4px">${esc(rez.pokoj_nazev)} <span style="color:var(--gray-400);font-size:12px;font-weight:400">${esc(rez.typ_nazev)}</span></div>
${mozeEditovatTermin ? `
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:6px">
<div>
<div style="font-size:10px;color:var(--gray-400);margin-bottom:3px">PŘÍJEZD</div>
<input type="date" id="editOd" value="${rez.datum_prijezdu}"
style="border:1px solid var(--gray-200);border-radius:6px;padding:5px 8px;font-size:13px;width:100%">
</div>
<div>
<div style="font-size:10px;color:var(--gray-400);margin-bottom:3px">ODJEZD</div>
<input type="date" id="editDo" value="${rez.datum_odjezdu}"
style="border:1px solid var(--gray-200);border-radius:6px;padding:5px 8px;font-size:13px;width:100%">
</div>
</div>
<button class="btn btn-secondary btn-sm" style="margin-bottom:8px" onclick="ulozitTermin(${id})">
Uložit termín
</button>
<div id="terminAlert" style="font-size:12px"></div>
` : `
<div style="font-size:13px;color:var(--gray-600)">
📅 ${formatDatumCS(rez.datum_prijezdu)} → ${formatDatumCS(rez.datum_odjezdu)}
</div>
`}
<div style="font-size:13px;color:var(--gray-600)">${rez.pocet_noci} nocí</div>
${mozeEditovatTermin ? `
<div style="display:flex;gap:10px;margin-top:8px">
<div>
<div style="font-size:10px;color:var(--gray-400)">CHECK-IN</div>
<input type="time" value="${checkin}" id="ciCas"
style="border:1px solid var(--gray-200);border-radius:6px;padding:5px 8px;font-size:13px;width:90px"
onchange="ulozitCheckinOut(${id},'checkin',this.value)">
</div>
<div>
<div style="font-size:10px;color:var(--gray-400)">CHECK-OUT</div>
<input type="time" value="${checkout}" id="coCas"
style="border:1px solid var(--gray-200);border-radius:6px;padding:5px 8px;font-size:13px;width:90px"
onchange="ulozitCheckinOut(${id},'checkout',this.value)">
</div>
</div>` : `
<div style="display:flex;gap:10px;margin-top:8px">
<div>
<div style="font-size:10px;color:var(--gray-400)">CHECK-IN</div>
<div style="font-size:13px;color:var(--gray-700);padding:5px 0">${checkin}</div>
</div>
<div>
<div style="font-size:10px;color:var(--gray-400)">CHECK-OUT</div>
<div style="font-size:13px;color:var(--gray-700);padding:5px 0">${checkout}</div>
</div>
</div>`}
</div>
</div>
<div style="padding:0 22px 14px">
<div style="font-size:11px;font-weight:600;letter-spacing:.5px;color:var(--gray-400);margin-bottom:8px">CENA</div>
<div style="display:flex;justify-content:space-between;font-size:13px;color:var(--gray-600);padding:3px 0">
<span>Pokoj (${rez.pocet_noci} nocí)</span><span>${formatCena(cenaZaklad)}</span>
</div>
${doplnkyHtml}
<div style="display:flex;justify-content:space-between;font-size:15px;font-weight:600;padding:10px 0 0;border-top:1px solid var(--gray-200);margin-top:8px">
<span>Celkem</span><span style="color:var(--blue-600)">${formatCena(cenaCelkem)}</span>
</div>
${(['admin','manazer'].includes(APP.user.role) || APP.opravneni?.hodnoceni) ? `<div style="margin-top:8px">Hodnocení: ${hodnoceniHtml}</div>` : ''}
</div>
<div class="modal-footer" style="flex-wrap:wrap;gap:8px">
${rez.stav !== 'zrusena' && mozeRusit ? `
<button class="btn btn-danger btn-sm" onclick="zrusitRezervaci(${id})">Zrušit rezervaci</button>` : ''}
${rez.stav !== 'zrusena' ? `
<button class="btn btn-secondary btn-sm" onclick="navrhnoutSlevu(${id})">Navrhnout slevu</button>` : ''}
<div style="flex:1"></div>
<button class="btn btn-secondary btn-sm" onclick="zobrazitQR(${id}, ${cenaCelkem}, '${esc(rez.jmeno + ' ' + rez.prijmeni)}')">QR platba</button>
${rez.stav === 'ceka' && (APP.user.role === 'admin' || APP.user.role === 'manazer' || APP.opravneni?.potvrzeni_rezervace) ? `
<button class="btn btn-success" onclick="potvrditRezervaci(${id})">✓ Potvrdit</button>` : ''}
<button class="btn btn-secondary" onclick="zavritModal('rezModal')">Zavřít</button>
</div>
</div>
</div>`;
document.body.insertAdjacentHTML('beforeend', html);
}
async function ulozitTermin(id) {
const od = $('editOd')?.value;
const do_ = $('editDo')?.value;
const alertEl = $('terminAlert');
if (!od || !do_) return;
if (od >= do_) {
if (alertEl) alertEl.innerHTML = '<span style="color:var(--red-600)">Datum odjezdu musí být po příjezdu.</span>';
return;
}
const r = await apiFetch('guest', 'upravit', { id }, {
datum_prijezdu: od, datum_odjezdu: do_,
// Zachovat ostatní údaje — načteme z aktuální rezervace
...window._rezDetail,
});
if (r.ok) {
if (alertEl) alertEl.innerHTML = '<span style="color:var(--green-600)">✓ Termín uložen.</span>';
setTimeout(() => nactiRezervace?.(), 1000);
} else {
if (alertEl) alertEl.innerHTML = `<span style="color:var(--red-600)">${esc(r.error)}</span>`;
}
}
async function ulozitCheckinOut(id, typ, cas) {
const akce = typ === 'checkin' ? 'checkin-cas' : 'checkout-cas';
// Admin/manažer — uložit rovnou
if (APP.user.role !== 'zamestnanec') {
const r = await apiFetch('guest', akce, { id }, { cas });
if (!r.ok) alert(r.error);
return;
}
// Zaměstnanec — nejdřív zjistit zda je čas nestandardní
const r = await apiFetch('guest', akce, { id }, { cas });
if (!r.ok) { alert(r.error); return; }
if (r.data?.nestandardni) {
// Zobrazit dialog s možnostmi
const vychozi = r.data.vychozi;
const typStr = typ === 'checkin' ? 'check-in' : 'check-out';
const smer = typ === 'checkin' ? 'dřívějšího příjezdu' : 'pozdějšího odjezdu';
const modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.id = 'casZadostModal';
modal.innerHTML = `<div class="modal" style="max-width:400px">
<div class="modal-header">
<div class="modal-title">Nestandardní čas ${typStr}</div>
<button class="modal-close" onclick="document.getElementById('casZadostModal').remove();nactiCheckinOut(${id})">×</button>
</div>
<div class="modal-body">
<p style="font-size:13px;color:var(--gray-600);margin-bottom:16px">
Standardní čas ${typStr} je <strong>${vychozi}</strong>. Změna na <strong>${cas}</strong> vyžaduje jednu z akcí:
</p>
<div style="display:flex;flex-direction:column;gap:8px">
<button class="btn btn-primary" onclick="casZadostPridat(${id},'${typ}','${cas}')">
➕ Přidat placenou položku (pozdní ${typStr})
</button>
<button class="btn btn-secondary" onclick="casZadostSchvaleni(${id},'${typ}','${cas}')">
📋 Požádat manažera o schválení zdarma
</button>
<button class="btn btn-secondary" style="color:var(--gray-400)" onclick="document.getElementById('casZadostModal').remove();nactiCheckinOut(${id})">
Zrušit (ponechat ${vychozi})
</button>
</div>
</div>
</div>`;
document.body.appendChild(modal);
}
}
async function casZadostPridat(id, typ, cas) {
document.getElementById('casZadostModal')?.remove();
// Otevřít výběr doplňků
const det = await apiFetch('guest', 'detail', { id });
if (det.ok) {
const rez = det.data.rezervace;
pridatDoplnek(id, rez.datum_prijezdu, rez.datum_odjezdu);
}
}
async function casZadostSchvaleni(id, typ, cas) {
document.getElementById('casZadostModal')?.remove();
const akce = typ === 'checkin' ? 'checkin-cas' : 'checkout-cas';
const r = await apiFetch('guest', akce, { id }, { cas, zadost: 1 });
if (r.ok && r.data?.zadost) {
alert('Žádost odeslána. Manažer obdrží e-mail a může ji schválit nebo zamítnout.');
otevritRezervaci(id);
} else if (!r.ok) {
alert(r.error);
}
}
function nactiCheckinOut(id) {
// Obnovit hodnoty v polích
otevritRezervaci(id);
}
async function schvalitCas(id) {
if (!confirm('Schválit změnu času? Zákazník obdrží e-mail s informací.')) return;
const r = await apiFetch('guest', 'schvalit-cas', { id });
if (r.ok) { zavritModal('rezModal'); otevritRezervaci(id); }
else alert(r.error);
}
async function zamitnoutCas(id) {
if (!confirm('Zamítnout žádost?')) return;
const poznamka = prompt('Poznámka pro zaměstnance (nepovinné):', '') || '';
const r = await apiFetch('guest', 'zamitnout-cas', { id }, { poznamka });
if (r.ok) { zavritModal('rezModal'); otevritRezervaci(id); }
else alert(r.error);
}
async function schvalitZmenuPokoje(id) {
if (!confirm('Schválit přiřazení dražšího pokoje za původní cenu?')) return;
const r = await apiFetch('guest', 'schvalit-pokoj', { id });
if (r.ok) { zavritModal('rezModal'); otevritRezervaci(id); nactiRezervace?.(); renderKalendar?.(); }
else alert(r.error);
}
async function zamitnoutZmenuPokoje(id) {
if (!confirm('Zamítnout žádost? Pokoj zůstane nezměněn.')) return;
const poznamka = prompt('Poznámka pro zaměstnance (nepovinné):', '') || '';
const r = await apiFetch('guest', 'zamitnout-pokoj', { id }, { poznamka });
if (r.ok) { zavritModal('rezModal'); otevritRezervaci(id); }
else alert(r.error);
}
// ============================================================
// QR PLATBA (SPD formát)
// ============================================================
async function zobrazitQR(rezervace_id, castka, jmeno_hosta) {
const nas = APP.nastaveni || {};
const iban = (nas.hotel_iban || '').replace(/\s/g, '').toUpperCase();
if (!iban) {
alert('IBAN účtu není nastaven. Vyplňte ho v Nastavení → Hotel.');
return;
}
if (!/^[A-Z]{2}[0-9A-Z]{4,32}$/.test(iban)) {
alert('IBAN účtu má neplatný formát. Zkontrolujte ho v Nastavení → Hotel.');
return;
}
const castkaFormatovana = parseFloat(castka || 0).toFixed(2);
let vs = ((nas.hotel_vs_prefix || '') + String(rezervace_id).padStart(6, '0')).replace(/[^0-9]/g, '');
if (vs.length > 10) vs = vs.slice(-10); // VS max 10 číslic — ponechat posledních 10
// Zpráva pro příjemce nesmí obsahovat diakritiku ani hvězdičku — SPD ji nepodporuje spolehlivě
const zprava = ('Rezervace ' + rezervace_id)
.normalize('NFD').replace(/[\u0300-\u036f]/g, '') // odstranit diakritiku
.replace(/\*/g, '');
// SPD QR string
const spd = `SPD*1.0*ACC:${iban}*AM:${castkaFormatovana}*CC:CZK*X-VS:${vs}*MSG:${zprava}`;
const qrUrl = `https://api.qrserver.com/v1/create-qr-code/?size=220x220&data=${encodeURIComponent(spd)}`;
const modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.id = 'qrModal';
modal.onclick = e => { if (e.target === modal) modal.remove(); };
modal.innerHTML = `
<div class="modal" style="max-width:340px">
<div class="modal-header">
<div class="modal-title">QR platba — Rezervace #${rezervace_id}</div>
<button class="modal-close" onclick="document.getElementById('qrModal').remove()">×</button>
</div>
<div class="modal-body" style="text-align:center">
<img src="${qrUrl}" alt="QR kód platby" style="width:220px;height:220px;margin:8px auto;display:block;border-radius:8px">
<div style="margin-top:16px;font-size:13px;color:var(--gray-600);line-height:1.8">
<div><strong>Účet:</strong> ${iban.replace(/(.{4})/g,'$1 ').trim()}</div>
<div><strong>Částka:</strong> ${Number(castka).toLocaleString('cs-CZ')} Kč</div>
<div><strong>VS:</strong> ${vs}</div>
<div><strong>Host:</strong> ${jmeno_hosta}</div>
</div>
<p style="font-size:11px;color:var(--gray-400);margin-top:12px">Naskenujte QR kód v bankovní aplikaci</p>
</div>
</div>`;
document.body.appendChild(modal);
}
// ============================================================
// ÚPRAVA DOPLŇKŮ V REZERVACI
// ============================================================
async function odebratrDoplnek(rezervace_id, doplnek_id, nazev) {
if (!confirm(`Odebrat "${nazev}" z rezervace?
Cena bude přepočítána.`)) return;
const r = await apiFetch('guest', 'odebrat-doplnek', { id: rezervace_id }, { doplnek_id, nazev });
if (r.ok) {
zavritModal('rezModal');
otevritRezervaci(rezervace_id);
nactiRezervace?.();
} else alert(r.error);
}
async function pridatDoplnek(rezervace_id, datum_od, datum_do) {
const det = await apiFetch('guest', 'detail', { id: rezervace_id });
const pocet_hostu = det.ok ? det.data.rezervace.pocet_hostu : 1;
const r = await apiFetch('dostupnost', 'doplnky', { datum_od, datum_do, rezervace_id, pocet_hostu });
if (!r.ok) { alert(r.error); return; }
const dostupne = r.data.doplnky.filter(d => d.dostupny);
if (!dostupne.length) { alert('Žádné dostupné doplňky pro tento termín.'); return; }
const volby = dostupne.map(d =>
`<div style="display:flex;align-items:center;justify-content:space-between;padding:10px 12px;margin-bottom:6px;background:var(--gray-50);border-radius:8px;border:0.5px solid var(--gray-200)">
<div>
<div style="font-size:13px;font-weight:500">${esc(d.nazev)}</div>
<div style="font-size:12px;color:var(--gray-500)">${formatCena(d.cena_celkem)}</div>
</div>
<button class="btn btn-sm btn-primary" onclick="potvrditPridatDoplnek(${rezervace_id}, ${d.id}, '${esc(d.nazev)}', ${d.cena}, '${d.typ_uctovani}', ${d.cena_celkem})">Přidat</button>
</div>`
).join('');
const modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.id = 'doplnekModal';
modal.onclick = e => { if (e.target === modal) modal.remove(); };
modal.innerHTML = `<div class="modal" style="max-width:400px">
<div class="modal-header">
<div class="modal-title">Přidat doplněk</div>
<button class="modal-close" onclick="document.getElementById('doplnekModal').remove()">×</button>
</div>
<div class="modal-body">${volby}</div>
</div>`;
document.body.appendChild(modal);
}
async function potvrditPridatDoplnek(rezervace_id, doplnek_id, nazev, cena, typ, cena_celkem) {
document.getElementById('doplnekModal')?.remove();
const r = await apiFetch('guest', 'pridat-doplnek', { id: rezervace_id }, { doplnek_id, nazev, cena, typ_uctovani: typ, cena_celkem });
if (r.ok) {
zavritModal('rezModal');
otevritRezervaci(rezervace_id);
nactiRezervace?.();
} else alert(r.error);
}
async function potvrditRezervaci(id) {
// Nejdřív načíst detail rezervace pro datum a typ
const det = await apiFetch('guest', 'detail', { id });
if (!det.ok) { alert(det.error); return; }
const rez = det.data.rezervace;
// Načíst dostupné pokoje pro daný termín a typ
const dos = await apiFetch('dostupnost', 'pokoje', {
datum_od: rez.datum_prijezdu,
datum_do: rez.datum_odjezdu,
pocet_hostu: rez.pocet_hostu,
typ_id: rez.typ_id,
vyjmout_rezervaci: id,
admin: 1,
});
const pokoje = dos.ok
? dos.data.pokoje.filter(p => p.dostupny && p.min_splnen && p.typ_id == rez.typ_id)
: [];
// Zobrazit modal s výběrem pokoje
const modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.id = 'potvrzeniModal';
modal.onclick = e => { if (e.target === modal) modal.remove(); };
const pokojVolby = pokoje.length
? pokoje.map(p => `
<div style="display:flex;align-items:center;justify-content:space-between;padding:10px 12px;margin-bottom:6px;background:var(--gray-50);border-radius:8px;border:0.5px solid var(--gray-200)">
<div>
<div style="font-size:13px;font-weight:500">Pokoj ${esc(p.cislo)}${p.nazev ? ' — ' + esc(p.nazev) : ''}</div>
<div style="font-size:11px;color:var(--gray-500)">${esc(p.typ_nazev)}</div>
</div>
<button class="btn btn-sm btn-success" onclick="dokonciPotvrzeni(${id}, ${p.id})">Přiřadit</button>
</div>`).join('')
: `<div class="alert alert-warn">Žádný volný pokoj tohoto typu pro daný termín.</div>`;
modal.innerHTML = `<div class="modal" style="max-width:420px">
<div class="modal-header">
<div class="modal-title">Přiřadit pokoj — Rezervace #${id}</div>
<button class="modal-close" onclick="document.getElementById('potvrzeniModal').remove()">×</button>
</div>
<div class="modal-body">
<p style="font-size:13px;color:var(--gray-600);margin-bottom:14px">
Host: <strong>${esc(rez.jmeno + ' ' + rez.prijmeni)}</strong><br>
Typ: <strong>${esc(rez.typ_nazev || '')}</strong><br>
Termín: ${esc(rez.datum_prijezdu)} → ${esc(rez.datum_odjezdu)}
</p>
${pokojVolby}
</div>
</div>`;
document.body.appendChild(modal);
}
// ============================================================
// ZMĚNA POKOJE — krok 1: počet hostů
// ============================================================
async function zmenitPokoj(id) {
const det = await apiFetch('guest', 'detail', { id });
if (!det.ok) { alert(det.error); return; }
const rez = det.data.rezervace;
const modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.id = 'zmenaPokojModal';
modal.onclick = e => { if (e.target === modal) modal.remove(); };
modal.innerHTML = `<div class="modal" style="max-width:380px">
<div class="modal-header">
<div class="modal-title">Změnit pokoj — Rezervace #${id}</div>
<button class="modal-close" onclick="document.getElementById('zmenaPokojModal').remove()">×</button>
</div>
<div class="modal-body">
<p style="font-size:13px;color:var(--gray-600);margin-bottom:14px">
Aktuální pokoj: <strong>${esc(rez.pokoj_nazev)}</strong> (${esc(rez.typ_nazev)})
</p>
<div class="field">
<label>Kolik hostů bude ve výsledku ubytováno?</label>
<input type="number" id="zpPocetHostu" value="${rez.pocet_hostu}" min="1" max="20"
style="width:100%;border:1px solid var(--gray-200);border-radius:6px;padding:8px 10px;font-size:14px">
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="document.getElementById('zmenaPokojModal').remove()">Zrušit</button>
<button class="btn btn-primary" onclick="zmenitPokojKrok2(${id})">Pokračovat</button>
</div>
</div>`;
document.body.appendChild(modal);
}
// ============================================================
// ZMĚNA POKOJE — krok 2: výběr typu a konkrétního pokoje
// ============================================================
async function zmenitPokojKrok2(id) {
const pocetHostu = parseInt($('zpPocetHostu')?.value || 1);
document.getElementById('zmenaPokojModal')?.remove();
const det = await apiFetch('guest', 'detail', { id });
if (!det.ok) { alert(det.error); return; }
const rez = det.data.rezervace;
const dos = await apiFetch('dostupnost', 'pokoje', {
datum_od: rez.datum_prijezdu,
datum_do: rez.datum_odjezdu,
pocet_hostu: pocetHostu,
vyjmout_rezervaci: id,
admin: 1,
});
const vsechnyPokoje = dos.ok ? dos.data.pokoje : [];
const dostupne = vsechnyPokoje.filter(p => p.dostupny && p.min_splnen && p.id != rez.pokoj_id);
// Seskupit podle typu
const podleTypu = {};
dostupne.forEach(p => {
if (!podleTypu[p.typ_id]) podleTypu[p.typ_id] = { typ_nazev: p.typ_nazev, kapacita: p.kapacita, cena_za_noc: p.cena_za_noc, pokoje: [] };
podleTypu[p.typ_id].pokoje.push(p);
});
const pocetNoci = rez.pocet_noci;
const staraCena = rez.cena_upravena !== null ? rez.cena_upravena : rez.cena_zaklad;
// Cena doplňků = staraCena - cena_zaklad (základ pokoje bez doplňků)
const cenaDoplnky = Math.round(staraCena - rez.cena_zaklad);
const aktualniTypId = String(rez.typ_id);
const typySeznam = Object.entries(podleTypu).map(([typId, t]) => {
const malaKapacita = t.kapacita < pocetHostu;
const novaCenaZaklad = Math.round(t.cena_za_noc * pocetNoci);
// Celková nová cena = základ nového pokoje + stávající doplňky (nemění se)
const novaCena = novaCenaZaklad + cenaDoplnky;
const stejnyTyp = typId === aktualniTypId;
const rozdil = novaCena - staraCena;
const rozdilStr = stejnyTyp || rozdil === 0
? 'stejná cena'
: (rozdil > 0 ? `+${formatCena(rozdil)} dráž` : `${formatCena(Math.abs(rozdil))} levněji`);
const rozdilBarva = (stejnyTyp || rozdil === 0) ? 'var(--gray-400)' : (rozdil > 0 ? 'var(--red-600)' : 'var(--green-600)');
return `
<div style="margin-bottom:10px;${malaKapacita ? 'opacity:0.6' : ''}">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:2px">
<div style="font-size:13px;font-weight:600">${esc(t.typ_nazev)}</div>
<div style="font-size:11px;color:var(--gray-500)">${formatCena(novaCena)}</div>
</div>
<div style="font-size:11px;color:${rozdilBarva};margin-bottom:4px;font-weight:500">${rozdilStr}</div>
${malaKapacita ? `<div style="font-size:11px;color:var(--amber-600);margin-bottom:4px">⚠️ Kapacita ${t.kapacita} os. — méně než zadaných ${pocetHostu} hostů</div>` : ''}
${t.pokoje.map(p => `
<div style="display:flex;align-items:center;justify-content:space-between;padding:8px 10px;margin-bottom:4px;background:var(--gray-50);border-radius:6px;border:0.5px solid var(--gray-200)">
<div style="font-size:12px">Pokoj ${esc(p.cislo)}${p.nazev ? ' — ' + esc(p.nazev) : ''}</div>
<button class="btn btn-sm btn-success" onclick="zmenitPokojKrok3(${id}, ${p.id}, ${typId}, '${esc(t.typ_nazev)}', ${novaCenaZaklad}, '${esc(p.cislo)}', '${esc(p.nazev||'')}', ${stejnyTyp})">Vybrat</button>
</div>`).join('')}
</div>`;
}).join('');
const modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.id = 'zmenaPokojModal2';
modal.onclick = e => { if (e.target === modal) modal.remove(); };
modal.innerHTML = `<div class="modal" style="max-width:460px">
<div class="modal-header">
<div class="modal-title">Vyberte pokoj — ${pocetHostu} ${pocetHostu === 1 ? 'host' : pocetHostu < 5 ? 'hosté' : 'hostů'}</div>
<button class="modal-close" onclick="document.getElementById('zmenaPokojModal2').remove()">×</button>
</div>
<div class="modal-body">
${typySeznam || `<div class="alert alert-warn">Žádný volný pokoj pro daný termín.</div>`}
</div>
</div>`;
document.body.appendChild(modal);
}
// ============================================================
// ZMĚNA POKOJE — krok 3: potvrzení s porovnáním ceny
// ============================================================
async function zmenitPokojKrok3(id, novy_pokoj_id, novy_typ_id, novy_typ_nazev, nova_cena_zaklad, novy_pokoj_cislo, novy_pokoj_nazev, stejny_typ) {
document.getElementById('zmenaPokojModal2')?.remove();
const det = await apiFetch('guest', 'detail', { id });
if (!det.ok) { alert(det.error); return; }
const rez = det.data.rezervace;
const stara_cena = rez.cena_upravena !== null ? rez.cena_upravena : rez.cena_zaklad;
const novyPokojLabel = `Pokoj ${esc(novy_pokoj_cislo)}${novy_pokoj_nazev ? ' — ' + esc(novy_pokoj_nazev) : ''}`;
// Pro stejný typ se cena nemění — základ pokoje je stejný, doplňky se nemění
const novaCenaCelkem = nova_cena_zaklad + Math.round(stara_cena - rez.cena_zaklad);
const rozdil = stejny_typ ? 0 : (novaCenaCelkem - stara_cena);
const rozdilStr = rozdil === 0 ? 'Stejná cena' : (rozdil > 0 ? `+${formatCena(rozdil)}` : formatCena(Math.abs(rozdil)) + ' levněji');
const rozdilBarva = rozdil === 0 ? 'var(--gray-500)' : (rozdil > 0 ? 'var(--red-600)' : 'var(--green-600)');
const modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.id = 'zmenaPokojModal3';
modal.onclick = e => { if (e.target === modal) modal.remove(); };
modal.innerHTML = `<div class="modal" style="max-width:380px">
<div class="modal-header">
<div class="modal-title">Potvrdit změnu pokoje</div>
<button class="modal-close" onclick="document.getElementById('zmenaPokojModal3').remove()">×</button>
</div>
<div class="modal-body">
<div style="background:var(--gray-50);border-radius:8px;padding:12px;font-size:13px">
<div style="margin-bottom:8px">
<div style="color:var(--gray-500);font-size:11px;margin-bottom:2px">PŮVODNÍ</div>
<div>${esc(rez.typ_nazev)}, ${esc(rez.pokoj_nazev)} — ${formatCena(stara_cena)}</div>
</div>
<div>
<div style="color:var(--gray-500);font-size:11px;margin-bottom:2px">NOVÝ POKOJ</div>
<div style="font-weight:600;color:var(--blue-600)">${esc(novy_typ_nazev)}, ${novyPokojLabel} — ${formatCena(stejny_typ ? stara_cena : novaCenaCelkem)}</div>
</div>
<div style="border-top:1px solid var(--gray-200);margin-top:8px;padding-top:8px;display:flex;justify-content:space-between">
<span style="color:var(--gray-500)">Rozdíl v ceně:</span>
<span style="font-weight:600;color:${rozdilBarva}">${rozdilStr}</span>
</div>
</div>
${!stejny_typ ? `
<div style="margin-top:12px">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;font-size:13px">
<input type="checkbox" id="zpPrepocitatCenu" checked onchange="zpToggleInfo(${rozdil !== 0}, ${novaCenaCelkem}, ${stara_cena})" style="width:16px;height:16px;cursor:pointer">
<span>Přepočítat cenu dle nového typu</span>
</label>
</div>
${rozdil !== 0 ? `
<div id="zpEmailInfo" class="alert alert-info" style="margin-top:10px;font-size:12px">
✉️ Zákazník dostane e-mail s informací o nové ceně.
</div>` : ''}
${APP.user.role === 'zamestnanec' && novaCenaCelkem > stara_cena ? `
<div id="zpSchvaleniInfo" class="alert alert-warn" style="margin-top:10px;font-size:12px;display:none">
⚠️ Ponecháváte nižší cenu u dražšího typu pokoje — změna bude čekat na schválení manažerem.
</div>` : ''}` : `
<div class="alert alert-ok" style="margin-top:12px;font-size:12px">
✓ Stejný typ pokoje — cena zůstane nezměněna, zákazníkovi nepůjde žádný e-mail.
</div>`}
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="document.getElementById('zmenaPokojModal3').remove()">Zrušit</button>
<button class="btn btn-primary" onclick="dokoncitZmenuPokoje(${id}, ${novy_pokoj_id}, ${stejny_typ ? 'null' : novaCenaCelkem})">Potvrdit změnu</button>
</div>
</div>`;
document.body.appendChild(modal);
}
function zpToggleInfo(rozdilExistuje, novaCena, staraCena) {
const prepocitat = $('zpPrepocitatCenu')?.checked;
const emailEl = document.getElementById('zpEmailInfo');
if (emailEl) emailEl.style.display = (rozdilExistuje && prepocitat) ? '' : 'none';
const schvEl = document.getElementById('zpSchvaleniInfo');
if (schvEl) schvEl.style.display = (!prepocitat && novaCena > staraCena) ? '' : 'none';
}
async function dokoncitZmenuPokoje(rezervace_id, novy_pokoj_id, nova_cena) {
const prepocitat = nova_cena !== null && $('zpPrepocitatCenu')?.checked;
document.getElementById('zmenaPokojModal3')?.remove();
const r = await apiFetch('guest', 'zmenit-pokoj', { id: rezervace_id }, {
pokoj_id: novy_pokoj_id,
nova_cena: prepocitat ? nova_cena : null,
});
if (r.ok) {
if (r.data?.zadost) {
alert('Žádost o změnu pokoje odeslána manažerovi ke schválení.');
}
zavritModal('rezModal');
otevritRezervaci(rezervace_id);
nactiRezervace?.();
renderKalendar?.();
} else {
alert(r.error);
}
}
async function dokonciPotvrzeni(rezervace_id, pokoj_id) {
document.getElementById('potvrzeniModal')?.remove();
const r = await apiFetch('guest', 'potvrdit', { id: rezervace_id }, { pokoj_id });
if (r.ok) {
zavritModal('rezModal');
nactiRezervace?.();
renderKalendar?.();
} else {
alert(r.error);
}
}
async function zrusitRezervaci(id) {
if (!confirm('Opravdu chcete zrušit tuto rezervaci?')) return;
const r = await apiFetch('guest', 'zrusit', { id }, { duvod: 'Zrušeno administrátorem' });
if (r.ok) { zavritModal('rezModal'); nactiRezervace(); }
else alert(r.error);
}
async function rozhodnoutSlevu(id, schvalit) {
let poznamka = '';
if (!schvalit) {
poznamka = prompt('Poznámka pro zaměstnance (nepovinné):', '') || '';
}
const r = await apiFetch('guest', schvalit ? 'schvalit-slevu' : 'zamitnout-slevu', { id }, { poznamka });
if (r.ok) { zavritModal('rezModal'); otevritRezervaci(id); }
else alert(r.error);
}
function navrhnoutSlevu(id) {
const castka = prompt('Výše slevy v Kč:');
if (!castka) return;
const poznamka = prompt('Důvod slevy:');
if (!poznamka) return;
apiFetch('guest', 'navrhnout-slevu', { id }, { castka: parseFloat(castka), poznamka })
.then(r => { if(r.ok) { zavritModal('rezModal'); otevritRezervaci(id); } else alert(r.error); });
}
function otevritNovouRezervaci() {
window.open('/', '_blank');
}
// ============================================================
// ZÁKAZNÍCI
// ============================================================
async function renderZakaznici() {
const mc = $('mainContent');
mc.innerHTML = `
<div class="card">
<div class="filter-bar">
<input type="search" id="zakSearch" placeholder="Jméno, e-mail, telefon…"
oninput="zakFilter()" style="max-width:300px">
</div>
<div class="table-wrap">
<table>
<thead><tr>
<th>Jméno</th><th>E-mail</th><th>Telefon</th>
<th>Pobytů</th><th>Poslední pobyt</th><th>Průměr ⭐</th><th></th>
</tr></thead>
<tbody id="zakTbody"><tr><td colspan="7" class="loading-row"><div class="spinner"></div></td></tr></tbody>
</table>
</div>
<div class="pagination" id="zakPagination"></div>
</div>`;
await nactiZakaznici();
}
let zakFilterTimeout;
function zakFilter() {
clearTimeout(zakFilterTimeout);
zakFilterTimeout = setTimeout(nactiZakaznici, 350);
}
async function nactiZakaznici() {
const tbody = $('zakTbody');
const r = await apiFetch('zakaznici', 'seznam', { search: $('zakSearch')?.value||'' });
if (!r.ok) { tbody.innerHTML = `<tr><td colspan="7"><div class="alert alert-error">${esc(r.error)}</div></td></tr>`; return; }
const zakaznici = r.data.zakaznici;
if (!zakaznici.length) {
tbody.innerHTML = '<tr><td colspan="7" class="loading-row" style="color:var(--gray-400)">Žádní zákazníci</td></tr>';
return;
}
tbody.innerHTML = zakaznici.map(z => `<tr>
<td><strong>${esc(z.jmeno + ' ' + z.prijmeni)}</strong></td>
<td style="font-size:12px">${esc(z.email)}</td>
<td style="font-size:12px">${esc(z.telefon||'—')}</td>
<td>${z.pocet_pobytu||0}</td>
<td style="font-size:12px">${z.posledni_pobyt ? formatDatumCS(z.posledni_pobyt) : '—'}</td>
<td>${z.prumerne_hodnoceni ? '⭐ ' + z.prumerne_hodnoceni : '—'}</td>
<td><button class="btn btn-secondary btn-sm" onclick="detailZakaznika(${z.id})">Detail</button></td>
</tr>`).join('');
}
async function detailZakaznika(id) {
const r = await apiFetch('zakaznici', 'detail', { id });
if (!r.ok) { alert(r.error); return; }
const { zakaznik: z, rezervace, stats } = r.data;
const rezHtml = rezervace.map(rez => `
<tr>
<td>#${rez.id}</td>
<td>${esc(rez.pokoj_cislo)}</td>
<td>${formatDatumCS(rez.datum_prijezdu)}</td>
<td>${rez.pocet_noci} nocí</td>
<td>${{ potvrzena:'Potvrzená', ceka:'Čeká', zrusena:'Zrušená' }[rez.stav]||rez.stav}</td>
<td>${rez.hvezdicky ? '⭐'.repeat(rez.hvezdicky) : '—'}</td>
</tr>`).join('');
const html = `
<div class="modal-overlay" id="zakModal" onclick="if(event.target===this)zavritModal('zakModal')">
<div class="modal" style="max-width:640px">
<div class="modal-header">
<div class="modal-title">${esc(z.jmeno + ' ' + z.prijmeni)}</div>
<button class="modal-close" onclick="zavritModal('zakModal')">×</button>
</div>
<div class="modal-body">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:16px">
<div>
<div style="font-size:12px;color:var(--gray-400)">E-mail</div>
<div>${esc(z.email)}</div>
</div>
<div>
<div style="font-size:12px;color:var(--gray-400)">Telefon</div>
<div>${esc(z.telefon||'—')}</div>
</div>
<div>
<div style="font-size:12px;color:var(--gray-400)">Celkem pobytů</div>
<div>${stats.pocet_pobytu||0}</div>
</div>
<div>
<div style="font-size:12px;color:var(--gray-400)">Průměrné hodnocení</div>
<div>${stats.prumerne_hodnoceni ? '⭐ ' + stats.prumerne_hodnoceni : '—'}</div>
</div>
</div>
${z.poznamka ? `<div class="alert alert-info" style="margin-bottom:14px">${esc(z.poznamka)}</div>` : ''}
<div style="font-size:11px;font-weight:600;letter-spacing:.5px;color:var(--gray-400);margin-bottom:8px">HISTORIE POBYTŮ</div>
<div class="table-wrap">
<table>
<thead><tr><th>#</th><th>Pokoj</th><th>Příjezd</th><th>Nocí</th><th>Stav</th><th>⭐</th></tr></thead>
<tbody>${rezHtml}</tbody>
</table>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="zavritModal('zakModal')">Zavřít</button>
</div>
</div>
</div>`;
document.body.insertAdjacentHTML('beforeend', html);
}
// ============================================================
// HODNOCENÍ
// ============================================================
async function renderHodnoceni() {
const mc = $('mainContent');
const r = await apiFetch('hodnoceni', 'seznam');
if (!r.ok) { mc.innerHTML = `<div class="alert alert-error">${esc(r.error)}</div>`; return; }
const { hodnoceni, prumer, celkem } = r.data;
mc.innerHTML = `
<div class="stat-grid" style="grid-template-columns:repeat(2,1fr);max-width:400px;margin-bottom:20px">
<div class="stat-card">
<div class="stat-label">Průměr</div>
<div class="stat-val blue">${prumer||'—'} <span style="font-size:16px">⭐</span></div>
</div>
<div class="stat-card">
<div class="stat-label">Celkem hodnocení</div>
<div class="stat-val">${celkem}</div>
</div>
</div>
<div class="card">
<div class="table-wrap">
<table>
<thead><tr><th>Host</th><th>Pokoj</th><th>Termín</th><th>Hodnocení</th><th>Datum</th>
${APP.user.role!=='zamestnanec'?'<th></th>':''}
</tr></thead>
<tbody>
${hodnoceni.length ? hodnoceni.map(h => `<tr>
<td>${esc(h.jmeno + ' ' + h.prijmeni)}</td>
<td>${esc(h.pokoj_cislo)}</td>
<td style="font-size:12px">${formatDatumCS(h.datum_prijezdu)} – ${formatDatumCS(h.datum_odjezdu)}</td>
<td><span style="color:#f59e0b">${'★'.repeat(h.hvezdicky)}${'☆'.repeat(5-h.hvezdicky)}</span></td>
<td style="font-size:12px;color:var(--gray-400)">${formatDatumCS(h.vytvoreno.split(' ')[0])}</td>
${APP.user.role!=='zamestnanec'?`<td><button class="btn btn-danger btn-sm" onclick="smazatHodnoceni(${h.id})">Smazat</button></td>`:''}
</tr>`).join('') : '<tr><td colspan="6" class="loading-row" style="color:var(--gray-400)">Žádná hodnocení</td></tr>'}
</tbody>
</table>
</div>
</div>`;
}
async function smazatHodnoceni(id) {
if (!confirm('Smazat toto hodnocení?')) return;
const r = await apiFetch('hodnoceni', 'smazat', { id });
if (r.ok) renderHodnoceni();
else alert(r.error);
}
// ============================================================
// POKOJE
// ============================================================
async function renderPokoje() {
const mc = $('mainContent');
$('topbarRight').innerHTML = `
<button class="btn btn-primary" onclick="otevritPokoj(null)">+ Přidat pokoj</button>`;
const [rPok, rTyp] = await Promise.all([
apiFetch('pokoje', 'seznam'),
apiFetch('typy-pokoju', 'seznam'),
]);
if (!rPok.ok) { mc.innerHTML = `<div class="alert alert-error">${esc(rPok.error)}</div>`; return; }
const pokoje = rPok.data;
const typy = rTyp.ok ? rTyp.data : [];
mc.innerHTML = `
<div class="card" style="margin-bottom:16px">
<div class="card-header">
<div class="card-title">Typy pokojů</div>
<button class="btn btn-secondary btn-sm" onclick="otevritTyp(null)">+ Nový typ</button>
</div>
<div class="table-wrap">
<table>
<thead><tr><th>Název</th><th>Kapacita</th><th>Cena/noc</th><th>Snídaně</th><th>Pokojů</th><th></th></tr></thead>
<tbody>
${typy.map(t => `<tr>
<td><strong>${esc(t.nazev)}</strong></td>
<td>${t.kapacita} os.</td>
<td>${formatCena(t.cena_za_noc)}</td>
<td>${t.snidane_dostupne ? '✓' : '—'}</td>
<td>${t.pocet_aktivnich||0} / ${t.pocet_pokoju||0}</td>
<td class="td-akce">
<button class="btn btn-secondary btn-sm" onclick="otevritTyp(${t.id})">Upravit</button>
<button class="btn btn-danger btn-sm" onclick="smazatTyp(${t.id})">Smazat</button>
</td>
</tr>`).join('') || '<tr><td colspan="6" class="loading-row" style="color:var(--gray-400)">Žádné typy</td></tr>'}
</tbody>
</table>
</div>
</div>
<div class="card">
<div class="card-header"><div class="card-title">Fyzické pokoje</div></div>
<div class="table-wrap">
<table>
<thead><tr><th>Číslo</th><th>Název</th><th>Typ</th><th>Stav</th><th></th></tr></thead>
<tbody>
${pokoje.map(p => `<tr style="${!p.aktivni?'opacity:.5':''}">
<td><strong>${esc(p.cislo)}</strong></td>
<td>${esc(p.nazev||'—')}</td>
<td>${esc(p.typ_nazev)}</td>
<td>${p.aktivni ? '<span class="badge badge-green">Aktivní</span>' : '<span class="badge badge-gray">Neaktivní</span>'}</td>
<td class="td-akce">
<button class="btn btn-secondary btn-sm" onclick="otevritPokoj(${p.id})">Upravit</button>
${p.aktivni
? `<button class="btn btn-danger btn-sm" onclick="aktivovatPokoj(${p.id},false)">Deaktivovat</button>`
: `<button class="btn btn-success btn-sm" onclick="aktivovatPokoj(${p.id},true)">Aktivovat</button>`}
</td>
</tr>`).join('') || '<tr><td colspan="5" class="loading-row" style="color:var(--gray-400)">Žádné pokoje</td></tr>'}
</tbody>
</table>
</div>
</div>`;
}
async function otevritPokoj(id) {
const typy = (await apiFetch('typy-pokoju', 'seznam')).data || [];
let pokoj = { cislo:'', nazev:'', typ_id:'', poradi:0 };
if (id) {
const r = await apiFetch('pokoje', 'detail', { id });
if (r.ok) pokoj = r.data.pokoj;
}
const html = `
<div class="modal-overlay" id="pokojModal" onclick="if(event.target===this)zavritModal('pokojModal')">
<div class="modal">
<div class="modal-header">
<div class="modal-title">${id ? 'Upravit pokoj' : 'Nový pokoj'}</div>
<button class="modal-close" onclick="zavritModal('pokojModal')">×</button>
</div>
<div class="modal-body">
<div id="pokojAlert"></div>
<div class="field-row">
<div class="field">
<label>Číslo pokoje *</label>
<input type="text" id="pokojCislo" value="${esc(pokoj.cislo)}" placeholder="101">
</div>
<div class="field">
<label>Název (volitelný)</label>
<input type="text" id="pokojNazev" value="${esc(pokoj.nazev||'')}" placeholder="Zahradní pokoj">
</div>
</div>
<div class="field">
<label>Typ pokoje *</label>
<select id="pokojTyp">
${typy.map(t => `<option value="${t.id}" ${t.id==pokoj.typ_id?'selected':''}>${esc(t.nazev)}</option>`).join('')}
</select>
</div>
<div class="field">
<label>Pořadí</label>
<input type="number" id="pokojPoradi" value="${pokoj.poradi||0}" min="0">
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="zavritModal('pokojModal')">Zrušit</button>
<button class="btn btn-primary" onclick="ulozitPokoj(${id||0})">Uložit</button>
</div>
</div>
</div>`;
document.body.insertAdjacentHTML('beforeend', html);
}
async function ulozitPokoj(id) {
const data = {
cislo: $('pokojCislo').value.trim(),
nazev: $('pokojNazev').value.trim(),
typ_id: $('pokojTyp').value,
poradi: $('pokojPoradi').value,
};
const r = id
? await apiFetch('pokoje', 'upravit', { id }, data)
: await apiFetch('pokoje', 'vytvorit', {}, data);
if (r.ok) { zavritModal('pokojModal'); renderPokoje(); }
else { $('pokojAlert').innerHTML = `<div class="alert alert-error">${esc(r.error)}</div>`; }
}
async function aktivovatPokoj(id, aktivni) {
const r = await apiFetch('pokoje', aktivni ? 'aktivovat' : 'deaktivovat', { id });
if (r.ok) renderPokoje();
else alert(r.error);
}
async function otevritTyp(id) {
let typ = { nazev:'', kapacita:2, cena_za_noc:'', min_pobyt_noci:1, snidane_dostupne:true, popis:'', poradi:0 };
if (id) {
const r = await apiFetch('typy-pokoju', 'detail', { id });
if (r.ok) typ = r.data.typ;
}
const html = `
<div class="modal-overlay" id="typModal" onclick="if(event.target===this)zavritModal('typModal')">
<div class="modal">
<div class="modal-header">
<div class="modal-title">${id ? 'Upravit typ pokoje' : 'Nový typ pokoje'}</div>
<button class="modal-close" onclick="zavritModal('typModal')">×</button>
</div>
<div class="modal-body">
<div id="typAlert"></div>
<div class="field">
<label>Název (CS) *</label>
<input type="text" id="typNazev" value="${esc(typ.nazev)}" placeholder="Dvoulůžkový Deluxe">
</div>
<div class="field-row">
<div class="field">
<label>Název (EN)</label>
<input type="text" id="typNazevEn" value="${esc(typ.nazev_en||'')}" placeholder="Double room Deluxe">
</div>
<div class="field">
<label>Název (DE)</label>
<input type="text" id="typNazevDe" value="${esc(typ.nazev_de||'')}" placeholder="Doppelzimmer Deluxe">
</div>
</div>
<div class="field-row">
<div class="field">
<label>Kapacita (os.) *</label>
<input type="number" id="typKapacita" value="${typ.kapacita}" min="1" max="20">
</div>
<div class="field">
<label>Cena / noc (Kč) *</label>
<input type="number" id="typCena" value="${typ.cena_za_noc}" min="0" step="50">
</div>
</div>
<div class="field-row">
<div class="field">
<label>Min. pobyt (nocí)</label>
<input type="number" id="typMinNoci" value="${typ.min_pobyt_noci}" min="1">
</div>
<div class="field">
<label>Pořadí</label>
<input type="number" id="typPoradi" value="${typ.poradi||0}" min="0">
</div>
</div>
<div class="field">
<label>Popis (CS)</label>
<textarea id="typPopis">${esc(typ.popis||'')}</textarea>
</div>
<div class="field">
<label>Popis (EN)</label>
<textarea id="typPopisEn">${esc(typ.popis_en||'')}</textarea>
</div>
<div class="field">
<label>Popis (DE)</label>
<textarea id="typPopisDe">${esc(typ.popis_de||'')}</textarea>
</div>
<div class="toggle-wrap">
<button class="toggle ${typ.snidane_dostupne?'on':''}" id="typSnidane"
onclick="this.classList.toggle('on')"></button>
<span class="toggle-label">Nabízet snídaně</span>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="zavritModal('typModal')">Zrušit</button>
<button class="btn btn-primary" onclick="ulozitTyp(${id||0})">Uložit</button>
</div>
</div>
</div>`;
document.body.insertAdjacentHTML('beforeend', html);
}
async function ulozitTyp(id) {
const data = {
nazev: $('typNazev').value.trim(),
nazev_en: $('typNazevEn').value.trim(),
nazev_de: $('typNazevDe').value.trim(),
kapacita: $('typKapacita').value,
cena_za_noc: $('typCena').value,
min_pobyt_noci: $('typMinNoci').value,
popis: $('typPopis').value.trim(),
popis_en: $('typPopisEn').value.trim(),
popis_de: $('typPopisDe').value.trim(),
snidane_dostupne: $('typSnidane').classList.contains('on'),
poradi: $('typPoradi').value,
};
const r = id
? await apiFetch('typy-pokoju', 'upravit', { id }, data)
: await apiFetch('typy-pokoju', 'vytvorit', {}, data);
if (r.ok) { zavritModal('typModal'); renderPokoje(); }
else { $('typAlert').innerHTML = `<div class="alert alert-error">${esc(r.error)}</div>`; }
}
async function smazatTyp(id) {
if (!confirm('Smazat tento typ pokoje? Tuto akci nelze vrátit.')) return;
const r = await apiFetch('typy-pokoju', 'smazat', { id });
if (r.ok) renderPokoje();
else alert(r.error);
}
// ============================================================
// DOPLŇKY
// ============================================================
async function renderDoplnky() {
const mc = $('mainContent');
$('topbarRight').innerHTML = `<button class="btn btn-primary" onclick="otevritDoplnek(null)">+ Přidat doplněk</button>`;
const r = await apiFetch('doplnky', 'seznam');
if (!r.ok) { mc.innerHTML = `<div class="alert alert-error">${esc(r.error)}</div>`; return; }
APP._doplnkySeznam = r.data;
mc.innerHTML = `
<div class="card">
<div class="table-wrap">
<table>
<thead><tr><th>Název</th><th>Cena</th><th>Účtování</th><th>Skupina</th><th>Kapacita</th><th>Stav</th><th></th></tr></thead>
<tbody>
${r.data.map(d => `<tr>
<td><strong>${esc(d.nazev)}</strong></td>
<td>${formatCena(d.cena)}</td>
<td>${{'za_noc':'Za noc', 'za_osobu_noc':'Za osobu / noc', 'jednorazove':'Jednorázově'}[d.typ_uctovani] || 'Jednorázově'}</td>
<td>${d.skupina ? `<span style="font-size:11px;background:var(--gray-100);padding:2px 8px;border-radius:10px;color:var(--gray-600)">${esc(d.skupina)}</span>` : '<span style="color:var(--gray-300)">—</span>'}</td>
<td>${d.kapacita !== null ? d.kapacita + ' míst' : 'Neomezeno'}</td>
<td>${d.aktivni ? '<span class="badge badge-green">Aktivní</span>' : '<span class="badge badge-gray">Neaktivní</span>'}</td>
<td class="td-akce">
<button class="btn btn-secondary btn-sm" onclick="otevritDoplnek(${d.id})">Upravit</button>
${d.aktivni
? `<button class="btn btn-danger btn-sm" onclick="aktivovatDoplnek(${d.id},false)">Deaktivovat</button>`
: `<button class="btn btn-success btn-sm" onclick="aktivovatDoplnek(${d.id},true)">Aktivovat</button>`}
</td>
</tr>`).join('') || '<tr><td colspan="6" class="loading-row" style="color:var(--gray-400)">Žádné doplňky</td></tr>'}
</tbody>
</table>
</div>
</div>`;
}
async function otevritDoplnek(id) {
let d = { nazev:'', cena:0, typ_uctovani:'jednorazove', kapacita:'', poradi:0 };
if (id) {
const r = await apiFetch('doplnky', 'detail', { id });
if (r.ok) d = r.data.doplnek;
}
const html = `
<div class="modal-overlay" id="doplnekModal" onclick="if(event.target===this)zavritModal('doplnekModal')">
<div class="modal">
<div class="modal-header">
<div class="modal-title">${id ? 'Upravit doplněk' : 'Nový doplněk'}</div>
<button class="modal-close" onclick="zavritModal('doplnekModal')">×</button>
</div>
<div class="modal-body">
<div id="doplnekAlert"></div>
<div class="field">
<label>Název (CS) *</label>
<input type="text" id="dpNazev" value="${esc(d.nazev)}" placeholder="Parkování">
</div>
<div class="field-row">
<div class="field">
<label>Název (EN)</label>
<input type="text" id="dpNazevEn" value="${esc(d.nazev_en||'')}" placeholder="Parking">
</div>
<div class="field">
<label>Název (DE)</label>
<input type="text" id="dpNazevDe" value="${esc(d.nazev_de||'')}" placeholder="Parkplatz">
</div>
</div>
<div class="field-row">
<div class="field">
<label>Cena (Kč) *</label>
<input type="number" id="dpCena" value="${d.cena}" min="0" step="10">
</div>
<div class="field">
<label>Typ účtování</label>
<select id="dpTyp">
<option value="jednorazove" ${d.typ_uctovani==='jednorazove'?'selected':''}>Jednorázově</option>
<option value="za_noc" ${d.typ_uctovani==='za_noc'?'selected':''}>Za každou noc</option>
<option value="za_osobu_noc" ${d.typ_uctovani==='za_osobu_noc'?'selected':''}>Za osobu a noc</option>
</select>
</div>
</div>
<div class="field">
<label>Kapacita (prázdné = neomezeno)</label>
<input type="number" id="dpKapacita" value="${d.kapacita||''}" min="0" placeholder="Např. 5 pro parkování">
<div class="field-hint">Zadejte číslo pro kapacitní doplňky (parkování, dobíjení). Prázdné = neomezeno.</div>
</div>
<div class="field">
<label>Skupina (volitelné)</label>
<input type="text" id="dpSkupina" value="${esc(d.skupina||'')}" placeholder="Např. stravovani" list="dpSkupinySeznam">
<datalist id="dpSkupinySeznam">
${[...new Set((APP._doplnkySeznam||[]).map(x=>x.skupina).filter(Boolean))].map(s=>`<option value="${esc(s)}">`).join('')}
</datalist>
<div class="field-hint">Doplňky se stejnou skupinou se zákazníkovi zobrazí jako výběr jedné možnosti (např. Snídaně / Polopenze / All inclusive), ne jako nezávislé zaškrtávátko. Ponechte prázdné pro samostatný doplněk.</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="zavritModal('doplnekModal')">Zrušit</button>
<button class="btn btn-primary" onclick="ulozitDoplnek(${id||0})">Uložit</button>
</div>
</div>
</div>`;
document.body.insertAdjacentHTML('beforeend', html);
}
async function ulozitDoplnek(id) {
const data = {
nazev: $('dpNazev').value.trim(),
nazev_en: $('dpNazevEn').value.trim(),
nazev_de: $('dpNazevDe').value.trim(),
cena: $('dpCena').value,
typ_uctovani: $('dpTyp').value,
kapacita: $('dpKapacita').value || '',
skupina: $('dpSkupina').value.trim(),
};
const r = id
? await apiFetch('doplnky', 'upravit', { id }, data)
: await apiFetch('doplnky', 'vytvorit', {}, data);
if (r.ok) { zavritModal('doplnekModal'); renderDoplnky(); }
else { $('doplnekAlert').innerHTML = `<div class="alert alert-error">${esc(r.error)}</div>`; }
}
async function aktivovatDoplnek(id, aktivni) {
const r = await apiFetch('doplnky', aktivni ? 'aktivovat' : 'deaktivovat', { id });
if (r.ok) renderDoplnky();
else alert(r.error);
}
// ============================================================
// UŽIVATELÉ
// ============================================================
async function renderUzivatele() {
const mc = $('mainContent');
$('topbarRight').innerHTML = `<button class="btn btn-primary" onclick="otevritUzivatele(null)">+ Přidat uživatele</button>`;
const r = await apiFetch('uzivatele', 'seznam');
if (!r.ok) { mc.innerHTML = `<div class="alert alert-error">${esc(r.error)}</div>`; return; }
const roleBadge = { admin:'<span class="badge badge-red">Admin</span>',
manazer:'<span class="badge badge-blue">Manažer</span>',
zamestnanec:'<span class="badge badge-gray">Zaměstnanec</span>' };
mc.innerHTML = `
<div class="card">
<div class="table-wrap">
<table>
<thead><tr><th>Jméno</th><th>Login</th><th>Role</th><th>Stav</th><th>Přihlášení</th><th></th></tr></thead>
<tbody>
${r.data.map(u => `<tr style="${!u.aktivni?'opacity:.5':''}">
<td><strong>${esc(u.jmeno + ' ' + u.prijmeni)}</strong>${u.email?`<br><span style="font-size:11px;color:var(--gray-400)">${esc(u.email)}</span>`:''}</td>
<td style="font-family:monospace;font-size:12px">${esc(u.login)}</td>
<td>${roleBadge[u.role]||u.role}</td>
<td>${u.aktivni?'<span class="badge badge-green">Aktivní</span>':'<span class="badge badge-gray">Blokován</span>'}</td>
<td style="font-size:12px;color:var(--gray-400)">${u.posl_prihlaseni?formatDatumCS(u.posl_prihlaseni.split(' ')[0]):'nikdy'}</td>
<td class="td-akce">
<button class="btn btn-secondary btn-sm" onclick="otevritUzivatele(${u.id})">Upravit</button>
${u.role !== 'admin' ? `
${u.aktivni
? `<button class="btn btn-danger btn-sm" onclick="blokovatUzivatele(${u.id},false)">Blokovat</button>`
: `<button class="btn btn-success btn-sm" onclick="blokovatUzivatele(${u.id},true)">Odblokovat</button>`}
` : ''}
</td>
</tr>`).join('') || '<tr><td colspan="6" class="loading-row" style="color:var(--gray-400)">Žádní uživatelé</td></tr>'}
</tbody>
</table>
</div>
</div>`;
}
async function otevritUzivatele(id) {
let u = { jmeno:'', prijmeni:'', email:'', role:'zamestnanec' };
let opravneni = {};
if (id) {
const r = await apiFetch('uzivatele', 'detail', { id });
if (r.ok) {
u = r.data.uzivatel;
opravneni = Object.fromEntries(r.data.opravneni.map(o => [o.opravneni, o.povoleno]));
}
}
const jeZam = u.role === 'zamestnanec';
const jeManazer = u.role === 'manazer' || u.role === 'admin';
const opHtml = jeZam || !id ? `
<div style="background:var(--gray-50);border-radius:8px;padding:14px;margin-top:4px" id="opravneniWrap">
<div style="font-size:11px;font-weight:600;letter-spacing:.5px;color:var(--gray-400);margin-bottom:10px">OPRÁVNĚNÍ ZAMĚSTNANCE</div>
${[
['kartoteka','👥 Zákazníci'],
['hodnoceni','⭐ Hodnocení'],
['trzby','📋 Přehled'],
['statistiky_modul','📊 Statistiky'],
['zmena_rezervace','✏️ Editace hosta a adresy'],
['zmena_terminu','📅 Změna termínu'],
['potvrzeni_rezervace','✓ Potvrzení rezervace'],
['storno_rezervace','🗑️ Storno rezervace'],
].map(([k,l]) => `
<div class="toggle-wrap" style="margin-bottom:8px">
<button class="toggle ${opravneni[k]?'on':''}" id="op_${k}" onclick="this.classList.toggle('on')"></button>
<span class="toggle-label">${l}</span>
</div>`).join('')}
</div>` : '';
const notifHtml = jeManazer && id ? `
<div style="background:var(--gray-50);border-radius:8px;padding:14px;margin-top:12px">
<div style="font-size:11px;font-weight:600;letter-spacing:.5px;color:var(--gray-400);margin-bottom:10px">📬 E-MAILOVÉ NOTIFIKACE</div>
${[
['notif_nova_rezervace','📥 Nová rezervace'],
['notif_navrh_slevy','💰 Návrh slevy ke schválení'],
['notif_storno','🗑️ Storno rezervace'],
['notif_zadost_cas','🕑 Žádost o změnu check-in/out'],
].map(([k,l]) => `
<div class="toggle-wrap" style="margin-bottom:8px">
<button class="toggle ${u[k]?'on':''}" id="nf_${k}" onclick="this.classList.toggle('on')"></button>
<span class="toggle-label">${l}</span>
</div>`).join('')}
</div>` : '';
const html = `
<div class="modal-overlay" id="uzModal" onclick="if(event.target===this)zavritModal('uzModal')">
<div class="modal">
<div class="modal-header">
<div class="modal-title">${id ? 'Upravit uživatele' : 'Nový uživatel'}</div>
<button class="modal-close" onclick="zavritModal('uzModal')">×</button>
</div>
<div class="modal-body">
<div id="uzAlert"></div>
<div class="field-row">
<div class="field"><label>Jméno *</label><input type="text" id="uzJmeno" value="${esc(u.jmeno)}"></div>
<div class="field"><label>Příjmení *</label><input type="text" id="uzPrijmeni" value="${esc(u.prijmeni)}"></div>
</div>
<div class="field" id="uzLoginPreview" style="${id?'display:none':''}">
<div class="field-hint">Login bude automaticky vygenerován: <strong id="uzLoginText">—</strong></div>
</div>
<div class="field">
<label>Role *</label>
<select id="uzRole" onchange="uzRoleZmena(this.value)" ${u.role==='admin'?'disabled':''}>
${APP.user.role !== 'zamestnanec' ? `<option value="manazer" ${u.role==='manazer'?'selected':''}>Manažer</option>` : ''}
<option value="zamestnanec" ${u.role==='zamestnanec'?'selected':''}>Zaměstnanec</option>
</select>
</div>
<div class="field" id="uzEmailField">
<label>E-mail ${u.role!=='zamestnanec'?'*':''}</label>
<input type="email" id="uzEmail" value="${esc(u.email||'')}">
</div>
${!id ? `
<div class="field">
<label>Heslo *</label>
<input type="password" id="uzHeslo" autocomplete="new-password">
<div class="field-hint">Minimálně 8 znaků.</div>
</div>` : `
<div class="field">
<label>Nové heslo (prázdné = beze změny)</label>
<input type="password" id="uzHeslo" autocomplete="new-password">
</div>`}
${opHtml}
${notifHtml}
</div>
<div class="modal-footer">
${id && u.role !== 'admin' ? `<button class="btn btn-danger" onclick="smazatUzivatele(${id})">Smazat</button>` : ''}
<div style="flex:1"></div>
<button class="btn btn-secondary" onclick="zavritModal('uzModal')">Zrušit</button>
<button class="btn btn-primary" onclick="ulozitUzivatele(${id||0})">Uložit</button>
</div>
</div>
</div>`;
document.body.insertAdjacentHTML('beforeend', html);
// Live preview loginu
if (!id) {
const upd = () => {
const j = ($('uzJmeno')?.value||'').trim().toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g,'').replace(/[^a-z]/g,'');
const p = ($('uzPrijmeni')?.value||'').trim().toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g,'').replace(/[^a-z]/g,'');
const el = $('uzLoginText');
if (el) el.textContent = j && p ? j + '.' + p : '—';
};
$('uzJmeno')?.addEventListener('input', upd);
$('uzPrijmeni')?.addEventListener('input', upd);
}
}
function uzRoleZmena(role) {
const label = document.querySelector('#uzEmailField label');
if (label) label.textContent = 'E-mail' + (role !== 'zamestnanec' ? ' *' : '');
const opWrap = $('opravneniWrap');
if (opWrap) opWrap.style.display = role === 'zamestnanec' ? '' : 'none';
}
async function ulozitUzivatele(id) {
const heslo = $('uzHeslo').value;
const role = $('uzRole').value;
const data = {
jmeno: $('uzJmeno').value.trim(),
prijmeni: $('uzPrijmeni').value.trim(),
email: $('uzEmail').value.trim(),
role,
};
let r;
if (id) {
// Pokud manažer/zaměstnanec upravuje sám sebe, použij vlastni-profil
const jeVlastni = APP.user && APP.user.id === id;
const akce = jeVlastni ? 'vlastni-profil' : 'upravit';
r = await apiFetch('uzivatele', akce, jeVlastni ? {} : { id }, data);
if (r.ok && heslo) {
await apiFetch('uzivatele', 'heslo-reset', { id }, { heslo });
}
// Uložit oprávnění
if (r.ok && role === 'zamestnanec') {
const op = {};
['kartoteka','hodnoceni','trzby','statistiky_modul','zmena_rezervace','zmena_terminu','potvrzeni_rezervace','storno_rezervace'].forEach(k => {
op[k] = $('op_' + k)?.classList.contains('on') || false;
});
await apiFetch('uzivatele', 'opravneni', { id }, op);
}
// Uložit notifikace pro manažera/admina
if (r.ok && (role === 'manazer' || role === 'admin')) {
const notif = {};
['notif_nova_rezervace','notif_navrh_slevy','notif_storno','notif_zadost_cas'].forEach(k => {
notif[k] = $('nf_' + k)?.classList.contains('on') ? 1 : 0;
});
await apiFetch('uzivatele', 'notifikace', { id }, notif);
}
} else {
r = await apiFetch('uzivatele', 'vytvorit', {}, { ...data, heslo });
if (r.ok && role === 'zamestnanec') {
const newId = r.data.id;
const op = {};
['kartoteka','hodnoceni','trzby','statistiky_modul','zmena_rezervace','zmena_terminu','potvrzeni_rezervace','storno_rezervace'].forEach(k => {
op[k] = $('op_' + k)?.classList.contains('on') || false;
});
await apiFetch('uzivatele', 'opravneni', { id: newId }, op);
}
}
if (r.ok) {
if (r.data?.login) alert('Uživatel vytvořen. Login: ' + r.data.login);
zavritModal('uzModal');
renderUzivatele();
} else {
$('uzAlert').innerHTML = `<div class="alert alert-error">${esc(r.error)}</div>`;
}
}
async function blokovatUzivatele(id, aktivovat) {
const r = await apiFetch('uzivatele', aktivovat ? 'aktivovat' : 'blokovat', { id });
if (r.ok) renderUzivatele();
else alert(r.error);
}
async function smazatUzivatele(id) {
if (!confirm('Opravdu smazat tohoto uživatele?')) return;
const r = await apiFetch('uzivatele', 'smazat', { id });
if (r.ok) { zavritModal('uzModal'); renderUzivatele(); }
else alert(r.error);
}
// ============================================================
// NASTAVENÍ
// ============================================================
async function renderNastaveni() {
const mc = $('mainContent');
const r = await apiFetch('nastaveni', 'seznam');
if (!r.ok) { mc.innerHTML = `<div class="alert alert-error">${esc(r.error)}</div>`; return; }
const nas = {};
r.data.forEach(n => { nas[n.klic] = n.hodnota; });
mc.innerHTML = `
<div id="nasAlert" style="margin-bottom:14px"></div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px">
<div class="card">
<div class="card-header"><div class="card-title">Hotel</div></div>
<div class="card-body">
<div class="field"><label>Název hotelu</label><input type="text" id="nas_hotel_nazev" value="${esc(nas.hotel_nazev||'')}"></div>
<div class="field"><label>Telefon</label><input type="tel" id="nas_hotel_telefon" value="${esc(nas.hotel_telefon||'')}"></div>
<div class="field"><label>E-mail</label><input type="email" id="nas_hotel_email" value="${esc(nas.hotel_email||'')}"></div>
<div class="field"><label>Adresa</label><input type="text" id="nas_hotel_adresa" value="${esc(nas.hotel_adresa||'')}"></div>
<div class="field"><label>Webová adresa</label><input type="url" id="nas_hotel_web" value="${esc(nas.hotel_web||'')}"></div>
<div class="field">
<label>IBAN (pro QR platbu)
<span style="font-weight:400;color:var(--gray-400);font-size:11px;margin-left:6px">
— <a href="https://www.penize.cz/kalkulacky/vypocet-iban" target="_blank" style="color:var(--blue-500);text-decoration:none">převést číslo účtu na IBAN</a>
</span>
</label>
<input type="text" id="nas_hotel_iban" placeholder="CZ65 0800 0000 0012 3456 7890" value="${esc(nas.hotel_iban||'')}">
</div>
<div class="field"><label>Variabilní symbol — prefix</label><input type="text" id="nas_hotel_vs_prefix" placeholder="Např. 2026" value="${esc(nas.hotel_vs_prefix||'')}"></div>
<div class="field"><label>Úvodní text (CS)</label><textarea id="nas_uvodni_text">${esc(nas.uvodni_text||'')}</textarea></div>
<div class="field"><label>Úvodní text (EN)</label><textarea id="nas_uvodni_text_en">${esc(nas.uvodni_text_en||'')}</textarea></div>
<div class="field"><label>Úvodní text (DE)</label><textarea id="nas_uvodni_text_de">${esc(nas.uvodni_text_de||'')}</textarea></div>
</div>
</div>
<div>
<div class="card" style="margin-bottom:16px">
<div class="card-header"><div class="card-title">Check-in / Check-out</div></div>
<div class="card-body">
<div class="field-row">
<div class="field"><label>Check-in čas</label><input type="time" id="nas_checkin_cas" value="${esc(nas.checkin_cas||'14:00')}"></div>
<div class="field"><label>Check-out čas</label><input type="time" id="nas_checkout_cas" value="${esc(nas.checkout_cas||'10:00')}"></div>
</div>
</div>
</div>
<div class="card" style="margin-bottom:16px">
<div class="card-header"><div class="card-title">Rezervace</div></div>
<div class="card-body">
<div class="field-row">
<div class="field"><label>Min. dnů předem</label><input type="number" id="nas_rezervace_min_predem_dni" value="${esc(nas.rezervace_min_predem_dni||'1')}" min="0"></div>
<div class="field"><label>Storno zdarma (dní)</label><input type="number" id="nas_rezervace_storno_dni" value="${esc(nas.rezervace_storno_dni||'3')}" min="0"></div>
</div>
</div>
</div>
<div class="card" style="margin-bottom:16px">
<div class="card-header"><div class="card-title">Kapacity doplňků</div></div>
<div class="card-body">
<div class="field-row">
<div class="field"><label>Parkovacích míst</label><input type="number" id="nas_parkovani_kapacita" value="${esc(nas.parkovani_kapacita||'5')}" min="0"></div>
<div class="field"><label>Dobíjecích míst</label><input type="number" id="nas_dobijeni_kapacita" value="${esc(nas.dobijeni_kapacita||'2')}" min="0"></div>
</div>
</div>
</div>
<div class="card" style="margin-bottom:16px">
<div class="card-header"><div class="card-title">Sleva — jednolůžkové obsazení</div></div>
<div class="card-body">
<div class="toggle-wrap" style="margin-bottom:12px">
<button class="toggle ${nas.sleva_jednoluzko_zapnuta==='1'?'on':''}" id="nas_sleva_jednoluzko_zapnuta" onclick="this.classList.toggle('on')"></button>
<span class="toggle-label">Automatická sleva pro 1 hosta ve vícelůžkovém pokoji</span>
</div>
<div class="field">
<label>Výše slevy (%)</label>
<input type="number" id="nas_sleva_jednoluzko_procent" value="${esc(nas.sleva_jednoluzko_procent||'20')}" min="0" max="100" style="width:100px">
</div>
</div>
</div>
<div class="card">
<div class="card-header"><div class="card-title">E-maily</div></div>
<div class="card-body">
<div class="toggle-wrap" style="margin-bottom:12px">
<button class="toggle ${nas.pripominka_zapnuta==='1'?'on':''}" id="nas_pripominka_zapnuta" onclick="this.classList.toggle('on')"></button>
<span class="toggle-label">Připomínka před příjezdem</span>
</div>
<div class="field"><label>Hodin před příjezdem</label><input type="number" id="nas_pripominka_hodin" value="${esc(nas.pripominka_hodin||'48')}" min="1"></div>
<div class="toggle-wrap" style="margin:12px 0">
<button class="toggle ${nas.hodnoceni_zapnuto==='1'?'on':''}" id="nas_hodnoceni_zapnuto" onclick="this.classList.toggle('on')"></button>
<span class="toggle-label">Žádost o hodnocení po odjezdu</span>
</div>
<div class="field"><label>Hodin po odjezdu</label><input type="number" id="nas_hodnoceni_hodin_po" value="${esc(nas.hodnoceni_hodin_po||'24')}" min="1"></div>
<div class="toggle-wrap" style="margin-top:12px">
<button class="toggle ${nas.hodnoceni_verejne==='1'?'on':''}" id="nas_hodnoceni_verejne" onclick="this.classList.toggle('on')"></button>
<span class="toggle-label">Zobrazit hodnocení na rezervační stránce</span>
</div>
</div>
</div>
</div>
</div>
<div style="margin-top:16px;display:flex;justify-content:flex-end">
<button class="btn btn-primary" onclick="ulozitNastaveni()">Uložit nastavení</button>
</div>`;
}
async function ulozitNastaveni() {
const klice = [
'hotel_nazev','hotel_telefon','hotel_email','hotel_adresa','hotel_web',
'uvodni_text','uvodni_text_en','uvodni_text_de','cislo_uctu','checkin_cas','checkout_cas',
'rezervace_min_predem_dni','rezervace_storno_dni',
'hotel_iban','hotel_vs_prefix',
'parkovani_kapacita','dobijeni_kapacita',
'sleva_jednoluzko_procent',
'pripominka_hodin','hodnoceni_hodin_po',
];
const data = {};
klice.forEach(k => {
const el = $('nas_' + k);
if (el) data[k] = el.value;
});
// Toggles
['pripominka_zapnuta','hodnoceni_zapnuto','hodnoceni_verejne','sleva_jednoluzko_zapnuta'].forEach(k => {
const el = $('nas_' + k);
if (el) data[k] = el.classList.contains('on') ? '1' : '0';
});
const r = await apiFetch('nastaveni', 'ulozit', {}, data);
const alert = $('nasAlert');
if (r.ok) {
alert.innerHTML = '<div class="alert alert-ok">Nastavení bylo uloženo.</div>';
setTimeout(() => { alert.innerHTML = ''; }, 3000);
} else {
alert.innerHTML = `<div class="alert alert-error">${esc(r.error)}</div>`;
}
}
// ============================================================
// HESLO
// ============================================================
function otevritHesloModal() {
if (APP.user?.zmena_hesla_zakaz) { alert('Tomuto účtu nelze měnit heslo.'); return; }
$('hesloStare').value = '';
$('hesloNove').value = '';
$('hesloZnovu').value = '';
$('hesloAlert').innerHTML = '';
$('hesloBar').style.width = '0';
$('hesloModal').style.display = 'flex';
}
function hesloSila(heslo) {
let sila = 0;
if (heslo.length >= 8) sila++;
if (/[A-Z]/.test(heslo)) sila++;
if (/[0-9]/.test(heslo)) sila++;
if (/[^A-Za-z0-9]/.test(heslo)) sila++;
const barvy = ['','#ef4444','#f59e0b','#22c55e','#16a34a'];
$('hesloBar').style.width = (sila * 25) + '%';
$('hesloBar').style.background = barvy[sila] || '#ef4444';
}
async function ulozitHeslo() {
const stare = $('hesloStare').value;
const nove = $('hesloNove').value;
const znovu = $('hesloZnovu').value;
const alertEl = $('hesloAlert');
if (!stare || !nove || !znovu) {
alertEl.innerHTML = '<div class="alert alert-error">Vyplňte všechna pole.</div>';
return;
}
if (nove !== znovu) {
alertEl.innerHTML = '<div class="alert alert-error">Nová hesla se neshodují.</div>';
return;
}
const r = await apiFetch('heslo', 'zmenit', {}, {
heslo_stare: stare, heslo_nove: nove, heslo_znovu: znovu,
});
if (r.ok) {
alertEl.innerHTML = '<div class="alert alert-ok">Heslo bylo změněno.</div>';
setTimeout(() => zavritModal('hesloModal'), 1500);
} else {
alertEl.innerHTML = `<div class="alert alert-error">${esc(r.error)}</div>`;
}
}
// ============================================================
// SDÍLENÉ HELPERY
// ============================================================
async function apiFetch(resource, action, params = {}, body = null) {
// PATH_INFO přístup - obejití WAF blokování query stringu
let url = API + '/' + encodeURIComponent(resource) + '/' + encodeURIComponent(action);
let qs = '';
Object.entries(params).forEach(([k,v]) => {
if (v !== undefined && v !== '') qs += (qs ? '&' : '?') + encodeURIComponent(k) + '=' + encodeURIComponent(v);
});
if (qs) url += qs;
const opts = {
credentials: 'include',
headers: { 'X-Session-Token': APP.token || '' },
};
if (body) {
opts.method = 'POST';
opts.headers['Content-Type'] = 'application/json';
opts.body = JSON.stringify(body);
}
const r = await fetch(url, opts);
const text = await r.text();
try {
return JSON.parse(text);
} catch(e) {
console.error('apiFetch:', resource, action, text.substring(0, 200));
return { ok: false, error: 'Chyba serveru. Viz konzole (F12).' };
}
}
function zavritModal(id) {
const el = $(id);
if (el) el.remove();
}
function toggleSidebar() {
$('sidebar').classList.toggle('open');
}
async function nactiStatsBadge() {
try {
const r = await apiFetch('statistiky', 'prehled');
if (r.ok && r.data.cekajici_slevy > 0) {
const badge = $('badgeCekajici');
if (badge) { badge.textContent = r.data.cekajici_slevy; badge.style.display = ''; }
}
} catch(e) {}
}
function formatDatum(d) {
return d.getFullYear() + '-' + String(d.getMonth()+1).padStart(2,'0') + '-' + String(d.getDate()).padStart(2,'0');
}
function formatDatumCS(str) {
if (!str) return '—';
const [r,m,d] = str.split('-');
const mesice = ['','ledna','února','března','dubna','května','června',
'července','srpna','září','října','listopadu','prosince'];
return parseInt(d) + '. ' + mesice[parseInt(m)] + ' ' + r;
}
function formatCena(c) {
return Number(c||0).toLocaleString('cs-CZ') + ' Kč';
}
// Responzivní menu tlačítko
if (window.innerWidth <= 900) {
$('btnMenu').style.display = 'block';
document.addEventListener('click', e => {
const sb = $('sidebar');
if (sb.classList.contains('open') && !sb.contains(e.target) && !$('btnMenu').contains(e.target)) {
sb.classList.remove('open');
}
});
}
// ============================================================
// EDITACE HOSTA
// ============================================================
async function nacistAres() {
const icoEl = document.getElementById('heIco');
const alertEl = document.getElementById('aresAlert');
const ico = (icoEl?.value || '').trim().replace(/[^0-9]/g, '');
if (!ico || ico.length < 6) return; // Tiše ignorovat pokud IČO nevypadá validně
if (alertEl) alertEl.innerHTML = '<span style="color:var(--gray-400)">Načítám z ARES…</span>';
try {
const r = await fetch(`https://ares.gov.cz/ekonomicke-subjekty-v-be/rest/ekonomicke-subjekty/${ico}`);
const text = await r.text();
let d;
try { d = JSON.parse(text); } catch(pe) { throw new Error('Neplatná odpověď ARES'); }
// API může vrátit chybu v poli 'popis' nebo nenajít ico
if (!d || !d.ico) throw new Error('Nenalezen');
// Název firmy
const firma = d.obchodniJmeno || '';
if (firma) { const el = document.getElementById('heFirma'); if (el) el.value = firma; }
// DIČ
const dic = d.dic || '';
if (dic) { const el = document.getElementById('heDic'); if (el) el.value = dic; }
// Adresa - ARES vrací sidlo
const s = d.sidlo || {};
// Ulice: nazevUlice + číslo, nebo nazevObce + číslo pokud ulice chybí
const nazevUlice = s.nazevUlice || '';
const cd = s.cisloDomovni ? String(s.cisloDomovni) : '';
const co = s.cisloOrientacni ? String(s.cisloOrientacni) : '';
const cislo = cd ? (co ? cd + '/' + co : cd) : '';
const ulice = [nazevUlice, cislo].filter(Boolean).join(' ');
if (ulice) { const el = document.getElementById('heUlice'); if (el) el.value = ulice; }
// PSČ
const psc = s.psc ? String(s.psc) : '';
if (psc) {
const el = document.getElementById('hePsc');
if (el) { el.value = psc; if (typeof formatPsc === 'function') formatPsc(el); }
}
// Město - nazevObce je vždy přítomno
const mesto = s.nazevObce || '';
if (mesto) { const el = document.getElementById('heMesto'); if (el) el.value = mesto; }
if (alertEl) alertEl.innerHTML = '<span style="color:var(--green-600)">✓ Načteno z ARES</span>';
setTimeout(() => { if (alertEl) alertEl.innerHTML = ''; }, 3000);
} catch(e) {
// Zobrazit chybu jen pokud IČO vypadá kompletní (8 číslic)
if (ico.length === 8) {
if (alertEl) alertEl.innerHTML = `<span style="color:var(--red-600)">IČO ${ico} nebylo v ARES nalezeno.</span>`;
} else {
if (alertEl) alertEl.innerHTML = '';
}
}
}
function toggleHeFirma(checked) {
const el = document.getElementById('heFirmaFields');
if (el) el.style.display = checked ? '' : 'none';
}
function toggleEditHost(id) {
const view = document.getElementById('hostView_' + id);
const edit = document.getElementById('hostEdit_' + id);
if (!view || !edit) return;
const isEdit = edit.style.display !== 'none';
view.style.display = isEdit ? '' : 'none';
edit.style.display = isEdit ? 'none' : '';
}
async function ulozitHosta(id) {
const jmeno = document.getElementById('heJmeno')?.value.trim();
const prijmeni = document.getElementById('hePrijmeni')?.value.trim();
const email = document.getElementById('heEmail')?.value.trim();
const alertEl = document.getElementById('hostEditAlert');
if (!jmeno || !prijmeni || !email) {
if (alertEl) alertEl.innerHTML = '<span style="color:var(--red-600)">Jméno, příjmení a e-mail jsou povinné.</span>';
return;
}
const naFirmu = document.getElementById('heNaFirmu')?.checked ? 1 : 0;
const r = await apiFetch('guest', 'upravit-hosta', { id }, {
jmeno, prijmeni, email,
telefon: document.getElementById('heTelefon')?.value.trim() || '',
poznamka: document.getElementById('hePoznamka')?.value.trim() || '',
faktura_ulice: document.getElementById('heUlice')?.value.trim() || '',
faktura_psc: document.getElementById('hePsc')?.value.trim() || '',
faktura_mesto: document.getElementById('heMesto')?.value.trim() || '',
na_firmu: naFirmu,
firma: document.getElementById('heFirma')?.value.trim() || '',
ico: document.getElementById('heIco')?.value.trim() || '',
dic: document.getElementById('heDic')?.value.trim() || '',
});
if (r.ok) {
zavritModal('rezModal');
otevritRezervaci(id);
} else {
if (alertEl) alertEl.innerHTML = `<span style="color:var(--red-600)">${esc(r.error)}</span>`;
}
}
// ============================================================
// ESC — zavření modalu
// ============================================================
document.addEventListener('keydown', e => {
if (e.key !== 'Escape') return;
// Zavřít v pořadí: vnořené modaly první
const modaly = ['doplnekModal','potvrzeniModal','qrModal','editRezModal','uzModal','rezModal'];
for (const id of modaly) {
const el = document.getElementById(id);
if (el) { el.remove(); return; }
}
// Modaly zobrazené přes display:flex
const viditelne = ['hesloModal'];
for (const id of viditelne) {
const el = document.getElementById(id);
if (el && el.style.display !== 'none') { el.style.display = 'none'; return; }
}
});
// ============================================================
// START
// ============================================================
init();
// ============================================================
// NOTIFIKACE — polling každých 30 sekund
// Zobrazuje aktuální nevyřízené položky (žluté + fialové)
// ============================================================
let _notifOtevreno = false;
let _notifData = [];
async function nactiNotifikace() {
try {
const r = await apiFetch('notifikace', 'get');
if (!r.ok) { console.error('Notifikace error:', r.error); return; }
_notifData = r.data.notifikace || [];
const pocet = r.data.pocet || 0;
const badge = $('notifBadge');
if (badge) {
badge.style.display = pocet > 0 ? '' : 'none';
badge.textContent = pocet > 9 ? '9+' : pocet;
}
const btn = $('notifBtn');
if (btn) btn.style.color = pocet > 0 ? '#d97706' : 'var(--gray-500)';
if (_notifOtevreno) renderNotifDropdown();
} catch(e) { console.error('Notifikace výjimka:', e); }
}
function renderNotifDropdown() {
const el = $('notifSeznam');
if (!el) return;
if (!_notifData.length) {
el.innerHTML = '<div style="padding:20px;text-align:center;color:var(--gray-400);font-size:13px">Vše vyřízeno ✓</div>';
return;
}
el.innerHTML = _notifData.map(n => `
<div class="notif-item" onclick="notifKlik(${n.rezervace_id})" style="border-left:3px solid ${n.barva}">
<div class="notif-ikona">${n.ikona}</div>
<div>
<div class="notif-text" style="font-weight:500">${esc(n.text)}</div>
<div class="notif-cas" style="color:${n.barva}">${esc(n.popis)}</div>
</div>
</div>`).join('');
}
function notifToggle() {
_notifOtevreno = !_notifOtevreno;
const dd = $('notifDropdown');
if (dd) dd.style.display = _notifOtevreno ? '' : 'none';
if (_notifOtevreno) renderNotifDropdown();
}
function notifKlik(rezervaceId) {
_notifOtevreno = false;
const dd = $('notifDropdown');
if (dd) dd.style.display = 'none';
otevritRezervaci(rezervaceId);
}
// Zavřít dropdown při kliknutí mimo
document.addEventListener('click', e => {
if (_notifOtevreno && !$('notifWrap')?.contains(e.target)) {
_notifOtevreno = false;
const dd = $('notifDropdown');
if (dd) dd.style.display = 'none';
}
});
// Spustit polling po přihlášení
function startNotifPolling() {
nactiNotifikace();
setInterval(nactiNotifikace, 30000);
}
</script>
</body>
</html>
<script>
// ============================================================
// STATISTIKY — grafický přehled
// ============================================================
async function renderStatistiky() {
const mc = $('mainContent');
const rokAkt = new Date().getFullYear();
mc.innerHTML = `
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px;flex-wrap:wrap;gap:10px">
<h2 style="font-size:18px;font-weight:600;color:var(--gray-900);margin:0">Statistiky</h2>
<div style="display:flex;align-items:center;gap:8px">
<label style="font-size:13px;color:var(--gray-600)">Rok:</label>
<select id="statRok" onchange="nactiStatistiky()" style="border:1px solid var(--gray-200);border-radius:8px;padding:6px 10px;font-size:13px">
${[rokAkt-1, rokAkt, rokAkt+1].map(r => `<option value="${r}" ${r===rokAkt?'selected':''}>${r}</option>`).join('')}
</select>
</div>
</div>
<!-- Grafy -->
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:16px">
<div class="card">
<div class="card-header"><div class="card-title">📈 Obsazenost po měsících (%)</div></div>
<div class="card-body" id="grafObsazenost" style="padding:16px 8px">
<div class="loading"><div class="spinner"></div></div>
</div>
</div>
<div class="card">
<div class="card-header"><div class="card-title">💰 Tržby po měsících (Kč)</div></div>
<div class="card-body" id="grafTrzby" style="padding:16px 8px">
<div class="loading"><div class="spinner"></div></div>
</div>
</div>
</div>
<!-- Tabulka -->
<div class="card">
<div class="card-header"><div class="card-title">📋 Přehled po měsících</div></div>
<div class="card-body" id="statTabulka">
<div class="loading"><div class="spinner"></div></div>
</div>
</div>`;
nactiStatistiky();
}
async function nactiStatistiky() {
const rok = parseInt($('statRok')?.value || new Date().getFullYear());
const mesice = ['Led','Úno','Bře','Dub','Kvě','Čvn','Čvc','Srp','Zář','Říj','Lis','Pro'];
// Načíst data paralelně
const [rObs, rTrz] = await Promise.all([
apiFetch('statistiky', 'mesicni', { rok }),
apiFetch('statistiky', 'trzby', { rok }),
]);
if (!rObs.ok || !rTrz.ok) {
$('grafObsazenost').innerHTML = '<div class="alert alert-error">Chyba načtení dat</div>';
return;
}
const obsData = rObs.data.mesice;
const trzData = rTrz.data.mesice;
// Graf obsazenosti
$('grafObsazenost').innerHTML = renderBarChart(
mesice,
obsData.map(m => m.obsazenost),
'#16a34a', '%', 100
);
// Graf tržeb
const maxTrz = Math.max(...trzData.map(m => m.trzby), 1);
$('grafTrzby').innerHTML = renderBarChart(
mesice,
trzData.map(m => m.trzby),
'#2563eb', ' Kč', maxTrz, true
);
// Tabulka
const tbody = mesice.map((m, i) => {
const obs = obsData[i];
const trz = trzData[i];
const jeAktualniMesic = (i + 1) === new Date().getMonth() + 1 && rok === new Date().getFullYear();
return `<tr style="${jeAktualniMesic ? 'background:var(--blue-50)' : ''}">
<td style="padding:8px 12px;font-weight:${jeAktualniMesic?600:400};color:var(--gray-900)">${m} ${rok}</td>
<td style="padding:8px 12px;text-align:center">
<div style="display:flex;align-items:center;gap:8px;justify-content:center">
<div style="width:60px;height:6px;background:var(--gray-100);border-radius:3px">
<div style="width:${obs.obsazenost}%;height:100%;background:#16a34a;border-radius:3px"></div>
</div>
<span style="font-size:13px;color:var(--gray-700);min-width:40px">${obs.obsazenost} %</span>
</div>
</td>
<td style="padding:8px 12px;text-align:right;font-size:13px;color:var(--gray-700)">${trz.pocet_rezervaci}</td>
<td style="padding:8px 12px;text-align:right;font-size:13px;color:var(--gray-700)">${trz.celkem_noci || 0}</td>
<td style="padding:8px 12px;text-align:right;font-size:13px;font-weight:500;color:var(--blue-600)">${Number(trz.trzby).toLocaleString('cs-CZ')} Kč</td>
</tr>`;
}).join('');
const celkemTrz = trzData.reduce((s, m) => s + parseFloat(m.trzby || 0), 0);
const celkemRez = trzData.reduce((s, m) => s + parseInt(m.pocet_rezervaci || 0), 0);
const celkemNoci = trzData.reduce((s, m) => s + parseInt(m.celkem_noci || 0), 0);
const prumObs = obsData.length ? (obsData.reduce((s, m) => s + m.obsazenost, 0) / 12).toFixed(1) : 0;
$('statTabulka').innerHTML = `
<table style="width:100%;border-collapse:collapse">
<thead>
<tr style="border-bottom:2px solid var(--gray-200)">
<th style="padding:8px 12px;text-align:left;font-size:12px;color:var(--gray-500);font-weight:500">Měsíc</th>
<th style="padding:8px 12px;text-align:center;font-size:12px;color:var(--gray-500);font-weight:500">Obsazenost</th>
<th style="padding:8px 12px;text-align:right;font-size:12px;color:var(--gray-500);font-weight:500">Rezervace</th>
<th style="padding:8px 12px;text-align:right;font-size:12px;color:var(--gray-500);font-weight:500">Nocí</th>
<th style="padding:8px 12px;text-align:right;font-size:12px;color:var(--gray-500);font-weight:500">Tržby</th>
</tr>
</thead>
<tbody>${tbody}</tbody>
<tfoot>
<tr style="border-top:2px solid var(--gray-200);background:var(--gray-50)">
<td style="padding:10px 12px;font-weight:600;font-size:13px">Celkem / Průměr</td>
<td style="padding:10px 12px;text-align:center;font-weight:600;font-size:13px">${prumObs} %</td>
<td style="padding:10px 12px;text-align:right;font-weight:600;font-size:13px">${celkemRez}</td>
<td style="padding:10px 12px;text-align:right;font-weight:600;font-size:13px">${celkemNoci}</td>
<td style="padding:10px 12px;text-align:right;font-weight:600;font-size:13px;color:var(--blue-600)">${Number(celkemTrz).toLocaleString('cs-CZ')} Kč</td>
</tr>
</tfoot>
</table>`;
}
function renderBarChart(labels, values, color, suffix, maxVal, formatKc = false) {
const max = maxVal || Math.max(...values, 1);
const w = 56; // šířka sloupce + mezera
const h = 160; // výška grafu
const pad = 8;
const totalW = labels.length * w + pad * 2;
const bars = labels.map((label, i) => {
const val = values[i] || 0;
const barH = Math.round((val / max) * h);
const x = pad + i * w + 4;
const y = h - barH;
const displayVal = formatKc
? (val >= 1000 ? Math.round(val/1000) + 'k' : val)
: val;
return `
<g>
<rect x="${x}" y="${y}" width="${w-8}" height="${barH}"
rx="4" fill="${color}" opacity="0.85"/>
<text x="${x + (w-8)/2}" y="${y - 4}" text-anchor="middle"
font-size="9" fill="var(--gray-600)">${val > 0 ? displayVal + (suffix === ' Kč' ? '' : suffix) : ''}</text>
<text x="${x + (w-8)/2}" y="${h + 14}" text-anchor="middle"
font-size="10" fill="var(--gray-400)">${label}</text>
</g>`;
}).join('');
// Mřížka
const gridLines = [0, 25, 50, 75, 100].filter(p => p <= (suffix === '%' ? 100 : 100)).map(p => {
const y = h - Math.round((p / 100) * h);
const val = suffix === '%' ? p + '%' : '';
return `<line x1="${pad}" y1="${y}" x2="${totalW - pad}" y2="${y}"
stroke="var(--gray-200)" stroke-width="1" stroke-dasharray="4,4"/>
${val ? `<text x="${pad-2}" y="${y+3}" text-anchor="end" font-size="9" fill="var(--gray-300)">${val}</text>` : ''}`;
}).join('');
return `<svg viewBox="0 0 ${totalW} ${h + 30}" style="width:100%;overflow:visible">
${gridLines}
${bars}
</svg>`;
}
</script>
<script>
// ============================================================
// GANTT DRAG & DROP + RESIZE — overlay architektura
// ============================================================
const GANTT_DND = {
active: false,
type: null,
bar: null,
rezId: null,
startX: 0,
origLeft: 0,
origWidth: 0,
origOd: null,
origDo: null,
CELL_W: 44,
dny: [],
roomW: 120,
wasDragged: false,
};
function ganttBarClick(e, id) {
if (GANTT_DND.wasDragged) { GANTT_DND.wasDragged = false; return; }
otevritRezervaci(id);
}
function ganttMoveStart(e, bar) {
const op = APP.opravneni || APP.user?.opravneni || {};
const mozeEdit = APP.user?.role === 'admin' || APP.user?.role === 'manazer' ||
!!(op.zmena_terminu === true || op.zmena_terminu === 1);
if (!mozeEdit) return;
e.preventDefault();
e.stopPropagation();
GANTT_DND.active = true;
GANTT_DND.type = 'move';
GANTT_DND.bar = bar;
GANTT_DND.rezId = parseInt(bar.dataset.rezId);
GANTT_DND.startX = e.clientX;
GANTT_DND.origLeft = parseInt(bar.style.left);
GANTT_DND.origWidth = parseInt(bar.style.width);
GANTT_DND.origOd = bar.dataset.datumOd;
GANTT_DND.origDo = bar.dataset.datumDo;
GANTT_DND.wasDragged = false;
bar.style.cursor = 'grabbing';
bar.style.zIndex = '20';
bar.style.opacity = '0.8';
document.body.style.userSelect = 'none';
}
function ganttResizeStart(e, smer, id) {
e.preventDefault();
e.stopPropagation();
const bar = e.target.closest('.gantt-bar');
if (!bar) return;
GANTT_DND.active = true;
GANTT_DND.type = smer === 'l' ? 'resize-l' : 'resize-r';
GANTT_DND.bar = bar;
GANTT_DND.rezId = parseInt(bar.dataset.rezId);
GANTT_DND.startX = e.clientX;
GANTT_DND.origLeft = parseInt(bar.style.left);
GANTT_DND.origWidth = parseInt(bar.style.width);
GANTT_DND.origOd = bar.dataset.datumOd;
GANTT_DND.origDo = bar.dataset.datumDo;
GANTT_DND.wasDragged = false;
bar.style.cursor = 'col-resize';
bar.style.zIndex = '20';
document.body.style.userSelect = 'none';
}
document.addEventListener('mousemove', e => {
if (!GANTT_DND.active) return;
const dx = e.clientX - GANTT_DND.startX;
const CW = GANTT_DND.CELL_W || 44;
const delta = Math.round(dx / CW);
if (Math.abs(dx) > 4) GANTT_DND.wasDragged = true;
const bar = GANTT_DND.bar;
if (!bar) return;
if (GANTT_DND.type === 'move') {
bar.style.left = (GANTT_DND.origLeft + delta * CW) + 'px';
} else if (GANTT_DND.type === 'resize-r') {
const newW = Math.max(CW - 4, GANTT_DND.origWidth + delta * CW);
bar.style.width = newW + 'px';
} else if (GANTT_DND.type === 'resize-l') {
const newW = Math.max(CW - 4, GANTT_DND.origWidth - delta * CW);
bar.style.width = newW + 'px';
bar.style.left = (GANTT_DND.origLeft + (GANTT_DND.origWidth - newW)) + 'px';
}
});
document.addEventListener('mouseup', async e => {
if (!GANTT_DND.active) return;
GANTT_DND.active = false;
const bar = GANTT_DND.bar;
const CW = GANTT_DND.CELL_W || 44;
const RW = GANTT_DND.roomW || 120;
const dny = GANTT_DND.dny || [];
document.body.style.userSelect = '';
if (!bar) return;
bar.style.cursor = 'grab';
bar.style.zIndex = '5';
bar.style.opacity = '1';
if (!GANTT_DND.wasDragged) return;
// Vypočítat nové datumy z pozice
const newLeft = parseInt(bar.style.left);
const newWidth = parseInt(bar.style.width);
const diStart = Math.round((newLeft - RW) / CW);
const diEnd = Math.round((newLeft - RW + newWidth + 4) / CW);
if (diStart < 0 || diStart >= dny.length || diEnd <= diStart) {
// Vrátit na původní pozici
bar.style.left = GANTT_DND.origLeft + 'px';
bar.style.width = GANTT_DND.origWidth + 'px';
return;
}
const novaOd = dny[Math.max(0, diStart)];
const novaDo = dny[Math.min(dny.length - 1, diEnd)] || dny[dny.length - 1];
if (!novaOd || !novaDo || (novaOd === GANTT_DND.origOd && novaDo === GANTT_DND.origDo)) {
bar.style.left = GANTT_DND.origLeft + 'px';
bar.style.width = GANTT_DND.origWidth + 'px';
return;
}
// Potvrzovací dialog
const origOdFmt = GANTT_DND.origOd ? GANTT_DND.origOd.split('-').reverse().join('.') : '';
const origDoFmt = GANTT_DND.origDo ? GANTT_DND.origDo.split('-').reverse().join('.') : '';
const novaOdFmt = novaOd.split('-').reverse().join('.');
const novaDoFmt = novaDo.split('-').reverse().join('.');
const dniPuvod = Math.round((new Date(GANTT_DND.origDo) - new Date(GANTT_DND.origOd)) / 86400000);
const dniNove = Math.round((new Date(novaDo) - new Date(novaOd)) / 86400000);
let akce = 'změněn termín';
if (dniNove > dniPuvod) akce = 'prodloužen pobyt';
else if (dniNove < dniPuvod) akce = 'zkrácen pobyt';
const potvrdit = await new Promise(resolve => {
const modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.id = 'dndConfirmModal';
modal.innerHTML = `<div class="modal" style="max-width:380px">
<div class="modal-header">
<div class="modal-title">Potvrdit změnu termínu</div>
<button class="modal-close" onclick="document.getElementById('dndConfirmModal').remove()">×</button>
</div>
<div class="modal-body">
<p style="font-size:13px;color:var(--gray-600);margin-bottom:16px">
Bude <strong>${akce}</strong> rezervace:
</p>
<div style="background:var(--gray-50);border-radius:8px;padding:12px;font-size:13px">
<div style="display:flex;justify-content:space-between;margin-bottom:6px">
<span style="color:var(--gray-500)">Původní termín:</span>
<span>${origOdFmt} — ${origDoFmt} (${dniPuvod} nocí)</span>
</div>
<div style="display:flex;justify-content:space-between">
<span style="color:var(--gray-500)">Nový termín:</span>
<span style="font-weight:600;color:var(--blue-600)">${novaOdFmt} — ${novaDoFmt} (${dniNove} nocí)</span>
</div>
</div>
<p style="font-size:12px;color:var(--gray-400);margin-top:12px">Zákazníkovi bude automaticky odeslán e-mail o změně termínu.</p>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="document.getElementById('dndConfirmModal').remove(); window._dndResolve(false)">Zrušit</button>
<button class="btn btn-primary" onclick="document.getElementById('dndConfirmModal').remove(); window._dndResolve(true)">Potvrdit změnu</button>
</div>
</div>`;
window._dndResolve = resolve;
document.body.appendChild(modal);
});
if (!potvrdit) {
bar.style.left = GANTT_DND.origLeft + 'px';
bar.style.width = GANTT_DND.origWidth + 'px';
bar.style.opacity = '1';
return;
}
// Uložit
bar.style.opacity = '0.5';
try {
const det = await apiFetch('guest', 'detail', { id: GANTT_DND.rezId });
if (!det.ok) throw new Error(det.error);
const rez = det.data.rezervace;
const r = await apiFetch('guest', 'upravit', { id: GANTT_DND.rezId }, {
typ_id: rez.typ_id, datum_prijezdu: novaOd, datum_odjezdu: novaDo,
jmeno: rez.jmeno, prijmeni: rez.prijmeni, email: rez.email,
telefon: rez.telefon, pocet_hostu: rez.pocet_hostu,
poznamka: rez.poznamka, na_firmu: rez.na_firmu,
firma: rez.firma, ico: rez.ico, dic: rez.dic,
faktura_ulice: rez.faktura_ulice, faktura_psc: rez.faktura_psc,
faktura_mesto: rez.faktura_mesto,
});
if (r.ok) {
nactiKalendar();
} else {
alert('Nepodařilo se uložit: ' + r.error);
bar.style.left = GANTT_DND.origLeft + 'px';
bar.style.width = GANTT_DND.origWidth + 'px';
bar.style.opacity = '1';
}
} catch(err) {
alert('Chyba: ' + err.message);
nactiKalendar();
}
});
document.addEventListener('mouseleave', () => {
if (!GANTT_DND.active) return;
GANTT_DND.active = false;
const bar = GANTT_DND.bar;
if (bar) {
bar.style.left = GANTT_DND.origLeft + 'px';
bar.style.width = GANTT_DND.origWidth + 'px';
bar.style.opacity = '1';
bar.style.cursor = 'grab';
bar.style.zIndex = '5';
}
document.body.style.userSelect = '';
});
</script>