768628d914
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>
179 lines
5.7 KiB
TypeScript
179 lines
5.7 KiB
TypeScript
/**
|
||
* Mock-сделки для UI-разводки. Заменяется на API-fetch при backend-интеграции
|
||
* (`GET /api/deals?tenant_id=...&status=...&page=...`).
|
||
*
|
||
* Slug'и статусов берутся из `composables/leadStatuses.ts` (источник —
|
||
* `db/schema.sql:2130`, не BRANDBOOK §3.6).
|
||
*/
|
||
import type { LeadStatus } from './leadStatuses';
|
||
|
||
export interface MockDeal {
|
||
id: number;
|
||
name: string;
|
||
phone: string;
|
||
statusSlug: LeadStatus['slug'];
|
||
project: string;
|
||
manager: { initials: string; name: string };
|
||
cost: number;
|
||
receivedMinutesAgo: number;
|
||
}
|
||
|
||
export const MOCK_DEALS: MockDeal[] = [
|
||
{
|
||
id: 1,
|
||
name: 'Анна Соколова',
|
||
phone: '+7 (916) 871-23-45',
|
||
statusSlug: 'new',
|
||
project: 'Натяжные потолки',
|
||
manager: { initials: 'ИП', name: 'Иван П.' },
|
||
cost: 1850,
|
||
receivedMinutesAgo: 7,
|
||
},
|
||
{
|
||
id: 2,
|
||
name: 'Дмитрий Кузнецов',
|
||
phone: '+7 (903) 412-58-90',
|
||
statusSlug: 'worked',
|
||
project: 'Окна Москва',
|
||
manager: { initials: 'ОР', name: 'Ольга Р.' },
|
||
cost: 2400,
|
||
receivedMinutesAgo: 23,
|
||
},
|
||
{
|
||
id: 3,
|
||
name: 'Светлана Иванова',
|
||
phone: '+7 (925) 309-44-12',
|
||
statusSlug: 'negotiations',
|
||
project: 'Окна Москва',
|
||
manager: { initials: 'ИП', name: 'Иван П.' },
|
||
cost: 2100,
|
||
receivedMinutesAgo: 47,
|
||
},
|
||
{
|
||
id: 4,
|
||
name: 'Марина Лебедева',
|
||
phone: '+7 (915) 778-90-32',
|
||
statusSlug: 'paid',
|
||
project: 'Натяжные потолки',
|
||
manager: { initials: 'ОР', name: 'Ольга Р.' },
|
||
cost: 2350,
|
||
receivedMinutesAgo: 105,
|
||
},
|
||
{
|
||
id: 5,
|
||
name: 'Алексей Петров',
|
||
phone: '+7 (905) 132-46-87',
|
||
statusSlug: 'missed',
|
||
project: 'Окна Москва',
|
||
manager: { initials: 'ИП', name: 'Иван П.' },
|
||
cost: 2400,
|
||
receivedMinutesAgo: 132,
|
||
},
|
||
{
|
||
id: 6,
|
||
name: 'Екатерина Морозова',
|
||
phone: '+7 (926) 554-21-09',
|
||
statusSlug: 'waiting_payment',
|
||
project: 'Натяжные потолки',
|
||
manager: { initials: 'ОР', name: 'Ольга Р.' },
|
||
cost: 1950,
|
||
receivedMinutesAgo: 178,
|
||
},
|
||
{
|
||
id: 7,
|
||
name: 'Игорь Васильев',
|
||
phone: '+7 (917) 882-30-55',
|
||
statusSlug: 'viewed',
|
||
project: 'Окна Москва',
|
||
manager: { initials: 'ИП', name: 'Иван П.' },
|
||
cost: 2400,
|
||
receivedMinutesAgo: 215,
|
||
},
|
||
{
|
||
id: 8,
|
||
name: 'Тимур Алиев',
|
||
phone: '+7 (903) 765-09-21',
|
||
statusSlug: 'hot',
|
||
project: 'Натяжные потолки',
|
||
manager: { initials: 'ОР', name: 'Ольга Р.' },
|
||
cost: 1850,
|
||
receivedMinutesAgo: 263,
|
||
},
|
||
{
|
||
id: 9,
|
||
name: 'Наталья Семёнова',
|
||
phone: '+7 (910) 244-67-83',
|
||
statusSlug: 'closed',
|
||
project: 'Окна Москва',
|
||
manager: { initials: 'ИП', name: 'Иван П.' },
|
||
cost: 2400,
|
||
receivedMinutesAgo: 312,
|
||
},
|
||
{
|
||
id: 10,
|
||
name: 'Олег Григорьев',
|
||
phone: '+7 (909) 411-52-76',
|
||
statusSlug: 'partnership',
|
||
project: 'Натяжные потолки',
|
||
manager: { initials: 'ОР', name: 'Ольга Р.' },
|
||
cost: 1850,
|
||
receivedMinutesAgo: 388,
|
||
},
|
||
{
|
||
id: 11,
|
||
name: 'Ирина Зайцева',
|
||
phone: '+7 (916) 671-98-04',
|
||
statusSlug: 'final_missed',
|
||
project: 'Окна Москва',
|
||
manager: { initials: 'ИП', name: 'Иван П.' },
|
||
cost: 2400,
|
||
receivedMinutesAgo: 445,
|
||
},
|
||
{
|
||
id: 12,
|
||
name: 'Сергей Никитин',
|
||
phone: '+7 (925) 198-43-58',
|
||
statusSlug: 'paid',
|
||
project: 'Натяжные потолки',
|
||
manager: { initials: 'ОР', name: 'Ольга Р.' },
|
||
cost: 1850,
|
||
receivedMinutesAgo: 521,
|
||
},
|
||
];
|
||
|
||
/**
|
||
* Срезы-фильтры для chiprow в DealsView. Каждый срез — массив slug'ов или
|
||
* предикат включения. На API-стороне уйдут как ?status_in=...
|
||
*/
|
||
export interface DealsTab {
|
||
id: 'all' | 'active' | 'waiting_payment' | 'closed' | 'invalid';
|
||
label: string;
|
||
slugs: LeadStatus['slug'][] | null; // null = все
|
||
}
|
||
|
||
export const DEALS_TABS: DealsTab[] = [
|
||
{ id: 'all', label: 'Все', slugs: null },
|
||
{ id: 'active', label: 'Активные', slugs: ['new', 'viewed', 'worked', 'negotiations', 'hot'] },
|
||
{ id: 'waiting_payment', label: 'Ждут оплату', slugs: ['waiting_payment'] },
|
||
{ id: 'closed', label: 'Закрытые', slugs: ['paid', 'closed'] },
|
||
{ id: 'invalid', label: 'Невалидные', slugs: ['missed', 'final_missed'] },
|
||
];
|
||
|
||
/**
|
||
* Доступные проекты и менеджеры для NewDealDialog. На API: GET /api/projects /
|
||
* GET /api/managers (фильтр по tenant_id из middleware).
|
||
*/
|
||
export const MOCK_PROJECTS = ['Натяжные потолки', 'Окна Москва', 'Кухни на заказ', 'Двери Премиум'] as const;
|
||
|
||
export interface MockManager {
|
||
initials: string;
|
||
name: string;
|
||
}
|
||
|
||
export const MOCK_MANAGERS: MockManager[] = [
|
||
{ initials: 'ИП', name: 'Иван П.' },
|
||
{ initials: 'ОР', name: 'Ольга Р.' },
|
||
{ initials: 'СК', name: 'Светлана К.' },
|
||
{ initials: 'АН', name: 'Андрей Н.' },
|
||
];
|