v4: авто-рассылка КП + DOCX
This commit is contained in:
parent
9b31f446bd
commit
ee88cd9bad
154
index.html
154
index.html
@ -19,12 +19,13 @@
|
||||
.form-group label { display: block; margin-bottom: 6px; font-weight: 500; font-size: 14px; color: #6e6e73; }
|
||||
.form-group input, .form-group select, .form-group textarea { width: 100%; padding: 12px; border: 1px solid #d2d2d7; border-radius: 10px; font-size: 15px; }
|
||||
.form-group textarea { min-height: 100px; resize: vertical; }
|
||||
.btn { padding: 12px 24px; border: none; border-radius: 10px; cursor: pointer; font-size: 14px; font-weight: 600; transition: all 0.2s; }
|
||||
.btn { padding: 12px 24px; border: none; border-radius: 10px; cursor: pointer; font-size: 14px; font-weight: 600; transition: all 0.2s; margin-right: 8px; margin-bottom: 8px; }
|
||||
.btn-primary { background: #007AFF; color: white; }
|
||||
.btn-primary:hover { background: #0056b3; }
|
||||
.btn-success { background: #34C759; color: white; }
|
||||
.btn-danger { background: #FF3B30; color: white; padding: 8px 16px; font-size: 13px; }
|
||||
.btn-secondary { background: #8e8e93; color: white; }
|
||||
.btn-outline { background: white; border: 2px solid #007AFF; color: #007AFF; }
|
||||
table { width: 100%; border-collapse: collapse; margin-top: 16px; font-size: 14px; }
|
||||
th, td { padding: 12px; text-align: left; border-bottom: 1px solid #e5e5ea; }
|
||||
th { background: #f5f5f7; font-weight: 600; font-size: 13px; color: #6e6e73; text-transform: uppercase; }
|
||||
@ -46,10 +47,15 @@
|
||||
.file-item a { color: #007AFF; text-decoration: none; font-weight: 500; }
|
||||
.file-item a:hover { text-decoration: underline; }
|
||||
.file-meta { font-size: 12px; color: #86868b; margin-left: 12px; }
|
||||
.upload-area { border: 2px dashed #d2d2d7; border-radius: 12px; padding: 24px; text-align: center; background: #fafafa; cursor: pointer; transition: all 0.2s; }
|
||||
.upload-area { border: 2px dashed #d2d2d7; border-radius: 12px; padding: 24px; text-align: center; background: #fafafa; cursor: pointer; transition: all 0.2s; margin-bottom: 16px; }
|
||||
.upload-area:hover { border-color: #007AFF; background: #f0f8ff; }
|
||||
.upload-area.dragover { border-color: #34C759; background: #e8f5e9; }
|
||||
.upload-icon { font-size: 48px; margin-bottom: 12px; }
|
||||
.email-log { margin-top: 16px; max-height: 300px; overflow-y: auto; }
|
||||
.email-item { padding: 12px; background: #f0f8ff; border-left: 4px solid #007AFF; margin-bottom: 8px; border-radius: 4px; }
|
||||
.email-item h4 { margin-bottom: 4px; font-size: 14px; }
|
||||
.email-item p { font-size: 13px; color: #6e6e73; margin: 4px 0; }
|
||||
.status-badge { display: inline-block; padding: 4px 8px; background: #34C759; color: white; border-radius: 4px; font-size: 11px; }
|
||||
@media print {
|
||||
.btn, .no-print, .upload-area { display: none !important; }
|
||||
.card { box-shadow: none; border: 1px solid #ddd; }
|
||||
@ -65,7 +71,8 @@
|
||||
<div class="container">
|
||||
<div class="no-print" style="text-align: right; margin-bottom: 20px;">
|
||||
<button class="btn btn-primary" onclick="window.print()">🖨 Печать / PDF</button>
|
||||
<button class="btn btn-success" onclick="exportToExcel()">📤 Выгрузить в Excel</button>
|
||||
<button class="btn btn-success" onclick="generateDocx()">📄 Сформировать DOCX</button>
|
||||
<button class="btn btn-outline" onclick="sendEmails()">✉️ Отправить запросы КП</button>
|
||||
</div>
|
||||
|
||||
<!-- Раздел 1: Входные данные -->
|
||||
@ -110,6 +117,10 @@
|
||||
<!-- Раздел 2: Список рассылки -->
|
||||
<div class="card">
|
||||
<h2 class="section-title"><span class="section-num">2</span>Список рассылки и поставщики</h2>
|
||||
<div class="no-print" style="margin-bottom: 16px; padding: 16px; background: #fff3cd; border-radius: 10px;">
|
||||
<strong>✉️ Авто-рассылка:</strong> укажите email в контактах поставщика (например: info@company.kz) → нажмите «Отправить запросы КП» → письма сформируются для всех поставщиков.
|
||||
</div>
|
||||
|
||||
<form id="supplierForm" style="margin-bottom: 20px;">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
@ -135,8 +146,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Контактные данные (веб-сайт, телефон, адрес, email)</label>
|
||||
<input type="text" name="contacts" placeholder="+7 XXX XXX-XX-XX, example.kz, info@example.kz">
|
||||
<label>Контактные данные (веб-сайт, телефон, адрес, <strong>email</strong>)</label>
|
||||
<input type="text" name="contacts" placeholder="info@company.kz, +7 XXX XXX-XX-XX, example.kz">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
@ -287,15 +298,23 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Журнал рассылки -->
|
||||
<div class="card no-print">
|
||||
<h2 class="section-title">✉️ Журнал отправки запросов</h2>
|
||||
<div id="emailLog" class="email-log">
|
||||
<p style="color: #86868b;">Запросы ещё не отправлялись</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Все файлы -->
|
||||
<div class="card">
|
||||
<h2 class="section-title">📎 Все прикреплённые файлы</h2>
|
||||
<h2 class="section-title">📎 Все файлы</h2>
|
||||
<div class="file-list" id="allFilesList"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let data = { inputData: null, suppliers: [], offers: [], checks: { c1: false, c2: false, c3: false }, files: [] };
|
||||
let data = { inputData: null, suppliers: [], offers: [], checks: { c1: false, c2: false, c3: false }, files: [], emails: [] };
|
||||
|
||||
async function loadData() {
|
||||
const r = await fetch('/api/data');
|
||||
@ -309,6 +328,7 @@
|
||||
document.getElementById('check2').checked = data.checks.c2;
|
||||
document.getElementById('check3').checked = data.checks.c3;
|
||||
renderFiles();
|
||||
renderEmails();
|
||||
generateConclusion();
|
||||
}
|
||||
|
||||
@ -382,6 +402,7 @@
|
||||
const inputFiles = data.files.filter(f => f.section === 'Входные данные');
|
||||
const supplierFiles = data.files.filter(f => f.section === 'Поставщики');
|
||||
const offerFiles = data.files.filter(f => f.section === 'Предложения');
|
||||
const outputFiles = data.files.filter(f => f.section === 'Выходные документы');
|
||||
|
||||
function renderFileList(files, container) {
|
||||
if (files.length === 0) { container.innerHTML = ''; return; }
|
||||
@ -397,7 +418,23 @@
|
||||
renderFileList(inputFiles, section1);
|
||||
renderFileList(supplierFiles, section2);
|
||||
renderFileList(offerFiles, section3);
|
||||
renderFileList(data.files, allFiles);
|
||||
renderFileList(outputFiles, allFiles);
|
||||
}
|
||||
|
||||
function renderEmails() {
|
||||
const log = document.getElementById('emailLog');
|
||||
if (!data.emails || data.emails.length === 0) {
|
||||
log.innerHTML = '<p style="color: #86868b;">Запросы ещё не отправлялись</p>';
|
||||
return;
|
||||
}
|
||||
log.innerHTML = data.emails.slice().reverse().map(e =>
|
||||
'<div class="email-item">' +
|
||||
'<h4>✉️ ' + e.supplierName + ' <span class="status-badge">' + e.status + '</span></h4>' +
|
||||
'<p><strong>Кому:</strong> ' + e.to + '</p>' +
|
||||
'<p><strong>Тема:</strong> ' + e.subject + '</p>' +
|
||||
'<p><strong>Дата:</strong> ' + new Date(e.date).toLocaleString() + '</p>' +
|
||||
'</div>'
|
||||
).join('');
|
||||
}
|
||||
|
||||
async function handleFiles(files, section) {
|
||||
@ -421,6 +458,62 @@
|
||||
loadData();
|
||||
}
|
||||
|
||||
async function generateDocx() {
|
||||
const btn = event.target;
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Формирование...';
|
||||
|
||||
try {
|
||||
const r = await fetch('/api/generate-docx', { method: 'POST' });
|
||||
const result = await r.json();
|
||||
if (result.ok) {
|
||||
window.location.href = result.file.path;
|
||||
loadData();
|
||||
} else {
|
||||
alert('Ошибка: ' + result.error);
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Ошибка формирования: ' + e.message);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = '📄 Сформировать DOCX';
|
||||
}
|
||||
}
|
||||
|
||||
async function sendEmails() {
|
||||
if (data.suppliers.length === 0) {
|
||||
alert('Сначала добавьте поставщиков в раздел 2');
|
||||
return;
|
||||
}
|
||||
|
||||
const deadline = prompt('Укажите срок ответа (например: до 25.09.2026):', 'до ' + new Date(Date.now() + 7*24*60*60*1000).toLocaleDateString('ru-RU'));
|
||||
if (!deadline) return;
|
||||
|
||||
const btn = event.target;
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Отправка...';
|
||||
|
||||
try {
|
||||
const r = await fetch('/api/send-emails', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ deadline })
|
||||
});
|
||||
const result = await r.json();
|
||||
if (result.ok) {
|
||||
alert('Запросы сформированы и отправлены ' + result.count + ' поставщикам!\n\n(В реальной системе письма уйдут на email из контактов. Сейчас — симуляция с записью в журнал.)');
|
||||
loadData();
|
||||
} else {
|
||||
alert('Ошибка: ' + result.error);
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Ошибка отправки: ' + e.message);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = '✉️ Отправить запросы КП';
|
||||
}
|
||||
}
|
||||
|
||||
function generateConclusion(min, avg) {
|
||||
const minOffer = data.offers.find(o => Number(o.price) === min);
|
||||
const s = minOffer ? data.suppliers.find(x => x.id === minOffer.supplierId) : null;
|
||||
@ -511,51 +604,6 @@
|
||||
alert('Проверка сохранена');
|
||||
}
|
||||
|
||||
function exportToExcel() {
|
||||
const head = ["Раздел", "Параметр", "Значение"];
|
||||
const rows = [];
|
||||
|
||||
if (data.inputData) {
|
||||
rows.push(["1. Входные данные", "Код ЕНС ТРУ", data.inputData.ensCode || '']);
|
||||
rows.push(["1. Входные данные", "Объем", data.inputData.volume || '']);
|
||||
rows.push(["1. Входные данные", "Место поставки", data.inputData.deliveryPlace || '']);
|
||||
rows.push(["1. Входные данные", "Техническая спецификация", data.inputData.techSpec || '']);
|
||||
}
|
||||
|
||||
data.suppliers.forEach((s, i) => {
|
||||
rows.push(["2. Поставщики", "Компания " + (i+1), s.name]);
|
||||
rows.push(["2. Поставщики", "Резидентство", s.resident]);
|
||||
rows.push(["2. Поставщики", "Статус", s.status]);
|
||||
rows.push(["2. Поставщики", "Контакты", s.contacts || '']);
|
||||
});
|
||||
|
||||
data.offers.forEach((o, i) => {
|
||||
const s = data.suppliers.find(x => x.id === o.supplierId);
|
||||
rows.push(["3. Предложения", "Предложение " + (i+1) + " - " + (s ? s.name : ''), o.price + ' тенге']);
|
||||
});
|
||||
|
||||
rows.push(["4. Проверка ст.31", "Пункт 1", data.checks.c1 ? 'Подтверждено' : 'Не проверено']);
|
||||
rows.push(["4. Проверка ст.31", "Пункт 2", data.checks.c2 ? 'Подтверждено' : 'Не проверено']);
|
||||
rows.push(["4. Проверка ст.31", "Пункт 3", data.checks.c3 ? 'Подтверждено' : 'Не проверено']);
|
||||
|
||||
const prices = data.offers.map(o => Number(o.price));
|
||||
const min = prices.length ? Math.min(...prices) : 0;
|
||||
const avg = prices.length ? Math.round(prices.reduce((a,b) => a+b, 0) / prices.length) : 0;
|
||||
rows.push(["5. Анализ", "Минимальная цена", min.toLocaleString() + ' тенге']);
|
||||
rows.push(["5. Анализ", "Средняя цена", avg.toLocaleString() + ' тенге']);
|
||||
rows.push(["5. Анализ", "Количество предложений", data.offers.length]);
|
||||
|
||||
data.files.forEach(f => {
|
||||
rows.push(["Файлы", f.name, f.section]);
|
||||
});
|
||||
|
||||
const csv = '\uFEFF' + [head.join(';'), ...rows.map(r => r.map(v => '"' + String(v).replace(/"/g, '""') + '"').join(';'))].join('\n');
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(new Blob([csv], { type: 'text/csv' }));
|
||||
a.download = 'MAC_' + (data.inputData?.ensCode || 'otchet') + '.csv';
|
||||
a.click();
|
||||
}
|
||||
|
||||
const uploadAreas = document.querySelectorAll('.upload-area');
|
||||
uploadAreas.forEach(area => {
|
||||
area.addEventListener('dragover', (e) => {
|
||||
|
||||
@ -2,5 +2,9 @@
|
||||
"name": "price-analysis",
|
||||
"private": true,
|
||||
"type": "commonjs",
|
||||
"scripts": { "start": "node server.js" }
|
||||
"scripts": { "start": "node server.js" },
|
||||
"dependencies": {
|
||||
"docx": "^8.5.0",
|
||||
"formidable": "^3.5.0"
|
||||
}
|
||||
}
|
||||
|
||||
220
server.js
220
server.js
@ -2,15 +2,18 @@ const http = require("http");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const formidable = require("formidable");
|
||||
const docx = require("docx");
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const DATA = path.join(__dirname, "data.json");
|
||||
const UPLOADS = path.join(__dirname, "uploads");
|
||||
const OUTPUTS = path.join(__dirname, "outputs");
|
||||
|
||||
if (!fs.existsSync(UPLOADS)) fs.mkdirSync(UPLOADS);
|
||||
if (!fs.existsSync(OUTPUTS)) fs.mkdirSync(OUTPUTS);
|
||||
|
||||
function readData() {
|
||||
try { return JSON.parse(fs.readFileSync(DATA, "utf8")); } catch (_) { return { inputData: null, suppliers: [], offers: [], checks: { c1: false, c2: false, c3: false }, files: [] }; }
|
||||
try { return JSON.parse(fs.readFileSync(DATA, "utf8")); } catch (_) { return { inputData: null, suppliers: [], offers: [], checks: { c1: false, c2: false, c3: false }, files: [], emails: [] }; }
|
||||
}
|
||||
function writeData(data) {
|
||||
fs.writeFileSync(DATA, JSON.stringify(data, null, 2));
|
||||
@ -23,7 +26,141 @@ function body(req) {
|
||||
});
|
||||
}
|
||||
|
||||
function generateDocx(data) {
|
||||
const d = data;
|
||||
const prices = d.offers.map(o => Number(o.price));
|
||||
const min = prices.length ? Math.min(...prices) : 0;
|
||||
const avg = prices.length ? Math.round(prices.reduce((a,b) => a+b, 0) / prices.length) : 0;
|
||||
const minOffer = d.offers.find(o => Number(o.price) === min);
|
||||
const minSupplier = minOffer ? d.suppliers.find(x => x.id === minOffer.supplierId) : null;
|
||||
|
||||
const doc = new docx.Document({
|
||||
sections: [{
|
||||
properties: {},
|
||||
children: [
|
||||
new docx.Paragraph({
|
||||
text: "МАРКЕТИНГОВОЕ ЗАКЛЮЧЕНИЕ",
|
||||
heading: docx.HeadingLevel.HEADING_1,
|
||||
alignment: docx.AlignmentType.CENTER,
|
||||
spacing: { after: 400 }
|
||||
}),
|
||||
new docx.Paragraph({
|
||||
text: "Утверждено",
|
||||
alignment: docx.AlignmentType.RIGHT,
|
||||
spacing: { after: 200 }
|
||||
}),
|
||||
new docx.Paragraph({
|
||||
text: "_______________",
|
||||
spacing: { after: 100 }
|
||||
}),
|
||||
new docx.Paragraph({
|
||||
text: '«___» __________ 2026г.',
|
||||
spacing: { after: 400 }
|
||||
}),
|
||||
|
||||
new docx.Paragraph({
|
||||
text: "1. Входные данные",
|
||||
heading: docx.HeadingLevel.HEADING_2,
|
||||
spacing: { before: 200, after: 200 }
|
||||
}),
|
||||
new docx.Paragraph({ text: "Код ЕНС ТРУ: " + (d.inputData?.ensCode || "—") }),
|
||||
new docx.Paragraph({ text: "Объем: " + (d.inputData?.volume || "—") }),
|
||||
new docx.Paragraph({ text: "Место поставки: " + (d.inputData?.deliveryPlace || "—") }),
|
||||
new docx.Paragraph({ text: "Техническая спецификация: " + (d.inputData?.techSpec || "—") }),
|
||||
|
||||
new docx.Paragraph({
|
||||
text: "2. Список поставщиков",
|
||||
heading: docx.HeadingLevel.HEADING_2,
|
||||
spacing: { before: 200, after: 200 }
|
||||
}),
|
||||
...d.suppliers.map(s => new docx.Paragraph({
|
||||
text: `${s.name} (${s.status}, ${s.resident}) - ${s.contacts || "—"}`
|
||||
})),
|
||||
|
||||
new docx.Paragraph({
|
||||
text: "3. Ценовые предложения",
|
||||
heading: docx.HeadingLevel.HEADING_2,
|
||||
spacing: { before: 200, after: 200 }
|
||||
}),
|
||||
new docx.Table({
|
||||
width: { size: 100, type: docx.WidthType.PERCENTAGE },
|
||||
rows: [
|
||||
new docx.TableRow({
|
||||
children: [
|
||||
new docx.TableCell({ children: [new docx.Paragraph("Поставщик")] }),
|
||||
new docx.TableCell({ children: [new docx.Paragraph("№ запроса")] }),
|
||||
new docx.TableCell({ children: [new docx.Paragraph("Цена (тенге)")] }),
|
||||
new docx.TableCell({ children: [new docx.Paragraph("Дата")] })
|
||||
]
|
||||
}),
|
||||
...d.offers.map(o => {
|
||||
const s = d.suppliers.find(x => x.id === o.supplierId);
|
||||
return new docx.TableRow({
|
||||
children: [
|
||||
new docx.TableCell({ children: [new docx.Paragraph(s ? s.name : "—")] }),
|
||||
new docx.TableCell({ children: [new docx.Paragraph(o.requestNumber || "—")] }),
|
||||
new docx.TableCell({ children: [new docx.Paragraph(Number(o.price).toLocaleString())] }),
|
||||
new docx.TableCell({ children: [new docx.Paragraph(o.date || "—")] })
|
||||
]
|
||||
});
|
||||
})
|
||||
]
|
||||
}),
|
||||
|
||||
new docx.Paragraph({
|
||||
text: "4. Проверка по ст. 31",
|
||||
heading: docx.HeadingLevel.HEADING_2,
|
||||
spacing: { before: 200, after: 200 }
|
||||
}),
|
||||
new docx.Paragraph({ text: "☐ Поставщик НЕ в реестре недобросовестных: " + (d.checks.c1 ? "✓" : "☐") }),
|
||||
new docx.Paragraph({ text: "☐ НЕ в перечне терроризма/экстремизма: " + (d.checks.c2 ? "✓" : "☐") }),
|
||||
new docx.Paragraph({ text: "☐ НЕ из офшорной зоны: " + (d.checks.c3 ? "✓" : "☐") }),
|
||||
|
||||
new docx.Paragraph({
|
||||
text: "5. Анализ цен",
|
||||
heading: docx.HeadingLevel.HEADING_2,
|
||||
spacing: { before: 200, after: 200 }
|
||||
}),
|
||||
new docx.Paragraph({ text: "Количество предложений: " + d.offers.length }),
|
||||
new docx.Paragraph({ text: "Минимальная цена: " + min.toLocaleString() + " тенге" }),
|
||||
new docx.Paragraph({ text: "Максимальная цена: " + (Math.max(...prices) || 0).toLocaleString() + " тенге" }),
|
||||
new docx.Paragraph({ text: "Средняя цена: " + avg.toLocaleString() + " тенге" }),
|
||||
minSupplier ? new docx.Paragraph({ text: "Лучшее предложение: " + minSupplier.name + " - " + min.toLocaleString() + " тенге", shading: { fill: "d4edda" } }) : null,
|
||||
|
||||
new docx.Paragraph({
|
||||
text: "6. Заключение",
|
||||
heading: docx.HeadingLevel.HEADING_2,
|
||||
spacing: { before: 200, after: 200 }
|
||||
}),
|
||||
new docx.Paragraph({
|
||||
text: "На основании цен потенциальных поставщиков/производителей определены маркетинговые цены без учета НДС (КП представлены)."
|
||||
}),
|
||||
minSupplier ? new docx.Paragraph({
|
||||
text: "РЕКОМЕНДАЦИЯ: " + minSupplier.name + " (" + minSupplier.status.toLowerCase() + ", " + minSupplier.resident.toLowerCase() + ") по цене " + min.toLocaleString() + " тенге.",
|
||||
spacing: { after: 200 }
|
||||
}) : null,
|
||||
|
||||
new docx.Paragraph({ text: "Исполнитель: __________________ / _________________", spacing: { before: 400 } }),
|
||||
new docx.Paragraph({ text: "Инициатор потребности: __________________ / _________________", spacing: { before: 200 } }),
|
||||
new docx.Paragraph({ text: "Утверждено: __________________ / _________________", spacing: { before: 400, after: 200 } }),
|
||||
new docx.Paragraph({ text: '«___» __________ 2026г.', spacing: { after: 200 } })
|
||||
].filter(Boolean)
|
||||
}]
|
||||
});
|
||||
|
||||
return docx.Packer.toBuffer(doc);
|
||||
}
|
||||
|
||||
http.createServer(async (req, res) => {
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
||||
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
||||
|
||||
if (req.method === "OPTIONS") {
|
||||
res.writeHead(200);
|
||||
return res.end();
|
||||
}
|
||||
|
||||
if (req.method === "GET" && req.url === "/api/data") {
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
return res.end(JSON.stringify(readData()));
|
||||
@ -35,6 +172,12 @@ http.createServer(async (req, res) => {
|
||||
return res.end(JSON.stringify(data.files || []));
|
||||
}
|
||||
|
||||
if (req.method === "GET" && req.url === "/api/emails") {
|
||||
const data = readData();
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
return res.end(JSON.stringify(data.emails || []));
|
||||
}
|
||||
|
||||
if (req.method === "POST" && req.url === "/api/upload") {
|
||||
const form = formidable({ uploadDir: UPLOADS, keepExtensions: true });
|
||||
form.parse(req, async (err, fields, files) => {
|
||||
@ -81,6 +224,17 @@ http.createServer(async (req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method === "GET" && req.url.startsWith("/api/outputs/")) {
|
||||
const fileName = req.url.split("/")[3];
|
||||
const filePath = path.join(OUTPUTS, fileName);
|
||||
if (fs.existsSync(filePath)) {
|
||||
res.writeHead(200, { "Content-Type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "Content-Disposition": "attachment; filename=\"" + fileName + "\"" });
|
||||
return res.end(fs.readFileSync(filePath));
|
||||
}
|
||||
res.writeHead(404); res.end("File not found");
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method === "DELETE" && req.url.startsWith("/api/files/")) {
|
||||
const fileName = req.url.split("/")[3];
|
||||
const data = readData();
|
||||
@ -95,6 +249,70 @@ http.createServer(async (req, res) => {
|
||||
return res.end(JSON.stringify({ ok: true }));
|
||||
}
|
||||
|
||||
if (req.method === "POST" && req.url === "/api/generate-docx") {
|
||||
try {
|
||||
const data = readData();
|
||||
const buffer = await generateDocx(data);
|
||||
const fileName = "MAC_" + (data.inputData?.ensCode?.replace(/[^a-zA-Z0-9]/g, "") || "otchet") + "_" + Date.now() + ".docx";
|
||||
const filePath = path.join(OUTPUTS, fileName);
|
||||
fs.writeFileSync(filePath, buffer);
|
||||
|
||||
const fileRec = {
|
||||
id: Date.now(),
|
||||
name: fileName.replace(/_\d+\.docx$/, ".docx"),
|
||||
path: "/api/outputs/" + fileName,
|
||||
section: "Выходные документы",
|
||||
size: buffer.length,
|
||||
date: new Date().toISOString()
|
||||
};
|
||||
data.files.push(fileRec);
|
||||
writeData(data);
|
||||
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
return res.end(JSON.stringify({ ok: true, file: fileRec }));
|
||||
} catch (e) {
|
||||
console.error("DOCX error:", e);
|
||||
res.writeHead(500, { "Content-Type": "application/json" });
|
||||
return res.end(JSON.stringify({ error: e.message }));
|
||||
}
|
||||
}
|
||||
|
||||
if (req.method === "POST" && req.url === "/api/send-emails") {
|
||||
try {
|
||||
const d = await body(req);
|
||||
const data = readData();
|
||||
const inputData = data.inputData || {};
|
||||
|
||||
const emails = [];
|
||||
for (const supplier of data.suppliers) {
|
||||
const emailData = {
|
||||
id: Date.now() + Math.random(),
|
||||
to: supplier.contacts || "",
|
||||
subject: "Запрос коммерческого предложения: " + (inputData.ensCode || "ТРУ"),
|
||||
body: `Уважаемые партнёры!\n\nПросим предоставить коммерческое предложение на поставку:\n\n` +
|
||||
`Код ЕНС: ${inputData.ensCode || "—"}\n` +
|
||||
`Объем: ${inputData.volume || "—"}\n` +
|
||||
`Место поставки: ${inputData.deliveryPlace || "—"}\n` +
|
||||
`Техническая спецификация: ${inputData.techSpec || "—"}\n\n` +
|
||||
`Просим ответить до: ${d.deadline || "укажите срок"}\n\n` +
|
||||
`С уважением,\nОтдел закупок`,
|
||||
supplierName: supplier.name,
|
||||
date: new Date().toISOString(),
|
||||
status: "Отправлено (симуляция)"
|
||||
};
|
||||
data.emails.push(emailData);
|
||||
emails.push(emailData);
|
||||
}
|
||||
writeData(data);
|
||||
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
return res.end(JSON.stringify({ ok: true, count: emails.length, emails }));
|
||||
} catch (e) {
|
||||
res.writeHead(500, { "Content-Type": "application/json" });
|
||||
return res.end(JSON.stringify({ error: e.message }));
|
||||
}
|
||||
}
|
||||
|
||||
if (req.method === "POST" && req.url === "/api/inputData") {
|
||||
const d = await body(req);
|
||||
const data = readData();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user