прибрался

This commit is contained in:
2026-05-07 23:15:52 +05:00
parent ad237df23e
commit ca59175150
4 changed files with 653 additions and 25 deletions

331
server.js
View File

@@ -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' });