Commit Graph

145 Commits

Author SHA1 Message Date
wasrusgen
016e3becdd feat: Яндекс.Карты в карточке клиента + gps_lat/lng в API клиентов
- backend: _ensure_client получает gps_lat/gps_lng поля
- backend: при агрегации Measurements берём gps_lat/gps_lng для клиента
- clients.js: в шапке карточки кнопка «🗺 Карта» если есть координаты
- podbor.css: стили .client-detail-addr, .map-link-btn
- index.html: cache bump → v=20260514k

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-05-15 22:11:17 +03:00
wasrusgen
bedef30465 feat: add entrance+floor fields, fix geocoder false-positive on locality match
- clients.js: new client + edit client forms get подъезд and этаж fields (optional)
- clients.js: address string includes подъезд/этаж when filled
- clients.js: splitAddress parses подъезд/этаж from stored address string
- clients.js: geocoder now checks result.kind — only shows ✓ for house/street precision;
  locality/province match shows warning "улица не найдена" without saving coords
- podbor.css: addr-grid grows to 3 rows (город/улица, дом/кв, подъезд/этаж)
- index.html: cache bump → v=20260514j

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-05-15 21:38:13 +03:00
wasrusgen
44799362c1 feat: client picker in measurement form, address geocoding, edit-client 4-field addr
- measurements.js: replace manual name/phone inputs with client picker overlay
  (search by ФИО or phone, sorted alphabetically, picks from /api/clients)
- clients.js: new-client and edit-client forms now use 4 split address fields
  (город, улица, дом, кв./офис) with geocode validation on save
- clients.js: add splitAddress() helper to pre-fill edit form from stored address
- clients.js: voice engine refactored (continuous=false + auto-restart, no duplication)
- clients.js: note block view/edit toggle (textarea closes after save)
- podbor.css: styles for picker overlay, picker-row, chosen-card, addr-grid, geo-status
- backend main.py: _handle_client_create and _handle_client_update accept gps_lat/gps_lng
- index.html: cache bump → v=20260514i

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 14:51:25 +03:00
wasrusgen
cbea202de5 fix(note): view/edit toggle — textarea closes after save
Note block now has display mode (read-only text) and edit mode (textarea).
Default is display. "Изменить" opens editor, "Сохранить" saves and returns
to display, "Отмена" discards. No more always-open textarea confusion.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 14:18:16 +03:00
wasrusgen
dd8691671a fix(voice): continuous=false + auto-restart — eliminates word duplication
Replaced continuous=true with phrase-by-phrase mode in all 3 voice inputs
(setupVoiceMicForField, setupVoiceInput, setupVoiceMic). Chrome mobile
resets ev.results mid-session with continuous=true causing repeated words.
Now each phrase is an independent SR() session; baseText grows cleanly.
Shared _buildVoiceEngine factory in clients.js.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 14:06:48 +03:00
wasrusgen
61f23c9bca feat(miniapp): redesign quick-action buttons + fix voice duplication
- Quick-actions: 2×2 grid, walnut circle icon chip, 3D card press effect
- Voice-to-text: recompute-from-scratch in setupVoiceInput (client note block)
- Cache bump v=20260514f

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 13:50:10 +03:00
wasrusgen
f280dea9ea UX: карточка клиента — кнопки наверх, объёмные, хронология свёрнута
1. Кнопки управления (Редактировать/Удалить) теперь сразу под шапкой
   карточки — не нужно скроллить через таймлайн/файлы.

2. Брендовые объёмные кнопки .ct-btn вместо текста с эмодзи:
   - inline SVG (карандаш + корзина), монолиния stroke-width 1.7
   - градиент 3-стопа (highlight → base → shadow)
   - inset highlights + drop-shadow для лёгкого 3D
   - Edit: палитра ореха #8A6541 → #6B4A2B → #523620
   - Delete: благородный кирпич #C95A4A → #A6382A → #832418
   - press-state: invert insets + translateY(1px)
   - filter:brightness на hover

3. Хронология свёрнута по умолчанию (<details> без open).
   Шеврон поворачивается при раскрытии. summary без default-маркера.

index.html: cache bump v=20260514e
2026-05-14 11:53:29 +03:00
wasrusgen
4612c3a4e4 fix: карточка клиента — данные, редактирование, удаление по правилам
Bug 1: «Завожу клиента и не вижу данных»
Причина: 3 эндпоинта проверяли user.get('role') == 'manager' напрямую,
вместо sheets.has_role(user, 'manager'). У админа теперь multi-role
(manager,measurer,assembler) → проверка падала с only_manager,
/api/measurements возвращало ошибку → таймлайн/файлы пустые.

Backend fixes:
- _handle_measurements_list: has_role + initDataUnsafe fallback + фильтр archived_at + возвращает client_name, client_phone, address, scheduled_at, client_no, contract_no, contract_date, assigned_to_tg_id
- _handle_measurement_request: has_role вместо ==
- _handle_measurement_detail: has_role + поддержка assigned_to_tg_id для мастера
- _handle_clients: возвращает address, contract_date, measurements_count, in_work
  in_work=True если: есть лиды ∨ есть не-draft замер ∨ есть сборка

Bug 2: «Не могу удалить клиента»
Причина: была спрятана в expand «Опасная зона» внизу страницы.

Новая логика прав (по запросу):
- Клиент не в работе → ✏️ Редактировать + 🗑 Удалить
- Клиент в работе → только ✏️ Редактировать
- Бэкенд тоже enforce: client_delete отвечает {error: 'in_work'}
  если есть лиды/сборки/не-draft замеры

