asu_tools_service/handlers/reports/parseExcel.handler.js

217 lines
7.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { BadRequestError, ServiceUnavailableError } from "#root/http-server/index.js";
import db from "#root/db.js";
import xlsx from "xlsx";
import pLimit from "p-limit";
import { clients } from "#root/index.js";
import { serializeError } from "serialize-error";
const sheetName = "Отчеты";
const limit = pLimit(10);
/** @type {xlsx.ParsingOptions} */
const xlsxParseOptions = {
sheets: sheetName,
cellHTML: false,
cellFormula: false,
cellDates: true,
dateNF: "yyyy-mm-dd"
};
/**
* URL: /api/reports_excel_file_parse
* Method: POST
* Description: Принимает на вход файл отчетов в формате Excel, парсит его, проверяет связи с БД,
* отдает идентификатор на статус парсинга.
*
* @type {import("#root/http-server/types/http-server.js").HttpRouteHandler}
*/
export default async (ctx) => {
const { res, params } = ctx;
const clientId = params.get("clientId");
if (!ctx.data) throw new BadRequestError("Cannot proceed uploaded file");
if (!clientId) throw new BadRequestError('"clientId" parameter is required');
const [, files] = ctx.data;
const excelFilePath = files?.report?.at(0)?.filepath;
const sseClientRes = clients.get(clientId);
if (!sseClientRes) throw new ServiceUnavailableError("Cannot proceed request due to unknown client");
let isSseClientClosed = false;
sseClientRes.res.on("close", () => (isSseClientClosed = true));
/** @type {ReportModel[]} */
const reports = [];
let parseResultCount = { equipments: 0, applicants: 0, executors: 0, reportsFullfilled: 0 };
// res.res.setHeaders(
// new Headers({
// Connection: "keep-alive",
// "Cache-Control": "no-cache",
// "Content-Type": "text/event-stream"
// })
// );
// ctx.res.res.on("close", () => (isReqClosed = true));
if (excelFilePath) {
const workbook = xlsx.readFile(excelFilePath, xlsxParseOptions);
const sheet = workbook.Sheets[sheetName];
console.log();
if (!sheet) throw new Error("Invalid reports workbook.", { cause: { statusCode: 400 } });
res.sendJson({ message: "OK" });
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") {
reports.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] || "",
isMarked: true
});
continue;
}
/** Остальные отчеты */
reports.push({
date: data[i][cols[0]],
equipment: data[i][cols[1]],
reason_call: parseReasonCallAndJobDesc(data[i][cols[3]]).trim(),
job_description: parseReasonCallAndJobDesc(data[i][cols[4]]).trim(),
root_cause: data[i]["__EMPTY_2"] || "",
applicantName: parseNames(data[i][cols[3]]).trim(),
executorNames: parseNames(data[i][cols[4]]).trim()
});
}
const requests = reports.map((report) =>
limit(() => {
return checkReportAssigments(
report,
(checkedReport) => {
const isEquipmentLinked = checkedReport.equipment instanceof Object;
const isApplicantLinked = checkedReport.applicant instanceof Object;
const isExecutorsLinked = checkedReport.executors && checkedReport.executors.length !== 0;
if (isEquipmentLinked) parseResultCount.equipments++;
if (isApplicantLinked) parseResultCount.applicants++;
if (isExecutorsLinked) parseResultCount.executors += checkedReport.executors?.length || 0;
if (isEquipmentLinked && isApplicantLinked && isExecutorsLinked) parseResultCount.reportsFullfilled++;
sseClientRes.sendSSEJson(checkedReport, "progress");
},
isSseClientClosed
);
})
);
await Promise.all(requests)
.then(() => {
console.log(parseResultCount);
sseClientRes.sendSSEJson(parseResultCount, "done");
sseClientRes.res.end();
})
.catch((error) => {
console.error(error);
sseClientRes.sendSSEJson({ error: serializeError(error) }, "error");
sseClientRes.res.end();
})
.finally(() => res.res.end());
} else {
throw new BadRequestError();
}
};
/**
* @param {ReportModel} report
* @param {(report: ReportModel) => void} callback
* @param {boolean} rejectCond
*/
async function checkReportAssigments(report, callback, rejectCond) {
if (rejectCond) return Promise.reject(`Report check is rejected`);
const executorSurnames = extractSurnamesFromExecutorField(report.executorNames);
const equipment = await db
.request()
.query(`select id, name from dbo.asu_system_api_subsystemlist where name like N'%${report.equipment}%';`);
const applicant = await db
.request()
.query(
`select id, name from dbo.gpp_report_api_applicant where name like N'${report.applicantName.split(" ")[0]}%';`
);
/** @type {ExecutorModel[]} */
const executors = [];
for (const eSurname of executorSurnames) {
if (eSurname) {
const executor = await db
.request()
.query(
`select id, surname + ' ' + name + ' ' + patronymic as fullname from user_user where surname like N'%${eSurname}%';`
);
if (executor.recordset.length !== 0) {
executors.push({ id: executor.recordset[0].id, fullname: executor.recordset[0].fullname });
}
}
}
const hasEquipmentLink = equipment.recordset.length !== 0;
const hasApplicantLink = applicant.recordset.length !== 0;
callback({
...report,
executors,
equipment: hasEquipmentLink ? { id: equipment.recordset[0].id, name: equipment.recordset[0].name } : null,
applicant: hasApplicantLink ? { id: applicant.recordset[0].id, name: applicant.recordset[0].name } : null
});
}
function parseReasonCallAndJobDesc(str = "") {
const splitted = splitLongSpacedSentence(str);
if (splitted.length > 1) return splitted.slice(1).join(" ");
return str.split("\r\n").slice(1).join(" ");
}
function parseNames(str = "") {
const splitted = splitLongSpacedSentence(str);
if (splitted.length > 1) return splitted[0];
return str.split("\r\n")[0];
}
/** @param {string} str */
function extractSurnamesFromExecutorField(str) {
return str
.replaceAll(",", "")
.split(/(?:.\..\.)|(?:.\..\s)|(?:.\.\s.\.)|(?:.\..)/g)
.map((s) => s.trim());
}
/**
* @param {string} str
* @param {number} min
*/
function splitLongSpacedSentence(str, min = 5) {
return str.split(new RegExp(`\\s{${min},}`));
}