280cfcd6cf54bd8a92ad3bbca254a8c8dbf3fe3d
23 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
79ff60ffd9 |
refactor(frontend): Sprint 4 Phase B/3 — split 2 utility views (audit O-refactor-04 хвост)
ErrorView 320→178 (+ ErrorBrand 54 + ErrorIllustration 31 + ErrorActions 55 + ErrorMeta 102).
DashboardView 302→84 (+ DashboardPageHead 65 + DashboardKpiRow 97 + DashboardBalance 124).
State (config, errorCode в ErrorView; range/kpis/balance в DashboardView) остаётся
в parent ради единого route.meta-driven flow + future API-fetch'а (Phase B/1 паттерн).
DashboardPageHead использует Vue 3.5 defineModel<T>() для двусторонней привязки range.
Sub-components читают только props — без Pinia stores (mock-data flow).
Все sub-components <250 строк (acceptance threshold). Shell line counts: 178/84.
ЗАМЕЧАНИЕ по acceptance «0 components >300»: НЕ закрыто полностью. 2 файла остались
выше порога — DealsView 560 + DealDetailDrawer 386. Зафиксировано в Sprint 3 Phase C
commit
|
||
|
|
849bc73290 |
refactor(frontend): Sprint 4 Phase B/2 — split 3 user views (audit O-refactor-04 хвост)
BillingView 416→114 (+ BalanceCard 155 + TransactionsTable 113 + InvoicesTable 90 + billingFormatters 51 composable: formatPlain/formatCost/statusChipColor/ statusLabel/formatLabel/formatIcon/txAmountClass). SecurityTab 354→39 (+ ChangePasswordCard 17 + TwoFactorCard 218 + RecoveryCodesCard 104 + SessionsTable 66; auth-store читается напрямую в каждом sub-component). RemindersView 345→183 (+ RemindersFilters 51 + RemindersList 173; ReminderDialog уже отдельный с прошлой фазы — служит как ReminderForm). State (`activeTab`, `editingReminder`, `deletingReminderId` в RemindersView) остаётся в parent ради единого reload-flow + confirm-dialog'ов. Auth-store читается напрямую в TwoFactorCard/RecoveryCodesCard через useAuthStore() — без prop-drilling. Reminders-store читается напрямую в RemindersFilters/ RemindersList. Все sub-components <250 строк (acceptance threshold). 3 view-shells: 114/39/183. Регрессия: ESLint 0 + vue-tsc 0 + Vitest 416/416 + build OK 968 ms. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
30ef61dff8 |
refactor(frontend): Sprint 4 Phase B/1 — split 3 admin/layout views (audit O-refactor-04 хвост)
3 view'а с >300 строк разделены на shell + sub-components: AdminTenantsView 377→155 (+ TenantsStatsHeader 82 / TenantsFilters 93 / TenantsTable 116). AdminTenantDetailView 436→109 (+ TenantDetailHeader 158 / TenantDetailTabs 176 + adminTenantDetailFormatters 43 composable). AppLayout 466→78 (+ AppSidebar 155 / AppTopbar 269; R0.6 hard-стоп снят явным запросом заказчика 10.05.2026). State (filterStatuses, tenantsState, activeTab, tenant, drawerOpen) остаётся в parent view'ах ради `defineExpose`-контракта Vitest тестов. Sub-components читают Pinia stores напрямую (auth + notifications + reminders) — без prop-drilling. AppTopbar 269 строк <300 — acceptance threshold выдержан (можно дальше split на NotificationsDropdown + UserMenu в отдельном flow, не критично). Регрессия: ESLint 0 + vue-tsc 0 + Vitest 416/416 + build OK 1.17 сек. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
6c2f0ce682 |
refactor(frontend): Sprint 3 Phase C — Top-3 Vue components split (audit O-refactor-04)
Sprint 3 Phase C. Закрытие audit O-refactor-04 (частичное — Top-3 из 12): - DealsView.vue: 852 → 560 строк. Выделены: DealsFilters (123), DealsBulkBar (150), DealsTable (165). CSV-utilities (csvEscape/triggerCsvDownload/triggerBlobDownload/ buildCsvString) вынесены в composables/useCsvDownload.ts. - ReportsView.vue: 592 → 261 строк. Выделены: ReportRequestForm (тип отчёта, даты, фильтры, формат, submit/reset), ReportJobsList (список заданий со статусами и actions retry/cancel/delete). - DealDetailDrawer.vue: 580 → 386 строк. Выделены: DealDetailHero (header + phone-link + status-chip), DealDetailTimeline (activity log с MOCK_EVENTS). Comment- и Reminders-секции оставлены inline — связаны с API и defineExpose. DealsView и DealDetailDrawer остались выше 350-целевого уровня: bulk-action функции (applyBulkStatus/applyBulkDelete/applyBulkExport/undoBulkDelete/ applyBulkRestoreFromTrash) и comment/reminders fetch — экспонируются через defineExpose в Vitest-тестах напрямую, дальнейшая декомпозиция требует изменения тест-контракта (отдельным flow). Layout-структуры (AppLayout 466, AuthLayout, AppShell) НЕ ТРОНУТЫ — R0.6 hard-стоп. Остальные 9 components >300 строк (AdminTenantDetailView, BillingView, AdminTenantsView, SecurityTab, RemindersView, ErrorView, DashboardView, ImpersonationDialog, далее) — вне scope Sprint 3, отдельным flow по запросу. vue-tsc: 0 errors. ESLint: 0. Vitest: 416/416 PASS. Build: success (1.15s). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
2d7d7d1188 |
feat(frontend): Sprint 2 Phase B — Vue 3.5 defineModel + Vuetify 3.12 typed slots + lazy-imports + ESLint check
Sprint 2 Phase B (modernization). Закрытие audit O-stack-04/05/07 + O-perf-06:
- O-stack-04: Vue 3.5 defineModel() в 3 диалогах (NewDealDialog,
ImpersonationDialog, ReminderDialog) — boilerplate −5 строк/файл.
+ useTemplateRef() в TwoFactorView (input v-for refs).
- O-stack-05: Vuetify 3.12 типизированные слоты VDataTable
(DealsView + AdminTenantsView) — inline-аннотации `{ item }: { item: T }`
на 6+7 scoped-slot bindings; vue-tsc проверяет доступ к полям статически.
- O-stack-07: ESLint flat-config verified — header-comment добавлен
в eslint.config.js. Legacy .eslintrc.json не используется.
- O-perf-06: defineAsyncComponent() для тяжёлых диалогов в 3 местах:
DealsView (DealDetailDrawer + NewDealDialog), DealDetailDrawer
(ReminderDialog), RemindersView (ReminderDialog). KanbanView оставлен
sync — async-загрузка приводила к EnvironmentTeardownError в jsdom
(KanbanView.spec.ts), see in-file comment. Сборка показывает chunk'и
ImpersonationDialog (7.61 kB), DealDetailDrawer (11.12 kB), NewDealDialog
и ReminderDialog как отдельные lazy-bundles.
vue-tsc: 0 errors. ESLint: 0. Vitest: 416/416 PASS. Build: success.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
dc1457a008 |
phase2(reminders-frontend): RemindersView + DealDetailDrawer + nav-badge
P0 этап 5 — frontend для reminders (после backend-этапа 4). Пользователь может создавать/просматривать/завершать/удалять напоминания из UI с inline-create в DealDetailDrawer. Frontend: - api/reminders.ts: типизированные helpers для 5 endpoints + ensureCsrfCookie для mutating. Types ReminderFilter/ApiReminder/ReminderCounts. - stores/reminders.ts: Pinia с items/counts/currentFilter + load/refreshCounts/create/update/complete/remove. Optimistic для complete/remove с revert на reject. - components/reminders/ReminderDialog.vue: dual-mode (create/edit) modal с native datetime-local input. Props dealId?/reminder? (edit), ISO-конверсия при submit. - views/RemindersView.vue: page-head с stats (active/overdue) + reload-btn; 4 tabs (today/upcoming/overdue/completed) с counts на бейджах (overdue=error color); v-list с complete-btn / dropdown Изменить/Удалить с confirm-dialog; empty-state. - router: /reminders маршрут (lazy). - AppLayout: nav-badge «Напоминания» биндит count из store (replace static «12»); скрыт при count=0; polling 60 сек для refreshCounts. - DealDetailDrawer: секция «Напоминания» (только при tenantId+deal): inline + create-btn / список / complete / встроенный ReminderDialog. Vitest +20 (369/369 за 21.20 сек): - reminders-store 11: initial / load+reject / refreshCounts / create+reject / complete optimistic+revert / remove+reject / reset. - RemindersView 7: mount / 4 tabs / counts / empty-state / список / reload-btn / filter=today default. - AppLayout +2: бейдж скрыт при count=0 / показывает count при >0. Pest 347/347 (без изменений — backend нетронут). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
7e1bf8b42d |
phase2(deal-patch): PATCH /api/deals/{id} + comment-editor в DealDetailDrawer (этап 1/5)
Drawer из read-only становится editable. ActivityLog event пишется на
каждое изменение поля.
Backend (DealController::update):
- PATCH /api/deals/{id} {tenant_id, comment?, manager_id?, status?}.
- Каждое изменённое поле → ActivityLog:
comment → deal.commented (context.text);
manager_id → deal.assigned (context.from/to + assigned_at=NOW);
status → deal.status_changed (context.from/to/source='manual').
- NO-OP не пишется в audit. Manager FK guard + status slug validation.
- RLS + defense-in-depth where(tenant_id) → 404 для чужой сделки.
Pest +10 (DealUpdateTest):
- 422/404 базовые / 404 чужая сделка / comment+audit / manager+audit+
assigned_at / status+audit / 422 неизвестный slug / 422 чужой manager /
NO-OP не пишет / комбинированно → 2 audit записи.
Frontend:
- api/deals.ts::updateDeal — PATCH helper c ensureCsrfCookie.
- DealDetailDrawer: новая секция «Комментарий» (только при tenantId).
v-textarea auto-grow + counter=5000 + Save-btn → updateDeal →
toast success + reload events (новый deal.commented в timeline).
На fail → warning toast.
Vitest +3 (DealDetailDrawerApi):
- saveComment вызывает updateDeal + toast + reload events (getDeal x2).
- saveComment reject → commentSaveError + warning toast.
- comment-section не рендерится без tenantId.
PHPStan baseline регенерирован.
Регресс:
- Lint+type-check+format passed.
- Vitest 283/283 за 18.13 сек (+3 от 280).
- Vite build 1.12 сек.
- Pint + PHPStan passed.
- Pest 220/220 за 25.64 сек (+10 от 210, 871 assertion).
Реестр v1.64→v1.65 / CLAUDE.md v1.55→v1.56.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
e31ea5354a |
phase2(lead-statuses): GET /api/lead-statuses + Pinia-store с snapshot fallback
Заменяет static-снапшот LEAD_STATUSES в коде на live-данные из БД. Custom slug'и (добавленные после deployment'а) теперь видны UI без rebuild'а. Backend: - LeadStatus model (PK=slug string, incrementing=false, timestamps=null). - LeadStatusController::index — GET /api/lead-statuses, ORDER BY sort_order, slug. Таблица глобальная (не tenant-aware), auth не требуется на MVP. Pest +5 (LeadStatusesIndexTest): - 200 + не пустой / все 14 системных slug'ов из seed / все нужные поля / sort_order ASC / кастомный slug после INSERT появляется в endpoint'е. Frontend: - api/leadStatuses.ts::listLeadStatuses — GET helper. - stores/leadStatuses.ts::useLeadStatusesStore — Pinia setup-store: statuses default = LEAD_STATUSES snapshot (UI работает без fetch'а), load(force=false) идемпотентен, bySlug computed Map, findBySlug helper. На fail — snapshot остаётся, fetchError=true. - DealsView/KanbanView/DealDetailDrawer переехали со static-импорта LEAD_STATUSES на store. KanbanView использует safe-access dealsByStatus[slug] || [] (защита от custom slug'а из API без seeded column). load() в onMounted у обоих view'ов. Vitest +7 (leadStatusesStore.spec.ts): - initial snapshot / findBySlug existing & null / load success replace + loaded / load reject — fetchError + snapshot fallback / load идемпотентен / load(force=true) refetch. - 2 spec'а DealDetailDrawer получили setActivePinia(createPinia()) в beforeEach (без этого Pinia store-injection в jsdom падает). PHPStan baseline регенерирован. Регресс: - Lint+type-check+format passed. - Vitest 280/280 за 19.44 сек (+7 от 273). - Vite build 1.17 сек. - Pint + PHPStan passed. - Pest 210/210 за 24.59 сек (+5 от 205, 840 assertions). Реестр v1.63→v1.64 / CLAUDE.md v1.54→v1.55. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
cba76c5d18 |
phase2(deal-show): GET /api/deals/{id} + DealDetailDrawer на реальный ActivityLog
Закрывает gap «timeline в drawer'е показывает hard-coded MOCK_EVENTS» —
теперь drawer fetch'ит реальные activity-events на open из tenant-filtered
activity_log. Без tenant_id — fallback на MOCK_EVENTS как раньше.
Backend (DealController::show):
- GET /api/deals/{id}?tenant_id={id} — возвращает {deal, events}.
- Deal extended (project_name, manager_name/initials, comment, assigned_at).
- Events — последние 50 записей activity_log по (tenant_id, deal_id)
ORDER BY created_at DESC, с актором (user через belongsTo).
- RLS-обёртка + defense-in-depth where(tenant_id) — 404 если чужая.
Pest +8 (DealShowTest):
- 422/404 базовые / 404 чужая сделка / deal-relations / events ORDER BY +
actor + actor=null для system-event / RLS+app-фильтр изоляция событий /
лимит 50 событий.
Frontend:
- api/deals.ts::getDeal — типизированный helper c ApiDealEvent/Detail/Response.
- composables/dealsApiMapper.ts::mapApiDealEvent — converter ApiDealEvent →
DealEvent: clamp event-slug на known types с fallback на 'deal.viewed';
detail зависит от type (status_changed: 'from → to'; created: source;
остальные: JSON-сводка context).
- DealDetailDrawer: optional tenantId prop, watch([open, deal.id, tenantId])
с immediate=true → loadEvents() на open. Reject → eventsFetchError +
v-alert warning + MOCK_EVENTS fallback.
- DealsView/KanbanView передают :tenant-id="auth.user?.tenant_id".
Vitest +4 (DealDetailDrawerApi.spec.ts):
- Без tenantId — getDeal не вызывается + MOCK_EVENTS видны.
- С tenantId — getDeal + events замещены + 'new → paid' виден.
- reject → fetchError + alert + MOCK_EVENTS fallback.
- open=false → getDeal не вызывается.
PHPStan baseline регенерирован.
Регресс:
- Lint+type-check+format passed.
- Vitest 273/273 за 20.76 сек (+4 от 269).
- Vite build 1.12 сек.
- Pint + PHPStan passed.
- Pest 205/205 за 24.19 сек (+8 от 197, 812 assertions).
Реестр v1.62→v1.63 / CLAUDE.md v1.53→v1.54.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
515114cff5 |
phase2(lookups+integrity): GET /api/managers+projects + manager FK guard + SupplierLeadCost для manual
3 интеграционных доработки после backend-completion v1.57.
(1) GET /api/managers + /api/projects + manager FK guard:
- ManagerController::index — active users тенанта (is_active+deleted_at IS NULL).
Формат {id, email, first_name, last_name, name, initials} с
formatName/formatInitials helpers (fallback на email).
- ProjectController::index — active projects (is_active=true).
- Оба endpoint'а: tenant_id query-param, 422 без, 404 unknown, RLS-обёртка.
- DealController::store FK guard: manager_id должен принадлежать tenant'у +
is_active. Иначе 422 (закрывает security-gap чужого менеджера).
- Pest +8 в LookupsTest.
(2) Replace MOCK_MANAGERS / MOCK_PROJECTS на API в NewDealDialog:
- projectOptions/managerOptions ref'ы с MOCK fallback.
- loadLookups через Promise.all([listProjects, listManagers]) на open
диалога с tenantId.
- managerIdByName Map name→id для submit'а.
- Silent fallback на mock при network-error.
- Vitest +2.
(3) SupplierLeadCost для manual-leads:
- В DealController::store после Deal::create — resolveSupplierId (копия
логики ProcessWebhookJob: project_suppliers JOIN suppliers + ORDER BY
sort_order). Если supplier найден — SupplierLeadCost с snapshot cost_rub
+ supplier_lead_id=NULL (manual: нет внешнего id).
- Manual по-прежнему НЕ списывает баланс (Ю-2 reseller-модель — charge
только при webhook'е); cost-аналитика всё равно нужна.
- Pest +2.
- TODO: рефактор resolveSupplierId в App\Services\SupplierResolver чтобы
Job + Controller разделяли логику.
Старый тест manager_id=42 переписан под FK guard через User::factory.
PHPStan baseline регенерирован (+28 ignored Pest TestCall warnings).
Регресс: lint+type-check+format ✅; vitest 247/247 за 16.32 сек (+2);
vite build 951 ms; Pint+PHPStan passed; Pest 166/166 за 22.11 сек
(+10 от 156, 699 assertions). Реестр v1.57→v1.58, CLAUDE.md v1.48→v1.49.
Production TODO остаточные:
- resolveSupplierId → SupplierResolver service.
- XLSX-export через PhpSpreadsheet.
- GET /api/deals для replace MOCK_DEALS в DealsView/KanbanView.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
83bb9de2bb |
phase2(backend-completion): POST /api/deals + webhook_hmac_required + POST /api/deals/export
3 backend-completion после tightening v1.56.
(1) POST /api/deals — manual create endpoint:
- DealController::store. Project firstOrCreate (type='manual'). Deal с
source_crm_id=NULL. RLS-обёрнутая транзакция.
- Manual НЕ списывает баланс / НЕ дедуп / НЕ SupplierLeadCost.
ActivityLog с context.source=manual.
- NewDealDialog получил optional tenantId prop. С tenantId — POST → backend-id;
на error fallback на local-id + warning + dialog open.
- DealsView/KanbanView передают auth.user?.tenant_id.
- Pest +8.
(2) webhook_hmac_required flag в system_settings:
- Seed-row в db/schema.sql (default false backward-compat).
- WebhookReceiveController::isHmacRequired private helper.
- При true: запрос без X-Webhook-Signature → 401.
- Pest +3.
(3) POST /api/deals/export — backend CSV:
- DealController::export. Валидация ids[1-10000]. RLS-обёрнутый whereIn.
- Excel-friendly CSV: BOM "\u{FEFF}" PHP-литерал, ; разделитель, \r\n.
- text/csv attachment headers.
- Frontend applyBulkExport: backend → fallback на client-side
(buildLocalCsv вынесен).
- Pest +4.
Vitest +3 (всего 245/245).
PHPStan убрал лишнюю Deal->id===null проверку (Eloquent int).
DealsView/KanbanView spec'ы получили setActivePinia.
Регресс: lint+type-check+format ✅; vitest 245/245 за 17.07 сек (+3);
vite build 1.04 сек; Pint+PHPStan passed; Pest 156/156 за 20.27 сек
(+15 от 141, 675 assertions). Реестр v1.56→v1.57, CLAUDE.md v1.47→v1.48.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
768628d914 |
phase2(7-features): bulk-actions / new-deal / tenant-card / system-edit / webhook / smart-filters / impersonation-list
7-фичный auto-mode пакет согласно «карте что осталось» (после v1.54).
(1) Bulk-actions DealsView:
- dealsState reactive-копия MOCK_DEALS (deep-clone) для безопасного bulk-edit.
- Bulk-bar (sticky, теало-нуар, theme=dark) при selected.length > 0:
count + Сменить статус (v-menu × 14 lead_statuses) + Экспорт (snackbar) +
Удалить (v-dialog confirm) + ✕ clear.
- На production: smart status-transition с проверкой allowed-переходов;
soft-delete (архив 30 дней); реальный CSV/XLSX export через xlsx-lib.
(2) NewDealDialog (used in DealsView+KanbanView):
- 6 полей: name/phone/project (MOCK_PROJECTS) / manager (MOCK_MANAGERS) /
cost / status (default 'new' или presetStatus). Phone-валидация ≥10 цифр.
- emit('created', deal) → DealsView push в начало dealsState; KanbanView push
в правильную колонку по statusSlug + totalDeals++.
(3) AdminTenantDetailView (/admin/tenants/:code):
- 4 KPI cards (Баланс/runway / Тариф+MRR/мес / Лиды сегодня+неделя+месяц /
Средняя цена). 4 v-tabs: Финансы (balance-history) / Пользователи /
Проекты / Активность с event-кодами.
- Кнопка «Войти как клиент» (использует ImpersonationDialog из v1.54).
404-fallback. composables/mockTenantDetail.ts с expandTenantDetail.
- AdminTenantsView получил @click:row → router.push.
(4) Edit-flow AdminSystemView (audit-log + 2-step):
- Backend: SystemSetting + SaasAdminAuditLog Eloquent (append-only,
payload_before/after JSONB casts).
- AdminSystemSettingsController с GET (list) + PUT (update в DB::transaction
+ INSERT в saas_admin_audit_log; hash-chain trigger BEFORE INSERT
заполняет log_hash).
- Type-validation: int/decimal/bool/json. Reason ≥30 chars. No-op → 422.
- Frontend SystemSettingEditDialog — 3-step (edit → confirm с diff
before/after → done).
(5) Webhook receive endpoint (POST /api/webhook/{token}):
- WebhookReceiveController::receive. Token = tenants.webhook_token.
- 404 unknown / 422 bad payload / 202 success + dispatch ProcessWebhookJob.
- Stub-INSERT в webhook_log через DB::table обёрнут в DB::transaction +
SET LOCAL app.current_tenant_id для RLS.
- CSRF-исключение для api/webhook/* в bootstrap/app.php.
- На prod: + HMAC X-Webhook-Signature + per-token rate-limit.
(6) Smart-filters:
- DealsView: multi-select v-select Проект+Менеджер с auto availableProjects/
availableManagers computed.
- AdminTenantsView: filterStatuses (4 STATUS_OPTIONS) + filterTariffs
(computed availableTariffs).
- Кнопка «Сбросить» появляется только когда фильтры активны.
(7) AdminImpersonationView (/admin/impersonation):
- Backend +2 GET endpoints: /active (used_at != null AND session_ended_at
== null) + /recent (last 20 завершённых с duration_seconds через
abs(diffInSeconds) — Carbon signed по умолчанию).
- ImpersonationToken получил belongsTo(Tenant).
- Frontend view: 2 секции (Активные с end-кнопкой / Недавно завершённые
read-only) + refresh + onMounted load.
- Маршрут /admin/impersonation + 5-й nav-пункт «Impersonation» в AdminLayout.
Vitest +48 (всего 238/238 за 15.31 сек).
Pest +16 (всего 136/136 за 15.8 сек, 495 assertions).
PHPStan baseline регенерирован (0 errors после фикса nullsafe.neverNull).
Регресс: lint+type-check+format ✅; vite build 937 ms; Pint+PHPStan passed;
Pest 136/136. Реестр v1.54→v1.55, CLAUDE.md v1.45→v1.46.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
61afa72591 |
phase2(impersonation-ui): UI dialog для Ю-1 в AdminTenantsView (frontend)
Закрывает TODO из v1.44 — frontend для Impersonation backend (`1963694`).
api/admin.ts:
- impersonationInit/Verify/End — типизированные axios-helpers для трёх
endpoint из v1.53. Все три — ensureCsrfCookie + apiClient.post.
На prod автоматически перейдут под middleware('auth:saas-admin').
components/admin/ImpersonationDialog.vue — 4-step state-machine:
- step 1 «reason»: v-textarea ≥30 chars + counter + hint «Ещё N символов».
- step 2 «verify»: info-alert email клиента + 6-digit input
(autocomplete=one-time-code) + dev-banner с _dev_plain_code.
- step 3 «active»: success-alert + кнопка «Завершить сессию».
- step 4 «done»: финальный success.
- persistent dialog (нельзя закрыть кликом за пределами — audit trail).
- watch(modelValue) сбрасывает state при каждом открытии.
AdminTenantsView:
- 8-я колонка actions (width=56) с v-tooltip + icon-btn mdi-account-switch.
- :disabled на suspended (по ТЗ §22.7 — только активные tenant'ы).
- @click.stop, data-testid=impersonate-btn-{id}.
- ADMIN_USER_ID=1 заглушка (на prod удалится — backend возьмёт из auth).
Vitest +11 (всего 190/190 за 13.23 сек):
- ImpersonationDialog.spec.ts (7): hide когда modelValue=false; step-1 mount;
reason<30 показывает counter; init→step2 (email+dev-banner); verify→step3
(end-btn); 5-digit code не вызывает API; end→step4; Cancel emit.
- AdminTenantsView.spec.ts (+4): impersonate-btn в каждой строке; suspended
disabled; click открывает диалог с правильным tenant; props.requestedBy=1.
Vitest quirk: v-dialog/v-tooltip требуют layout-injection — stub'ы
VDialog как passthrough <div v-if="modelValue"><slot/></div>, VTooltip как
<div><slot name="activator" :props="{}"/></div>. ImpersonationDialog
stub'ится в AdminTenantsView spec. api/admin + helpers extractValidationErrors/
extractErrorMessage мокаются через vi.mock — axios.isAxiosError(plain Error)
в jsdom возвращает false (паттерн из auth-store.spec.ts).
Production TODO: SaaS-admin auth (Yandex 360 SSO, Б-1) → middleware,
two-person approval (CTO-15/Ю-9), MailService → _dev_plain_code исчезает,
live cookie-swap session, страница «Активные impersonation-сессии».
Регресс: lint+type-check+format+build OK (924 ms; AdminTenantsView lazy-chunk
20.68 KB включает inline ImpersonationDialog); Vitest 190/190 за 13.23 сек;
Pest 120/120 за 15.69 сек (нетронут). Реестр v1.53→v1.54, CLAUDE.md v1.44→v1.45.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
da65cf4bf7 |
phase2(admin): AdminLayout + AdminTenantsView - админка SaaS (12/13 концептов)
- AdminLayout: отдельный sidebar теало-нуар с под-брендом ADMIN (red error 10px JBM uppercase) + 4 nav (Тенанты 142 / Биллинг / Инциденты 3 / Система) + topbar с crumb «Админка → currentPage» + admin-user-chip с error-color avatar. - AdminTenantsView (/admin/tenants): page-head + 5-stats + Экспорт + search/Статус/Тариф фильтры + v-data-table 7 колонок (Тенант с двухстрочным name+inn / Статус-chip 4 цвета / Тариф / Баланс ₽ с error-color при <0 / Желаем×факт / MRR с «—» / Активность). - mockTenants.ts соответствует schema v8.7 §3: 4 статуса × 5 тарифов, 7 mock с разнообразием (active/trial/overdue/suspended) + AdminStats (142/128/9/5/ 1 248 600 ₽). - AdminPlaceholderView универсальный для Биллинг/Инциденты/Система с описаниями ссылающимися на schema v8.7 (incidents_log §9 / system_settings §10). - AppShell расширен meta.layout='admin'. Router: /admin redirect на /tenants + 4 admin-route'а с lazy-imports. Web.php fallback покрывает /admin/*. - cspell-words.txt: Екб. Vitest +11 (всего 129/129 за 10.02s): - заголовок + 5 stats (regex nbsp в 1 248 600 ₽) + 7 columns + 7 rows + Окна Москва ИНН + overdue −1 200 + trial 4 дня + suspended + search filter «Натяжные» → 1 row + Экспорт/Статус/Тариф кнопки. Регресс: lint+type+format OK; vitest 129/129; vite build (admin views в lazy-chunks; main 104.99 KB); story:build 21/28 за 30.32s; Pest 48/48 за 4.89s. CLAUDE.md v1.30->v1.31, реестр Открытых_вопросов v1.39->v1.40. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
034657788d |
phase2(errors): ErrorView 404/403/500 + Laravel fallback
- ErrorView универсальный с конфигурацией через route.meta.errorCode
(404/403/500). По v8_errors.html: full-bleed теало-нуар bg, top-brand,
err-code 96px JBM с accent на средней цифре, title/desc, 2 actions,
опциональные status-list (500) и err-id с copy-btn (403/500).
- AppShell: meta.layout='error' → RouterView напрямую (ErrorView сам
предоставляет v-app).
- Router: /403, /500, catch-all /:pathMatch(.*)* → ErrorView с meta.errorCode.
- web.php: явные Route::view + Route::fallback (срабатывает после Pest
runtime-routes, не ломает SetTenantContextTest).
- cspell-words.txt: резолвится, роуты.
Vitest +8 (всего 118/118 за 9.39s):
- 404 default + 403 с REQ-ID + 500 с INC-ID + status-list (API/Telegram/YooKassa) +
404 actions (На дашборд + Назад) + 403 mailto-link + 500 status-link +
brand-блок + 404 НЕ содержит REQ/INC/status-list (regression-guard).
- stubs:{VApp/VMain} как passthrough — обходим Vuetify layout-injection в jsdom.
Регресс: lint+type+format OK; vitest 118/118; vite build (ErrorView lazy-chunk;
main app-chunk 101.01KB упал на 7KB благодаря shared chunk'ам); story:build
19/26 за 30.96s; Pest 48/48 за 4.88s (fallback не сломал runtime-routes).
CLAUDE.md v1.29->v1.30, реестр Открытых_вопросов v1.38->v1.39.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
45239f6602 |
phase2(deal-drawer): DealDetailDrawer - правая панель с деталями сделки
- DealDetailDrawer (v-navigation-drawer right temporary 480px):
- hero (#id eyebrow + name h5 + close + tel:link + clock + status-chip)
- section Параметры (2-col grid: Проект/Стоимость/Менеджер/Источник)
- section Активность (timeline 6 events с iconified vertical-line)
- mockDealEvents.ts: 6 mock-events (created/balance_charged/assigned/viewed/
status_changed/commented) - соответствуют ActivityLog event-константам v8.7.
- Интеграция в DealsView (@click:row) и KanbanView (через @open-deal от карточки).
- cspell-words.txt: iconified, мапы, резолвятся, резолвером, stub'ить, инлайнен.
Vue3 quirk: v-navigation-drawer требует layout-injection от v-app/v-layout,
но в Vitest vite-plugin-vuetify auto-import не работает. Решение:
- DealsView/KanbanView тесты: stubs:{DealDetailDrawer:true}
- DealDetailDrawer тесты: stubs:{VNavigationDrawer:passthrough-div}
Vitest +8 (всего 79/79 за 7.57s):
- DealDetailDrawer 8 (open=false скрытие, deal=null no-content, hero+id,
tel:link, status-chip, params, timeline 6 items, emit update:open(false)).
Регресс: lint+type+format OK; vitest 79/79; vite build (drawer инлайнен в
DealsView+KanbanView lazy-chunks); story:build 15/22 за 31.55s; Pest 48/48.
CLAUDE.md v1.25->v1.26, реестр Открытых_вопросов v1.34->v1.35.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
c8131a39c8 |
phase2(kanban-dnd): vuedraggable@4 - drag-and-drop карточек между 14 колонками
- vuedraggable@^4.1.0 + sortablejs@1.14.0 (--legacy-peer-deps). - KanbanColumn: <draggable v-model="localDeals" group="kanban-deals" item-key="id" ghost-class="ghost-card" drag-class="drag-card" animation="150"> + #footer empty-state «пусто · перетащите сюда». - DraggableChangeEvent типизирован (added/removed/moved discriminated union). - KanbanView: const → reactive<Record<slug, MockDeal[]>> (vuedraggable v-model требует независимые arrays); shallow-clone {...d} чтобы не мутировать MOCK_DEALS. - onColumnChange: при event.added → element.statusSlug = targetSlug. TODO: POST /api/deals/{id}/transition с проверкой allowed-переходов. - cspell-words.txt: vuedraggable, симулируется. Visual: ghost-card opacity 0.4 + ivory-tint bg, drag-card rotate 1deg. Vitest +1 (всего 71/71 за 7.48s): эмулирует $emit('change', {added}) → проверяет statusSlug update. Полный mouse-DnD не симулируется — JSDOM не умеет drag-events, но event-handler логика покрыта. Регресс: lint+type+format OK; vitest 71/71; vite build (KanbanView lazy-chunk вырос до 180.53KB - SortableJS-обёртка, грузится только на /kanban); story:build 14/20 за 30.45s; Pest 48/48 за 4.88s. CLAUDE.md v1.24->v1.25, реестр Открытых_вопросов v1.33->v1.34. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
d39934c8d9 |
phase2(kanban): KanbanView - 14 колонок по lead_statuses (БЕЗ DnD)
- KanbanCard: компактная карточка (name/phone/project/cost/manager-avatar),
emit('open',id) на click для будущего DealDetailDrawer.
- KanbanColumn: header с border-top по colorHex статуса (--accent CSS-var) +
name+count+total ₽; body с v-for карточек + empty-state «пусто».
- KanbanView: orchestrator, 14 колонок (по LEAD_STATUSES) с группировкой
MOCK_DEALS по statusSlug, horizontal-scroll с custom scrollbar.
- Маршрут /kanban (meta.layout=app) в router + web.php.
- .gitleaks.toml: tests/Frontend/*.spec.ts в allowlist (assertion на mock-телефоны).
- cspell-words.txt: инлайн, vueuse.
DnD НЕ реализован на MVP - отдельный коммит после выбора библиотеки
(vue-draggable-next или @vueuse/integrations/useSortable).
Vitest +14 (всего 70/70 за 7.37s):
- KanbanCard 3 (data + initials + emit open)
- KanbanColumn 5 (header + total + empty + accent CSS-var case-insensitive +
проброс openDeal)
- KanbanView 6 (заголовок + 14 columns + правильные status'ы + stats + кнопка +
DnD-предупреждение)
Регресс: lint+type+format OK; vitest 70/70; vite build (KanbanView lazy-chunk);
story:build 14/20 за 31.17s; Pest 48/48 за 5.06s.
CLAUDE.md v1.23->v1.24, реестр Открытых_вопросов v1.32->v1.33.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
290e7dbc34 |
phase2(charts): ActivityChart + FunnelChart - Dashboard закрыт по дизайну
- ActivityChart: native SVG line chart (без chart-library, чтобы не +400KB зависимость для статичных дашборд-графиков). Y-grid 5 линий, area-gradient, 7 точек (предпоследняя выделена primary teal как «сегодня»), 3 tabs (Принято/Оплачено/Отказ). - FunnelChart: segmented bar по 14 статусам + funnel-list (sort desc). - composables/leadStatuses.ts: snapshot 14 статусов из db/schema.sql:2130 (НЕ из BRANDBOOK §3.6 - расхождение #1 handoff vs ТЗ из реестра v1.13). 14 правильных slug'ов: new/viewed/worked/base/missed/negotiations/ waiting_payment/partnership/paid/closed/test_drive/hot/replacement/final_missed. - DashboardView интегрирует оба чарта в charts-row (md=7+5). - cspell-words.txt: ldot, композаблом, инлайнингом, инлайнены. Vue compiler quirk: withDefaults factory не разрешает референсить module-level const'ы (checkInvalidScopeReference). Обходим инлайнингом литерала. Vitest +13 тестов (всего 48/48 за 5.5s): - ActivityChart 6 (3 tabs + 7 circles + 'сегодня' + custom points + legend) - FunnelChart 7 (14 segments + 14 list-items + assertion на отсутствие 'Думает'/'Спам' из handoff + сортировка + colorHex + total) Stories +2 с 3 variants каждый (Histoire 10/14 за 30.43s). Регресс: lint+type-check+format OK; vitest 48/48; vite build (DashboardView chunk 14.9->21.17 KB с чартами); story:build 10/14 за 30.43s; Pest 48/48 за 5.10s. CLAUDE.md v1.21->v1.22, реестр Открытых_вопросов v1.30->v1.31. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
1e233a70c8 |
phase2(dashboard): AppLayout + DashboardView - default-layout приложения
- AppLayout: v-navigation-drawer (теало-нуар sidebar 240px) + brand-block + nav-tree из 8 пунктов в 3 группах (Работа/Финансы/Команда), v-app-bar с crumb «Рабочая область → currentPage» + search ⌘K + bell + user-chip. Mobile (md<): drawer toggleable. - DashboardView: page-head «Доброе утро, Иван» + page-meta + range-toggle 4 опции (Сегодня/7д/30д/Период). KPI-row из 4 cards: 3 outlined (получено лидов/конверсия/активные проекты) + 1 hero balance с runway-bar 4/7 заполненных сегментов teal #32C8A9. - AppShell упрощён до layout-mapper (route.meta.layout 'app'/'auth'). - Маршрут /dashboard (meta.layout='app') в router + web.php. - histoire.setup расширен 8 app-stub-маршрутами для AppLayout. - Vitest +11 тестов: AppLayout 6 (brand+3 группы+8 пунктов+счётчики+crumb), DashboardView 5, AppShell.spec.ts переписан под layout-mapper. - cspell-words.txt: JBM. Регресс: lint+type-check+format OK; vitest 35/35 за 4.92s; vite build DashboardView lazy-chunk 14.9KB; story:build 8/8 за 28.97s; Pest 48/48 за 4.88s. CLAUDE.md v1.20->v1.21, реестр Открытых_вопросов v1.29->v1.30. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
e909a95a8d |
phase2(login): vue-router 4 + AuthLayout + LoginView - первый реальный экран
- vue-router@^4.6.4 (--legacy-peer-deps из-за Histoire vs Vite 8 peerDep). - resources/js/router/index.ts: createWebHistory + lazy-imports + meta.layout. Маршрут / -> /login, /login -> LoginView (meta.layout=auth). - resources/js/layouts/AuthLayout.vue: двухпанельный (brand-pane тёмный с radial-gradient акцентами + form-pane warm ivory). На mobile brand-pane скрыт. - resources/js/views/auth/LoginView.vue: форма Vuetify по v8_login.html секция #form-login - email/password (autocomplete + eye-icon), primary submit, Yandex 360 SSO, RouterLink на /register и /forgot. - AppShell.vue: layout-mapper по route.meta.layout (default/auth). - routes/web.php: явные Route::view для 6 SPA-путей (НЕ catch-all - он перехватывал /_test/* в Pest beforeEach и валил 5 SetTenantContextTest). - Vitest: +LoginView.spec.ts (5), +router.spec.ts (2), AppShell.spec.ts переписан (3). Vitest 10/10 за 3.01s. - LoginView.story.vue для Histoire. Setup расширен memory-router'ом. - cspell-words.txt: рендерят, коммиты, Лидерру. - Регресс: lint:vue OK, type-check OK, format:check OK, vitest 10/10, vite build 212 модулей за 383ms (LoginView lazy-chunk 43.5KB JS / 51.7KB CSS), story:build 2/2 за 29.94s, Pest 48/48 за 4.86s. CLAUDE.md v1.18->v1.19, реестр Открытых_вопросов v1.27->v1.28. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
c3e45343ac |
phase2(histoire): #24 Histoire 1.0-beta.1 — каталог компонентов, фаза 2 закрыта 6/6
- histoire@1.0.0-beta.1 + @histoire/plugin-vue@1.0.0-beta.1 (--legacy-peer-deps: Histoire требует vite ^7, у нас vite 8 ради @vitejs/plugin-vue 6 — runtime smoke OK). - app/histoire.config.ts: HstVue plugin + setupFile + Forest primary palette (Teal #0F6E56). - app/resources/js/histoire.setup.ts: defineSetupVue3 регистрирует vuetify для каждой story. - app/resources/js/components/AppShell.story.vue — первая story (smoke). - npm-scripts: story / story:build / story:preview. - .gitignore: /app/.histoire/ (статическая сборка). - cspell-words.txt: рендерятся, репо, тулчейна. - Регресс: lint:vue, type-check, format:check OK; vitest 3/3 за 2.98s; vite build 158 модулей за 334ms; story:build 1/1 за 30.25s; Pest 48/48 за 5.12s. Активно 18/28 инструментов: 9 фазы 0 + 8 фазы 1 + 6 фазы 2 (фаза 2 по тулчейну закрыта). CLAUDE.md v1.17->v1.18, Tooling v1.6->v1.7, реестр Открытых_вопросов v1.26->v1.27. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
952373fce5 |
phase2(trigger): Vue 3 + Vuetify 3 + ESLint+Prettier+Vue + vue-tsc + Vitest
Триггер фазы 2 выполнен — первый коммит в app/resources/js/. Активирует 5 из 6 инструментов фазы 2 (CLAUDE.md §3.3) — без Histoire (отдельно). Tailwind удалён (правило CLAUDE.md §5 п.2). Стек: - vue@3.5, vuetify@3.12, @vitejs/plugin-vue@6 (для Vite 8 совместимости — @vitejs/plugin-vue@5 поддерживает только Vite 5/6), vite-plugin-vuetify@2 (auto-import), sass-embedded - eslint@10 (flat-config), eslint-plugin-vue@10, @vue/eslint-config-typescript@14, eslint-config-prettier, prettier@3.8 - typescript@6, vue-tsc@3.2 - vitest@4.1, @vue/test-utils, jsdom, @vitest/coverage-v8 Конфиги: - app/vite.config.js — vue + vuetify auto-import - app/vitest.config.ts — jsdom + setup file для ResizeObserver/ IntersectionObserver/matchMedia/CSS.supports stubs (без них Vuetify падает в JSDOM) - app/eslint.config.js — flat-config - app/.prettierrc.json, app/tsconfig.json Frontend-структура: - resources/js/app.ts — точка входа - resources/js/plugins/vuetify.ts — палитра Forest (Teal/ivory/теало-нуар из BRANDBOOK_v2 §3) - resources/js/components/AppShell.vue — первый компонент: v-app + v-app-bar + v-card - resources/css/app.css — Inter (UI, opsz axis), JetBrains Mono (.numeric, tnum) - resources/views/welcome.blade.php — упрощён под Vue mount Smoke-тесты Vitest 3/3 за 2.8 сек: - монтируется без ошибок - содержит «Лидерра.» - рендерит chip с фазой 2 Build: 158 модулей за 386 ms (184 KB JS / 295 KB CSS gzipped). npm-scripts: lint:vue, format, format:check, type-check, test:vue. lefthook job #8 (ESLint на staged .ts/.vue) добавлен в pre-commit. Полный type-check (vue-tsc) и Vitest — отдельно вручную (медленно для pre-commit: ~3 сек на каждый). CLAUDE.md v1.16 → v1.17 (фаза 1 → фаза 2 в §6). Реестр Открытые_вопросы v1.25 → v1.26. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |