Files
portal/app/resources/js/composables/dealsApiMapper.ts
T
Дмитрий 73d4c8c14f fix/ui: убрать жаргон в клиентском UI — без конкурент/синхронизация/crm.bp-gr.ru/Pay-per-lead
U1 остаток: пояснения источника проекта без слова конкурент.
U5: баннер списка проектов про результат а не синхронизацию + статус Собирает заявки.
Деалы/импорт/логин: убрано имя поставщика и англ Pay-per-lead.
Активность сделки: технический supplier_webhook заменён понятной фразой.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 12:02:52 +03:00

102 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Маппер `ApiDeal` (backend GET /api/deals) → `MockDeal` (UI-формат).
*
* UI-компоненты (DealsView/KanbanView/KanbanCard/DealDetailDrawer) построены
* вокруг `MockDeal`. Чтобы не переписывать их под backend-форму, маппим
* на текущий контракт: вычисляем `receivedMinutesAgo` из ISO-метки,
* подставляем placeholder для отсутствующих manager/project.
*
* `cost` пока 0 — отдельного endpoint'а на сделку нет (cost живёт в
* supplier_lead_costs.cost_rub и снимок берётся в момент webhook'а;
* на UI-листинге не критично, появится отдельным запросом при необходимости).
*/
import type { ApiDeal, ApiDealEvent } from '../api/deals';
import type { LeadStatus } from './leadStatuses';
import type { DealEvent } from './mockDealEvents';
import type { MockDeal } from './mockDeals';
/**
* Маппит backend-событие из ActivityLog в `DealEvent` для timeline.
* Backend `event` slug может быть любым из `deal.*` — clamp на known-types,
* иначе отображаем как `deal.viewed` (generic-icon).
*/
export function mapApiDealEvent(
api: ApiDealEvent,
now: Date = new Date(),
statusLabel: (slug: string) => string = (s) => s,
): DealEvent {
const knownTypes: DealEvent['type'][] = [
'deal.created',
'deal.status_changed',
'deal.viewed',
'deal.commented',
'deal.assigned',
'deal.balance_charged',
];
const type = knownTypes.includes(api.event as DealEvent['type']) ? (api.event as DealEvent['type']) : 'deal.viewed';
const created = api.created_at ? new Date(api.created_at) : now;
const minutesAgo = Math.max(0, Math.floor((now.getTime() - created.getTime()) / 60000));
let detail = '';
if (api.event === 'deal.status_changed' && api.context) {
const from = statusLabel((api.context.from as string) ?? '?');
const to = statusLabel((api.context.to as string) ?? '?');
detail = `${from}${to}`;
} else if (api.event === 'deal.created' && api.context) {
// Технический код источника → понятная клиенту фраза (UX-аудит 25.06).
const sourceLabels: Record<string, string> = {
supplier_webhook: 'получена автоматически',
csv: 'загружена из файла',
csv_recovery: 'загружена из файла',
import: 'загружена из файла',
manual: 'добавлена вручную',
};
const rawSource = (api.context.source as string) ?? '';
detail = sourceLabels[rawSource] ? `Заявка ${sourceLabels[rawSource]}` : 'Заявка получена';
} else if (api.event === 'deal.commented' && api.context) {
// Текст комментария — без служебного ключа «text:» (UI-аудит 21.06.2026).
detail = String(api.context.text ?? api.context.comment ?? '');
} else if (api.context && typeof api.context === 'object') {
// Generic-fallback: JSON-сводка контекста.
detail = Object.entries(api.context)
.map(([k, v]) => `${k}: ${String(v)}`)
.join(', ');
}
return {
id: api.id,
type,
actor: api.actor ? { initials: api.actor.initials, name: api.actor.name } : null,
minutesAgo,
detail,
};
}
export function mapApiDeal(api: ApiDeal, now: Date = new Date()): MockDeal {
const receivedAt = api.received_at ? new Date(api.received_at) : now;
const receivedMinutesAgo = Math.max(0, Math.floor((now.getTime() - receivedAt.getTime()) / 60000));
return {
id: api.id,
name: api.contact_name ?? api.phone,
phone: api.phone,
statusSlug: api.status as LeadStatus['slug'],
project: api.project_name ?? '—',
manager: {
initials: api.manager_initials ?? '—',
name: api.manager_name ?? 'Не назначен',
},
cost: 0,
costKopecks: api.cost_kopecks ?? null,
receivedMinutesAgo,
signalType: (api.project_signal_type as MockDeal['signalType']) ?? null,
city: api.city,
comment: api.comment,
receivedAt: api.received_at,
projectSignalType: (api.project_signal_type as MockDeal['projectSignalType']) ?? null,
projectSignalIdentifier: api.project_signal_identifier ?? null,
projectSmsKeyword: api.project_sms_keyword ?? null,
projectSmsSenders: api.project_sms_senders ?? null,
};
}