прибрался
This commit is contained in:
@@ -77,6 +77,16 @@ tr.row-checkout-today { background: #fef2f2 !important; border-left: 4px solid #
|
||||
.filter-bar .sort-btn { background: var(--primary); color: #fff; border: none; }
|
||||
.filter-bar .sort-btn:hover { background: #1d4ed8; }
|
||||
|
||||
.history-timeline { border-left: 2px solid #e2e8f0; padding-left: 20px; }
|
||||
.history-item { position: relative; padding: 12px 0; border-bottom: 1px solid #f1f5f9; }
|
||||
.history-item:last-child { border-bottom: none; }
|
||||
.history-item::before { content: ''; position: absolute; left: -27px; top: 16px; width: 12px; height: 12px; border-radius: 50%; background: var(--primary); border: 2px solid #fff; }
|
||||
.history-item .history-date { font-size: 0.75rem; color: #94a3b8; }
|
||||
.history-item .history-user { font-size: 0.8rem; color: #64748b; font-weight: 500; }
|
||||
.history-item .history-change { font-size: 0.85rem; color: var(--dark); margin-top: 4px; }
|
||||
.history-item .history-change .old-val { text-decoration: line-through; color: #ef4444; background: #fee2e2; padding: 1px 6px; border-radius: 4px; }
|
||||
.history-item .history-change .new-val { color: #16a34a; background: #dcfce7; padding: 1px 6px; border-radius: 4px; }
|
||||
|
||||
.modal-backdrop-custom { position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 200; display: none; align-items: center; justify-content: center; }
|
||||
.modal-backdrop-custom.show { display: flex; }
|
||||
.modal-custom { background: #fff; border-radius: 16px; width: 100%; max-width: 480px; max-height: 90vh; overflow-y: auto; }
|
||||
@@ -151,6 +161,7 @@ tr.row-checkout-today { background: #fef2f2 !important; border-left: 4px solid #
|
||||
<a href="#" class="active" data-tab="dashboard"><i class="fas fa-chart-pie"></i> Дашборд</a>
|
||||
<a href="#" data-tab="users"><i class="fas fa-users"></i> Пользователи</a>
|
||||
<a href="#" data-tab="bookings"><i class="fas fa-calendar-check"></i> Бронирования</a>
|
||||
<a href="#" data-tab="promocodes"><i class="fas fa-ticket-alt"></i> Промокоды</a>
|
||||
<a href="#" data-tab="profile"><i class="fas fa-user-circle"></i> Профиль</a>
|
||||
<a href="/" style="margin-top: 8px; color: #94a3b8; border-top: 1px solid rgba(255,255,255,0.1); padding-top: 16px;"><i class="fas fa-arrow-left"></i> На сайт</a>
|
||||
</nav>
|
||||
@@ -194,7 +205,7 @@ tr.row-checkout-today { background: #fef2f2 !important; border-left: 4px solid #
|
||||
<div class="card-header-custom"><h3>Последние бронирования</h3></div>
|
||||
<div class="card-body-custom">
|
||||
<table class="table">
|
||||
<thead><tr><th>Имя</th><th>Телефон</th><th>Заезд</th><th>Выезд</th><th>Статус</th></tr></thead>
|
||||
<thead><tr><th>Имя</th><th>Номер</th><th>Заезд</th><th>Выезд</th><th>Статус</th></tr></thead>
|
||||
<tbody id="recentBookings"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -234,7 +245,7 @@ tr.row-checkout-today { background: #fef2f2 !important; border-left: 4px solid #
|
||||
</div>
|
||||
<div style="overflow-x: auto;">
|
||||
<table class="table">
|
||||
<thead><tr><th>Имя</th><th>Телефон</th><th>Взр.</th><th>Дет.</th><th>Заезд</th><th>Выезд</th><th>Пожелания</th><th>Статус</th><th>Действия</th></tr></thead>
|
||||
<thead><tr><th>Имя</th><th>Телефон</th><th>Номер</th><th>Взр.</th><th>Дет.</th><th>Заезд</th><th>Выезд</th><th>Пожелания</th><th>Комментарий</th><th>База (₽)</th><th>Скидка (%)</th><th>Сумма скидки (₽)</th><th>Итого (₽)</th><th>Промокод</th><th>Статус</th><th>Действия</th></tr></thead>
|
||||
<tbody id="allBookings"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -242,6 +253,21 @@ tr.row-checkout-today { background: #fef2f2 !important; border-left: 4px solid #
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="tab-promocodes" class="tab-content">
|
||||
<div class="top-bar">
|
||||
<h1>Промокоды</h1>
|
||||
<button class="btn-primary-custom admin-only" onclick="showPromocodeModal()"><i class="fas fa-plus"></i> Добавить</button>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-body-custom">
|
||||
<table class="table">
|
||||
<thead><tr><th>Код</th><th>Скидка (%)</th><th>Действует с</th><th>Действует по</th><th>Период (дн)</th><th>Статус</th><th class="admin-only">Действия</th></tr></thead>
|
||||
<tbody id="promocodesTable"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="tab-profile" class="tab-content">
|
||||
<div class="top-bar"><h1>Мой профиль</h1></div>
|
||||
<div class="card">
|
||||
@@ -335,6 +361,63 @@ tr.row-checkout-today { background: #fef2f2 !important; border-left: 4px solid #
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-backdrop-custom" id="historyModal">
|
||||
<div class="modal-custom" style="max-width: 600px;">
|
||||
<div class="modal-header-custom">
|
||||
<h3 id="historyModalTitle">История изменений</h3>
|
||||
<button class="modal-close" onclick="hideHistoryModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body-custom" style="max-height: 400px; overflow-y: auto;">
|
||||
<div id="historyContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-backdrop-custom" id="promocodeModal">
|
||||
<div class="modal-custom">
|
||||
<div class="modal-header-custom">
|
||||
<h3 id="promocodeModalTitle">Добавить промокод</h3>
|
||||
<button class="modal-close" onclick="hidePromocodeModal()">×</button>
|
||||
</div>
|
||||
<form id="promocodeForm">
|
||||
<input type="hidden" id="editPromocodeId">
|
||||
<div class="modal-body-custom">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Код *</label>
|
||||
<input type="text" class="form-control" id="promocodeCode" required placeholder="SUMMER2026" style="text-transform: uppercase;">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Скидка (%) * от 1 до 99</label>
|
||||
<input type="number" class="form-control" id="promocodeDiscount" required min="1" max="99" value="10">
|
||||
</div>
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Действует с</label>
|
||||
<input type="datetime-local" class="form-control" id="promocodeValidFrom">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Действует по</label>
|
||||
<input type="datetime-local" class="form-control" id="promocodeValidTo">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Или период (дней от сейчас)</label>
|
||||
<input type="number" class="form-control" id="promocodeValidDays" min="1" placeholder="Например: 30">
|
||||
<small class="text-muted">Если указано, отменяет даты "с" и "по"</small>
|
||||
</div>
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input" id="promocodeIsActive" checked>
|
||||
<label class="form-check-label" for="promocodeIsActive">Активен</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer-custom">
|
||||
<button type="button" class="btn btn-secondary btn-sm" onclick="hidePromocodeModal()">Отмена</button>
|
||||
<button type="submit" class="btn-primary-custom btn-sm">Сохранить</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API = '';
|
||||
let token = localStorage.getItem('token');
|
||||
@@ -370,6 +453,7 @@ function initTabs() {
|
||||
if (tab === 'dashboard') loadDashboard();
|
||||
if (tab === 'users') loadUsers();
|
||||
if (tab === 'bookings') { bookingsLoaded = false; loadBookings(); }
|
||||
if (tab === 'promocodes') loadPromocodes();
|
||||
if (tab === 'profile') loadProfile();
|
||||
});
|
||||
});
|
||||
@@ -444,7 +528,7 @@ async function loadDashboard() {
|
||||
document.getElementById('statBookings').textContent = rows.length;
|
||||
document.getElementById('statNew').textContent = rows.filter(r => r.status === 'новая').length;
|
||||
document.getElementById('recentBookings').innerHTML = rows.filter(r => r.status !== 'отменена' && r.status !== 'выехала').slice(0, 5).map(r => '<tr>' +
|
||||
'<td>' + esc(r.name) + '</td><td>' + esc(r.phone) + '</td>' +
|
||||
'<td>' + esc(r.name) + '</td><td>' + esc(r.room_type || '—') + '</td>' +
|
||||
'<td>' + esc(r.checkin_date) + '</td><td>' + esc(r.checkout_date) + '</td>' +
|
||||
'<td><span class="badge badge-status badge-status-' + r.status + '">' + r.status + '</span></td></tr>').join('');
|
||||
if (!rows.filter(r => r.status !== 'отменена' && r.status !== 'выехала').length) {
|
||||
@@ -518,11 +602,11 @@ function renderBookings() {
|
||||
});
|
||||
|
||||
const statuses = ['новая', 'оплачена', 'зарезервирована', 'заселена', 'выехала', 'отменена'];
|
||||
const statusIcons = { 'новая': 'fa-file', 'оплачена': 'fa-check', 'зарезервирована': 'fa-calendar', 'заселена': 'fa-door-open', 'выехала': 'fa-door-closed', 'отменена': 'fa-ban' };
|
||||
const rooms = ['Эконом', 'Стандарт', 'VIP Люкс'];
|
||||
|
||||
const tbody = document.getElementById('allBookings');
|
||||
if (rows.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="9" class="text-center text-muted">Нет записей</td></tr>';
|
||||
tbody.innerHTML = '<tr><td colspan="10" class="text-center text-muted">Нет записей</td></tr>';
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = rows.map(r => {
|
||||
@@ -533,11 +617,25 @@ function renderBookings() {
|
||||
if (checkoutDiff === 0 && r.status !== 'отменена' && r.status !== 'выехала') rowClass = 'row-checkout-today';
|
||||
|
||||
const selectOpts = statuses.map(s => '<option value="' + s + '"' + (r.status === s ? ' selected' : '') + '>' + s.charAt(0).toUpperCase() + s.slice(1) + '</option>').join('');
|
||||
const roomOpts = '<option value="">—</option>' + rooms.map(s => '<option value="' + s + '"' + (r.room_type === s ? ' selected' : '') + '>' + s + '</option>').join('');
|
||||
|
||||
const commentHtml = '<input type="text" class="form-control form-control-sm" style="width: 120px; font-size: 0.8rem;" value="' + esc(r.comment || '') + '" onchange="changeComment(' + r.id + ', this.value)" placeholder="Комментарий">';
|
||||
const discountHtml = '<input type="number" min="0" max="99" class="form-control form-control-sm" style="width: 70px; font-size: 0.8rem;" value="' + (r.discount_percent || 0) + '" onchange="changeDiscount(' + r.id + ', this.value)">';
|
||||
|
||||
return '<tr class="' + rowClass + '">' +
|
||||
'<td><strong>' + esc(r.name) + '</strong></td><td>' + esc(r.phone) + '</td><td>' + r.adults + '</td><td>' + r.children + '</td>' +
|
||||
'<td>' + esc(r.checkin_date) + '</td><td>' + esc(r.checkout_date) + '</td><td>' + esc(r.wishes || '—') + '</td>' +
|
||||
'<td><strong>' + esc(r.name) + '</strong></td><td>' + esc(r.phone) + '</td>' +
|
||||
'<td><select class="form-select form-select-sm room-select" style="width: 120px; font-size: 0.8rem;" data-booking-id="' + r.id + '" onchange="changeRoom(' + r.id + ', this.value)">' + roomOpts + '</select></td>' +
|
||||
'<td>' + r.adults + '</td><td>' + r.children + '</td>' +
|
||||
'<td>' + esc(r.checkin_date) + '</td><td>' + esc(r.checkout_date) + '</td>' +
|
||||
'<td>' + esc(r.wishes || '—') + '</td>' +
|
||||
'<td>' + commentHtml + '</td>' +
|
||||
'<td>' + (r.base_price || 0) + '</td>' +
|
||||
'<td>' + discountHtml + '</td>' +
|
||||
'<td>' + (r.discount_amount || 0) + '</td>' +
|
||||
'<td><strong>' + (r.total_price || r.base_price || 0) + '</strong></td>' +
|
||||
'<td><span class="badge bg-info" style="font-size: 0.7rem;">' + esc(r.promocode_code || '—') + '</span></td>' +
|
||||
'<td><span class="badge badge-status badge-status-' + r.status + '">' + r.status + '</span></td>' +
|
||||
'<td><select class="form-select form-select-sm" style="width: 140px; font-size: 0.8rem;" onchange="changeStatus(' + r.id + ', this.value)">' + selectOpts + '</select></td>' +
|
||||
'<td style="white-space: nowrap;"><select class="form-select form-select-sm" style="width: 130px; font-size: 0.8rem;" onchange="changeStatus(' + r.id + ', this.value)">' + selectOpts + '</select> <button class="btn btn-outline-secondary btn-sm" style="padding: 2px 8px; font-size: 0.7rem; margin-left: 4px;" onclick="showHistory(' + r.id + ')"><i class="fas fa-clock"></i></button></td>' +
|
||||
'</tr>';
|
||||
}).join('');
|
||||
}
|
||||
@@ -558,6 +656,15 @@ async function changeStatus(id, status) {
|
||||
} catch(err) { showToast(err.message, 'error'); }
|
||||
}
|
||||
|
||||
async function changeRoom(id, room_type) {
|
||||
try {
|
||||
const data = await api('/api/admin/bookings/' + id + '/room', { method: 'PATCH', body: JSON.stringify({ room_type }) });
|
||||
allBookingsData = allBookingsData.map(b => b.id === id ? data.booking : b);
|
||||
renderBookings(); updateBookingStats(); loadDashboard();
|
||||
showToast('Номер обновлён: ' + (room_type || 'Не указан'));
|
||||
} catch(err) { showToast(err.message, 'error'); }
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.getElementById('filterStatus').addEventListener('change', renderBookings);
|
||||
document.getElementById('filterUrgent').addEventListener('change', renderBookings);
|
||||
@@ -658,6 +765,124 @@ async function deleteUser(id) {
|
||||
|
||||
function esc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
||||
|
||||
async function changeComment(id, comment) {
|
||||
try {
|
||||
const data = await api('/api/admin/bookings/' + id + '/comment', { method: 'PATCH', body: JSON.stringify({ comment }) });
|
||||
allBookingsData = allBookingsData.map(b => b.id === id ? data.booking : b);
|
||||
showToast('Комментарий обновлён');
|
||||
} catch(err) { showToast(err.message, 'error'); }
|
||||
}
|
||||
|
||||
async function changeDiscount(id, discount_percent) {
|
||||
try {
|
||||
const data = await api('/api/admin/bookings/' + id + '/discount', { method: 'PATCH', body: JSON.stringify({ discount_percent: parseInt(discount_percent) }) });
|
||||
allBookingsData = allBookingsData.map(b => b.id === id ? data.booking : b);
|
||||
renderBookings();
|
||||
showToast('Скидка обновлена: ' + discount_percent + '%');
|
||||
} catch(err) { showToast(err.message, 'error'); }
|
||||
}
|
||||
|
||||
async function loadPromocodes() {
|
||||
try {
|
||||
const promocodes = await api('/api/admin/promocodes');
|
||||
const tbody = document.getElementById('promocodesTable');
|
||||
tbody.innerHTML = promocodes.map(p => '<tr>' +
|
||||
'<td><strong>' + esc(p.code) + '</strong></td>' +
|
||||
'<td><span class="badge bg-success" style="font-size: 0.8rem;">' + p.discount_percent + '%</span></td>' +
|
||||
'<td>' + (p.valid_from || '—') + '</td>' +
|
||||
'<td>' + (p.valid_to || '—') + '</td>' +
|
||||
'<td>' + (p.valid_days ? p.valid_days + ' дн.' : 'Бессрочно') + '</td>' +
|
||||
'<td><span class="badge ' + (p.is_active ? 'bg-success' : 'bg-secondary') + '">' + (p.is_active ? 'Активен' : 'Неактивен') + '</span></td>' +
|
||||
'<td class="admin-only">' +
|
||||
'<button class="btn-primary-custom btn-sm me-1" onclick="showPromocodeModal(' + p.id + ')"><i class="fas fa-edit"></i></button>' +
|
||||
'<button class="btn-danger-custom btn-sm" onclick="deletePromocode(' + p.id + ')"><i class="fas fa-trash"></i></button>' +
|
||||
'</td></tr>').join('');
|
||||
updateUI();
|
||||
} catch(err) { showToast(err.message, 'error'); }
|
||||
}
|
||||
|
||||
function showPromocodeModal(id) {
|
||||
document.getElementById('promocodeModal').classList.add('show');
|
||||
document.getElementById('promocodeForm').reset();
|
||||
document.getElementById('editPromocodeId').value = '';
|
||||
document.getElementById('promocodeModalTitle').textContent = 'Добавить промокод';
|
||||
document.getElementById('promocodeCode').disabled = false;
|
||||
if (id) {
|
||||
api('/api/admin/promocodes').then(promocodes => {
|
||||
const p = promocodes.find(x => x.id === id);
|
||||
if (p) {
|
||||
document.getElementById('editPromocodeId').value = p.id;
|
||||
document.getElementById('promocodeCode').value = p.code;
|
||||
document.getElementById('promocodeCode').disabled = true;
|
||||
document.getElementById('promocodeDiscount').value = p.discount_percent;
|
||||
document.getElementById('promocodeValidFrom').value = p.valid_from ? p.valid_from.slice(0, 16) : '';
|
||||
document.getElementById('promocodeValidTo').value = p.valid_to ? p.valid_to.slice(0, 16) : '';
|
||||
document.getElementById('promocodeValidDays').value = p.valid_days || '';
|
||||
document.getElementById('promocodeIsActive').checked = p.is_active === 1;
|
||||
document.getElementById('promocodeModalTitle').textContent = 'Редактировать промокод';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function hidePromocodeModal() { document.getElementById('promocodeModal').classList.remove('show'); }
|
||||
|
||||
document.getElementById('promocodeForm').addEventListener('submit', async e => {
|
||||
e.preventDefault();
|
||||
const id = document.getElementById('editPromocodeId').value;
|
||||
const body = {
|
||||
code: document.getElementById('promocodeCode').value,
|
||||
discount_percent: parseInt(document.getElementById('promocodeDiscount').value),
|
||||
valid_from: document.getElementById('promocodeValidFrom').value || null,
|
||||
valid_to: document.getElementById('promocodeValidTo').value || null,
|
||||
valid_days: document.getElementById('promocodeValidDays').value ? parseInt(document.getElementById('promocodeValidDays').value) : null,
|
||||
is_active: document.getElementById('promocodeIsActive').checked ? 1 : 0
|
||||
};
|
||||
try {
|
||||
if (id) {
|
||||
await api('/api/admin/promocodes/' + id, { method: 'PUT', body: JSON.stringify(body) });
|
||||
showToast('Промокод обновлён');
|
||||
} else {
|
||||
await api('/api/admin/promocodes', { method: 'POST', body: JSON.stringify(body) });
|
||||
showToast('Промокод создан');
|
||||
}
|
||||
hidePromocodeModal(); loadPromocodes();
|
||||
} catch(err) { showToast(err.message, 'error'); }
|
||||
});
|
||||
|
||||
async function deletePromocode(id) {
|
||||
if (!confirm('Удалить промокод?')) return;
|
||||
try { await api('/api/admin/promocodes/' + id, { method: 'DELETE' }); showToast('Промокод удалён'); loadPromocodes(); }
|
||||
catch(err) { showToast(err.message, 'error'); }
|
||||
}
|
||||
|
||||
async function showHistory(bookingId) {
|
||||
try {
|
||||
const booking = allBookingsData.find(b => b.id === bookingId);
|
||||
document.getElementById('historyModalTitle').textContent = 'История: ' + (booking ? booking.name : '#'+bookingId);
|
||||
const history = await api('/api/admin/bookings/' + bookingId + '/history');
|
||||
const fieldNames = { status: 'Статус', room_type: 'Тип номера', name: 'Имя', phone: 'Телефон', checkin_date: 'Дата заезда', checkout_date: 'Дата выезда', wishes: 'Пожелания' };
|
||||
if (history.length === 0) {
|
||||
document.getElementById('historyContent').innerHTML = '<p class="text-center text-muted">Нет записей</p>';
|
||||
} else {
|
||||
document.getElementById('historyContent').innerHTML = '<div class="history-timeline">' + history.map(h => {
|
||||
const fieldName = fieldNames[h.field] || h.field;
|
||||
return '<div class="history-item">' +
|
||||
'<div class="history-date">' + h.created_at + '</div>' +
|
||||
'<div class="history-user">' + (h.user_login || '—') + '</div>' +
|
||||
'<div class="history-change">' + fieldName + ': <span class="old-val">' + esc(h.old_value || '—') + '</span> → <span class="new-val">' + esc(h.new_value || '—') + '</span></div>' +
|
||||
'</div>';
|
||||
}).join('') + '</div>';
|
||||
}
|
||||
document.getElementById('historyModal').classList.add('show');
|
||||
} catch(err) { showToast(err.message, 'error'); }
|
||||
}
|
||||
|
||||
function hideHistoryModal() { document.getElementById('historyModal').classList.remove('show'); }
|
||||
|
||||
document.getElementById('historyModal').addEventListener('click', function(e) { if (e.target === this) hideHistoryModal(); });
|
||||
document.getElementById('promocodeModal').addEventListener('click', function(e) { if (e.target === this) hidePromocodeModal(); });
|
||||
|
||||
checkAuth();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -647,6 +647,29 @@
|
||||
<input type="text" class="form-control" name="wishes" placeholder="Например: вид на море">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3 mt-1">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Промокод</label>
|
||||
<input type="text" class="form-control" id="promocodeInput" name="promocode" placeholder="Введите промокод" style="text-transform: uppercase;">
|
||||
</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%;">Проверить промокод</button>
|
||||
</div>
|
||||
</div>
|
||||
<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;">
|
||||
<span>Базовая стоимость:</span>
|
||||
<span id="basePriceDisplay">—</span>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; margin-bottom: 8px; color: #16a34a;">
|
||||
<span>Скидка (<span id="discountPercentDisplay">0</span>%):</span>
|
||||
<span id="discountAmountDisplay">—</span>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; font-weight: 700; font-size: 1.1rem; border-top: 1px solid #dee2e6; padding-top: 8px;">
|
||||
<span>Итого к оплате:</span>
|
||||
<span id="totalPriceDisplay" style="color: var(--primary);">—</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<button type="submit" class="btn-submit-booking">
|
||||
<i class="fas fa-paper-plane me-2"></i>Отправить заявку
|
||||
|
||||
@@ -66,9 +66,88 @@ document.querySelectorAll('.btn-book').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const room = this.getAttribute('data-room');
|
||||
document.getElementById('selectedRoom').value = room;
|
||||
hidePriceInfo();
|
||||
});
|
||||
});
|
||||
|
||||
// Price calculation
|
||||
const ROOM_PRICES = { 'Эконом': 2500, 'Стандарт': 4000, 'VIP Люкс': 8000 };
|
||||
let currentPromocodeData = null;
|
||||
|
||||
function calculateNights(checkin, checkout) {
|
||||
const ci = new Date(checkin);
|
||||
const co = new Date(checkout);
|
||||
return Math.ceil((co - ci) / (1000 * 60 * 60 * 24));
|
||||
}
|
||||
|
||||
function updatePriceDisplay(basePrice, discountPercent, discountAmount, totalPrice) {
|
||||
document.getElementById('basePriceDisplay').textContent = basePrice + ' ₽';
|
||||
document.getElementById('discountPercentDisplay').textContent = discountPercent;
|
||||
document.getElementById('discountAmountDisplay').textContent = '-' + discountAmount + ' ₽';
|
||||
document.getElementById('totalPriceDisplay').textContent = totalPrice + ' ₽';
|
||||
document.getElementById('priceInfo').style.display = 'block';
|
||||
}
|
||||
|
||||
function hidePriceInfo() {
|
||||
document.getElementById('priceInfo').style.display = 'none';
|
||||
currentPromocodeData = null;
|
||||
}
|
||||
|
||||
function getFormData() {
|
||||
return {
|
||||
room: document.getElementById('selectedRoom').value,
|
||||
checkin: document.querySelector('[name="checkin"]').value,
|
||||
checkout: document.querySelector('[name="checkout"]').value,
|
||||
promocode: document.getElementById('promocodeInput').value.trim()
|
||||
};
|
||||
}
|
||||
|
||||
async function checkPromocode() {
|
||||
const { room, checkin, checkout, promocode } = getFormData();
|
||||
if (!room || !checkin || !checkout) {
|
||||
hidePriceInfo();
|
||||
return;
|
||||
}
|
||||
|
||||
const basePrice = ROOM_PRICES[room] ? ROOM_PRICES[room] * calculateNights(checkin, checkout) : 0;
|
||||
|
||||
if (!promocode) {
|
||||
updatePriceDisplay(basePrice, 0, 0, basePrice);
|
||||
currentPromocodeData = null;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/promocodes/validate', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ code: promocode, room_type: room, checkin, checkout })
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
if (data.valid) {
|
||||
currentPromocodeData = data;
|
||||
updatePriceDisplay(data.base_price, data.discount_percent, data.discount_amount, data.total_price);
|
||||
} else {
|
||||
hidePriceInfo();
|
||||
currentPromocodeData = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Promocode validation error:', error);
|
||||
hidePriceInfo();
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('checkPromocodeBtn').addEventListener('click', checkPromocode);
|
||||
document.getElementById('promocodeInput').addEventListener('blur', checkPromocode);
|
||||
document.querySelector('[name="checkin"]').addEventListener('change', checkPromocode);
|
||||
document.querySelector('[name="checkout"]').addEventListener('change', checkPromocode);
|
||||
document.querySelectorAll('.btn-book').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
setTimeout(checkPromocode, 100);
|
||||
});
|
||||
});
|
||||
|
||||
// Form submission
|
||||
document.getElementById('bookingForm').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
@@ -86,7 +165,8 @@ document.getElementById('bookingForm').addEventListener('submit', async function
|
||||
checkin: this.querySelector('[name="checkin"]').value,
|
||||
checkout: this.querySelector('[name="checkout"]').value,
|
||||
wishes: this.querySelector('[name="wishes"]').value,
|
||||
room: document.getElementById('selectedRoom').value
|
||||
room: document.getElementById('selectedRoom').value,
|
||||
promocode: document.getElementById('promocodeInput').value.trim() || null
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -100,6 +180,7 @@ document.getElementById('bookingForm').addEventListener('submit', async function
|
||||
btn.innerHTML = '<i class="fas fa-check me-2"></i>Заявка отправлена Рауфу Алексеевичу!';
|
||||
btn.style.background = '#25d366';
|
||||
this.reset();
|
||||
hidePriceInfo();
|
||||
setTimeout(() => {
|
||||
btn.innerHTML = originalText;
|
||||
btn.style.background = '';
|
||||
|
||||
331
server.js
331
server.js
@@ -14,6 +14,40 @@ const ADMIN_LOGIN = process.env.ADMIN_LOGIN;
|
||||
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD;
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'fallback-secret-change-in-production';
|
||||
|
||||
const ROOM_PRICES = {
|
||||
'Эконом': 2500,
|
||||
'Стандарт': 4000,
|
||||
'VIP Люкс': 8000
|
||||
};
|
||||
|
||||
function calculateNights(checkin, checkout) {
|
||||
const ci = new Date(checkin);
|
||||
const co = new Date(checkout);
|
||||
return Math.ceil((co - ci) / (1000 * 60 * 60 * 24));
|
||||
}
|
||||
|
||||
function calculateBasePrice(roomType, checkin, checkout) {
|
||||
const pricePerNight = ROOM_PRICES[roomType] || 0;
|
||||
const nights = calculateNights(checkin, checkout);
|
||||
return pricePerNight * nights;
|
||||
}
|
||||
|
||||
function validatePromocode(promocode, callback) {
|
||||
if (!promocode) return callback(null, null);
|
||||
const now = new Date().toISOString();
|
||||
db.get(`SELECT * FROM promocodes WHERE code = ? AND is_active = 1`, [promocode], (err, row) => {
|
||||
if (err || !row) return callback(null, null);
|
||||
if (row.valid_from && row.valid_from > now) return callback(null, null);
|
||||
if (row.valid_to && row.valid_to < now) return callback(null, null);
|
||||
if (row.valid_days) {
|
||||
const createdDate = new Date(row.created_at);
|
||||
const expireDate = new Date(createdDate.getTime() + row.valid_days * 24 * 60 * 60 * 1000);
|
||||
if (expireDate < new Date()) return callback(null, null);
|
||||
}
|
||||
callback(null, row);
|
||||
});
|
||||
}
|
||||
|
||||
if (!API_KEY) {
|
||||
console.error('FATAL: HOTEL777KEY environment variable not set');
|
||||
process.exit(1);
|
||||
@@ -53,6 +87,87 @@ db.run(`ALTER TABLE bookings ADD COLUMN status TEXT DEFAULT 'новая'`, (err)
|
||||
}
|
||||
});
|
||||
|
||||
db.run(`ALTER TABLE bookings ADD COLUMN room_type TEXT`, (err) => {
|
||||
if (err && !err.message.includes('duplicate column name')) {
|
||||
console.error('Migration error:', err);
|
||||
}
|
||||
});
|
||||
|
||||
db.run(`ALTER TABLE bookings ADD COLUMN comment TEXT`, (err) => {
|
||||
if (err && !err.message.includes('duplicate column name')) console.error('Migration error:', err);
|
||||
});
|
||||
db.run(`ALTER TABLE bookings ADD COLUMN base_price REAL`, (err) => {
|
||||
if (err && !err.message.includes('duplicate column name')) console.error('Migration error:', err);
|
||||
});
|
||||
db.run(`ALTER TABLE bookings ADD COLUMN discount_percent INTEGER DEFAULT 0`, (err) => {
|
||||
if (err && !err.message.includes('duplicate column name')) console.error('Migration error:', err);
|
||||
});
|
||||
db.run(`ALTER TABLE bookings ADD COLUMN discount_amount REAL DEFAULT 0`, (err) => {
|
||||
if (err && !err.message.includes('duplicate column name')) console.error('Migration error:', err);
|
||||
});
|
||||
db.run(`ALTER TABLE bookings ADD COLUMN total_price REAL`, (err) => {
|
||||
if (err && !err.message.includes('duplicate column name')) console.error('Migration error:', err);
|
||||
});
|
||||
db.run(`ALTER TABLE bookings ADD COLUMN promocode_id INTEGER`, (err) => {
|
||||
if (err && !err.message.includes('duplicate column name')) console.error('Migration error:', err);
|
||||
});
|
||||
|
||||
db.run(`CREATE TABLE IF NOT EXISTS promocodes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
code TEXT NOT NULL UNIQUE,
|
||||
discount_percent INTEGER NOT NULL CHECK(discount_percent BETWEEN 1 AND 99),
|
||||
valid_from DATETIME,
|
||||
valid_to DATETIME,
|
||||
valid_days INTEGER,
|
||||
is_active INTEGER DEFAULT 1,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)`, (err) => {
|
||||
if (err) console.error('Promocodes table creation error:', err);
|
||||
});
|
||||
|
||||
db.run(`CREATE TABLE IF NOT EXISTS rooms (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
type TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
rooms_count INTEGER DEFAULT 1,
|
||||
single_beds INTEGER DEFAULT 0,
|
||||
double_beds INTEGER DEFAULT 0,
|
||||
has_sofa INTEGER DEFAULT 0,
|
||||
has_ac INTEGER DEFAULT 0,
|
||||
has_wifi INTEGER DEFAULT 0,
|
||||
has_shower INTEGER DEFAULT 0,
|
||||
max_guests INTEGER DEFAULT 2,
|
||||
price_per_guest INTEGER NOT NULL,
|
||||
image_path TEXT,
|
||||
is_active INTEGER DEFAULT 1,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)`, (err) => {
|
||||
if (err) console.error('Rooms table creation error:', err);
|
||||
else initDefaultRooms();
|
||||
});
|
||||
|
||||
db.run(`CREATE TABLE IF NOT EXISTS booking_history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
booking_id INTEGER NOT NULL,
|
||||
user_id INTEGER,
|
||||
user_login TEXT,
|
||||
field TEXT NOT NULL,
|
||||
old_value TEXT,
|
||||
new_value TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (booking_id) REFERENCES bookings(id)
|
||||
)`, (err) => {
|
||||
if (err) console.error('Booking history table creation error:', err);
|
||||
});
|
||||
|
||||
function logHistory(bookingId, userId, userLogin, field, oldValue, newValue) {
|
||||
db.run(`INSERT INTO booking_history (booking_id, user_id, user_login, field, old_value, new_value) VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
[bookingId, userId, userLogin, field, oldValue, newValue], (err) => {
|
||||
if (err) console.error('History log error:', err);
|
||||
});
|
||||
}
|
||||
|
||||
db.run(`CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
login TEXT NOT NULL UNIQUE,
|
||||
@@ -132,25 +247,40 @@ async function convertImages() {
|
||||
}
|
||||
|
||||
app.post('/api/bookings', (req, res) => {
|
||||
const { name, phone, adults, children, checkin, checkout, wishes } = req.body;
|
||||
const { name, phone, adults, children, checkin, checkout, wishes, room, promocode } = req.body;
|
||||
if (!name || !phone || !adults || !checkin || !checkout) {
|
||||
return res.status(400).json({ error: 'Missing required fields' });
|
||||
}
|
||||
const stmt = db.prepare(`INSERT INTO bookings (name, phone, adults, children, checkin_date, checkout_date, wishes, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, 'новая')`);
|
||||
stmt.run(name, phone, parseInt(adults), parseInt(children || 0), checkin, checkout, wishes || null, function(err) {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return res.status(500).json({ error: 'Database error' });
|
||||
const basePrice = calculateBasePrice(room, checkin, checkout);
|
||||
validatePromocode(promocode, (err, promo) => {
|
||||
if (err) return res.status(500).json({ error: 'Database error' });
|
||||
let discountPercent = 0;
|
||||
let promocodeId = null;
|
||||
if (promo) {
|
||||
discountPercent = promo.discount_percent;
|
||||
promocodeId = promo.id;
|
||||
}
|
||||
res.status(201).json({ id: this.lastID, message: 'Booking saved' });
|
||||
const safeBasePrice = basePrice || 0;
|
||||
const discountAmount = Math.round(safeBasePrice * discountPercent / 100);
|
||||
const totalPrice = safeBasePrice - discountAmount;
|
||||
const stmt = db.prepare(`INSERT INTO bookings (name, phone, adults, children, checkin_date, checkout_date, wishes, status, room_type, base_price, discount_percent, discount_amount, total_price, promocode_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, 'новая', ?, ?, ?, ?, ?, ?)`);
|
||||
stmt.run(name, phone, parseInt(adults), parseInt(children || 0), checkin, checkout, wishes || null, room || null,
|
||||
safeBasePrice || null, discountPercent || 0, discountAmount || 0, totalPrice || null, promocodeId, function(err) {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return res.status(500).json({ error: 'Database error' });
|
||||
}
|
||||
res.status(201).json({ id: this.lastID, message: 'Booking saved', base_price: safeBasePrice, discount_percent: discountPercent, discount_amount: discountAmount, total_price: totalPrice });
|
||||
});
|
||||
stmt.finalize();
|
||||
});
|
||||
stmt.finalize();
|
||||
});
|
||||
|
||||
app.get('/api/admin/bookings', authenticateToken, (req, res) => {
|
||||
db.all(`SELECT id, name, phone, adults, children, checkin_date, checkout_date, wishes, status, created_at
|
||||
FROM bookings ORDER BY checkin_date ASC`, [], (err, rows) => {
|
||||
db.all(`SELECT b.*, p.code as promocode_code FROM bookings b
|
||||
LEFT JOIN promocodes p ON b.promocode_id = p.id
|
||||
ORDER BY b.checkin_date ASC`, [], (err, rows) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return res.status(500).json({ error: 'Database error' });
|
||||
@@ -166,22 +296,107 @@ app.patch('/api/admin/bookings/:id', authenticateToken, requireAdmin, (req, res)
|
||||
if (!status || !validStatuses.includes(status)) {
|
||||
return res.status(400).json({ error: 'Invalid status. Valid: ' + validStatuses.join(', ') });
|
||||
}
|
||||
db.run(`UPDATE bookings SET status = ? WHERE id = ?`, [status, bookingId], (err) => {
|
||||
db.get(`SELECT status FROM bookings WHERE id = ?`, [bookingId], (err, row) => {
|
||||
if (err) return res.status(500).json({ error: 'Database error' });
|
||||
db.get(`SELECT id, name, phone, adults, children, checkin_date, checkout_date, wishes, status, created_at FROM bookings WHERE id = ?`, [bookingId], (err, row) => {
|
||||
if (!row) return res.status(404).json({ error: 'Booking not found' });
|
||||
const oldValue = row.status;
|
||||
db.run(`UPDATE bookings SET status = ? WHERE id = ?`, [status, bookingId], (err) => {
|
||||
if (err) return res.status(500).json({ error: 'Database error' });
|
||||
res.json({ message: 'Status updated', booking: row });
|
||||
logHistory(bookingId, req.user.id, req.user.login, 'status', oldValue, status);
|
||||
db.get(`SELECT id, name, phone, adults, children, checkin_date, checkout_date, wishes, status, room_type, created_at FROM bookings WHERE id = ?`, [bookingId], (err, row) => {
|
||||
if (err) return res.status(500).json({ error: 'Database error' });
|
||||
res.json({ message: 'Status updated', booking: row });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
app.patch('/api/admin/bookings/:id/room', authenticateToken, requireAdmin, (req, res) => {
|
||||
const bookingId = parseInt(req.params.id);
|
||||
const { room_type } = req.body;
|
||||
const validRooms = ['Эконом', 'Стандарт', 'VIP Люкс'];
|
||||
if (!room_type || !validRooms.includes(room_type)) {
|
||||
return res.status(400).json({ error: 'Invalid room type. Valid: ' + validRooms.join(', ') });
|
||||
}
|
||||
db.get(`SELECT room_type, checkin_date, checkout_date, discount_percent FROM bookings WHERE id = ?`, [bookingId], (err, row) => {
|
||||
if (err) return res.status(500).json({ error: 'Database error' });
|
||||
if (!row) return res.status(404).json({ error: 'Booking not found' });
|
||||
const oldValue = row.room_type || 'Не указан';
|
||||
const basePrice = calculateBasePrice(room_type, row.checkin_date, row.checkout_date);
|
||||
const discountAmount = Math.round(basePrice * (row.discount_percent || 0) / 100);
|
||||
const totalPrice = basePrice - discountAmount;
|
||||
db.run(`UPDATE bookings SET room_type = ?, base_price = ?, discount_amount = ?, total_price = ? WHERE 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_type', oldValue, room_type);
|
||||
db.get(`SELECT b.*, p.code as promocode_code FROM bookings b LEFT JOIN promocodes p ON b.promocode_id = p.id WHERE b.id = ?`, [bookingId], (err, row) => {
|
||||
if (err) return res.status(500).json({ error: 'Database error' });
|
||||
res.json({ message: 'Room updated', booking: row });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
app.patch('/api/admin/bookings/:id/comment', authenticateToken, requireAdmin, (req, res) => {
|
||||
const bookingId = parseInt(req.params.id);
|
||||
const { comment } = req.body;
|
||||
db.get(`SELECT comment FROM bookings WHERE id = ?`, [bookingId], (err, row) => {
|
||||
if (err) return res.status(500).json({ error: 'Database error' });
|
||||
if (!row) return res.status(404).json({ error: 'Booking not found' });
|
||||
const oldValue = row.comment || 'Нет';
|
||||
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 FROM bookings b LEFT JOIN promocodes p ON b.promocode_id = p.id WHERE b.id = ?`, [bookingId], (err, row) => {
|
||||
if (err) return res.status(500).json({ error: 'Database error' });
|
||||
res.json({ message: 'Comment updated', booking: row });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
app.patch('/api/admin/bookings/:id/discount', authenticateToken, requireAdmin, (req, res) => {
|
||||
const bookingId = parseInt(req.params.id);
|
||||
const { discount_percent } = req.body;
|
||||
if (discount_percent === undefined || discount_percent < 0 || discount_percent > 99) {
|
||||
return res.status(400).json({ error: 'Discount percent must be between 0 and 99' });
|
||||
}
|
||||
db.get(`SELECT base_price, discount_percent FROM bookings WHERE id = ?`, [bookingId], (err, row) => {
|
||||
if (err) return res.status(500).json({ error: 'Database error' });
|
||||
if (!row) return res.status(404).json({ error: 'Booking not found' });
|
||||
const oldValue = row.discount_percent || 0;
|
||||
const basePrice = row.base_price || 0;
|
||||
const discountAmount = Math.round(basePrice * discount_percent / 100);
|
||||
const totalPrice = basePrice - discountAmount;
|
||||
db.run(`UPDATE bookings SET discount_percent = ?, discount_amount = ?, total_price = ? WHERE id = ?`,
|
||||
[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 FROM bookings b LEFT JOIN promocodes p ON b.promocode_id = p.id WHERE b.id = ?`, [bookingId], (err, row) => {
|
||||
if (err) return res.status(500).json({ error: 'Database error' });
|
||||
res.json({ message: 'Discount updated', booking: row });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/api/admin/bookings/:id/history', authenticateToken, (req, res) => {
|
||||
const bookingId = parseInt(req.params.id);
|
||||
db.all(`SELECT id, booking_id, user_id, user_login, field, old_value, new_value, created_at
|
||||
FROM booking_history WHERE booking_id = ? ORDER BY created_at DESC`, [bookingId], (err, rows) => {
|
||||
if (err) return res.status(500).json({ error: 'Database error' });
|
||||
res.json(rows);
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/api/bookings', (req, res) => {
|
||||
const providedKey = req.headers['x-api-key'];
|
||||
if (!providedKey || providedKey !== API_KEY) {
|
||||
return res.status(401).json({ error: 'Invalid or missing API key' });
|
||||
}
|
||||
db.all(`SELECT id, name, phone, adults, children, checkin_date, checkout_date, wishes, status, created_at
|
||||
FROM bookings ORDER BY checkin_date ASC`, (err, rows) => {
|
||||
db.all(`SELECT b.*, p.code as promocode_code FROM bookings b
|
||||
LEFT JOIN promocodes p ON b.promocode_id = p.id
|
||||
ORDER BY b.checkin_date ASC`, (err, rows) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return res.status(500).json({ error: 'Database error' });
|
||||
@@ -190,6 +405,90 @@ app.get('/api/bookings', (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/api/promocodes/validate', (req, res) => {
|
||||
const { code, room_type, checkin, checkout } = req.body;
|
||||
if (!code) return res.status(400).json({ error: 'Promocode required' });
|
||||
validatePromocode(code, (err, promo) => {
|
||||
if (err) return res.status(500).json({ error: 'Database error' });
|
||||
if (!promo) return res.status(404).json({ error: 'Invalid or expired promocode' });
|
||||
const basePrice = calculateBasePrice(room_type, checkin, checkout);
|
||||
const discountAmount = Math.round(basePrice * promo.discount_percent / 100);
|
||||
const totalPrice = basePrice - discountAmount;
|
||||
res.json({
|
||||
valid: true,
|
||||
discount_percent: promo.discount_percent,
|
||||
base_price: basePrice,
|
||||
discount_amount: discountAmount,
|
||||
total_price: totalPrice,
|
||||
code: promo.code
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/api/admin/promocodes', authenticateToken, requireAdmin, (req, res) => {
|
||||
db.all(`SELECT * FROM promocodes ORDER BY created_at DESC`, [], (err, rows) => {
|
||||
if (err) return res.status(500).json({ error: 'Database error' });
|
||||
res.json(rows);
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/api/admin/promocodes', authenticateToken, requireAdmin, (req, res) => {
|
||||
const { code, discount_percent, valid_from, valid_to, valid_days, is_active } = req.body;
|
||||
if (!code || !discount_percent) return res.status(400).json({ error: 'Code and discount percent required' });
|
||||
if (discount_percent < 1 || discount_percent > 99) return res.status(400).json({ error: 'Discount must be between 1 and 99' });
|
||||
db.run(`INSERT INTO promocodes (code, discount_percent, valid_from, valid_to, valid_days, is_active)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
[code, discount_percent, valid_from || null, valid_to || null, valid_days || null, is_active !== undefined ? is_active : 1], function(err) {
|
||||
if (err) {
|
||||
if (err.message.includes('UNIQUE constraint')) return res.status(409).json({ error: 'Promocode already exists' });
|
||||
return res.status(500).json({ error: 'Database error' });
|
||||
}
|
||||
db.get(`SELECT * FROM promocodes WHERE id = ?`, [this.lastID], (err, row) => {
|
||||
res.status(201).json({ message: 'Promocode created', promocode: row });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
app.put('/api/admin/promocodes/:id', authenticateToken, requireAdmin, (req, res) => {
|
||||
const promoId = parseInt(req.params.id);
|
||||
const { code, discount_percent, valid_from, valid_to, valid_days, is_active } = req.body;
|
||||
if (discount_percent !== undefined && (discount_percent < 1 || discount_percent > 99)) {
|
||||
return res.status(400).json({ error: 'Discount must be between 1 and 99' });
|
||||
}
|
||||
db.get(`SELECT id FROM promocodes WHERE id = ?`, [promoId], (err, row) => {
|
||||
if (err) return res.status(500).json({ error: 'Database error' });
|
||||
if (!row) return res.status(404).json({ error: 'Promocode not found' });
|
||||
let fields = [];
|
||||
let values = [];
|
||||
if (code !== undefined) { fields.push('code = ?'); values.push(code); }
|
||||
if (discount_percent !== undefined) { fields.push('discount_percent = ?'); values.push(discount_percent); }
|
||||
if (valid_from !== undefined) { fields.push('valid_from = ?'); values.push(valid_from || null); }
|
||||
if (valid_to !== undefined) { fields.push('valid_to = ?'); values.push(valid_to || null); }
|
||||
if (valid_days !== undefined) { fields.push('valid_days = ?'); values.push(valid_days || null); }
|
||||
if (is_active !== undefined) { fields.push('is_active = ?'); values.push(is_active); }
|
||||
if (fields.length === 0) return res.status(400).json({ error: 'No fields to update' });
|
||||
values.push(promoId);
|
||||
db.run(`UPDATE promocodes SET ${fields.join(', ')} WHERE id = ?`, values, (err) => {
|
||||
if (err) return res.status(500).json({ error: 'Database error' });
|
||||
db.get(`SELECT * FROM promocodes WHERE id = ?`, [promoId], (err, row) => {
|
||||
res.json({ message: 'Promocode updated', promocode: row });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
app.delete('/api/admin/promocodes/:id', authenticateToken, requireAdmin, (req, res) => {
|
||||
const promoId = parseInt(req.params.id);
|
||||
db.get(`SELECT id FROM promocodes WHERE id = ?`, [promoId], (err, row) => {
|
||||
if (err) return res.status(500).json({ error: 'Database error' });
|
||||
if (!row) return res.status(404).json({ error: 'Promocode not found' });
|
||||
db.run(`DELETE FROM promocodes WHERE id = ?`, [promoId], (err) => {
|
||||
if (err) return res.status(500).json({ error: 'Database error' });
|
||||
res.json({ message: 'Promocode deleted' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/api/auth/login', (req, res) => {
|
||||
const { login, password } = req.body;
|
||||
if (!login || !password) return res.status(400).json({ error: 'Login and password required' });
|
||||
|
||||
Reference in New Issue
Block a user