- Добавить проверку data.error после fetchClients() в renderClientHistory
- Сравнивать client_tg_id как строки (String(c.client_tg_id) === String(clientKey))
чтобы избежать 5937498515 !== '5937498515'
- Показывать явное сообщение если клиент не найден вместо пустой страницы
- Версия clients.js: 20260518b
- Заголовок 'История подборов' → 'Карточка клиента'
- Убрать инлайн-монтирование Proposals.mountManager в карточке клиента
- Оставить только кнопку 'Открыть →' для перехода к подборам
- Версия clients.js: 20260518a
Every "Назад" / "На главную" button was calling location.reload() which
triggered a full page reload → splash screen appeared again. Fix: replace
reload() with routeByHash() call (global router function from app.js) which
re-renders the role-appropriate home screen from cached window.__zovMe
without any network round-trips.
Affected files: app.js, clients.js, measurements.js, request.js,
assembly.js, podbor.js. Bump asset versions to 20260517b to bust cache.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fetchClients() was not sending initDataUnsafe, so on Telegram Desktop
(where initData can be unreliable) _handle_clients returned
{"error":"invalid_init_data"} and the frontend showed an empty list
instead of an error — making newly created clients appear missing.
- fetchClients(): add initDataUnsafe to request body (matches renderManagerHome pattern)
- renderList(): surface data.error explicitly instead of silent empty state
- _handle_client_create: gps_lat/gps_lng None → "" to avoid "None" strings in sheet
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>
- 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>
- 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.
Карточка клиента (#/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.
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.
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
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