Новые эндпоинты:
- POST /api/client_update — обновляет имя/телефон/адрес/договор
  во всех Measurements этого клиента. Возвращает обновлённый client_key
  если имя изменилось

Frontend:
- Секция «⚙️ Управление карточкой» вместо «Опасной зоны»
- Кнопка ✏️ Редактировать всегда видна, 🗑 Удалить только если !in_work
- renderEditClient — форма редактирования (имя, тел, адрес, договор № + дата)
- В шапке карточки теперь видны адрес и (если не в работе) бейдж «ещё не в работе»
- Draft-карточки скрыты из таймлайна (это техническая строка, не событие)

index.html: cache bump v=20260514d
2026-05-14 11:42:27 +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
34b83899b5 fix client create — 7 багов + правило самопроверки
#7 — submit-flow:
  - При успехе скрываем CTA с «Сохраняем...» (style.display=none)
  - Обработчики «Ещё клиент» / «Открыть карточку» прикрепляются к
    result-блоку (был form.querySelector — там их нет)
  - Backend возвращает client_key = name.lower() — совместимо
    с тем как ищет _handle_clients
  - clientsCache = null после успеха

#3 — голос дублировался:
  - Переписан алгоритм: финальные транскрипты пересчитываются с нуля
    из ev.results[0..N] каждое событие (не аккумулируем дельтами).
  - confirmedFinal фиксируется в baseText только на onend.
  - Применено в measurements.js + clients.js

#2 — телефон:
  - Frontend normalizePhone: убирает не-цифры, +7/8 → +7, добавляет +7
    к 10-значным; auto-нормализация на blur
  - Backend _normalize_phone(): тот же алгоритм
  - Валидация: ровно 11 цифр начиная с 7
  - Field-error «Введите корректный российский номер...»

#1 — адрес:
  - Min 5 chars (улица + дом)
  - Backend проверка длины
  - Hint «Укажите город, улицу, дом, кв.»

#5 — номер клиента:
  - Новая колонка client_no
  - _next_client_no() — максимум для текущего менеджера + 1
  - Шильд #N рядом с именем в карточке клиента

#6 — номер договора:
  - Новые колонки contract_no, contract_date
  - Поля в форме «Новый клиент» (опционально)
  - Шильд «📋 договор N» в карточке клиента

#4 — удаление клиента:
  - Soft-delete через колонку archived_at
  - Endpoint /api/client_delete
  - «⚠️ Опасная зона» в карточке клиента (collapsible)
  - Confirm dialog через Telegram.WebApp.showConfirm
  - Архивированные клиенты не показываются в /api/clients

#8 — правило самопроверки:
  - docs/SELF_CHECK_RULE.md — 10 пунктов чек-листа перед «готово»
    (end-to-end, ключи, UI-состояния, валидация, голос, deploy, логи)

Cache bust v=20260514a.
2026-05-14 00:09:14 +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
18c2325440 splash: убрать вращающееся кольцо, оставить только опилки + breathing
Cache bust v=20260513zm.
2026-05-13 22:58:09 +03:00
wasrusgen
ec3929ae94 splash: убрал wobble, 16 опилок, добавил вращающийся диск-кольцо
- Убрана анимация sawWobble — пила больше не качается
- Опилки расширены с 8 до 16 (8 влево + 8 вправо от пилы)
- Новый saw-rotor SVG поверх саблона:
  · кольцо с пунктиром (stroke-dasharray «3 4.5»)
  · внутреннее тонкое кольцо (opacity 0.35)
  · вращается 3.5s/оборот линейно
  · позиция top:22% left:50% width:36% — над диском пилы

Cache bust v=20260513zl.
2026-05-13 22:46:53 +03:00
wasrusgen
0aa7a8e35a splash: эффекты I + K — wobble пилы + опилки
I: sawWobble — весь логотип покачивается ±1.5° каждые 5.5с
   (имитация работающего инструмента). Анимация на wrapper,
   чтобы не конфликтовать с breathing scale на самом SVG.

K: splash-dust — 8 опилок-точек разлетаются от пилы по
   индивидуальным траекториям (--dx/--dy через CSS-vars),
   разные delays и durations для естественности.

Cache bust v=20260513zk.
2026-05-13 22:36:23 +03:00
wasrusgen
00de1baef9 splash: flex column center + меньше лого + drop-shadow
- .loader: grid → flex column. Теперь элементы плотным
  кластером в центре, а не разброс по высоте.
- Лого max-width 360px → 260px (как просил, чуть меньше)
- Добавил drop-shadow под лого для глубины

Cache bust v=20260513zj.
2026-05-13 22:21:32 +03:00
wasrusgen
f71590b05a splash: точный viewBox + цвет blue-grey #2C3E50
- Посчитал tight bbox SVG-содержимого (5280 5260 → 11630×5570),
  viewBox теперь точно по контенту → лого центруется
- Цвет #C9A227 (золото) → #2C3E50 (слейт blue-grey) везде:
  · сам SVG (саблон + wordmark)
  · CRM tagline (без рамки, чистый текст)
  · loader bar gradient
  · theme-color
- Лого: max-width 360px, margin auto

Cache bust v=20260513zi.
2026-05-13 22:15:01 +03:00
wasrusgen
0a3d6598f6 logo: настоящий SVG из CRM.cdr (золото) + CRM без рамки
Заменил мой ручной SVG на оригинальный экспорт из CRM.cdr:
- Pixel-perfect формы пилы и wordmark @WASRUSGEN1 от тебя
- Все #5B5B5B (серый) → #C9A227 (золотой)
- ViewBox обрезан под контент

