This commit is contained in:
Sergey Yarkov 2026-08-26 10:08:25 +03:00
parent 9cff9624a8
commit 53a8cab4fb
14 changed files with 299 additions and 193 deletions

View File

@ -1,5 +1,4 @@
import { equipmentRepository } from "#root/repositories/equipment.repository.js"; import { dbExporterService, XlsExporterAdapter } from "#root/services/dbExporter.service.js";
import ExcelJS from "exceljs";
/** @import { HttpRouteHandler } from "#root/http-server/types/http-server.js" */ /** @import { HttpRouteHandler } from "#root/http-server/types/http-server.js" */
@ -12,193 +11,14 @@ import ExcelJS from "exceljs";
*/ */
export default async (ctx) => { export default async (ctx) => {
const { res, searchParams } = ctx; const { res, searchParams } = ctx;
const systemIdsParam = searchParams.get("systemIds")?.split(","); const systemIdsParam = searchParams.get("systemIds")?.split(",");
const wb = new ExcelJS.Workbook();
const ws = wb.addWorksheet("Перечень систем", {
views: [{ state: "frozen", ySplit: 1, zoomScale: 70 }]
});
const filename = encodeURIComponent("АСУ промплощадка.xlsx"); const filename = encodeURIComponent("АСУ промплощадка.xlsx");
const equipmentData = await equipmentRepository.getAll({ filter: { systemIds: systemIdsParam } }); const exportResult = await dbExporterService.export(XlsExporterAdapter, { filter: { systemIds: systemIdsParam } });
const excelData = [];
/** @type {Map<string, { row: number }} */
const addedBaseLocationNames = new Map();
/** @type {{ colStart: string, colEnd: string, from: number; to: number }[]} */
const mergeRanges = [];
let currentRow = 2;
for (const system of equipmentData) {
const subsystems = system.subsystems || [];
if (subsystems.length === 0) {
excelData.push([system.name, ...new Array(8).fill("", 0, 8)]);
currentRow++;
continue;
}
// Заголовок для расположения систем
if (!addedBaseLocationNames.has(system.baseLocationName)) {
addedBaseLocationNames.set(system.baseLocationName, { row: currentRow });
excelData.push([system.baseLocationName]);
mergeRanges.push({ colStart: "A", colEnd: "I", from: currentRow, to: currentRow });
currentRow++;
}
const startRow = currentRow;
// Добавление систем подсистем в таблицу excel
subsystems.forEach((subsystem) => {
const hasDocLink = Boolean(subsystem?.docLink);
const hasElectroPlan = Boolean(subsystem?.electrDiagLink);
const plcs = subsystem?.plcs || [];
const hmis = subsystem?.hmis || [];
const fgs = subsystem?.fgs || [];
const pcs = subsystem?.pcs || [];
excelData.push([
system.name,
subsystem.name,
`${system.baseLocationName} ${subsystem.locationName}`,
hasDocLink ? { text: "Документация", hyperlink: subsystem?.docLink } : "",
hasElectroPlan ? { text: "Электроплан", hyperlink: subsystem?.electrDiagLink } : "",
plcs.map((plc) => `${plc.nameModel || ""}`).join("\n"),
hmis.map((hmi) => `${hmi.nameModel || ""}`).join("\n"),
fgs.map((fg) => `${fg.nameModel || ""}`).join("\n"),
pcs.map((pc) => `${pc.name || ""}`).join("\n")
]);
currentRow++;
});
mergeRanges.push({ colStart: "A", colEnd: "A", from: startRow, to: currentRow - 1 });
}
ws.getRow(1).values = [
"Наименование системы",
"Наименование внутренних подсистем.",
"Расположение",
"Документация",
"Электроплан",
"PLC",
"HMI",
"ПЧ",
"Промышленные ПК"
];
ws.autoFilter = `A1:I${excelData.length + 1}`;
ws.addRows(excelData);
mergeRanges.forEach((r) => ws.mergeCells(`${r.colStart}${r.from}:${r.colEnd}${r.to}`));
ws.getColumn(1).width = 50;
ws.getColumn(2).width = 50;
ws.getColumn(3).width = 35;
ws.getColumn(4).width = 17;
ws.getColumn(5).width = 17;
ws.getColumn(6).width = 45;
ws.getColumn(7).width = 45;
ws.getColumn(8).width = 45;
ws.getColumn(9).width = 30;
ws.getRow(1).height = 45;
ws.eachRow((row, rowNumber) => {
row.eachCell({ includeEmpty: true }, (cell) => {
cell.alignment = {
wrapText: true,
vertical: "middle",
horizontal: "center"
};
if (rowNumber > 1) {
cell.font = {
name: "Calibri",
size: 12
};
cell.border = {
top: { style: "thin", color: { argb: "FF000000" } },
left: { style: "thin", color: { argb: "FF000000" } },
right: { style: "thin", color: { argb: "FF000000" } },
bottom: { style: "thin", color: { argb: "FF000000" } }
};
cell.fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FFF4F4F4" }
};
if (cell.value === null || cell.value === undefined || cell.value === "") {
cell.fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FFFCE4D6" }
};
}
// Ссылки
if (cell.value && typeof cell.value === "object" && "hyperlink" in cell.value) {
cell.font = {
name: "Calibri",
size: 11,
color: { argb: "FF0563C1" },
underline: "single"
};
}
} else {
cell.font = {
name: "Calibri",
size: 11,
color: { argb: "FFFFFFFF" }
};
cell.border = {
top: { style: "thin", color: { argb: "FF000000" } },
right: { style: "thin", color: { argb: "FF000000" } },
bottom: { style: "thin", color: { argb: "FF000000" } }
};
cell.fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FFF27A2B" }
};
}
});
});
addedBaseLocationNames.forEach((baseLocationHeaderRow) => {
const row = ws.getRow(baseLocationHeaderRow.row);
row.height = 30;
row.eachCell((cell) => {
cell.font = {
name: "Tahoma",
bold: true,
size: 11
};
cell.border = {
top: { style: "medium", color: { argb: "FF000000" } },
bottom: { style: "medium", color: { argb: "FF000000" } }
};
cell.fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FF92D050" }
};
});
});
const excelBuffer = await wb.xlsx.writeBuffer();
res.res.setHeader("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); res.res.setHeader("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
res.res.setHeader("Content-Disposition", `attachment; filename=${filename}; filename*=UTF-8''${filename}`); res.res.setHeader("Content-Disposition", `attachment; filename=${filename}; filename*=UTF-8''${filename}`);
res.res.setHeader("Content-Length", excelBuffer.byteLength); res.res.setHeader("Content-Length", exportResult.byteLength);
res.res.end(excelBuffer); res.res.end(exportResult);
return; return;
}; };

