diff --git a/handlers/asu_table/getAsuTable.handler.js b/handlers/asu_table/getAsuTable.handler.js index 17085e8..4087c74 100644 --- a/handlers/asu_table/getAsuTable.handler.js +++ b/handlers/asu_table/getAsuTable.handler.js @@ -1,5 +1,4 @@ -import { equipmentRepository } from "#root/repositories/equipment.repository.js"; -import ExcelJS from "exceljs"; +import { dbExporterService, XlsExporterAdapter } from "#root/services/dbExporter.service.js"; /** @import { HttpRouteHandler } from "#root/http-server/types/http-server.js" */ @@ -12,193 +11,14 @@ import ExcelJS from "exceljs"; */ export default async (ctx) => { const { res, searchParams } = ctx; - 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 equipmentData = await equipmentRepository.getAll({ filter: { systemIds: systemIdsParam } }); - - const excelData = []; - - /** @type {Map { - 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(); + const exportResult = await dbExporterService.export(XlsExporterAdapter, { filter: { systemIds: systemIdsParam } }); 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-Length", excelBuffer.byteLength); - res.res.end(excelBuffer); + res.res.setHeader("Content-Length", exportResult.byteLength); + res.res.end(exportResult); return; }; diff --git a/models/index.js b/models/index.js index a900aec..289ae52 100644 --- a/models/index.js +++ b/models/index.js @@ -1 +1,2 @@ export * from "./report.model.js"; +export * from "./system.model.js"; diff --git a/models/report.model.js b/models/report.model.js index f646363..205eea3 100644 --- a/models/report.model.js +++ b/models/report.model.js @@ -1,6 +1,6 @@ import { BaseModel } from "./base.model.js"; -/** @import { ReportColumns } from "#tables/report.table.js" */ +/** @import { ReportColumns } from "./types.js" */ export class ReportModel extends BaseModel { /** diff --git a/models/system.model.js b/models/system.model.js new file mode 100644 index 0000000..d33e1e0 --- /dev/null +++ b/models/system.model.js @@ -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; + } +} diff --git a/models/types.d.ts b/models/types.d.ts new file mode 100644 index 0000000..3a5edcd --- /dev/null +++ b/models/types.d.ts @@ -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; +}; diff --git a/repositories/equipment.repository.js b/repositories/equipment.repository.js index 11da19e..c1b41a1 100644 --- a/repositories/equipment.repository.js +++ b/repositories/equipment.repository.js @@ -1,9 +1,23 @@ import { UOW } from "./index.js"; 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, + * fgs?: Array, hmis: Array, + * pcs?: Array + * }} SubSystemDbRecord; + * + * @typedef {{ id: string, name: string, baseLocationName: string, subsystems: Array }} SystemDbRecord; + */ + export const equipmentRepository = { /** * @param {{ filter?: { systemIds?: string[] | number[] } }} [options] + * @returns {Promise>} */ async getAll(options) { const request = UOW.request; diff --git a/services/dbExporter.service.js b/services/dbExporter.service.js new file mode 100644 index 0000000..f85fc95 --- /dev/null +++ b/services/dbExporter.service.js @@ -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"; diff --git a/services/exporters/excel.exporter.js b/services/exporters/excel.exporter.js new file mode 100644 index 0000000..b2e32f2 --- /dev/null +++ b/services/exporters/excel.exporter.js @@ -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} */ + static mainWsOptions = { views: [{ state: "frozen", ySplit: 1, zoomScale: 70 }] }; + + constructor() { + this.wb = new ExcelJS.Workbook(); + this.ws = this.wb.addWorksheet("Перечень систем", XlsExporterAdapter.mainWsOptions); + + /** @type {Array} */ + this.data = []; + } + + /** @param {SystemDbRecord[]} db */ + async export(db) { + /** @type {Map} */ + 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() {} +} diff --git a/services/exporters/index.js b/services/exporters/index.js new file mode 100644 index 0000000..d8bd05d --- /dev/null +++ b/services/exporters/index.js @@ -0,0 +1 @@ +export * from "./excel.exporter.js"; diff --git a/services/index.js b/services/index.js index 3d61005..df40465 100644 --- a/services/index.js +++ b/services/index.js @@ -1 +1,2 @@ export * from "./reports.service.js"; +export * from "./dbExporter.service.js"; diff --git a/services/types.d.ts b/services/types.d.ts new file mode 100644 index 0000000..7ce61aa --- /dev/null +++ b/services/types.d.ts @@ -0,0 +1,5 @@ +import { SystemDbRecord } from "#root/repositories/equipment.repository.js"; + +export interface ExporterAdapterInterface { + export: (database: Array) => Promise; +} diff --git a/tables/report.table.js b/tables/report.table.js index fb62fbf..9539dfc 100644 --- a/tables/report.table.js +++ b/tables/report.table.js @@ -1,10 +1,6 @@ import sql from "mssql"; 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 { static tableName = "gpp_report_api_report"; static table = new sql.Table(this.tableName); diff --git a/tables/reportExecutor.table.js b/tables/reportExecutor.table.js index d5c325d..511f57b 100644 --- a/tables/reportExecutor.table.js +++ b/tables/reportExecutor.table.js @@ -1,10 +1,6 @@ import sql from "mssql"; import { BaseTable } from "./base.table.js"; -/** - * @typedef {{ id: string | number; report_id: string | number; user_id: string | number; }} ReportExecutorTableCols - */ - export class ReportExecutorTable extends BaseTable { static tableName = "gpp_report_api_report_executor"; static table = new sql.Table(this.tableName); diff --git a/tables/system.table.js b/tables/system.table.js new file mode 100644 index 0000000..a9b5888 --- /dev/null +++ b/tables/system.table.js @@ -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();