Commit Graph

49 Commits

Author SHA1 Message Date
wasrusgen
5fafdc35fb feat: replace hand-drawn role icons with Tabler Icons (MIT)
Manager: user-star (person + star badge)
Client: home-2 (house with square window)
Staff: tools (wrench + screwdriver crossed)
Source: tabler-icons.io, MIT license, stroke=#6B4A2B

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 11:53:19 +03:00
wasrusgen
35c3c3f440 feat: replace geometric role icons with pencil-sketch SVG illustrations
Manager: suit, lapels, tie, pocket square, face with hair/eyes/nose
Client: house with chimney+smoke, pane windows, paneled door, garden bush
Staff: dark hard hat, face with stubble+brows, work jacket, wrench in hand
All use feTurbulence displacement filter for hand-drawn line wobble
and diagonal hatching lines for shadow/volume.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 11:19:12 +03:00
wasrusgen
dfba5899bd feat: replace role-chooser emoji with Editorial Calm SVG pictograms
- Manager: person with V-collar and tie (professional silhouette)
- Client: house outline with roof, door, windows and door knob
- Staff: hard-hat person silhouette with wrench in hand
- Bump cache version to 20260516g across all assets

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 09:59:49 +03:00
wasrusgen
22dbbed112 feat: AI contract review for clients (#/c/contract)
Backend:
- main.py: _handle_contract_review() — GigaChat analyzes
  contract text, returns structured JSON (summary, payment,
  deadlines, risks, recommendations, missing_clauses)
  with optional targeted question support
- /api/contract_review route (async, thread-pool)
- CONTRACT_SYSTEM prompt: plain-language analysis in Russian,
  risk levels high/medium/low, missing-clause detection

Frontend:
- proposals.js: Proposals.mountContractReview(container)
  · Textarea for contract text with char counter (16k limit)
  · 5 preset quick-questions as tappable chips
  · Free-form question input
  · Thinking animation while AI processes
  · Renders structured analysis: Summary, Payment,
    Deadlines, Risks (color-coded), Recommendations,
    Missing clauses, disclaimer footer
  · Falls back to raw text if AI returns unstructured reply
- app.js: #/c/contract route in init() and routeByHash();
  «Калькулятор бюджета» replaced by «Проверить договор»
  in client home menu (active, not «скоро»)
- podbor.css: ~180 lines of contract review styles
  (cr-* classes, risk colors, thinking animation)
- index.html: cache version → 20260516f

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 09:35:29 +03:00
wasrusgen
4abd7b2ecd feat: proposal cycle — client brief + manager editor + voting
Backend:
- proposals.py: new module with full proposal cycle
  (brief → draft → sent → reviewed → done),
  Google Sheets «Proposals» tab, Telegram notifications
- main.py: import proposals_mod, 9 new /api/proposal_* routes
  added to both dispatch map and native /api/* handlers

Frontend:
- proposals.js: self-contained Proposals module
  · Client: brief form (6 items + budget + notes),
    waiting screen, proposal view with / per variant,
    overall comment + submit
  · Manager: empty state → create, editor with categories,
    add-variant form, send button, client votes/feedback view
- clients.js: «Подбор техники» button now opens proposals
  editor page (#/clients/client/{key}/proposals); inline
  Proposals.mountManager() section added to client card;
  new renderClientProposalsPage() route handler
- app.js: #/c/proposal route for client side; client home
  «Подобрать технику» menu item activated (was «скоро»)
- podbor.css: ~350 lines of Proposals UI styles
- index.html: proposals.js added, cache version → 20260516e

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 09:28:36 +03:00
wasrusgen
1b8f70e44a fix: 3 bugs found in audit
1. _xlsx_auth_manager: возвращал (tg_id, user) при успехе → callers делали
   `if err: return err` и возвращали dict пользователя вместо данных.
   Исправлено: возвращает (tg_id, None) при успешной авторизации.

2. Promise.all с 4 запросами: ошибка Drive (сервис-аккаунт не добавлен к файлу)
   роняла весь дашборд. Исправлено: складские запросы изолированы в отдельный
   .then/.catch — дашборд замеров отрисовывается независимо.

3. Секции склада теперь появляются с задержкой (non-blocking), а не блокируют
   отрисовку главного экрана.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 08:28:18 +03:00
wasrusgen
e97c84e126 fix: BACKEND_URL → api.wasrusgen1.pro (cloudflare tunnel истёк)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 07:50:53 +03:00
wasrusgen
cc38782b85 feat: arrivals module + refactor xlsx parser
- Добавлен /api/arrivals для «Поступление заказов на склад СПб.xlsx»
  (Drive ID захардкожен в ARRIVALS_FILE_ID, дефолт = 1kgrDEIGcVMFnSdZs1Y...)
- _parse_xlsx_groups() — единый парсер для обоих файлов:
  * находит строку заголовков динамически (первая строка с «Товар»),
    чтобы корректно работать с файлом «Поступление» (2 строки шапки перед хедером)
  * пропускает разделители «Кухни» / «Дозаказы» внутри листа
  * пропускает шаблонные пустые строки (Заказ/Дозаказ без данных)
- _xlsx_auth_manager() — вынесена общая проверка initData + роль
- config: поле arrivals_file_id
- frontend: вторая секция «📥 Поступление в СПб» на дашборде менеджера;
  renderManagerShipments принимает label-параметр и переиспользуется для обоих файлов;
  оба запроса загружаются параллельно с измерениями

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 07:49:56 +03:00
wasrusgen
f5ee9e5b33 feat: warehouse module — ОТГРУЗКИ.xlsx в дашборде менеджера
- backend: новый модуль drive.py (Google Drive download + 5-мин кэш)
- backend: /api/shipments — читает xlsx из Drive, парсит листы «ЗОВ ДД.ММ.ГГ»,
  возвращает позиции (Заказ/Дозаказ) сгруппированные по дате отгрузки с завода
- config: поле shipments_file_id (SHIPMENTS_FILE_ID env; дефолт = ID ОТГРУЗКИ.xlsx)
- frontend: секция «📦 Отгрузки» на главной менеджера (после активных проектов),
  загружается параллельно с замерами и pending; показывает последние 3 партии
- CSS: стили .ship-group / .ship-row / .ship-badge / .ship-check
- deps: добавлен openpyxl>=3.1.0

ВАЖНО после деплоя: добавить сервис-аккаунт как Viewer к ОТГРУЗКИ.xlsx в Drive
и прописать SHIPMENTS_FILE_ID в /opt/zov-tech/deploy/.env на сервере.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 07:21:23 +03:00
wasrusgen
52eb0e4a96 Phase 4 stage 1: Сборки — модель + создание + список
Backend:
- sheets.is_master(user) — единая роль measurer ∨ assembler
- grant_role() автоматически выдаёт парную роль (measurer ↔ assembler)
- Новая таблица Assemblies со схемой: client, scope, scheduled_at,
  status (created|scheduled|in_progress|completed|cancelled),
  photos_before/in_progress/after, signature_file, gcal_event_id
- POST /api/assembly_create — менеджер заводит сборку,
  при scheduled_at создаётся событие Google Calendar (4 часа)
- POST /api/assembly_list — фильтр по роли: менеджер видит свои,
  мастер — назначенные + неназначенные (created/scheduled)
- POST /api/assembly_detail — карточка с правами доступа
- /api/photo: добавил MIME для pdf/dwg/dxf (для DWG-блока B+E)

Frontend (assembly.js — новый модуль):
- Форма /api/assembly_create с валидацией: имя, адрес, scope
- Pre-fill из карточки клиента (sessionStorage.prefillAssembly,
  адрес + measurement_id из последнего замера)
- Список сборок + детальная карточка со статусом и составом работ
- Маршруты: #/assembly, #/assembly/new, #/assembly/<id>

Frontend (app.js + clients.js):
- Кнопка «🔨 Заказать сборку» в карточке клиента
- Quick-action «Сборки» на главной менеджера
- Блок «🔨 Сборки» в кабинете мастера (caps.measurer ∨ assembler)

CSS: .assembly-card / .assembly-card-* (золотой бордер)
index.html: cache bump v=20260514c
2026-05-14 09:53:40 +03:00
wasrusgen
5e6746e676 B+E: DWG/чертежи + карточка «Замер готов — нужен подбор?»
Backend (main.py):
- Колонки в Measurements: design_files, podbor_decision, podbor_decision_at, podbor_lead_id
- _save_design_file() сохраняет DWG/DXF/PDF/PNG/JPG в PHOTOS_DIR/<id>/design_*.ext
- POST /api/measurement_design_upload — загрузка чертежей (до 10 файлов, 30МБ каждый)
- POST /api/measurement_decision — фиксация решения (needed|not_needed|later|done)
- POST /api/manager_pending — список завершённых замеров без решения про подбор
- /api/photo расширен MIME-типами pdf/dwg/dxf
- /api/measurement_detail отдаёт design_files, podbor_decision, podbor_lead_id

Frontend (app.js + clients.js + podbor.css):
- На главной менеджера: карточки « Замеры готовы» с вопросом
  «Клиенту потребуется помощь с подбором техники?» и кнопками Да/Нет/Позже
- «Да» → переход в #/podbor с pre-fill клиента
- «Нет/Позже» → фиксация решения, анимация удаления карточки
- В карточке замера (renderMeasurement) — секция «📐 Чертёж / DWG»
  с inline-загрузкой файлов и списком прикреплённых чертежей
- index.html: cache bump v=20260514b
2026-05-14 09:17:32 +03:00
wasrusgen
e808880d8e A+B: голос в мастере замера + Google Calendar события
A — голосовой ввод заметок в мастере замера:
- Кнопка 🎤 Диктовать рядом с textarea «Заметки»
- Web Speech API ru-RU, interimResults показывает диктовку в реальном времени
- Текст накапливается + сохраняется в state
- Красная пульсация во время записи

B — Google Calendar:
- Новый модуль app/gcalendar.py — service account + Calendar API
- Создание/обновление события при /api/measurement_schedule
- 2 новые колонки в Measurements: gcal_event_id, gcal_event_url
- При ошибке (нет API/прав) — fail gracefully, лог warning
- Ссылка «📅 Открыть в Google Calendar» в карточке заявки
- В DM менеджеру при назначении — clickable ссылка на событие
- Требует env: GOOGLE_CALENDAR_ID + SA добавлен в редакторы календаря

ДОПОЛНИТЕЛЬНО — заведение клиента менеджером:
- Новый endpoint /api/client_create
- /api/clients теперь читает И Leads И Measurements (включая draft)
- UI: action card «Новый клиент» в quick-actions + кнопка
  «+ Новый клиент» в шапке списка клиентов
- Форма (ФИО / Тел / Адрес / Примечание с 🎤 диктовкой)
- После сохранения — переход в карточку клиента
- has_role проверка вместо устаревшего user.role

Cache bust v=20260513zn.
2026-05-13 23:49:20 +03:00
wasrusgen
17c0f73328 rebrand: tagline «сборщик» → «CRM» (orange uppercase stamp style)
«CRM» в курсиве смотрелось бы странно — заменил на штамп-стиль:
Inter Black 14pt uppercase, оранжевая обводка, letter-spacing
0.32em. Цветовая гамма та же (оранжевый #F08720).

Везде где было «сборщик» в подписях → теперь «CRM».

Cache bust v=20260513ze.
2026-05-13 21:40:45 +03:00
wasrusgen
c41c938a67 rebrand: ZOV → @wasrusgen1 · сборщик (твой бренд)
Брендирование как ЛИЧНЫЙ CRM Руслана Васильева (не ЗОВ).

Splash:
- Убрана inline-SVG ZOV-лого
- Добавлена иконка пилы (assets/wasrusgen-saw.png, оранжевая,
  дыхательная анимация)
- Wordmark «@WASRUSGEN1» — Inter Black 28pt, серый #4A4A4A,
  «1» в оранжевом
- Подпись «сборщик» — Caveat 32pt оранж, поворот -3° (как в твоём лого)
- Полоса прогресса теперь оранжевая

Title окна: «WASRUSGEN1 · Кабинет»
Theme-color: #F08720 (для статусной строки Telegram WebApp)

Bot:
- Menu-кнопка слева от ввода: «Кабинет» (вместо «ЗОВ»)
- Welcome /start: «@wasrusgen1 · сборщик — Рабочий кабинет Руслана Васильева»

Footer клиента: «@wasrusgen1 · сборщик» + «Кабинет от Руслана Васильева»
Meta клиента без менеджера: «@wasrusgen1 · сборщик» (вместо «ЗОВ — кухонная мебель»)

ZOV-упоминания НЕ убраны там, где это про реальный контекст
(подбор техники для кухонь ЗОВ, AI-промпт, аудитория-менеджеры ЗОВ
в роли «Сотрудник»).

Cache bust v=20260513zd.
2026-05-13 21:38:45 +03:00
wasrusgen
548b4b6177 E: главная менеджера — реальные «На сегодня» + «Срочно» + проекты
Убраны mock-данные (А.Пестова, Семья Иваниковых и пр.). Теперь
данные грузятся из /api/measurements (текущего менеджера) и
сортируются:

ПРИВЕТСТВИЕ
  «Руслан, 2 замера сегодня» / «ничего на сегодня» /
  «1 просрочка» — реактивно по фактическим замерам.

HERO (если есть)
  Первый замер сегодня — крупно: время, имя, адрес, кнопки
  «Открыть заявку» + 📞 звонок.

⚠️ СРОЧНО
  Просроченные scheduled_at в прошлом, не completed.
  Красный акцент на инбокс-картах.

📅 ЕЩЁ СЕГОДНЯ
  Все остальные замеры на сегодня (без первого, который в hero).

📞 БЕЗ ДАТЫ
  Заявки в статусе requested — менеджеру напоминает что надо
  созвониться с замерщиком/клиентом.

АКТИВНЫЕ ПРОЕКТЫ
  Последние 5 замеров по дате создания. Прогресс-бар по статусу
  (requested → scheduled → in_progress → completed). Тап →
  карточка замера #/clients/measurement/<id>.

Пусто? — карточка «Свободный день».
Cache bust v=20260513zc.
2026-05-13 19:09:59 +03:00
wasrusgen
b8d9ff937f F: кабинет замерщика — week-strip + группировка по дням + 📞 звонок
Шапка → 📅 strip недели → 📥 заявки группами.

Week strip (компакт):
- 7 дней начиная с понедельника текущей недели
- Каждый день: название (Пн/Вт/...), число, полоса загрузки (высота =
  доля от max за неделю), счётчик замеров
- Цвет полосы: 1-2 = walnut light, 3-4 = walnut, 5+ = #C0392B (перегруз)
- Сегодня — выделен walnut-рамкой + warm-фоном; прошлые дни приглушены

Группированный инбокс:
  ⚠️ Просрочено   (scheduled_at в прошлом, но не completed)
  🔥 Сегодня
  📅 Завтра
  🗓️ На неделе    (до воскресенья)
  📆 Позже
  📞 Без даты      (нужно согласовать)

Каждый ряд:
- Слева время (10:00 / Пт 15.05 14:00) — крупно, walnut, моно
- Центр: ФИО клиента + адрес (truncate)
- Справа: chevron → переход к заявке
- Зелёная кнопка 📞 — звонок в один тап (tel: ссылка)

Cache bust v=20260513zb.
2026-05-13 18:51:43 +03:00
wasrusgen
366625be66 flow: упрощённая заявка + 3 чёткие стадии у замерщика
По цепочке менеджер→замерщик→замер:

Менеджер «Заказать замер»:
  - ФИО, телефон, адрес, кому назначить
  - Одно поле «Примечание» (рекомендации по дате + особенности)
    Убраны radio-buttons specific/this_week/next_week — слишком сложно.
    Точную дату всё равно согласует замерщик с клиентом.

Замерщик в карточке заявки — 3 чёткие стадии:

  1. ЕСЛИ статус requested (дата не назначена):
     - Блок «📞 Согласовать дату с клиентом»
     - Подсказка «Позвоните клиенту и зафиксируйте»
     - datetime-local + кнопка «Назначить»

  2. ЕСЛИ статус scheduled (дата уже есть):
     - Блок «📅 Замер назначен» крупно (Newsreader 22pt italic)
     - Кнопка «Изменить дату» — разворачивает скрытую форму
     - ОСНОВНАЯ кнопка «📐 Начать замер» (большая, primary, 16pt)
     - До «Начать замер» чек-листа не видно

Чек-лист (📋 в шапке) теперь живёт ТОЛЬКО в мастере замера
(когда нажали «Начать замер»). До этого момента не отвлекает.

Backend: DM при создании заявки шлёт только примечание
(без расшифровки preferred_type).

Cache bust v=20260513z.
2026-05-13 18:12:18 +03:00
wasrusgen
effb62a1d8 geocoder: порт из проекта СЕКРЕТАРЬ + кнопка «По адресу»
backend/app/geocoder.py — Python-порт хибридного геокодера из
secretary/lib/geocoder.js: Yandex (если есть YANDEX_GEOCODER_API_KEY
в env) → fallback OSM Nominatim (бесплатный, rate-limit 1/sec).

normalize_address — та же логика, что и в JS-версии: расшифровка
сокращений улиц, срез номера квартиры/этажа/подъезда, корпус → к<N>.

POST /api/geocode — текст адреса → {lat, lng, formatted, source}.

Frontend в логистике замера:
- Новая кнопка «🔍 По адресу» — берёт текст адреса заявки и
  вызывает геокодер. Заполняет GPS автоматически.
- В сводке ссылка на 📍 теперь ведёт на Я.Карты (а не Google) —
  для России лучше: открывает приложение Я.Карты на телефоне.

Cache bust v=20260513w.
2026-05-13 17:55:34 +03:00
wasrusgen
e2e17fd5a6 measurement logistics: подъезд, GPS, парковка, заметки для логистов
Замерщик/сборщик/менеджер при выезде на объект может дополнить
адрес деталями. Эти же данные будут видны и при сборке —
существенно облегчает планирование подъезда и парковки.

Поля:
  - Подъезд + этаж
  - GPS-координаты (с кнопкой «Сейчас» — забирает с устройства через
    navigator.geolocation, ссылка на Google Maps в сводке)
  - Парковка: бесплатная / платная / на улице / нет + текст-уточнение
  - Заметки логистики: домофон, шлагбаум, размер лифта, узкий проезд

UX:
  - В карточке заявки секция «📍 Логистика» свёрнута по умолчанию,
    показывает сводку. Кнопка «Заполнить» / «Изменить» раскрывает форму.
  - Точка-индикатор после заголовка если есть данные.
  - Сводка собирается строкой: подъезд · этаж · GPS-ссылка · парковка · заметка.

Backend:
  - 7 новых колонок в Measurements (entrance, floor, gps_lat, gps_lng,
    parking_type, parking_note, delivery_notes).
  - POST /api/measurement_logistics — точечный апдейт. Право:
    назначенный замерщик / менеджер-владелец / любой сборщик.

Cache bust v=20260513v.
2026-05-13 17:46:53 +03:00
wasrusgen
fdce3b3c64 measurement workflow: приблизительная дата от менеджера
По процедуре пользователя: менеджер при создании заявки может не
знать точной даты. Указывает диапазон, замерщик потом созванивается
с клиентом и фиксирует точную дату.

Менеджер при «Заказать замер» — радио-выбор:
  ○ Конкретная дата       (открывает date + утром/днём/вечером)
  ○ Эта неделя
  ○ Следующая неделя
  ● Согласовать с клиентом (default)
  + поле «Уточнение по времени» (свободный текст)

Замерщик в инбоксе:
  - Если scheduled_at заполнено → 📅 точная дата
  - Иначе → 🕐 приблизительная (эта неделя / след. неделя / 15.05 утром)
  + Note выводится после ·
  - В DM-уведомлении строка «Когда: …» подсказывает что согласовать

Замерщик в карточке заявки:
  - Если нет точной даты — отдельный блок « Когда удобно клиенту»
    с подсказкой «позвоните клиенту и согласуйте точную дату»
  - После назначения через datetime-input → блок исчезает

Backend: 4 новые колонки preferred_type / preferred_date /
preferred_time_of_day / preferred_note, добавлены в schema,
serialize/deserialize в request + detail + inbox.

Cache bust v=20260513u.
2026-05-13 16:21:09 +03:00
wasrusgen
e37a5e723f slogan: убран с role-chooser, остался только на splash
Внутри кабинета и функциональных экранов слоган не нужен —
там акцент на действиях. Слоган живёт только на splash,
как бренд-приветствие при загрузке.

Cache bust v=20260513t.
2026-05-13 11:32:22 +03:00
wasrusgen
7f01c1e595 role-chooser: «Сделано с душой!» каллиграфическим Caveat
Заменили kicker «ЗОВ — кухня и техника» (моно-uppercase) на
рукописный tagline «Сделано с душой!» — Caveat 34pt walnut,
лёгкий поворот -2°.

Cache bust v=20260513p.
2026-05-13 10:23:44 +03:00
wasrusgen
c9b78d9e5b fix: role chooser игнорировал hash #tgWebAppData от Telegram
При открытии MiniApp Telegram ставит location.hash = '#tgWebAppData=...'.
Старая проверка !location.hash считала это занятым роутом и пропускала
chooser. Теперь проверяем только наши роуты #/podbor / #/clients и т.п.

Cache bust v=20260513o.
2026-05-13 10:12:30 +03:00
wasrusgen
b352c4927f universal entry: bot menu button + role chooser в MiniApp
Перешли на единый универсальный паттерн вместо reply/inline-keyboard:

1. Bot menu-button — постоянная кнопка «ЗОВ» слева от input в чате
   (set_chat_menu_button с WebAppInfo). Видна на ВСЕХ платформах:
   Telegram Desktop, iOS, Android, Web. Один тап — открывает MiniApp.

2. MiniApp без ?role= в URL показывает role chooser как первый экран:
   три большие карточки [Я менеджер] [Я клиент] [Я сотрудник].
   Тап → URL получает ?role=X → re-run init() → загрузка кабинета
   с правильно подписанным initData.

Решение универсальное — не зависит от reply/inline-кнопок и их
поведения с initData на разных клиентах Telegram.

Cache bust v=20260513n.
2026-05-13 07:36:55 +03:00
wasrusgen
643acd29c5 fix: добавить пробел между label и value в карточке no-staff-role 2026-05-12 23:31:07 +03:00
wasrusgen
cb6398622b auth: fallback на initDataUnsafe для Telegram Desktop side-panel
В Telegram Desktop при открытии MiniApp в side-panel (boxed mode)
WebApp.initData приходит пустой. Backend не может проверить подпись.

Временный fallback: если initData пустой, доверяем initDataUnsafe.user
для определения роли. Action-endpoints (grant_role, measurement,
podbor) продолжают требовать подписанный initData.

Cache bust v=20260513i.
2026-05-12 21:35:51 +03:00
wasrusgen
4848b3a3ef fix: splash блокировал клики во время fade-out + сократил min-show
1. .loader.splash.hide теперь имеет pointer-events:none !important —
   во время 400мс fade splash не блокирует тапы.

2. minShow 2500мс → 1200мс — меньше ожидания, меньше шансов попасть
   в окно когда splash ещё блокирует.

Cache bust v=20260513h.
2026-05-12 20:16:59 +03:00
wasrusgen
67034e011a workflow B: заявка на замер от менеджера → инбокс замерщика → завершение
End-to-end поток:

1. Менеджер на главной тапает «Заказать замер» → #/request → форма:
   ФИО · телефон · адрес · dropdown «Кому назначить» · заметки.
   Submit → POST /api/measurement_request → строка в Measurements
   со status=requested + assigned_to_tg_id. Бот шлёт DM замерщику.

2. Замерщик открывает кабинет (?role=staff) → видит inbox с заявкой.
   Тап → #/inbox/<id> → карточка с реквизитами + поле datetime-local.
   Сохранить дату → POST /api/measurement_schedule → status=scheduled.
   Бот уведомляет менеджера.

3. В нужный день замерщик тапает «📐 Сделать замер сейчас» →
   wizard открывается в update-mode (#/measure?id=<id>), pre-fill
   client_name/phone из заявки, пропускает шаг «Клиент». После submit
   → backend обновляет ту же строку (status=completed) + DM менеджеру.

Backend changes:
- Расширена схема Measurements: assigned_to_tg_id, requested_by_tg_id,
  scheduled_at, address, client_name, client_phone (отдельные колонки).
  ensure_measurements_sheet() автоматически дополняет колонки.
- _handle_measurement переписан под 2 режима (create/update).
- 3 новые ручки: /api/measurement_request, /api/measurement_inbox,
  /api/measurement_schedule. Все с правильной проверкой ролей.
- Telegram-уведомления на каждом переходе статуса.

MiniApp:
- Новый модуль request.js — wizard заявки с dropdown замерщиков
  (грузится из /api/staff_list?role=measurer).
- renderStaff теперь грузит реальный инбокс из /api/measurement_inbox.
- renderInboxDetail — карточка заявки с datetime-picker.
- В quick-actions менеджера: «Заказать замер» (primary) +
  «Замер сейчас» (legacy direct fill).
- measurements.js поддерживает update-mode через ?id=.

Cache bust v=20260513g.
2026-05-12 20:00:16 +03:00
wasrusgen
d859e9791c roles: multi-role foundation (manager / client / measurer / assembler)
Users.role теперь хранит CSV-список ролей: 'manager,measurer'.
Парсим, добавляем, отзываем — все через sheets.parse_roles / grant_role /
revoke_role / list_users_with_role. Старые однострочные значения работают
как раньше (legacy compat).

Backend:
- /api/me возвращает roles[] (массив), role (главная для legacy-UI),
  plus capabilities {measurer, assembler} для staff
- /api/grant_role (admin-only) — добавить/отозвать роль
- /api/staff_list (manager-only) — список сотрудников по роли
  (будет использоваться в dropdown «выбрать замерщика»)
- При role=staff отдаём отдельный кабинет; если у юзера нет measurer/
  assembler — возвращаем error=no_staff_role

Bot:
- /start — 3-я reply-кнопка [🔧 Я сотрудник]. При тапе MiniApp получает
  ?role=staff и решает кабинет по capabilities.
- /whoami — сотрудник присылает свой Telegram ID, пересылает куратору
  чтобы тот выдал роль через /api/grant_role.

MiniApp:
- renderStaff() — заглушка кабинета сотрудника с шапкой (имя, аватар,
  список ролей) и пустым inbox («Пока пусто»). Если есть measurer —
  быстрая кнопка «Сделать новый замер».
- При error=no_staff_role — экран с инструкцией как получить роль.
- CSS .staff-head / .staff-no-role.

Cache bust v=20260513f.
2026-05-12 19:14:39 +03:00
wasrusgen
6d57372b0b splash: bump min display time to 2.5s
Cache bust v=20260513e.
2026-05-12 19:00:34 +03:00
wasrusgen
c767954535 splash: move out of #app + guarantee 700ms min visibility
Splash was nested inside <main id='app'> — render functions wipe
app.innerHTML before hideSplash() runs, so the splash never showed.
Moved it to a sibling of #app and made hideSplash() wait for at least
700ms of total time elapsed since page load before fade-out.

Cache bust v=20260513d.
2026-05-12 18:57:40 +03:00
wasrusgen
1ca8b3a5a1 bot: role buttons → MiniApp directly + branded splash loader
Bot: упрощён до одного шага — /start показывает 2 reply-кнопки
[👤 Я менеджер] [🏠 Я клиент], обе уже WebApp — открывают кабинет
сразу с нужным role= в query. Никаких промежуточных меню.

MiniApp: новый брендированный загрузочный экран с логотипом ZOV
(inline SVG, fill = walnut #6B4A2B), дыхательной анимацией 2.2s,
тонкой полоской прогресса и подписью «Открываем кабинет · ZOV».
Splash прячется (350мс минимум + fade-out) после рендера главного
экрана или маунта подэкрана (Podbor/Clients/Measurements).

Cache bust v=20260513c.
2026-05-12 18:54:09 +03:00
wasrusgen
b2438507c3 bot: persistent reply keyboard with WebApp buttons + info actions
Now after /start, manager sees a bottom keyboard (4 rows) for fast access:
  Row 1: 🤖 Подбор техники | 📐 Новый замер   ← WebApp
  Row 2: 👥 Мои клиенты    | 🏠 Кабинет       ← WebApp
  Row 3: ℹ️ Что умеет бот? | 📞 Куратор       ← text
  Row 4: 📋 Чек-лист встречи                  ← text

WebApp buttons jump straight to a MiniApp screen via ?go=<podbor|measure|clients>;
app.js parses ?go on load and pre-sets location.hash so the right module mounts.

Added /menu (re-show keyboard) and /hide (remove). Text buttons trigger
in-chat info responses (bot description, contact, meeting checklist).

Cache bust v=20260513b.
2026-05-12 18:37:24 +03:00
wasrusgen
10bcc75b13 measurements: kitchen layout wizard + 5 layout pictograms + profile integration
5 LAYOUT PICTOGRAMS (podbor.picts.js):
- linear: одна стена с гарнитуром
- l_shape: Г-образная, две стены с подсвеченным углом
- u_shape: П-образная, три стены
- island: линейный гарнитур + отдельный остров посередине
- peninsula: Г-образная + барная стойка-полуостров
Все в стиле D · top-down view, walnut stroke, теплые градиенты

MEASUREMENTS.JS WIZARD (5 шагов):
1. client_info — имя + телефон (валидация)
2. layout — pict-карточки 5 типов
3. size — длины стен (1-3 по layout), площадь, потолок (мм)
4. openings — окно / дверь / коммуникации / заметки
5. summary — обзор + Сохранить → POST /api/measurement

BACKEND (main.py):
- New /api/measurements (POST) для списка замеров менеджера
  с опц. фильтрами по client_tg_id
- _handle_measurement теперь дописывает имя+телефон клиента в notes
  (если client_tg_id не зарегистрирован — это новый клиент без аккаунта)
- handlers dispatcher: 'measurements' route added

ROUTING (app.js):
- Quick-action 'Новый замер' wired to '#/measure'
- routeByHash: Measurements.mount on #/measure

CLIENT PROFILE (clients.js):
- New section 'Замеры · N' on client history page
- fetchMeasurements() filters by client_tg_id or name match in notes
- layoutLabel() shows Russian label (Прямая / Угловая Г / etc.)
- Cache bump v=20260512c
2026-05-12 17:41:01 +03:00
wasrusgen
c4f3016b56 miniapp: client profile tab — list + history + lead detail
NEW FILE assets/clients.js:
- Clients.mount(container) — hash-routed view
  - #/clients              — list of all clients (cards: avatar, name, phone, leads count, last date)
  - #/clients/client/<key> — single client history (all leads as items)
  - #/clients/lead/<id>    — full lead detail with re-rendered report

UI:
- Card style: avatar with initial, name + phone, footer with N подборов + дата
- Pluralization for Russian (1 подбор / 2 подбора / 5 подборов)
- Date format: 'сегодня · 14:30' or 'DD.MM.YYYY'
- Status pills: new / sent / viewed / ordered

PODBOR.JS:
- Exposed renderSavedReport(ai, leadId) for Clients module reuse
- Same renderer as live podbor — same matrix, pros/cons, links

APP.JS:
- Quick action 'Клиенты' added (icon: user)
- Hash router: #/clients → Clients.mount()

INDEX.HTML:
- clients.js script added
- Cache bumped to v=20260512a

CSS:
- .client-list, .client-card with avatar+meta+footer
- .client-detail-head (big card with avatar 56px)
- .leads-list with .lead-item (grid: date | id | status | arrow)
- .loader-inline for async fetch
- .ai-text-fallback for legacy text-only responses
2026-05-12 07:20:54 +03:00
wasrusgen
a849491f56 fix(miniapp): /api/me path for FastAPI backend (was ?path=me from Apps Script) 2026-05-10 22:19:34 +03:00
wasrusgen
f85d3a9d1e feat(miniapp): switch BACKEND_URL to Cloudflare Tunnel → VPS backend (GigaChat live) 2026-05-10 22:18:53 +03:00
wasrusgen
d1165d5e4f feat(miniapp): «Подбор техники» screen — 7-step picker (categories/niches/budget/infra/scenario/brands/summary) wired to /api/podbor 2026-05-09 13:34:46 +03:00
wasrusgen
86cd4eb614 fix(miniapp/A): tighter quick action cards (no min-height) + smaller hero buttons 2026-05-09 13:06:54 +03:00
wasrusgen
af7dc07720 feat: one-tap role buttons (WebApp directly, no intermediate step) + role param in URL/backend 2026-05-09 13:05:20 +03:00
wasrusgen
017d179746 feat(miniapp): manager home v2 — greeting + hero today-task + 2x2 quick actions + active projects + bottom nav 2026-05-09 12:59:41 +03:00
wasrusgen
ce91c0283b feat(miniapp): lock to variant A; green active dot; tighter spacing in menu and profile card 2026-05-09 12:25:19 +03:00
wasrusgen
7e0d2b98b0 feat(miniapp): three-variant design switcher (Brand/A/C) with literal palettes from mockups 2026-05-09 12:19:14 +03:00
wasrusgen
5032b27049 fix(miniapp): detect Telegram dark theme via tg.colorScheme; bump dark-mode contrast 2026-05-09 11:47:28 +03:00
wasrusgen
d7bd0aa5c2 feat(miniapp): hybrid Architectural Clean design — Inter + Instrument Serif italic + JetBrains Mono, paper palette, ZOV accents 2026-05-09 11:31:30 +03:00
wasrusgen
67dd0eac0c fix(miniapp): correct backend URL format + drop Content-Type to avoid CORS preflight 2026-05-09 11:11:21 +03:00
wasrusgen
4f0c51c453 feat(miniapp): connect to deployed Apps Script backend 2026-05-09 10:45:35 +03:00
wasrusgen
57eefbbf5c feat(miniapp): premium redesign — gradient profile card, SVG icons, native-style grouped menus, dark theme 2026-05-09 01:22:30 +03:00
wasrusgen
0c5ed48303 chore: initial scaffold (bot, miniapp, backend, docs) 2026-05-08 23:56:48 +03:00