mirror of
https://github.com/sergeyyarkov/asu_tools_service.git
synced 2026-10-11 02:19:09 +03:00
262 lines
8.8 KiB
JavaScript
262 lines
8.8 KiB
JavaScript
import xlsx from "xlsx";
|
||
import pLimit from "p-limit";
|
||
import db from "#root/db.js";
|
||
import { reportRepository, UOW } from "#repositories/index.js";
|
||
import * as utilsDate from "#utils/date.js";
|
||
|
||
/** @import { reportsSchema } from "#schemas/report.schema.js" */
|
||
/** @import { InferType } from "yup" */
|
||
|
||
const sheetName = "Отчеты";
|
||
|
||
/** @type {xlsx.ParsingOptions} */
|
||
const xlsxParseOptions = {
|
||
sheets: sheetName,
|
||
cellHTML: false,
|
||
cellFormula: false,
|
||
cellDates: true,
|
||
dateNF: "yyyy-mm-dd"
|
||
};
|
||
|
||
export const reportsService = {
|
||
/**
|
||
* Читает файл Excel и возвращает данные о репорте в массиве
|
||
*
|
||
* @param {string} filepath
|
||
* @returns {ParsedReport[]}
|
||
*/
|
||
parseExcelFile(filepath) {
|
||
const workbook = xlsx.readFile(filepath, xlsxParseOptions);
|
||
const sheet = workbook.Sheets[sheetName];
|
||
|
||
/** @type {ParsedReport[]} */
|
||
const reports = [];
|
||
|
||
if (!sheet) throw new Error("Invalid reports workbook.");
|
||
|
||
const data = xlsx.utils.sheet_to_json(sheet, { raw: false, blankrows: true, header: "A" });
|
||
|
||
for (let i = 0; i < data.length; i++) {
|
||
const cols = Object.keys(data[i]);
|
||
|
||
if (i === 0) continue;
|
||
|
||
/** Несинхронизированные помеченные отчеты */
|
||
if ("G" in data[i] && data[i]["G"] === "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]["F"] || "",
|
||
applicantName: data[i][cols[3]].split("\r\r\n")[0] || "",
|
||
executorNames: data[i][cols[4]].split("\r\r\n")[0] || "",
|
||
rowNum: data[i].__rowNum__ + 1,
|
||
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]["F"] || "",
|
||
applicantName: parseNames(data[i][cols[3]]).trim(),
|
||
executorNames: parseNames(data[i][cols[4]]).trim(),
|
||
rowNum: data[i].__rowNum__ + 1
|
||
});
|
||
}
|
||
|
||
return reports;
|
||
},
|
||
|
||
/**
|
||
* Проверяет отчет на наличие связей с БД АСУ и возвращает
|
||
* этот отчет с устанновленными связями или без них
|
||
*
|
||
* @param {ParsedReport} report
|
||
* @param {() => boolean} cancelCondFn
|
||
* @returns {Promise<ParsedReport>}
|
||
*/
|
||
async checkReportLinks(report, cancelCondFn) {
|
||
const executorSurnames = extractSurnamesFromExecutorField(report.executorNames);
|
||
if (cancelCondFn()) return Promise.reject("Report check rejected");
|
||
|
||
const equipment = await db.request()
|
||
.query(`select t1.id, t1.name, t1.location_id, t3.name as base_location_name, t2.name as location_name, t2.base_location_id
|
||
from dbo.asu_system_api_subsystemlist as t1
|
||
left join dbo.asu_system_api_location as t2 on t2.id = t1.location_id
|
||
left join dbo.asu_system_api_baselocation as t3 on t3.id = t2.base_location_id
|
||
where t1.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;
|
||
|
||
const location =
|
||
hasEquipmentLink && equipment.recordset[0].location_id
|
||
? {
|
||
id: equipment.recordset[0].location_id,
|
||
name: equipment.recordset[0].location_name,
|
||
base_location_name: equipment.recordset[0].base_location_name,
|
||
base_location: equipment.recordset[0].base_location_id
|
||
}
|
||
: null;
|
||
|
||
return {
|
||
...report,
|
||
executors,
|
||
equipment: hasEquipmentLink
|
||
? { id: equipment.recordset[0].id, name: equipment.recordset[0].name, location }
|
||
: null,
|
||
applicant: hasApplicantLink ? { id: applicant.recordset[0].id, name: applicant.recordset[0].name } : null
|
||
};
|
||
},
|
||
|
||
/**
|
||
* Проверяет связи отчетов с БД и возвращает результат
|
||
*
|
||
* @param {ParsedReport[]} reports
|
||
* @param {() => boolean} cancelCondFn
|
||
* @param {(report: ParsedReport) => Promise<void> | void} [progressCb]
|
||
*/
|
||
analyzeParseResult(reports, cancelCondFn, progressCb) {
|
||
const limit = pLimit(10);
|
||
const result = { reportsFullfilled: 0 };
|
||
|
||
return {
|
||
result,
|
||
reports: reports.map((report) =>
|
||
limit(async () => {
|
||
const checkedReport = await this.checkReportLinks(report, cancelCondFn);
|
||
const isEquipmentLinked = checkedReport.equipment instanceof Object;
|
||
const isApplicantLinked = checkedReport.applicant instanceof Object;
|
||
const isExecutorsLinked = checkedReport.executors && checkedReport.executors.length !== 0;
|
||
|
||
if (isEquipmentLinked && isApplicantLinked && isExecutorsLinked) {
|
||
result.reportsFullfilled++;
|
||
}
|
||
|
||
progressCb && (await progressCb(checkedReport));
|
||
return checkedReport;
|
||
})
|
||
)
|
||
};
|
||
},
|
||
|
||
/**
|
||
* @param {InferType<typeof reportsSchema>} payload
|
||
*/
|
||
async syncReportsWithDatabase(payload) {
|
||
const INVALID_DATE_FALLBACK = new Date("2016-10-24"); // дата для постановки вместо неверной. (колонка `date` в таблице имеет ограничение not null)
|
||
const { reports: inReports, deleteAllBeforeImport } = payload;
|
||
|
||
/** @type {typeof payload['reports']} */
|
||
let unImportedReports = [];
|
||
|
||
if (!deleteAllBeforeImport) {
|
||
const reports = await reportRepository.getAll();
|
||
|
||
/** Отфильтровывание дубликатов по полям. */
|
||
unImportedReports = inReports.filter((inputR) => {
|
||
const isDub = reports.some((dbR) => {
|
||
return (
|
||
inputR.reason_call === dbR.reason_call &&
|
||
inputR.job_description === dbR.job_description &&
|
||
inputR.root_cause === dbR.root_cause &&
|
||
inputR.equipment?.id == dbR.equipment_id &&
|
||
inputR.applicant?.id == dbR.applicant_id
|
||
);
|
||
});
|
||
return !isDub;
|
||
});
|
||
|
||
if (unImportedReports.length === 0) return 0;
|
||
} else {
|
||
await reportRepository.delAll();
|
||
unImportedReports = inReports;
|
||
}
|
||
|
||
/** Добавление отчетов и связей с исполнителями к ним */
|
||
const ret = await UOW.run(async () => {
|
||
const bulkResult = await reportRepository.bulk(
|
||
unImportedReports.map((r) => {
|
||
const reportDate = new Date(r.date || "");
|
||
return {
|
||
applicant_id: r.applicant?.id || null,
|
||
equipment_id: r.equipment?.id || null,
|
||
date: utilsDate.isDateValid(reportDate) ? reportDate : INVALID_DATE_FALLBACK,
|
||
job_description: r.job_description || "",
|
||
reason_call: r.reason_call || "",
|
||
root_cause: r.root_cause || ""
|
||
};
|
||
})
|
||
);
|
||
|
||
const executorsLinks = unImportedReports.map((r, i) => {
|
||
return Object.fromEntries([[`${bulkResult.addedRowIds[i]}`, r.executors.map((e) => e.id)]]);
|
||
});
|
||
|
||
await reportRepository.attachExecutors(executorsLinks);
|
||
|
||
return bulkResult.rowsAffected;
|
||
});
|
||
|
||
return ret;
|
||
}
|
||
};
|
||
|
||
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},}`));
|
||
}
|