Убраны mock-данные (А.Пестова, Семья Иваниковых и пр.). Теперь
данные грузятся из /api/measurements (текущего менеджера) и
сортируются:
ПРИВЕТСТВИЕ
«Руслан, 2 замера сегодня» / «ничего на сегодня» /
«1 просрочка» — реактивно по фактическим замерам.
HERO (если есть)
Первый замер сегодня — крупно: время, имя, адрес, кнопки
«Открыть заявку» + 📞 звонок.
⚠️ СРОЧНО
Просроченные scheduled_at в прошлом, не completed.
Красный акцент на инбокс-картах.
📅 ЕЩЁ СЕГОДНЯ
Все остальные замеры на сегодня (без первого, который в hero).
📞 БЕЗ ДАТЫ
Заявки в статусе requested — менеджеру напоминает что надо
созвониться с замерщиком/клиентом.
АКТИВНЫЕ ПРОЕКТЫ
Последние 5 замеров по дате создания. Прогресс-бар по статусу
(requested → scheduled → in_progress → completed). Тап →
карточка замера #/clients/measurement/<id>.
Пусто? — карточка «Свободный день».
Cache bust v=20260513zc.
Шапка → 📅 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.
Карточка клиента (#/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.
По цепочке менеджер→замерщик→замер:
Менеджер «Заказать замер»:
- ФИО, телефон, адрес, кому назначить
- Одно поле «Примечание» (рекомендации по дате + особенности)
Убраны 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.
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.
По аналогии с пиктограммами категорий техники добавил инструктивные
рисунки к чек-листу замера. 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.
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.
Замерщик/сборщик/менеджер при выезде на объект может дополнить
адрес деталями. Эти же данные будут видны и при сборке —
существенно облегчает планирование подъезда и парковки.
Поля:
- Подъезд + этаж
- 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.
По процедуре пользователя: менеджер при создании заявки может не
знать точной даты. Указывает диапазон, замерщик потом созванивается
с клиентом и фиксирует точную дату.
Менеджер при «Заказать замер» — радио-выбор:
○ Конкретная дата (открывает 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.
Внутри кабинета и функциональных экранов слоган не нужен —
там акцент на действиях. Слоган живёт только на splash,
как бренд-приветствие при загрузке.
Cache bust v=20260513t.
Сдержанный 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.
При открытии MiniApp Telegram ставит location.hash = '#tgWebAppData=...'.
Старая проверка !location.hash считала это занятым роутом и пропускала
chooser. Теперь проверяем только наши роуты #/podbor / #/clients и т.п.
Cache bust v=20260513o.
Перешли на единый универсальный паттерн вместо 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.
Reply-keyboard web_app кнопки на мобиле тоже не передают initData
(или с issues). Оставляем только inline-кнопки в самом сообщении —
они работают на Telegram Desktop side-panel + на мобильных.
/start теперь шлёт ОДНО сообщение с inline-keyboard внутри.
/menu — пере-открыть выбор роли.
/hide — убрать reply-keyboard (если осталась от предыдущих версий).
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.
По чек-листу ЗАМЕРОВ (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.
По логике пользователя: задача замера в боте — только сфоткать
рукописные эскизы с размерами + общие фото помещения. Перевод
в 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.
Telegram Desktop side-panel does NOT forward initData when WebApp is
opened via ReplyKeyboardButton.web_app. Resulting in empty initData
and falling back to client cabinet for everyone.
Inline-keyboard buttons (web_app inside the message) open the MiniApp
in modal mode where initData is correctly forwarded.
/start now sends two messages:
1. Welcome + reply-keyboard at bottom (works on mobile)
2. Inline-keyboard with role buttons (works on Desktop too)
В 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.
При входе менеджером (?role=manager) get_or_create_user обновлял CSV
в Sheets, но в памяти существующего dict обновлял только roles[],
а старое поле role оставалось со старым значением. _handle_me читал
role и не находил 'manager' → fallback на client cabinet.
Теперь после grant_role перечитываем строку из Sheets и обновляем
оба поля (role + roles). Плюс в _handle_me предпочитаем roles[]
если он уже распарсен.
1. .loader.splash.hide теперь имеет pointer-events:none !important —
во время 400мс fade splash не блокирует тапы.
2. minShow 2500мс → 1200мс — меньше ожидания, меньше шансов попасть
в окно когда splash ещё блокирует.
Cache bust v=20260513h.
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.
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.
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.
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.
/start теперь показывает только две reply-кнопки внизу:
[👤 Я менеджер] [🏠 Я клиент]
Тап «Я менеджер» → меню менеджера (4 ряда):
🤖 Подбор техники | 📐 Новый замер ← WebApp
👥 Мои клиенты | 🏠 Кабинет ← WebApp
ℹ️ Что умеет бот?| 📞 Куратор ← текст
📋 Чек-лист встречи | ⬅️ Сменить роль ← текст
Тап «Я клиент» → меню клиента (3 ряда):
🏠 Мой кабинет | 📐 Мой замер ← WebApp
📞 Связь с менеджером | ℹ️ О сервисе ← текст
⬅️ Сменить роль
«⬅️ Сменить роль» в любом меню → возврат к выбору роли.
Заменён inline-keyboard на reply-keyboard (постоянная панель снизу).
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.
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.
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
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
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
NEW HANDLERS:
- _handle_clients: groups manager's Leads by client (name or tg_id), returns
list with leads_count, last_lead_at, client_phone (extracted from checklist),
and full leads array per client. Sorted by recency desc.
- _handle_lead: fetches single lead with parsed checklist + ai_response JSON.
Validates ownership (manager_tg_id matches caller).
NEW ENDPOINTS:
- POST /api/clients — list of manager's clients with summary
- POST /api/lead — single lead detail with ai_response for re-render
Both accept initData auth, only manager role can call.
Apps Script compat: ?path=clients and ?path=lead also work.
NEW MODULE app/catalog.py:
- refresh_catalog(cats, sources, per_brand, delay) — runs parsers for seed brand+category pairs
- list_catalog(cat, tier, brand) — reads from Sheets
- list_for_ai(cats, tiers) — compact text for AI prompt context
- SEED_BRANDS_BY_TIER + CATEGORY_QUERIES — 22 brands × 8 cats = 176 combos
- Saves top-2 relevant results per (brand × cat), filters by brand presence in title
- Dedup by title hash within (cat, brand) bucket
SHEETS:
- ensure_sheet(name, headers) — auto-creates Catalog tab on first refresh
- Schema: id, category, brand, tier, model_name, search_query, price_min/max, image_url, source, url, last_seen_at
ENDPOINTS:
- POST /api/catalog/refresh?cat=X&per_brand=N — manual refresh (1 cat ~2-5 min)
- GET /api/catalog/list?cat=&tier=&brand= — read with filters
- GET /api/catalog/preview_ai?cats=fridge — debug what AI receives
AI PROMPT:
- Rule #0: if catalog passed in user prompt — MUST select only from there
- _build_catalog_context: filters by checklist.budget_preset → tier subset
(luxe→premium, premium→premium+middle, middle→middle, budget→middle+budget)
_handle_podbor:
- Loads catalog subset, appends to user_prompt as 'ДОСТУПНЫЙ КАТАЛОГ МОДЕЛЕЙ'
- AI 'выбирай ТОЛЬКО из этого списка' rule reinforced
NEXT: trigger refresh manually for 1 category (~3 min), then real podbor test
to verify AI uses catalog models instead of hallucinating SKUs
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 теперь короче и проще: тип → размер → ключевые параметры → готово