mirror of
https://github.com/sergeyyarkov/asu_tools_service.git
synced 2026-10-11 10:29:22 +03:00
excel reports into json api call
This commit is contained in:
commit
dae69f46a5
5
.env.example
Normal file
5
.env.example
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
MSSQL_USER=""
|
||||||
|
MSSQL_PASSWORD=""
|
||||||
|
MSSQL_DB=""
|
||||||
|
MSSQL_HOST="localhost"
|
||||||
|
MSSQL_PORT="1433"
|
||||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
node_modules
|
||||||
|
uploads
|
||||||
|
.env
|
||||||
3
context.js
Normal file
3
context.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { AsyncLocalStorage } from "node:async_hooks";
|
||||||
|
|
||||||
|
export const ctxAsyncLocalStorage = new AsyncLocalStorage();
|
||||||
21
db.js
Normal file
21
db.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import sql from "mssql";
|
||||||
|
|
||||||
|
/** @type {sql.config} */
|
||||||
|
const connectionConfig = {
|
||||||
|
user: process.env.MSSQL_USER,
|
||||||
|
password: process.env.MSSQL_PASSWORD,
|
||||||
|
database: process.env.MSSQL_DB,
|
||||||
|
server: process.env.MSSQL_HOST,
|
||||||
|
port: Number.parseInt(process.env.MSSQL_PORT, 10) || 1433,
|
||||||
|
pool: {
|
||||||
|
max: 10,
|
||||||
|
min: 0,
|
||||||
|
idleTimeoutMillis: 5000,
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
trustServerCertificate: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const pool = new sql.ConnectionPool(connectionConfig);
|
||||||
|
export default pool;
|
||||||
24
index.d.ts
vendored
Normal file
24
index.d.ts
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import type http from "node:http";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
type ReportModel = {
|
||||||
|
date: string;
|
||||||
|
equipment: string;
|
||||||
|
reason_call: string;
|
||||||
|
job_description: string;
|
||||||
|
root_cause: string;
|
||||||
|
applicantName: string;
|
||||||
|
executorNames: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type HTTPResponse = {
|
||||||
|
sendJson: (data: any, statusCode?: number = 200) => http.ServerResponse;
|
||||||
|
sendText: (text: string, statusCode?: number = 200) => http.ServerResponse;
|
||||||
|
res: http.ServerResponse | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type HTTPRoute = { method: "POST" | "GET"; handle: HTTPRouteHandler };
|
||||||
|
type HTTPRouteHandler = (ctx: HTTPContext) => Promise<void>;
|
||||||
|
type HTTPRouteMap = Record<string, HTTPRoute>;
|
||||||
|
type HTTPContext = { data: any; req: http.IncomingMessage; res: HTTPResponse };
|
||||||
|
}
|
||||||
28
index.js
Normal file
28
index.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import http from "node:http";
|
||||||
|
import { routeHandler } from "./router/index.js";
|
||||||
|
import packageJson from "./package.json" with { type: "json" };
|
||||||
|
import { ctxAsyncLocalStorage } from "./context.js";
|
||||||
|
import db from "./db.js";
|
||||||
|
import response from "./response.js";
|
||||||
|
|
||||||
|
const SERVER_PORT = 3001;
|
||||||
|
|
||||||
|
await db.connect().then(() => console.log("Database connected!"));
|
||||||
|
|
||||||
|
http
|
||||||
|
.createServer((req, res) => {
|
||||||
|
try {
|
||||||
|
const url = new URL(`http://localhost:${SERVER_PORT}${req.url}`);
|
||||||
|
|
||||||
|
/** @type {HTTPContext} */
|
||||||
|
const ctx = { data: null, req, res: Object.create(response) };
|
||||||
|
ctx.res.res = res;
|
||||||
|
|
||||||
|
ctxAsyncLocalStorage.run(ctx, () => routeHandler(url));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.listen(3001, () => {
|
||||||
|
console.log(`Service ${packageJson.name} is running at port ${SERVER_PORT}.`);
|
||||||
|
});
|
||||||
12
jsconfig.json
Normal file
12
jsconfig.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"checkJs": true,
|
||||||
|
"lib": ["ESNext"],
|
||||||
|
"module": "nodenext",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"paths": {
|
||||||
|
"#root/*": ["./*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1097
package-lock.json
generated
Normal file
1097
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
package.json
Normal file
22
package.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "asu_tools-sync-service",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node --env-file=.env index.js",
|
||||||
|
"dev": "node --env-file=.env --watch index.js"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "Sergey Yarkov",
|
||||||
|
"license": "ISC",
|
||||||
|
"description": "API сервис инструментов таблицы АСУ.",
|
||||||
|
"dependencies": {
|
||||||
|
"formidable": "^3.5.4",
|
||||||
|
"mssql": "^12.2.0",
|
||||||
|
"xlsx": "^0.18.5"
|
||||||
|
},
|
||||||
|
"imports": {
|
||||||
|
"#root/*": "./*"
|
||||||
|
}
|
||||||
|
}
|
||||||
12
response.js
Normal file
12
response.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
/** @type {HTTPResponse} */
|
||||||
|
export default {
|
||||||
|
res: null,
|
||||||
|
sendJson(data, statusCode = 200) {
|
||||||
|
this.res.writeHead(statusCode, { "content-type": "application/json" });
|
||||||
|
return this.res.end(JSON.stringify(data, null, 2));
|
||||||
|
},
|
||||||
|
sendText(text, statusCode = 200) {
|
||||||
|
this.res.writeHead(statusCode, { "content-type": "text/plain; charset=utf-8" });
|
||||||
|
return this.res.end(text);
|
||||||
|
},
|
||||||
|
};
|
||||||
52
router/index.js
Normal file
52
router/index.js
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { ctxAsyncLocalStorage } from "#root/context.js";
|
||||||
|
import handleExcelReportsFileParse from "./reportsFileParse.handler.js";
|
||||||
|
import handleReportsSyncParsed from "./reportsSyncParsed.handler.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {HTTPRouteMap}
|
||||||
|
*/
|
||||||
|
export const ROUTE_MAP = {
|
||||||
|
"/api/reports_excel_file_parse": {
|
||||||
|
method: "POST",
|
||||||
|
handle: handleExcelReportsFileParse,
|
||||||
|
},
|
||||||
|
"/api/reports_sync": {
|
||||||
|
method: "POST",
|
||||||
|
handle: handleReportsSyncParsed,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {URL} url
|
||||||
|
*/
|
||||||
|
export async function routeHandler(url) {
|
||||||
|
/** @type {HTTPContext} */
|
||||||
|
const ctx = ctxAsyncLocalStorage.getStore();
|
||||||
|
const { req, res } = ctx;
|
||||||
|
|
||||||
|
if (url.pathname in ROUTE_MAP) {
|
||||||
|
if (req.headers["content-type"] === "application/json") {
|
||||||
|
ctx.data = await getJsonData();
|
||||||
|
}
|
||||||
|
|
||||||
|
const route = ROUTE_MAP[url.pathname];
|
||||||
|
if (route.method === req.method) route.handle(ctx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.sendText("Указанный URL не имеет обработчика.", 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getJsonData() {
|
||||||
|
/** @type {HTTPContext} */
|
||||||
|
const { req } = ctxAsyncLocalStorage.getStore();
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let body = [];
|
||||||
|
req.on("data", (chunk) => body.push(chunk));
|
||||||
|
req.on("end", () => resolve(JSON.parse(Buffer.concat(body).toString())));
|
||||||
|
req.on("error", (err) => reject(err));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export * from "./reportsFileParse.handler.js";
|
||||||
112
router/reportsFileParse.handler.js
Normal file
112
router/reportsFileParse.handler.js
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
import xlsx from "xlsx";
|
||||||
|
import formidable from "formidable";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Читает Excel файл отчетов и возвращает его в формате JSON
|
||||||
|
*
|
||||||
|
* @type {HTTPRouteHandler}
|
||||||
|
*/
|
||||||
|
export default async ({ req, res }) => {
|
||||||
|
const sheetName = "Отчеты";
|
||||||
|
|
||||||
|
/** @type {{ markedReports: ReportModel[], reports: ReportModel[] }} */
|
||||||
|
const ret = { reports: [], markedReports: [] };
|
||||||
|
|
||||||
|
/** @type {xlsx.ParsingOptions} */
|
||||||
|
const xlsxParseOptions = {
|
||||||
|
sheets: sheetName,
|
||||||
|
cellHTML: false,
|
||||||
|
cellFormula: false,
|
||||||
|
cellDates: true,
|
||||||
|
dateNF: "yyyy-mm-dd",
|
||||||
|
};
|
||||||
|
|
||||||
|
const form = formidable({
|
||||||
|
maxFields: 1,
|
||||||
|
maxFiles: 1,
|
||||||
|
filename: (_, ext) => `gpp-reports${ext}`,
|
||||||
|
keepExtensions: true,
|
||||||
|
uploadDir: path.join(process.cwd(), "./uploads"),
|
||||||
|
filter: ({ mimetype }) => mimetype && mimetype.includes("application/vnd.ms-excel"),
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [, files] = await form.parse(req);
|
||||||
|
const excelFilePath = files?.report?.at(0)?.filepath;
|
||||||
|
|
||||||
|
if (excelFilePath) {
|
||||||
|
const workbook = xlsx.readFile(excelFilePath, xlsxParseOptions);
|
||||||
|
const sheet = workbook.Sheets[sheetName];
|
||||||
|
|
||||||
|
if (!sheet) {
|
||||||
|
throw new Error("Invalid reports workbook.", { cause: { statusCode: 400 } });
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = xlsx.utils.sheet_to_json(sheet, { raw: false, blankrows: true });
|
||||||
|
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
const cols = Object.keys(data[i]);
|
||||||
|
|
||||||
|
if (i === 0) continue;
|
||||||
|
|
||||||
|
/** Несинхронизированные помеченные отчеты */
|
||||||
|
if ("__EMPTY_3" in data[i] && data[i]["__EMPTY_3"] === "r") {
|
||||||
|
ret.markedReports.push({
|
||||||
|
date: data[i][cols[0]],
|
||||||
|
equipment: data[i][cols[1]],
|
||||||
|
reason_call: data[i][cols[3]].split("\r\r\n")[1] || "",
|
||||||
|
job_description: data[i][cols[4]].split("\r\r\n")[1] || "",
|
||||||
|
root_cause: data[i]["__EMPTY_2"] || "",
|
||||||
|
applicantName: data[i][cols[3]].split("\r\r\n")[0] || "",
|
||||||
|
executorNames: data[i][cols[4]].split("\r\r\n")[0] || "",
|
||||||
|
});
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseReasonCallAndJobDesc = (str = "") => {
|
||||||
|
const splitted = splitLongSpacedSentence(str);
|
||||||
|
if (splitted.length > 1) return splitted.slice(1).join(" ");
|
||||||
|
return str.split("\r\n").slice(1).join(" ");
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseNames = (str = "") => {
|
||||||
|
const splitted = splitLongSpacedSentence(str);
|
||||||
|
if (splitted.length > 1) return splitted[0];
|
||||||
|
return str.split("\r\n")[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Остальные отчеты */
|
||||||
|
ret.reports.push({
|
||||||
|
date: data[i][cols[0]],
|
||||||
|
equipment: data[i][cols[1]],
|
||||||
|
reason_call: parseReasonCallAndJobDesc(data[i][cols[3]]),
|
||||||
|
job_description: parseReasonCallAndJobDesc(data[i][cols[4]]),
|
||||||
|
root_cause: data[i]["__EMPTY_2"] || "",
|
||||||
|
applicantName: parseNames(data[i][cols[3]]),
|
||||||
|
executorNames: parseNames(data[i][cols[4]]),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error("Bad request", { cause: { statusCode: 400 } });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.sendJson(
|
||||||
|
{ error: error?.stack || error?.message || "Server error" },
|
||||||
|
error?.cause?.statusCode || 500,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.sendJson({ data: ret });
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} str
|
||||||
|
* @param {number} min
|
||||||
|
*/
|
||||||
|
function splitLongSpacedSentence(str, min = 5) {
|
||||||
|
return str.split(new RegExp(`\\s{${min},}`));
|
||||||
|
}
|
||||||
4
router/reportsSyncParsed.handler.js
Normal file
4
router/reportsSyncParsed.handler.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
/** @type {HTTPRouteHandler} */
|
||||||
|
export default async ({ data, res }) => {
|
||||||
|
res.sendJson({ data });
|
||||||
|
};
|
||||||
Loading…
x
Reference in New Issue
Block a user