130 lines
6.1 KiB
JavaScript
130 lines
6.1 KiB
JavaScript
let db;
|
|
|
|
function init(database) {
|
|
db = database;
|
|
}
|
|
|
|
function getSummary(req, res) {
|
|
const today = new Date().toISOString().split('T')[0];
|
|
const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
|
const monthAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
|
|
|
const queries = {
|
|
revenue_today: `SELECT COALESCE(SUM(total_price), 0) as revenue FROM bookings WHERE status NOT IN ('отменена','новая') AND checkin_date = ?`,
|
|
revenue_week: `SELECT COALESCE(SUM(total_price), 0) as revenue FROM bookings WHERE status NOT IN ('отменена','новая') AND checkin_date >= ?`,
|
|
revenue_month: `SELECT COALESCE(SUM(total_price), 0) as revenue FROM bookings WHERE status NOT IN ('отменена','новая') AND checkin_date >= ?`,
|
|
checkin_today: `SELECT COUNT(*) as count FROM bookings WHERE status IN ('заселена') AND checkin_date = ?`,
|
|
checkout_today: `SELECT COUNT(*) as count FROM bookings WHERE status IN ('выехала') AND checkout_date = ?`,
|
|
new_bookings: `SELECT COUNT(*) as count FROM bookings WHERE status = 'новая'`,
|
|
active_bookings: `SELECT COUNT(*) as count FROM bookings WHERE status IN ('оплачена','зарезервирована','заселена')`,
|
|
avg_review: `SELECT COALESCE(AVG(stars), 0) as avg FROM reviews WHERE is_approved = 1`,
|
|
total_bookings: `SELECT COUNT(*) as total, AVG(julianday(checkout_date) - julianday(checkin_date)) as avg_stay FROM bookings WHERE status NOT IN ('отменена')`
|
|
};
|
|
|
|
const results = {};
|
|
let completed = 0;
|
|
const total = Object.keys(queries).length;
|
|
|
|
function finish() {
|
|
completed++;
|
|
if (completed < total) return;
|
|
|
|
const now = new Date().toISOString();
|
|
const stayingSql = `SELECT COUNT(DISTINCT b.id) as count FROM bookings b WHERE b.status IN ('оплачена','зарезервирована','заселена') AND b.checkin_date <= ? AND b.checkout_date > ?`;
|
|
db.get(stayingSql, [now, now], (err, staying) => {
|
|
const stayingCount = (err ? 0 : staying?.count) || 0;
|
|
db.all(`SELECT r.type, r.rooms_count, COUNT(b.id) as booked FROM rooms r LEFT JOIN bookings b ON b.room_id = r.id AND b.status IN ('новая','оплачена','зарезервирована','заселена') AND b.checkin_date <= ? AND b.checkout_date > ? WHERE r.is_active = 1 GROUP BY r.type, r.rooms_count`, [now, now], (err, occRows) => {
|
|
const occupancy = { current: 0, by_type: {} };
|
|
let totalRooms = 0;
|
|
let totalBooked = 0;
|
|
if (occRows) {
|
|
occRows.forEach(r => {
|
|
const booked = r.booked || 0;
|
|
const total = r.rooms_count || 0;
|
|
occupancy.by_type[r.type] = total > 0 ? Math.round((booked / total) * 100) : 0;
|
|
totalRooms += total;
|
|
totalBooked += booked;
|
|
});
|
|
occupancy.current = totalRooms > 0 ? Math.round((totalBooked / totalRooms) * 100) : 0;
|
|
}
|
|
|
|
res.json({
|
|
revenue: {
|
|
today: results.revenue_today,
|
|
week: results.revenue_week,
|
|
month: results.revenue_month
|
|
},
|
|
occupancy: occupancy,
|
|
bookings: {
|
|
new: results.new_bookings,
|
|
active: results.active_bookings,
|
|
checkin_today: results.checkin_today,
|
|
checkout_today: results.checkout_today,
|
|
staying_now: stayingCount,
|
|
total: results.total_bookings?.total || 0
|
|
},
|
|
avg_stay_days: Math.round((results.total_bookings?.avg_stay || 0) * 10) / 10,
|
|
avg_review_stars: Math.round((results.avg_review || 0) * 10) / 10
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
Object.entries(queries).forEach(([key, sql]) => {
|
|
const params = [];
|
|
if (key === 'revenue_today' || key === 'checkin_today' || key === 'checkout_today') params.push(today);
|
|
else if (key === 'revenue_week') params.push(weekAgo);
|
|
else if (key === 'revenue_month') params.push(monthAgo);
|
|
|
|
db.get(sql, params, (err, row) => {
|
|
if (err) { results[key] = 0; }
|
|
else if (key.includes('revenue')) results[key] = row.revenue || 0;
|
|
else if (key === 'total_bookings') results[key] = { total: row.total || 0, avg_stay: row.avg_stay || 0 };
|
|
else results[key] = row?.count || row?.avg || 0;
|
|
finish();
|
|
});
|
|
});
|
|
}
|
|
|
|
function getRevenue(req, res) {
|
|
const period = req.query.period || 'monthly';
|
|
const months = parseInt(req.query.months) || 6;
|
|
|
|
let groupBy;
|
|
let dateFormat;
|
|
|
|
if (period === 'daily') {
|
|
groupBy = "date(b.checkin_date)";
|
|
dateFormat = 'daily';
|
|
} else if (period === 'weekly') {
|
|
groupBy = "strftime('%Y-W%W', b.checkin_date)";
|
|
dateFormat = 'weekly';
|
|
} else {
|
|
groupBy = "strftime('%Y-%m', b.checkin_date)";
|
|
dateFormat = 'monthly';
|
|
}
|
|
|
|
const sinceDate = new Date();
|
|
sinceDate.setMonth(sinceDate.getMonth() - months);
|
|
const since = sinceDate.toISOString().split('T')[0];
|
|
|
|
db.all(
|
|
`SELECT ${groupBy} as label, COALESCE(SUM(total_price), 0) as revenue, COUNT(*) as bookings
|
|
FROM bookings b
|
|
WHERE b.status NOT IN ('отменена','новая') AND b.checkin_date >= ?
|
|
GROUP BY label ORDER BY label ASC`,
|
|
[since],
|
|
(err, rows) => {
|
|
if (err) return res.status(500).json({ error: 'Database error' });
|
|
res.json({ period: dateFormat, data: rows });
|
|
}
|
|
);
|
|
}
|
|
|
|
function setupRoutes(app, authenticateToken, requireAdmin) {
|
|
app.get('/api/admin/reports/summary', authenticateToken, getSummary);
|
|
app.get('/api/admin/reports/revenue', authenticateToken, getRevenue);
|
|
}
|
|
|
|
module.exports = { init, setupRoutes };
|