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>
- Добавлен /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>
- 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>
- Убрана широкая кнопка btn-primary над поиском
- Добавлен FAB (56×56, position:fixed, bottom-right) — кружок с «+»
всегда виден поверх списка при любой прокрутке
- .client-list: padding-bottom 84px — последняя карточка не прячется
- Пустое состояние: обновлён текст («Нажмите + чтобы завести первого»)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Добавлены 4 детальных обучающих SVG в ZAMER_PICTS:
- wall1: фронтальный вид чистой стены — ширина L, высота H,
4 угла (ЛВ/ПВ/ЛН/ПН), дуга α° (замер угла), БАЗА: ПУ, Lc (середина)
- wall2: стена с дверным проёмом — общая L=A+Ш+B, высота двери В,
три сегмента ниже с цветовым разделением
- wall3: стена с окном + коммуникации — Ш_ОК/В_ОК/П (подоконник),
розетка R1 с двумя привязками (A горизонталь→ПУ, B вертикаль↑пол),
труба Wc1 с привязками C и D
- topview_comms: вид сверху с пунктирными линиями привязки точек
R1 и Wc1 к базовым углам стен
zamer-checklist.md: @pict:wall1 в разделе 2, @pict:wall3 в разделе 4,
@pict:wall2 в разделе 6, @pict:topview_comms в разделе 8
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- backend: _handle_measurement_add_photos — дозагрузка фото (до 20 шт за раз)
сохраняет в PHOTOS_DIR, дописывает в колонку photos через update_cell_by_key
- clients.js: renderPhotoUploadBlock — выбор файлов, превью сетка 3 колонки,
конвертация в dataURL, POST /api/measurement_add_photos, счётчик в заголовке
- podbor.css: .photo-upload-block, .photo-preview-grid, .photo-upload-actions
- index.html: cache bump → v=20260514p
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- clients.js: renderList — поле поиска над списком, renderFiltered() фильтрует без API-запроса
- фильтрация: имя (подстрока), телефон (только цифры), номер договора
- счётчик обновляется: «Найдено N из M» при поиске, полная статистика без запроса
- podbor.css: .client-search-wrap, .client-search, .client-search-meta
- index.html: cache bump → v=20260514n
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- measurements.js: renderClientPicker — одно поле адреса заменено на addr-grid
- добавлен локальный _splitAddr() — разбирает строку адреса обратно в поля
- при выборе клиента из пикера адрес автоматически раскладывается по полям
- каждое поле собирает state.address через readAndSaveAddr()
- index.html: cache bump → v=20260514l
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- 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>
- 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>
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>
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>
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
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
#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.
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.
I: sawWobble — весь логотип покачивается ±1.5° каждые 5.5с
(имитация работающего инструмента). Анимация на wrapper,
чтобы не конфликтовать с breathing scale на самом SVG.
K: splash-dust — 8 опилок-точек разлетаются от пилы по
индивидуальным траекториям (--dx/--dy через CSS-vars),
разные delays и durations для естественности.
Cache bust v=20260513zk.
- .loader: grid → flex column. Теперь элементы плотным
кластером в центре, а не разброс по высоте.
- Лого max-width 360px → 260px (как просил, чуть меньше)
- Добавил drop-shadow под лого для глубины
Cache bust v=20260513zj.
Перерисовал SVG-лого ближе к референсу из исходника:
- Пила компактнее, шире, наклонённый корпус
- Wordmark @WASRUSGEN1 крупнее (115pt Inter Black)
- Тесная связка пила↔wordmark — один знак
- Цвет: золотой #C9A227
Удалил неиспользуемые PNG/JPG из бета-версии.
Cache bust v=20260513zg.
Полностью векторный логотип (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.
«CRM» в курсиве смотрелось бы странно — заменил на штамп-стиль:
Inter Black 14pt uppercase, оранжевая обводка, letter-spacing
0.32em. Цветовая гамма та же (оранжевый #F08720).
Везде где было «сборщик» в подписях → теперь «CRM».
Cache bust v=20260513ze.
Убраны 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.