This commit is contained in:
@@ -74,7 +74,7 @@ function getBookingsForAdmin(req, res) {
|
|||||||
const total = countRow.total;
|
const total = countRow.total;
|
||||||
const totalPages = Math.ceil(total / limit);
|
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
|
FROM bookings b
|
||||||
LEFT JOIN promocodes p ON b.promocode_id = p.id
|
LEFT JOIN promocodes p ON b.promocode_id = p.id
|
||||||
LEFT JOIN rooms r ON b.room_id = r.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) => {
|
db.run(`UPDATE bookings SET status = ? WHERE id = ?`, [status, bookingId], (err) => {
|
||||||
if (err) return res.status(500).json({ error: 'Database error' });
|
if (err) return res.status(500).json({ error: 'Database error' });
|
||||||
logHistory(bookingId, req.user.id, req.user.login, 'status', oldValue, status);
|
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
|
FROM bookings b
|
||||||
LEFT JOIN promocodes p ON b.promocode_id = p.id
|
LEFT JOIN promocodes p ON b.promocode_id = p.id
|
||||||
LEFT JOIN rooms r ON b.room_id = r.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) => {
|
[room.id, room.type, basePrice, discountAmount, totalPrice, bookingId], (err) => {
|
||||||
if (err) return res.status(500).json({ error: 'Database error' });
|
if (err) return res.status(500).json({ error: 'Database error' });
|
||||||
logHistory(bookingId, req.user.id, req.user.login, 'room', oldValue, newValue);
|
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
|
FROM bookings b
|
||||||
LEFT JOIN promocodes p ON b.promocode_id = p.id
|
LEFT JOIN promocodes p ON b.promocode_id = p.id
|
||||||
LEFT JOIN rooms r ON b.room_id = r.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) => {
|
db.run(`UPDATE bookings SET comment = ? WHERE id = ?`, [comment || null, bookingId], (err) => {
|
||||||
if (err) return res.status(500).json({ error: 'Database error' });
|
if (err) return res.status(500).json({ error: 'Database error' });
|
||||||
logHistory(bookingId, req.user.id, req.user.login, 'comment', oldValue, comment || 'Нет');
|
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
|
FROM bookings b
|
||||||
LEFT JOIN promocodes p ON b.promocode_id = p.id
|
LEFT JOIN promocodes p ON b.promocode_id = p.id
|
||||||
LEFT JOIN rooms r ON b.room_id = r.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) => {
|
[discount_percent, discountAmount, totalPrice, bookingId], (err) => {
|
||||||
if (err) return res.status(500).json({ error: 'Database error' });
|
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());
|
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
|
FROM bookings b
|
||||||
LEFT JOIN promocodes p ON b.promocode_id = p.id
|
LEFT JOIN promocodes p ON b.promocode_id = p.id
|
||||||
LEFT JOIN rooms r ON b.room_id = r.id
|
LEFT JOIN rooms r ON b.room_id = r.id
|
||||||
@@ -320,7 +320,7 @@ function updateBookingDetails(req, res) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
function finishUpdate() {
|
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
|
FROM bookings b
|
||||||
LEFT JOIN promocodes p ON b.promocode_id = p.id
|
LEFT JOIN promocodes p ON b.promocode_id = p.id
|
||||||
LEFT JOIN rooms r ON b.room_id = r.id
|
LEFT JOIN rooms r ON b.room_id = r.id
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ function init(database, jwtSecret) {
|
|||||||
|
|
||||||
function authenticateToken(req, res, next) {
|
function authenticateToken(req, res, next) {
|
||||||
const authHeader = req.headers['authorization'];
|
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' });
|
if (!token) return res.status(401).json({ error: 'Unauthorized' });
|
||||||
jwt.verify(token, JWT_SECRET, (err, user) => {
|
jwt.verify(token, JWT_SECRET, (err, user) => {
|
||||||
if (err) return res.status(403).json({ error: 'Invalid or expired token' });
|
if (err) return res.status(403).json({ error: 'Invalid or expired token' });
|
||||||
@@ -81,6 +81,7 @@ function login(req, res) {
|
|||||||
clearRateLimit(clientIp);
|
clearRateLimit(clientIp);
|
||||||
|
|
||||||
const token = jwt.sign({ id: user.id, login: user.login, role: user.role }, JWT_SECRET, { expiresIn: '24h' });
|
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({
|
res.json({
|
||||||
token,
|
token,
|
||||||
user: { id: user.id, login: user.login, full_name: user.full_name, email: user.email, role: user.role }
|
user: { id: user.id, login: user.login, full_name: user.full_name, email: user.email, role: user.role }
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
{
|
{
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
|
"cookie-parser": "^1.4.7",
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"geoip-lite": "^2.0.3",
|
"geoip-lite": "^2.0.3",
|
||||||
|
"helmet": "^8.3.0",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"node-cron": "^4.2.1",
|
"node-cron": "^4.2.1",
|
||||||
|
|||||||
@@ -960,7 +960,7 @@ function showToast(msg, type = 'success') {
|
|||||||
const c = document.getElementById('toastContainer');
|
const c = document.getElementById('toastContainer');
|
||||||
const t = document.createElement('div');
|
const t = document.createElement('div');
|
||||||
t.className = 'toast ' + type;
|
t.className = 'toast ' + type;
|
||||||
t.innerHTML = '<span class="toast-icon"><i class="fas fa-' + (type === 'success' ? 'check-circle' : 'exclamation-circle') + '"></i></span><span>' + msg + '</span>';
|
t.innerHTML = '<span class="toast-icon"><i class="fas fa-' + (type === 'success' ? 'check-circle' : 'exclamation-circle') + '"></i></span><span>' + esc(msg) + '</span>';
|
||||||
c.appendChild(t);
|
c.appendChild(t);
|
||||||
setTimeout(() => t.remove(), 3000);
|
setTimeout(() => t.remove(), 3000);
|
||||||
}
|
}
|
||||||
@@ -1092,12 +1092,10 @@ async function loadDashboard() {
|
|||||||
try {
|
try {
|
||||||
const reviewData = await api('/api/admin/reviews');
|
const reviewData = await api('/api/admin/reviews');
|
||||||
document.getElementById('statPendingReviews')?.textContent != null && (document.getElementById('statPendingReviews').textContent = reviewData.stats.pending);
|
document.getElementById('statPendingReviews')?.textContent != null && (document.getElementById('statPendingReviews').textContent = reviewData.stats.pending);
|
||||||
} catch(e) {}
|
} catch(e) { console.error('loadDashboard reviews failed:', e); }
|
||||||
loadRevenueChart('monthly');
|
loadRevenueChart('monthly');
|
||||||
}
|
}
|
||||||
|
|
||||||
let currentChartPeriod = 'monthly';
|
|
||||||
|
|
||||||
async function loadRevenueChart(period) {
|
async function loadRevenueChart(period) {
|
||||||
currentChartPeriod = period;
|
currentChartPeriod = period;
|
||||||
document.querySelectorAll('#chartWeekBtn, #chartDayBtn, [onclick*="loadRevenueChart"]').forEach(b => {
|
document.querySelectorAll('#chartWeekBtn, #chartDayBtn, [onclick*="loadRevenueChart"]').forEach(b => {
|
||||||
@@ -1706,26 +1704,6 @@ async function deleteReview(id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Settings Tab Functions
|
// 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 => {
|
document.getElementById('settingsForm').addEventListener('submit', async e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const newCode = document.getElementById('newReviewCode').value.trim();
|
const newCode = document.getElementById('newReviewCode').value.trim();
|
||||||
@@ -1754,17 +1732,6 @@ function toggleCodeShow() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Backup Functions
|
// 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() {
|
async function saveBackupSettings() {
|
||||||
const enabled = document.getElementById('backupAutoEnabled').checked;
|
const enabled = document.getElementById('backupAutoEnabled').checked;
|
||||||
const time = document.getElementById('backupAutoTime').value;
|
const time = document.getElementById('backupAutoTime').value;
|
||||||
@@ -2037,62 +2004,6 @@ function renderRooms(rooms) {
|
|||||||
let editingRoomId = null;
|
let editingRoomId = null;
|
||||||
let currentRooms = [];
|
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() {
|
function closeRoomModal() {
|
||||||
document.getElementById('roomModal').classList.remove('show');
|
document.getElementById('roomModal').classList.remove('show');
|
||||||
document.body.style.overflow = '';
|
document.body.style.overflow = '';
|
||||||
@@ -2199,7 +2110,7 @@ async function loadRoomImages(roomId) {
|
|||||||
const room = rooms.find(r => r.id === roomId);
|
const room = rooms.find(r => r.id === roomId);
|
||||||
if (!room || !room.images) return;
|
if (!room || !room.images) return;
|
||||||
renderRoomImages(room.images);
|
renderRoomImages(room.images);
|
||||||
} catch(e) {}
|
} catch(e) { console.error('loadRoomImages failed:', e); }
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderRoomImages(images) {
|
function renderRoomImages(images) {
|
||||||
@@ -2300,36 +2211,43 @@ function renderCalendar(data) {
|
|||||||
const days = data.days || {};
|
const days = data.days || {};
|
||||||
const month = data.month;
|
const month = data.month;
|
||||||
const [y, m] = month.split('-').map(Number);
|
const [y, m] = month.split('-').map(Number);
|
||||||
const firstDay = new Date(y, m-1, 1);
|
|
||||||
const lastDay = new Date(y, m, 0);
|
const lastDay = new Date(y, m, 0);
|
||||||
const startDayOfWeek = firstDay.getDay() || 7;
|
const dayNames = ['Вс','Пн','Вт','Ср','Чт','Пт','Сб'];
|
||||||
|
|
||||||
let html = '<table style="width:100%;min-width:700px;border-collapse:collapse;font-size:0.8rem;">';
|
let html = '<div class="calendar-cards">';
|
||||||
html += '<thead><tr><th style="padding:8px;border:1px solid #e2e8f0;background:#f8fafc;">Месяц</th>';
|
|
||||||
const dayNames = ['Пн','Вт','Ср','Чт','Пт','Сб','Вс'];
|
|
||||||
for (let d = 1; d <= lastDay.getDate(); d++) {
|
|
||||||
html += `<th style="padding:6px 8px;border:1px solid #e2e8f0;background:#f8fafc;text-align:center;font-size:0.7rem;">${d}<br><small>${dayNames[(new Date(y,m-1,d).getDay()||7)-1]}</small></th>`;
|
|
||||||
}
|
|
||||||
html += '</tr></thead><tbody>';
|
|
||||||
|
|
||||||
rooms.forEach(room => {
|
|
||||||
html += '<tr><td style="padding:6px 8px;border:1px solid #e2e8f0;font-weight:600;white-space:nowrap;background:#f8fafc;">' + esc(room.type) + '</td>';
|
|
||||||
for (let d = 1; d <= lastDay.getDate(); d++) {
|
for (let d = 1; d <= lastDay.getDate(); d++) {
|
||||||
const dateStr = `${month}-${String(d).padStart(2,'0')}`;
|
const dateStr = `${month}-${String(d).padStart(2,'0')}`;
|
||||||
|
const dow = new Date(y, m-1, d).getDay();
|
||||||
|
const dayName = dayNames[dow];
|
||||||
|
let hasData = false;
|
||||||
|
|
||||||
|
let roomsHtml = '';
|
||||||
|
rooms.forEach(room => {
|
||||||
const dayData = days[dateStr] ? days[dateStr][room.type] : null;
|
const dayData = days[dateStr] ? days[dateStr][room.type] : null;
|
||||||
const booked = dayData ? dayData.booked : 0;
|
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;
|
const ratio = total > 0 ? booked / total : 0;
|
||||||
let bg = '#dcfce7';
|
let bg = '#dcfce7';
|
||||||
if (ratio >= 1) bg = '#fee2e2';
|
if (ratio >= 1) bg = '#fee2e2';
|
||||||
else if (ratio >= 0.7) bg = '#fef3c7';
|
else if (ratio >= 0.7) bg = '#fef3c7';
|
||||||
|
const pct = Math.round(ratio * 100);
|
||||||
|
|
||||||
const title = room.type + ': ' + booked + '/' + total;
|
if (booked > 0) hasData = true;
|
||||||
html += `<td style="padding:4px;border:1px solid #e2e8f0;background:${bg};text-align:center;cursor:pointer;" title="${title}" onclick="showCalendarDay('${dateStr}','${esc(room.type)}')">${booked > 0 ? '<span style="font-weight:600;font-size:0.75rem;">'+booked+'</span>' : ''}</td>`;
|
roomsHtml += `<div class="cal-room-row" style="background:${bg};cursor:pointer;" onclick="showCalendarDay('${dateStr}','${esc(room.type)}')">
|
||||||
}
|
<span class="cal-room-type">${esc(room.type)}</span>
|
||||||
html += '</tr>';
|
<span class="cal-room-bar"><span class="cal-room-fill" style="width:${pct}%;background:${ratio >= 1 ? '#ef4444' : ratio >= 0.7 ? '#f59e0b' : '#22c55e'}"></span></span>
|
||||||
|
<span class="cal-room-count">${booked}/${total}</span>
|
||||||
|
</div>`;
|
||||||
});
|
});
|
||||||
html += '</tbody></table>';
|
|
||||||
|
html += `<div class="cal-card">
|
||||||
|
<div class="cal-card-header">${d} ${dayName}</div>
|
||||||
|
<div class="cal-card-rooms">${roomsHtml}</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
grid.innerHTML = html;
|
grid.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2602,7 +2520,7 @@ async function loadSecurityKeys() {
|
|||||||
<button class="btn btn-sm" style="margin-left:8px;background:#f59e0b;color:#fff;border:none;border-radius:6px;padding:2px 8px;" onclick="regenerateKey('hotel777key')">Перегенерировать</button>
|
<button class="btn btn-sm" style="margin-left:8px;background:#f59e0b;color:#fff;border:none;border-radius:6px;padding:2px 8px;" onclick="regenerateKey('hotel777key')">Перегенерировать</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} catch(e) {}
|
} catch(e) { console.error('loadSecurityKeys failed:', e); }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function regenerateKey(type) {
|
async function regenerateKey(type) {
|
||||||
@@ -2631,7 +2549,7 @@ async function loadEmailSettings() {
|
|||||||
document.getElementById('smtpFrom').value = data.smtp_from || '';
|
document.getElementById('smtpFrom').value = data.smtp_from || '';
|
||||||
document.getElementById('adminEmail').value = data.admin_email || '';
|
document.getElementById('adminEmail').value = data.admin_email || '';
|
||||||
document.getElementById('emailEnabled').checked = data.email_notifications_enabled === 'true';
|
document.getElementById('emailEnabled').checked = data.email_notifications_enabled === 'true';
|
||||||
} catch(e) {}
|
} catch(e) { console.error('loadEmailSettings failed:', e); }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveEmailSettings() {
|
async function saveEmailSettings() {
|
||||||
@@ -2672,7 +2590,7 @@ async function loadPhoneSettings() {
|
|||||||
document.getElementById('phoneMaxActive').checked = data.maxActive === 'true';
|
document.getElementById('phoneMaxActive').checked = data.maxActive === 'true';
|
||||||
document.getElementById('phoneInstagramId').value = data.instagramId || '';
|
document.getElementById('phoneInstagramId').value = data.instagramId || '';
|
||||||
document.getElementById('phoneInstagramActive').checked = data.instagramActive === 'true';
|
document.getElementById('phoneInstagramActive').checked = data.instagramActive === 'true';
|
||||||
} catch(e) {}
|
} catch(e) { console.error('loadPhoneSettings failed:', e); }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function savePhoneSettings() {
|
async function savePhoneSettings() {
|
||||||
@@ -2707,7 +2625,7 @@ async function loadTelegramSettings() {
|
|||||||
document.getElementById('telegramEnabled').checked = data.telegram_notify_enabled === 'true';
|
document.getElementById('telegramEnabled').checked = data.telegram_notify_enabled === 'true';
|
||||||
document.getElementById('telegramApiUrl').value = data.telegram_api_url || 'https://api.telegram.org';
|
document.getElementById('telegramApiUrl').value = data.telegram_api_url || 'https://api.telegram.org';
|
||||||
updateTelegramStatus(data.telegram_proxy_status || 'unknown');
|
updateTelegramStatus(data.telegram_proxy_status || 'unknown');
|
||||||
} catch(e) {}
|
} catch(e) { console.error('loadTelegramSettings failed:', e); }
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateTelegramStatus(status) {
|
function updateTelegramStatus(status) {
|
||||||
@@ -2790,7 +2708,7 @@ async function loadSettings() {
|
|||||||
try {
|
try {
|
||||||
const data = await api('/api/admin/settings');
|
const data = await api('/api/admin/settings');
|
||||||
document.getElementById('currentCodeDisplay').value = data.review_code || '';
|
document.getElementById('currentCodeDisplay').value = data.review_code || '';
|
||||||
} catch(e) {}
|
} catch(e) { console.error('loadSettings review_code failed:', e); }
|
||||||
try {
|
try {
|
||||||
const backupSettings = await api('/api/admin/backup/settings');
|
const backupSettings = await api('/api/admin/backup/settings');
|
||||||
if (backupSettings.backup_auto_enabled !== undefined) {
|
if (backupSettings.backup_auto_enabled !== undefined) {
|
||||||
@@ -2802,8 +2720,8 @@ async function loadSettings() {
|
|||||||
if (backupSettings.backup_retention_days) {
|
if (backupSettings.backup_retention_days) {
|
||||||
document.getElementById('backupRetentionDays').value = backupSettings.backup_retention_days;
|
document.getElementById('backupRetentionDays').value = backupSettings.backup_retention_days;
|
||||||
}
|
}
|
||||||
loadBackups();
|
loadBackupsList();
|
||||||
} catch(e) {}
|
} catch(e) { console.error('loadSettings backup failed:', e); }
|
||||||
loadSecurityKeys();
|
loadSecurityKeys();
|
||||||
loadEmailSettings();
|
loadEmailSettings();
|
||||||
loadPhoneSettings();
|
loadPhoneSettings();
|
||||||
@@ -3161,11 +3079,11 @@ async function loadIPs(offset) {
|
|||||||
'</tr></thead><tbody>';
|
'</tr></thead><tbody>';
|
||||||
data.entries.forEach(e => {
|
data.entries.forEach(e => {
|
||||||
html += `<tr>` +
|
html += `<tr>` +
|
||||||
`<td style="padding:4px 8px;border-bottom:1px solid #f1f5f9;font-family:monospace;">${e.ip}</td>` +
|
`<td style="padding:4px 8px;border-bottom:1px solid #f1f5f9;font-family:monospace;">${esc(e.ip)}</td>` +
|
||||||
`<td style="padding:4px 8px;border-bottom:1px solid #f1f5f9;">${e.country || '-'} <span style="color:#94a3b8;font-size:0.7rem;">${e.country_code || ''}</span></td>` +
|
`<td style="padding:4px 8px;border-bottom:1px solid #f1f5f9;">${esc(e.country || '-')} <span style="color:#94a3b8;font-size:0.7rem;">${esc(e.country_code || '')}</span></td>` +
|
||||||
`<td style="padding:4px 8px;border-bottom:1px solid #f1f5f9;">${e.accept_language || '-'}</td>` +
|
`<td style="padding:4px 8px;border-bottom:1px solid #f1f5f9;">${esc(e.accept_language || '-')}</td>` +
|
||||||
`<td style="padding:4px 8px;border-bottom:1px solid #f1f5f9;white-space:nowrap;">${e.created_at ? e.created_at.substring(0, 16) : e.visit_date}</td>` +
|
`<td style="padding:4px 8px;border-bottom:1px solid #f1f5f9;white-space:nowrap;">${e.created_at ? e.created_at.substring(0, 16) : e.visit_date}</td>` +
|
||||||
`<td style="padding:4px 8px;border-bottom:1px solid #f1f5f9;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:0.8rem;color:#64748b;" title="${e.page_path}">${e.page_path}</td>` +
|
`<td style="padding:4px 8px;border-bottom:1px solid #f1f5f9;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:0.8rem;color:#64748b;" title="${esc(e.page_path)}">${esc(e.page_path)}</td>` +
|
||||||
`</tr>`;
|
`</tr>`;
|
||||||
});
|
});
|
||||||
html += '</tbody></table>';
|
html += '</tbody></table>';
|
||||||
@@ -3185,6 +3103,13 @@ async function loadIPs(offset) {
|
|||||||
document.getElementById('visIPsTable').innerHTML = '<div style="text-align:center;color:#ef4444;padding:20px;">Ошибка загрузки</div>';
|
document.getElementById('visIPsTable').innerHTML = '<div style="text-align:center;color:#ef4444;padding:20px;">Ошибка загрузки</div>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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 = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Room Edit Modal -->
|
<!-- Room Edit Modal -->
|
||||||
@@ -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 { 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-primary-custom { flex: 1; }
|
||||||
.room-admin-actions .btn-danger-custom { padding: 8px 12px; }
|
.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; }
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="modal-backdrop-custom" id="seasonalModal">
|
<div class="modal-backdrop-custom" id="seasonalModal">
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ html, body {
|
|||||||
gap: 5px;
|
gap: 5px;
|
||||||
}
|
}
|
||||||
.nav-link {
|
.nav-link {
|
||||||
padding: 10px 15px !important;
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
.nav-link:hover {
|
.nav-link:hover {
|
||||||
@@ -115,12 +114,12 @@ h1, h2, h3, h4 {
|
|||||||
|
|
||||||
/* Navigation */
|
/* Navigation */
|
||||||
.navbar {
|
.navbar {
|
||||||
transition: all 0.4s ease;
|
transition: transform 0.4s ease, box-shadow 0.4s ease, background 0.4s ease, color 0.4s ease, opacity 0.4s ease;
|
||||||
padding: 20px 0;
|
padding: 20px 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
.navbar.scrolled {
|
#mainNavbar.navbar.scrolled {
|
||||||
background: rgba(10, 77, 104, 0.95) !important;
|
background: rgba(10, 77, 104, 0.95);
|
||||||
backdrop-filter: blur(20px);
|
backdrop-filter: blur(20px);
|
||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
box-shadow: 0 4px 30px rgba(0,0,0,0.15);
|
box-shadow: 0 4px 30px rgba(0,0,0,0.15);
|
||||||
@@ -128,17 +127,17 @@ h1, h2, h3, h4 {
|
|||||||
.navbar-brand {
|
.navbar-brand {
|
||||||
font-family: 'Playfair Display', serif;
|
font-family: 'Playfair Display', serif;
|
||||||
font-size: 1.8rem;
|
font-size: 1.8rem;
|
||||||
color: #fff !important;
|
color: #fff;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
.navbar-brand span { color: var(--gold); }
|
.navbar-brand span { color: var(--gold); }
|
||||||
.nav-link {
|
.nav-link {
|
||||||
color: rgba(255,255,255,0.9) !important;
|
color: rgba(255,255,255,0.9);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
padding: 8px 16px !important;
|
padding: 8px 16px;
|
||||||
transition: all 0.3s ease;
|
transition: transform 0.3s ease, box-shadow 0.3s ease, background 0.3s ease, color 0.3s ease, opacity 0.3s ease;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
.nav-link::after {
|
.nav-link::after {
|
||||||
@@ -147,11 +146,11 @@ h1, h2, h3, h4 {
|
|||||||
bottom: 0; left: 50%;
|
bottom: 0; left: 50%;
|
||||||
width: 0; height: 2px;
|
width: 0; height: 2px;
|
||||||
background: var(--gold);
|
background: var(--gold);
|
||||||
transition: all 0.3s ease;
|
transition: transform 0.3s ease, box-shadow 0.3s ease, background 0.3s ease, color 0.3s ease, opacity 0.3s ease;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
}
|
}
|
||||||
.nav-link:hover::after { width: 60%; }
|
.nav-link:hover::after { width: 60%; }
|
||||||
.nav-link:hover { color: var(--gold) !important; }
|
#mainNavbar .nav-link:hover { color: var(--gold); }
|
||||||
|
|
||||||
/* Hero Section */
|
/* Hero Section */
|
||||||
.hero {
|
.hero {
|
||||||
@@ -266,7 +265,7 @@ h1, h2, h3, h4 {
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: clamp(0.85rem, 1.2vw, 1rem);
|
font-size: clamp(0.85rem, 1.2vw, 1rem);
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
transition: all 0.3s ease;
|
transition: transform 0.3s ease, box-shadow 0.3s ease, background 0.3s ease, color 0.3s ease, opacity 0.3s ease;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -286,7 +285,7 @@ h1, h2, h3, h4 {
|
|||||||
border-radius: 50px;
|
border-radius: 50px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: clamp(0.85rem, 1.2vw, 1rem);
|
font-size: clamp(0.85rem, 1.2vw, 1rem);
|
||||||
transition: all 0.3s ease;
|
transition: transform 0.3s ease, box-shadow 0.3s ease, background 0.3s ease, color 0.3s ease, opacity 0.3s ease;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -298,8 +297,7 @@ h1, h2, h3, h4 {
|
|||||||
border-color: #fff;
|
border-color: #fff;
|
||||||
transform: translateY(-3px);
|
transform: translateY(-3px);
|
||||||
}
|
}
|
||||||
.btn-whatsapp {
|
.btn-social {
|
||||||
background: #25D366;
|
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border: none;
|
border: none;
|
||||||
width: 44px;
|
width: 44px;
|
||||||
@@ -307,89 +305,27 @@ h1, h2, h3, h4 {
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
transition: all 0.3s ease;
|
transition: transform 0.3s ease, box-shadow 0.3s ease, background 0.3s ease;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
margin: 0 4px;
|
||||||
}
|
}
|
||||||
.btn-whatsapp:hover {
|
.btn-social:hover {
|
||||||
background: #1ebe57;
|
|
||||||
transform: translateY(-3px);
|
transform: translateY(-3px);
|
||||||
box-shadow: 0 8px 20px rgba(37,211,102,0.4);
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
.btn-telegram {
|
|
||||||
background: #0088cc;
|
|
||||||
color: #fff;
|
|
||||||
border: none;
|
|
||||||
width: 44px;
|
|
||||||
height: 44px;
|
|
||||||
border-radius: 50%;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
text-decoration: none;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 4px;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
.btn-telegram:hover {
|
|
||||||
background: #0077b5;
|
|
||||||
transform: translateY(-3px);
|
|
||||||
box-shadow: 0 8px 20px rgba(0,136,204,0.4);
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
.btn-max {
|
|
||||||
background: #471AFF;
|
|
||||||
color: #fff;
|
|
||||||
border: none;
|
|
||||||
width: 44px;
|
|
||||||
height: 44px;
|
|
||||||
border-radius: 50%;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
text-decoration: none;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 4px;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
.btn-max:hover {
|
|
||||||
background: #3b15d9;
|
|
||||||
transform: translateY(-3px);
|
|
||||||
box-shadow: 0 8px 20px rgba(71,26,255,0.4);
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
.btn-instagram {
|
|
||||||
background: #E4405F;
|
|
||||||
color: #fff;
|
|
||||||
border: none;
|
|
||||||
width: 44px;
|
|
||||||
height: 44px;
|
|
||||||
border-radius: 50%;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
text-decoration: none;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 4px;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
.btn-instagram:hover {
|
|
||||||
background: #d12e4e;
|
|
||||||
transform: translateY(-3px);
|
|
||||||
box-shadow: 0 8px 20px rgba(228,64,95,0.4);
|
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
.btn-social--wa { background: #25D366; }
|
||||||
|
.btn-social--wa:hover { background: #1ebe57; box-shadow: 0 8px 20px rgba(37,211,102,0.4); }
|
||||||
|
.btn-social--tg { background: #0088cc; }
|
||||||
|
.btn-social--tg:hover { background: #0077b5; box-shadow: 0 8px 20px rgba(0,136,204,0.4); }
|
||||||
|
.btn-social--max { background: #471AFF; }
|
||||||
|
.btn-social--max:hover { background: #3b15d9; box-shadow: 0 8px 20px rgba(71,26,255,0.4); }
|
||||||
|
.btn-social--ig { background: #E4405F; }
|
||||||
|
.btn-social--ig:hover { background: #d12e4e; box-shadow: 0 8px 20px rgba(228,64,95,0.4); }
|
||||||
.max-icon-circle {
|
.max-icon-circle {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -417,7 +353,7 @@ h1, h2, h3, h4 {
|
|||||||
font-size: 0.7em;
|
font-size: 0.7em;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
transition: all 0.3s ease;
|
transition: transform 0.3s ease, box-shadow 0.3s ease, background 0.3s ease, color 0.3s ease, opacity 0.3s ease;
|
||||||
}
|
}
|
||||||
.footer-social a:hover .max-icon-circle-footer {
|
.footer-social a:hover .max-icon-circle-footer {
|
||||||
background: #471AFF;
|
background: #471AFF;
|
||||||
@@ -582,7 +518,7 @@ h1, h2, h3, h4 {
|
|||||||
gap: clamp(10px, 2vw, 15px);
|
gap: clamp(10px, 2vw, 15px);
|
||||||
padding: clamp(10px, 2vw, 15px);
|
padding: clamp(10px, 2vw, 15px);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
transition: all 0.3s ease;
|
transition: transform 0.3s ease, box-shadow 0.3s ease, background 0.3s ease, color 0.3s ease, opacity 0.3s ease;
|
||||||
}
|
}
|
||||||
.feature-item:hover {
|
.feature-item:hover {
|
||||||
background: rgba(212,168,67,0.08);
|
background: rgba(212,168,67,0.08);
|
||||||
@@ -617,7 +553,7 @@ h1, h2, h3, h4 {
|
|||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: 0 10px 40px rgba(0,0,0,0.08);
|
box-shadow: 0 10px 40px rgba(0,0,0,0.08);
|
||||||
transition: all 0.4s ease;
|
transition: transform 0.4s ease, box-shadow 0.4s ease, background 0.4s ease, color 0.4s ease, opacity 0.4s ease;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -773,7 +709,7 @@ h1, h2, h3, h4 {
|
|||||||
border-radius: 50px;
|
border-radius: 50px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: clamp(0.8rem, 1.2vw, 0.9rem);
|
font-size: clamp(0.8rem, 1.2vw, 0.9rem);
|
||||||
transition: all 0.3s ease;
|
transition: transform 0.3s ease, box-shadow 0.3s ease, background 0.3s ease, color 0.3s ease, opacity 0.3s ease;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.btn-book:hover {
|
.btn-book:hover {
|
||||||
@@ -898,7 +834,7 @@ h1, h2, h3, h4 {
|
|||||||
background: rgba(255,255,255,0.08);
|
background: rgba(255,255,255,0.08);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s ease;
|
transition: transform 0.3s ease, box-shadow 0.3s ease, background 0.3s ease, color 0.3s ease, opacity 0.3s ease;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
@@ -939,7 +875,7 @@ h1, h2, h3, h4 {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: clamp(250px, 40vw, 350px);
|
height: clamp(250px, 40vw, 350px);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.4s ease;
|
transition: transform 0.4s ease, box-shadow 0.4s ease, background 0.4s ease, color 0.4s ease, opacity 0.4s ease;
|
||||||
}
|
}
|
||||||
.activity-card:hover {
|
.activity-card:hover {
|
||||||
transform: translateY(-8px);
|
transform: translateY(-8px);
|
||||||
@@ -994,7 +930,7 @@ h1, h2, h3, h4 {
|
|||||||
border: 1px solid rgba(255,255,255,0.1);
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
padding: clamp(20px, 4vw, 35px);
|
padding: clamp(20px, 4vw, 35px);
|
||||||
transition: all 0.3s ease;
|
transition: transform 0.3s ease, box-shadow 0.3s ease, background 0.3s ease, color 0.3s ease, opacity 0.3s ease;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
.review-card:hover {
|
.review-card:hover {
|
||||||
@@ -1101,7 +1037,7 @@ h1, h2, h3, h4 {
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: clamp(0.9rem, 1.5vw, 1.1rem);
|
font-size: clamp(0.9rem, 1.5vw, 1.1rem);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
transition: all 0.3s ease;
|
transition: transform 0.3s ease, box-shadow 0.3s ease, background 0.3s ease, color 0.3s ease, opacity 0.3s ease;
|
||||||
}
|
}
|
||||||
.host-contact:hover {
|
.host-contact:hover {
|
||||||
background: var(--primary-light);
|
background: var(--primary-light);
|
||||||
@@ -1214,7 +1150,7 @@ h1, h2, h3, h4 {
|
|||||||
color: rgba(255,255,255,0.6);
|
color: rgba(255,255,255,0.6);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-size: clamp(0.8rem, 1.2vw, 0.9rem);
|
font-size: clamp(0.8rem, 1.2vw, 0.9rem);
|
||||||
transition: all 0.3s ease;
|
transition: transform 0.3s ease, box-shadow 0.3s ease, background 0.3s ease, color 0.3s ease, opacity 0.3s ease;
|
||||||
}
|
}
|
||||||
.footer-links a:hover {
|
.footer-links a:hover {
|
||||||
color: var(--gold);
|
color: var(--gold);
|
||||||
@@ -1235,7 +1171,7 @@ h1, h2, h3, h4 {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: rgba(255,255,255,0.6);
|
color: rgba(255,255,255,0.6);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
transition: all 0.3s ease;
|
transition: transform 0.3s ease, box-shadow 0.3s ease, background 0.3s ease, color 0.3s ease, opacity 0.3s ease;
|
||||||
}
|
}
|
||||||
.footer-social a:hover {
|
.footer-social a:hover {
|
||||||
background: var(--gold);
|
background: var(--gold);
|
||||||
@@ -1319,7 +1255,7 @@ h1, h2, h3, h4 {
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: clamp(0.8rem, 1vw, 0.9rem);
|
font-size: clamp(0.8rem, 1vw, 0.9rem);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s ease;
|
transition: transform 0.3s ease, box-shadow 0.3s ease, background 0.3s ease, color 0.3s ease, opacity 0.3s ease;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.cookie-accept-btn:hover {
|
.cookie-accept-btn:hover {
|
||||||
@@ -1335,7 +1271,7 @@ h1, h2, h3, h4 {
|
|||||||
.animate-on-scroll {
|
.animate-on-scroll {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(40px);
|
transform: translateY(40px);
|
||||||
transition: all 0.8s ease;
|
transition: opacity 0.8s ease, transform 0.8s ease;
|
||||||
}
|
}
|
||||||
.animate-on-scroll.visible {
|
.animate-on-scroll.visible {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
@@ -1395,7 +1331,7 @@ h1, h2, h3, h4 {
|
|||||||
border-radius: 50px;
|
border-radius: 50px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: clamp(0.9rem, 1.4vw, 1rem);
|
font-size: clamp(0.9rem, 1.4vw, 1rem);
|
||||||
transition: all 0.3s ease;
|
transition: transform 0.3s ease, box-shadow 0.3s ease, background 0.3s ease, color 0.3s ease, opacity 0.3s ease;
|
||||||
position: relative;
|
position: relative;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@@ -1431,7 +1367,7 @@ h1, h2, h3, h4 {
|
|||||||
border: 2px solid #e0e0e0;
|
border: 2px solid #e0e0e0;
|
||||||
padding: 12px 18px;
|
padding: 12px 18px;
|
||||||
font-size: clamp(0.85rem, 1.3vw, 0.95rem);
|
font-size: clamp(0.85rem, 1.3vw, 0.95rem);
|
||||||
transition: all 0.3s ease;
|
transition: transform 0.3s ease, box-shadow 0.3s ease, background 0.3s ease, color 0.3s ease, opacity 0.3s ease;
|
||||||
}
|
}
|
||||||
.form-control:focus, .form-select:focus {
|
.form-control:focus, .form-select:focus {
|
||||||
border-color: var(--primary-light);
|
border-color: var(--primary-light);
|
||||||
@@ -1452,7 +1388,7 @@ h1, h2, h3, h4 {
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: clamp(0.9rem, 1.4vw, 1rem);
|
font-size: clamp(0.9rem, 1.4vw, 1rem);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
transition: all 0.3s ease;
|
transition: transform 0.3s ease, box-shadow 0.3s ease, background 0.3s ease, color 0.3s ease, opacity 0.3s ease;
|
||||||
}
|
}
|
||||||
.btn-submit-booking:hover {
|
.btn-submit-booking:hover {
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
@@ -1475,7 +1411,7 @@ h1, h2, h3, h4 {
|
|||||||
font-size: clamp(1.2rem, 3vw, 1.5rem);
|
font-size: clamp(1.2rem, 3vw, 1.5rem);
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
box-shadow: 0 5px 25px rgba(37,211,102,0.4);
|
box-shadow: 0 5px 25px rgba(37,211,102,0.4);
|
||||||
transition: all 0.3s ease;
|
transition: transform 0.3s ease, box-shadow 0.3s ease, background 0.3s ease, color 0.3s ease, opacity 0.3s ease;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
animation: phonePulse 2s ease infinite;
|
animation: phonePulse 2s ease infinite;
|
||||||
}
|
}
|
||||||
@@ -1602,7 +1538,7 @@ h1, h2, h3, h4 {
|
|||||||
.reveal-img {
|
.reveal-img {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: scale(0.95);
|
transform: scale(0.95);
|
||||||
transition: all 0.6s ease;
|
transition: transform 0.6s ease, box-shadow 0.6s ease, background 0.6s ease, opacity 0.6s ease;
|
||||||
}
|
}
|
||||||
.reveal-img.visible {
|
.reveal-img.visible {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
@@ -1688,7 +1624,7 @@ h1, h2, h3, h4 {
|
|||||||
border: 1px solid rgba(255,255,255,0.1);
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
padding: clamp(18px, 3.5vw, 28px);
|
padding: clamp(18px, 3.5vw, 28px);
|
||||||
transition: all 0.35s ease;
|
transition: transform 0.35s ease, box-shadow 0.35s ease, background 0.35s ease, color 0.35s ease, opacity 0.35s ease;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
.tips-card:hover {
|
.tips-card:hover {
|
||||||
@@ -1799,7 +1735,7 @@ h1, h2, h3, h4 {
|
|||||||
border: 1px solid rgba(255,255,255,0.1);
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
padding: clamp(14px, 2.5vw, 20px);
|
padding: clamp(14px, 2.5vw, 20px);
|
||||||
transition: all 0.35s ease;
|
transition: transform 0.35s ease, box-shadow 0.35s ease, background 0.35s ease, color 0.35s ease, opacity 0.35s ease;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,10 +20,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Navigation -->
|
<!-- Navigation -->
|
||||||
<nav class="navbar navbar-expand-lg fixed-top">
|
<nav class="navbar navbar-expand-lg fixed-top" id="mainNavbar">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<a class="navbar-brand" href="#">HOTEL <span>777</span></a>
|
<a class="navbar-brand" href="#">HOTEL <span>777</span></a>
|
||||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navMenu">
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navMenu"
|
||||||
|
aria-label="Меню" aria-expanded="false" aria-controls="navMenu">
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
<div class="collapse navbar-collapse" id="navMenu">
|
<div class="collapse navbar-collapse" id="navMenu">
|
||||||
@@ -61,10 +62,10 @@
|
|||||||
<div class="hero-buttons">
|
<div class="hero-buttons">
|
||||||
<a href="#rooms" class="btn-gold"><i class="fas fa-bed"></i> <span data-i18n="hero.btn_rooms">Выбрать номер</span></a>
|
<a href="#rooms" class="btn-gold"><i class="fas fa-bed"></i> <span data-i18n="hero.btn_rooms">Выбрать номер</span></a>
|
||||||
<a href="tel:89409270080" class="btn-outline-white" data-phone="tel"><i class="fas fa-phone"></i> <span data-i18n="hero.btn_call">Позвонить</span></a>
|
<a href="tel:89409270080" class="btn-outline-white" data-phone="tel"><i class="fas fa-phone"></i> <span data-i18n="hero.btn_call">Позвонить</span></a>
|
||||||
<a href="https://wa.me/89409270080" class="btn-whatsapp" target="_blank" title="Написать в WhatsApp" data-phone="wa"><i class="fab fa-whatsapp"></i></a>
|
<a href="https://wa.me/89409270080" class="btn-social btn-social--wa" target="_blank" title="Написать в WhatsApp" data-phone="wa"><i class="fab fa-whatsapp"></i></a>
|
||||||
<a href="https://t.me/+89409270080" class="btn-telegram" target="_blank" title="Написать в Telegram" data-phone="tg"><i class="fab fa-telegram"></i></a>
|
<a href="https://t.me/+89409270080" class="btn-social btn-social--tg" target="_blank" title="Написать в Telegram" data-phone="tg"><i class="fab fa-telegram"></i></a>
|
||||||
<a href="https://max.ru/kalugin66" class="btn-max" target="_blank" title="Написать в Max" data-phone="max"><span class="max-icon-circle">M</span></a>
|
<a href="https://max.ru/kalugin66" class="btn-social btn-social--max" target="_blank" title="Написать в Max" data-phone="max" aria-label="Max messenger"><span class="max-icon-circle">M</span></a>
|
||||||
<a href="#" data-phone="ig" class="btn-instagram" target="_blank" title="Instagram"><i class="fab fa-instagram"></i></a>
|
<a href="#" data-phone="ig" class="btn-social btn-social--ig" target="_blank" title="Instagram"><i class="fab fa-instagram"></i></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -723,7 +724,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer-bottom">
|
<div class="footer-bottom">
|
||||||
<p data-i18n="footer.copyright">© 2025 Hotel 777. Хозяин — Рауф Алексеевич. Все права защищены. Абхазия, Гудаутский район, с. Мгудзырхуа.</p>
|
<p data-i18n="footer.copyright">© <span id="copyrightYear">2025</span> Hotel 777. Хозяин — Рауф Алексеевич. Все права защищены. Абхазия, Гудаутский район, с. Мгудзырхуа.</p>
|
||||||
<p class="footer-legal">
|
<p class="footer-legal">
|
||||||
<a href="/privacy">Политика конфиденциальности</a>
|
<a href="/privacy">Политика конфиденциальности</a>
|
||||||
<span class="footer-legal-divider">|</span>
|
<span class="footer-legal-divider">|</span>
|
||||||
@@ -740,7 +741,7 @@
|
|||||||
|
|
||||||
<!-- Booking Modal -->
|
<!-- Booking Modal -->
|
||||||
<div class="modal fade" id="bookingModal" tabindex="-1">
|
<div class="modal fade" id="bookingModal" tabindex="-1">
|
||||||
<div class="modal-dialog modal-dialog-centered">
|
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title" style="font-family: 'Playfair Display', serif; font-size: 1.5rem;">
|
<h5 class="modal-title" style="font-family: 'Playfair Display', serif; font-size: 1.5rem;">
|
||||||
@@ -751,33 +752,36 @@
|
|||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<form id="bookingForm">
|
<form id="bookingForm">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label"><span data-i18n="booking.selected_room">Выбранный номер</span></label>
|
<label class="form-label"><i class="fas fa-door-open me-1"></i> <span data-i18n="booking.selected_room">Выбранный номер</span></label>
|
||||||
<input type="text" class="form-control" id="selectedRoom" name="room" readonly style="background: #f0f4f8; font-weight: 600;">
|
<input type="text" class="form-control" id="selectedRoom" name="room" readonly style="background: #f0f4f8; font-weight: 600;">
|
||||||
<input type="hidden" id="selectedRoomId" name="room_id">
|
<input type="hidden" id="selectedRoomId" name="room_id">
|
||||||
</div>
|
</div>
|
||||||
<div class="row g-3">
|
<div class="row g-2">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label"><span data-i18n="booking.your_name">Ваше имя</span></label>
|
<label class="form-label"><i class="fas fa-user me-1"></i> <span data-i18n="booking.your_name">Ваше имя</span></label>
|
||||||
<input type="text" class="form-control" name="name" data-i18n-placeholder="booking.name_placeholder" placeholder="Иван Иванов" required>
|
<input type="text" class="form-control" name="name" data-i18n-placeholder="booking.name_placeholder" placeholder="Иван Иванов" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label"><span data-i18n="booking.phone">Телефон</span></label>
|
<label class="form-label"><i class="fas fa-phone me-1"></i> <span data-i18n="booking.phone">Телефон</span></label>
|
||||||
<input type="tel" class="form-control" name="phone" data-i18n-placeholder="booking.phone_placeholder" placeholder="+7 (___) ___-__-__" required>
|
<input type="tel" class="form-control" name="phone" data-i18n-placeholder="booking.phone_placeholder" placeholder="+7 (___) ___-__-__" required>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row g-3 mt-1">
|
<div class="row g-2 mt-2">
|
||||||
<div class="col-md-6">
|
<div class="col-5">
|
||||||
<label class="form-label"><span data-i18n="booking.checkin">Дата заезда</span></label>
|
<label class="form-label"><i class="fas fa-calendar-alt me-1"></i> <span data-i18n="booking.checkin">Дата заезда</span></label>
|
||||||
<input type="date" class="form-control" name="checkin" required>
|
<input type="date" class="form-control" name="checkin" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-2 d-flex align-items-center justify-content-center" style="padding-top: 1.8rem;">
|
||||||
<label class="form-label"><span data-i18n="booking.checkout">Дата выезда</span></label>
|
<span id="nightsCount" style="background:#f0f4f8;padding:4px 12px;border-radius:20px;font-size:0.85rem;color:#64748b;white-space:nowrap;">1 ночь</span>
|
||||||
|
</div>
|
||||||
|
<div class="col-5">
|
||||||
|
<label class="form-label"><i class="fas fa-calendar-check me-1"></i> <span data-i18n="booking.checkout">Дата выезда</span></label>
|
||||||
<input type="date" class="form-control" name="checkout" required>
|
<input type="date" class="form-control" name="checkout" required>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row g-3 mt-1">
|
<div class="row g-2 mt-2">
|
||||||
<div class="col-md-6">
|
<div class="col-md-5">
|
||||||
<label class="form-label"><span data-i18n="booking.guests">Кол-во гостей</span></label>
|
<label class="form-label"><i class="fas fa-users me-1"></i> <span data-i18n="booking.guests">Кол-во гостей</span></label>
|
||||||
<select class="form-select" name="guests">
|
<select class="form-select" name="guests">
|
||||||
<option value="1" data-i18n="booking.guest_1">1 гость</option>
|
<option value="1" data-i18n="booking.guest_1">1 гость</option>
|
||||||
<option value="2" data-i18n="booking.guest_2">2 гостя</option>
|
<option value="2" data-i18n="booking.guest_2">2 гостя</option>
|
||||||
@@ -785,21 +789,19 @@
|
|||||||
<option value="4" data-i18n="booking.guest_4">4 гостя</option>
|
<option value="4" data-i18n="booking.guest_4">4 гостя</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-7">
|
||||||
<label class="form-label"><span data-i18n="booking.wishes">Пожелания</span></label>
|
<label class="form-label"><i class="fas fa-comment me-1"></i> <span data-i18n="booking.wishes">Пожелания</span></label>
|
||||||
<input type="text" class="form-control" name="wishes" data-i18n-placeholder="booking.wishes_placeholder" placeholder="Например: вид на море">
|
<input type="text" class="form-control" name="wishes" data-i18n-placeholder="booking.wishes_placeholder" placeholder="Вид на море, этаж...">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row g-3 mt-1">
|
<div class="mt-2">
|
||||||
<div class="col-md-6">
|
<label class="form-label"><i class="fas fa-tag me-1"></i> <span data-i18n="booking.promocode">Промокод</span></label>
|
||||||
<label class="form-label"><span data-i18n="booking.promocode">Промокод</span></label>
|
<div class="input-group">
|
||||||
<input type="text" class="form-control" id="promocodeInput" name="promocode" data-i18n-placeholder="booking.promocode_placeholder" placeholder="Введите промокод" style="text-transform: uppercase;">
|
<input type="text" class="form-control" id="promocodeInput" name="promocode" data-i18n-placeholder="booking.promocode_placeholder" placeholder="Введите промокод" style="text-transform: uppercase;">
|
||||||
|
<button type="button" class="btn btn-outline-primary" id="checkPromocodeBtn"><i class="fas fa-check me-1"></i><span data-i18n="booking.promocode_check">Проверить</span></button>
|
||||||
|
</div>
|
||||||
<div id="promocodeError" class="text-danger small mt-1" style="display: none;"></div>
|
<div id="promocodeError" class="text-danger small mt-1" style="display: none;"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 d-flex align-items-end">
|
|
||||||
<button type="button" class="btn btn-outline-primary btn-sm" id="checkPromocodeBtn" style="width: 100%;"><span data-i18n="booking.promocode_check">Проверить промокод</span></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="priceInfo" class="mt-3" style="display: none; padding: 15px; background: #f8f9fa; border-radius: 10px;">
|
<div id="priceInfo" class="mt-3" style="display: none; padding: 15px; background: #f8f9fa; border-radius: 10px;">
|
||||||
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
|
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
|
||||||
<span data-i18n="booking.base_price">Базовая стоимость:</span>
|
<span data-i18n="booking.base_price">Базовая стоимость:</span>
|
||||||
@@ -885,6 +887,7 @@
|
|||||||
if (instagramId) href = 'https://instagram.com/' + instagramId;
|
if (instagramId) href = 'https://instagram.com/' + instagramId;
|
||||||
active = instagramActive === 'true';
|
active = instagramActive === 'true';
|
||||||
}
|
}
|
||||||
|
if (!/^(https?:\/\/|tel:|mailto:|#)/.test(href)) href = '#';
|
||||||
el.setAttribute('href', href);
|
el.setAttribute('href', href);
|
||||||
if (href === '#') active = false;
|
if (href === '#') active = false;
|
||||||
if (!active) el.style.display = 'none';
|
if (!active) el.style.display = 'none';
|
||||||
@@ -904,7 +907,7 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(function() {});
|
.catch(function(err) { console.error('Config load failed:', err); });
|
||||||
|
|
||||||
function acceptCookies() {
|
function acceptCookies() {
|
||||||
try { localStorage.setItem('cookie_consent', '1'); } catch(e) {}
|
try { localStorage.setItem('cookie_consent', '1'); } catch(e) {}
|
||||||
|
|||||||
@@ -84,6 +84,27 @@ const checkoutInput = document.querySelector('[name="checkout"]');
|
|||||||
if (checkinInput) checkinInput.min = today;
|
if (checkinInput) checkinInput.min = today;
|
||||||
if (checkoutInput) checkoutInput.min = today;
|
if (checkoutInput) checkoutInput.min = today;
|
||||||
|
|
||||||
|
checkinInput.addEventListener('change', function() {
|
||||||
|
if (this.value) {
|
||||||
|
var next = new Date(this.value);
|
||||||
|
next.setDate(next.getDate() + 1);
|
||||||
|
checkoutInput.min = next.toISOString().split('T')[0];
|
||||||
|
if (checkoutInput.value && checkoutInput.value <= this.value) {
|
||||||
|
checkoutInput.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateNightsCount();
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateNightsCount() {
|
||||||
|
var ci = checkinInput.value, co = checkoutInput.value;
|
||||||
|
var el = document.getElementById('nightsCount');
|
||||||
|
if (ci && co && el) {
|
||||||
|
var n = Math.ceil((new Date(co) - new Date(ci)) / 86400000);
|
||||||
|
if (n > 0) el.textContent = n + ' ' + (n === 1 ? 'ночь' : n < 5 ? 'ночи' : 'ночей');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Booking modal - set room name
|
// Booking modal - set room name
|
||||||
document.querySelectorAll('.btn-book').forEach(btn => {
|
document.querySelectorAll('.btn-book').forEach(btn => {
|
||||||
btn.addEventListener('click', function() {
|
btn.addEventListener('click', function() {
|
||||||
@@ -200,8 +221,8 @@ function validateBookingDates() {
|
|||||||
return checkinInput.checkValidity() && checkoutInput.checkValidity();
|
return checkinInput.checkValidity() && checkoutInput.checkValidity();
|
||||||
}
|
}
|
||||||
|
|
||||||
document.querySelector('[name="checkin"]').addEventListener('change', function() { validateBookingDates(); if (typeof checkRoomAvailability === 'function') checkRoomAvailability(); });
|
document.querySelector('[name="checkin"]').addEventListener('change', function() { validateBookingDates(); if (typeof checkRoomAvailability === 'function') checkRoomAvailability(); updateNightsCount(); });
|
||||||
document.querySelector('[name="checkout"]').addEventListener('change', function() { validateBookingDates(); if (typeof checkRoomAvailability === 'function') checkRoomAvailability(); });
|
document.querySelector('[name="checkout"]').addEventListener('change', function() { validateBookingDates(); if (typeof checkRoomAvailability === 'function') checkRoomAvailability(); updateNightsCount(); });
|
||||||
|
|
||||||
function updatePriceDisplay(basePrice, discountPercent, discountAmount, totalPrice) {
|
function updatePriceDisplay(basePrice, discountPercent, discountAmount, totalPrice) {
|
||||||
document.getElementById('basePriceDisplay').textContent = basePrice + ' ₽';
|
document.getElementById('basePriceDisplay').textContent = basePrice + ' ₽';
|
||||||
@@ -532,6 +553,14 @@ window.addEventListener('scroll', () => {
|
|||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
initHeroSlideshow();
|
initHeroSlideshow();
|
||||||
|
var cy = document.getElementById('copyrightYear');
|
||||||
|
if (cy) cy.textContent = new Date().getFullYear();
|
||||||
|
var navToggler = document.querySelector('.navbar-toggler');
|
||||||
|
var navMenu = document.getElementById('navMenu');
|
||||||
|
if (navToggler && navMenu) {
|
||||||
|
navMenu.addEventListener('show.bs.collapse', () => navToggler.setAttribute('aria-expanded', 'true'));
|
||||||
|
navMenu.addEventListener('hide.bs.collapse', () => navToggler.setAttribute('aria-expanded', 'false'));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Gallery Lightbox
|
// Gallery Lightbox
|
||||||
@@ -540,6 +569,7 @@ function openLightbox(src, caption) {
|
|||||||
const img = document.getElementById('galleryLightboxImg');
|
const img = document.getElementById('galleryLightboxImg');
|
||||||
const cap = document.getElementById('galleryLightboxCaption');
|
const cap = document.getElementById('galleryLightboxCaption');
|
||||||
img.src = src;
|
img.src = src;
|
||||||
|
img.alt = caption || 'Фото отеля Hotel 777';
|
||||||
cap.textContent = caption || '';
|
cap.textContent = caption || '';
|
||||||
lb.classList.add('active');
|
lb.classList.add('active');
|
||||||
document.body.style.overflow = 'hidden';
|
document.body.style.overflow = 'hidden';
|
||||||
|
|||||||
59
server.js
59
server.js
@@ -7,6 +7,8 @@ const jwt = require('jsonwebtoken');
|
|||||||
const bcrypt = require('bcryptjs');
|
const bcrypt = require('bcryptjs');
|
||||||
const client = require('prom-client');
|
const client = require('prom-client');
|
||||||
const multer = require('multer');
|
const multer = require('multer');
|
||||||
|
const helmet = require('helmet');
|
||||||
|
const cookieParser = require('cookie-parser');
|
||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
|
|
||||||
const config = require('./config');
|
const config = require('./config');
|
||||||
@@ -168,9 +170,9 @@ const roomAvailability = new client.Gauge({
|
|||||||
registers: [register]
|
registers: [register]
|
||||||
});
|
});
|
||||||
|
|
||||||
const loginAttempts = new client.Gauge({
|
const loginAttempts = new client.Counter({
|
||||||
name: 'login_attempts',
|
name: 'login_attempts_total',
|
||||||
help: 'Number of login attempts',
|
help: 'Total number of login attempts',
|
||||||
registers: [register]
|
registers: [register]
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -180,6 +182,11 @@ if (!API_KEY) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
app.use(express.json({ limit: '1mb' }));
|
app.use(express.json({ limit: '1mb' }));
|
||||||
|
app.use(helmet({
|
||||||
|
contentSecurityPolicy: false,
|
||||||
|
crossOriginEmbedderPolicy: false
|
||||||
|
}));
|
||||||
|
app.use(cookieParser());
|
||||||
const CORS_ORIGIN = process.env.CORS_ORIGIN || '*';
|
const CORS_ORIGIN = process.env.CORS_ORIGIN || '*';
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
res.header('Access-Control-Allow-Origin', CORS_ORIGIN);
|
res.header('Access-Control-Allow-Origin', CORS_ORIGIN);
|
||||||
@@ -309,7 +316,9 @@ setTimeout(updateMetrics, 5000);
|
|||||||
const dataDir = path.join(__dirname, 'data');
|
const dataDir = path.join(__dirname, 'data');
|
||||||
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir);
|
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir);
|
||||||
const dbPath = path.join(dataDir, 'bookings.db');
|
const dbPath = path.join(dataDir, 'bookings.db');
|
||||||
const db = new sqlite3.Database(dbPath);
|
const db = new sqlite3.Database(dbPath, (err) => {
|
||||||
|
if (err) { console.error('FATAL: Cannot open database:', err.message); process.exit(1); }
|
||||||
|
});
|
||||||
|
|
||||||
initDatabase(db);
|
initDatabase(db);
|
||||||
|
|
||||||
@@ -377,28 +386,28 @@ function initDefaultRooms() {
|
|||||||
if (err) return console.error('Check rooms count error:', err);
|
if (err) return console.error('Check rooms count error:', err);
|
||||||
if (row.count > 0) return;
|
if (row.count > 0) return;
|
||||||
|
|
||||||
|
db.serialize(() => {
|
||||||
|
db.run("BEGIN TRANSACTION");
|
||||||
const stmt = db.prepare(`INSERT INTO rooms (type, name, description, rooms_count, area_sqm, max_guests, furniture, amenities, floors, price_per_night, image_path, extra_beds, extra_bed_price, is_active) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)`);
|
const stmt = db.prepare(`INSERT INTO rooms (type, name, description, rooms_count, area_sqm, max_guests, furniture, amenities, floors, price_per_night, image_path, extra_beds, extra_bed_price, is_active) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)`);
|
||||||
|
|
||||||
config.DEFAULT_ROOMS.forEach(r => {
|
config.DEFAULT_ROOMS.forEach(r => {
|
||||||
stmt.run(
|
stmt.run(
|
||||||
r.type,
|
r.type, r.name, r.description,
|
||||||
r.name,
|
r.rooms_count, r.area_sqm, r.max_guests,
|
||||||
r.description,
|
|
||||||
r.rooms_count,
|
|
||||||
r.area_sqm,
|
|
||||||
r.max_guests,
|
|
||||||
JSON.stringify(r.furniture || []),
|
JSON.stringify(r.furniture || []),
|
||||||
JSON.stringify(r.amenities || []),
|
JSON.stringify(r.amenities || []),
|
||||||
JSON.stringify(r.floors || []),
|
JSON.stringify(r.floors || []),
|
||||||
r.price_per_night,
|
r.price_per_night, r.image_path,
|
||||||
r.image_path,
|
r.extra_beds || 0, r.extra_bed_price || 0
|
||||||
r.extra_beds || 0,
|
|
||||||
r.extra_bed_price || 0
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
stmt.finalize();
|
stmt.finalize();
|
||||||
console.log('✅ Default rooms initialized');
|
db.run("COMMIT", (err) => {
|
||||||
|
if (err) { console.error('Default rooms commit error:', err); db.run("ROLLBACK"); }
|
||||||
|
else console.log('✅ Default rooms initialized');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -407,7 +416,7 @@ function syncAdmin() {
|
|||||||
console.warn('WARNING: ADMIN_LOGIN or ADMIN_PASSWORD not set, skipping admin sync');
|
console.warn('WARNING: ADMIN_LOGIN or ADMIN_PASSWORD not set, skipping admin sync');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const hash = bcrypt.hashSync(ADMIN_PASSWORD, 10);
|
bcrypt.hash(ADMIN_PASSWORD, 10).then(hash => {
|
||||||
db.get(`SELECT id, role FROM users WHERE login = ?`, [ADMIN_LOGIN], (err, row) => {
|
db.get(`SELECT id, role FROM users WHERE login = ?`, [ADMIN_LOGIN], (err, row) => {
|
||||||
if (err) { console.error('Admin sync error:', err); return; }
|
if (err) { console.error('Admin sync error:', err); return; }
|
||||||
if (row) {
|
if (row) {
|
||||||
@@ -423,6 +432,7 @@ function syncAdmin() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}).catch(err => console.error('Admin hash error:', err));
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -503,10 +513,6 @@ app.get('/api/countries', (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function safeJsonString(str) {
|
|
||||||
return str.replace(/'/g, "'").replace(/\\"/g, '"');
|
|
||||||
}
|
|
||||||
|
|
||||||
app.get('/api/cities/:countryCode', (req, res) => {
|
app.get('/api/cities/:countryCode', (req, res) => {
|
||||||
const countryCode = req.params.countryCode;
|
const countryCode = req.params.countryCode;
|
||||||
|
|
||||||
@@ -520,20 +526,11 @@ app.get('/api/cities/:countryCode', (req, res) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const data = fs.readFileSync(filePath, 'utf8');
|
const data = fs.readFileSync(filePath, 'utf8');
|
||||||
|
|
||||||
let cities = [];
|
let cities = [];
|
||||||
const arrayMatch = data.match(/const\s+CITIES_\w+\s*=\s*(\[.*?\]);/s);
|
|
||||||
if (arrayMatch) {
|
|
||||||
try {
|
try {
|
||||||
cities = JSON.parse(safeJsonString(arrayMatch[1]));
|
cities = new Function(data.replace(/^const\s+\w+\s*=\s*/, 'return '))();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const jsMatch = arrayMatch[1].replace(/'/g, '"');
|
console.error('Failed to parse cities:', e);
|
||||||
try {
|
|
||||||
cities = JSON.parse(jsMatch);
|
|
||||||
} catch (e2) {
|
|
||||||
console.error('Failed to parse cities:', e2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({ cities, popular: [], countryCode });
|
res.json({ cities, popular: [], countryCode });
|
||||||
|
|||||||
Reference in New Issue
Block a user