From 172f1148af060a31437dfcdba5ffd69d53c755a8 Mon Sep 17 00:00:00 2001 From: Zulfija Date: Tue, 1 Sep 2026 10:04:09 +0000 Subject: [PATCH] =?UTF-8?q?site:=20=D0=B0=D0=BD=D0=B0=D0=BB=D0=B8=D0=B7=20?= =?UTF-8?q?=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20=E2=80=94=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D0=B0=20Excel/CSV=20=D1=84=D0=B0?= =?UTF-8?q?=D0=B9=D0=BB=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 12 +- design.md | 249 +++++++---- index.html | 1193 +++++++++++++++++++++------------------------------- script.js | 720 ++++++++++++++++++++----------- 4 files changed, 1139 insertions(+), 1035 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 6c18247..8b3c5d3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,4 +1,4 @@ - + # Vibe42 — учебная песочница: сайты, боты и первые приложения Workspace юзера `Zulfija`. Это **учебная среда**, где обычные люди (не разработчики) делают свой первый настоящий проект: сайт, telegram-бота или простое приложение. @@ -23,7 +23,7 @@ Workspace юзера `Zulfija`. Это **учебная среда**, где о | Что хочет юзер | Стек (ЖЁСТКО) | Рецепт | |----------------|---------------|--------| -| Сайт, лендинг, визитка, портфолио, меню, афиша, waitlist | **Статика:** `index.html` + `style.css` + `script.js`, ванильный JS. БЕЗ сборки, БЕЗ React/Vue/Vite, БЕЗ npm. Tailwind — только CDN. | старт с `design.md`, публикация в `pages` | +| Сайт, лендинг, визитка, портфолио, меню, афиша, waitlist | **Статика:** `index.html` + `style.css` + `script.js`, ванильный JS. БЕЗ сборки, БЕЗ React/Vue/Vite, БЕЗ npm. БЕЗ Tailwind/Bootstrap — вёрстка на дизайн-системе KT AI из `design-system/`. | старт с `design.md`, публикация в `pages` | | Telegram-бот | **Node.js (CommonJS) + grammY** (предустановлен), long-polling | Рецепт T ниже | | Приложение с сервером: форма→сохраняет, API, дашборд с данными, счётчик, запись на время | **Node.js (CommonJS), сервер на `node:http` БЕЗ зависимостей + хранение в `data.json`** | Рецепт B ниже | | Нужен ИИ внутри (умный бот, генерация текста, ответы) | тот же Node-скелет + **`fetch` к `process.env.AI_BASE_URL`** (без SDK) | блок «ИИ» ниже | @@ -839,6 +839,10 @@ git push origin HEAD:pages --- -## 🎨 design.md +## 🎨 design.md + дизайн-система KT AI -Рядом лежит `design.md` с готовой палитрой, типографикой и стартер-шаблоном `index.html`. **Начинай с него.** Не выдумывай новые цвета — модифицируй существующие. +Рядом лежит `design.md` — **прочитай его перед первой строкой вёрстки**. В нём каркас `index.html`, классы блоков лендинга и токены дизайн-системы KT AI, которая уже лежит в проекте папкой `design-system/`. + +Правило простое: **весь UI собирается из классов и токенов ДС**. Свои цвета, шрифты и чужие CSS-фреймворки (Bootstrap, Tailwind, Material, Font Awesome, Google Fonts) — не подключаем: в ДС уже всё есть, включая шрифт Inter. + +Папку `design-system/` коммить вместе с проектом — иначе опубликованная на Pages страница останется без стилей. diff --git a/design.md b/design.md index 1a16f8b..fb49a5b 100644 --- a/design.md +++ b/design.md @@ -1,132 +1,207 @@ - -# Design system — Vibe42 песочница + +# Дизайн-система KT AI — по ней собирается весь UI -Базовые цвета и типографика для лендингов. Начинай с этого, можно отклоняться. +В папке проекта лежит `design-system/` — это **дизайн-система KT AI** +(источник: `github.com/erjan-A/kt-ai-design-system`, MIT, версия в `design-system/VERSION`). +Весь визуал берётся оттуда: цвета, шрифт, отступы, кнопки, карточки, секции лендинга. -## Палитра +**Не выдумывай свой стиль и не тащи чужие фреймворки.** Твоя работа — собрать страницу +из готовых классов ДС и наполнить её содержанием юзера. -| Token | Hex | Использование | -|-------|-----|---------------| -| `--ink` | `#0F1218` | Тёмный фон / основной текст | -| `--cyan` | `#00E5FF` | Основной акцент (кнопки, лого) | -| `--cyan-50` | `#E8FCFF` | Светлая подложка для акцентов | -| `--white` | `#FFFFFF` | Основной фон | -| `--gray-500` | `#5B6573` | Вторичный текст | -| `--gray-100` | `#F2F4F7` | Сепараторы / тонкие фоны | +Если папки `design-system/` в проекте нет (старый проект) — принеси её один раз: -## Типографика - -```css -font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, system-ui, sans-serif; +```bash +cp -r /srv/opencode/design-system/kt-ai design-system ``` -| Уровень | Размер | Вес | line-height | -|---------|--------|-----|-------------| -| h1 (hero) | 56px | 800 | 1.05 | -| h2 (section) | 36px | 700 | 1.15 | -| h3 | 22px | 700 | 1.3 | -| body | 17px | 400 | 1.6 | +--- -На мобиле — h1 → 36px, h2 → 28px. +## 🔒 Три правила, которые нельзя нарушать -## Лейаут +1. **Цвета и размеры — только токенами** `var(--kt-ai-…)`. Сырой hex (`#0F1218`, `#fff`) в вёрстке + запрещён: он ломает светлую/тёмную тему. +2. **Никакого Bootstrap / Tailwind / Material / Font Awesome / Google Fonts.** Шрифт (Inter) уже + внутри ДС и работает офлайн. Всё, что нужно лендингу, в ДС уже есть. +3. **Свой ` -
-

