feat: Оптимизация навигации AdminJS в логические группы

- Объединены ресурсы в 5 логических групп: Контент сайта, Бронирования, Отзывы и рейтинги, Персонал и гиды, Администрирование
- Удалены дублирующие настройки navigation для чистой группировки
- Добавлены CSS стили для визуального отображения иерархии с отступами
- Добавлены эмодзи-иконки для каждого типа ресурсов через CSS
- Улучшена навигация с правильной вложенностью элементов
This commit is contained in:
2025-11-30 21:57:58 +09:00
parent 1e7d7c06eb
commit 13c752b93a
47 changed files with 14148 additions and 61 deletions

View File

@@ -0,0 +1,303 @@
import express from 'express';
import db from '../config/database.js';
const router = express.Router();
// Получение расписания гидов за месяц
router.get('/', async (req, res) => {
try {
const { year, month, guide_id } = req.query;
let query = `
SELECT gwd.*, g.name as guide_name, g.specialization
FROM guide_working_days gwd
LEFT JOIN guides g ON gwd.guide_id = g.id
WHERE 1=1
`;
const params = [];
if (year && month) {
query += ` AND EXTRACT(YEAR FROM gwd.work_date) = $${params.length + 1} AND EXTRACT(MONTH FROM gwd.work_date) = $${params.length + 2}`;
params.push(year, month);
}
if (guide_id) {
query += ` AND gwd.guide_id = $${params.length + 1}`;
params.push(guide_id);
}
query += ` ORDER BY gwd.work_date, g.name`;
const result = await db.query(query, params);
res.json({ success: true, data: result.rows });
} catch (error) {
console.error('Ошибка получения расписания:', error);
res.status(500).json({ success: false, error: 'Ошибка получения расписания' });
}
});
// Массовое создание/обновление расписания гидов за месяц
router.put('/', async (req, res) => {
try {
const { year, month } = req.query;
const { schedules } = req.body;
if (!year || !month || !schedules || !Array.isArray(schedules)) {
return res.status(400).json({
success: false,
error: 'Требуются параметры year, month и массив schedules'
});
}
await db.query('BEGIN');
// Удаляем существующее расписание за этот месяц
await db.query(`
DELETE FROM guide_working_days
WHERE EXTRACT(YEAR FROM work_date) = $1
AND EXTRACT(MONTH FROM work_date) = $2
`, [year, month]);
// Добавляем новое расписание
if (schedules.length > 0) {
const values = schedules.map((schedule, index) => {
const baseIndex = index * 2;
return `($${baseIndex + 1}, $${baseIndex + 2})`;
}).join(', ');
const params = schedules.flatMap(schedule => [
schedule.guide_id,
schedule.work_date
]);
await db.query(`
INSERT INTO guide_schedules (guide_id, work_date)
VALUES ${values}
`, params);
}
await db.query('COMMIT');
res.json({
success: true,
message: `Расписание за ${year}-${month.toString().padStart(2, '0')} обновлено`,
count: schedules.length
});
} catch (error) {
await db.query('ROLLBACK');
console.error('Ошибка обновления расписания:', error);
res.status(500).json({ success: false, error: 'Ошибка обновления расписания' });
}
});
// Массовое добавление расписания (для копирования)
router.post('/batch', async (req, res) => {
try {
const { schedules } = req.body;
if (!schedules || !Array.isArray(schedules)) {
return res.status(400).json({
success: false,
error: 'Требуется массив schedules'
});
}
if (schedules.length === 0) {
return res.json({ success: true, message: 'Нет расписания для добавления' });
}
await db.query('BEGIN');
// Проверяем существующие записи и добавляем только новые
const existingQuery = `
SELECT guide_id, work_date
FROM guide_working_days
WHERE (guide_id, work_date) IN (${schedules.map((_, index) => {
const baseIndex = index * 2;
return `($${baseIndex + 1}, $${baseIndex + 2})`;
}).join(', ')})
`;
const existingParams = schedules.flatMap(schedule => [
schedule.guide_id,
schedule.work_date
]);
const existingResult = await db.query(existingQuery, existingParams);
const existingSet = new Set(
existingResult.rows.map(row => `${row.guide_id}-${row.work_date}`)
);
// Фильтруем новые записи
const newSchedules = schedules.filter(schedule => {
const key = `${schedule.guide_id}-${schedule.work_date}`;
return !existingSet.has(key);
});
if (newSchedules.length > 0) {
const values = newSchedules.map((schedule, index) => {
const baseIndex = index * 2;
return `($${baseIndex + 1}, $${baseIndex + 2})`;
}).join(', ');
const params = newSchedules.flatMap(schedule => [
schedule.guide_id,
schedule.work_date
]);
await db.query(`
INSERT INTO guide_schedules (guide_id, work_date)
VALUES ${values}
`, params);
}
await db.query('COMMIT');
res.json({
success: true,
message: 'Расписание добавлено',
added: newSchedules.length,
skipped: schedules.length - newSchedules.length
});
} catch (error) {
await db.query('ROLLBACK');
console.error('Ошибка массового добавления расписания:', error);
res.status(500).json({ success: false, error: 'Ошибка добавления расписания' });
}
});
// Добавление одного рабочего дня
router.post('/', async (req, res) => {
try {
const { guide_id, work_date } = req.body;
if (!guide_id || !work_date) {
return res.status(400).json({
success: false,
error: 'Требуются параметры guide_id и work_date'
});
}
// Проверяем, что запись не существует
const existingResult = await db.query(
'SELECT id FROM guide_working_days WHERE guide_id = $1 AND work_date = $2',
[guide_id, work_date]
);
if (existingResult.rows.length > 0) {
return res.json({
success: true,
message: 'Рабочий день уже существует'
});
}
const result = await db.query(`
INSERT INTO guide_working_days (guide_id, work_date)
VALUES ($1, $2)
RETURNING *
`, [guide_id, work_date]);
res.json({
success: true,
data: result.rows[0],
message: 'Рабочий день добавлен'
});
} catch (error) {
console.error('Ошибка добавления рабочего дня:', error);
res.status(500).json({ success: false, error: 'Ошибка добавления рабочего дня' });
}
});
// Удаление рабочего дня
router.delete('/', async (req, res) => {
try {
const { guide_id, work_date } = req.body;
if (!guide_id || !work_date) {
return res.status(400).json({
success: false,
error: 'Требуются параметры guide_id и work_date'
});
}
const result = await db.query(`
DELETE FROM guide_working_days
WHERE guide_id = $1 AND work_date = $2
RETURNING *
`, [guide_id, work_date]);
if (result.rows.length === 0) {
return res.status(404).json({
success: false,
error: 'Рабочий день не найден'
});
}
res.json({
success: true,
message: 'Рабочий день удален'
});
} catch (error) {
console.error('Ошибка удаления рабочего дня:', error);
res.status(500).json({ success: false, error: 'Ошибка удаления рабочего дня' });
}
});
// Получение статистики работы гидов
router.get('/stats', async (req, res) => {
try {
const { year, month } = req.query;
let dateFilter = '';
const params = [];
if (year && month) {
dateFilter = 'WHERE EXTRACT(YEAR FROM gwd.work_date) = $1 AND EXTRACT(MONTH FROM gwd.work_date) = $2';
params.push(year, month);
}
const statsQuery = `
SELECT
g.id,
g.name,
g.specialization,
COUNT(gwd.work_date) as working_days,
MIN(gwd.work_date) as first_work_date,
MAX(gwd.work_date) as last_work_date
FROM guides g
LEFT JOIN guide_working_days gwd ON g.id = gwd.guide_id
${dateFilter}
GROUP BY g.id, g.name, g.specialization
ORDER BY working_days DESC, g.name
`;
const result = await db.query(statsQuery, params);
// Общая статистика
const totalStats = await db.query(`
SELECT
COUNT(DISTINCT gwd.guide_id) as active_guides,
COUNT(gwd.work_date) as total_working_days,
ROUND(AVG(guide_days.working_days), 1) as avg_days_per_guide
FROM (
SELECT guide_id, COUNT(work_date) as working_days
FROM guide_working_days gwd
${dateFilter}
GROUP BY guide_id
) guide_days
RIGHT JOIN guides g ON guide_days.guide_id = g.id
`, params);
res.json({
success: true,
data: {
guides: result.rows,
summary: totalStats.rows[0]
}
});
} catch (error) {
console.error('Ошибка получения статистики:', error);
res.status(500).json({ success: false, error: 'Ошибка получения статистики' });
}
});
export default router;