From 204e6a37ed4e1b504e7b421c765387e7b44a74e4 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Mon, 10 Aug 2026 23:29:34 +0300 Subject: [PATCH] ntp&modercontent_front --- admin-frontend/src/components/Editor.jsx | 15 +- admin-server1/logs/admin.log | 93 ++++++ admin-server1/moders/routes/content.js | 54 +++- admin-server1/server.js | 2 +- backend/package-lock.json | 42 +++ backend/package.json | 2 + backend/src/routes/employee.js | 18 +- backend/src/server.js | 37 ++- frontend/.env | 1 + frontend/package-lock.json | 99 ++---- frontend/package.json | 5 +- frontend/src/components/BottomBar.tsx | 59 +++- frontend/src/components/TradeUnion.tsx | 393 ++++++++++++++++++----- 13 files changed, 628 insertions(+), 192 deletions(-) create mode 100644 frontend/.env diff --git a/admin-frontend/src/components/Editor.jsx b/admin-frontend/src/components/Editor.jsx index d2cb6be..88d1143 100644 --- a/admin-frontend/src/components/Editor.jsx +++ b/admin-frontend/src/components/Editor.jsx @@ -29,6 +29,7 @@ function Editor() { setNotify({ message: '', type: '', show: false }); }, 2200); }, []); + // Fetch CSRF token //useEffect(() => { @@ -256,8 +257,18 @@ const authenticatedFetch = useCallback((url, options = {}) => {

Редактирование вкладок