CRM на splash:
- Убрана обводка-рамка
- Размер увеличен (16pt → 18pt)
- Чистый текст: золотая разрядка 0.4em

Cache bust v=20260513zh.
2026-05-13 22:08:44 +03:00
wasrusgen
7f7bfd7a33 logo: композиция как в CRM.cdr (саблон тесно над wordmark)
Перерисовал SVG-лого ближе к референсу из исходника:
- Пила компактнее, шире, наклонённый корпус
- Wordmark @WASRUSGEN1 крупнее (115pt Inter Black)
- Тесная связка пила↔wordmark — один знак
- Цвет: золотой #C9A227

Удалил неиспользуемые PNG/JPG из бета-версии.

Cache bust v=20260513zg.
2026-05-13 22:02:40 +03:00
wasrusgen
dd02136b92 brand: SVG-лого @wasrusgen1 в золоте + CRM как слоган (поз. как у ЗОВ)
Полностью векторный логотип (assets/wasrusgen-logo.svg):
- Циркулярная пила (корпус + кожух + диск + зубья + центральный болт
  + опорный брус) — stroke 14, золотой #C9A227
- Wordmark «@WASRUSGEN1» — Inter Black 78pt золотой
- Компоновка как у тебя: пила сверху, wordmark снизу

Splash:
- Большой SVG-лого (70%, max 320px), дыхательная анимация
- Loader bar
- CRM штамп (золотая обводка, letter-spacing 0.4em) — на месте
  где у ЗОВ был «Сделано с душой!»

Theme-color → #C9A227 (золотой статус-бар в Telegram WebApp).
Loader bar gradient → золотой.

