657 lines
25 KiB
HTML
657 lines
25 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="hu">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Révfülöp · Nyaraló Naptár</title>
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,wght@0,400;0,500;0,600;0,700;1,400&family=Playfair+Display:wght@600;700&display=swap" rel="stylesheet">
|
||
<style>
|
||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||
:root {
|
||
--font-size: 15px;
|
||
--title-size: 32px;
|
||
--calendar-size: 14px;
|
||
--button-size: 14px;
|
||
}
|
||
body {
|
||
font-family: 'DM Sans', sans-serif;
|
||
background: #F5F0EB;
|
||
color: #3A332D;
|
||
min-height: 100vh;
|
||
padding: 20px 16px;
|
||
font-size: var(--font-size);
|
||
}
|
||
.container { max-width: 520px; margin: 0 auto; }
|
||
|
||
/* Header */
|
||
.header { text-align: center; margin-bottom: 28px; }
|
||
.header-sub { font-size: calc(var(--font-size) - 2px); letter-spacing: 0.2em; color: #B0A89E; font-weight: 500; text-transform: uppercase; margin-bottom: 4px; }
|
||
.header h1 { font-family: 'Playfair Display', serif; font-size: var(--title-size); font-weight: 700; color: #3A332D; line-height: 1.2; }
|
||
.header-line { width: 40px; height: 2px; background: #C17F59; margin: 10px auto 0; border-radius: 1px; }
|
||
|
||
/* User bar */
|
||
.user-bar {
|
||
display: flex; align-items: center; justify-content: center; gap: 10px;
|
||
margin-bottom: 18px; padding: 8px 14px; border-radius: 10px;
|
||
background: #FFF; box-shadow: 0 1px 3px rgba(0,0,0,0.04);
|
||
}
|
||
.user-bar-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
|
||
.user-bar-name { font-size: var(--font-size); font-weight: 600; color: #3A332D; }
|
||
.user-bar-label { font-size: calc(var(--font-size) - 2px); color: #B0A89E; }
|
||
.btn-logout {
|
||
margin-left: auto; border: none; background: #F5F0EB; border-radius: 6px;
|
||
padding: 4px 10px; font-size: 11px; font-weight: 500; color: #8B7E74;
|
||
cursor: pointer; font-family: 'DM Sans', sans-serif;
|
||
}
|
||
.btn-logout:hover { background: #EAE3DB; color: #5C524A; }
|
||
|
||
/* Cards */
|
||
.card { background: #FFF; border-radius: 14px; padding: 16px; margin-bottom: 14px; box-shadow: 0 1px 3px rgba(0,0,0,0.04); }
|
||
.card-title { font-size: calc(var(--font-size) - 2px); font-weight: 600; color: #8B7E74; margin-bottom: 10px; text-transform: uppercase; letter-spacing: 0.08em; }
|
||
|
||
/* Controls */
|
||
.controls { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
|
||
.controls select, .controls input {
|
||
padding: 7px 10px; border-radius: 8px; border: 1px solid #E8E0D8;
|
||
background: #FDFBF9; font-size: var(--button-size); font-family: 'DM Sans', sans-serif;
|
||
color: #3A332D; flex: 1; min-width: 100px;
|
||
}
|
||
.hint { font-size: calc(var(--font-size) - 2px); color: #B0A89E; margin-top: 8px; }
|
||
|
||
/* Calendar */
|
||
.cal-nav { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
|
||
.cal-nav button {
|
||
border: none; background: #F5F0EB; border-radius: 8px; width: 36px; height: 36px;
|
||
cursor: pointer; font-size: 18px; color: #5C524A; display: flex; align-items: center; justify-content: center;
|
||
}
|
||
.cal-nav button:hover { background: #EAE3DB; }
|
||
.cal-month { font-size: calc(var(--title-size) * 0.55); font-weight: 600; color: #3A332D; }
|
||
.cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }
|
||
.cal-day-header { text-align: center; padding: 6px 0; font-size: calc(var(--calendar-size) - 1px); font-weight: 600; color: #8B7E74; letter-spacing: 0.05em; }
|
||
.cal-day {
|
||
position: relative; min-height: 48px; padding: 3px; border-radius: 6px;
|
||
background: #FDFBF9; cursor: pointer; border: 1px solid transparent;
|
||
transition: all 0.15s ease;
|
||
}
|
||
.cal-day:hover { background: #F0EAE3; }
|
||
.cal-day.empty { background: transparent; cursor: default; }
|
||
.cal-day.today { border: 2px solid #C17F59; }
|
||
.cal-day.in-selection { background: #D4C5B9; }
|
||
.cal-day-num { font-size: var(--calendar-size); font-weight: 400; color: #5C524A; text-align: right; padding-right: 2px; display: block; }
|
||
.cal-day.today .cal-day-num { font-weight: 700; color: #C17F59; }
|
||
.cal-day-dots { display: flex; flex-wrap: wrap; gap: 2px; margin-top: 2px; }
|
||
.dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
|
||
.dot.planned { opacity: 1; border: 2px dashed; background: transparent !important; }
|
||
|
||
/* Legend */
|
||
.legend { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; margin-bottom: 14px; padding: 8px 0; }
|
||
.legend-item { display: flex; align-items: center; gap: 5px; font-size: calc(var(--font-size) - 2px); color: #5C524A; }
|
||
.legend-dot { width: 10px; height: 10px; border-radius: 50%; }
|
||
.legend-row { width: 100%; display: flex; justify-content: center; gap: 16px; margin-top: 2px; }
|
||
.legend-row span { font-size: calc(var(--font-size) - 3px); color: #B0A89E; }
|
||
|
||
/* Booking list */
|
||
.booking-item {
|
||
display: flex; align-items: center; gap: 10px; padding: 8px 12px;
|
||
border-radius: 8px; background: #FDFBF9; margin-bottom: 6px;
|
||
}
|
||
.booking-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
|
||
.booking-info { flex: 1; min-width: 0; }
|
||
.booking-name { font-size: var(--font-size); font-weight: 600; color: #3A332D; }
|
||
.booking-badge {
|
||
display: inline-block; margin-left: 8px; font-size: calc(var(--font-size) - 3px); font-weight: 500;
|
||
padding: 1px 6px; border-radius: 10px;
|
||
}
|
||
.booking-badge.confirmed { background: #81B29A22; color: #5A9A78; }
|
||
.booking-badge.planned { background: #F2CC8F33; color: #C49A4A; }
|
||
.booking-dates { font-size: calc(var(--font-size) - 2px); color: #8B7E74; margin-top: 2px; }
|
||
.booking-actions button {
|
||
border: none; background: none; cursor: pointer; font-size: 14px; padding: 4px; border-radius: 4px;
|
||
}
|
||
.booking-actions .toggle { color: #B0A89E; }
|
||
.booking-actions .delete { color: #D4756B; }
|
||
.empty-state { text-align: center; padding: 30px; color: #B0A89E; font-style: italic; font-size: var(--font-size); }
|
||
|
||
/* Comments */
|
||
.comments-list { display: flex; flex-direction: column; gap: 8px; margin-bottom: 16px; max-height: 400px; overflow-y: auto; }
|
||
.comment {
|
||
padding: 10px 14px; border-radius: 10px; background: #FDFBF9;
|
||
border-left: 3px solid #ccc; position: relative;
|
||
}
|
||
.comment-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; }
|
||
.comment-author { font-size: calc(var(--font-size) - 1px); font-weight: 600; }
|
||
.comment-time { font-size: calc(var(--font-size) - 3px); color: #B0A89E; }
|
||
.comment-text { font-size: var(--font-size); color: #5C524A; line-height: 1.5; }
|
||
.comment-delete {
|
||
border: none; background: none; cursor: pointer; font-size: 12px;
|
||
color: #D4756B; padding: 2px 4px; border-radius: 4px; margin-left: 8px;
|
||
opacity: 0.6;
|
||
}
|
||
.comment-delete:hover { opacity: 1; }
|
||
.comment-form { display: flex; gap: 8px; align-items: flex-end; }
|
||
.comment-form input {
|
||
flex: 1; padding: 8px 12px; border-radius: 8px; border: 1px solid #E8E0D8;
|
||
background: #FFF; font-size: var(--button-size); font-family: 'DM Sans', sans-serif;
|
||
color: #3A332D; outline: none;
|
||
}
|
||
.comment-form input:focus { border-color: #C17F59; }
|
||
.btn-send {
|
||
padding: 8px 16px; border-radius: 8px; border: none;
|
||
background: #C17F59; color: #FFF; font-size: var(--button-size); font-weight: 600;
|
||
cursor: pointer; font-family: 'DM Sans', sans-serif; white-space: nowrap;
|
||
}
|
||
.btn-send:hover { background: #A96A4A; }
|
||
|
||
/* Login */
|
||
.login-overlay {
|
||
position: fixed; inset: 0; background: #F5F0EB;
|
||
display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 100;
|
||
overflow-y: auto; padding: 20px 16px;
|
||
}
|
||
.login-hero {
|
||
width: 100%; max-width: 420px; margin-bottom: 24px; text-align: center;
|
||
}
|
||
.login-hero img {
|
||
width: 100%; max-width: 380px;
|
||
border-radius: 0;
|
||
filter: drop-shadow(0 4px 12px rgba(0,0,0,0.15));
|
||
}
|
||
.login-tagline {
|
||
font-family: 'Playfair Display', serif;
|
||
font-size: calc(var(--title-size) * 0.7);
|
||
font-weight: 600; color: #3A332D;
|
||
line-height: 1.35; margin: 20px 0 6px;
|
||
font-style: italic;
|
||
}
|
||
.login-tagline-sub {
|
||
font-size: var(--font-size); color: #8B7E74;
|
||
line-height: 1.5; margin-bottom: 4px;
|
||
}
|
||
.login-box { text-align: center; width: 300px; }
|
||
.login-box input, .login-box select {
|
||
padding: 10px 16px; border-radius: 8px; border: 1px solid #E8E0D8;
|
||
font-size: var(--button-size); font-family: 'DM Sans', sans-serif; width: 100%;
|
||
margin-bottom: 10px; text-align: center; background: #FFF; color: #3A332D;
|
||
}
|
||
.login-box .btn-send { display: block; width: 100%; padding: 10px; }
|
||
.login-error { font-size: 12px; color: #D4756B; margin-top: 8px; }
|
||
.login-members { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; margin: 16px 0; }
|
||
.login-member-btn {
|
||
display: flex; align-items: center; gap: 6px;
|
||
padding: 8px 14px; border-radius: 10px; border: 2px solid #E8E0D8;
|
||
background: #FFF; cursor: pointer; font-family: 'DM Sans', sans-serif;
|
||
font-size: var(--button-size); font-weight: 500; color: #5C524A; transition: all 0.15s ease;
|
||
}
|
||
.login-member-btn:hover { border-color: #C17F59; }
|
||
.login-member-btn.selected { border-color: #C17F59; background: #FDF6F0; font-weight: 600; }
|
||
.login-member-dot { width: 10px; height: 10px; border-radius: 50%; }
|
||
|
||
.footer { text-align: center; font-size: 10px; color: #C8BFB5; padding: 0 0 20px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div id="login-screen" class="login-overlay" style="display:none;">
|
||
<div class="login-hero">
|
||
<img src="/hero.png" alt="Révfülöp, Balaton">
|
||
<div class="login-tagline">Ahol a család találkozik,<br>ahol a nyár kezdődik.</div>
|
||
<div class="login-tagline-sub">A révfülöpi nyaraló foglalási naptára</div>
|
||
</div>
|
||
<div class="login-box">
|
||
<div class="header-line" style="margin:0 auto 16px;"></div>
|
||
<div style="font-size:calc(var(--font-size) - 1px);color:#8B7E74;margin-bottom:4px;">Ki vagy?</div>
|
||
<div class="login-members" id="login-members"></div>
|
||
<input type="password" id="login-password" placeholder="Jelszó" onkeydown="if(event.key==='Enter')doLogin()">
|
||
<button class="btn-send" onclick="doLogin()">Belépés</button>
|
||
<div id="login-error" class="login-error"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="app" class="container" style="display:none;">
|
||
<div class="header">
|
||
<div class="header-sub site-subtitle">Révfülöp · Balaton</div>
|
||
<h1 class="site-title">Nyaraló Naptár</h1>
|
||
<div class="header-line"></div>
|
||
</div>
|
||
|
||
<!-- User bar -->
|
||
<div class="user-bar" id="user-bar">
|
||
<div class="user-bar-dot" id="user-bar-dot"></div>
|
||
<span class="user-bar-name" id="user-bar-name"></span>
|
||
<span class="user-bar-label">bejelentkezve</span>
|
||
<button class="btn-logout" onclick="doLogout()">Kijelentkezés</button>
|
||
</div>
|
||
|
||
<!-- New Booking Controls -->
|
||
<div class="card">
|
||
<div class="card-title">Új foglalás</div>
|
||
<div class="controls">
|
||
<select id="sel-status">
|
||
<option value="planned">○ Tervezett</option>
|
||
<option value="confirmed">✓ Megerősített</option>
|
||
</select>
|
||
</div>
|
||
<div class="hint" id="sel-hint">Kattints a kezdő dátumra a naptárban ↓</div>
|
||
</div>
|
||
|
||
<!-- Calendar -->
|
||
<div class="card">
|
||
<div class="cal-nav">
|
||
<button onclick="prevMonth()">‹</button>
|
||
<div class="cal-month" id="cal-month-label"></div>
|
||
<button onclick="nextMonth()">›</button>
|
||
</div>
|
||
<div class="cal-grid" id="cal-grid"></div>
|
||
</div>
|
||
|
||
<!-- Legend -->
|
||
<div class="legend" id="legend"></div>
|
||
|
||
<!-- Bookings List -->
|
||
<div class="card">
|
||
<div class="card-title">Foglalások</div>
|
||
<div id="bookings-list"></div>
|
||
</div>
|
||
|
||
<!-- Comments -->
|
||
<div class="card">
|
||
<div class="card-title">Hozzászólások</div>
|
||
<div class="comments-list" id="comments-list"></div>
|
||
<div class="comment-form">
|
||
<input type="text" id="comment-text" placeholder="Hozzászólás írása..." onkeydown="if(event.key==='Enter')addComment()">
|
||
<button class="btn-send" onclick="addComment()">Küldés</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="footer">revfulop.dooplex.hu</div>
|
||
</div>
|
||
|
||
<script>
|
||
const MONTHS_HU = ["Január","Február","Március","Április","Május","Június","Július","Augusztus","Szeptember","Október","November","December"];
|
||
const DAYS_HU = ["H","K","Sze","Cs","P","Szo","V"];
|
||
|
||
let members = [];
|
||
let bookings = [];
|
||
let comments = [];
|
||
let currentYear, currentMonth;
|
||
let selectionStart = null;
|
||
let isSelecting = false;
|
||
let authToken = localStorage.getItem('revfulop_token') || '';
|
||
let currentMemberId = localStorage.getItem('revfulop_member') || '';
|
||
let selectedLoginMember = '';
|
||
|
||
// API helpers
|
||
async function api(method, path, body) {
|
||
const opts = {
|
||
method,
|
||
headers: { 'Content-Type': 'application/json' },
|
||
};
|
||
if (authToken) opts.headers['X-Auth-Token'] = authToken;
|
||
if (body) opts.body = JSON.stringify(body);
|
||
const res = await fetch('/api' + path, opts);
|
||
if (res.status === 401) {
|
||
authToken = '';
|
||
currentMemberId = '';
|
||
localStorage.removeItem('revfulop_token');
|
||
localStorage.removeItem('revfulop_member');
|
||
showLogin();
|
||
throw new Error('Unauthorized');
|
||
}
|
||
return res.json();
|
||
}
|
||
|
||
// Apply UI config as CSS variables
|
||
async function loadConfig() {
|
||
try {
|
||
const config = await fetch('/api/config').then(r => r.json());
|
||
const root = document.documentElement;
|
||
root.style.setProperty('--font-size', config.fontSize + 'px');
|
||
root.style.setProperty('--title-size', config.titleSize + 'px');
|
||
root.style.setProperty('--calendar-size', config.calendarSize + 'px');
|
||
root.style.setProperty('--button-size', config.buttonSize + 'px');
|
||
// Update site name/subtitle if customized
|
||
document.querySelectorAll('.site-title').forEach(el => el.textContent = config.siteName);
|
||
document.querySelectorAll('.site-subtitle').forEach(el => el.textContent = config.siteSubtitle);
|
||
const taglineEl = document.querySelector('.login-tagline');
|
||
if (taglineEl && config.loginTagline) taglineEl.innerHTML = config.loginTagline;
|
||
const loginSubEl = document.querySelector('.login-tagline-sub');
|
||
if (loginSubEl && config.loginSubtitle) loginSubEl.textContent = config.loginSubtitle;
|
||
} catch (e) {
|
||
console.log('Using default UI config');
|
||
}
|
||
}
|
||
|
||
// Auth
|
||
async function checkAuth() {
|
||
// Load UI config first (no auth needed)
|
||
await loadConfig();
|
||
|
||
// Always load members first (available without auth)
|
||
members = await fetch('/api/members').then(r => r.json());
|
||
buildLoginMembers();
|
||
|
||
const status = await fetch('/api/auth-status').then(r => r.json());
|
||
if (!status.authEnabled) {
|
||
showApp();
|
||
return;
|
||
}
|
||
if (authToken && currentMemberId) {
|
||
try {
|
||
await api('GET', '/me');
|
||
showApp();
|
||
} catch {
|
||
showLogin();
|
||
}
|
||
} else {
|
||
showLogin();
|
||
}
|
||
}
|
||
|
||
function buildLoginMembers() {
|
||
const container = document.getElementById('login-members');
|
||
container.innerHTML = members.map(m =>
|
||
`<button class="login-member-btn" data-id="${m.id}" onclick="selectLoginMember('${m.id}')">
|
||
<div class="login-member-dot" style="background:${m.color}"></div>
|
||
${m.name}
|
||
</button>`
|
||
).join('');
|
||
}
|
||
|
||
function selectLoginMember(id) {
|
||
selectedLoginMember = id;
|
||
document.querySelectorAll('.login-member-btn').forEach(btn => {
|
||
btn.classList.toggle('selected', btn.dataset.id === id);
|
||
});
|
||
}
|
||
|
||
function showLogin() {
|
||
document.getElementById('login-screen').style.display = 'flex';
|
||
document.getElementById('app').style.display = 'none';
|
||
document.getElementById('login-error').textContent = '';
|
||
document.getElementById('login-password').value = '';
|
||
selectedLoginMember = '';
|
||
document.querySelectorAll('.login-member-btn').forEach(b => b.classList.remove('selected'));
|
||
}
|
||
|
||
function showApp() {
|
||
document.getElementById('login-screen').style.display = 'none';
|
||
document.getElementById('app').style.display = 'block';
|
||
updateUserBar();
|
||
init();
|
||
}
|
||
|
||
function updateUserBar() {
|
||
const member = members.find(m => m.id === currentMemberId);
|
||
const bar = document.getElementById('user-bar');
|
||
if (!member) {
|
||
bar.style.display = 'none';
|
||
return;
|
||
}
|
||
bar.style.display = 'flex';
|
||
document.getElementById('user-bar-dot').style.background = member.color;
|
||
document.getElementById('user-bar-name').textContent = member.name;
|
||
}
|
||
|
||
async function doLogin() {
|
||
if (!selectedLoginMember) {
|
||
document.getElementById('login-error').textContent = 'Válassz egy családtagot!';
|
||
return;
|
||
}
|
||
const pw = document.getElementById('login-password').value;
|
||
try {
|
||
const res = await fetch('/api/login', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ password: pw, memberId: selectedLoginMember }),
|
||
}).then(r => r.json());
|
||
if (res.success) {
|
||
authToken = res.token;
|
||
currentMemberId = res.memberId;
|
||
localStorage.setItem('revfulop_token', authToken);
|
||
localStorage.setItem('revfulop_member', currentMemberId);
|
||
showApp();
|
||
} else {
|
||
document.getElementById('login-error').textContent = res.error || 'Hibás jelszó';
|
||
}
|
||
} catch {
|
||
document.getElementById('login-error').textContent = 'Hiba történt';
|
||
}
|
||
}
|
||
|
||
function doLogout() {
|
||
authToken = '';
|
||
currentMemberId = '';
|
||
localStorage.removeItem('revfulop_token');
|
||
localStorage.removeItem('revfulop_member');
|
||
showLogin();
|
||
}
|
||
|
||
// Init
|
||
async function init() {
|
||
const now = new Date();
|
||
currentYear = now.getFullYear();
|
||
currentMonth = now.getMonth();
|
||
|
||
await loadData();
|
||
|
||
// Legend
|
||
const legend = document.getElementById('legend');
|
||
legend.innerHTML = members.map(m =>
|
||
`<div class="legend-item"><div class="legend-dot" style="background:${m.color}"></div>${m.name}</div>`
|
||
).join('') + `
|
||
<div class="legend-row">
|
||
<div class="legend-item"><div class="legend-dot" style="background:transparent;border:2px dashed #999;box-sizing:border-box;"></div><span>tervezett</span></div>
|
||
<div class="legend-item"><div class="legend-dot" style="background:#999"></div><span>megerősített</span></div>
|
||
</div>`;
|
||
|
||
render();
|
||
}
|
||
|
||
async function loadData() {
|
||
bookings = await api('GET', '/bookings');
|
||
comments = await api('GET', '/comments');
|
||
}
|
||
|
||
function render() {
|
||
renderCalendar();
|
||
renderBookings();
|
||
renderComments();
|
||
}
|
||
|
||
// Calendar
|
||
function prevMonth() { if (currentMonth === 0) { currentMonth = 11; currentYear--; } else currentMonth--; renderCalendar(); }
|
||
function nextMonth() { if (currentMonth === 11) { currentMonth = 0; currentYear++; } else currentMonth++; renderCalendar(); }
|
||
|
||
function dateKey(y, m, d) {
|
||
return `${y}-${String(m+1).padStart(2,'0')}-${String(d).padStart(2,'0')}`;
|
||
}
|
||
|
||
function inRange(dk, start, end) { return dk >= start && dk <= end; }
|
||
|
||
function renderCalendar() {
|
||
document.getElementById('cal-month-label').textContent = `${MONTHS_HU[currentMonth]} ${currentYear}`;
|
||
|
||
const firstDay = new Date(currentYear, currentMonth, 1);
|
||
const lastDay = new Date(currentYear, currentMonth + 1, 0);
|
||
const startOffset = (firstDay.getDay() + 6) % 7;
|
||
const today = new Date(); today.setHours(0,0,0,0);
|
||
const todayKey = dateKey(today.getFullYear(), today.getMonth(), today.getDate());
|
||
|
||
let html = DAYS_HU.map(d => `<div class="cal-day-header">${d}</div>`).join('');
|
||
|
||
for (let i = 0; i < startOffset; i++) html += '<div class="cal-day empty"></div>';
|
||
|
||
for (let d = 1; d <= lastDay.getDate(); d++) {
|
||
const dk = dateKey(currentYear, currentMonth, d);
|
||
const isToday = dk === todayKey;
|
||
const dayBookings = bookings.filter(b => inRange(dk, b.start_date, b.end_date));
|
||
|
||
let classes = 'cal-day';
|
||
if (isToday) classes += ' today';
|
||
|
||
const dots = dayBookings.map(b => {
|
||
const m = members.find(x => x.id === b.member_id);
|
||
const color = m ? m.color : '#999';
|
||
if (b.status === 'planned') {
|
||
return `<div class="dot planned" style="border-color:${color}" title="${m?.name || ''} (tervezett)"></div>`;
|
||
}
|
||
return `<div class="dot" style="background:${color}" title="${m?.name || ''} (megerősített)"></div>`;
|
||
}).join('');
|
||
|
||
html += `<div class="${classes}" onclick="dayClick('${dk}')" onmouseenter="dayHover('${dk}')">
|
||
<span class="cal-day-num">${d}</span>
|
||
<div class="cal-day-dots">${dots}</div>
|
||
</div>`;
|
||
}
|
||
|
||
document.getElementById('cal-grid').innerHTML = html;
|
||
}
|
||
|
||
function dayClick(dk) {
|
||
if (!isSelecting) {
|
||
selectionStart = dk;
|
||
isSelecting = true;
|
||
document.getElementById('sel-hint').textContent = '📍 Kattints a befejező dátumra (ESC = mégse)';
|
||
highlightSelection(dk, dk);
|
||
} else {
|
||
const start = selectionStart < dk ? selectionStart : dk;
|
||
const end = selectionStart < dk ? dk : selectionStart;
|
||
isSelecting = false;
|
||
selectionStart = null;
|
||
document.getElementById('sel-hint').textContent = 'Kattints a kezdő dátumra a naptárban ↓';
|
||
addBooking(start, end);
|
||
}
|
||
}
|
||
|
||
function dayHover(dk) {
|
||
if (isSelecting && selectionStart) {
|
||
highlightSelection(selectionStart, dk);
|
||
}
|
||
}
|
||
|
||
function highlightSelection(s, e) {
|
||
const start = s < e ? s : e;
|
||
const end = s < e ? e : s;
|
||
document.querySelectorAll('.cal-day').forEach(el => {
|
||
el.classList.remove('in-selection');
|
||
const numEl = el.querySelector('.cal-day-num');
|
||
if (!numEl) return;
|
||
const d = parseInt(numEl.textContent);
|
||
const dk = dateKey(currentYear, currentMonth, d);
|
||
if (inRange(dk, start, end)) el.classList.add('in-selection');
|
||
});
|
||
}
|
||
|
||
// Bookings
|
||
async function addBooking(start, end) {
|
||
const status = document.getElementById('sel-status').value;
|
||
await api('POST', '/bookings', { start_date: start, end_date: end, status });
|
||
await loadData();
|
||
render();
|
||
}
|
||
|
||
async function toggleBooking(id) {
|
||
const b = bookings.find(x => x.id === id);
|
||
if (!b) return;
|
||
await api('PUT', `/bookings/${id}`, { status: b.status === 'planned' ? 'confirmed' : 'planned' });
|
||
await loadData();
|
||
render();
|
||
}
|
||
|
||
async function deleteBooking(id) {
|
||
await api('DELETE', `/bookings/${id}`);
|
||
await loadData();
|
||
render();
|
||
}
|
||
|
||
function renderBookings() {
|
||
const list = document.getElementById('bookings-list');
|
||
if (bookings.length === 0) {
|
||
list.innerHTML = '<div class="empty-state">Még nincs foglalás. Kattints a naptárra egy új hozzáadásához!</div>';
|
||
return;
|
||
}
|
||
list.innerHTML = bookings.map(b => {
|
||
const m = members.find(x => x.id === b.member_id);
|
||
const color = m ? m.color : '#999';
|
||
const start = new Date(b.start_date);
|
||
const end = new Date(b.end_date);
|
||
const nights = Math.round((end - start) / 86400000);
|
||
return `<div class="booking-item" style="border-left:3px solid ${color}">
|
||
<div class="booking-dot" style="background:${b.status==='planned'?'transparent':color};border:${b.status==='planned'?'2px dashed '+color:'none'};box-sizing:border-box;"></div>
|
||
<div class="booking-info">
|
||
<div class="booking-name">${m?.name || b.member_id}
|
||
<span class="booking-badge ${b.status}">${b.status === 'confirmed' ? '✓ megerősített' : '○ tervezett'}</span>
|
||
</div>
|
||
<div class="booking-dates">${b.start_date} → ${b.end_date} (${nights} éj)</div>
|
||
</div>
|
||
<div class="booking-actions">
|
||
<button class="toggle" onclick="toggleBooking(${b.id})" title="Státusz váltás">↻</button>
|
||
<button class="delete" onclick="deleteBooking(${b.id})" title="Törlés">✕</button>
|
||
</div>
|
||
</div>`;
|
||
}).join('');
|
||
}
|
||
|
||
// Comments
|
||
async function addComment() {
|
||
const text = document.getElementById('comment-text').value.trim();
|
||
if (!text) return;
|
||
await api('POST', '/comments', { text });
|
||
document.getElementById('comment-text').value = '';
|
||
await loadData();
|
||
renderComments();
|
||
}
|
||
|
||
async function deleteComment(id) {
|
||
await api('DELETE', `/comments/${id}`);
|
||
await loadData();
|
||
renderComments();
|
||
}
|
||
|
||
function renderComments() {
|
||
const list = document.getElementById('comments-list');
|
||
if (comments.length === 0) {
|
||
list.innerHTML = '<div class="empty-state">Még nincsenek hozzászólások.</div>';
|
||
return;
|
||
}
|
||
list.innerHTML = comments.map(c => {
|
||
const m = members.find(x => x.id === c.member_id);
|
||
const d = new Date(c.created_at + 'Z');
|
||
const dateStr = d.toLocaleDateString('hu-HU') + ' ' + d.toLocaleTimeString('hu-HU', { hour: '2-digit', minute: '2-digit' });
|
||
const isOwn = c.member_id === currentMemberId;
|
||
const deleteBtn = isOwn ? `<button class="comment-delete" onclick="deleteComment(${c.id})" title="Törlés">✕</button>` : '';
|
||
return `<div class="comment" style="border-left-color:${m?.color || '#ccc'}">
|
||
<div class="comment-header">
|
||
<span class="comment-author" style="color:${m?.color || '#666'}">${m?.name || c.member_id}</span>
|
||
<span class="comment-time">
|
||
${dateStr}
|
||
${deleteBtn}
|
||
</span>
|
||
</div>
|
||
<div class="comment-text">${escapeHtml(c.text)}</div>
|
||
</div>`;
|
||
}).join('');
|
||
}
|
||
|
||
function escapeHtml(t) {
|
||
const d = document.createElement('div');
|
||
d.textContent = t;
|
||
return d.innerHTML;
|
||
}
|
||
|
||
// Cancel selection with ESC
|
||
document.addEventListener('keydown', function(e) {
|
||
if (e.key === 'Escape' && isSelecting) {
|
||
isSelecting = false;
|
||
selectionStart = null;
|
||
document.getElementById('sel-hint').textContent = 'Kattints a kezdő dátumra a naptárban ↓';
|
||
document.querySelectorAll('.cal-day').forEach(el => el.classList.remove('in-selection'));
|
||
}
|
||
});
|
||
|
||
// Start
|
||
checkAuth();
|
||
</script>
|
||
</body>
|
||
</html> |