- - + + + +
diff --git a/admin-server1/logs/admin.log b/admin-server1/logs/admin.log index 9af4dff..e39ff77 100644 --- a/admin-server1/logs/admin.log +++ b/admin-server1/logs/admin.log @@ -1658,3 +1658,96 @@ [2026-08-10T07:18:02.945Z] [INFO] [LOGIN] Ответ отправлен [2026-08-10T07:22:25.057Z] [INFO] Server running on http://localhost:3005 [2026-08-10T07:22:25.060Z] [INFO] Card logs initialized successfully +[2026-08-10T17:21:24.568Z] [INFO] Server running on http://localhost:3005 +[2026-08-10T17:21:24.572Z] [INFO] Card logs initialized successfully +[2026-08-10T17:38:05.413Z] [INFO] Server running on http://localhost:3005 +[2026-08-10T17:38:05.416Z] [INFO] Card logs initialized successfully +[2026-08-10T17:38:24.712Z] [INFO] [LOGIN] Попытка входа: grivova +[2026-08-10T17:38:24.713Z] [INFO] [LOGIN] Проверка подключения к базе... +[2026-08-10T17:38:24.749Z] [INFO] [LOGIN] Результат поиска пользователя: не найден +[2026-08-10T17:38:24.750Z] [WARN] [LOGIN] Пользователь не найден: grivova +[2026-08-10T17:38:29.893Z] [INFO] [LOGIN] Попытка входа: vova +[2026-08-10T17:38:29.894Z] [INFO] [LOGIN] Проверка подключения к базе... +[2026-08-10T17:38:29.897Z] [INFO] [LOGIN] Результат поиска пользователя: найден +[2026-08-10T17:38:29.898Z] [INFO] [LOGIN] Сравнение пароля через bcrypt... +[2026-08-10T17:38:30.033Z] [INFO] [LOGIN] Результат сравнения пароля: true +[2026-08-10T17:38:30.034Z] [INFO] [LOGIN] Сессия установлена: {"isModerator":true,"userId":4,"username":"vova"} +[2026-08-10T17:38:30.038Z] [INFO] [LOGIN] Ответ отправлен +[2026-08-10T17:42:06.235Z] [INFO] Server running on http://localhost:3005 +[2026-08-10T17:42:06.238Z] [INFO] Card logs initialized successfully +[2026-08-10T17:43:18.054Z] [INFO] [DB CHECK] Проверка подключения к MySQL: localhost:3306/rasch_content +[2026-08-10T17:43:18.068Z] [INFO] [DB CHECK] ✅ Успешное подключение к MySQL +[2026-08-10T17:43:42.744Z] [INFO] [LOGIN] Попытка входа: vova +[2026-08-10T17:43:42.745Z] [INFO] [LOGIN] Проверка подключения к базе... +[2026-08-10T17:43:42.751Z] [INFO] [LOGIN] Результат поиска пользователя: найден +[2026-08-10T17:43:42.752Z] [INFO] [LOGIN] Сравнение пароля через bcrypt... +[2026-08-10T17:43:42.833Z] [INFO] [LOGIN] Результат сравнения пароля: true +[2026-08-10T17:43:42.834Z] [INFO] [LOGIN] Сессия установлена: {"isModerator":true,"userId":4,"username":"vova"} +[2026-08-10T17:43:42.835Z] [INFO] [LOGIN] Ответ отправлен +[2026-08-10T17:44:32.453Z] [INFO] [DB CHECK] Проверка подключения к MySQL: localhost:3306/rasch_content +[2026-08-10T17:44:32.465Z] [INFO] [DB CHECK] ✅ Успешное подключение к MySQL +[2026-08-10T17:44:38.654Z] [INFO] [LOGIN] Попытка входа: vova +[2026-08-10T17:44:38.655Z] [INFO] [LOGIN] Проверка подключения к базе... +[2026-08-10T17:44:38.657Z] [INFO] [LOGIN] Результат поиска пользователя: найден +[2026-08-10T17:44:38.658Z] [INFO] [LOGIN] Сравнение пароля через bcrypt... +[2026-08-10T17:44:38.794Z] [INFO] [LOGIN] Результат сравнения пароля: true +[2026-08-10T17:44:38.795Z] [INFO] [LOGIN] Сессия установлена: {"isModerator":true,"userId":4,"username":"vova"} +[2026-08-10T17:44:38.798Z] [INFO] [LOGIN] Ответ отправлен +[2026-08-10T17:49:04.372Z] [INFO] Server running on http://localhost:3005 +[2026-08-10T17:49:04.376Z] [INFO] Card logs initialized successfully +[2026-08-10T18:07:19.446Z] [INFO] Server running on http://localhost:3005 +[2026-08-10T18:07:19.450Z] [INFO] Card logs initialized successfully +[2026-08-10T18:07:21.917Z] [INFO] [DB CHECK] Проверка подключения к MySQL: localhost:3306/rasch_content +[2026-08-10T18:07:21.973Z] [INFO] [DB CHECK] ✅ Успешное подключение к MySQL +[2026-08-10T18:07:22.009Z] [ERROR] [DB CHECK] ❌ Ошибка связи с бэкендом: connect ECONNREFUSED 127.0.0.1:3001; connect ECONNREFUSED ::1:3001 +[2026-08-10T18:09:04.717Z] [INFO] Server running on http://localhost:3005 +[2026-08-10T18:09:04.720Z] [INFO] Card logs initialized successfully +[2026-08-10T18:11:04.542Z] [INFO] Server running on http://localhost:3005 +[2026-08-10T18:11:04.545Z] [INFO] Card logs initialized successfully +[2026-08-10T19:48:50.904Z] [INFO] Server running on http://localhost:3005 +[2026-08-10T19:48:50.908Z] [INFO] Card logs initialized successfully +[2026-08-10T19:49:05.177Z] [INFO] [DB CHECK] Проверка подключения к MySQL: localhost:3306/rasch_content +[2026-08-10T19:49:05.244Z] [INFO] [DB CHECK] ✅ Успешное подключение к MySQL +[2026-08-10T19:49:05.309Z] [ERROR] [DB CHECK] ❌ Ошибка связи с бэкендом: connect ECONNREFUSED 127.0.0.1:3001; connect ECONNREFUSED ::1:3001 +[2026-08-10T19:52:24.799Z] [INFO] Server running on http://localhost:3005 +[2026-08-10T19:52:24.804Z] [INFO] Card logs initialized successfully +[2026-08-10T19:54:26.429Z] [INFO] [LOGIN] Попытка входа: vova +[2026-08-10T19:54:26.430Z] [INFO] [LOGIN] Проверка подключения к базе... +[2026-08-10T19:54:26.437Z] [INFO] [LOGIN] Результат поиска пользователя: найден +[2026-08-10T19:54:26.439Z] [INFO] [LOGIN] Сравнение пароля через bcrypt... +[2026-08-10T19:54:26.774Z] [INFO] [LOGIN] Результат сравнения пароля: true +[2026-08-10T19:54:26.776Z] [INFO] [LOGIN] Сессия установлена: {"isModerator":true,"userId":4,"username":"vova"} +[2026-08-10T19:54:26.780Z] [INFO] [LOGIN] Ответ отправлен +[2026-08-10T19:57:35.827Z] [INFO] [DB CHECK] Проверка подключения к MySQL: localhost:3306/rasch_content +[2026-08-10T19:57:35.848Z] [INFO] [DB CHECK] ✅ Успешное подключение к MySQL +[2026-08-10T20:00:31.524Z] [ERROR] PayloadTooLargeError: request entity too large + at readStream (D:\rasch\admin-server1\node_modules\raw-body\index.js:163:17) + at getRawBody (D:\rasch\admin-server1\node_modules\raw-body\index.js:116:12) + at read (D:\rasch\admin-server1\node_modules\body-parser\lib\read.js:113:3) + at jsonParser (D:\rasch\admin-server1\node_modules\body-parser\lib\types\json.js:60:5) + at Layer.handleRequest (D:\rasch\admin-server1\node_modules\router\lib\layer.js:152:17) + at trimPrefix (D:\rasch\admin-server1\node_modules\router\index.js:342:13) + at D:\rasch\admin-server1\node_modules\router\index.js:297:9 + at processParams (D:\rasch\admin-server1\node_modules\router\index.js:582:12) + at next (D:\rasch\admin-server1\node_modules\router\index.js:291:5) + at logger (D:\rasch\admin-server1\node_modules\morgan\index.js:170:5) +[2026-08-10T20:02:19.620Z] [INFO] Server running on http://localhost:3005 +[2026-08-10T20:02:19.623Z] [INFO] Card logs initialized successfully +[2026-08-10T20:05:33.895Z] [INFO] Server running on http://localhost:3005 +[2026-08-10T20:05:33.899Z] [INFO] Card logs initialized successfully +[2026-08-10T20:08:11.744Z] [INFO] Server running on http://localhost:3005 +[2026-08-10T20:08:11.746Z] [INFO] Card logs initialized successfully +[2026-08-10T20:09:55.295Z] [INFO] Server running on http://localhost:3005 +[2026-08-10T20:09:55.298Z] [INFO] Card logs initialized successfully +[2026-08-10T20:11:38.925Z] [INFO] Server running on http://localhost:3005 +[2026-08-10T20:11:38.928Z] [INFO] Card logs initialized successfully +[2026-08-10T20:13:11.668Z] [INFO] Server running on http://localhost:3005 +[2026-08-10T20:13:11.671Z] [INFO] Card logs initialized successfully +[2026-08-10T20:13:47.030Z] [INFO] Server running on http://localhost:3005 +[2026-08-10T20:13:47.033Z] [INFO] Card logs initialized successfully +[2026-08-10T20:17:59.208Z] [INFO] Server running on http://localhost:3005 +[2026-08-10T20:17:59.211Z] [INFO] Card logs initialized successfully +[2026-08-10T20:18:33.110Z] [INFO] Server running on http://localhost:3005 +[2026-08-10T20:18:33.113Z] [INFO] Card logs initialized successfully +[2026-08-10T20:24:20.748Z] [INFO] Server running on http://localhost:3005 +[2026-08-10T20:24:20.752Z] [INFO] Card logs initialized successfully diff --git a/admin-server1/moders/routes/content.js b/admin-server1/moders/routes/content.js index 59c9229..b94e107 100644 --- a/admin-server1/moders/routes/content.js +++ b/admin-server1/moders/routes/content.js @@ -37,7 +37,7 @@ const upload = multer({ // Получить все блоки для вкладки router.get('/:tab', async (req, res) => { const { tab } = req.params; - if (!['info', 'union'].includes(tab)) return res.status(400).json({ message: 'Некорректная вкладка' }); + if (!['info', 'union', 'events', 'help', 'housing'].includes(tab)) return res.status(400).json({ message: 'Некорректная вкладка' }); try { const [rows] = await db.query('SELECT * FROM content_blocks WHERE tab = ? ORDER BY created_at ASC', [tab]); res.json(rows); @@ -57,7 +57,7 @@ router.post('/:tab/upload', requireModerator, upload.single('file'), (req, res) router.post('/:tab', requireModerator, async (req, res) => { const { tab } = req.params; const { content } = req.body; - if (!['info', 'union'].includes(tab)) return res.status(400).json({ message: 'Некорректная вкладка' }); + if (!['info', 'union', 'events', 'help', 'housing'].includes(tab)) return res.status(400).json({ message: 'Некорректная вкладка' }); try { const [result] = await db.query( 'INSERT INTO content_blocks (tab, type, content, created_by, created_at, updated_at) VALUES (?, ?, ?, ?, NOW(), NOW())', @@ -73,7 +73,7 @@ router.post('/:tab', requireModerator, async (req, res) => { router.put('/:tab/:id', requireModerator, async (req, res) => { const { tab, id } = req.params; const { content } = req.body; - if (!['info', 'union'].includes(tab)) return res.status(400).json({ message: 'Некорректная вкладка' }); + if (!['info', 'union', 'events', 'help', 'housing'].includes(tab)) return res.status(400).json({ message: 'Некорректная вкладка' }); try { await db.query( 'UPDATE content_blocks SET content = ?, updated_at = NOW() WHERE id = ? AND tab = ?', @@ -88,7 +88,7 @@ router.put('/:tab/:id', requireModerator, async (req, res) => { // Удалить блок router.delete('/:tab/:id', requireModerator, async (req, res) => { const { tab, id } = req.params; - if (!['info', 'union'].includes(tab)) return res.status(400).json({ message: 'Некорректная вкладка' }); + if (!['info', 'union', 'events', 'help', 'housing'].includes(tab)) return res.status(400).json({ message: 'Некорректная вкладка' }); try { await db.query('DELETE FROM content_blocks WHERE id = ? AND tab = ?', [id, tab]); res.json({ message: 'Удалено' }); @@ -96,5 +96,51 @@ router.delete('/:tab/:id', requireModerator, async (req, res) => { res.status(500).json({ message: 'Ошибка сервера' }); } }); +router.get('/section/:section', async (req, res) => { + const { section } = req.params; + if (!['events', 'help', 'housing'].includes(section)) { + return res.status(400).json({ message: 'Некорректный раздел' }); + } + try { + const [rows] = await db.query( + 'SELECT * FROM content_blocks WHERE tab = ? ORDER BY created_at ASC', + [section] + ); + // Если есть блоки — возвращаем их, иначе пустой массив + res.json(rows); + } catch (err) { + console.error(err); + res.status(500).json({ message: 'Ошибка сервера' }); + } +}); +// Сохранить контент раздела (очищаем старые и вставляем новый) +router.post('/section/:section', requireModerator, async (req, res) => { + const { section } = req.params; + const { content } = req.body; + + if (!['events', 'help', 'housing'].includes(section)) { + return res.status(400).json({ message: 'Некорректный раздел' }); + } + + try { + // Удаляем старые блоки для этого раздела + await db.query('DELETE FROM content_blocks WHERE tab = ?', [section]); + + // Вставляем новый блок + const [result] = await db.query( + 'INSERT INTO content_blocks (tab, type, content, created_by, created_at, updated_at) VALUES (?, ?, ?, ?, NOW(), NOW())', + [section, 'text', content, req.session.userId] + ); + + res.json({ + success: true, + id: result.insertId, + message: 'Раздел сохранён' + }); + } catch (err) { + console.error(err); + res.status(500).json({ message: 'Ошибка сервера' }); + } +}); module.exports = router; \ No newline at end of file diff --git a/admin-server1/server.js b/admin-server1/server.js index bcd4313..ef0f56b 100644 --- a/admin-server1/server.js +++ b/admin-server1/server.js @@ -30,7 +30,7 @@ const sessionConfig = require('./config/session'); app.use(helmet()); // Добавление helmet для защиты app.use(cors(corsConfig)); app.use(morgan('dev')); -app.use(express.json()); +app.use(express.json({ limit: '100mb' })); app.use(session(sessionConfig(session))); // Роуты app.use('/api/auth', authRouter); diff --git a/backend/package-lock.json b/backend/package-lock.json index fa39fba..5c0c20d 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -12,6 +12,8 @@ "cors": "^2.8.5", "dotenv": "^17.2.1", "express": "^5.1.0", + "express-rate-limit": "^8.6.2", + "helmet": "^8.3.0", "mammoth": "^1.9.1", "mysql2": "^3.14.2", "node-cron": "^4.2.1", @@ -598,6 +600,25 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-rate-limit": { + "version": "8.6.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.2.tgz", + "integrity": "sha512-YH4ru+eOJxQABscKFfRCy9R7x9QFGdezclVMwwgFFndzS2Xnm0uo6B0ABZsLhcpeptGv2qvuJVWlQr9gQZoC3A==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, "node_modules/fecha": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", @@ -791,6 +812,18 @@ "node": ">= 0.4" } }, + "node_modules/helmet": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.3.0.tgz", + "integrity": "sha512-Qgpiaws3Sm30Av8Eah6sjMCZZwjlBu+E68rhpCWBshY1lb09HtLwj5GviX0OyQIn+ulUS0iX0AxN5n3tLZzz1w==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/EvanHahn" + } + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -846,6 +879,15 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ip-address": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.5.0.tgz", + "integrity": "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", diff --git a/backend/package.json b/backend/package.json index ae9a300..ba974dc 100644 --- a/backend/package.json +++ b/backend/package.json @@ -15,6 +15,8 @@ "cors": "^2.8.5", "dotenv": "^17.2.1", "express": "^5.1.0", + "express-rate-limit": "^8.6.2", + "helmet": "^8.3.0", "mammoth": "^1.9.1", "mysql2": "^3.14.2", "node-cron": "^4.2.1", diff --git a/backend/src/routes/employee.js b/backend/src/routes/employee.js index a76a925..90c18cf 100644 --- a/backend/src/routes/employee.js +++ b/backend/src/routes/employee.js @@ -21,7 +21,7 @@ async function readJson(filePath) { /** * Получение mock-данных сотрудника из config.json */ -async function getMockEmployee(searchParam, type = 'card') { +async function getMockEmployee(searchParam) { try { const configPath = path.join(__dirname, '../../config.json'); const config = await readJson(configPath); @@ -31,14 +31,20 @@ async function getMockEmployee(searchParam, type = 'card') { } const mockEmployees = config.mockData.employees || []; + const paramStr = String(searchParam); - if (type === 'card') { - return mockEmployees.find(e => String(e.cardNumber) === String(searchParam)) || null; - } else if (type === 'id') { - return mockEmployees.find(e => e.id === parseInt(searchParam)) || null; + // Сначала пробуем найти по cardNumber (как строка, без приведения) + let employee = mockEmployees.find(e => String(e.cardNumber) === paramStr); + + // Если не нашли — пробуем по id (как число) + if (!employee) { + const paramNum = parseInt(paramStr); + if (!isNaN(paramNum)) { + employee = mockEmployees.find(e => e.id === paramNum); + } } - return null; + return employee || null; } catch (err) { logger.error('[MOCK ERROR]', { error: err.message }); return null; diff --git a/backend/src/server.js b/backend/src/server.js index 8221685..d754594 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -6,7 +6,13 @@ const { connectDB } = require('./config/db'); const { logger } = require('./config/logger'); const handbookRoutes = require('./routes/handbook'); const employeeRoutes = require('./routes/employee'); +const helmet = require('helmet'); +const rateLimit = require('express-rate-limit'); require('dotenv').config({ path: path.join(__dirname, '../config.env') }); +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 100 +}); async function readJson(filePath) { const data = await fs.readFile(filePath, 'utf8'); @@ -58,11 +64,20 @@ async function connectToNetworkShare(config) { const app = express(); app.use(cors()); app.use(express.json()); +app.use((req, res, next) => { + if (req.method === 'POST' || req.method === 'PUT') { + if (!req.is('application/json')) { + return res.status(415).json({ message: 'Only JSON allowed' }); + } + } + next(); +}); // ============================================ // 📍 Маршруты API // ============================================ - +app.use(helmet()); +app.use('/api/', limiter); app.use('/api/handbook', handbookRoutes); app.use('/api/employee', employeeRoutes); @@ -320,7 +335,12 @@ app.get('/api/db-status', async (req, res) => { res.status(500).json({ status: 'error', message: 'DB connection failed' }); } }); - +app.get('/api/time', (req, res) => { + res.json({ + time: new Date().toISOString(), + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone + }); +}); async function startServer() { try { const configPath = path.join(__dirname, '../config.json'); @@ -328,6 +348,14 @@ async function startServer() { const { host, port } = config.server; await connectDB(); +app.get('/api/time', (req, res) => { + const now = new Date(); + logger.info(`[TIME] Запрос времени от ${req.ip || req.connection.remoteAddress}`); + res.json({ + time: now.toISOString(), + timestamp: now.getTime() + }); +}); app.listen(port, host, () => { logger.info(`Server is running on http://${host}:${port}`); }); @@ -339,5 +367,8 @@ async function startServer() { } } } - +app.use((err, req, res, next) => { + logger.error('Unhandled error', { error: err.message, stack: err.stack }); + res.status(500).json({ message: 'Внутренняя ошибка сервера' }); +}); startServer(); \ No newline at end of file diff --git a/frontend/.env b/frontend/.env new file mode 100644 index 0000000..55de2dc --- /dev/null +++ b/frontend/.env @@ -0,0 +1 @@ +VITE_NTP_SERVER=http://192.168.139.249:3001/api/time \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index bf2f6fa..ce32cde 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -13,11 +13,12 @@ "@testing-library/user-event": "^13.5.0", "@types/react": "^19.2.0", "@types/react-dom": "^19.2.0", - "axios": "^1.6.0", + "axios": "^1.19.0", "date-fns": "^2.30.0", + "dompurify": "^3.4.13", "dotenv": "^17.4.2", "electron-is-dev": "^2.0.0", - "framer-motion": "^13.0.0", + "framer-motion": "^13.1.0", "pdfjs-dist": "^3.11.174", "react": "^19.2.0", "react-dom": "^19.2.0", @@ -885,9 +886,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1137,9 +1135,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1157,9 +1152,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1177,9 +1169,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1197,9 +1186,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1217,9 +1203,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1237,9 +1220,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1399,9 +1379,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1416,9 +1393,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1433,9 +1407,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1450,9 +1421,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1467,9 +1435,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1484,9 +1449,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1501,9 +1463,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1518,9 +1477,6 @@ "ppc64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1535,9 +1491,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1552,9 +1505,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1569,9 +1519,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1586,9 +1533,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1603,9 +1547,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2040,6 +1981,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/yargs": { "version": "17.0.35", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", @@ -3737,6 +3685,15 @@ "license": "MIT", "peer": true }, + "node_modules/dompurify": { + "version": "3.4.13", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.13.tgz", + "integrity": "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/dotenv": { "version": "17.4.2", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", @@ -4656,9 +4613,9 @@ } }, "node_modules/framer-motion": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-13.0.0.tgz", - "integrity": "sha512-nQGZXlsiigN48nzvE7AL1GLekql+etcphp8v+PQ0X04oxO20yVmV9rU9XQ25c136RdeIYoaWlKT9oAwvJza3mg==", + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-13.1.0.tgz", + "integrity": "sha512-QSZrF0Id3QGuHJ+OL+9PSY9pk86C8ERFalwAGSchzTm65+ZoGH/RM26lmEARLljcHj2lqhv0jZOOks+EI3COOw==", "license": "MIT", "dependencies": { "motion-dom": "^13.0.0", @@ -5783,9 +5740,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -5807,9 +5761,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -5831,9 +5782,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -5855,9 +5803,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ diff --git a/frontend/package.json b/frontend/package.json index 9773877..797b067 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,11 +9,12 @@ "@testing-library/user-event": "^13.5.0", "@types/react": "^19.2.0", "@types/react-dom": "^19.2.0", - "axios": "^1.6.0", + "axios": "^1.19.0", "date-fns": "^2.30.0", + "dompurify": "^3.4.13", "dotenv": "^17.4.2", "electron-is-dev": "^2.0.0", - "framer-motion": "^13.0.0", + "framer-motion": "^13.1.0", "pdfjs-dist": "^3.11.174", "react": "^19.2.0", "react-dom": "^19.2.0", diff --git a/frontend/src/components/BottomBar.tsx b/frontend/src/components/BottomBar.tsx index 1098a3f..9e35c0d 100644 --- a/frontend/src/components/BottomBar.tsx +++ b/frontend/src/components/BottomBar.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useRef } from 'react'; import { motion } from 'framer-motion'; - +const NTP_URL = import.meta.env.VITE_NTP_SERVER || 'http://192.168.139.249:3001/api/time'; interface CurrencyRate { currency: string; rate: number; @@ -30,7 +30,6 @@ const BottomBar: React.FC = () => { const [isOnline, setIsOnline] = useState(navigator.onLine); const [isPaused, setIsPaused] = useState(false); - const [currentTime, setCurrentTime] = useState(new Date()); // ✅ Добавлено const scrollRef = useRef(null); const getAllItems = () => { @@ -58,14 +57,57 @@ const BottomBar: React.FC = () => { const items = getAllItems(); const doubledItems = [...items, ...items, ...items]; - // ✅ Обновление времени каждую секунду +const NTP_URL = 'http://localhost:3005/api/time'; // или твой URL + +const useServerTime = (interval: number = 1000) => { + const [time, setTime] = useState(() => new Date()); + useEffect(() => { - const interval = setInterval(() => { - setCurrentTime(new Date()); - }, 1000); + let timer: NodeJS.Timeout; + let diff = 0; + + const fetchTime = async () => { + try { + const res = await fetch(NTP_URL); + if (!res.ok) throw new Error('Server time unavailable'); + const data = await res.json(); + const serverTime = new Date(data.time); + + // Вычисляем разницу между серверным и локальным временем + diff = serverTime.getTime() - Date.now(); + + // Устанавливаем время + setTime(new Date(Date.now() + diff)); + + // Запускаем локальный таймер + if (timer) clearInterval(timer); + timer = setInterval(() => { + setTime(new Date(Date.now() + diff)); + }, interval); + } catch (err) { + console.warn('Server time unavailable, using local time:', err); + // Если сервер недоступен — используем локальное время + setTime(new Date()); + if (timer) clearInterval(timer); + timer = setInterval(() => { + setTime(new Date()); + }, interval); + } + }; + + // Первый запрос + fetchTime(); + + // Очистка при размонтировании + return () => { + if (timer) clearInterval(timer); + }; + }, [interval]); + + return time; +}; +const currentTime = useServerTime(1000); - return () => clearInterval(interval); - }, []); useEffect(() => { const handleOnline = () => setIsOnline(true); @@ -308,4 +350,5 @@ const BottomBar: React.FC = () => { ); }; + export default BottomBar; \ No newline at end of file diff --git a/frontend/src/components/TradeUnion.tsx b/frontend/src/components/TradeUnion.tsx index 5cab94f..882058a 100644 --- a/frontend/src/components/TradeUnion.tsx +++ b/frontend/src/components/TradeUnion.tsx @@ -1,18 +1,79 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { motion } from 'framer-motion'; -import { FaHandsHelping, FaCalendarCheck, FaMoneyBillWave, FaHome } from 'react-icons/fa'; +import { FaHandsHelping, FaCalendarCheck, FaMoneyBillWave, FaHome, FaTimes } from 'react-icons/fa'; +import DOMPurify from 'dompurify'; interface TradeUnionProps { onBack: () => void; employee: any; } -// Функция-обертка для рендеринга иконок -const renderIcon = (Icon: any, style?: React.CSSProperties) => { - return React.createElement(Icon, { style }); -}; +interface Block { + id: number; + tab: string; + content: string; + created_at: string; +} + +const sections = [ + { id: 'events', title: 'Мероприятия', description: 'Анонсы и запись на мероприятия', icon: FaCalendarCheck, color: '#e74c3c' }, + { id: 'help', title: 'Материальная помощь', description: 'Размеры и виды материальной помощи', icon: FaMoneyBillWave, color: '#f39c12' }, + { id: 'housing', title: 'Жилищные условия', description: 'Информация об очереди на улучшение жилищных условий', icon: FaHome, color: '#3498db' }, +]; const TradeUnion: React.FC = ({ onBack, employee }) => { + const [selectedSection, setSelectedSection] = useState(null); + const [blocksMap, setBlocksMap] = useState>({}); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchAllSections = async () => { + try { + setLoading(true); + const results: Record = {}; + for (const sec of sections) { + const response = await fetch(`http://localhost:3005/moders/content/section/${sec.id}`, { + credentials: 'include', + }); + if (response.ok) { + const data = await response.json(); + results[sec.id] = Array.isArray(data) ? data : []; + } else { + results[sec.id] = []; + } + } + setBlocksMap(results); + } catch (err) { + console.error('Ошибка загрузки разделов профсоюза:', err); + } finally { + setLoading(false); + } + }; + fetchAllSections(); + }, []); + + const openModal = (sectionId: string) => { + setSelectedSection(sectionId); + }; + + const closeModal = () => { + setSelectedSection(null); + }; + + const selectedData = selectedSection ? sections.find(s => s.id === selectedSection) : null; + const selectedBlocks = selectedSection ? blocksMap[selectedSection] || [] : []; + + const formatDate = (dateStr: string) => { + const date = new Date(dateStr); + return date.toLocaleString('ru-RU', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); + }; + return ( = ({ onBack, employee }) => { alignItems: 'center', gap: '12px', }}> - {renderIcon(FaHandsHelping, { color: '#e74c3c', fontSize: '24px' })} + Профсоюз -
- Загрузка...
+ ) : ( +
+ {sections.map((section) => { + const Icon = section.icon; + const blocks = blocksMap[section.id] || []; + const hasContent = blocks.length > 0; + return ( + openModal(section.id)} + style={{ + background: 'white', + borderRadius: '16px', + padding: '25px', + boxShadow: '0 4px 20px rgba(0,0,0,0.06)', + cursor: 'pointer', + transition: 'all 0.3s ease', + border: `1px solid ${hasContent ? section.color : '#f1f2f6'}`, + position: 'relative', + }} + > + {hasContent && ( +
+ {blocks.length} блок(ов) +
+ )} +
+ +
+

+ {section.title} +

+

+ {section.description} +

+ {!hasContent && ( +
+ Информация отсутствует +
+ )} +
+ ); + })} +
+ )} + + {/* ===== МОДАЛКА ===== */} + {selectedSection && selectedData && ( +
{ + if (e.target === e.currentTarget) closeModal(); }} > -
- {renderIcon(FaCalendarCheck)} -
-

- Мероприятия -

-

- Анонсы и запись на мероприятия -

- +
+ {/* Шапка (заголовок + крестик) — фиксирована */} +
+

+ + {selectedData.title} +

+
+ +
+
- -
- {renderIcon(FaMoneyBillWave)} -
-

- Материальная помощь -

-

- Размеры и виды материальной помощи -

-
+ {/* Тело с прокруткой */} +
+ {selectedBlocks.length === 0 ? ( +
+ Информация отсутствует +
+ ) : ( +
+ {selectedBlocks.map((block) => ( +
+
+
+ 🕒 {formatDate(block.created_at)} + ID: {block.id} +
+
+ ))} +
+ )} +
- -
- {renderIcon(FaHome)} + {/* Нижняя кнопка закрытия */} +
+ +
-

- Жилищные условия -

-

- Информация об очереди на улучшение жилищных условий -

-
-
- -
-

- ℹ️ Раздел в разработке. Функционал будет доступен в ближайшее время. -

-
+
+ )} ); };