73d4c8c14f
U1 остаток: пояснения источника проекта без слова конкурент. U5: баннер списка проектов про результат а не синхронизацию + статус Собирает заявки. Деалы/импорт/логин: убрано имя поставщика и англ Pay-per-lead. Активность сделки: технический supplier_webhook заменён понятной фразой. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
102 lines
4.7 KiB
TypeScript
102 lines
4.7 KiB
TypeScript
/**
|
||
* Маппер `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,
|
||
};
|
||
}
|