diff --git a/.vibe42-run.log b/.vibe42-run.log
index cf1d345..6069ff9 100644
--- a/.vibe42-run.log
+++ b/.vibe42-run.log
@@ -12,3 +12,9 @@ server on port 3000
--- run Sun Sep 27 04:42:10 PM UTC 2026 :: node server.js
CHECK: syntax OK, data.json rows: 103, 2025 total: 8 350 018 093
server on port 3000
+--- run Sun Sep 27 04:58:42 PM UTC 2026 :: node server.js
+CHECK: syntax OK, data.json rows: 103, 2025 total: 8 350 018 093
+server on port 3000
+--- run Sun Sep 27 05:13:37 PM UTC 2026 :: node server.js
+CHECK: syntax OK, data.json rows: 103, 2025 total: 8 350 018 093
+server on port 3000
diff --git a/.vibe42-run.pid b/.vibe42-run.pid
index c882b49..a377090 100644
--- a/.vibe42-run.pid
+++ b/.vibe42-run.pid
@@ -1 +1 @@
-2597
+4826
diff --git a/index.html b/index.html
index 4d42c13..f37b911 100644
--- a/index.html
+++ b/index.html
@@ -107,6 +107,10 @@
Показать презентацию
+
+
+ Анализ_расходов_по_сервисным_контрактам.docx
+
По кнопке «Опубликовать» откроется таблица; презентация живёт на отдельном URL — её можно открыть в другом окне.
diff --git a/mkword.py b/mkword.py
new file mode 100644
index 0000000..02ad19a
--- /dev/null
+++ b/mkword.py
@@ -0,0 +1,331 @@
+import json, datetime
+from docx import Document
+from docx.shared import Pt, Cm, RGBColor
+from docx.enum.text import WD_ALIGN_PARAGRAPH
+from docx.enum.table import WD_TABLE_ALIGNMENT, WD_ALIGN_VERTICAL
+from docx.oxml.ns import qn, nsmap
+from docx.oxml import OxmlElement
+
+DARK = RGBColor(0x0D, 0x1B, 0x2A)
+PANEL = RGBColor(0x14, 0x24, 0x33)
+ACCENT = RGBColor(0x1B, 0x4F, 0x72)
+OK = RGBColor(0x1A, 0x7F, 0x4B)
+RISK = RGBColor(0xC0, 0x39, 0x2B)
+WARN = RGBColor(0xD4, 0x68, 0x0A)
+MUTE = RGBColor(0x88, 0x99, 0xA6)
+DIM = RGBColor(0x6C, 0x8F, 0xA8)
+FG = RGBColor(0xF0, 0xF4, 0xF8)
+TXT = RGBColor(0x0C, 0x0A, 0x09)
+TXT2 = RGBColor(0x78, 0x71, 0x6C)
+
+doc = Document()
+# А4, поля
+for s in doc.sections:
+ s.page_height = Cm(29.7); s.page_width = Cm(21.0)
+ s.top_margin = Cm(1.8); s.bottom_margin = Cm(1.5)
+ s.left_margin = Cm(1.5); s.right_margin = Cm(1.5)
+
+d = json.load(open('data.json'))
+rows = d['rows']
+t23 = sum(r.get('y2023') or 0 for r in rows)
+t24 = sum(r.get('y2024') or 0 for r in rows)
+t25 = sum(r.get('y2025') or 0 for r in rows)
+q26 = sum(r.get('q1_2026') or 0 for r in rows)
+b = lambda v: v / 1e9
+m = lambda v: v / 1e6
+
+# ---------- Заголовок ----------
+t = doc.add_paragraph()
+t.alignment = WD_ALIGN_PARAGRAPH.CENTER
+r = t.add_run('АНАЛИЗ РАСХОДОВ ПО СЕРВИСНЫМ КОНТРАКТАМ')
+r.bold = True; r.font.size = Pt(22); r.font.color.rgb = DARK; r.font.name = 'Segoe UI'
+sub = doc.add_paragraph()
+sub.alignment = WD_ALIGN_PARAGRAPH.CENTER
+r = sub.add_run('Детализация 2023 г. – 1-е полугодие 2026 г. · 103 позиции · сумма в тенге (факт)')
+r.font.size = Pt(11); r.font.color.rgb = TXT2; r.font.name = 'Segoe UI'
+sub.paragraph_format.space_after = Pt(14)
+
+# ---------- KPI ----------
+def kpi_card(label, value, sub='', color=DARK):
+ p = doc.add_paragraph()
+ p.paragraph_format.space_after = Pt(2)
+ r = p.add_run(value)
+ r.bold = True; r.font.size = Pt(20); r.font.color.rgb = color; r.font.name = 'Segoe UI'
+ p2 = doc.add_paragraph()
+ p2.paragraph_format.space_after = Pt(0)
+ r = p2.add_run(label)
+ r.font.size = Pt(9); r.font.color.rgb = TXT2; r.font.name = 'Segoe UI'
+ if sub:
+ p3 = doc.add_paragraph()
+ p3.paragraph_format.space_after = Pt(4)
+ r = p3.add_run(sub)
+ r.font.size = Pt(8); r.font.italic = True; r.font.color.rgb = MUTE; r.font.name = 'Segoe UI'
+
+tbl = doc.add_table(rows=1, cols=4)
+tbl.style = 'Table Grid'
+tbl.alignment = WD_TABLE_ALIGNMENT.CENTER
+for i, (lbl, val, sub, col) in enumerate([
+ ('2023', f'{b(t23):,.2f} млрд тг', '55 позиций', DARK),
+ ('2024', f'{b(t24):,.2f} млрд тг', '54 позиции · пик', RISK),
+ ('2025', f'{b(t25):,.2f} млрд тг', '45 позиций', DARK),
+ ('1пг 2026', f'{b(q26):,.2f} млрд тг', '31 позиция', OK),
+]):
+ cell = tbl.rows[0].cells[i]
+ cell.width = Cm(3.8)
+ # Кнопка-фон: светлый
+ tcPr = cell._tc.get_or_add_tcPr()
+ shd = OxmlElement('w:shd'); shd.set(qn('w:fill'), 'F0F4F8'); tcPr.append(shd)
+ p = cell.paragraphs[0]; p.alignment = WD_ALIGN_PARAGRAPH.CENTER; p.paragraph_format.space_after = Pt(2)
+ r = p.add_run(val)
+ r.bold = True; r.font.size = Pt(18); r.font.color.rgb = col; r.font.name = 'Segoe UI'
+ p2 = cell.add_paragraph(); p2.alignment = WD_ALIGN_PARAGRAPH.CENTER; p2.paragraph_format.space_after = Pt(0)
+ r = p2.add_run(lbl)
+ r.bold = True; r.font.size = Pt(10); r.font.color.rgb = TXT; r.font.name = 'Segoe UI'
+ p3 = cell.add_paragraph(); p3.alignment = WD_ALIGN_PARAGRAPH.CENTER; p3.paragraph_format.space_after = Pt(2)
+ r = p3.add_run(sub)
+ r.font.size = Pt(8); r.font.color.rgb = MUTE; r.font.name = 'Segoe UI'
+doc.add_paragraph()
+
+# ---------- Сводка ----------
+h = doc.add_paragraph()
+h.paragraph_format.space_before = Pt(6); h.paragraph_format.space_after = Pt(6)
+r = h.add_run('1. Итоговый свод')
+r.bold = True; r.font.size = Pt(14); r.font.color.rgb = DARK; r.font.name = 'Segoe UI'
+
+def bullet(text, bold_lead=None):
+ p = doc.add_paragraph(style='List Bullet')
+ p.paragraph_format.space_after = Pt(6)
+ if bold_lead:
+ r = p.add_run(bold_lead + ' ')
+ r.bold = True; r.font.size = Pt(10.5); r.font.color.rgb = TXT; r.font.name = 'Segoe UI'
+ r = p.add_run(text)
+ r.font.size = Pt(10.5); r.font.color.rgb = TXT; r.font.name = 'Segoe UI'
+ return p
+
+d24_25 = t25 - t24; p24_25 = (t24 and d24_25 / t24 * 100)
+d23_24 = t24 - t23; p23_24 = d23_24 / t23 * 100
+bullet(f'Изменение 2024 → 2025: −{b(abs(d24_25)):,.2f} млрд тг ({p24_25:+.1f} %) — это не экономия, а смена номенклатуры (FLM Expenses → FLM, Ericsson CS/5G part → общий договор, MS EA/COGS/G&A).'.replace('','').replace('',''),
+ bold_lead='Ключевой вывод')
+bullet(f'2023 → 2024: рост на {b(d23_24):,.2f} млрд тг ({p23_24:+.1f} %) — пик расходов.',
+ bold_lead='Динамика')
+bullet(f'Темп 1пг 2026 ({b(q26):,.2f} млрд за полгода), удвоенный на год ≈ {b(q26*2):,.2f} млрд — на уровне 2024.',
+ bold_lead='Прогноз 2026')
+bullet('Структура 2025: FLM 3 232 млн · Ericsson 790 млн · MS EA 630 млн · MS EA COGS 513 млн · FLM Expenses 3 481 млн (2024) — крупнейшие статьи.',
+ bold_lead='Топ статей 2025')
+
+# ---------- Отклонения 2025 vs 2024 ----------
+doc.add_paragraph()
+h = doc.add_paragraph()
+h.paragraph_format.space_before = Pt(10)
+r = h.add_run('2. Отклонения 2025 vs 2024 (существующие статьи, |Δ| ≥ 50 млн тг)')
+r.bold = True; r.font.size = Pt(14); r.font.color.rgb = DARK; r.font.name = 'Segoe UI'
+
+tab = doc.add_table(rows=1, cols=5)
+tab.alignment = WD_TABLE_ALIGNMENT.CENTER
+hdr = tab.rows[0].cells
+labels = ['Позиция', '2024, млн тг', '2025, млн тг', 'Δ, млн тг', 'Δ %']
+for i, l in enumerate(labels):
+ cell = hdr[i]
+ tcPr = cell._tc.get_or_add_tcPr()
+ shd = OxmlElement('w:shd'); shd.set(qn('w:fill'), '142433'); tcPr.append(shd)
+ p = cell.paragraphs[0]; p.alignment = WD_ALIGN_PARAGRAPH.RIGHT if i > 0 else WD_ALIGN_PARAGRAPH.LEFT
+ r = p.add_run(l)
+ r.bold = True; r.font.size = Pt(9); r.font.color.rgb = MUTE; r.font.name = 'Segoe UI'
+
+cands = [r for r in rows if (r.get('d25') or 0) not in (0, None) and (r.get('y2025') or 0) > 0 and (r.get('y2024') or 0) > 0]
+cands.sort(key=lambda r: abs(r.get('d25') or 0), reverse=True)
+cands = [c for c in cands if abs(c['d25']) >= 50e6][:15]
+ri = 0
+for it in cands:
+ row = tab.add_row().cells
+ for i, (v, fmt) in enumerate([
+ (it.get('name'), '{:s}'),
+ (it.get('y2024') / 1e6, '{:,.1f}'),
+ (it.get('y2025') / 1e6, '{:,.1f}'),
+ (it.get('d25') / 1e6, '{:+,.1f}'),
+ (it.get('p25'), '{:+.1f} %'),
+ ]):
+ cell = row[i]
+ if v is None or v == '':
+ continue
+ tcPr = cell._tc.get_or_add_tcPr()
+ shd = OxmlElement('w:shd')
+ if i == 0:
+ shd.set(qn('w:fill'), 'F0F4F8' if ri % 2 else 'FFFFFF')
+ else:
+ shd.set(qn('w:fill'), 'F0F4F8' if ri % 2 else 'FFFFFF')
+ tcPr.append(shd)
+ if i == 0:
+ p = cell.paragraphs[0]; r = p.add_run(fmt.format(v)); r.font.size = Pt(9); r.font.color.rgb = TXT; r.font.name = 'Segoe UI'
+ else:
+ p = cell.paragraphs[0]; p.alignment = WD_ALIGN_PARAGRAPH.RIGHT
+ r = p.add_run(fmt.format(v)); r.font.size = Pt(9); r.font.name = 'Segoe UI'
+ if i == 3 or i == 4:
+ color = RISK if v > 0 else OK
+ r.bold = True; r.font.color.rgb = color
+ ri += 1
+cap = doc.add_paragraph()
+cap.paragraph_format.space_before = Pt(4)
+r = cap.add_run('Красное — рост относительно 2024, зелёное — снижение. Полный список — лист «Детализация» в Excel-отчёте.')
+r.font.size = Pt(8); r.font.italic = True; r.font.color.rgb = MUTE; r.font.name = 'Segoe UI'
+
+# ---------- 1пг 2026 ----------
+doc.add_paragraph()
+h = doc.add_paragraph()
+h.paragraph_format.space_before = Pt(10)
+r = h.add_run('3. 1-е полугодие 2026: существующие статьи (|Δ| ≥ 50 млн тг, к темпу 2025)')
+r.bold = True; r.font.size = Pt(14); r.font.color.rgb = DARK; r.font.name = 'Segoe UI'
+tab = doc.add_table(rows=1, cols=5)
+tab.alignment = WD_TABLE_ALIGNMENT.CENTER
+hdr = tab.rows[0].cells
+labels = ['Позиция', 'Темп 1пг 2025, млн', '1пг 2026, млн', 'Δ, млн тг', 'Δ %']
+for i, l in enumerate(labels):
+ cell = hdr[i]
+ tcPr = cell._tc.get_or_add_tcPr()
+ shd = OxmlElement('w:shd'); shd.set(qn('w:fill'), '142433'); tcPr.append(shd)
+ p = cell.paragraphs[0]; p.alignment = WD_ALIGN_PARAGRAPH.RIGHT if i > 0 else WD_ALIGN_PARAGRAPH.LEFT
+ r = p.add_run(l)
+ r.bold = True; r.font.size = Pt(9); r.font.color.rgb = MUTE; r.font.name = 'Segoe UI'
+
+cands26 = [r for r in rows if (r.get('q1_2026') or 0) > 0 and (r.get('y2025') or 0) > 0]
+for c in cands26:
+ c['q1_2025'] = c['y2025'] / 2
+ c['d26'] = c['q1_2026'] - c['q1_2025']
+ c['p26'] = c['d26'] / c['q1_2025'] * 100 if c['q1_2025'] else 0
+cands26.sort(key=lambda r: abs(r.get('d26') or 0), reverse=True)
+cands26 = [c for c in cands26 if abs(c['d26']) >= 50e6][:15]
+ri = 0
+for it in cands26:
+ row = tab.add_row().cells
+ for i, (v, fmt) in enumerate([
+ (it.get('name'), '{:s}'),
+ (it.get('q1_2025') / 1e6, '{:,.1f}'),
+ (it.get('q1_2026') / 1e6, '{:,.1f}'),
+ (it.get('d26') / 1e6, '{:+,.1f}'),
+ (it.get('p26'), '{:+.1f} %'),
+ ]):
+ cell = row[i]
+ tcPr = cell._tc.get_or_add_tcPr()
+ shd = OxmlElement('w:shd'); shd.set(qn('w:fill'), 'F0F4F8' if ri % 2 else 'FFFFFF'); tcPr.append(shd)
+ if i == 0:
+ p = cell.paragraphs[0]; r = p.add_run(fmt.format(v)); r.font.size = Pt(9); r.font.color.rgb = TXT; r.font.name = 'Segoe UI'
+ else:
+ p = cell.paragraphs[0]; p.alignment = WD_ALIGN_PARAGRAPH.RIGHT
+ r = p.add_run(fmt.format(v)); r.font.size = Pt(9); r.font.name = 'Segoe UI'
+ if i == 3 or i == 4:
+ color = RISK if v > 0 else OK
+ r.bold = True; r.font.color.rgb = color
+ ri += 1
+cap = doc.add_paragraph()
+cap.paragraph_format.space_before = Pt(4)
+r = cap.add_run('«Темп 1пг 2025» = факт 2025 / 2 (ориентир при равномерном распределении). Красное — рост, зелёное — снижение.')
+r.font.size = Pt(8); r.font.italic = True; r.font.color.rgb = MUTE; r.font.name = 'Segoe UI'
+
+# ---------- Новые / исчезнувшие ----------
+doc.add_paragraph()
+h = doc.add_paragraph()
+h.paragraph_format.space_before = Pt(10)
+r = h.add_run('4. Смена номенклатуры: что появилось / исчезло в 2025')
+r.bold = True; r.font.size = Pt(14); r.font.color.rgb = DARK; r.font.name = 'Segoe UI'
+
+names24 = set(r['name'] for r in rows if (r.get('y2024') or 0) > 0 and (r.get('y2025') or 0) == 0)
+new25 = [r for r in rows if (r.get('y2025') or 0) > 0 and (r.get('y2024') or 0) == 0]
+gone25 = [r for r in rows if (r.get('y2024') or 0) > 0 and (r.get('y2025') or 0) == 0]
+new25.sort(key=lambda r: r['y2025'], reverse=True)
+gone25.sort(key=lambda r: r['y2024'], reverse=True)
+
+def small_table(rows_, idx, ykey, title):
+ h2 = doc.add_paragraph()
+ h2.paragraph_format.space_before = Pt(6); h2.paragraph_format.space_after = Pt(4)
+ r = h2.add_run(title)
+ r.bold = True; r.font.size = Pt(11); r.font.color.rgb = ACCENT; r.font.name = 'Segoe UI'
+ t2 = doc.add_table(rows=1, cols=3)
+ t2.alignment = WD_TABLE_ALIGNMENT.CENTER
+ hdr = t2.rows[0].cells
+ for i, l in enumerate(['Позиция', f'{ykey}, млн тг', 'Коммент']):
+ cell = hdr[i]
+ tcPr = cell._tc.get_or_add_tcPr()
+ shd = OxmlElement('w:shd'); shd.set(qn('w:fill'), 'F0F4F8'); tcPr.append(shd)
+ p = cell.paragraphs[0]; p.alignment = WD_ALIGN_PARAGRAPH.RIGHT if i == 1 else WD_ALIGN_PARAGRAPH.LEFT
+ r = p.add_run(l)
+ r.bold = True; r.font.size = Pt(8); r.font.color.rgb = MUTE; r.font.name = 'Segoe UI'
+ for it in rows_:
+ row = t2.add_row().cells
+ row[0].paragraphs[0].add_run(it['name']).font.size = Pt(8)
+ p = row[1].paragraphs[0]; p.alignment = WD_ALIGN_PARAGRAPH.RIGHT
+ r = p.add_run(f"{it[ykey]/1e6:,.1f}"); r.font.size = Pt(8)
+ row[2].paragraphs[0].add_run('').font.size = Pt(8)
+ # Итог
+ tr = t2.add_row().cells
+ tr[0].paragraphs[0].add_run(f"Итого: {len(rows_)} шт.").bold = True
+ p = tr[1].paragraphs[0]; p.alignment = WD_ALIGN_PARAGRAPH.RIGHT
+ r = p.add_run(f"{sum(r_[ykey] for r_ in rows_)/1e6:,.1f}"); r.bold = True; r.font.size = Pt(8)
+ # Заполнить фон итога
+ for cell in tr:
+ tcPr = cell._tc.get_or_add_tcPr()
+ shd = OxmlElement('w:shd'); shd.set(qn('w:fill'), 'EEF4FB'); tcPr.append(shd)
+
+small_table(new25[:12], 0, 'y2025', f'Появились в 2025 (нет в 2024): {len(new25)} шт.')
+small_table(gone25[:12], 1, 'y2024', f'Исчезли в 2025 (были в 2024): {len(gone25)} шт.')
+cap = doc.add_paragraph()
+cap.paragraph_format.space_before = Pt(4)
+sum_new = sum(r['y2025'] for r in new25)
+sum_gone = sum(r['y2024'] for r in gone25)
+r = cap.add_run(f'Пришло {b(sum_new):,.2f} млрд тг, ушло {b(sum_gone):,.2f} млрд тг. Чистая смена номенклатуры: {b(sum_new - sum_gone):+.2f} млрд тг — это механика спада 2025.')
+r.font.size = Pt(9); r.font.color.rgb = TXT2; r.font.name = 'Segoe UI'
+
+# ---------- Рекомендации ----------
+doc.add_paragraph()
+h = doc.add_paragraph()
+h.paragraph_format.space_before = Pt(10)
+r = h.add_run('5. Рекомендации')
+r.bold = True; r.font.size = Pt(14); r.font.color.rgb = DARK; r.font.name = 'Segoe UI'
+recs = [
+ ('Приоритет 1 — сверить периметры 1пг 2026.',
+ 'MS EA (базовая строка 630 млн = как за весь 2025), Oracle HW (376 млн, новая), Ericsson «общий новый договор» (827 млн, объединение CS part + 5G part).'),
+ ('Приоритет 2 — консолидация Ericsson.',
+ 'Объединение в один договор уже сделано в 2026. Следующий шаг — переход на гибридную модель (P1 + RMA за вендора, P2/P3 силами КТ).'),
+ ('Приоритет 3 — проверить FLM.',
+ '+35,1 % к темпу 2025. 2 184 млн тг за 1пг 2026. Если рост продолжается — пересмотреть условия.'),
+ ('Приоритет 4 — зафиксировать сокращения.',
+ 'CORM-Ронекс −26,9 млн, Aitu Passport −21,9 млн, Nexign −68 млн (к темпу 2025). Заложить в план 2027.'),
+ ('Приоритет 5 — реестр статусов.',
+ 'К каждой статье: «прямой / посредник / дистрибьютор», «готов к инсорсингу / нужен вендор», P1/RMA за период.'),
+]
+for i, (b_, s_) in enumerate(recs, 1):
+ p = doc.add_paragraph()
+ p.paragraph_format.space_after = Pt(4)
+ r = p.add_run(f' {b_}')
+ r.bold = True; r.font.size = Pt(10.5); r.font.color.rgb = DARK; r.font.name = 'Segoe UI'
+ p2 = doc.add_paragraph()
+ p2.paragraph_format.space_after = Pt(10)
+ p2.paragraph_format.left_indent = Cm(0.75)
+ r = p2.add_run(s_)
+ r.font.size = Pt(9.5); r.font.color.rgb = TXT2; r.font.name = 'Segoe UI'
+
+# ---------- Итог ----------
+doc.add_paragraph()
+h = doc.add_paragraph()
+h.paragraph_format.space_before = Pt(10)
+r = h.add_run('6. Итог')
+r.bold = True; r.font.size = Pt(14); r.font.color.rgb = DARK; r.font.name = 'Segoe UI'
+p = doc.add_paragraph()
+p.paragraph_format.space_after = Pt(6)
+r = p.add_run(f'−{b(abs(t25-t24)):,.2f} млрд тг ({p24_25:+.1f} %) к 2025')
+r.bold = True; r.font.size = Pt(20); r.font.color.rgb = ACCENT; r.font.name = 'Segoe UI'
+bullets = [
+ 'Падение 2025 — не экономия, а смена номенклатуры (FLM Expenses → FLM, Ericsson part → общий договор, MS EA COGS/G&A).',
+ '1пг 2026 показывает рост +1,3 % к темпу 2025: драйверы — MS EA +15,7 млн, Checkpoint +64,9 млн.',
+ 'Главный риск 2027 — MS EA: 630 млн за 1пг 2026 = как за весь 2025. При сохранении темпа год ≈ +100 %. Следующий шаг — сверить периметры договоров и зафиксировать реестр статусов (прямой / посредник, инсорсинг / вендор).',
+]
+for b_ in bullets:
+ bullet(b_)
+
+# ---------- Подпись ----------
+doc.add_paragraph()
+p = doc.add_paragraph()
+r = p.add_run('Источник: «Расходы по сервисным контрактам.xlsx», факт. Детали: Excel-отчёт (6 листов) и презентация Power Point.')
+r.font.size = Pt(8); r.font.italic = True; r.font.color.rgb = MUTE; r.font.name = 'Segoe UI'
+
+doc.save('Анализ_расходов_по_сервисным_контрактам.docx')
+print('OK: Анализ_расходов_по_сервисным_контрактам.docx')
diff --git a/server.js b/server.js
index abc039d..a997c40 100644
--- a/server.js
+++ b/server.js
@@ -32,14 +32,15 @@ const srv = http.createServer((req, res) => {
res.writeHead(200, { "Content-Type": "application/json" });
return fs.createReadStream(path.join(__dirname, "data.json")).pipe(res);
}
- // Отчёт — xlsx. Отдаём как приложение, чтобы браузер скачал, а не попробовал показать.
- const m = url.match(/^\/([^\/]+\.(xlsx|csv|json|html|js|css))/i);
+ // Отчёт — xlsx/docx. Отдаём как приложение, чтобы браузер скачал, а не попробовал показать.
+ const m = url.match(/^\/([^\/]+\.(xlsx|docx|csv|json|html|js|css))/i);
if (m) {
const file = path.basename(decodeURIComponent(m[1]));
const fp = path.join(__dirname, file);
if (fs.existsSync(fp)) {
const ct =
file.toLowerCase().endsWith(".xlsx") ? "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" :
+ file.toLowerCase().endsWith(".docx") ? "application/vnd.openxmlformats-officedocument.wordprocessingml.document" :
file.toLowerCase().endsWith(".csv") ? "text/csv; charset=utf-8" :
file.toLowerCase().endsWith(".json") ? "application/json" :
file.toLowerCase().endsWith(".js") ? "text/javascript" :
diff --git a/Анализ_расходов_по_сервисным_контрактам.docx b/Анализ_расходов_по_сервисным_контрактам.docx
new file mode 100644
index 0000000..70683d8
Binary files /dev/null and b/Анализ_расходов_по_сервисным_контрактам.docx differ