По чек-листу ЗАМЕРОВ (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.
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.
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.
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
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)
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
- 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
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
- 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
- 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