From acdccf7c96d57f61fef9bec400ec5291f5b399dd Mon Sep 17 00:00:00 2001 From: tore Date: Tue, 22 Sep 2026 18:29:29 +0000 Subject: [PATCH] =?UTF-8?q?v3:=20=D0=B2=D1=8B=D0=B3=D1=80=D1=83=D0=B7?= =?UTF-8?q?=D0=BA=D0=B0=20=D1=80=D0=B5=D0=B5=D1=81=D1=82=D1=80=D0=B0=20?= =?UTF-8?q?=D0=B2=20Excel=20(/api/export.xlsx,=20xlsx=200.18.5)=20+=20CSV?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.js | 13 +++--- package-lock.json | 108 +++++++++++++++++++++++++++++++++++++++++++++- package.json | 3 ++ server.js | 33 +++++++++----- 4 files changed, 141 insertions(+), 16 deletions(-) diff --git a/app.js b/app.js index 0f7cdcc..0b2449a 100644 --- a/app.js +++ b/app.js @@ -457,7 +457,8 @@ function renderRegistry() { '' + selStatus() + selSrc() + - '' + + '' + + '' + "" + '
' + "" + @@ -513,12 +514,14 @@ function bindRegistry() { var src = document.querySelector(".reg-src"); if (src) src.addEventListener("change", function () { S.regSrc = this.value; saveState(); renderRegistry(); }); var exp = document.querySelector('#view [data-act="csv"]'); - if (exp) exp.addEventListener("click", exportCsv); + if (exp) exp.addEventListener("click", function () { exportFile("/api/export.csv", "CSV"); }); + var expX = document.querySelector('#view [data-act="xlsx"]'); + if (expX) expX.addEventListener("click", function () { exportFile("/api/export.xlsx", "Excel"); }); } -function exportCsv() { - window.location.href = "/api/export.csv"; - toast("Реестр выгружен в CSV", "ok"); +function exportFile(url, label) { + window.location.href = url; + toast("Реестр выгружен в " + label, "ok"); } // ================= СОГЛАСОВАНИЕ ================= diff --git a/package-lock.json b/package-lock.json index 8628466..7fe0250 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,113 @@ "packages": { "": { "name": "soglasovanie-zakupki", - "version": "1.0.0" + "version": "1.0.0", + "dependencies": { + "xlsx": "^0.18.5" + } + }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } } } } diff --git a/package.json b/package.json index 2b79e7c..2283ad7 100644 --- a/package.json +++ b/package.json @@ -6,5 +6,8 @@ "main": "server.js", "scripts": { "start": "node server.js" + }, + "dependencies": { + "xlsx": "^0.18.5" } } diff --git a/server.js b/server.js index 63aeef4..07a49fd 100644 --- a/server.js +++ b/server.js @@ -1,6 +1,7 @@ const http = require("http"); const fs = require("fs"); const path = require("path"); +const XLSX = require("xlsx"); const PORT = process.env.PORT || 3000; const DATA = path.join(__dirname, "data.json"); @@ -224,20 +225,32 @@ http.createServer(async function (req, res) { return sendJson(res, 200, { ok: true }); } - if (req.method === "GET" && url === "/api/export.csv") { + if (url === "/api/export.csv" || url === "/api/export.xlsx") { var d6 = loadData(); - var head = "Номер;Инициатор;Сумма;Статус;Срок (дней);Поступила;Обработана;Источник;Предмет;Обоснование"; - var rows = d6.requests.map(function (r) { + var aoa = [ + ["Номер", "Инициатор", "Сумма", "Статус", "Срок (дней)", "Поступила", "Обработана", "Источник", "Предмет", "Обоснование"] + ]; + d6.requests.forEach(function (r) { var st = STATUS[r.st] ? STATUS[r.st].label : r.st; var days = daysBetween(r.in, r.done); - return [ - r.id, r.init, r.sum, st, days, - isoToDdMm(r.in), r.done ? isoToDdMm(r.done) : "", - r.src, '"' + (r.subj || "").replace(/"/g, '""') + '"', - '"' + (r.justif || "").replace(/"/g, '""') + '"' - ].join(";"); + aoa.push([r.id, r.init, r.sum, st, days, isoToDdMm(r.in), r.done ? isoToDdMm(r.done) : "", r.src, r.subj || "", r.justif || ""]); }); - var csv = "\uFEFF" + head + "\n" + rows.join("\n"); + if (url === "/api/export.xlsx") { + var ws = XLSX.utils.aoa_to_sheet(aoa); + var wb = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(wb, ws, "Реестр заявок"); + var buf = XLSX.write(wb, { type: "buffer", bookType: "xlsx" }); + res.writeHead(200, { + "Content-Type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "Content-Disposition": 'attachment; filename="reestr-zakupki.xlsx"', + "Content-Length": buf.length + }); + return res.end(buf); + } + // CSV + var csv = "\uFEFF" + aoa.map(function (row) { + return row.map(function (v) { return '"' + String(v == null ? "" : v).replace(/"/g, '""') + '"'; }).join(";"); + }).join("\n"); res.writeHead(200, { "Content-Type": "text/csv; charset=utf-8", "Content-Disposition": 'attachment; filename="reestr-zakupki.csv"',
НомерИнициаторСуммаСтатусСрок, днейПоступилаОбработанаИсточник