Заголовок

-

Подзаголовок — пара предложений о чём это.

- Начать -
-
-

Секция

-
Контент карточки.
-
Контент карточки.
-
+ +
+
+ Название + + О нас + Оставить заявку +
+
+ +
+
+

Главное обещание в одну строку

+

Подзаголовок: кому это и почему стоит остаться.

+ +
+
+ +
+
+
+

Что вы получите

+

Одно-два предложения пояснения к секции.

+
+
+

Пункт первый

Коротко и по делу.

+

Пункт второй

Коротко и по делу.

+

Пункт третий

Коротко и по делу.

+
+
+
+ +
+
+

Призыв к действию

+

Город, адрес, часы работы — то, что нужно, чтобы дойти.

+ Написать +
+
+ + + ``` -## Что НЕ юзать +--- -- Bootstrap / Material UI / Chakra / Ant Design — слишком тяжело -- Font Awesome — используй emoji (🚀 ⚡ ✨) или inline SVG -- jQuery — vanilla JS достаточно +## Блоки страницы (`kt-ai-page.css`) -## Что МОЖНО (если очень надо) +| Класс | Что это | +|-------|---------| +| `.kt-ai-wrap` | контейнер по центру (1080px). `data-width="narrow"` — 720px, `"wide"` — 1200px | +| `.kt-ai-pagehead` + `.kt-ai-pagehead-in` | липкая шапка страницы | +| `.kt-ai-hero`, `.kt-ai-hero-sub`, `.kt-ai-hero-actions` | первый экран. `data-align="center"` — по центру | +| `.kt-ai-section` + `.kt-ai-section-head` (h2 + p) | обычная секция. `data-divider="true"` — линия сверху | +| `.kt-ai-grid` + `.kt-ai-tile` (h3 + p) | сетка плиток «возможности». `data-cols="2"/"4"`, у плитки `data-span="2"/"full"` | +| `.kt-ai-tile-icon` | иконка над заголовком плитки (спрайт — `design-system/icons/`) | +| `.kt-ai-cta` | финальный блок с призывом | +| `.kt-ai-pagefoot` | подвал | +| `.kt-ai-spacer` | распорка внутри шапки/подвала (толкает остальное вправо) | +| `.kt-ai-centered` | одна карточка по центру экрана — для страницы входа и 404 | -- **Tailwind через CDN**: `` -- **Placeholder картинки**: `https://picsum.photos/800/600` -- **Google Fonts через ``** в head +Отступы между секциями ДС ставит сама. Адаптив (3→2→1 колонка) — тоже, `@media` писать не нужно. -## 📦 Готовые рецепты (используй их, не изобретай) +## Элементы (`kt-ai-components.css`) + +| Класс | Как применять | +|-------|---------------| +| `.kt-ai-btn` | кнопка / ссылка-кнопка. `data-variant="primary"` — главная, `"ghost"` — прозрачная, `"danger"` — опасная. `data-size="sm"/"lg"` | +| `.kt-ai-card` | карточка контента | +| `.kt-ai-chip` | статус-пилюля / тег | +| `.kt-ai-link` | текстовая ссылка | +| `.kt-ai-field` + `.kt-ai-input` / `.kt-ai-textarea` / `.kt-ai-select` | поле формы с подписью | + +**Один `primary` на экран.** Остальные кнопки — обычные или `ghost`. + +## Токены (`kt-ai-tokens.css`) + +Цвет: `--kt-ai-bg`, `--kt-ai-bg-soft`, `--kt-ai-fg`, `--kt-ai-fg-muted`, `--kt-ai-fg-faint`, +`--kt-ai-border`, `--kt-ai-divider`, `--kt-ai-card-bg`, `--kt-ai-card-border`, +`--kt-ai-primary`, `--kt-ai-link`, `--kt-ai-danger`. + +Отступы: `--kt-ai-space-1…12` (2px → 64px). Скругления: `--kt-ai-radius-md/lg/xl/2xl/3xl/full`. +Текст: `--kt-ai-text-xs…3xl`. Вес: `--kt-ai-weight-regular/medium/semibold/bold`. +Шрифты: `--kt-ai-font-sans` (Inter), `--kt-ai-font-mono`. + +**Тема** — атрибут на ``: `data-theme="auto"` (по системной, ставь его по умолчанию), +`"light"`, `"dark"`. Проверь страницу в обеих темах — с токенами это бесплатно. + +--- + +## Не лендинг, а приложение (дашборд, таблица, очередь заявок, чат) + +Тогда экран собирается **не вёрсткой руками, а конфигом**: + +1. Возьми `design-system/templates/kt-ai-app-shell.html` — самодостаточный HTML-каркас приложения + (без React и npm). +2. Опиши экран JSON-конфигом по `design-system/docs/PRODUCT_CONTRACT.md` и положи его в тег + `` внутри каркаса — он сам отрисует + таблицу, KPI, фильтры, drawer и темы. +3. Проверь конфиг: `python3 design-system/scripts/validate_product.py config.json --strict` → нужен `0/0`. + +Полные правила для сложных экранов — `design-system/AGENTS.md`, список компонентов — +`design-system/COMPONENTS.md`, приёмка — `design-system/CHECKLIST.md`. Читай их, когда лендинга +уже мало. + +--- + +## 📦 Готовые рецепты ### Фото без ключей -- Детерминированные (одно и то же слово → одна и та же картинка): `https://picsum.photos/seed/coffee/800/600`. Меняй `coffee` на любое слово-seed. -- Тематические (по тегу): `https://loremflickr.com/800/600/coffee` — тег прямо в URL (`/coffee`, `/mountains`, `/food`). Тема не гарантируется на 100%, но обычно в кассу. +- Одно и то же слово → одна и та же картинка: `https://picsum.photos/seed/coffee/800/600` +- По теме: `https://loremflickr.com/800/600/coffee` (тег прямо в URL) + +Вставляй с рамкой ДС: `…` ### Кнопка WhatsApp ```html -Написать в WhatsApp +Написать в WhatsApp ``` -`7XXXXXXXXXX` — номер в межд. формате без `+`. `text=` — предзаполненное сообщение. +`7XXXXXXXXXX` — номер в международном формате без `+`. ### Форма заявки без бэкенда ```html -
- - - + +
+
+
``` -Скажи юзеру: замени `EMAIL` на свою почту — заявки будут приходить туда (первая заявка активирует, письмо-подтверждение придёт один раз). +Скажи юзеру: замени `EMAIL` на свою почту — заявки будут приходить туда (первая заявка активирует форму). -### Google Fonts (максимум 2 шрифта: один display + один text) -```html - - -``` -В CSS: `font-family:'Unbounded',sans-serif` для заголовков, `'Manrope',sans-serif` для текста. - -### Favicon-эмодзи (inline SVG data-URI, без файла) +### Favicon-эмодзи (без файла) ```html ``` -Меняй эмодзи `☕` на подходящее теме. -### Карта (Google Maps embed, без ключа) -На maps.google.com найди место → «Поделиться» → «Встроить карту» → скопируй iframe. Вставь как есть: +### Карта +На maps.google.com: место → «Поделиться» → «Встроить карту» → вставь iframe как есть: ```html - + ``` -### Плавные появления при скролле (IntersectionObserver) +### Плавные появления при скролле ```html -
- -
- - -
+
-
-
-

