прибрался
This commit is contained in:
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