Cache bust v=20260513zf.
2026-05-13 21:47:38 +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
8201fee9f2 client card D1: хронология + файлы + быстрые действия
Карточка клиента (#/clients/client/<key>) переработана:

1. Шапка с большой круглой кнопкой 📞 справа — звонок одним тапом
   через tel: ссылку.

2. Быстрые действия (3 в ряд):
   - 🤖 Подбор техники — переход в #/podbor с pre-fill ФИО/телефона
   - 📐 Заказать замер — переход в #/request с pre-fill (через sessionStorage)
   - 📋 Копировать ФИО+тел — clipboard для пересылки коллеге

3. Примечание менеджера (с голосовым вводом) — уже было.

4. 🕒 Хронология — единая лента событий:
   - Лиды (подборы) с датой создания и статусом
   - Заявки на замер
   - Назначенные замеры (на дату scheduled_at)
   - Завершённые замеры
   События отсортированы по дате desc, рядом крупная точка timeline,
   тап → переход к детали.

5. 📂 Файлы — группы по замерам с миниатюрами фото (до 6 в ряд +
   плашка «+N» если больше). Тап на миниатюру открывает в новом
   окне (фото открывается в полный размер).

6. Свёрнутые секции «Подборы» и «Замеры» внизу — для тех кому
   нужны плоские списки.

Cache bust v=20260513za.
2026-05-13 18:39:25 +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
9e23239f57 client note: примечание менеджера + голосовой ввод
Backend:
- Лист ClientNotes (auto-create через ensure_sheet) — колонки
  manager_tg_id, client_key, note, updated_at.
- Ключ клиента: «p:7XXXXXXXXXX» если есть телефон ≥10 цифр,
  иначе «n:<имя в lower>». Привязан к менеджеру.
- POST /api/client_note — без поля note читает текущую,
  с note — upsert (хард-кап 4000 символов).

Frontend в карточке клиента (#/clients/client/<key>):
- Новый блок «📝 Примечание» сверху над списком подборов
- Textarea + дата обновления в meta
- Кнопка «🎤 Диктовать» — Web Speech API (ru-RU)
  · interimResults показывает прямо во время речи
  · final-результаты добавляются к baseText
  · красная пульсация во время записи
  · graceful degrade если SR недоступен (Telegram WebApp на iOS)
- Кнопка «Сохранить» → PUT в /api/client_note + статус «✓ сохранено»

CSS: .client-note-block, .btn-mic, .btn-mic.rec (pulse animation),
.note-status.ok / .err.

Cache bust v=20260513y.
2026-05-13 18:06:45 +03:00
wasrusgen
7a5df7d011 checklist: 5 SVG-эскизов в стиле Editorial Calm
По аналогии с пиктограммами категорий техники добавил инструктивные
рисунки к чек-листу замера. Walnut stroke + cream fill + лёгкая тень.

1. topview — вид сверху, комната с пронумерованными стенами + компас.
2. clockwise — стрелка по часовой стрелке с точкой «старт».
3. openings — фронт стены с дверью / окном / балконом + размеры
   (ширина, высота, низ подоконника).
4. comms — стена с точками R1 / Sw1 / Wc1, двумя размерами на каждую
   (горизонталь до угла-базы + вертикаль до пола), указанная БАЗА: ПУ.
5. levels — разрез помещения с нулевым полом (волнистая стяжка),
   плитой +88, потолком и коробом справа, размеры H1/H2.

Реализация:
- Новый файл miniapp/assets/zamer-picts.js — экспортирует ZAMER_PICTS.
- renderMarkdown в measurements.js поддерживает директиву
  «@pict:KEY» на отдельной строке → подставляет SVG.
- Эскизы вставлены в ЧЕКЛИСТ_ЗАМЕРА.md в соответствующие разделы
  (1, 4, 6, 8). По часовой стрелке — после § 1.
- CSS .cl-pict — рамка пунктиром + цент., max-height 220.

Cache bust v=20260513x.
2026-05-13 18:00:43 +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
3866160efb splash: убрал «Открываем кабинет», оставил только слоган
Splash теперь показывает только:
  логотип ЗОВ + полоса прогресса + «Сделано с душой!» (28pt italic).

Cache bust v=20260513s.
2026-05-13 10:43:08 +03:00
wasrusgen
d8cb36703f slogan: переход с Caveat на Cormorant Garamond Italic (вариант 8)
Сдержанный serif-италик, не контрастирует с Editorial Calm стилем.
Убран наклон -2°, шрифт элегантный сам по себе.

Размеры:
- splash subtitle: 22pt (Caveat был 26pt — serif читается крупнее)
- role chooser: 28pt (Caveat был 34pt)

Loaded Cormorant Garamond italic from Google Fonts.
Cache bust v=20260513r.
2026-05-13 10:41:59 +03:00
wasrusgen
eeefbd1a59 add font preview page with 14 cursive variants for slogan 2026-05-13 10:37:35 +03:00
wasrusgen
897bded49d splash: ZOV · КУХНЯ И ТЕХНИКА → каллиграфический «Сделано с душой!»
Заменяем подпись на splash там же где была фраза «кухня и техника» —
рукописный слоган в Caveat 26pt walnut с лёгким наклоном.

Cache bust v=20260513q.
2026-05-13 10:35:58 +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
a437b55447 measurements: auto-suggest № замера, активные галочки чек-листа, убрана стяжка
1. № замера подбирается автоматически:
   - POST /api/measurement_next_no возвращает max(zamer_no) + 1
   - Wizard при открытии вызывает endpoint и заполняет input
   - Менеджер может переписать вручную (поле редактируемое)
   - Подпись «Подобран автоматически — можно изменить»

2. Поле «Стяжка / нулевой пол» удалено из формы:
   - По логике пользователя — стяжка пишется на самих фото с замером
   - Backend колонка floor_base остаётся для backward compat (старые записи)

3. Чек-лист стал интерактивным:
   - Каждый [ ] item теперь .cl-item с cursor:pointer
   - Тап переключает галочку (☐ ↔ ☑) + страйкаут текста
   - Состояние сохраняется в localStorage по measurement_id (или draft)
   - Sticky прогресс-бар сверху: «N из M · X%» + градиентная полоса
   - Кнопка ↺ в шапке — сбросить все галочки
   - Hapt-фидбэк на каждый тап

Cache bust v=20260513m.
2026-05-13 07:29:18 +03:00
wasrusgen
121927ab2d measurements: структура фото + чек-лист + общая инфа
По чек-листу ЗАМЕРОВ (D:\!!! GOOGLE DISK\ЗАМЕРЫ\...\ЧЕКЛИСТ_ЗАМЕРА.md):
каждая стена снимается отдельно, имя файла отражает тип.

Wizard:
- Каждое фото получает dropdown «Что это»:
  Стена 1, 2, 3, 4 · План комнаты · Общий вид · Деталь
- Авто-предложение типа: w1 → w2 → w3 → w4 → plan → general
- Добавлены поля общей инфы:
  · № замера (опционально)
  · Дата замера (auto-сегодня)
  · Стяжка / нулевой пол (default «0,000 = +88 мм над плитой»)
- В шапке кнопка 📋 — открывает чек-лист отдельной страницей
- Inline-рендер markdown с поддержкой заголовков, списков, таблиц, code

Backend:
- _save_measurement_photo принимает kind+kind_seq → имена файлов
  структурные: w1.jpg, w2.jpg, plan.jpg, general_2.jpg, detail_1.jpg.
  Это упрощает дальнейшую обработку для генерации DWG.
- Расширена схема Measurements: zamer_no, zamer_date, floor_base, photos_meta.
- /api/measurement_detail отдаёт новые поля.

Cache bust v=20260513l.
2026-05-13 07:19:25 +03:00
wasrusgen
5c2a5bb335 measurements: упрощение — только фото + заметки (DWG потом)
По логике пользователя: задача замера в боте — только сфоткать
рукописные эскизы с размерами + общие фото помещения. Перевод
в DWG-чертёж происходит отдельным процессом (вручную технологом
или через AI vision позже).

Что убрано:
- Шаг «Форма кухни» (linear / l_shape / u_shape / island / peninsula)
- Шаг «Размеры стен + потолок + площадь»
- Шаг «Окна и двери»

Что осталось:
- Клиент (имя/телефон/адрес) — только для прямого ввода менеджером.
  При закрытии заявки замерщиком берётся из заявки read-only.
- Фото (до 20, со сжатием до 1800px на клиенте)
- Заметки опционально

Wizard стал одностраничным: всё на одном экране, единый submit.
В update-mode (#/measure?id=...) wizard сразу подгружает заявку
и не спрашивает данные клиента.

Backend схема не менялась — старые поля (layout, walls и т.п.)
просто остаются пустыми. Существующие замеры с заполненными полями
отрисовываются без изменений.

Cache bust v=20260513k.
2026-05-13 07:03:45 +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
a084542bbf measurements: photo upload + measurement detail page + PDF/print
Wizard: new 'photos' step (6 total) — camera/gallery input, client-side
canvas compression to 1600px @ ~78% JPEG, max 12 photos. Thumbnails
with delete in step; preview in summary.

Backend: POST /api/measurement now decodes data-URL photos and saves
to /app/photos/<id>/N.jpg (volume-mounted). New GET /api/photo/{id}/{n}
serves files with path-traversal protection. New POST /api/measurement_detail
returns full measurement record (walls/openings/photos/notes/...).

Clients page: measurement rows now clickable → renderMeasurement detail
view with key-value grid + photo gallery + 'Скачать PDF / Печать'.
Print stylesheet (@media print) hides navigation/buttons/uploaders and
prints clean A4-friendly layout.

Podbor report: existing 'Печать → PDF' now falls back to inline
window.print() inside Telegram WebApp (popups are blocked there).

Cache bust v=20260513a.
2026-05-12 18:11:29 +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
43c43af795 remove duplicate vent question + AI must propose charcoal filter on recirc hood
USER REPORT: 'после прогона всей техники, перед запросом опять вопрос про фильтр для вытяжки в конце'

ROOT CAUSE: Infra-шаг спрашивает 'Вытяжка → вентшахта?', хотя у hood-категории уже есть шаг 'Подключение' с вариантами Отвод/Рециркуляция/Универсальная. Это дубликат.

FIX:
- renderInfra: убран блок vent. Шаг показывается только если выбрана варочная.
- Auto-skip infra если нет hob (раньше требовался hob ИЛИ hood, теперь только hob)
- renderSummary: убрана строка 'Вентиляция'
- summaryBack: 'infra' только если cats.includes('hob')

AI PROMPT:
- Новый блок: режим вытяжки читать из per_cat.hood.answers.mode
- exhaust → обычная установка
- recirc → ОБЯЗАТЕЛЬНО упомянуть 'Угольный фильтр в комплекте/докупаем' в pros
  + в первой строке pros указать 'для квартир без вентшахты'
- combi → упомянуть универсальность
- 'Если recirc и фильтр не предложен — это ОШИБКА'

Cache: v=20260512b
2026-05-12 17:22:59 +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
0b48dd2371 simplify: remove 'features' step from all 8 categories
USER FEEDBACK:
'Особенности везде убрать, их можно в SWOT анализе приводить в качестве примечания
не акцентируя на них особого внимания. Современные фичи на 95% одинаковые.'

REMOVED features step from:
- fridge (NoFrost, Inverter, Wi-Fi, etc.)
- hob (Booster, FlexZone, FFD, Hob2Hood, etc.)
- oven (Wi-Fi, autoprogram, probe, softclose, etc.)
- dw (Wi-Fi, AutoOpen, AutoDose, AquaStop, Inverter)
- hood (touch, LED, auto, silent, turbo, wifi, perimeter)
- microwave (Wi-Fi, humid sensor, defrost, antibac)
- coffee (Wi-Fi, touch, grinder, autoclean)
- washer (inverter, steam, wifi, autodose, silent, aquastop)

KEPT: hood.color (about visible material/aesthetics, not feature)
KEPT: oven.location (where in kitchen — design-relevant)

NEW STEP COUNTS:
- fridge: 3 (was 4)
- hob: 4 (was 5)
- oven: 3-4 (was 4-5)
- dw: 3 (was 4)
- hood: 3-4 (was 4-5)
- microwave: 3 (was 4)
- coffee: 1-4 (was 2-5)
- washer: 5 (was 6)

AI PROMPT updated:
- Features no longer come from user — AI mentions important ones in highlights/pros
- Emphasis on MEASURABLE advantages in pros (N dB quieter, Y l more, N% cheaper)
- Не делать акцент на стандартных фичах — 95% одинаковые

USER WIZARD теперь короче и проще: тип → размер → ключевые параметры → готово
2026-05-11 23:37:41 +03:00
wasrusgen
cecb8d3444 review screen: fix text wrap + cleanup stale answers from removed steps
CSS:
- .rev-val: flex:1, min-width:0, overflow-wrap:break-word — длинные значения
  больше не ломаются мид-словом ('энергоэффективнос·...')
- .rev-label: max-width:40% — лейбл не съедает всё место
- hyphens:auto для перенос длинных слов на дефис

JS (getCatState):
- При загрузке per_cat фильтруем answers — оставляем только ключи которые
  есть в текущем config.steps
- Это убирает stale-поля типа 'class' у ПММ, оставшиеся в localStorage
  после рефакторинга шагов
- Безопасно: меняет только в памяти, не перезаписывает state (renderReview
  всё равно итерирует config.steps)
2026-05-11 23:25:23 +03:00
wasrusgen
6915bba845 user feedback: oven proportions + dw simplification
OVEN PICTOGRAMS (per user: 'духовка не очень похожа, прямоугольные, фасадом не закрываются'):
- oven_install_builtin: REMOVED dashed niche outline (ovens don't close with façade — sit in open cabinet)
- Made body wider+shorter — 78×74 viewBox area (was 68×112, too tall)
- Real 60×60 cm proportions, control panel at top + handle + glass window with racks
- oven_install_stove: тщательнее прорисован — cooktop (with concentric burners), control strip,
  oven door with handle + window, ножки чётче, линия пола

DW LOGIC SIMPLIFIED (per user: 'энергопотребление уже перебор'):
4 шага вместо 5:
1. Тип встройки (full/partial/freestanding) — was step 1, kept
2. Размер ширина (45/60) — was step 3, moved up to step 2
3. Корзины + программы — merged in one step:
   - 2 корзины · базовый (5-6 программ)
   - 3 корзины · стандарт  (8-10 программ)
   - 3 корзины · расширенный (12+ программ, стекло, авто, кастрюли)
4. Особенности (multi) — теперь содержит Wi-Fi, AutoOpen, AutoDose, Beam, AquaStop, ≤44dB,
   Inverter (включая A+++), GlassZone

Removed: separate 'class' step (energy efficiency moved into features as Inverter option)
2026-05-11 23:06:25 +03:00
wasrusgen
555c5568ff miniapp: 25 SVG pictograms for 7 remaining categories (style D · 3D)
PODBOR_PICTS additions (25 total):
- HOB (3): elec — induction concentric circles, gas — burners with grid + knobs, combi — split panel
- OVEN (2): built-in with niche + control panel + glass window, stove (combo unit on legs)
- DW (3): full (hidden facade with handle strip), partial (control bar on top), freestanding (full controls + door + feet)
- HOOD (7): drawer (cabinet + sliding panel), hidden (cabinet only), dome (chimney shape), inclined (angled glass), island (ceiling tubes), downdraft (counter panel rising), hob-in-combo (cooktop with center exhaust slot)
- MICROWAVE (2): built-in (in niche with window+keypad), freestanding (countertop with feet)
- COFFEE (5): built-in (display + buttons + spout + cup), free-grinder (bean hopper + display), capsule (small + capsule slot), manual (with portafilter + steam wand + pressure gauge), tap (faucet integrated into countertop)
- WASHER (3): built-in (hidden facade), under-top (control panel visible, big door), freestanding (full unit + feet + powder tray)

CONFIG wiring: all 25 pict keys referenced in podbor.config.js step 1 of each category

PREVIEW: new preview-all-picts.html shows all 25 in one page for visual review
2026-05-11 21:27:40 +03:00
wasrusgen
da8a98f34f market 2026 update: Kuppersberg budget default, Haier mid, full RF brand realism
PODBOR_SINGLE_BRAND_OPTIONS (single-brand kitchen picker):
- Premium: + Gaggenau ⚠, V-Zug ⚠, Liebherr ⚠ (all parallel-import)
- Middle: + NEFF ⚠, Haier  marked recommended
- Budget: + Kuppersberg  recommended, Maunfeld, Weissgauff, Gorenje, Hotpoint, Indesit, Midea, Candy
- Removed budget-only Бирюса/Pozis/DEXP (not popular in built-in segment)

PODBOR_BRANDS per category — fully refreshed with realistic 2026 lists:
- Coffee: Bosch/Siemens/NEFF in mid, Kuppersberg/Maunfeld in budget (instead of obscure ones)
- All categories now include Kuppersberg/Maunfeld/Weissgauff in budget tier
- Premium adds Gaggenau, V-Zug consistently

AI PROMPT — new section 'РЫНОК РФ 2026':
- Documents exact tier composition with brands and price ranges
- 'Типичный выбор клиента ЗОВ-СПб: Bosch + Haier + Maunfeld'
- Premium combo: Bosch + Miele washer (для кухонь 600к+)
- Trends: parallel import normalized, Haier #2 after Bosch, Kuppersberg builder default
- СВЧ category fading — combined ovens with microwave taking over
- Induction wins, gas only in private houses

EXAMPLES в prompt expanded:
- Haier C4F744CMG, Kuppersberg NRS 1857 X, Maunfeld MBL 88LU, Weissgauff WBI 30 ATX
- Clear 'НЕ выдуманное' guidance
2026-05-11 20:23:45 +03:00
wasrusgen
ef500fa446 user feedback batch: model count, specs, manual link, dimensions, export
1. MODEL COUNT SELECTOR (strategy step):
   - new PODBOR_MODEL_COUNTS [3/5/7]
   - state.model_count default '5'
   - UI on strategy page with description (быстро/оптимально/максимум)

2. AI PROMPT EXPANDED:
   - new field: manual_search_query — for Google search instruction PDF
   - new specs object per model: dimensions_mm/volume_l/weight_kg/noise_db/energy_class/color
   - 'specs ОБЯЗАТЕЛЬНЫ для проектирования кухни' explicit rule
   - reads checklist.model_count to determine how many models per category
   - max_tokens 4000 → 8000 (room for richer responses)

3. MODEL CARD RICHER:
   - _renderSpecsBlock — characteristics in 2-col grid, dimensions highlighted
   - _renderUtilityLinks — Google search buttons for инструкция (PDF) + Схема установки
   - Specs critical for ZOV kitchen design (manager needs to verify niche fits)

4. EXPORT BUTTONS:
   - 'Скачать HTML' — generates standalone HTML with inline styles, downloads as file
   - 'Печать → PDF' — opens new window with cleaned layout + auto-prints
   - User can save as PDF via system print dialog

5. PREVIEW updated with realistic specs/manual_query for all 3 fridges
2026-05-11 17:11:30 +03:00
wasrusgen
5ceffa4f69 miniapp: phone validation on intro — blocks transition with bad number
- New isValidPhone(raw): checks 11-digit Russian after normalization (8/7/+7/9-prefix)
- Intro 'Начать' button now custom click handler instead of data-go
- Validates name (non-empty) and phone (Russian format)
- Inline .field-error red message under invalid field
- .field-hint shows format help under phone input
- Haptic 'warning' feedback on invalid submit
- Phone is auto-normalized to '+7 900 123-45-67' before transition
2026-05-11 16:48:52 +03:00
wasrusgen
d7be644aed miniapp: price comparison matrix as PRIMARY view per category
WHAT CHANGED:
- New _renderPriceMatrix(models) — table with rows=models, columns=stores
- Inserted as PRIMARY view above model cards (was secondary accordion)
- Columns dynamically include only stores that returned data
- Sticky model column (left) — scrolls horizontally on mobile
- Best price per row highlighted: green bg + ✓ badge + green text
- Empty cells: '—' if no URL, 'смотреть →' if URL but no price yet
- 'Мин' column on far right — explicit cheapest price summary

CSS:
- .report-matrix-wrap with rounded card
- Sticky col-model with box-shadow on right edge
- Cell-price.best with rgba green background
- .best-mark circle badge

PREVIEW:
- Updated mock with 3 fridges + 3 hobs across multiple stores (real pricing spread)
- Demonstrates min-price highlighting working

UX:
- User can now visually compare 'where is it cheapest' at a glance
- Tap any cell with price → opens store page
- Tap empty cell with URL → opens search in store

NEXT: same matrix can become PDF/Excel export for client briefcase
2026-05-11 14:56:41 +03:00
wasrusgen
ca342c0641 ai+report: deeper analysis — required pros/cons, category insights, source visibility
AI PROMPT (ai.py):
- Requires minimum 3 pros + 2 cons per model with NUMBERS (36 dB, 463 L, A++, не 'тихий/большой')
- New field 'reasoning' — 1-sentence why-this-model justification
- New per-category 'analysis' — 2-3 sentences about trade-offs
- Strict rules: no fake article numbers, account for parallel-import price markup
- Russian market 2026 awareness: Haier/Korting up, Bosch/Siemens ⚠

TELEGRAM FORMAT (main.py):
- Renders category analysis as italic prelude
- Lists pros/cons as bullet lists (up to 4 pros, 3 cons)
- Shows '🛒 Нашли в: OZON · Citilink · WB' line listing successful sources
- Rating + reviews + stores count line: '📊 ★ 4.7 · 1242 отзыв. · 12 магаз.'
- Direct link to best store: '🔗 Открыть в магазине'

WB PARSER:
- Generates 3 query variants per request: full → brand+model → model only
- Increases hit rate when AI search_query is too verbose
- First non-empty variant wins

MINIAPP REPORT (podbor.js + podbor.css):
- Category analysis block above models (italic, walnut left-border)
- Pros block: green tinted bg, bullet list, header 'Плюсы'
- Cons block: terracotta tinted bg, bullet list, header 'Минусы'
- Reasoning chip: 💡 italic in warm background
- Source badges with per-store price '<store> · 89 990 ₽'
- Color-coded source links: OZON blue, Citilink yellow, WB pink, Я.Маркет red, DNS orange
- 'X магазинов нашли товар' header + plural fix
- '— не найден' fallback if 0 sources

PREVIEW (preview-report.html):
- Mock updated with Haier as flagship (more relevant for 2026 RF)
- Shows analysis, reasoning, source spread (4 stores with different prices)
2026-05-11 14:34:08 +03:00
wasrusgen
4b04f2de54 miniapp: summary page hides Подключение/Вентиляция if hob/hood not picked 2026-05-11 14:26:12 +03:00
wasrusgen
80580db446 miniapp: 4 UX fixes from user feedback
1. PHONE NORMALIZATION
   - On blur (or before submit): '9001234567' -> '+7 900 123-45-67'
   - Handles 8XXX, 7XXX, +7XXX, 10-digit mobile prefixes
   - Leaves untouched if not Russian-looking number

2. BRAND LIST FOR RF 2026
   - PODBOR_SINGLE_BRAND_OPTIONS updated with realistic 2026 brands
   - Promoted: Haier, Korting, Midea, Hisense, Бирюса, Атлант, Pozis, DEXP
   - Bosch/Siemens marked with ⚠ (parallel-import)
   - Miele/Liebherr/Smeg also marked ⚠
   - PODBOR_BRANDS per-category fully refreshed

3. BUDGET ADAPTIVE HINTS
   - Hints now scale by selected categories share of full kitchen
   - Just fridge picked → 'Средний' shows ~88-175 тыс instead of 350-700к
   - Full 8 categories → original 350-700к
   - PODBOR_BUDGET_SHARES + PODBOR_BUDGET_RANGES constants

4. INFRA STEP CONDITIONAL
   - Stove power question only shown if hob category picked
   - Vent question only shown if hood category picked
   - If neither → step auto-skips to summary (with brief notice)
   - Summary 'Назад' button respects skip — goes to strategy if needed
2026-05-11 14:25:25 +03:00
wasrusgen
c2be5e846f miniapp: inline report after submit + standalone preview-report.html
REPORT RENDERER (podbor.js):
- New renderReport(ai, leadId) function — beautiful inline report after submit success
- Shows by_category with up to 5 models per category
- Model card: photo (88x88), brand · name, price range, rating + reviews + stores
- Highlights (with tech translations), pros (green), cons (orange)
- External links to WB / Я.Маркет / OZON / DNS (when enriched data present)
- Comparison table per category (accordion details)
- Total price block (dark theme contrast)
- Warnings block (when AI returns concerns)

CSS (podbor.css):
- .report-* classes: head, summary, cat, model, links, compare, total, warnings
- Editorial Calm palette — walnut accents, paper bg, Newsreader for titles
- Responsive: model card grid 88px image + 1fr body
- Placeholder gradient when no image (camera emoji)

STANDALONE PREVIEW (preview-report.html):
- Mock AI response with 3 fridges + 2 hobs
- Same render logic, runs without backend
- Visit: https://wasrusgen.github.io/zov-tech/preview-report.html

NEXT: integrate proxy6 token → real photos/prices instead of placeholders
2026-05-11 12:26:58 +03:00
wasrusgen
717c6ea138 miniapp: hierarchical wizard for all 8 categories + condition support
CATEGORIES MIGRATED to steps[] schema:
- hob: Источник нагрева → Подтип (multi, optionsBy) → Размер → Конфорки → Особенности
- oven: Установка → Функции (multi) → Размер → Где ставим (cond:built_in) → Особенности
- dw: Тип встройки → Класс (multi) → Ширина → Корзины → Особенности
- hood: Форм-фактор → Подключение → Ширина → Цвет (cond:visible-types) → Особенности
- microwave: Установка → Функции (multi) → Размер (optionsBy) → Особенности
- coffee: Тип → Молоко (cond:grinder/manual) → Вода (cond:built-in/tap) → Размер (cond:built-in) → Особенности
- washer: Установка → Функция → Глубина → Загрузка → Объём → Особенности

NEW PODBOR.JS FEATURES:
- isStepActive(step, answers) — predicate for condition field
- findNextActiveIdx / findPrevActiveIdx — skip inactive steps in navigation
- Auto-advance through inactive on single-select pick
- Review screen filters inactive steps
- isCategoryFilled checks only active single-steps
- buildPerCatSummary skips inactive
- Clearing dependent answers when condition's parent changes (in addition to optionsBy)

NEXT: pictograms for step 1 of each category (currently text-pin layout)
2026-05-11 11:28:50 +03:00
wasrusgen
dd400b71ac miniapp: new pricing flow — brand strategy + budget presets + multi pick strategy
NEW STRUCTURE:
- Step 4 'Бренд' — ai/single/different + brand picker or per-cat chips (now 4-state with 'avoid')
- Step 5 'Бюджет' — Люкс/Премиум/Средний/Бюджет/Точные цифры presets
- Step 6 'Стратегия' — multi: Лучшее по отзывам / Цена-качество / Топ-бренды / Доступное / Tech / Стиль
- Step 7 'Инфра' — перенесено после стратегии
- Step 8 'Итог' — обновлённый summary с новыми полями

FIXES:
- Keyboard-disappearing bug in price inputs — removed render() on input, total recomputed locally
- localStorage merge with defaults for backward compat with new fields
- Bumped STORAGE_KEY to v4

REMAINING:
- Backend still reads checklist.priorities (old shape) — needs update to read pick_strategies + brand_strategy + budget_preset
2026-05-11 10:43:54 +03:00
wasrusgen
496ddf793c miniapp: persistent category strip with active highlight + tap-to-jump
- Visible on all steps after categories are selected
- Highlights current category when inside its wizard
- Filled categories show checkmark
- Tap chip jumps directly to that category's wizard
- Horizontal scroll if many categories don't fit
2026-05-11 00:46:43 +03:00
wasrusgen
d289f7601e miniapp: compact pin layout for wizard steps without pictograms
- Steps with pict (1-3 fridge: install/chamber/size) keep grid cards
- Steps without pict (4 features) render as flex-wrap pill pins
- Auto-detect via options.some(o => o.pict)
2026-05-11 00:02:25 +03:00
wasrusgen
17b112f061 miniapp: hierarchical wizard for fridge category (style D pictograms)
- New PODBOR_PARAMS schema with steps[] supporting single/multi + optionsBy branches
- 11 fridge SVG pictograms in podbor.picts.js (style D — 3D perspective with shadow)
- renderCategoryWizard with step-by-step flow, chips for prior answers, review screen
- Legacy renderCategoryDetail still used for other 7 categories until migrated
- Auto-advance on single-select, Дальше button for multi-select
- Backend-compatible: per_cat[catKey].answers replaces .params/.features
2026-05-10 23:57:03 +03:00
wasrusgen
cd5d92ea17 miniapp: redraw fridge pictograms in style D (3D perspective with shadow) 2026-05-10 23:33:07 +03:00
wasrusgen
8991e7890d miniapp: add fridge style preview — 4 styles × 4 variants for comparison 2026-05-10 23:26:30 +03:00
wasrusgen
fe51f44bd9 fix(preview): correct fridge taxonomy — columns moved to built-in (3 variants: cold, freeze, pair); morozilka stays in freestanding 2026-05-10 23:16:43 +03:00
wasrusgen
f2e3333846 feat(miniapp/preview): SVG pictogram preview for fridge types (Editorial Calm, line-art, walnut) 2026-05-10 22:56:36 +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
cc28984122 feat(podbor iter2): per-category detail menu with primary params + accordion 'Подробнее' for tech features; updated AI prompt to require feature explanations 2026-05-09 15:21:05 +03:00
wasrusgen
129046de07 feat(podbor): drop niches; price-range from-to per cat; ventilation Y/N; priorities multi-select; brand tiers as color (no labels) 2026-05-09 15:10:23 +03:00
wasrusgen
571297c017 fix(podbor): niche inputs overflow — min-width:0 + shorter placeholders 2026-05-09 13:43:17 +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
435ef6817b feat(miniapp/A): rounded corners (16/12/6) + tighter rows (48px) 2026-05-09 12:34:49 +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
6fadc11163 fix(miniapp): bust browser/Telegram cache via versioned asset URLs + no-cache headers 2026-05-09 11:17:20 +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