Социальная поддержка для людей с инвалидностью

-

Единый портал государственной и благотворительной помощи. Узнайте свои права, подайте заявку и получите поддержку в одном месте.

- -
- -
+

Анализ данных онлайн

+

Загрузите Excel или CSV файл — получите готовый отчёт с графиками и выводами за секунды

-
-

Получателям помощи

-

Узнайте, на какую поддержку вы имеете право, и подайте заявку онлайн

+
+

Загрузка файла

-
-
- -

Пройдите тест

-

Ответьте на 10 вопросов о вашей ситуации и узнайте все доступные виды поддержки

- -
-
- -

Получите список

-

Система покажет льготы, выплаты и услуги, которые вам положены по закону

-
-
- -

Подайте заявки

-

Оформите документы онлайн или запишитесь в центр соцзащиты

-
+
+ +
Перетащите файл сюда
+
или нажмите, чтобы выбрать (.xlsx, .xls, .csv)
+
- -
-
- -
-
-
-
-

Благотворителям

-

Помогайте людям с инвалидностью напрямую и прозрачно

- -
-
- -

Целевая помощь

-

Выберите конкретный проект или человека и помогите именно тем, что нужно

-
-
- -

Полная прозрачность

-

Отчёты о каждом потраченном тенге. Фото и видео результатов

-
-
- -

Личное участие

-

Станьте наставником или волонтёром. Помогайте не только деньгами

-
+
+
+
-Выбрать проект -
-
-
-
847
-
людей получили помощь
-
-
-
2.3M ₸
-
собрано в этом месяце
-
-
-
156
-
активных проектов
-
-
-
100%
-
прозрачность
-
-
-
-
-
- -
-
-

Виды поддержки

-

Государственная и благотворительная помощь в одном месте

- -
-
-
Государственная
- -

Денежные выплаты

