From 7955b224b0565f47c259edba8cb6d7a5cdffac04 Mon Sep 17 00:00:00 2001 From: sergeyyarkov Date: Fri, 27 Feb 2026 15:38:02 +0300 Subject: [PATCH] error handling --- router/index.js | 32 +++-- router/reports/parseExcel.handler.js | 115 ++++++++++++++++++ .../syncParsed.handler.js} | 0 router/reportsFileParse.handler.js | 112 ----------------- 4 files changed, 136 insertions(+), 123 deletions(-) create mode 100644 router/reports/parseExcel.handler.js rename router/{reportsSyncParsed.handler.js => reports/syncParsed.handler.js} (100%) delete mode 100644 router/reportsFileParse.handler.js diff --git a/router/index.js b/router/index.js index 42a61ee..1d9a0db 100644 --- a/router/index.js +++ b/router/index.js @@ -1,6 +1,6 @@ import { ctxAsyncLocalStorage } from "#root/context.js"; -import handleExcelReportsFileParse from "./reportsFileParse.handler.js"; -import handleReportsSyncParsed from "./reportsSyncParsed.handler.js"; +import handleExcelReportsFileParse from "./reports/parseExcel.handler.js"; +import handleReportsSyncParsed from "./reports/syncParsed.handler.js"; /** * @type {HTTPRouteMap} @@ -24,17 +24,27 @@ export async function routeHandler(url) { const ctx = ctxAsyncLocalStorage.getStore(); const { req, res } = ctx; - if (url.pathname in ROUTE_MAP) { - if (req.headers["content-type"] === "application/json") { - ctx.data = await getJsonData(); + try { + if (url.pathname in ROUTE_MAP) { + const route = ROUTE_MAP[url.pathname]; + if (req.headers["content-type"] === "application/json") ctx.data = await getJsonData(); + if (route.method === req.method) await route.handle(ctx); + return; } - const route = ROUTE_MAP[url.pathname]; - if (route.method === req.method) route.handle(ctx); - return; + res.sendText("Указанный URL не имеет обработчика.", 200); + } catch (error) { + console.error(error); + res.sendJson( + { + error: { + message: error?.message || "Internal Server Error", + stack: error?.stack || null, + }, + }, + error?.cause?.statusCode || 500, + ); } - - res.sendText("Указанный URL не имеет обработчика.", 200); } function getJsonData() { @@ -49,4 +59,4 @@ function getJsonData() { }); } -export * from "./reportsFileParse.handler.js"; +export * from "./reports/parseExcel.handler.js"; diff --git a/router/reports/parseExcel.handler.js b/router/reports/parseExcel.handler.js new file mode 100644 index 0000000..cf0e5fb --- /dev/null +++ b/router/reports/parseExcel.handler.js @@ -0,0 +1,115 @@ +import path from "node:path"; +import xlsx from "xlsx"; +import formidable from "formidable"; +import db from "#root/db.js"; + +/** + * Читает Excel файл отчетов и возвращает его в формате JSON + * + * @type {HTTPRouteHandler} + */ +export default async ({ req, res }) => { + const sheetName = "Отчеты"; + + /** @type {{ markedReports: ReportModel[], reports: ReportModel[] }} */ + const ret = { reports: [], markedReports: [] }; + + /** @type {xlsx.ParsingOptions} */ + const xlsxParseOptions = { + sheets: sheetName, + cellHTML: false, + cellFormula: false, + cellDates: true, + dateNF: "yyyy-mm-dd", + }; + + const form = formidable({ + maxFields: 1, + maxFiles: 1, + filename: (_, ext) => `gpp-reports${ext}`, + keepExtensions: true, + uploadDir: path.join(process.cwd(), "./uploads"), + filter: ({ mimetype }) => mimetype && mimetype.includes("application/vnd.ms-excel"), + }); + + const [, files] = await form.parse(req); + const excelFilePath = files?.report?.at(0)?.filepath; + + if (excelFilePath) { + const workbook = xlsx.readFile(excelFilePath, xlsxParseOptions); + const sheet = workbook.Sheets[sheetName]; + + if (!sheet) { + throw new Error("Invalid reports workbook.", { cause: { statusCode: 400 } }); + } + + 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") { + ret.markedReports.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] || "", + }); + + continue; + } + + const parseReasonCallAndJobDesc = (str = "") => { + const splitted = splitLongSpacedSentence(str); + if (splitted.length > 1) return splitted.slice(1).join(" "); + return str.split("\r\n").slice(1).join(" "); + }; + + const parseNames = (str = "") => { + const splitted = splitLongSpacedSentence(str); + if (splitted.length > 1) return splitted[0]; + return str.split("\r\n")[0]; + }; + + /** Остальные отчеты */ + ret.reports.push({ + date: data[i][cols[0]], + equipment: data[i][cols[1]], + reason_call: parseReasonCallAndJobDesc(data[i][cols[3]]), + job_description: parseReasonCallAndJobDesc(data[i][cols[4]]), + root_cause: data[i]["__EMPTY_2"] || "", + applicantName: parseNames(data[i][cols[3]]), + executorNames: parseNames(data[i][cols[4]]), + }); + } + } else { + throw new Error("Bad request", { cause: { statusCode: 400 } }); + } + + res.sendJson({ data: ret }); +}; + +// /** +// * @param {string} filepath +// */ +// function reportsExcelFileToJson(filepath) { +// try { + +// } catch (error) { + +// } +// } + +/** + * @param {string} str + * @param {number} min + */ +function splitLongSpacedSentence(str, min = 5) { + return str.split(new RegExp(`\\s{${min},}`)); +} diff --git a/router/reportsSyncParsed.handler.js b/router/reports/syncParsed.handler.js similarity index 100% rename from router/reportsSyncParsed.handler.js rename to router/reports/syncParsed.handler.js diff --git a/router/reportsFileParse.handler.js b/router/reportsFileParse.handler.js deleted file mode 100644 index dc59e65..0000000 --- a/router/reportsFileParse.handler.js +++ /dev/null @@ -1,112 +0,0 @@ -import path from "node:path"; -import xlsx from "xlsx"; -import formidable from "formidable"; - -/** - * Читает Excel файл отчетов и возвращает его в формате JSON - * - * @type {HTTPRouteHandler} - */ -export default async ({ req, res }) => { - const sheetName = "Отчеты"; - - /** @type {{ markedReports: ReportModel[], reports: ReportModel[] }} */ - const ret = { reports: [], markedReports: [] }; - - /** @type {xlsx.ParsingOptions} */ - const xlsxParseOptions = { - sheets: sheetName, - cellHTML: false, - cellFormula: false, - cellDates: true, - dateNF: "yyyy-mm-dd", - }; - - const form = formidable({ - maxFields: 1, - maxFiles: 1, - filename: (_, ext) => `gpp-reports${ext}`, - keepExtensions: true, - uploadDir: path.join(process.cwd(), "./uploads"), - filter: ({ mimetype }) => mimetype && mimetype.includes("application/vnd.ms-excel"), - }); - - try { - const [, files] = await form.parse(req); - const excelFilePath = files?.report?.at(0)?.filepath; - - if (excelFilePath) { - const workbook = xlsx.readFile(excelFilePath, xlsxParseOptions); - const sheet = workbook.Sheets[sheetName]; - - if (!sheet) { - throw new Error("Invalid reports workbook.", { cause: { statusCode: 400 } }); - } - - 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") { - ret.markedReports.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] || "", - }); - - continue; - } - - const parseReasonCallAndJobDesc = (str = "") => { - const splitted = splitLongSpacedSentence(str); - if (splitted.length > 1) return splitted.slice(1).join(" "); - return str.split("\r\n").slice(1).join(" "); - }; - - const parseNames = (str = "") => { - const splitted = splitLongSpacedSentence(str); - if (splitted.length > 1) return splitted[0]; - return str.split("\r\n")[0]; - }; - - /** Остальные отчеты */ - ret.reports.push({ - date: data[i][cols[0]], - equipment: data[i][cols[1]], - reason_call: parseReasonCallAndJobDesc(data[i][cols[3]]), - job_description: parseReasonCallAndJobDesc(data[i][cols[4]]), - root_cause: data[i]["__EMPTY_2"] || "", - applicantName: parseNames(data[i][cols[3]]), - executorNames: parseNames(data[i][cols[4]]), - }); - } - } else { - throw new Error("Bad request", { cause: { statusCode: 400 } }); - } - } catch (error) { - console.error(error); - res.sendJson( - { error: error?.stack || error?.message || "Server error" }, - error?.cause?.statusCode || 500, - ); - return; - } - - res.sendJson({ data: ret }); -}; - -/** - * @param {string} str - * @param {number} min - */ -function splitLongSpacedSentence(str, min = 5) { - return str.split(new RegExp(`\\s{${min},}`)); -}