diff --git a/admin-frontend/package-lock.json b/admin-frontend/package-lock.json index fa82fe3..088d088 100644 --- a/admin-frontend/package-lock.json +++ b/admin-frontend/package-lock.json @@ -15,7 +15,8 @@ "dompurify": "^3.4.13", "quill": "^2.0.3", "react": "^19.2.8", - "react-dom": "^19.2.8" + "react-dom": "^19.2.8", + "xlsx": "^0.18.5" }, "devDependencies": { "@vitejs/plugin-react": "^6.0.5", @@ -441,6 +442,15 @@ } } }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -471,6 +481,40 @@ "dequal": "^2.0.3" } }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", @@ -541,6 +585,15 @@ } } }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1074,6 +1127,18 @@ "node": ">=0.10.0" } }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/strip-indent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", @@ -1180,6 +1245,45 @@ "optional": true } } + }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } } } } diff --git a/admin-frontend/package.json b/admin-frontend/package.json index 01f4a8c..8de0b83 100644 --- a/admin-frontend/package.json +++ b/admin-frontend/package.json @@ -10,9 +10,11 @@ "dompurify": "^3.4.13", "quill": "^2.0.3", "react": "^19.2.8", - "react-dom": "^19.2.8" + "react-dom": "^19.2.8", + "xlsx": "^0.18.5" }, "scripts": { + "rebuild": "rm -rf dist && npm run build", "start": "vite", "build": "vite build", "preview": "vite preview" diff --git a/admin-server1/logs/admin.log b/admin-server1/logs/admin.log index e39ff77..ba3aff4 100644 --- a/admin-server1/logs/admin.log +++ b/admin-server1/logs/admin.log @@ -1751,3 +1751,75 @@ [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 +[2026-08-14T13:24:44.818Z] [INFO] Server running on http://localhost:3005 +[2026-08-14T13:24:44.821Z] [INFO] Card logs initialized successfully +[2026-08-14T13:27:10.675Z] [INFO] [LOGIN] Попытка входа: grivova +[2026-08-14T13:27:10.675Z] [INFO] [LOGIN] Проверка подключения к базе... +[2026-08-14T13:27:10.687Z] [INFO] [LOGIN] Результат поиска пользователя: найден +[2026-08-14T13:27:10.688Z] [INFO] [LOGIN] Сравнение пароля через bcrypt... +[2026-08-14T13:27:10.760Z] [INFO] [LOGIN] Результат сравнения пароля: true +[2026-08-14T13:27:10.761Z] [INFO] [LOGIN] Сессия установлена: {"isModerator":true,"userId":3,"username":"grivova"} +[2026-08-14T13:27:10.763Z] [INFO] [LOGIN] Ответ отправлен +[2026-08-14T13:29:02.161Z] [INFO] Server running on http://localhost:3005 +[2026-08-14T13:29:02.164Z] [INFO] Card logs initialized successfully +[2026-08-14T13:29:12.245Z] [INFO] [LOGIN] Попытка входа: vova +[2026-08-14T13:29:12.245Z] [INFO] [LOGIN] Проверка подключения к базе... +[2026-08-14T13:29:12.247Z] [INFO] [LOGIN] Результат поиска пользователя: найден +[2026-08-14T13:29:12.248Z] [INFO] [LOGIN] Сравнение пароля через bcrypt... +[2026-08-14T13:29:12.326Z] [INFO] [LOGIN] Результат сравнения пароля: false +[2026-08-14T13:29:12.327Z] [WARN] [LOGIN] Неверный пароль для пользователя: vova +[2026-08-14T13:29:17.059Z] [INFO] [LOGIN] Попытка входа: grivova +[2026-08-14T13:29:17.060Z] [INFO] [LOGIN] Проверка подключения к базе... +[2026-08-14T13:29:17.062Z] [INFO] [LOGIN] Результат поиска пользователя: найден +[2026-08-14T13:29:17.063Z] [INFO] [LOGIN] Сравнение пароля через bcrypt... +[2026-08-14T13:29:17.132Z] [INFO] [LOGIN] Результат сравнения пароля: true +[2026-08-14T13:29:17.133Z] [INFO] [LOGIN] Сессия установлена: {"isModerator":true,"userId":3,"username":"grivova"} +[2026-08-14T13:29:17.134Z] [INFO] [LOGIN] Ответ отправлен +[2026-08-14T13:31:55.307Z] [INFO] Server running on http://localhost:3005 +[2026-08-14T13:31:55.311Z] [INFO] Card logs initialized successfully +[2026-08-14T13:32:01.120Z] [INFO] [LOGIN] Попытка входа: grivova +[2026-08-14T13:32:01.121Z] [INFO] [LOGIN] Проверка подключения к базе... +[2026-08-14T13:32:01.130Z] [INFO] [LOGIN] Результат поиска пользователя: найден +[2026-08-14T13:32:01.131Z] [INFO] [LOGIN] Сравнение пароля через bcrypt... +[2026-08-14T13:32:01.211Z] [INFO] [LOGIN] Результат сравнения пароля: true +[2026-08-14T13:32:01.212Z] [INFO] [LOGIN] Сессия установлена: {"isModerator":true,"userId":3,"username":"grivova"} +[2026-08-14T13:32:01.213Z] [INFO] [LOGIN] Ответ отправлен +[2026-08-14T13:32:24.329Z] [INFO] [LOGIN] Попытка входа: grivova +[2026-08-14T13:32:24.329Z] [INFO] [LOGIN] Проверка подключения к базе... +[2026-08-14T13:32:24.332Z] [INFO] [LOGIN] Результат поиска пользователя: найден +[2026-08-14T13:32:24.332Z] [INFO] [LOGIN] Сравнение пароля через bcrypt... +[2026-08-14T13:32:24.403Z] [INFO] [LOGIN] Результат сравнения пароля: true +[2026-08-14T13:32:24.403Z] [INFO] [LOGIN] Сессия установлена: {"isModerator":true,"userId":3,"username":"grivova"} +[2026-08-14T13:32:24.404Z] [INFO] [LOGIN] Ответ отправлен +[2026-08-14T13:38:25.063Z] [INFO] Server running on http://localhost:3005 +[2026-08-14T13:38:25.067Z] [INFO] Card logs initialized successfully +[2026-08-16T01:27:22.968Z] [INFO] Server running on http://localhost:3005 +[2026-08-16T01:27:22.971Z] [INFO] Card logs initialized successfully +[2026-08-16T01:28:31.640Z] [INFO] [DB CHECK] Проверка подключения к MySQL: localhost:3306/rasch_content +[2026-08-16T01:28:31.649Z] [INFO] [DB CHECK] ✅ Успешное подключение к MySQL +[2026-08-16T01:28:40.817Z] [WARN] Неверный пароль +[2026-08-16T02:02:39.663Z] [INFO] Server running on http://localhost:3005 +[2026-08-16T02:02:39.665Z] [INFO] Card logs initialized successfully +[2026-08-17T03:07:53.436Z] [INFO] Server running on http://localhost:3005 +[2026-08-17T03:07:53.439Z] [INFO] Card logs initialized successfully +[2026-08-17T03:45:34.461Z] [INFO] Server running on http://localhost:3005 +[2026-08-17T03:45:34.464Z] [INFO] Card logs initialized successfully +[2026-08-17T03:45:42.164Z] [INFO] [LOGIN] Попытка входа: vova +[2026-08-17T03:45:42.164Z] [INFO] [LOGIN] Проверка подключения к базе... +[2026-08-17T03:45:42.182Z] [INFO] [LOGIN] Результат поиска пользователя: найден +[2026-08-17T03:45:42.183Z] [INFO] [LOGIN] Сравнение пароля через bcrypt... +[2026-08-17T03:45:42.264Z] [INFO] [LOGIN] Результат сравнения пароля: false +[2026-08-17T03:45:42.265Z] [WARN] [LOGIN] Неверный пароль для пользователя: vova +[2026-08-17T03:45:46.561Z] [INFO] [LOGIN] Попытка входа: grivova +[2026-08-17T03:45:46.562Z] [INFO] [LOGIN] Проверка подключения к базе... +[2026-08-17T03:45:46.564Z] [INFO] [LOGIN] Результат поиска пользователя: найден +[2026-08-17T03:45:46.565Z] [INFO] [LOGIN] Сравнение пароля через bcrypt... +[2026-08-17T03:45:46.642Z] [INFO] [LOGIN] Результат сравнения пароля: true +[2026-08-17T03:45:46.643Z] [INFO] [LOGIN] Сессия установлена: {"isModerator":true,"userId":3,"username":"grivova"} +[2026-08-17T03:45:46.647Z] [INFO] [LOGIN] Ответ отправлен +[2026-08-17T13:53:20.830Z] [INFO] [LOGIN] Попытка входа: grivova +[2026-08-17T13:53:20.845Z] [INFO] [LOGIN] Проверка подключения к базе... +[2026-08-17T13:53:20.849Z] [INFO] [LOGIN] Результат поиска пользователя: не найден +[2026-08-17T13:53:20.849Z] [WARN] [LOGIN] Пользователь не найден: grivova +[2026-08-18T10:10:33.384Z] [INFO] Server running on http://localhost:3005 +[2026-08-18T10:10:33.394Z] [INFO] Card logs initialized successfully diff --git a/admin-server1/package-lock.json b/admin-server1/package-lock.json index f9c6c04..ff30023 100644 --- a/admin-server1/package-lock.json +++ b/admin-server1/package-lock.json @@ -56,6 +56,18 @@ "node": ">= 0.6" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/append-field": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", @@ -141,9 +153,9 @@ } }, "node_modules/body-parser/node_modules/content-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", - "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", "license": "MIT", "engines": { "node": ">=18" @@ -534,15 +546,6 @@ "node": ">=0.10.0" } }, - "node_modules/express-mysql-session/node_modules/lru-cache": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz", - "integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==", - "license": "ISC", - "engines": { - "node": ">=16.14" - } - }, "node_modules/express-mysql-session/node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -884,18 +887,6 @@ "node": ">= 6" } }, - "node_modules/https-proxy-agent/node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "license": "MIT", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, "node_modules/iconv-lite": { "version": "0.7.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", @@ -919,9 +910,9 @@ "license": "ISC" }, "node_modules/ip-address": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.4.0.tgz", - "integrity": "sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ==", + "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" @@ -973,6 +964,15 @@ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", "license": "Apache-2.0" }, + "node_modules/lru-cache": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz", + "integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==", + "license": "ISC", + "engines": { + "node": ">=16.14" + } + }, "node_modules/lru.min": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", @@ -1151,15 +1151,14 @@ } }, "node_modules/mysql2": { - "version": "3.23.2", - "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.23.2.tgz", - "integrity": "sha512-fxh3HpQ8vJtu/Mmnd4Xsur19jGjHGzRLMxptiDtOkbX7EVBgnafGSGDx1WGGVmJLClVh2LeeBMMo24IFv8wCyQ==", + "version": "3.23.3", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.23.3.tgz", + "integrity": "sha512-ehp9HEKr4wVJaBOUVxNFa+CNrsCCCZ6363/jbGhb7WpEmSRNIXjHBjFs5K2s2cXn7j/RhDieejsQJ6nfUwD6vQ==", "license": "MIT", "dependencies": { "aws-ssl-profiles": "^1.1.2", - "denque": "^2.1.0", "generate-function": "^2.3.1", - "iconv-lite": "^0.7.2", + "iconv-lite": "^0.7.3", "long": "^5.3.2", "lru.min": "^1.1.4", "named-placeholders": "^1.1.6", @@ -1635,9 +1634,9 @@ } }, "node_modules/type-is/node_modules/content-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", - "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", "license": "MIT", "engines": { "node": ">=18" diff --git a/backend/config.json b/backend/config.json index bbc3d97..06c647b 100644 --- a/backend/config.json +++ b/backend/config.json @@ -18,7 +18,7 @@ "category": "Высшая", "departmentName": "Технический отдел", "departmentCode": "101", - "organization": "ООО ТехноЦентр", + "organization": "Филиал «Завод Химволокно» ОАО «Гродно Азот»", "identNumber": "123456789" }, { @@ -32,7 +32,7 @@ "category": "1", "departmentName": "Бухгалтерия", "departmentCode": "202", - "organization": "ООО ТехноЦентр", + "organization": "Филиал «Завод Химволокно» ОАО «Гродно Азот»", "identNumber": "987654321" }, { @@ -46,7 +46,7 @@ "category": "2", "departmentName": "IT-отдел", "departmentCode": "303", - "organization": "ООО ТехноЦентр", + "organization": "Филиал «Завод Химволокно» ОАО «Гродно Азот»", "identNumber": "555555555" } ], diff --git a/backend/package-lock.json b/backend/package-lock.json index 5c0c20d..ef9affc 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -17,10 +17,11 @@ "mammoth": "^1.9.1", "mysql2": "^3.14.2", "node-cron": "^4.2.1", - "winston": "^3.17.0" + "winston": "^3.17.0", + "xlsx": "^0.18.5" }, "devDependencies": { - "nodemon": "^2.0.22" + "nodemon": "^3.1.14" } }, "node_modules/@colors/colors": { @@ -91,6 +92,15 @@ "node": ">= 0.6" } }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -130,11 +140,14 @@ } }, "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } }, "node_modules/base64-js": { "version": "1.5.1", @@ -213,14 +226,16 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", - "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" } }, "node_modules/braces": { @@ -274,6 +289,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -299,6 +327,15 @@ "fsevents": "~2.3.2" } }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/color": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/color/-/color-5.0.3.tgz", @@ -345,13 +382,6 @@ "node": ">=18" } }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, "node_modules/content-disposition": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", @@ -415,6 +445,18 @@ "url": "https://opencollective.com/express" } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -674,6 +716,15 @@ "node": ">= 0.6" } }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/fresh": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", @@ -1133,16 +1184,19 @@ } }, "node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^5.0.8" }, "engines": { - "node": "*" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/ms": { @@ -1204,19 +1258,19 @@ } }, "node_modules/nodemon": { - "version": "2.0.22", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.22.tgz", - "integrity": "sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ==", + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz", + "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==", "dev": true, "license": "MIT", "dependencies": { "chokidar": "^3.5.2", - "debug": "^3.2.7", + "debug": "^4", "ignore-by-default": "^1.0.1", - "minimatch": "^3.1.2", + "minimatch": "^10.2.1", "pstree.remy": "^1.1.8", - "semver": "^5.7.1", - "simple-update-notifier": "^1.0.7", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", "supports-color": "^5.5.0", "touch": "^3.1.0", "undefsafe": "^2.0.5" @@ -1225,23 +1279,13 @@ "nodemon": "bin/nodemon.js" }, "engines": { - "node": ">=8.10.0" + "node": ">=10" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/nodemon" } }, - "node_modules/nodemon/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -1492,13 +1536,16 @@ "license": "MIT" }, "node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "dev": true, "license": "ISC", "bin": { - "semver": "bin/semver" + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/send": { @@ -1631,26 +1678,16 @@ } }, "node_modules/simple-update-notifier": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz", - "integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", "dev": true, "license": "MIT", "dependencies": { - "semver": "~7.0.0" + "semver": "^7.5.3" }, "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/simple-update-notifier/node_modules/semver": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", - "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "node": ">=10" } }, "node_modules/sprintf-js": { @@ -1674,6 +1711,18 @@ "url": "https://github.com/mysqljs/sql-escaper?sponsor=1" } }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/stack-trace": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", @@ -1900,12 +1949,51 @@ "node": ">= 6" } }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/xmlbuilder": { "version": "10.1.1", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz", diff --git a/backend/package.json b/backend/package.json index ba974dc..c3621fd 100644 --- a/backend/package.json +++ b/backend/package.json @@ -20,9 +20,10 @@ "mammoth": "^1.9.1", "mysql2": "^3.14.2", "node-cron": "^4.2.1", - "winston": "^3.17.0" + "winston": "^3.17.0", + "xlsx": "^0.18.5" }, "devDependencies": { - "nodemon": "^2.0.22" + "nodemon": "^3.1.14" } } diff --git a/backend/rates.xlsx b/backend/rates.xlsx new file mode 100644 index 0000000..b804e1b Binary files /dev/null and b/backend/rates.xlsx differ diff --git a/backend/src/server.js b/backend/src/server.js index d754594..f873f4f 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -1,27 +1,34 @@ const express = require('express'); const cors = require('cors'); const path = require('path'); -const fs = require('fs').promises; +const fs = require('fs'); +const fsPromises = require('fs').promises; 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'); +const XLSX = require('xlsx'); 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'); + const data = await fsPromises.readFile(filePath, 'utf8'); return JSON.parse(data); } async function pathExists(filePath) { try { - await fs.access(filePath); + await fsPromises.access(filePath); return true; } catch { return false; @@ -38,33 +45,34 @@ async function connectToNetworkShare(config) { const username = config.networkCredentials.username; const password = config.networkCredentials.password; const domain = config.networkCredentials.domain; - const netUseCommand = `net use \\${payslipsPath} ${password} /user:${domain ? domain + '\\' : ''}${username}`; + const netUseCommand = 'net use \\\\' + payslipsPath + ' ' + password + ' /user:' + (domain ? domain + '\\' : '') + username; - logger.info(`Attempting to connect to network share with credentials`); - const { stdout, stderr } = await execAsync(netUseCommand); + logger.info('Attempting to connect to network share with credentials'); + const result = await execAsync(netUseCommand); - if (stderr) { - logger.warn(`Network share connection warning: ${stderr}`); + if (result.stderr) { + logger.warn('Network share connection warning: ' + result.stderr); } - logger.info(`Network share connection result: ${stdout}`); + logger.info('Network share connection result: ' + result.stdout); return true; } catch (error) { logger.error('Failed to connect to network share', { error: error.message }); return false; } } - return true; + return true; } // ============================================ -// 🚀 Инициализация Express +// 🚀 ИНИЦИАЛИЗАЦИЯ EXPRESS // ============================================ const app = express(); app.use(cors()); app.use(express.json()); -app.use((req, res, next) => { + +app.use(function(req, res, next) { if (req.method === 'POST' || req.method === 'PUT') { if (!req.is('application/json')) { return res.status(415).json({ message: 'Only JSON allowed' }); @@ -73,47 +81,292 @@ app.use((req, res, next) => { next(); }); -// ============================================ -// 📍 Маршруты API -// ============================================ app.use(helmet()); app.use('/api/', limiter); + +// ============================================ +// 📍 МАРШРУТЫ API +// ============================================ + app.use('/api/handbook', handbookRoutes); app.use('/api/employee', employeeRoutes); // ============================================ -// 🧪 Тестовые эндпоинты +// 🧪 ТЕСТОВЫЙ ЭНДПОИНТ // ============================================ -app.get('/api/test', (req, res) => { +app.get('/api/test', function(req, res) { res.json({ message: 'API работает' }); }); // ============================================ -// 📊 Эндпоинт для получения статуса системы +// 📊 КУРСЫ ВАЛЮТ ИЗ EXCEL // ============================================ -app.get('/api/system-status', async (req, res) => { +var EXCEL_PATH = path.join(__dirname, '../rates.xlsx'); + +async function createExcelTemplate() { + var wb = XLSX.utils.book_new(); + + var currencyData = [ + ['currency', 'rate', 'change'], + ['USD', 3.22, 0.5], + ['EUR', 3.45, -0.2], + ['CNY', 0.44, 0.1], + ['RUB', 0.035, -0.3] + ]; + var currencySheet = XLSX.utils.aoa_to_sheet(currencyData); + XLSX.utils.book_append_sheet(wb, currencySheet, 'Currency'); + + var metalsData = [ + ['metal', 'price', 'change'], + ['Золото', 1950, 0.8], + ['Серебро', 23.5, -0.5], + ['Платина', 920, 0.3], + ['Палладий', 1450, -0.7] + ]; + var metalsSheet = XLSX.utils.aoa_to_sheet(metalsData); + XLSX.utils.book_append_sheet(wb, metalsSheet, 'Metals'); + + XLSX.writeFile(wb, EXCEL_PATH); + logger.info('Excel-шаблон создан'); +} + +function getRatesFromExcel() { try { - const configPath = path.join(__dirname, '../config.json'); - const config = await readJson(configPath); - const { getDBStatus } = require('./config/db'); - const dbStatus = await getDBStatus(); + if (!fs.existsSync(EXCEL_PATH)) { + return null; + } + var workbook = XLSX.readFile(EXCEL_PATH); - let networkAccess = false; - let networkError = null; - let networkContents = []; + var currencySheet = workbook.Sheets['Currency']; + var currency = []; + if (currencySheet) { + var data = XLSX.utils.sheet_to_json(currencySheet); + for (var i = 0; i < data.length; i++) { + var row = data[i]; + currency.push({ + currency: String(row.currency || row.Currency || '').toUpperCase(), + rate: parseFloat(row.rate || row.Rate || 0), + change: parseFloat(row.change || row.Change || 0) + }); + } + } + + var metalsSheet = workbook.Sheets['Metals']; + var metals = []; + if (metalsSheet) { + var data = XLSX.utils.sheet_to_json(metalsSheet); + for (var i = 0; i < data.length; i++) { + var row = data[i]; + metals.push({ + metal: String(row.metal || row.Metal || row.name || row.Name || ''), + price: parseFloat(row.price || row.Price || 0), + change: parseFloat(row.change || row.Change || 0) + }); + } + } + + return { currency: currency, metals: metals }; + } catch (error) { + logger.error('Ошибка чтения Excel:', error); + return null; + } +} + +// ============================================ +// 📊 GET /api/rates +// ============================================ + +app.get('/api/rates', async function(req, res) { + try { + var fileExists = await pathExists(EXCEL_PATH); + if (!fileExists) { + await createExcelTemplate(); + } + + var data = getRatesFromExcel(); + + if (data && data.currency.length > 0 && data.metals.length > 0) { + res.json({ + currency: data.currency, + metals: data.metals, + source: 'excel', + updated: new Date().toISOString() + }); + } else { + res.json({ + currency: [ + { currency: 'USD', rate: 3.22, change: 0.5 }, + { currency: 'EUR', rate: 3.45, change: -0.2 }, + { currency: 'CNY', rate: 0.44, change: 0.1 }, + { currency: 'RUB', rate: 0.035, change: -0.3 } + ], + metals: [ + { metal: 'Золото', price: 1950, change: 0.8 }, + { metal: 'Серебро', price: 23.5, change: -0.5 }, + { metal: 'Платина', price: 920, change: 0.3 }, + { metal: 'Палладий', price: 1450, change: -0.7 } + ], + source: 'default' + }); + } + } catch (err) { + logger.error('Ошибка получения курсов:', err); + res.status(500).json({ error: err.message }); + } +}); + +// ============================================ +// 🔄 POST /api/rates +// ============================================ + +app.post('/api/rates', async function(req, res) { + try { + var currency = req.body.currency; + var metals = req.body.metals; + + var wb = XLSX.utils.book_new(); + + if (currency && currency.length > 0) { + var currencyData = [ + ['currency', 'rate', 'change'] + ]; + for (var i = 0; i < currency.length; i++) { + currencyData.push([currency[i].currency, currency[i].rate, currency[i].change]); + } + var currencySheet = XLSX.utils.aoa_to_sheet(currencyData); + XLSX.utils.book_append_sheet(wb, currencySheet, 'Currency'); + } + + if (metals && metals.length > 0) { + var metalsData = [ + ['metal', 'price', 'change'] + ]; + for (var i = 0; i < metals.length; i++) { + metalsData.push([metals[i].metal, metals[i].price, metals[i].change]); + } + var metalsSheet = XLSX.utils.aoa_to_sheet(metalsData); + XLSX.utils.book_append_sheet(wb, metalsSheet, 'Metals'); + } + + XLSX.writeFile(wb, EXCEL_PATH); + logger.info('Excel-файл обновлён'); + + res.json({ + success: true, + message: 'Курсы обновлены в Excel', + path: EXCEL_PATH + }); + + } catch (err) { + logger.error('Ошибка при обновлении Excel:', err); + res.status(500).json({ error: err.message }); + } +}); + +// ============================================ +// 🔄 POST /api/rates/update-from-nbrb +// ============================================ + +app.post('/api/rates/update-from-nbrb', async function(req, res) { + try { + var currencyResponse = await fetch('https://api.nbrb.by/exrates/rates?periodicity=0'); + var currencyData = await currencyResponse.json(); + + var metalResponse = await fetch('https://api.nbrb.by/ingots/prices'); + var metalData = await metalResponse.json(); + + var currency = [ + { currency: 'USD', rate: 0, change: 0 }, + { currency: 'EUR', rate: 0, change: 0 }, + { currency: 'CNY', rate: 0, change: 0 }, + { currency: 'RUB', rate: 0, change: 0 } + ]; + + for (var i = 0; i < currencyData.length; i++) { + var c = currencyData[i]; + if (c.Cur_Abbreviation === 'USD') currency[0].rate = c.Cur_OfficialRate || 0; + if (c.Cur_Abbreviation === 'EUR') currency[1].rate = c.Cur_OfficialRate || 0; + if (c.Cur_Abbreviation === 'CNY') currency[2].rate = c.Cur_OfficialRate || 0; + if (c.Cur_Abbreviation === 'RUB') currency[3].rate = c.Cur_OfficialRate || 0; + } + + var metals = [ + { metal: 'Золото', price: 0, change: 0 }, + { metal: 'Серебро', price: 0, change: 0 }, + { metal: 'Платина', price: 0, change: 0 }, + { metal: 'Палладий', price: 0, change: 0 } + ]; + + for (var i = 0; i < metalData.length; i++) { + var m = metalData[i]; + if (m.MetalId === 0) metals[0].price = m.EntitiesDollars || 0; + if (m.MetalId === 1) metals[1].price = m.EntitiesDollars || 0; + if (m.MetalId === 2) metals[2].price = m.EntitiesDollars || 0; + if (m.MetalId === 3) metals[3].price = m.EntitiesDollars || 0; + } + + var wb = XLSX.utils.book_new(); + + var currencySheetData = [ + ['currency', 'rate', 'change'] + ]; + for (var i = 0; i < currency.length; i++) { + currencySheetData.push([currency[i].currency, currency[i].rate, currency[i].change]); + } + var currencySheet = XLSX.utils.aoa_to_sheet(currencySheetData); + XLSX.utils.book_append_sheet(wb, currencySheet, 'Currency'); + + var metalsSheetData = [ + ['metal', 'price', 'change'] + ]; + for (var i = 0; i < metals.length; i++) { + metalsSheetData.push([metals[i].metal, metals[i].price, metals[i].change]); + } + var metalsSheet = XLSX.utils.aoa_to_sheet(metalsSheetData); + XLSX.utils.book_append_sheet(wb, metalsSheet, 'Metals'); + + XLSX.writeFile(wb, EXCEL_PATH); + logger.info('Курсы обновлены из НБРБ'); + + res.json({ + success: true, + message: 'Курсы обновлены из НБРБ', + data: { currency: currency, metals: metals } + }); + + } catch (err) { + logger.error('Ошибка при обновлении курсов из НБРБ:', err); + res.status(500).json({ error: err.message }); + } +}); + +// ============================================ +// 🗄️ ДРУГИЕ ЭНДПОИНТЫ +// ============================================ + +app.get('/api/system-status', async function(req, res) { + try { + var configPath = path.join(__dirname, '../config.json'); + var config = await readJson(configPath); + var db = require('./config/db'); + var dbStatus = await db.getDBStatus(); + + var networkAccess = false; + var networkError = null; + var networkContents = []; try { networkAccess = await pathExists(config.payslipsPath); if (networkAccess) { - const stats = await fs.stat(config.payslipsPath); + var stats = await fsPromises.stat(config.payslipsPath); networkAccess = stats.isDirectory(); if (networkAccess) { - networkContents = await fs.readdir(config.payslipsPath); + networkContents = await fsPromises.readdir(config.payslipsPath); if (networkContents.length > 10) { networkContents = networkContents.slice(0, 10); - networkContents.push(`... и еще ${networkContents.length - 10} элементов`); + networkContents.push('... и еще ' + (networkContents.length - 10) + ' элементов'); } } } @@ -122,7 +375,7 @@ app.get('/api/system-status', async (req, res) => { networkError = err.message; } - const serverInfo = { + var serverInfo = { uptime: process.uptime(), memory: process.memoryUsage(), nodeVersion: process.version, @@ -145,8 +398,8 @@ app.get('/api/system-status', async (req, res) => { }, config: { testMode: config.testMode || false, - serverHost: config.server?.host || 'localhost', - serverPort: config.server?.port || '3001' + serverHost: config.server ? config.server.host : 'localhost', + serverPort: config.server ? config.server.port : '3001' } }); @@ -160,22 +413,18 @@ app.get('/api/system-status', async (req, res) => { } }); -// ============================================ -// 🌐 Тест сетевой папки -// ============================================ - -app.get('/api/test-network', async (req, res) => { +app.get('/api/test-network', async function(req, res) { try { - const configPath = path.join(__dirname, '../config.json'); - const config = await readJson(configPath); - const payslipBasePath = config.payslipsPath; + var configPath = path.join(__dirname, '../config.json'); + var config = await readJson(configPath); + var payslipBasePath = config.payslipsPath; - logger.info(`Testing network connection to: ${payslipBasePath}`); + logger.info('Testing network connection to: ' + payslipBasePath); - const networkConnected = await connectToNetworkShare(config); - logger.info(`Network share connection successful: ${networkConnected}`); + var networkConnected = await connectToNetworkShare(config); + logger.info('Network share connection successful: ' + networkConnected); - const testResults = { + var testResults = { basePath: payslipBasePath, basePathExists: false, basePathAccessible: false, @@ -186,16 +435,16 @@ app.get('/api/test-network', async (req, res) => { try { testResults.basePathExists = await pathExists(payslipBasePath); - logger.info(`Base path exists: ${testResults.basePathExists}`); + logger.info('Base path exists: ' + testResults.basePathExists); if (testResults.basePathExists) { - const stats = await fs.stat(payslipBasePath); + var stats = await fsPromises.stat(payslipBasePath); testResults.basePathAccessible = stats.isDirectory(); - logger.info(`Base path is directory: ${testResults.basePathAccessible}`); + logger.info('Base path is directory: ' + testResults.basePathAccessible); if (testResults.basePathAccessible) { - testResults.basePathContents = await fs.readdir(payslipBasePath); - logger.info(`Base path contents: ${JSON.stringify(testResults.basePathContents)}`); + testResults.basePathContents = await fsPromises.readdir(payslipBasePath); + logger.info('Base path contents: ' + JSON.stringify(testResults.basePathContents)); } } } catch (error) { @@ -210,55 +459,52 @@ app.get('/api/test-network', async (req, res) => { } }); -// ============================================ -// 📄 Получение расчетного листка (с периодом) -// ============================================ - -app.get('/api/payslip/:period/:fileName', async (req, res) => { +app.get('/api/payslip/:period/:fileName', async function(req, res) { try { - const { period, fileName } = req.params; - const configPath = path.join(__dirname, '../config.json'); - const config = await readJson(configPath); - const payslipBasePath = config.payslipsPath; + var period = req.params.period; + var fileName = req.params.fileName; + var configPath = path.join(__dirname, '../config.json'); + var config = await readJson(configPath); + var payslipBasePath = config.payslipsPath; - logger.info(`Payslip request - Period: ${period}, File: ${fileName}`); - logger.info(`Base path from config: ${payslipBasePath}`); + logger.info('Payslip request - Period: ' + period + ', File: ' + fileName); + logger.info('Base path from config: ' + payslipBasePath); - const networkConnected = await connectToNetworkShare(config); - logger.info(`Network share connection for payslip: ${networkConnected}`); + var networkConnected = await connectToNetworkShare(config); + logger.info('Network share connection for payslip: ' + networkConnected); - let finalPayslipPath; + var finalPayslipPath; if (path.isAbsolute(payslipBasePath)) { finalPayslipPath = path.join(payslipBasePath, period, fileName); } else { - const projectRoot = path.resolve(__dirname, '../../'); + var projectRoot = path.resolve(__dirname, '../../'); finalPayslipPath = path.join(projectRoot, payslipBasePath, period, fileName); } - logger.info(`Final payslip path: ${finalPayslipPath}`); + logger.info('Final payslip path: ' + finalPayslipPath); try { - const basePathExists = await pathExists(payslipBasePath); - logger.info(`Base path exists: ${basePathExists}`); + var basePathExists = await pathExists(payslipBasePath); + logger.info('Base path exists: ' + basePathExists); if (basePathExists) { - const basePathStats = await fs.stat(payslipBasePath); - logger.info(`Base path stats: ${JSON.stringify({ + var basePathStats = await fsPromises.stat(payslipBasePath); + logger.info('Base path stats: ' + JSON.stringify({ isDirectory: basePathStats.isDirectory(), permissions: basePathStats.mode, size: basePathStats.size - })}`); + })); - const basePathContents = await fs.readdir(payslipBasePath); - logger.info(`Base path contents: ${JSON.stringify(basePathContents)}`); + var basePathContents = await fsPromises.readdir(payslipBasePath); + logger.info('Base path contents: ' + JSON.stringify(basePathContents)); - const periodPath = path.join(payslipBasePath, period); - const periodPathExists = await pathExists(periodPath); - logger.info(`Period path exists: ${periodPathExists} (${periodPath})`); + var periodPath = path.join(payslipBasePath, period); + var periodPathExists = await pathExists(periodPath); + logger.info('Period path exists: ' + periodPathExists + ' (' + periodPath + ')'); if (periodPathExists) { - const periodPathContents = await fs.readdir(periodPath); - logger.info(`Period path contents: ${JSON.stringify(periodPathContents)}`); + var periodPathContents = await fsPromises.readdir(periodPath); + logger.info('Period path contents: ' + JSON.stringify(periodPathContents)); } } } catch (basePathError) { @@ -269,10 +515,10 @@ app.get('/api/payslip/:period/:fileName', async (req, res) => { } if (await pathExists(finalPayslipPath)) { - logger.info(`Payslip found, sending file: ${finalPayslipPath}`); + logger.info('Payslip found, sending file: ' + finalPayslipPath); res.sendFile(finalPayslipPath); } else { - logger.warn(`Payslip not found: ${finalPayslipPath}`); + logger.warn('Payslip not found: ' + finalPayslipPath); res.status(404).json({ message: 'Расчетный листок не найден', path: finalPayslipPath, @@ -288,31 +534,27 @@ app.get('/api/payslip/:period/:fileName', async (req, res) => { } }); -// ============================================ -// 📄 Получение расчетного листка (без периода) -// ============================================ - -app.get('/api/payslip/:fileName', async (req, res) => { +app.get('/api/payslip/:fileName', async function(req, res) { try { - const { fileName } = req.params; - const configPath = path.join(__dirname, '../config.json'); - const config = await readJson(configPath); - const payslipBasePath = config.payslipsPath; + var fileName = req.params.fileName; + var configPath = path.join(__dirname, '../config.json'); + var config = await readJson(configPath); + var payslipBasePath = config.payslipsPath; - let finalPayslipPath; + var finalPayslipPath; if (path.isAbsolute(payslipBasePath)) { finalPayslipPath = path.join(payslipBasePath, fileName); } else { - const projectRoot = path.resolve(__dirname, '../../'); + var projectRoot = path.resolve(__dirname, '../../'); finalPayslipPath = path.join(projectRoot, payslipBasePath, fileName); } - logger.info(`Attempting to find payslip at resolved path: ${finalPayslipPath}`); + logger.info('Attempting to find payslip at resolved path: ' + finalPayslipPath); if (await pathExists(finalPayslipPath)) { res.sendFile(finalPayslipPath); } else { - logger.warn(`Payslip not found: ${finalPayslipPath}`); + logger.warn('Payslip not found: ' + finalPayslipPath); res.status(404).json({ message: 'Расчетный листок не найден' }); } } catch (err) { @@ -321,43 +563,47 @@ app.get('/api/payslip/:fileName', async (req, res) => { } }); -// ============================================ - -// ============================================ - -app.get('/api/db-status', async (req, res) => { +app.get('/api/db-status', async function(req, res) { try { - const { query } = require('./config/db'); - await query('SELECT 1'); + var db = require('./config/db'); + await db.query('SELECT 1'); res.status(200).json({ status: 'ok', message: 'DB connection successful' }); } catch (err) { logger.error('Database connection check failed', { error: err.message, stack: err.stack }); 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'); - const config = await readJson(configPath); - 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}`); +app.get('/api/time', function(req, res) { + var 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}`); + +// ============================================ +// 🚀 ЗАПУСК СЕРВЕРА +// ============================================ + +async function startServer() { + try { + var configPath = path.join(__dirname, '../config.json'); + var config = await readJson(configPath); + var host = config.server.host; + var port = config.server.port; + + var excelExists = await pathExists(EXCEL_PATH); + if (!excelExists) { + await createExcelTemplate(); + logger.info('Создан Excel-шаблон'); + } + + await connectDB(); + app.listen(port, host, function() { + logger.info('Server is running on http://' + host + ':' + port); + logger.info('Excel file: ' + EXCEL_PATH); }); } catch (err) { if (logger) { @@ -367,8 +613,14 @@ app.get('/api/time', (req, res) => { } } } -app.use((err, req, res, next) => { + +// ============================================ +// ❌ ОБРАБОТКА ОШИБОК +// ============================================ + +app.use(function(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/src/App.tsx b/frontend/src/App.tsx index e9fa578..b5e37d0 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -5,10 +5,11 @@ import LoginWindow from './components/LoginWindow'; import GuestMode from './components/GuestMode'; import MainMenu from './components/MenuComponent'; import VideoBackground from './components/VideoBackground'; +import WeatherWidget from './components/WeatherWidget'; import BottomBar from './components/BottomBar'; import PayslipViewer from './components/PayslipViewer'; import EmployeeInfo from './components/EmployeeInfo'; -import { useActivityTimer } from './components/useActivityTimer' +import { useActivityTimer } from './components/useActivityTimer'; import config from './config.json'; // Импорты разделов @@ -314,6 +315,9 @@ function App() { // ============================================ // 🚀 Рендер приложения // ============================================ + const showTopBar = !showVideo && !showPayslip; + const topBarHeight = 70; // высота виджета + return (
+ )} - {/* 📱 Нижняя панель — ПОКАЗЫВАЕМ ВСЕГДА, кроме видео и расчетного листка */} - {!showVideo && !showPayslip && ( -${row.desc} ${row.internal}${row.city ? ' ' + row.city : ''}
`); + } + phoneRows = []; + }; + children.forEach(node => { + if (node.nodeType === 1) { + const tag = (node as HTMLElement).tagName.toLowerCase(); + const text = (node as HTMLElement).textContent?.trim() || ''; + const phones = Array.from(text.matchAll(phoneRegex), m => m[1]); + if ((tag === 'p' || tag === 'li') && phones.length >= 1) { + flushHtmlBuffer(); + if (phones.length === 2) { + const desc = text.replace(phoneRegex, '').trim(); + phoneRows.push({ desc, internal: phones[0], city: phones[1] }); + lastDesc = desc; + } else if (phones.length === 1) { + phoneRows.push({ desc: lastDesc, internal: phones[0], city: '' }); + } else if (text && !phones.length) { + lastDesc = text; + } + } else { + if (phoneRows.length) { + flushPhoneRows(); + } + if ((node as HTMLElement).outerHTML) { + htmlBuffer += (node as HTMLElement).outerHTML; + } else { + htmlBuffer += node.textContent || ''; + } + } + } else if (node.nodeType === 3) { + htmlBuffer += node.textContent; + } + }); + if (phoneRows.length) { + flushPhoneRows(); + } + if (htmlBuffer) { + blocks.push(htmlBuffer); + } + return blocks; +} + +// ============================================ +// 📚 Функции рендеринга с подсветкой и поиском +// ============================================ +const ADMIN_SERVER_URL = config.adminServerUrl || 'http://localhost:3005'; + +function patchMediaSrc(html: string): string { + return html + .replace(/