Commit Graph

26 Commits

Author SHA1 Message Date
wasrusgen
a4124c6b50 fix: открывать карточку клиента вместо списка подборов
- Заголовок 'История подборов' → 'Карточка клиента'
- Убрать инлайн-монтирование Proposals.mountManager в карточке клиента
- Оставить только кнопку 'Открыть →' для перехода к подборам
- Версия clients.js: 20260518a
2026-05-18 00:03:38 +03:00
wasrusgen
2f50f6e920 fix(miniapp): remove location.reload() on back-to-home navigation (splash bug)
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>
2026-05-17 00:39:12 +03:00
wasrusgen
0551f1fad0 fix: client list empty after create — add initDataUnsafe to fetchClients
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>
2026-05-16 10:14:42 +03:00
wasrusgen
4abd7b2ecd feat: proposal cycle — client brief + manager editor + voting
Backend:
- proposals.py: new module with full proposal cycle
  (brief → draft → sent → reviewed → done),
  Google Sheets «Proposals» tab, Telegram notifications
- main.py: import proposals_mod, 9 new /api/proposal_* routes
  added to both dispatch map and native /api/* handlers

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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 09:28:36 +03:00
wasrusgen
34ef51c4c8 ux: FAB вместо кнопки «Новый клиент» в списке клиентов
- Убрана широкая кнопка btn-primary над поиском
- Добавлен FAB (56×56, position:fixed, bottom-right) — кружок с «+»
  всегда виден поверх списка при любой прокрутке
- .client-list: padding-bottom 84px — последняя карточка не прячется
- Пустое состояние: обновлён текст («Нажмите + чтобы завести первого»)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 23:30:28 +03:00
wasrusgen
7a25ee3d36 feat: загрузка фото к замеру из карточки менеджера
- 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>
2026-05-15 22:44:45 +03:00
wasrusgen
5186afe0e0 feat: смена статуса замера из карточки + статус-бейдж
- backend: новый endpoint /api/measurement_set_status (cancelled / completed)
  только менеджер-владелец, только из requested/scheduled
- clients.js: renderMeasurement — статус-бейдж с цветом в шапке
  кнопки «Отметить выполненным» (requested) и «Отменить замер» (requested/scheduled)
  setMeasurementStatus() — confirm → API → reload
- podbor.css: .mz-status-badge, .mz-status-actions, .mz-status-btn, .ct-ok/.ct-err
- index.html: cache bump → v=20260514o

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-05-15 22:42:40 +03:00
wasrusgen
715ac96de8 feat: поиск клиентов на главном экране (фильтр по ФИО / телефону / договору)
- 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>
2026-05-15 22:40:03 +03:00
wasrusgen
bfd661575c feat: история примечаний — append-only лента вместо одной записи
- backend: _handle_client_note — запись всегда append (не upsert), чтение возвращает notes[]
- clients.js: renderClientNoteBlock переписан — лог-лента всех записей, «+ Добавить» открывает textarea
- podbor.css: .note-history, .note-entry, .note-loading, .note-empty
- index.html: cache bump → v=20260514m

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-05-15 22:16:47 +03:00
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
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
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
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
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