This commit is contained in:
2026-10-29 16:51:26 +03:00
parent 70a20665ae
commit 4b2f0903b1
51 changed files with 6540 additions and 1212 deletions
+185
View File
@@ -2336,3 +2336,188 @@
[2026-08-31T12:27:21.644Z] [INFO] Card logs initialized successfully
[2026-08-31T12:30:33.180Z] [INFO] Server running on http://localhost:3005
[2026-08-31T12:30:33.183Z] [INFO] Card logs initialized successfully
[2026-09-21T06:46:42.261Z] [INFO] Server running on http://localhost:3005
[2026-09-21T06:46:42.445Z] [INFO] Card logs initialized successfully
[2026-09-24T08:35:23.724Z] [INFO] Server running on http://localhost:3005
[2026-09-24T08:35:23.739Z] [INFO] Card logs initialized successfully
[2026-10-18T09:42:48.767Z] [INFO] Server running on http://localhost:3005
[2026-10-18T09:42:48.774Z] [INFO] Card logs initialized successfully
[2026-10-18T17:07:04.649Z] [INFO] Server running on http://localhost:3005
[2026-10-18T17:07:04.663Z] [INFO] Card logs initialized successfully
[2026-10-18T17:07:45.889Z] [INFO] [DB CHECK] Проверка подключения к MySQL: localhost:3306/rasch_content
[2026-10-18T17:07:45.916Z] [INFO] [DB CHECK] ✅ Успешное подключение к MySQL
[2026-10-18T17:08:29.318Z] [INFO] [LOGIN] Попытка входа: vova
[2026-10-18T17:08:29.320Z] [INFO] [LOGIN] Проверка подключения к базе...
[2026-10-18T17:08:29.350Z] [INFO] [LOGIN] Результат поиска пользователя: найден
[2026-10-18T17:08:29.352Z] [INFO] [LOGIN] Сравнение пароля через bcrypt...
[2026-10-18T17:08:29.458Z] [INFO] [LOGIN] Результат сравнения пароля: true
[2026-10-18T17:08:29.461Z] [INFO] [LOGIN] Сессия установлена: {"isModerator":true,"userId":4,"username":"vova"}
[2026-10-18T17:08:29.464Z] [INFO] [LOGIN] Ответ отправлен
[2026-10-18T17:10:17.359Z] [INFO] [DB CHECK] Проверка подключения к MySQL: localhost:3306/rasch_content
[2026-10-18T17:10:17.409Z] [INFO] [DB CHECK] ✅ Успешное подключение к MySQL
[2026-10-18T17:10:32.276Z] [WARN] Неверный пароль
[2026-10-18T17:10:37.859Z] [WARN] Неверный пароль
[2026-10-18T17:10:39.203Z] [WARN] Неверный пароль
[2026-10-18T17:15:23.261Z] [INFO] Server running on http://localhost:3005
[2026-10-18T17:15:23.275Z] [INFO] Card logs initialized successfully
[2026-10-21T03:42:25.554Z] [INFO] Server running on http://localhost:3005
[2026-10-21T03:42:25.562Z] [INFO] Card logs initialized successfully
[2026-10-21T03:45:23.041Z] [INFO] [LOGIN] Попытка входа: vova
[2026-10-21T03:45:23.041Z] [INFO] [LOGIN] Проверка подключения к базе...
[2026-10-21T03:45:23.052Z] [INFO] [LOGIN] Результат поиска пользователя: найден
[2026-10-21T03:45:23.053Z] [INFO] [LOGIN] Сравнение пароля через bcrypt...
[2026-10-21T03:45:23.128Z] [INFO] [LOGIN] Результат сравнения пароля: true
[2026-10-21T03:45:23.129Z] [INFO] [LOGIN] Сессия установлена: {"isModerator":true,"userId":4,"username":"vova"}
[2026-10-21T03:45:23.141Z] [INFO] [LOGIN] Ответ отправлен
[2026-10-21T04:41:10.860Z] [INFO] Server running on http://localhost:3005
[2026-10-21T04:41:10.885Z] [INFO] Card logs initialized successfully
[2026-10-21T18:14:23.958Z] [INFO] Server running on http://localhost:3005
[2026-10-21T18:14:23.964Z] [INFO] Card logs initialized successfully
[2026-10-26T10:30:06.261Z] [INFO] Server running on http://localhost:3005
[2026-10-26T10:30:06.272Z] [INFO] Card logs initialized successfully
[2026-10-28T18:25:51.489Z] [INFO] Server running on http://localhost:3005
[2026-10-28T18:25:51.499Z] [INFO] Card logs initialized successfully
[2026-10-28T19:29:04.657Z] [INFO] Server running on http://localhost:3005
[2026-10-28T19:29:04.725Z] [INFO] Card logs initialized successfully
[2026-10-28T20:19:46.381Z] [INFO] Server running on http://localhost:3005
[2026-10-28T20:19:46.394Z] [INFO] Card logs initialized successfully
[2026-10-28T20:32:55.955Z] [INFO] Server running on http://localhost:3005
[2026-10-28T20:32:55.960Z] [INFO] Card logs initialized successfully
[2026-10-28T20:46:01.429Z] [INFO] Server running on http://localhost:3005
[2026-10-28T20:46:01.433Z] [INFO] Card logs initialized successfully
[2026-10-28T20:59:03.413Z] [INFO] [DB CHECK] Проверка подключения к MySQL: localhost:3306/rasch_content
[2026-10-28T20:59:03.494Z] [INFO] [DB CHECK] ✅ Успешное подключение к MySQL
[2026-10-28T21:00:18.133Z] [INFO] [LOGIN] Попытка входа: vova
[2026-10-28T21:00:18.134Z] [INFO] [LOGIN] Проверка подключения к базе...
[2026-10-28T21:00:18.154Z] [INFO] [LOGIN] Результат поиска пользователя: найден
[2026-10-28T21:00:18.155Z] [INFO] [LOGIN] Сравнение пароля через bcrypt...
[2026-10-28T21:00:18.264Z] [INFO] [LOGIN] Результат сравнения пароля: true
[2026-10-28T21:00:18.265Z] [INFO] [LOGIN] Сессия установлена: {"isModerator":true,"userId":4,"username":"vova"}
[2026-10-28T21:00:18.266Z] [INFO] [LOGIN] Ответ отправлен
[2026-10-28T21:13:11.494Z] [INFO] Server running on http://localhost:3005
[2026-10-28T21:13:11.515Z] [INFO] Card logs initialized successfully
[2026-10-28T21:14:42.274Z] [INFO] [LOGIN] Попытка входа: vova
[2026-10-28T21:14:42.274Z] [INFO] [LOGIN] Проверка подключения к базе...
[2026-10-28T21:14:42.276Z] [INFO] [LOGIN] Результат поиска пользователя: найден
[2026-10-28T21:14:42.277Z] [INFO] [LOGIN] Сравнение пароля через bcrypt...
[2026-10-28T21:14:42.390Z] [INFO] [LOGIN] Результат сравнения пароля: true
[2026-10-28T21:14:42.391Z] [INFO] [LOGIN] Сессия установлена: {"isModerator":true,"userId":4,"username":"vova"}
[2026-10-28T21:14:42.392Z] [INFO] [LOGIN] Ответ отправлен
[2026-10-28T21:20:21.465Z] [INFO] Server running on http://localhost:3005
[2026-10-28T21:20:21.469Z] [INFO] Card logs initialized successfully
[2026-10-28T21:27:08.156Z] [INFO] Server running on http://localhost:3005
[2026-10-28T21:27:08.158Z] [INFO] Card logs initialized successfully
[2026-10-28T21:27:21.606Z] [ERROR] [admin/food/orders GET] Request failed with status code 500
[2026-10-28T21:27:24.092Z] [ERROR] [admin/food/orders GET] Request failed with status code 500
[2026-10-28T21:27:25.235Z] [ERROR] [admin/food/orders GET] Request failed with status code 500
[2026-10-28T21:27:26.257Z] [ERROR] [admin/food/orders GET] Request failed with status code 500
[2026-10-28T21:27:27.402Z] [ERROR] [admin/food/orders GET] Request failed with status code 500
[2026-10-28T21:29:56.599Z] [ERROR] [admin/food/orders GET] Request failed with status code 500
[2026-10-28T21:29:58.442Z] [ERROR] [admin/food/orders GET] Request failed with status code 500
[2026-10-28T21:29:59.302Z] [ERROR] [admin/food/orders GET] Request failed with status code 500
[2026-10-28T21:30:01.382Z] [ERROR] [admin/food/orders GET] Request failed with status code 500
[2026-10-28T21:30:02.524Z] [ERROR] [admin/food/orders GET] Request failed with status code 500
[2026-10-28T21:30:03.627Z] [ERROR] [admin/food/orders GET] Request failed with status code 500
[2026-10-28T21:30:16.016Z] [INFO] [LOGIN] Попытка входа: vova
[2026-10-28T21:30:16.016Z] [INFO] [LOGIN] Проверка подключения к базе...
[2026-10-28T21:30:16.018Z] [INFO] [LOGIN] Результат поиска пользователя: найден
[2026-10-28T21:30:16.018Z] [INFO] [LOGIN] Сравнение пароля через bcrypt...
[2026-10-28T21:30:16.130Z] [INFO] [LOGIN] Результат сравнения пароля: true
[2026-10-28T21:30:16.131Z] [INFO] [LOGIN] Сессия установлена: {"isModerator":true,"userId":4,"username":"vova"}
[2026-10-28T21:30:16.132Z] [INFO] [LOGIN] Ответ отправлен
[2026-10-28T21:30:19.009Z] [ERROR] [admin/food/orders GET] Request failed with status code 500
[2026-10-28T21:30:20.720Z] [ERROR] [admin/food/orders GET] Request failed with status code 500
[2026-10-28T21:30:21.657Z] [ERROR] [admin/food/orders GET] Request failed with status code 500
[2026-10-28T21:30:22.658Z] [ERROR] [admin/food/orders GET] Request failed with status code 500
[2026-10-28T21:30:23.631Z] [ERROR] [admin/food/orders GET] Request failed with status code 500
[2026-10-28T21:30:24.526Z] [ERROR] [admin/food/orders GET] Request failed with status code 500
[2026-10-28T21:30:24.948Z] [ERROR] [admin/food/orders GET] Request failed with status code 500
[2026-10-28T21:35:22.344Z] [INFO] Server running on http://localhost:3005
[2026-10-28T21:35:22.348Z] [INFO] Card logs initialized successfully
[2026-10-28T21:39:47.954Z] [INFO] Server running on http://localhost:3005
[2026-10-28T21:39:47.959Z] [INFO] Card logs initialized successfully
[2026-10-28T21:40:26.037Z] [INFO] [LOGIN] Попытка входа: vova
[2026-10-28T21:40:26.039Z] [INFO] [LOGIN] Проверка подключения к базе...
[2026-10-28T21:40:26.049Z] [INFO] [LOGIN] Результат поиска пользователя: найден
[2026-10-28T21:40:26.050Z] [INFO] [LOGIN] Сравнение пароля через bcrypt...
[2026-10-28T21:40:26.155Z] [INFO] [LOGIN] Результат сравнения пароля: true
[2026-10-28T21:40:26.155Z] [INFO] [LOGIN] Сессия установлена: {"isModerator":true,"userId":4,"username":"vova"}
[2026-10-28T21:40:26.157Z] [INFO] [LOGIN] Ответ отправлен
[2026-10-28T21:44:16.803Z] [INFO] Server running on http://localhost:3005
[2026-10-28T21:44:16.808Z] [INFO] Card logs initialized successfully
[2026-10-28T21:44:59.309Z] [INFO] [LOGIN] Попытка входа: vova
[2026-10-28T21:44:59.310Z] [INFO] [LOGIN] Проверка подключения к базе...
[2026-10-28T21:44:59.312Z] [INFO] [LOGIN] Результат поиска пользователя: найден
[2026-10-28T21:44:59.313Z] [INFO] [LOGIN] Сравнение пароля через bcrypt...
[2026-10-28T21:44:59.426Z] [INFO] [LOGIN] Результат сравнения пароля: true
[2026-10-28T21:44:59.427Z] [INFO] [LOGIN] Сессия установлена: {"isModerator":true,"userId":4,"username":"vova"}
[2026-10-28T21:44:59.428Z] [INFO] [LOGIN] Ответ отправлен
[2026-10-28T21:48:59.108Z] [INFO] Server running on http://localhost:3005
[2026-10-28T21:48:59.112Z] [INFO] Card logs initialized successfully
[2026-10-28T21:49:23.402Z] [INFO] [LOGIN] Попытка входа: vova
[2026-10-28T21:49:23.403Z] [INFO] [LOGIN] Проверка подключения к базе...
[2026-10-28T21:49:23.406Z] [INFO] [LOGIN] Результат поиска пользователя: найден
[2026-10-28T21:49:23.407Z] [INFO] [LOGIN] Сравнение пароля через bcrypt...
[2026-10-28T21:49:23.520Z] [INFO] [LOGIN] Результат сравнения пароля: true
[2026-10-28T21:49:23.522Z] [INFO] [LOGIN] Сессия установлена: {"isModerator":true,"userId":4,"username":"vova"}
[2026-10-28T21:49:23.523Z] [INFO] [LOGIN] Ответ отправлен
[2026-10-28T21:59:33.857Z] [INFO] Server running on http://localhost:3005
[2026-10-28T21:59:33.860Z] [INFO] Card logs initialized successfully
[2026-10-28T22:04:29.620Z] [INFO] Server running on http://localhost:3005
[2026-10-28T22:04:29.626Z] [INFO] Card logs initialized successfully
[2026-10-28T22:12:28.385Z] [INFO] Server running on http://localhost:3005
[2026-10-28T22:12:28.411Z] [INFO] Card logs initialized successfully
[2026-10-28T22:30:18.979Z] [INFO] Server running on http://localhost:3005
[2026-10-28T22:30:18.982Z] [INFO] Card logs initialized successfully
[2026-10-28T22:39:36.568Z] [INFO] Server running on http://localhost:3005
[2026-10-28T22:39:36.572Z] [INFO] Card logs initialized successfully
[2026-10-28T22:43:12.105Z] [INFO] Server running on http://localhost:3005
[2026-10-28T22:43:12.109Z] [INFO] Card logs initialized successfully
[2026-10-28T22:50:49.508Z] [INFO] Server running on http://localhost:3005
[2026-10-28T22:50:49.511Z] [INFO] Card logs initialized successfully
[2026-10-28T23:02:07.770Z] [INFO] Server running on http://localhost:3005
[2026-10-28T23:02:07.773Z] [INFO] Card logs initialized successfully
[2026-10-28T23:02:26.714Z] [INFO] [LOGIN] Попытка входа: vova
[2026-10-28T23:02:26.715Z] [INFO] [LOGIN] Проверка подключения к базе...
[2026-10-28T23:02:26.728Z] [INFO] [LOGIN] Результат поиска пользователя: найден
[2026-10-28T23:02:26.729Z] [INFO] [LOGIN] Сравнение пароля через bcrypt...
[2026-10-28T23:02:26.853Z] [INFO] [LOGIN] Результат сравнения пароля: true
[2026-10-28T23:02:26.854Z] [INFO] [LOGIN] Сессия установлена: {"isModerator":true,"userId":4,"username":"vova"}
[2026-10-28T23:02:26.856Z] [INFO] [LOGIN] Ответ отправлен
[2026-10-28T23:03:20.549Z] [ERROR] [admin/food/orders PATCH] Request failed with status code 400
[2026-10-28T23:03:24.249Z] [ERROR] [admin/food/orders PATCH] Request failed with status code 400
[2026-10-28T23:03:25.985Z] [ERROR] [admin/food/orders PATCH] Request failed with status code 400
[2026-10-28T23:03:28.558Z] [ERROR] [admin/food/orders PATCH] Request failed with status code 400
[2026-10-28T23:03:31.683Z] [ERROR] [admin/food/orders PATCH] Request failed with status code 400
[2026-10-28T23:03:34.003Z] [ERROR] [admin/food/orders PATCH] Request failed with status code 400
[2026-10-28T23:03:35.191Z] [ERROR] [admin/food/orders PATCH] Request failed with status code 400
[2026-10-28T23:03:36.865Z] [ERROR] [admin/food/orders PATCH] Request failed with status code 400
[2026-10-28T23:04:09.307Z] [ERROR] [admin/food/orders PATCH] Request failed with status code 400
[2026-10-28T23:04:12.689Z] [ERROR] [admin/food/orders PATCH] Request failed with status code 400
[2026-10-28T23:04:14.448Z] [ERROR] [admin/food/orders PATCH] Request failed with status code 400
[2026-10-28T23:05:12.507Z] [ERROR] [admin/food/orders PATCH] Request failed with status code 400
[2026-10-28T23:07:37.357Z] [INFO] Server running on http://localhost:3005
[2026-10-28T23:07:37.367Z] [INFO] Card logs initialized successfully
[2026-10-29T01:23:30.890Z] [INFO] Server running on http://localhost:3005
[2026-10-29T01:23:30.921Z] [INFO] Card logs initialized successfully
[2026-10-29T03:14:32.608Z] [INFO] Server running on http://localhost:3005
[2026-10-29T03:14:32.634Z] [INFO] Card logs initialized successfully
[2026-10-29T03:15:11.513Z] [INFO] [LOGIN] Попытка входа: vova
[2026-10-29T03:15:11.514Z] [INFO] [LOGIN] Проверка подключения к базе...
[2026-10-29T03:15:11.537Z] [INFO] [LOGIN] Результат поиска пользователя: найден
[2026-10-29T03:15:11.539Z] [INFO] [LOGIN] Сравнение пароля через bcrypt...
[2026-10-29T03:15:11.627Z] [INFO] [LOGIN] Результат сравнения пароля: true
[2026-10-29T03:15:11.628Z] [INFO] [LOGIN] Сессия установлена: {"isModerator":true,"userId":4,"username":"vova"}
[2026-10-29T03:15:11.630Z] [INFO] [LOGIN] Ответ отправлен
[2026-10-29T03:15:48.284Z] [WARN] Неверный пароль
[2026-10-29T03:15:52.963Z] [WARN] Неверный пароль
[2026-10-29T03:15:54.203Z] [WARN] Неверный пароль
[2026-10-29T03:16:03.099Z] [WARN] Неверный пароль
[2026-10-29T03:16:04.050Z] [WARN] Неверный пароль
[2026-10-29T03:16:05.890Z] [WARN] Неверный пароль
[2026-10-29T04:24:56.000Z] [INFO] [DB CHECK] Проверка подключения к MySQL: localhost:3306/rasch_content
[2026-10-29T04:24:56.088Z] [INFO] [DB CHECK] ✅ Успешное подключение к MySQL
[2026-10-29T04:25:34.381Z] [WARN] Неверный пароль
[2026-10-29T04:25:38.869Z] [INFO] Login successful
[2026-10-29T04:25:39.079Z] [INFO] [DB CHECK] Проверка подключения к MySQL: localhost:3306/rasch_content
[2026-10-29T04:25:39.118Z] [INFO] [DB CHECK] ✅ Успешное подключение к MySQL
+111
View File
@@ -0,0 +1,111 @@
const express = require('express');
const router = express.Router();
const axios = require('axios');
const logger = require('../../logger');
const multer = require('multer');
const FormData = require('form-data');
const fs = require('fs');
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:3001';
const upload = multer({ dest: 'uploads/tmp/' });
// ================= ЗАКАЗЫ =================
router.get('/orders', async (req, res) => {
try {
const { status, date } = req.query;
const { data } = await axios.get(`${BACKEND_URL}/api/food/orders`, {
params: { status, date }, timeout: 5000,
});
res.json(data);
} catch (e) {
logger.error('[admin/food/orders GET]', e.message);
res.status(500).json({ error: 'Не удалось получить заказы' });
}
});
router.patch('/orders/:id/status', async (req, res) => {
try {
const { data } = await axios.patch(`${BACKEND_URL}/api/food/orders/${req.params.id}/status`, req.body, { timeout: 5000 });
res.json(data);
} catch (e) {
logger.error('[admin/food/orders PATCH]', e.message);
res.status(500).json({ error: 'Ошибка обновления статуса' });
}
});
// ================= МЕНЮ =================
router.get('/items', async (req, res) => {
try {
const { data } = await axios.get(`${BACKEND_URL}/api/food/menu`);
res.json(data);
} catch (e) {
res.status(500).json({ error: 'Не удалось получить меню' });
}
});
router.post('/items', upload.single('image'), async (req, res) => {
try {
const formData = new FormData();
// Передаем текстовые поля
Object.keys(req.body).forEach(key => {
formData.append(key, req.body[key]);
});
// Передаем файл, если он есть
if (req.file) {
formData.append('image', fs.createReadStream(req.file.path));
}
const { data } = await axios.post(`${BACKEND_URL}/api/food/items`, formData, {
headers: formData.getHeaders(),
maxContentLength: Infinity,
maxBodyLength: Infinity
});
if (req.file) fs.unlinkSync(req.file.path);
res.json(data);
} catch (e) {
console.error('Ошибка проксирования POST:', e.response?.data || e.message);
res.status(500).json({ error: e.response?.data?.error || 'Ошибка добавления блюда' });
}
});
router.put('/items/:id', upload.single('image'), async (req, res) => {
try {
const formData = new FormData();
// Передаем текстовые поля
Object.keys(req.body).forEach(key => {
formData.append(key, req.body[key]);
});
// Передаем файл, если он есть
if (req.file) {
formData.append('image', fs.createReadStream(req.file.path));
}
const { data } = await axios.put(`${BACKEND_URL}/api/food/items/${req.params.id}`, formData, {
headers: formData.getHeaders(),
maxContentLength: Infinity,
maxBodyLength: Infinity
});
if (req.file) fs.unlinkSync(req.file.path);
res.json(data);
} catch (e) {
console.error('Ошибка проксирования PUT:', e.response?.data || e.message);
res.status(500).json({ error: e.response?.data?.error || 'Ошибка обновления блюда' });
}
});
router.delete('/items/:id', async (req, res) => {
try {
const { data } = await axios.delete(`${BACKEND_URL}/api/food/items/${req.params.id}`);
res.json(data);
} catch (e) {
res.status(500).json({ error: 'Ошибка удаления блюда' });
}
});
module.exports = router;
+1
View File
@@ -18,6 +18,7 @@
"express-mysql-session": "^3.0.3",
"express-rate-limit": "^8.0.1",
"express-session": "^1.18.1",
"form-data": "^4.0.6",
"helmet": "^8.3.0",
"morgan": "^1.10.0",
"multer": "^2.0.1",
+1
View File
@@ -22,6 +22,7 @@
"express-mysql-session": "^3.0.3",
"express-rate-limit": "^8.0.1",
"express-session": "^1.18.1",
"form-data": "^4.0.6",
"helmet": "^8.3.0",
"morgan": "^1.10.0",
"multer": "^2.0.1",
+20 -5
View File
@@ -19,6 +19,7 @@ const healthRouter = require('./routes/health');
const monitoringRouter = require('./routes/monitoring');
const setupWebSocket = require('./services/websocket');
const cardController = require('./controllers/cardController');
const foodOrdersRouter = require('./moders/routes/foodOrders');
const app = express();
const server = require('http').createServer(app);
@@ -27,17 +28,29 @@ const corsConfig = require('./config/cors');
const sessionConfig = require('./config/session');
// Middleware
app.use(helmet()); // Добавление helmet для защиты
app.use(helmet({
contentSecurityPolicy: false, // Разрешаем скрипты на странице питания
}));
app.use(cors(corsConfig));
app.use(morgan('dev'));
app.use(express.json({ limit: '100mb' }));
app.use(session(sessionConfig(session)));
// Роуты
app.use('/api/auth', authRouter);
// Статические файлы
app.use(express.static(path.join(__dirname, '..','admin-frontend', 'build')));
app.use('/uploads', express.static(__dirname + '/uploads'));
// ============================================
// ПИТАНИЕ (API + HTML страница)
// ============================================
app.use('/moders/food', foodOrdersRouter); // 👈 ВОТ ЭТА СТРОКА ИСПРАВЛЕНА
app.get('/moders/food', (req, res) => {
// Отдаем HTML файл
res.sendFile(path.join(__dirname, 'views', 'food-orders.html'));
});
// Роуты
app.use('/api/card', cardRouter);
app.use('/api/logs', logsRouter);
@@ -47,10 +60,12 @@ app.use('/api/health', healthRouter);
app.use('/api/monitoring', monitoringRouter);
app.use('/moders', moderAuthRouter);
app.use('/moders/content', moderContentRouter);
app.use((req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
next();
});
// Initialize card logs
(async () => {
try {
@@ -74,9 +89,10 @@ app.get(/.*/, (req, res, next) => {
// Обработчик ошибок (должен быть последним!)
app.use(require('./middleware/errorHandler'));
app.get('/api/status', async (req, res) => {
try {
res.json({ status: 'online' }); // или другая логика проверки
res.json({ status: 'online' });
} catch (err) {
res.status(500).json({ status: 'error', message: err.message });
}
@@ -96,19 +112,17 @@ app.get('/api/config', async (req, res) => {
res.status(500).json({ error: 'Ошибка загрузки конфигурации' });
}
});
// Добавьте рядом с существующим GET обработчиком
app.post('/api/config', async (req, res) => {
try {
const backendConfigPath = path.join(__dirname, '../backend/config.json');
const content = await fs.readFile(backendConfigPath, 'utf8');
const currentConfig = JSON.parse(content);
// Обновляем только разрешенные поля
if (req.body.payslipsPath) {
currentConfig.payslipsPath = req.body.payslipsPath;
}
// Сохраняем обновленный конфиг
await fs.writeFile(backendConfigPath, JSON.stringify(currentConfig, null, 4));
res.json({
@@ -123,6 +137,7 @@ app.post('/api/config', async (req, res) => {
});
}
});
server.listen(process.env.PORT || 3005, () => {
logger.info(`Server running on ${process.env.ADMIN_URL}`);
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

+460
View File
@@ -0,0 +1,460 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Заказы питания — панель модератора</title>
<style>
* { box-sizing: border-box; }
body {
margin: 0;
font-family: -apple-system, "Segoe UI", Roboto, sans-serif;
background: #f4f7fb;
color: #2d3436;
}
header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 24px;
background: #fff;
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
position: sticky;
top: 0;
z-index: 10;
flex-wrap: wrap;
gap: 16px;
}
header h1 {
margin: 0;
font-size: 20px;
color: #2b6cb0;
display: flex;
align-items: center;
gap: 10px;
}
.nav-link {
text-decoration: none;
color: #2b6cb0;
font-size: 14px;
font-weight: 500;
margin-right: 15px;
}
.nav-link:hover { text-decoration: underline; }
.filters {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.filters button {
padding: 8px 16px;
border: none;
border-radius: 999px;
background: #eef2f7;
color: #4a5568;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.filters button:hover { background: #dde5ef; }
.filters button.active {
background: #2b6cb0;
color: #fff;
}
.filters button .badge {
display: inline-block;
margin-left: 6px;
background: rgba(255,255,255,0.3);
padding: 0 7px;
border-radius: 999px;
font-size: 12px;
}
main {
max-width: 1100px;
margin: 24px auto;
padding: 0 16px 60px;
}
.toolbar {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
color: #4a5568;
font-size: 14px;
}
.toolbar .dot {
width: 10px; height: 10px;
border-radius: 50%;
background: #2ecc71;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
.orders {
display: flex;
flex-direction: column;
gap: 14px;
}
.order {
background: #fff;
border-radius: 14px;
padding: 18px 20px;
box-shadow: 0 4px 16px rgba(0,0,0,0.06);
border-left: 5px solid #bdc3c7;
transition: transform 0.2s;
}
.order:hover {
transform: translateY(-2px);
}
.order.status-new { border-left-color: #e74c3c; }
.order.status-accepted { border-left-color: #f39c12; }
.order.status-done { border-left-color: #2ecc71; }
.order.status-cancelled { border-left-color: #95a5a6; opacity: 0.7; }
.order-head {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 12px;
margin-bottom: 10px;
}
.order-head h2 {
margin: 0;
font-size: 16px;
display: flex;
align-items: center;
gap: 8px;
}
.order-id {
background: #eef2f7;
color: #2b6cb0;
padding: 2px 10px;
border-radius: 6px;
font-size: 14px;
font-weight: 700;
}
.order-time {
color: #95a5a6;
font-size: 13px;
font-weight: 500;
}
.employee {
font-size: 15px;
color: #4a5568;
margin-bottom: 12px;
}
.employee b { color: #2d3436; }
.items {
display: flex;
flex-direction: column;
gap: 6px;
padding: 12px 0;
border-top: 1px dashed #eef2f7;
border-bottom: 1px dashed #eef2f7;
margin-bottom: 14px;
}
.item-row {
display: flex;
justify-content: space-between;
font-size: 14px;
color: #2d3436;
}
.item-row .qty { color: #95a5a6; margin-left: 6px; font-weight: 500; }
.order-foot {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.total {
font-size: 18px;
font-weight: 700;
color: #1abc9c;
}
.total small { font-size: 13px; color: #95a5a6; font-weight: 500; }
.actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.actions button {
padding: 8px 18px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
color: #fff;
transition: transform 0.1s, opacity 0.2s;
}
.actions button:active { transform: scale(0.96); }
.actions button:hover { opacity: 0.85; }
.btn-accept { background: #f39c12; }
.btn-done { background: #2ecc71; }
.btn-cancel { background: #e74c3c; }
.actions button:disabled {
background: #bdc3c7;
cursor: not-allowed;
transform: none;
opacity: 0.7;
}
.empty, .loading {
text-align: center;
color: #95a5a6;
padding: 60px 20px;
background: #fff;
border-radius: 14px;
font-size: 16px;
box-shadow: 0 4px 16px rgba(0,0,0,0.04);
}
.toast {
position: fixed;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
background: #2d3436;
color: #fff;
padding: 12px 24px;
border-radius: 10px;
font-size: 14px;
box-shadow: 0 8px 24px rgba(0,0,0,0.25);
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
z-index: 1000;
}
.toast.show { opacity: 1; }
</style>
</head>
<body>
<header>
<div style="display: flex; align-items: center;">
<a href="/moders" class="nav-link">← В панель модератора</a>
<h1>🍔 Заказы питания</h1>
</div>
<div class="filters" id="filters">
<button data-status="new" class="active">Новые <span class="badge" id="b-new">0</span></button>
<button data-status="accepted">Принятые <span class="badge" id="b-accepted">0</span></button>
<button data-status="done">Выполненные <span class="badge" id="b-done">0</span></button>
<button data-status="cancelled">Отменённые <span class="badge" id="b-cancelled">0</span></button>
</div>
</header>
<main>
<div class="toolbar">
<div class="dot"></div>
<span id="auto-status">Автообновление каждые 10 сек…</span>
</div>
<div id="container" class="orders">
<div class="loading">Загрузка...</div>
</div>
</main>
<div class="toast" id="toast"></div>
<script>
// ============================================================
// Конфиг
// ============================================================
const API = '/moders/food/orders';
const API_STATUS = '/moders/food/orders/';
const REFRESH_MS = 10000;
let currentStatus = 'new';
let allOrders = [];
const container = document.getElementById('container');
const toastEl = document.getElementById('toast');
// ============================================================
// Загрузка данных
// ============================================================
async function loadOrders() {
try {
const r = await fetch(`${API}?status=${currentStatus}`, {
credentials: 'include',
});
if (!r.ok) throw new Error(`HTTP ${r.status}`);
const data = await r.json();
allOrders = data;
render(data);
} catch (e) {
console.error('Ошибка загрузки:', e);
if (container.innerHTML.includes('loading')) {
container.innerHTML = `<div class="empty">Ошибка загрузки: ${e.message}</div>`;
}
}
}
// ============================================================
// Бейджи (кол-во по каждому статусу)
// ============================================================
async function updateBadges() {
try {
const statuses = ['new', 'accepted', 'done', 'cancelled'];
const results = await Promise.all(
statuses.map((s) =>
fetch(`${API}?status=${s}`, { credentials: 'include' })
.then((r) => r.ok ? r.json() : [])
.then((arr) => arr.length)
.catch(() => 0)
)
);
statuses.forEach((s, i) => {
const el = document.getElementById('b-' + s);
if (el) el.textContent = results[i];
});
} catch (err) {
// Игнорируем ошибки бейджей
}
}
// ============================================================
// Рендер карточек
// ============================================================
function render(orders) {
if (!orders || orders.length === 0) {
container.innerHTML = `<div class="empty">Заказов нет</div>`;
return;
}
container.innerHTML = orders.map(renderOrder).join('');
// Вешаем обработчики на кнопки
container.querySelectorAll('[data-action]').forEach((btn) => {
btn.addEventListener('click', () => {
const id = btn.dataset.id;
const status = btn.dataset.action;
changeStatus(id, status, btn);
});
});
}
function renderOrder(o) {
const itemsHtml = (o.items || [])
.map(
(it) => `
<div class="item-row">
<span>${escapeHtml(it.name)}<span class="qty">× ${it.quantity}</span></span>
<span>${Number(it.subtotal).toFixed(2)} BYN</span>
</div>`
)
.join('');
const actions = [];
if (o.status === 'new') {
actions.push(`<button class="btn-accept" data-action="accepted" data-id="${o.id}">Принять</button>`);
actions.push(`<button class="btn-cancel" data-action="cancelled" data-id="${o.id}">Отменить</button>`);
} else if (o.status === 'accepted') {
actions.push(`<button class="btn-done" data-action="done" data-id="${o.id}">Выполнен</button>`);
actions.push(`<button class="btn-cancel" data-action="cancelled" data-id="${o.id}">Отменить</button>`);
} else if (o.status === 'cancelled') {
actions.push(`<button class="btn-accept" data-action="accepted" data-id="${o.id}">Восстановить</button>`);
}
return `
<div class="order status-${o.status}">
<div class="order-head">
<h2><span class="order-id">Заказ #${o.id}</span></h2>
<span class="order-time">${formatDate(o.createdAt)}</span>
</div>
<div class="employee">
${escapeHtml(o.employee || 'Сотрудник')} · таб. № <b>${escapeHtml(o.tnom)}</b>
</div>
<div class="items">${itemsHtml}</div>
<div class="order-foot">
<div class="total">${Number(o.totalAmount).toFixed(2)} <small>BYN</small></div>
<div class="actions">${actions.join('')}</div>
</div>
</div>
`;
}
// ============================================================
// Смена статуса
// ============================================================
async function changeStatus(id, status, btn) {
if (btn) {
btn.disabled = true;
btn.textContent = '...';
}
try {
const r = await fetch(`${API_STATUS}${id}/status`, {
method: 'PATCH',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status }),
});
if (!r.ok) throw new Error(`HTTP ${r.status}`);
showToast(`Заказ #${id} → ${statusLabel(status)}`);
await loadOrders();
await updateBadges();
} catch (e) {
showToast(`Ошибка: ${e.message}`);
if (btn) {
btn.disabled = false;
// Кнопка вернет свой текст при перерисовке
}
}
}
function statusLabel(s) {
return { new: 'Новый', accepted: 'Принят', done: 'Выполнен', cancelled: 'Отменён' }[s] || s;
}
// ============================================================
// Утилиты
// ============================================================
function escapeHtml(str) {
return String(str || '').replace(/[&<>"']/g, (c) => ({
'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
}[c]));
}
function formatDate(iso) {
const d = new Date(iso);
return d.toLocaleString('ru-RU', {
day: '2-digit', month: '2-digit', year: 'numeric',
hour: '2-digit', minute: '2-digit',
});
}
let toastTimer = null;
function showToast(text) {
toastEl.textContent = text;
toastEl.classList.add('show');
clearTimeout(toastTimer);
toastTimer = setTimeout(() => toastEl.classList.remove('show'), 2500);
}
// ============================================================
// Фильтры
// ============================================================
document.getElementById('filters').addEventListener('click', (e) => {
const btn = e.target.closest('button[data-status]');
if (!btn) return;
document.querySelectorAll('#filters button').forEach((b) => b.classList.remove('active'));
btn.classList.add('active');
currentStatus = btn.dataset.status;
container.innerHTML = `<div class="loading">Загрузка...</div>`;
loadOrders();
});
// ============================================================
// Старт и Автообновление
// ============================================================
loadOrders();
updateBadges();
setInterval(() => {
loadOrders();
updateBadges();
}, REFRESH_MS);
</script>
</body>
</html>