import axios from 'axios'; import { apiClient } from './client'; /** * Адресные сообщения по коду ошибки автоподбора (бэкенд кладёт `{ error: 'code' }`). * Общий `extractErrorMessage` читает `message`, поэтому для наших кодов нужен отдельный маппер — * иначе клиент видит общий текст «Проверьте баланс» на ЛЮБУЮ ошибку. */ const AUTOPODBOR_ERROR_MESSAGES: Record = { balance_insufficient: 'Не хватает денег на балансе — пополните счёт, чтобы запустить.', run_in_flight: 'Подбор уже идёт — дождитесь результата, повторно запускать не нужно.', name_or_site_required: 'Укажите название или сайт конкурента.', has_active_project: 'Сначала остановите проект на этом источнике.', has_active_projects: 'Сначала остановите проекты этого конкурента.', manage_via_project: 'Смена адреса/номера источника — через «Сменить источник» в проекте.', }; export function autopodborErrorMessage(error: unknown, fallback: string): string { if (axios.isAxiosError(error)) { const code = (error.response?.data as { error?: string } | undefined)?.error; if (code && AUTOPODBOR_ERROR_MESSAGES[code]) { return AUTOPODBOR_ERROR_MESSAGES[code]; } } return fallback; } // ——— DTOs ——— export type RunKind = 'search' | 'study' | 'resolve'; export type RunStatus = 'queued' | 'running' | 'done' | 'empty' | 'failed'; export interface RunDto { id: number; kind: RunKind; status: RunStatus; region_code: number | null; params: Record; price_rub_charged: string | null; error_code: string | null; competitors_count: number; sources_count: number; started_at: string | null; finished_at: string | null; created_at: string | null; competitor_id: number | null; } export type Box = 'proposal' | 'field'; export type PhoneType = 'city' | 'mobile' | 'tollfree' | null; export interface CompetitorDto { id: number; name: string; description: string | null; is_federal: boolean; relevance_pct: number | null; origin: 'auto' | 'manual' | 'resolve'; box: Box; site_url: string | null; directory_urls: string[]; studied_at: string | null; study_run_id: number | null; search_run_id: number | null; } /** Одно место, где нашли источник (для кликабельного списка «где нашли»). */ export interface WhereFound { label: string; url: string | null; } export interface SourceDto { id: number; competitor_id: number; signal_type: 'site' | 'call'; identifier: string; phone_kind: 'real' | 'substitute' | null; phone_type: PhoneType; box: Box; provenance_url: string | null; provenance_label: string | null; created_project_id: number | null; existing_project_id?: number | null; /** Полный список «где нашли» (места + ссылки); подтверждения = его длина. */ where_found?: WhereFound[]; confirmations?: number; /** Адрес офиса/филиала, если источник привязан к конкретному адресу. */ office?: string | null; } /** Статус проекта, привязанного к источнику (для рабочего места «поле»). */ export interface SourceProjectDto { id: number; name: string; signal_identifier: string | null; is_active: boolean; paused_at: string | null; preflight_blocked_at: string | null; daily_limit_target: number; delivered_in_month: number; delivery_days_mask: number; regions: number[]; } /** Ответ смены источника проекта (change_source, §14.10). */ export interface ChangeSourceResult { applies_from?: string | null; source_locked?: boolean; source_change_message?: string | null; } export interface FieldSourceDto extends SourceDto { project: SourceProjectDto | null; } export interface FieldCompetitorDto extends CompetitorDto { counters: { sources: number; projects_created: number; projects_in_work: number }; sources: FieldSourceDto[]; } export interface StateDto { enabled: boolean; runs: RunDto[]; prices: { search: string; study: string }; } // ——— API functions ——— export async function fetchState(): Promise { const { data } = await apiClient.get('/api/autopodbor/state'); return data; } export async function fetchRun(id: number): Promise { const { data } = await apiClient.get<{ data: RunDto }>(`/api/autopodbor/runs/${id}`); return data.data; } export async function fetchCompetitor( id: number, ): Promise<{ competitor: CompetitorDto; sources: FieldSourceDto[] }> { const { data } = await apiClient.get<{ data: CompetitorDto; sources: FieldSourceDto[] }>( `/api/autopodbor/competitors/${id}`, ); return { competitor: data.data, sources: data.sources }; } export async function startSearch(p: { region_code: number; examples: string[]; about_self: string[]; include_federal: boolean; }): Promise { const { data } = await apiClient.post<{ data: RunDto }>('/api/autopodbor/search', p); return data.data; } export async function startStudy(competitor_id: number): Promise { const { data } = await apiClient.post<{ data: RunDto }>('/api/autopodbor/study', { competitor_id }); return data.data; } export async function startResolve(p: { name: string; region_code: number }): Promise { const { data } = await apiClient.post<{ data: RunDto }>('/api/autopodbor/resolve', p); return data.data; } export async function startManualStudy(p: { competitor_id?: number; name?: string; site_url?: string; directory?: string; region_code: number; }): Promise { const { data } = await apiClient.post<{ data: RunDto }>('/api/autopodbor/manual-study', p); return data.data; } export async function addManualSource(p: { competitor_id: number; raw: string }): Promise { const { data } = await apiClient.post<{ data: SourceDto }>('/api/autopodbor/sources/manual', p); return data.data; } export async function createProjects(p: { source_ids: number[]; regions: number[]; daily_limit_target: number; delivery_days_mask: number; launch: boolean; }): Promise> { const { data } = await apiClient.post<{ data: Array<{ id: number; name: string }> }>('/api/autopodbor/projects', p); return data.data; } export async function fetchRunCompetitors(runId: number): Promise { const { data } = await apiClient.get<{ data: CompetitorDto[] }>(`/api/autopodbor/runs/${runId}/competitors`); return data.data; } // ——— «Конкурентное поле»: рабочее место (два ящика) ——— /** Конкуренты в поле с источниками в работе и счётчиками. */ export async function fetchField(): Promise { const { data } = await apiClient.get<{ competitors: FieldCompetitorDto[] }>('/api/autopodbor/field'); return data.competitors; } /** Конкуренты в ящике «предложения» (сорт по похожести). */ export async function fetchProposals(): Promise { const { data } = await apiClient.get<{ data: CompetitorDto[] }>('/api/autopodbor/proposals'); return data.data; } export async function setCompetitorBox(id: number, box: Box): Promise { const { data } = await apiClient.patch<{ data: CompetitorDto }>(`/api/autopodbor/competitors/${id}/box`, { box }); return data.data; } export async function setSourceBox(id: number, box: Box): Promise { const { data } = await apiClient.patch<{ data: SourceDto }>(`/api/autopodbor/sources/${id}/box`, { box }); return data.data; } export interface CompetitorPatch { name?: string; description?: string | null; is_federal?: boolean; relevance_pct?: number | null; site_url?: string | null; directory_urls?: string[]; box?: Box; } export async function updateCompetitor(id: number, patch: CompetitorPatch): Promise { const { data } = await apiClient.patch<{ data: CompetitorDto }>(`/api/autopodbor/competitors/${id}`, patch); return data.data; } export async function deleteCompetitor(id: number): Promise { await apiClient.delete(`/api/autopodbor/competitors/${id}`); } export interface SourcePatch { identifier?: string; phone_kind?: 'real' | 'substitute' | null; phone_type?: PhoneType; provenance_url?: string | null; provenance_label?: string | null; box?: Box; } export async function updateSource(id: number, patch: SourcePatch): Promise { const { data } = await apiClient.patch<{ data: SourceDto }>(`/api/autopodbor/sources/${id}`, patch); return data.data; } export async function deleteSource(id: number): Promise { await apiClient.delete(`/api/autopodbor/sources/${id}`); } export async function createManualCompetitor(p: { name: string; description?: string; site_url?: string; directory?: string; is_federal?: boolean; }): Promise { const { data } = await apiClient.post<{ data: CompetitorDto }>('/api/autopodbor/competitors/manual', p); return data.data; } /** * Включить/выключить проект источника через ГОТОВУЮ ручку проектов — * там все гварды (слепок 18:00 МСК, баланс, сделки, §14.9). */ export async function toggleProjectActive(projectId: number, active: boolean): Promise { await apiClient.patch(`/api/projects/${projectId}/toggle-active`, { is_active: active }); } /** * Сменить источник проекта (адрес/номер) через ГОТОВУЮ ручку проектов — это и есть * change_source со всеми гвардами §14.10 (тип источника не меняется). Возвращает * сообщение о сроках вступления в силу. */ export async function changeProjectSource(projectId: number, signalIdentifier: string): Promise { const { data } = await apiClient.patch(`/api/projects/${projectId}`, { signal_identifier: signalIdentifier, }); return data ?? {}; } /** Настройки проекта (лимит/регионы/дни) — через готовую ручку проектов (слепок §14.9). */ export async function updateProjectSettings( projectId: number, p: { daily_limit_target?: number; regions?: number[]; delivery_days_mask?: number }, ): Promise { const { data } = await apiClient.patch(`/api/projects/${projectId}`, p); return data ?? {}; }