-
    -
  • Пенсия по инвалидности
  • -
  • Ежемесячные выплаты (ЕДВ)
  • -
  • Компенсации по уходу
  • -
  • Материальная помощь
  • -
-Подробнее → -
- -
-
Государственная
- -

Жильё и коммунальные услуги

-
    -
  • Субсидии на оплату ЖКУ
  • -
  • Программы доступного жилья
  • -
  • Адаптация жилья
  • -
  • Льготы по капремонту
  • -
-Подробнее → -
- -
-
Государственная
- -

Транспорт и парковка

-
    -
  • Бесплатный проезд
  • -
  • Парковочные разрешения
  • -
  • Компенсация проезда к месту лечения
  • -
  • Льготы на такси
  • -
-Подробнее → -
- -
-
Государственная
- -

Медицина и реабилитация

-
    -
  • Бесплатные лекарства
  • -
  • Санаторно-курортное лечение
  • -
  • Протезирование
  • -
  • Реабилитационные программы
  • -
-Подробнее → -
- -
-
Государственная
- -

Образование и работа

-
    -
  • Бесплатное образование
  • -
  • Квоты при трудоустройстве
  • -
  • Профпереподготовка
  • -
  • Специальные рабочие места
  • -
-Подробнее → -
- -
-
Государственная
- -

Технические средства (ТСР)

-
    -
  • Коляски и протезы
  • -
  • Слуховые аппараты
  • -
  • Средства для самообслуживания
  • -
  • Специальная одежда и обувь
  • -
-Подробнее → -
- -
-
Благотворительная
- -

Адресная помощь

-
    -
  • Сбор на лечение
  • -
  • Покупка техники реабилитации
  • -
  • Оплата услуг сиделки
  • -
  • Экстренная помощь
  • -
-Поддержать → -
- -
-
Благотворительная
- -

Волонтёрская поддержка

-
    -
  • Сопровождение и наставничество
  • -
  • Помощь по хозяйству
  • -
  • Транспортное волонтёрство
  • -
  • Юридические консультации
  • -
-Стать волонтёром → -
-
-
-
- -
-
-

Активные проекты

-

Выберите проект, которому хотите помочь

- -
-
-
-Коляска для спорта -
Срочно
-
-
-
-Техническая помощь -Осталось 12 дней -
-

Спортивная коляска для Айдина

-

Айдин мечтает заниматься баскетболом на колясках. Нужна специализированная спортивная коляска.

-
-
-
-
-
-130 000 ₸ -из 200 000 ₸ -
-
-
-Семья Айдина -Семья Айдина -
- -
-
- -
-
-Улыбающийся ребёнок -
Важно
-
-
-
-Лечение -Осталось 25 дней -
-

Курс реабилитации для Дины

-

Дине 8 лет, ДЦП. Ежегодный курс реабилитации поможет закрепить навыки ходьбы.

-
-
-
-
-
-420 000 ₸ -из 1 000 000 ₸ -
-
-
-Мама с Диной -Мама Дины -
- -
-
- -
-
-Пожилые люди дома -
Госпрограмма
-
-
-
-Жильё -Постоянно -
-

Адаптация жилья для пожилых

-

Установка поручней, пандусов и противоскользящих покрытий для людей старше 65 лет.

-
-
-
-
-
-780 000 ₸ -из 1 000 000 ₸ -
-
-
-Волонтёры -Центр соцзащиты -
- -
-
-
- -Показать все проекты -
-
- -
-
-

Частые вопросы

- -
-
- -
-

Пройдите наш тест в разделе «Получателям» — он займёт 5 минут и покажет все доступные виды поддержки. Также можно обратиться в центр социальной защиты по месту жительства.

-
-
-
- -
-

Большинство заявок рассматривается от 5 до 30 дней. Срочная благотворительная помощь может быть оказана в течение 1-3 дней.

-
-
+ -
- -
-

Нет, все государственные услуги бесплатны. Благотворительные взносы идут напрямую получателям, комиссия платформы — 0%.

-
-
- -
- -
-

Государственная поддержка доступна гражданам РК и оралманам. Благотворительная помощь может быть оказана любому человеку, находящемуся на территории Казахстана.

-
-
- -
- -
-

Заполните анкету волонтёра в разделе «Благотворителям». Мы свяжемся с вами в течение 3 дней и подберём подходящую форму участия.

-
-
-
+
+ +
Анализируем данные...
-
-
-
-

Нужна помощь или хотите помочь?

-

Мы соединяем тех, кто нуждается, с теми, кто может помочь. Начните прямо сейчас.

-
-Получить поддержку -Стать частью команды +
+

📈 Результаты анализа

+ +
+

💡 Ключевые выводы

+
+
+ +

📊 Статистика

+
+ +

📉 Графики

+
+ +

📋 Данные

+
+
+
+ +
+

💾 Экспорт отчёта

