This commit is contained in:
Sergey Yarkov 2026-03-12 16:28:17 +03:00
parent 9342a9cf5e
commit c0e1e2f655
16 changed files with 221 additions and 40 deletions

View File

@ -45,7 +45,7 @@ export default async (ctx) => {
throw new ServiceUnavailableError("Your request on parsing is processing");
}
/** @param {ReportModel} report */
/** @param {ParsedReport} report */
const onParseProgress = async (report) => sseClientCtx.res.sendSSEJson(report, "progress");
const cancelAnalyzeCond = () => Boolean(sseClientCtx.local?.isClosed);

View File

@ -14,6 +14,6 @@ export default async ({ res, data }) => {
throw new BadRequestError(err.message);
});
await reportsService.syncReportsWithDatabase(reports);
res.sendJson({ data: { message: "OK" } });
const rows = await reportsService.syncReportsWithDatabase(reports);
res.sendJson({ data: { rows } });
};

View File

@ -11,7 +11,11 @@
"#root/*": ["./*"],
"#handlers/*": ["./handlers/*"],
"#services/*": ["./services/*"],
"#schemas/*": ["./schemas/*"]
"#schemas/*": ["./schemas/*"],
"#repositories/*": ["./repositories/*"],
"#tables/*": ["./tables/*"],
"#models/*": ["./models/*"],
"#utils/*": ["./utils/*"]
}
},
"exclude": ["./http-server/types/*.d.ts"]

2
models.d.ts vendored
View File

