weather&bottom

This commit is contained in:
2026-08-18 13:19:05 +03:00
parent c8168ff8d7
commit f052486e71
16 changed files with 2240 additions and 989 deletions
+3 -3
View File
@@ -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"
}
],
+149 -61
View File
@@ -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",
+3 -2
View File
@@ -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"
}
}
Binary file not shown.
+369 -117
View File
@@ -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();