+ + +
+
- - - - - - - - + diff --git a/script.js b/script.js index 6fe49d9..907f851 100644 --- a/script.js +++ b/script.js @@ -1,258 +1,510 @@ -document.addEventListener('DOMContentLoaded', function() { - const reveals = document.querySelectorAll('.reveal'); - const faqItems = document.querySelectorAll('.faq-item'); - const sizeBtns = document.querySelectorAll('.size-btn'); - const contrastBtns = document.querySelectorAll('.contrast-btn'); - const amountBtns = document.querySelectorAll('.amount-btn'); - const customAmount = document.getElementById('custom-amount'); +let workbook = null; +let worksheet = null; +let rawData = []; +let fileName = ''; - function checkReveal() { - const triggerBottom = window.innerHeight * 0.85; - reveals.forEach(reveal => { - const boxTop = reveal.getBoundingClientRect().top; - if (boxTop < triggerBottom) { - reveal.classList.add('in'); - } - }); - } +const uploadArea = document.getElementById('upload-area'); +const fileInput = document.getElementById('file-input'); +const fileInfo = document.getElementById('file-info'); +const file_name = document.getElementById('file-name'); +const fileMeta = document.getElementById('file-meta'); +const analyzeBtn = document.getElementById('analyze-btn'); +const errorMessage = document.getElementById('error-message'); +const loading = document.getElementById('loading'); +const results = document.getElementById('results'); - window.addEventListener('scroll', checkReveal); - checkReveal(); +uploadArea.addEventListener('click', () => fileInput.click()); - faqItems.forEach(item => { - const question = item.querySelector('.faq-question'); - question.addEventListener('click', function() { - const isExpanded = this.getAttribute('aria-expanded') === 'true'; - faqItems.forEach(i => { - i.classList.remove('active'); - i.querySelector('.faq-question').setAttribute('aria-expanded', 'false'); - }); - if (!isExpanded) { - item.classList.add('active'); - this.setAttribute('aria-expanded', 'true'); - } - }); - }); - - sizeBtns.forEach(btn => { - btn.addEventListener('click', function() { - sizeBtns.forEach(b => b.classList.remove('active')); - this.classList.add('active'); - document.body.classList.remove('text-normal', 'text-large', 'text-xlarge'); - document.body.classList.add('text-' + this.dataset.size); - }); - }); - - contrastBtns.forEach(btn => { - btn.addEventListener('click', function() { - contrastBtns.forEach(b => b.classList.remove('active')); - this.classList.add('active'); - document.body.classList.remove('contrast-normal', 'contrast-high'); - if (this.dataset.contrast === 'high') { - document.body.classList.add('contrast-high'); - } - }); - }); - - amountBtns.forEach(btn => { - btn.addEventListener('click', function() { - amountBtns.forEach(b => b.classList.remove('active')); - this.classList.add('active'); - if (customAmount) customAmount.value = ''; - }); - }); - - if (customAmount) { - customAmount.addEventListener('input', function() { - amountBtns.forEach(b => b.classList.remove('active')); - }); +uploadArea.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + fileInput.click(); } }); -function openModal(modalId) { - const modal = document.getElementById(modalId); - if (modal) { - modal.removeAttribute('hidden'); - modal.setAttribute('aria-hidden', 'false'); - document.body.style.overflow = 'hidden'; - const focusable = modal.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'); - if (focusable) focusable.focus(); - - modal.addEventListener('keydown', function(e) { - if (e.key === 'Escape') { - closeModal(modalId); - } - if (e.key === 'Tab') { - const focusableElements = modal.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'); - const first = focusableElements[0]; - const last = focusableElements[focusableElements.length - 1]; - if (e.shiftKey && document.activeElement === first) { - e.preventDefault(); - last.focus(); - } else if (!e.shiftKey && document.activeElement === last) { - e.preventDefault(); - first.focus(); - } - } - }); +uploadArea.addEventListener('dragover', (e) => { + e.preventDefault(); + uploadArea.classList.add('dragover'); +}); + +uploadArea.addEventListener('dragleave', () => { + uploadArea.classList.remove('dragover'); +}); + +uploadArea.addEventListener('drop', (e) => { + e.preventDefault(); + uploadArea.classList.remove('dragover'); + const files = e.dataTransfer.files; + if (files.length > 0) { + handleFile(files[0]); } -} +}); -function closeModal(modalId) { - const modal = document.getElementById(modalId); - if (modal) { - modal.setAttribute('hidden', ''); - modal.setAttribute('aria-hidden', 'true'); - document.body.style.overflow = ''; +fileInput.addEventListener('change', (e) => { + if (e.target.files.length > 0) { + handleFile(e.target.files[0]); } -} +}); -let currentStep = 1; -const totalSteps = 10; - -function showStep(step) { - const steps = document.querySelectorAll('.form-step:not(.result-step)'); - steps.forEach((s, index) => { - s.classList.remove('active'); - if (index === step - 1) { - s.classList.add('active'); - } - }); - currentStep = step; -} - -function nextStep(current) { - const currentStepEl = document.querySelectorAll('.form-step:not(.result-step)')[current - 1]; - const inputs = currentStepEl.querySelectorAll('input[type="radio"], input[type="checkbox"]'); - let answered = false; - inputs.forEach(input => { - if (input.checked) answered = true; - }); +function handleFile(file) { + const validTypes = [ + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'application/vnd.ms-excel', + 'text/csv', + 'application/vnd.ms-excel.sheet.macroEnabled.12' + ]; - if (!answered) { - alert('Пожалуйста, выберите вариант ответа'); + const ext = file.name.split('.').pop().toLowerCase(); + const isValid = validTypes.includes(file.type) || + ['xlsx', 'xls', 'csv'].includes(ext); + + if (!isValid) { + showError('Неподдерживаемый формат файла. Загрузите .xlsx, .xls или .csv'); return; } - if (current < totalSteps) { - showStep(current + 1); - } + fileName = file.name; + file_name.textContent = '📄 ' + file.name; + fileMeta.textContent = 'Размер: ' + formatFileSize(file.size); + fileInfo.classList.add('visible'); + uploadArea.classList.add('has-file'); + analyzeBtn.disabled = false; + hideError(); + results.classList.remove('visible'); + + const reader = new FileReader(); + reader.onload = (e) => { + try { + const data = new Uint8Array(e.target.result); + workbook = XLSX.read(data, { type: 'array' }); + + if (workbook.SheetNames.length > 0) { + worksheet = workbook.Sheets[workbook.SheetNames[0]]; + rawData = XLSX.utils.sheet_to_json(worksheet, { defval: '' }); + + if (rawData.length === 0) { + showError('Файл пуст или не содержит данных'); + analyzeBtn.disabled = true; + return; + } + + analyzeBtn.disabled = false; + } + } catch (err) { + showError('Ошибка чтения файла: ' + err.message); + analyzeBtn.disabled = true; + } + }; + reader.onerror = () => { + showError('Ошибка при чтении файла'); + analyzeBtn.disabled = true; + }; + reader.readAsArrayBuffer(file); } -function prevStep(current) { - if (current > 1) { - showStep(current - 1); +analyzeBtn.addEventListener('click', () => { + if (rawData.length === 0) { + showError('Нет данных для анализа'); + return; } + + loading.classList.add('visible'); + results.classList.remove('visible'); + + setTimeout(() => { + try { + performAnalysis(); + loading.classList.remove('visible'); + results.classList.add('visible'); + results.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } catch (err) { + loading.classList.remove('visible'); + showError('Ошибка анализа: ' + err.message); + } + }, 500); +}); + +function performAnalysis() { + const columns = Object.keys(rawData[0]); + const stats = calculateStats(rawData, columns); + const insights = generateInsights(rawData, columns, stats); + + renderStats(stats); + renderInsights(insights); + renderCharts(rawData, columns, stats); + renderTable(rawData, columns); } -function submitTest() { - const resultsDiv = document.getElementById('test-results'); - const answers = { - disability: document.querySelector('input[name="disability"]:checked'), - work: document.querySelector('input[name="work"]:checked'), - location: document.querySelector('input[name="location"]:checked'), - income: document.querySelector('input[name="income"]:checked'), - housing: document.querySelector('input[name="housing"]:checked'), - tsr: document.querySelectorAll('input[name="tsr"]:checked'), - employment: document.querySelector('input[name="employment"]:checked'), - children: document.querySelector('input[name="children"]:checked'), - legal: document.querySelector('input[name="legal"]:checked'), - volunteer: document.querySelector('input[name="volunteer"]:checked') +function calculateStats(data, columns) { + const stats = { + totalRows: data.length, + totalColumns: columns.length, + columns: columns, + emptyCells: 0, + numericColumns: [], + textColumns: [], + dateColumns: [], + columnStats: {} }; - let results = []; - - if (answers.disability && answers.disability.value !== 'none') { - results.push({ - title: 'Пенсия по инвалидности', - desc: 'Положена всем гражданам с инвалидностью. Размер зависит от группы.' + columns.forEach(col => { + const values = data.map(row => row[col]).filter(v => v !== '' && v !== null && v !== undefined); + const emptyCount = data.length - values.length; + stats.emptyCells += emptyCount; + + const firstValue = values[0]; + const isNumeric = values.every(v => !isNaN(parseFloat(v)) && isFinite(v)); + const isDate = values.some(v => { + const d = new Date(v); + return !isNaN(d.getTime()); }); - results.push({ - title: 'Ежемесячная выплата (ЕДВ)', - desc: 'Дополнительная выплата для отдельных категорий.' - }); - } - - if (answers.income && answers.income.value === 'low') { - results.push({ - title: 'Материальная помощь', - desc: 'Единовременная выплата в трудной жизненной ситуации.' - }); - } - - if (answers.housing && answers.housing.value === 'yes') { - results.push({ - title: 'Субсидия на оплату ЖКУ', - desc: 'Компенсация до 50% расходов на коммунальные услуги.' - }); - results.push({ - title: 'Программа доступного жилья', - desc: 'Постановка в очередь на социальное жильё.' - }); - } - - if (answers.tsr && answers.tsr.length > 0) { - results.push({ - title: 'Технические средства реабилитации', - desc: 'Бесплатное получение колясок, протезов и других ТСР.' - }); - } - - if (answers.employment && answers.employment.value === 'yes') { - results.push({ - title: 'Содействие в трудоустройстве', - desc: 'Квоты для людей с инвалидностью, программы переобучения.' - }); - } - - if (answers.volunteer && answers.volunteer.value !== 'no') { - results.push({ - title: 'Волонтёрская поддержка', - desc: 'Помощь волонтёров в быту, сопровождении, транспорте.' - }); - } - - if (answers.children && answers.children.value === 'yes') { - results.push({ - title: 'Пособие на детей', - desc: 'Дополнительные выплаты на детей в семье.' - }); - } - - if (answers.legal && answers.legal.value === 'yes') { - results.push({ - title: 'Юридическая помощь', - desc: 'Бесплатные консультации юриста по вопросам инвалидности.' - }); - } - - if (results.length === 0) { - results.push({ - title: 'Обратитесь в центр соцзащиты', - desc: 'Специалисты подберут индивидуальную программу поддержки.' - }); - } - - resultsDiv.innerHTML = results.map(r => - '

' + r.title + '

' + r.desc + '

' - ).join(''); - - const steps = document.querySelectorAll('.form-step:not(.result-step)'); - steps.forEach(s => s.classList.remove('active')); - document.querySelector('.result-step').classList.add('active'); -} - -function resetTest() { - const form = document.getElementById('test-form'); - form.reset(); - showStep(1); -} - -if (window.location.hash) { - setTimeout(function() { - const element = document.querySelector(window.location.hash); - if (element) { - element.scrollIntoView({ behavior: 'smooth' }); + + if (isNumeric) { + stats.numericColumns.push(col); + const numValues = values.map(v => parseFloat(v)); + stats.columnStats[col] = { + type: 'numeric', + min: Math.min(...numValues), + max: Math.max(...numValues), + avg: numValues.reduce((a, b) => a + b, 0) / numValues.length, + sum: numValues.reduce((a, b) => a + b, 0), + empty: emptyCount + }; + } else if (isDate) { + stats.dateColumns.push(col); + stats.columnStats[col] = { + type: 'date', + empty: emptyCount + }; + } else { + stats.textColumns.push(col); + const unique = [...new Set(values)]; + const counts = {}; + values.forEach(v => { counts[v] = (counts[v] || 0) + 1; }); + const sorted = Object.entries(counts).sort((a, b) => b[1] - a[1]); + stats.columnStats[col] = { + type: 'text', + unique: unique.length, + top: sorted.slice(0, 5), + empty: emptyCount + }; } - }, 100); + }); + + return stats; +} + +function generateInsights(data, columns, stats) { + const insights = []; + + insights.push({ + icon: '📊', + text: `Всего записей: ${stats.totalRows} в ${stats.totalColumns} колонках` + }); + + if (stats.numericColumns.length > 0) { + insights.push({ + icon: '💰', + text: `Числовые колонки: ${stats.numericColumns.join(', ')}` + }); + + stats.numericColumns.forEach(col => { + const s = stats.columnStats[col]; + if (s && s.type === 'numeric') { + insights.push({ + icon: '📈', + text: `${col}: среднее = ${formatNumber(s.avg)}, мин = ${formatNumber(s.min)}, макс = ${formatNumber(s.max)}` + }); + } + }); + } + + if (stats.textColumns.length > 0) { + insights.push({ + icon: '🏷️', + text: `Текстовые колонки: ${stats.textColumns.join(', ')}` + }); + + stats.textColumns.forEach(col => { + const s = stats.columnStats[col]; + if (s && s.type === 'text' && s.top.length > 0) { + const topList = s.top.map(item => item[0]).slice(0, 3).join(', '); + insights.push({ + icon: '🔝', + text: `${col}: чаще всего встречается "${topList}"` + }); + } + }); + } + + const emptyPercent = ((stats.emptyCells / (stats.totalRows * stats.totalColumns)) * 100).toFixed(1); + if (emptyPercent > 10) { + insights.push({ + icon: '⚠️', + text: `Внимание: ${emptyPercent}% пустых ячеек — проверьте полноту данных` + }); + } + + return insights; +} + +function renderStats(stats) { + const grid = document.getElementById('stats-grid'); + grid.innerHTML = ` +
+
${stats.totalRows}
+
Всего записей
+
+
+
${stats.totalColumns}
+
Колонок
+
+
+
${stats.numericColumns.length}
+
Числовых полей
+
+
+
${formatNumber(stats.emptyCells)}
+
Пустых ячеек
+
+ `; +} + +function renderInsights(insights) { + const list = document.getElementById('insights-list'); + list.innerHTML = insights.map(i => ` +
+
${i.icon}
+
${i.text}
+
+ `).join(''); +} + +let charts = []; + +function renderCharts(data, columns, stats) { + const container = document.getElementById('charts-section'); + container.innerHTML = ''; + + charts.forEach(chart => chart.destroy()); + charts = []; + + if (stats.numericColumns.length >= 2) { + const chartCard = document.createElement('div'); + chartCard.className = 'chart-card'; + chartCard.innerHTML = ` +

Распределение числовых данных

+
+ +
+ `; + container.appendChild(chartCard); + + const ctx = document.getElementById('barChart').getContext('2d'); + const chart = new Chart(ctx, { + type: 'bar', + data: { + labels: stats.numericColumns.slice(0, 10), + datasets: [{ + label: 'Среднее значение', + data: stats.numericColumns.slice(0, 10).map(col => + stats.columnStats[col] ? stats.columnStats[col].avg : 0 + ), + backgroundColor: 'rgba(99, 102, 241, 0.6)', + borderColor: 'rgba(99, 102, 241, 1)', + borderWidth: 2 + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + display: true + } + }, + scales: { + y: { + beginAtZero: true + } + } + } + }); + charts.push(chart); + } + + if (stats.textColumns.length > 0 && stats.textColumns.some(col => + stats.columnStats[col] && stats.columnStats[col].unique <= 10 + )) { + const textCol = stats.textColumns.find(col => + stats.columnStats[col] && stats.columnStats[col].unique <= 10 + ); + + if (textCol) { + const s = stats.columnStats[textCol]; + const chartCard = document.createElement('div'); + chartCard.className = 'chart-card'; + chartCard.innerHTML = ` +

Распределение: ${textCol}

+
+ +
+ `; + container.appendChild(chartCard); + + const ctx = document.getElementById('pieChart').getContext('2d'); + const chart = new Chart(ctx, { + type: 'pie', + data: { + labels: s.top.map(item => item[0]), + datasets: [{ + data: s.top.map(item => item[1]), + backgroundColor: [ + 'rgba(99, 102, 241, 0.8)', + 'rgba(16, 185, 129, 0.8)', + 'rgba(245, 158, 11, 0.8)', + 'rgba(239, 68, 68, 0.8)', + 'rgba(139, 92, 246, 0.8)' + ] + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: 'bottom' + } + } + } + }); + charts.push(chart); + } + } + + if (stats.numericColumns.length >= 1 && data.length <= 100) { + const chartCard = document.createElement('div'); + chartCard.className = 'chart-card'; + chartCard.innerHTML = ` +

Динамика данных

+
+ +
+ `; + container.appendChild(chartCard); + + const firstNumeric = stats.numericColumns[0]; + const values = data.slice(0, 50).map((_, i) => + data[i] && data[i][firstNumeric] ? parseFloat(data[i][firstNumeric]) : 0 + ); + + const ctx = document.getElementById('lineChart').getContext('2d'); + const chart = new Chart(ctx, { + type: 'line', + data: { + labels: values.map((_, i) => i + 1), + datasets: [{ + label: firstNumeric, + data: values, + borderColor: 'rgba(16, 185, 129, 1)', + backgroundColor: 'rgba(16, 185, 129, 0.1)', + tension: 0.4, + fill: true + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + display: true + } + }, + scales: { + y: { + beginAtZero: false + } + } + } + }); + charts.push(chart); + } +} + +function renderTable(data, columns) { + const table = document.getElementById('data-table'); + const maxRows = Math.min(100, data.length); + + let html = ''; + columns.forEach(col => { + html += '' + escapeHtml(col) + ''; + }); + html += ''; + + for (let i = 0; i < maxRows; i++) { + html += ''; + columns.forEach(col => { + html += '' + escapeHtml(data[i][col]) + ''; + }); + html += ''; + } + + if (data.length > maxRows) { + html += ''; + html += '... показано 100 из ' + data.length + ' записей ...'; + html += ''; + } + + html += ''; + table.innerHTML = html; +} + +function exportToCSV() { + if (!rawData || rawData.length === 0) return; + + const ws = XLSX.utils.json_to_sheet(rawData); + const csv = XLSX.utils.sheet_to_csv(ws); + const blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8;' }); + const link = document.createElement('a'); + link.href = URL.createObjectURL(blob); + link.download = fileName.replace(/\.[^/.]+$/, '') + '_analysis.csv'; + link.click(); +} + +function exportToExcel() { + if (!rawData || rawData.length === 0) return; + + const wb = XLSX.utils.book_new(); + const ws = XLSX.utils.json_to_sheet(rawData); + XLSX.utils.book_append_sheet(wb, ws, 'Data'); + XLSX.writeFile(wb, fileName.replace(/\.[^/.]+$/, '') + '_analysis.xlsx'); +} + +function showError(msg) { + errorMessage.textContent = msg; + errorMessage.classList.add('visible'); +} + +function hideError() { + errorMessage.classList.remove('visible'); +} + +function formatFileSize(bytes) { + if (bytes < 1024) return bytes + ' Б'; + if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' КБ'; + return (bytes / 1048576).toFixed(1) + ' МБ'; +} + +function formatNumber(num) { + if (typeof num !== 'number') return num; + if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M'; + if (num >= 1000) return (num / 1000).toFixed(1) + 'K'; + return Math.round(num * 100) / 100; +} + +function escapeHtml(text) { + if (text === null || text === undefined) return ''; + const div = document.createElement('div'); + div.textContent = String(text); + return div.innerHTML; }