diff --git a/modules/adminBookings/index.js b/modules/adminBookings/index.js index 294d673..f136b67 100644 --- a/modules/adminBookings/index.js +++ b/modules/adminBookings/index.js @@ -74,7 +74,7 @@ function getBookingsForAdmin(req, res) { const total = countRow.total; const totalPages = Math.ceil(total / limit); - db.all(`SELECT b.*, p.code as promocode_code, r.name as room_name, r.type as room_type + db.all(`SELECT b.*, p.code as promocode_code, r.name as room_name FROM bookings b LEFT JOIN promocodes p ON b.promocode_id = p.id LEFT JOIN rooms r ON b.room_id = r.id @@ -112,7 +112,7 @@ function updateBookingStatus(req, res) { db.run(`UPDATE bookings SET status = ? WHERE id = ?`, [status, bookingId], (err) => { if (err) return res.status(500).json({ error: 'Database error' }); logHistory(bookingId, req.user.id, req.user.login, 'status', oldValue, status); - db.get(`SELECT b.*, p.code as promocode_code, r.name as room_name, r.type as room_type + db.get(`SELECT b.*, p.code as promocode_code, r.name as room_name FROM bookings b LEFT JOIN promocodes p ON b.promocode_id = p.id LEFT JOIN rooms r ON b.room_id = r.id @@ -155,7 +155,7 @@ function updateBookingRoom(req, res) { [room.id, room.type, basePrice, discountAmount, totalPrice, bookingId], (err) => { if (err) return res.status(500).json({ error: 'Database error' }); logHistory(bookingId, req.user.id, req.user.login, 'room', oldValue, newValue); - db.get(`SELECT b.*, p.code as promocode_code, r.name as room_name, r.type as room_type + db.get(`SELECT b.*, p.code as promocode_code, r.name as room_name FROM bookings b LEFT JOIN promocodes p ON b.promocode_id = p.id LEFT JOIN rooms r ON b.room_id = r.id @@ -194,7 +194,7 @@ function updateBookingComment(req, res) { db.run(`UPDATE bookings SET comment = ? WHERE id = ?`, [comment || null, bookingId], (err) => { if (err) return res.status(500).json({ error: 'Database error' }); logHistory(bookingId, req.user.id, req.user.login, 'comment', oldValue, comment || 'Нет'); - db.get(`SELECT b.*, p.code as promocode_code, r.name as room_name, r.type as room_type + db.get(`SELECT b.*, p.code as promocode_code, r.name as room_name FROM bookings b LEFT JOIN promocodes p ON b.promocode_id = p.id LEFT JOIN rooms r ON b.room_id = r.id @@ -223,7 +223,7 @@ function updateBookingDiscount(req, res) { [discount_percent, discountAmount, totalPrice, bookingId], (err) => { if (err) return res.status(500).json({ error: 'Database error' }); logHistory(bookingId, req.user.id, req.user.login, 'discount_percent', oldValue.toString(), discount_percent.toString()); - db.get(`SELECT b.*, p.code as promocode_code, r.name as room_name, r.type as room_type + db.get(`SELECT b.*, p.code as promocode_code, r.name as room_name FROM bookings b LEFT JOIN promocodes p ON b.promocode_id = p.id LEFT JOIN rooms r ON b.room_id = r.id @@ -320,7 +320,7 @@ function updateBookingDetails(req, res) { }); }); function finishUpdate() { - db.get(`SELECT b.*, p.code as promocode_code, r.name as room_name, r.type as room_type + db.get(`SELECT b.*, p.code as promocode_code, r.name as room_name FROM bookings b LEFT JOIN promocodes p ON b.promocode_id = p.id LEFT JOIN rooms r ON b.room_id = r.id diff --git a/modules/auth/index.js b/modules/auth/index.js index 3b32622..899f697 100644 --- a/modules/auth/index.js +++ b/modules/auth/index.js @@ -49,7 +49,7 @@ function init(database, jwtSecret) { function authenticateToken(req, res, next) { const authHeader = req.headers['authorization']; - const token = authHeader && authHeader.split(' ')[1]; + const token = (authHeader && authHeader.split(' ')[1]) || (req.cookies && req.cookies.token); if (!token) return res.status(401).json({ error: 'Unauthorized' }); jwt.verify(token, JWT_SECRET, (err, user) => { if (err) return res.status(403).json({ error: 'Invalid or expired token' }); @@ -81,6 +81,7 @@ function login(req, res) { clearRateLimit(clientIp); const token = jwt.sign({ id: user.id, login: user.login, role: user.role }, JWT_SECRET, { expiresIn: '24h' }); + res.cookie('token', token, { httpOnly: true, sameSite: 'lax', maxAge: 24 * 60 * 60 * 1000 }); res.json({ token, user: { id: user.id, login: user.login, full_name: user.full_name, email: user.email, role: user.role } diff --git a/package.json b/package.json index 330f400..1207226 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,11 @@ { "dependencies": { "bcryptjs": "^3.0.3", + "cookie-parser": "^1.4.7", "dotenv": "^17.4.2", "express": "^5.2.1", "geoip-lite": "^2.0.3", + "helmet": "^8.3.0", "jsonwebtoken": "^9.0.3", "multer": "^1.4.5-lts.1", "node-cron": "^4.2.1", diff --git a/public/admin.html b/public/admin.html index 08e8416..e7aa6f3 100644 --- a/public/admin.html +++ b/public/admin.html @@ -960,7 +960,7 @@ function showToast(msg, type = 'success') { const c = document.getElementById('toastContainer'); const t = document.createElement('div'); t.className = 'toast ' + type; - t.innerHTML = '' + msg + ''; + t.innerHTML = '' + esc(msg) + ''; c.appendChild(t); setTimeout(() => t.remove(), 3000); } @@ -1092,12 +1092,10 @@ async function loadDashboard() { try { const reviewData = await api('/api/admin/reviews'); document.getElementById('statPendingReviews')?.textContent != null && (document.getElementById('statPendingReviews').textContent = reviewData.stats.pending); - } catch(e) {} + } catch(e) { console.error('loadDashboard reviews failed:', e); } loadRevenueChart('monthly'); } -let currentChartPeriod = 'monthly'; - async function loadRevenueChart(period) { currentChartPeriod = period; document.querySelectorAll('#chartWeekBtn, #chartDayBtn, [onclick*="loadRevenueChart"]').forEach(b => { @@ -1706,26 +1704,6 @@ async function deleteReview(id) { } // Settings Tab Functions -async function loadSettings() { - try { - const data = await api('/api/admin/settings'); - const display = document.getElementById('currentCodeDisplay'); - if (display) { - display.value = data.review_code || 'Не установлен'; - } - } catch(err) { - const display = document.getElementById('currentCodeDisplay'); - if (display) display.value = 'Ошибка загрузки'; - } - - try { - await loadBackupSettings(); - await loadBackupsList(); - } catch(err) { - console.error('Failed to load backup module:', err); - } -} - document.getElementById('settingsForm').addEventListener('submit', async e => { e.preventDefault(); const newCode = document.getElementById('newReviewCode').value.trim(); @@ -1754,17 +1732,6 @@ function toggleCodeShow() { } // Backup Functions -async function loadBackupSettings() { - try { - const settings = await api('/api/admin/backup/settings'); - document.getElementById('backupAutoEnabled').checked = settings.backup_auto_enabled === 'true'; - document.getElementById('backupAutoTime').value = settings.backup_auto_time || '03:00'; - document.getElementById('backupRetentionDays').value = settings.backup_retention_days || '30'; - } catch(err) { - console.error('Failed to load backup settings:', err); - } -} - async function saveBackupSettings() { const enabled = document.getElementById('backupAutoEnabled').checked; const time = document.getElementById('backupAutoTime').value; @@ -2037,62 +2004,6 @@ function renderRooms(rooms) { let editingRoomId = null; let currentRooms = []; -function showRoomModal(id) { - editingRoomId = id; - const modal = document.getElementById('roomModal'); - const form = document.getElementById('roomForm'); - const title = document.getElementById('roomModalTitle'); - - form.reset(); - document.getElementById('roomImagePreview').src = 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 120"%3E%3Crect fill="%23274151" width="200" height="120"/%3E%3Ctext fill="%2364748b" font-family="sans-serif" font-size="12" x="50%25" y="50%25" text-anchor="middle" dy=".3em"%3EВыберите фото%3C/text%3E%3C/svg%3E'; - document.getElementById('roomImagePreview').removeAttribute('data-path'); - - document.querySelectorAll('#roomForm input[type="checkbox"]').forEach(cb => cb.checked = false); - - if (id) { - title.textContent = 'Редактировать номер'; - const room = currentRooms.find(r => r.id === id); - if (room) fillRoomForm(room); - } else { - title.textContent = 'Добавить номер'; - } - - modal.classList.add('show'); - document.body.style.overflow = 'hidden'; -} - -function fillRoomForm(r) { - document.getElementById('roomName').value = r.name || ''; - document.getElementById('roomType').value = r.type || '2x-местный'; - document.getElementById('roomDescription').value = r.description || ''; - document.getElementById('roomPrice').value = r.price_per_night || 0; - document.getElementById('roomArea').value = r.area_sqm || 20; - document.getElementById('roomMaxGuests').value = r.max_guests || 2; - document.getElementById('roomCount').value = r.rooms_count || 1; - document.getElementById('roomFloors').value = (Array.isArray(r.floors) ? r.floors.join(', ') : ''); - document.getElementById('roomExtraBeds').value = r.extra_beds || 0; - document.getElementById('roomExtraBedPrice').value = r.extra_bed_price || 0; - document.getElementById('roomIsActive').checked = r.is_active !== 0; - - if (r.image_path) { - const src = r.image_path.startsWith('uploads') ? '/' + r.image_path : r.image_path; - document.getElementById('roomImagePreview').src = src; - document.getElementById('roomImagePreview').dataset.path = r.image_path; - } - - const furniture = Array.isArray(r.furniture) ? r.furniture : []; - furniture.forEach(f => { - const cb = document.querySelector('#roomForm input[name="furniture"][value="' + f + '"]'); - if (cb) cb.checked = true; - }); - - const amenities = Array.isArray(r.amenities) ? r.amenities : []; - amenities.forEach(a => { - const cb = document.querySelector('#roomForm input[name="amenities"][value="' + a + '"]'); - if (cb) cb.checked = true; - }); -} - function closeRoomModal() { document.getElementById('roomModal').classList.remove('show'); document.body.style.overflow = ''; @@ -2199,7 +2110,7 @@ async function loadRoomImages(roomId) { const room = rooms.find(r => r.id === roomId); if (!room || !room.images) return; renderRoomImages(room.images); - } catch(e) {} + } catch(e) { console.error('loadRoomImages failed:', e); } } function renderRoomImages(images) { @@ -2300,36 +2211,43 @@ function renderCalendar(data) { const days = data.days || {}; const month = data.month; const [y, m] = month.split('-').map(Number); - const firstDay = new Date(y, m-1, 1); const lastDay = new Date(y, m, 0); - const startDayOfWeek = firstDay.getDay() || 7; + const dayNames = ['Вс','Пн','Вт','Ср','Чт','Пт','Сб']; + + let html = '
'; - let html = ''; - html += ''; - const dayNames = ['Пн','Вт','Ср','Чт','Пт','Сб','Вс']; for (let d = 1; d <= lastDay.getDate(); d++) { - html += ``; - } - html += ''; + const dateStr = `${month}-${String(d).padStart(2,'0')}`; + const dow = new Date(y, m-1, d).getDay(); + const dayName = dayNames[dow]; + let hasData = false; - rooms.forEach(room => { - html += ''; - for (let d = 1; d <= lastDay.getDate(); d++) { - const dateStr = `${month}-${String(d).padStart(2,'0')}`; + let roomsHtml = ''; + rooms.forEach(room => { const dayData = days[dateStr] ? days[dateStr][room.type] : null; const booked = dayData ? dayData.booked : 0; - const total = dayData ? dayData.total : room.rooms_count; + const total = room.rooms_count || 1; const ratio = total > 0 ? booked / total : 0; let bg = '#dcfce7'; if (ratio >= 1) bg = '#fee2e2'; else if (ratio >= 0.7) bg = '#fef3c7'; + const pct = Math.round(ratio * 100); - const title = room.type + ': ' + booked + '/' + total; - html += ``; - } - html += ''; - }); - html += '
Месяц${d}
${dayNames[(new Date(y,m-1,d).getDay()||7)-1]}
' + esc(room.type) + '${booked > 0 ? ''+booked+'' : ''}
'; + if (booked > 0) hasData = true; + roomsHtml += `
+ ${esc(room.type)} + + ${booked}/${total} +
`; + }); + + html += `
+
${d} ${dayName}
+
${roomsHtml}
+
`; + } + + html += '
'; grid.innerHTML = html; } @@ -2602,7 +2520,7 @@ async function loadSecurityKeys() { `; - } catch(e) {} + } catch(e) { console.error('loadSecurityKeys failed:', e); } } async function regenerateKey(type) { @@ -2631,7 +2549,7 @@ async function loadEmailSettings() { document.getElementById('smtpFrom').value = data.smtp_from || ''; document.getElementById('adminEmail').value = data.admin_email || ''; document.getElementById('emailEnabled').checked = data.email_notifications_enabled === 'true'; - } catch(e) {} + } catch(e) { console.error('loadEmailSettings failed:', e); } } async function saveEmailSettings() { @@ -2672,7 +2590,7 @@ async function loadPhoneSettings() { document.getElementById('phoneMaxActive').checked = data.maxActive === 'true'; document.getElementById('phoneInstagramId').value = data.instagramId || ''; document.getElementById('phoneInstagramActive').checked = data.instagramActive === 'true'; - } catch(e) {} + } catch(e) { console.error('loadPhoneSettings failed:', e); } } async function savePhoneSettings() { @@ -2707,7 +2625,7 @@ async function loadTelegramSettings() { document.getElementById('telegramEnabled').checked = data.telegram_notify_enabled === 'true'; document.getElementById('telegramApiUrl').value = data.telegram_api_url || 'https://api.telegram.org'; updateTelegramStatus(data.telegram_proxy_status || 'unknown'); - } catch(e) {} + } catch(e) { console.error('loadTelegramSettings failed:', e); } } function updateTelegramStatus(status) { @@ -2790,7 +2708,7 @@ async function loadSettings() { try { const data = await api('/api/admin/settings'); document.getElementById('currentCodeDisplay').value = data.review_code || ''; - } catch(e) {} + } catch(e) { console.error('loadSettings review_code failed:', e); } try { const backupSettings = await api('/api/admin/backup/settings'); if (backupSettings.backup_auto_enabled !== undefined) { @@ -2802,8 +2720,8 @@ async function loadSettings() { if (backupSettings.backup_retention_days) { document.getElementById('backupRetentionDays').value = backupSettings.backup_retention_days; } - loadBackups(); - } catch(e) {} + loadBackupsList(); + } catch(e) { console.error('loadSettings backup failed:', e); } loadSecurityKeys(); loadEmailSettings(); loadPhoneSettings(); @@ -3161,11 +3079,11 @@ async function loadIPs(offset) { ''; data.entries.forEach(e => { html += `` + - `${e.ip}` + - `${e.country || '-'} ${e.country_code || ''}` + - `${e.accept_language || '-'}` + + `${esc(e.ip)}` + + `${esc(e.country || '-')} ${esc(e.country_code || '')}` + + `${esc(e.accept_language || '-')}` + `${e.created_at ? e.created_at.substring(0, 16) : e.visit_date}` + - `${e.page_path}` + + `${esc(e.page_path)}` + ``; }); html += ''; @@ -3185,6 +3103,13 @@ async function loadIPs(offset) { document.getElementById('visIPsTable').innerHTML = '
Ошибка загрузки
'; } } +document.addEventListener('keydown', function(e) { + if (e.key === 'Escape') { + var modals = document.querySelectorAll('.modal-backdrop-custom.show'); + modals.forEach(function(m) { m.classList.remove('show'); }); + if (modals.length > 0) document.body.style.overflow = ''; + } +}); @@ -3317,6 +3242,17 @@ async function loadIPs(offset) { .room-admin-actions { display: flex; gap: 8px; margin-top: 12px; border-top: 1px solid #f1f5f9; padding-top: 12px; } .room-admin-actions .btn-primary-custom { flex: 1; } .room-admin-actions .btn-danger-custom { padding: 8px 12px; } +.calendar-cards { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; } +@media (min-width: 768px) { .calendar-cards { grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); } } +.cal-card { background: #fff; border: 1px solid #e2e8f0; border-radius: 8px; overflow: hidden; } +.cal-card-header { padding: 8px 12px; background: #f8fafc; font-weight: 600; font-size: 0.85rem; color: #0f172a; border-bottom: 1px solid #e2e8f0; } +.cal-card-rooms { padding: 4px 0; } +.cal-room-row { display: flex; align-items: center; gap: 6px; padding: 4px 10px; font-size: 0.75rem; border-bottom: 1px solid #f1f5f9; } +.cal-room-row:last-child { border-bottom: none; } +.cal-room-type { width: 80px; flex-shrink: 0; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.cal-room-bar { flex: 1; height: 6px; background: rgba(0,0,0,0.1); border-radius: 3px; overflow: hidden; } +.cal-room-fill { display: block; height: 100%; border-radius: 3px; transition: width 0.3s ease; } +.cal-room-count { width: 36px; flex-shrink: 0; text-align: right; font-weight: 600; font-size: 0.7rem; } -