View File

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

View File

@ -1,6 +1,6 @@
import { BaseModel } from "./base.model.js"; import { BaseModel } from "./base.model.js";
/** @import { ReportColumns } from "#tables/report.table.js" */ /** @import { ReportColumns } from "./types.js" */
export class ReportModel extends BaseModel { export class ReportModel extends BaseModel {
/** /**

20
models/system.model.js Normal file
View File

@ -0,0 +1,20 @@
import { BaseModel } from "./base.model.js";
/** @import { SystemModelTableCols } from "./types.js" */
/**
* @implements { SystemModelTableCols }
*/
export class SystemModel extends BaseModel {
id;
location_id;
name;
/** @param {SystemModelTableCols} columns */
constructor(columns) {
super();
this.id = columns.id;
this.location_id = columns.location_id;
this.name = columns.name;
}
}

17
models/types.d.ts vendored Normal file
View File

@ -0,0 +1,17 @@
export type ReportColumns = {
id?: number | string;
date: Date;
reason_call: string;
job_description: string;
root_cause: string | null;
applicant_id: number | string | null;
equipment_id: number | string | null;
};
export type ReportExecutorTableCols = { id: string | number; report_id: string | number; user_id: string | number };
export type SystemModelTableCols = {
id: string | number;
name: string | null;
location_id: string | number | null;
};

View File

@ -1,9 +1,23 @@
import { UOW } from "./index.js"; import { UOW } from "./index.js";
import sql from "mssql"; import sql from "mssql";
/**
* @typedef {{ name: string, nameModel?: string | null; }} EquipmentDbRecord;
*
* @typedef {{ id: string, name?: string,
* docLink?: string | null, electrDiagLink?: string | null,
* locationName?: string | null, plcs?: Array<EquipmentDbRecord>,
* fgs?: Array<EquipmentDbRecord>, hmis: Array<EquipmentDbRecord>,
* pcs?: Array<EquipmentDbRecord>
* }} SubSystemDbRecord;
*
* @typedef {{ id: string, name: string, baseLocationName: string, subsystems: Array<SubSystemDbRecord> }} SystemDbRecord;
*/
export const equipmentRepository = { export const equipmentRepository = {
/** /**
* @param {{ filter?: { systemIds?: string[] | number[] } }} [options] * @param {{ filter?: { systemIds?: string[] | number[] } }} [options]
* @returns {Promise<Array<SystemDbRecord>>}
*/ */
async getAll(options) { async getAll(options) {
const request = UOW.request; const request = UOW.request;

View File

@ -0,0 +1,20 @@
import { equipmentRepository } from "#root/repositories/equipment.repository.js";
/** @import { ExporterAdapterInterface } from './types.js' */
export const dbExporterService = {
/**
* @param { new () => ExporterAdapterInterface } Adapter
* @param {{ filter?: { systemIds?: number[] | string[] } }} options
* */
async export(Adapter, options) {
const db = await equipmentRepository.getAll({ filter: options.filter });
const AdapterInstance = new Adapter();
/** @type {Buffer} */
const data = await AdapterInstance.export(db);
return data;
}
};
export * from "./exporters/index.js";

View File

@ -0,0 +1,199 @@
import ExcelJS from "exceljs";
/**
* @import { ExporterAdapterInterface } from '../types.js'
* @import { SystemDbRecord } from '#repositories/equipment.repository.js'
*/
/** @implements {ExporterAdapterInterface} */
export class XlsExporterAdapter {
/** @type {Partial<ExcelJS.AddWorksheetOptions>} */
static mainWsOptions = { views: [{ state: "frozen", ySplit: 1, zoomScale: 70 }] };
constructor() {
this.wb = new ExcelJS.Workbook();
this.ws = this.wb.addWorksheet("Перечень систем", XlsExporterAdapter.mainWsOptions);
/** @type {Array<any[]>} */
this.data = [];
}
/** @param {SystemDbRecord[]} db */
async export(db) {
/** @type {Map<string, { row: number }>} */
const addedBaseLocationNames = new Map();
/** @type {{ colStart: string, colEnd: string, from: number; to: number }[]} */
const mergeRanges = [];
let currentRow = 2;
for (const system of db) {
const subsystems = system.subsystems || [];
// Подсистема не найдены, добавляет название системы с пустыми ячейками
if (subsystems.length === 0) {
this.data.push([system.name, ...new Array(8).fill("", 0, 8)]);
currentRow++;
continue;
}
// Заголовок для расположения систем
if (!addedBaseLocationNames.has(system.baseLocationName)) {
addedBaseLocationNames.set(system.baseLocationName, { row: currentRow });
this.data.push([system.baseLocationName]);
mergeRanges.push({ colStart: "A", colEnd: "I", from: currentRow, to: currentRow });
currentRow++;
}
const startRow = currentRow;
// Добавление систем и подсистем в таблицу Excel
subsystems.forEach((subsystem) => {
const hasDocLink = Boolean(subsystem?.docLink);
const hasElectroPlan = Boolean(subsystem?.electrDiagLink);
const plcs = subsystem?.plcs || [];
const hmis = subsystem?.hmis || [];
const fgs = subsystem?.fgs || [];
const pcs = subsystem?.pcs || [];
this.data.push([
system.name,
subsystem.name,
`${system.baseLocationName} ${subsystem.locationName}`,
hasDocLink ? { text: "Документация", hyperlink: subsystem?.docLink } : "",
hasElectroPlan ? { text: "Электроплан", hyperlink: subsystem?.electrDiagLink } : "",
plcs.map((plc) => `${plc.nameModel || ""}`).join("\n"),
hmis.map((hmi) => `${hmi.nameModel || ""}`).join("\n"),
fgs.map((fg) => `${fg.nameModel || ""}`).join("\n"),
pcs.map((pc) => `${pc.name || ""}`).join("\n")
]);
currentRow++;
});
mergeRanges.push({ colStart: "A", colEnd: "A", from: startRow, to: currentRow - 1 });
}
this.ws.getRow(1).values = [
"Наименование системы",
"Наименование внутренних подсистем.",
"Расположение",
"Документация",
"Электроплан",
"PLC",
"HMI",
"ПЧ",
"Промышленные ПК"
];
this.ws.autoFilter = `A1:I${this.data.length + 1}`;
this.ws.addRows(this.data);
mergeRanges.forEach((r) => this.ws.mergeCells(`${r.colStart}${r.from}:${r.colEnd}${r.to}`));
this.ws.getColumn(1).width = 50;
this.ws.getColumn(2).width = 50;
this.ws.getColumn(3).width = 35;
this.ws.getColumn(4).width = 17;
this.ws.getColumn(5).width = 17;
this.ws.getColumn(6).width = 45;
this.ws.getColumn(7).width = 45;
this.ws.getColumn(8).width = 45;
this.ws.getColumn(9).width = 30;
this.ws.getRow(1).height = 45;
this.ws.eachRow((row, rowNumber) => {
row.eachCell({ includeEmpty: true }, (cell) => {
cell.alignment = {
wrapText: true,
vertical: "middle",
horizontal: "center"
};
if (rowNumber > 1) {
cell.font = {
name: "Calibri",
size: 12
};
cell.border = {
top: { style: "thin", color: { argb: "FF000000" } },
left: { style: "thin", color: { argb: "FF000000" } },
right: { style: "thin", color: { argb: "FF000000" } },
bottom: { style: "thin", color: { argb: "FF000000" } }
};
cell.fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FFF4F4F4" }
};
if (cell.value === null || cell.value === undefined || cell.value === "") {
cell.fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FFFCE4D6" }
};
}
// Ссылки
if (cell.value && typeof cell.value === "object" && "hyperlink" in cell.value) {
cell.font = {
name: "Calibri",
size: 11,
color: { argb: "FF0563C1" },
underline: "single"
};
}
} else {
cell.font = {
name: "Calibri",
size: 11,
color: { argb: "FFFFFFFF" }
};
cell.border = {
top: { style: "thin", color: { argb: "FF000000" } },
right: { style: "thin", color: { argb: "FF000000" } },
bottom: { style: "thin", color: { argb: "FF000000" } }
};
cell.fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FFF27A2B" }
};
}
});
});
addedBaseLocationNames.forEach((baseLocationHeaderRow) => {
const row = this.ws.getRow(baseLocationHeaderRow.row);
row.height = 30;
row.eachCell((cell) => {
cell.font = {
name: "Tahoma",
bold: true,
size: 11
};
cell.border = {
top: { style: "medium", color: { argb: "FF000000" } },
bottom: { style: "medium", color: { argb: "FF000000" } }
};
cell.fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FF92D050" }
};
});
});
const excelBuffer = await this.wb.xlsx.writeBuffer();
/** @type {Buffer} */
return excelBuffer;
}
setupStyles() {}
}

View File

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

View File

@ -1 +1,2 @@
export * from "./reports.service.js"; export * from "./reports.service.js";
export * from "./dbExporter.service.js";

5
services/types.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
import { SystemDbRecord } from "#root/repositories/equipment.repository.js";
export interface ExporterAdapterInterface {
export: (database: Array<SystemDbRecord>) => Promise<any>;
}

View File

@ -1,10 +1,6 @@
import sql from "mssql"; import sql from "mssql";
import { BaseTable } from "./base.table.js"; import { BaseTable } from "./base.table.js";
/**
* @typedef {{ id?: number | string; date: Date; reason_call: string; job_description: string; root_cause: string | null; applicant_id: number | string | null; equipment_id: number | string | null; }} ReportColumns
*/
export class ReportTable extends BaseTable { export class ReportTable extends BaseTable {
static tableName = "gpp_report_api_report"; static tableName = "gpp_report_api_report";
static table = new sql.Table(this.tableName); static table = new sql.Table(this.tableName);

View File

@ -1,10 +1,6 @@
import sql from "mssql"; import sql from "mssql";
import { BaseTable } from "./base.table.js"; import { BaseTable } from "./base.table.js";
/**
* @typedef {{ id: string | number; report_id: string | number; user_id: string | number; }} ReportExecutorTableCols
*/
export class ReportExecutorTable extends BaseTable { export class ReportExecutorTable extends BaseTable {
static tableName = "gpp_report_api_report_executor"; static tableName = "gpp_report_api_report_executor";
static table = new sql.Table(this.tableName); static table = new sql.Table(this.tableName);

16
tables/system.table.js Normal file
View File

@ -0,0 +1,16 @@
import sql from "mssql";
import { BaseTable } from "./base.table.js";
export class SystemTable extends BaseTable {
static tableName = "asu_system_api_systemlist";
static table = new sql.Table(this.tableName);
static columns = {
id: { type: sql.BigInt, nullable: false },
location_id: { type: sql.BigInt, nullable: true },
name: { type: sql.VarChar, nullable: true }
};
}
SystemTable.table.create = false;
SystemTable.init();