@ -1,4 +1,4 @@
type ReportModel = {
type ParsedReport = {
date: string;
equipment?:
| string

1
models/index.js Normal file
View File

@ -0,0 +1 @@
export * from "./report.model.js";

50
models/report.model.js Normal file
View File

@ -0,0 +1,50 @@
/** @import { ReportColumns } from "#tables/report.table.js" */
export class ReportModel {
/**
* @type {ReportColumns['id'] | undefined}
*/
id;
/**
* @type {ReportColumns['date']}
*/
date;
/**
* @type {ReportColumns['reason_call']}
*/
reason_call;
/**
* @type {ReportColumns['job_description']}
*/
job_description;
/**
* @type {ReportColumns['root_cause']}
*/
root_cause;
/**
* @type {ReportColumns['applicant_id']}
*/
applicant_id;
/**
* @type {ReportColumns['equipment_id']}
*/
equipment_id;
/**
* @param {Omit<ReportColumns, 'id'>} columns
*/
constructor(columns) {
this.date = columns.date;
this.reason_call = columns.reason_call;
this.job_description = columns.job_description;
this.root_cause = columns.root_cause;
this.applicant_id = columns.applicant_id;
this.equipment_id = columns.equipment_id;
}
}

View File

@ -25,7 +25,11 @@
"#root/*": "./*",
"#handlers/*": "./handlers/*",
"#services/*": "./services/*",
"#schemas/*": "./schemas/*"
"#schemas/*": "./schemas/*",
"#repositories/*": "./repositories/*",
"#tables/*": "./tables/*",
"#models/*": "./models/*",
"#utils/*": "./utils/*"
},
"devDependencies": {
"@eslint/js": "^10.0.1",

1
repositories/index.js Normal file
View File

@ -0,0 +1 @@
export * from "./report.repository.js";

View File

@ -0,0 +1,61 @@
import sql from "mssql";
import db from "#root/db.js";
import { ReportModel } from "#models/index.js";
import { ReportTable } from "#tables/index.js";
/** @import { ReportColumns } from "#tables/report.table.js" */
export const reportRepository = {
async getAll() {
const request = new sql.Request(db);
console.log(ReportTable.tableName);
const reports = await request.query(`select * from dbo.${ReportTable.tableName};`);
console.log(reports);
return reports.recordset;
},
/**
* @param {Omit<ReportColumns, 'id'>} entry
*/
async create(entry) {
const report = new ReportModel(entry);
const request = new sql.Request(db);
request.input("date", ReportTable.getColumn("date"), report.date);
request.input("reasonCall", ReportTable.getColumn("reason_call"), report.reason_call);
request.input("jobDesc", ReportTable.getColumn("job_description"), report.job_description);
request.input("rootCause", ReportTable.getColumn("root_cause"), report.root_cause);
request.input("applicantId", ReportTable.getColumn("applicant_id"), report.applicant_id);
request.input("eqpId", ReportTable.getColumn("equipment_id"), report.equipment_id);
const createdReport = await request.query(
`insert into ${ReportTable.table.name}
(date, reason_call, job_description, root_cause, applicant_id, equipment_id)
output inserted.id
values (@date, @reasonCall, @jobDesc, @rootCause, @applicantId, @eqpId);
`
);
report.id = Number.parseInt(createdReport.recordset[0].id, 10);
return report;
},
/**
* @param {Omit<ReportColumns, 'id'>[]} entries
* @param {sql.Transaction} [trx]
*/
async bulk(entries, trx) {
const request = trx ? new sql.Request(trx) : new sql.Request(db);
const table = ReportTable.createInstance();
for (const r of entries) {
table.rows.add(r.date, r.reason_call, r.job_description, r.root_cause, r.applicant_id, r.equipment_id);
}
await request.bulk(table);
return table.rows.length;
}
};

View File

@ -23,4 +23,4 @@ export const reportItemSchema = y.object({
executors: y.array(y.object({ id: y.number(), fullname: y.string() })).required()
});
export const reportsSchema = y.object({ reports: y.array(reportItemSchema).required() }).required();
export const reportsSchema = y.object({ reports: y.array(reportItemSchema).min(1).required() }).required();

View File

@ -2,6 +2,8 @@ import xlsx from "xlsx";
import pLimit from "p-limit";
import db from "#root/db.js";
import mssql from "mssql";
import { reportRepository } from "#repositories/index.js";
// import * as utilsDate from "#utils/date.js";
/** @import { reportsSchema } from "#schemas/report.schema.js" */
/** @import { InferType } from "yup" */
@ -22,13 +24,13 @@ export const reportsService = {
* Читает файл Excel и возвращает данные о репорте в массиве
*
* @param {string} filepath
* @returns {ReportModel[]}
* @returns {ParsedReport[]}
*/
parseExcelFile(filepath) {
const workbook = xlsx.readFile(filepath, xlsxParseOptions);
const sheet = workbook.Sheets[sheetName];
/** @type {ReportModel[]} */
/** @type {ParsedReport[]} */
const reports = [];
if (!sheet) throw new Error("Invalid reports workbook.");
@ -77,9 +79,9 @@ export const reportsService = {
* Проверяет отчет на наличие связей с БД АСУ и возвращает
* этот отчет с устанновленными связями или без них
*
* @param {ReportModel} report
* @param {ParsedReport} report
* @param {() => boolean} cancelCondFn
* @returns {Promise<ReportModel>}
* @returns {Promise<ParsedReport>}
*/
async checkReportLinks(report, cancelCondFn) {
const executorSurnames = extractSurnamesFromExecutorField(report.executorNames);
@ -141,9 +143,9 @@ export const reportsService = {
/**
* Проверяет связи отчетов с БД и возвращает результат
*
* @param {ReportModel[]} reports
* @param {ParsedReport[]} reports
* @param {() => boolean} cancelCondFn
* @param {(report: ReportModel) => Promise<void> | void} [progressCb]
* @param {(report: ParsedReport) => Promise<void> | void} [progressCb]
*/
analyzeParseResult(reports, cancelCondFn, progressCb) {
const limit = pLimit(10);
@ -174,38 +176,29 @@ export const reportsService = {
*/
async syncReportsWithDatabase(reports) {
const trx = new mssql.Transaction(db);
const table = new mssql.Table("dbo.gpp_report_api_report");
const dbReports = await reportRepository.getAll();
table.create = false;
table.columns.add("date", mssql.Date, { nullable: false });
table.columns.add("reason_call", mssql.NVarChar(mssql.MAX), { nullable: false });
table.columns.add("job_description", mssql.NVarChar(mssql.MAX), { nullable: false });
table.columns.add("root_cause", mssql.NVarChar(mssql.MAX), { nullable: true });
table.columns.add("applicant_id", mssql.BigInt, { nullable: true });
table.columns.add("equipment_id", mssql.BigInt, { nullable: true });
/**
* 1. проверить на существование дубликатов и отфильтровать
* 2. начать транзакцию на множественную вставку отчетов
* 3. установить связь добавленных отчетов с исполнителями и завершить транзакцию
*/
try {
const addReportsRequest = new mssql.Request(trx);
for (const r of reports) {
let date = new Date(r.date || "2026");
if (!(date instanceof Date && !Number.isNaN(date.valueOf()))) date = new Date("2026");
table.rows.add(
date,
r.reason_call || " ",
r.job_description || " ",
r.root_cause || " ",
r.applicant?.id || null,
r.equipment?.id || null
);
}
await trx.begin();
await addReportsRequest.bulk(table);
const countRows = await reportRepository.bulk(
reports.map((r) => ({
applicant_id: r.applicant?.id || null,
equipment_id: r.equipment?.id || null,
date: new Date("2026"),
job_description: r.job_description || "",
reason_call: r.reason_call || "",
root_cause: r.root_cause || ""
})),
trx
);
await trx.commit();
return countRows;
} catch (error) {
console.error(error);
await trx.rollback();

35
tables/base.table.js Normal file
View File

@ -0,0 +1,35 @@
import sql from "mssql";
export class BaseTable {
static tableName = "";
static table = new sql.Table(this.tableName);
/** @type {Record<string, sql.ISqlType & sql.IColumnOptions>} */
static columns = {};
/**
* @template {typeof BaseTable} T
* @this {T}
* @param {keyof T['columns']} name
*/
static getColumn(name) {
const col = this.table.columns.find((col) => col.name === name);
if (!col) throw new Error(`Unknown column with name "${String(name)}"`);
return col;
}
/**
* @param {sql.Table} [table]
*/
static init(table) {
table = table || this.table;
// TODO: fix types
Object.entries(this.columns).forEach(([name, { type, ...opts }]) => table.columns.add(name, type, opts));
}
static createInstance() {
const table = new sql.Table(this.tableName);
this.init(table);
return table;
}
}

1
tables/index.js Normal file
View File

@ -0,0 +1 @@
export * from "./report.table.js";

23
tables/report.table.js Normal file
View File

@ -0,0 +1,23 @@
import sql from "mssql";
import { BaseTable } from "./base.table.js";
/**
* @typedef {{ id: number; date: Date; reason_call: string; job_description: string; root_cause: string | null; applicant_id: number | null; equipment_id: number | null; }} ReportColumns
*/
export class ReportTable extends BaseTable {
static tableName = "gpp_report_api_report";
static table = new sql.Table(this.tableName);
static columns = {
date: { type: sql.Date, nullable: false },
reason_call: { type: sql.NVarChar(sql.MAX), nullable: false },
job_description: { type: sql.NVarChar(sql.MAX), nullable: false },
root_cause: { type: sql.NVarChar(sql.MAX), nullable: true },
applicant_id: { type: sql.BigInt, nullable: true },
equipment_id: { type: sql.BigInt, nullable: true }
};
}
ReportTable.table.create = false;
ReportTable.init();

7
utils/date.js Normal file
View File

@ -0,0 +1,7 @@
/**
* @param {unknown} date
* @returns {boolean is Date}
*/
export function isDateValid(date) {
return date instanceof Date && !Number.isNaN(date.valueOf());
}

1
utils/index.js Normal file
View File

@ -0,0 +1 @@
export * from "./date.js";