import axios from 'axios'; /** * API-клиент для портала отдела продаж (/api/sales/*). * * Использует Bearer-токен из salesAuth store (localStorage 'sales_token'). * НЕ использует Sanctum cookie/CSRF — это отдельный auth через токен. * * Base path: /api/sales */ export interface SalesUser { id: number; name: string; email: string; role: 'manager' | 'head'; } export interface SalesLoginResponse { token: string; user: SalesUser; } // ─── helpers ──────────────────────────────────────────────────────────────── function getToken(): string | null { try { return localStorage.getItem('sales_token'); } catch { return null; } } function authHeaders(): Record { const token = getToken(); return token ? { Authorization: `Bearer ${token}` } : {}; } /** * Извлекает читаемое сообщение об ошибке из ответа API. */ export function extractSalesErrorMessage(error: unknown, fallback = 'Произошла ошибка. Попробуйте позже.'): string { if (axios.isAxiosError(error)) { const data = error.response?.data as { message?: string } | undefined; if (data?.message) return data.message; if (error.response?.status === 401) return 'Неверный email или пароль.'; if (error.response?.status === 403) return 'Нет прав на это действие.'; if (error.response?.status === 422) { const errData = error.response.data as { errors?: Record } | undefined; const firstField = errData?.errors ? Object.values(errData.errors)[0] : undefined; if (firstField?.[0]) return firstField[0]; } if (error.response?.status === 500) return 'Внутренняя ошибка сервера.'; } return fallback; } // ─── types ─────────────────────────────────────────────────────────────────── export interface SalesClientRow { tenant_id: number; organization_name: string; inn: string | null; subject_type: string | null; last_activity_at: string | null; // ISO datetime or null balance_rub: string; status: 'trial' | 'suspended' | 'overdue' | 'active' | string; tariff_name: string | null; projects_count: number; runway_days: number | null; leads_delivered: number; oborot_rub: number; earned_rub: number | null; } export interface SalesClientsParams { period: string; from?: string; to?: string; search?: string; } // ─── auth endpoints ────────────────────────────────────────────────────────── /** * POST /api/sales/auth/login → { token, user } */ export async function salesLogin(email: string, password: string): Promise { const { data } = await axios.post( '/api/sales/auth/login', { email, password }, { headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest' } }, ); return data; } /** * GET /api/sales/auth/me (Bearer) → { id, name, email, role } */ export async function salesMe(): Promise { const { data } = await axios.get('/api/sales/auth/me', { headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest', ...authHeaders(), }, }); return data; } /** * POST /api/sales/auth/logout (Bearer) */ export async function salesLogout(): Promise { await axios.post( '/api/sales/auth/logout', {}, { headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest', ...authHeaders(), }, }, ); } // ─── clients endpoint ───────────────────────────────────────────────────────── /** * GET /api/sales/clients?period=...&from=...&to=...&search=... (Bearer) * → { data: SalesClientRow[] } */ export async function listSalesClients(params: SalesClientsParams): Promise { const { data } = await axios.get<{ data: SalesClientRow[] }>('/api/sales/clients', { params, headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest', ...authHeaders(), }, }); return data.data; } // ─── client card types ──────────────────────────────────────────────────────── export interface SalesClientCardProfile { organization_name: string; contact_email: string | null; contact_name: string | null; contact_phone: string | null; inn: string | null; subject_type: string | null; created_at: string; // ISO desired_daily_numbers: number | null; last_activity_at: string | null; // ISO } export interface SalesClientCardKpi { balance_rub: string; runway_days: number | null; projects_count: number; leads_delivered: number; leads_target: number; avg_lead_price_rub: number; earned_rub: number | null; } export interface SalesClientCardProject { id: number; name: string; signal_type: 'site' | 'call' | 'sms'; region: string[]; daily_limit_target: number; delivered_today: number; status: 'active' | 'paused'; } export interface SalesClientCardLeadByDay { date: string; // YYYY-MM-DD count: number; oborot_rub: number; } export interface SalesClientCardRecentLead { received_at: string; // ISO phone_masked: string; region: string; source: string; project: string; } export interface SalesClientCardActivityItem { created_at: string; // ISO type: string; amount_rub: string; description: string; } export interface SalesClientCard { profile: SalesClientCardProfile; kpi: SalesClientCardKpi; projects: SalesClientCardProject[]; leads_by_day: SalesClientCardLeadByDay[]; recent_leads: SalesClientCardRecentLead[]; activity: SalesClientCardActivityItem[]; } // ─── overview types ─────────────────────────────────────────────────────────── export interface SalesOverviewTotals { clients_count: number; active: number; trial: number; overdue: number; balance_sum_rub: string; leads_delivered: number; oborot_rub: number; earned_rub: number | null; } export interface SalesOverviewAttentionRow { tenant_id: number; organization_name: string; status: string; balance_rub: string; runway_days: number | null; } export interface SalesOverviewTopClient { tenant_id: number; organization_name: string; leads_delivered: number; oborot_rub: number; } export interface SalesOverview { totals: SalesOverviewTotals; attention: SalesOverviewAttentionRow[]; top_clients: SalesOverviewTopClient[]; } export interface SalesOverviewParams { period: string; from?: string; to?: string; } // ─── overview endpoint ──────────────────────────────────────────────────────── /** * GET /api/sales/overview?period=...&from=...&to=... (Bearer) * → SalesOverview */ export async function getSalesOverview(params: SalesOverviewParams): Promise { const { data } = await axios.get('/api/sales/overview', { params, headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest', ...authHeaders(), }, }); return data; } // ─── client card endpoint ───────────────────────────────────────────────────── /** * GET /api/sales/clients/{tenantId}?period=...&from=...&to=... (Bearer) * → SalesClientCard * 403 если клиент не закреплён за менеджером. */ export async function getSalesClientCard( tenantId: string | number, params: SalesClientsParams, ): Promise { const { data } = await axios.get(`/api/sales/clients/${tenantId}`, { params, headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest', ...authHeaders(), }, }); return data; } // ─── attachments (привязка клиентов) types ──────────────────────────────────── export type SalesAttachmentStatus = 'pending' | 'approved' | 'rejected' | 'not_found'; /** * Строка заявки на привязку клиента к менеджеру. * Форма зависит от роли (см. бэк-контракт GET /api/sales/attachments): * - менеджер: базовые поля; * - начальник (pending): + manager_name, hint; * - начальник (history): + manager_name. */ export interface SalesAttachmentRow { id: number; login_input: string; status: SalesAttachmentStatus; status_label: string; tenant_id: number | null; organization_name: string | null; comment: string | null; created_at: string | null; // ISO decided_at: string | null; // ISO /** Только у начальника: имя менеджера-автора заявки */ manager_name?: string; /** * Только у начальника в очереди pending: результат проверки. * 'свободен' | 'уже за: Имя' | null (когда клиент не найден). */ hint?: string | null; } /** * Результат отправки заявки / решения по ней. * POST /api/sales/attachments → 201; POST /api/sales/attachments/{id}/decide → 200. */ export interface SalesAttachmentSubmitResult { id: number; login_input: string; status: SalesAttachmentStatus; status_label: string; tenant_id: number | null; comment: string | null; } /** * Ответ GET /api/sales/attachments для начальника. */ export interface SalesAttachmentQueue { pending: SalesAttachmentRow[]; history: SalesAttachmentRow[]; } // ─── attachments endpoints ──────────────────────────────────────────────────── /** * POST /api/sales/attachments (Bearer, менеджер) * Тело: { login }. Клиент не найден → status='not_found' (201, не ошибка). * Уже за этим менеджером → 422. */ export async function submitAttachment(login: string): Promise { const { data } = await axios.post( '/api/sales/attachments', { login }, { headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest', ...authHeaders(), }, }, ); return data; } /** * GET /api/sales/attachments (Bearer, менеджер) → { data: SalesAttachmentRow[] } * Только свои заявки, newest first. */ export async function listMyAttachments(): Promise { const { data } = await axios.get<{ data: SalesAttachmentRow[] }>('/api/sales/attachments', { headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest', ...authHeaders(), }, }); return data.data; } /** * GET /api/sales/attachments (Bearer, начальник) → { pending, history } */ export async function listAttachmentQueue(): Promise { const { data } = await axios.get('/api/sales/attachments', { headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest', ...authHeaders(), }, }); return data; } /** * POST /api/sales/attachments/{id}/decide (Bearer, только начальник) * action='approve' сам переназначает (если клиент был за другим менеджером). * 403 если не начальник. */ export async function decideAttachment( id: number, action: 'approve' | 'reject', comment?: string, ): Promise { const { data } = await axios.post( `/api/sales/attachments/${id}/decide`, { action, ...(comment ? { comment } : {}) }, { headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest', ...authHeaders(), }, }, ); return data; } // ─── tariffs (тарифы менеджеров) types ───────────────────────────────────────── /** * Тип (вид) тарифа менеджера (модель 03.07.2026): * - daily_salary — суточный оклад (ставка ₽/день = base_salary_rub, per-manager); * - topup_step — процент от пополнений с порогом + разовым бонусом и ступенями по сроку. */ export type SalesTariffKind = 'daily_salary' | 'topup_step'; /** Одна ступень для topup_step: срок клиента у менеджера from–to мес и ставка %. */ export interface SalesTariffStep { from: number; to: number; rate: number; } /** * Тариф менеджера. Форма params зависит от kind: * - daily_salary: {} (ставка ₽/день задаётся per-manager при назначении); * - topup_step: { threshold: number, reward: number, periods: SalesTariffStep[] }. */ export interface SalesTariff { id: number; name: string; kind: SalesTariffKind; params: Record; } /** Строка менеджера в таблице назначения тарифа (для новых клиентов). */ export interface SalesTariffManager { id: number; name: string; current_tariff_id: number | null; current_tariff_name: string | null; current_tariff_kind: SalesTariffKind | null; base_salary_rub: number; clients_count: number; topups_rub: number; oborot_rub: number; estimated_earned_rub: number; } export interface SalesTariffsResponse { tariffs: SalesTariff[]; managers: SalesTariffManager[]; } export interface SalesTariffsParams { period: string; from?: string; to?: string; } /** Результат POST /tariffs/assign. */ export interface SalesTariffAssignResult { id: number; name: string; current_tariff_id: number | null; base_salary_rub: number; } // ─── tariffs endpoints ───────────────────────────────────────────────────────── /** * GET /api/sales/tariffs?period=...&from=...&to=... (Bearer, начальник) * → { tariffs, managers } */ export async function listSalesTariffs(params: SalesTariffsParams): Promise { const { data } = await axios.get('/api/sales/tariffs', { params, headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest', ...authHeaders(), }, }); return data; } /** * POST /api/sales/tariffs (Bearer, начальник) → 201 SalesTariff * Валидация params по kind; ошибка → 422. */ export async function createSalesTariff(payload: { name: string; kind: SalesTariffKind; params: Record; }): Promise { const { data } = await axios.post('/api/sales/tariffs', payload, { headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest', ...authHeaders(), }, }); return data; } /** * PUT /api/sales/tariffs/{id} (Bearer, начальник) → 200 SalesTariff * kind менять нельзя; передаём только name + params. */ export async function updateSalesTariff( id: number, payload: { name: string; params: Record }, ): Promise { const { data } = await axios.put(`/api/sales/tariffs/${id}`, payload, { headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest', ...authHeaders(), }, }); return data; } /** * POST /api/sales/tariffs/assign (Bearer, начальник) → 200 * Назначает менеджеру текущий тариф (для НОВЫХ клиентов). */ export async function assignSalesTariff(payload: { manager_id: number; tariff_id: number; base_salary_rub?: number; }): Promise { const { data } = await axios.post('/api/sales/tariffs/assign', payload, { headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest', ...authHeaders(), }, }); return data; } // ─── payouts / income (выплаты менеджерам · мой доход) types ─────────────────── /** * Одна запись журнала выплаты менеджеру. * Менеджер видит только свои выплаты, начальник — все. */ export interface SalesPayoutRow { id: number; sales_user_id: number; manager_name: string | null; amount_rub: number; paid_on: string; // YYYY-MM-DD comment: string | null; created_at: string; // ISO creator_name: string | null; } /** * Строка остатка к выплате по менеджеру (только для начальника). * remaining_rub = accrued_rub − paid_all_time_rub (нарастающим итогом). */ export interface SalesRemainingRow { manager_id: number; name: string; clients_count: number; oborot_rub: number; accrued_rub: number; paid_period_rub: number; paid_all_time_rub: number; remaining_rub: number; } /** Итоги дохода менеджера за период. */ export interface SalesIncomeTotals { oborot_rub: number; accrued_rub: number; paid_all_time_rub: number; to_pay_period_rub: number; } /** Начисление менеджеру по одному клиенту за период. */ export interface SalesIncomePerClient { tenant_id: number; organization_name: string | null; tariff_kind: string | null; tariff_label: string; topups_rub: number; earned_rub: number; } export interface SalesIncomeResponse { totals: SalesIncomeTotals; per_client: SalesIncomePerClient[]; payouts: SalesPayoutRow[]; } export interface SalesPayoutsParams { period: string; from?: string; to?: string; } // ─── payouts / income endpoints ──────────────────────────────────────────────── /** * GET /api/sales/payouts (Bearer) * Менеджер видит свои выплаты, начальник — все. → { data: SalesPayoutRow[] } */ export async function listSalesPayouts(): Promise { const { data } = await axios.get<{ data: SalesPayoutRow[] }>('/api/sales/payouts', { headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest', ...authHeaders(), }, }); return data.data; } /** * GET /api/sales/payouts/remaining?period=... (Bearer, начальник) * → { data: SalesRemainingRow[] } */ export async function getSalesPayoutsRemaining(params: SalesPayoutsParams): Promise { const { data } = await axios.get<{ data: SalesRemainingRow[] }>('/api/sales/payouts/remaining', { params, headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest', ...authHeaders(), }, }); return data.data; } /** * POST /api/sales/payouts (Bearer, начальник) → 201 SalesPayoutRow * Ошибка валидации → 422. */ export async function createSalesPayout(payload: { manager_id: number; amount_rub: number; paid_on: string; comment?: string; }): Promise { const { data } = await axios.post('/api/sales/payouts', payload, { headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest', ...authHeaders(), }, }); return data; } /** * GET /api/sales/income?period=... (Bearer, менеджер) → SalesIncomeResponse */ export async function getSalesIncome(params: SalesPayoutsParams): Promise { const { data } = await axios.get('/api/sales/income', { params, headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest', ...authHeaders(), }, }); return data; } // ─── invoices (счета · отметка оплаты) types ─────────────────────────────────── /** Строка счёта в таблице «Счета» портала продаж (только для начальника). */ export interface SalesInvoiceRow { id: number; invoice_number: string; amount_total: string | number; status: 'issued' | 'paid' | 'overdue' | 'cancelled'; issued_at: string; // ISO expires_at: string | null; // ISO or null tenant_id: number; tenant_name: string | null; payer_name: string | null; } export interface SalesInvoicesResponse { data: SalesInvoiceRow[]; meta: { current_page: number; last_page: number; total: number; per_page: number; }; } export interface SalesInvoicesParams { status?: string; search?: string; per_page?: number; } // ─── invoices endpoints ──────────────────────────────────────────────────────── /** * GET /api/sales/invoices?status=...&search=...&per_page=... (Bearer, начальник) * → { data: SalesInvoiceRow[], meta: {...} } */ export async function listSalesInvoices(params: SalesInvoicesParams): Promise { const { data } = await axios.get('/api/sales/invoices', { params, headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest', ...authHeaders(), }, }); return data; } /** * POST /api/sales/invoices/{id}/mark-paid (Bearer, начальник) → { status: 'ok' } * Зачисляет баланс клиенту + формирует Акт на бэкенде. */ export async function markSalesInvoicePaid(id: number): Promise<{ status: string }> { const { data } = await axios.post<{ status: string }>( `/api/sales/invoices/${id}/mark-paid`, {}, { headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest', ...authHeaders(), }, }, ); return data; } // ─── dashboard overview (сводка отдела · начальник) types ────────────────────── /** KPI-плитки сводки отдела начальника. */ export interface SalesDashboardKpi { managers_count: number; managers_active: number; managers_vacation: number; clients_count: number; balance_sum_rub: string; oborot_rub: number; paid_period_rub: number; } /** Счётчики-алерты сводки отдела (кликабельные плитки). */ export interface SalesDashboardAlerts { invoices_awaiting: number; attachments_pending: number; clients_balance_problem: number; } /** Строка таблицы «Результативность менеджеров». */ export interface SalesDashboardPerformanceRow { manager_id: number; name: string; clients_count: number; leads_delivered: number; oborot_rub: number; paid_all_time_rub: number; earned_rub: number; } export interface SalesDashboardOverview { kpi: SalesDashboardKpi; alerts: SalesDashboardAlerts; performance: SalesDashboardPerformanceRow[]; } export interface SalesDashboardParams { period: string; from?: string; to?: string; } // ─── dashboard overview endpoint ─────────────────────────────────────────────── /** * GET /api/sales/dashboard/overview?period=...&from=...&to=... (Bearer, начальник) * → SalesDashboardOverview */ export async function getSalesDashboardOverview(params: SalesDashboardParams): Promise { const { data } = await axios.get('/api/sales/dashboard/overview', { params, headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest', ...authHeaders(), }, }); return data; } // ─── managers performance (результативность менеджеров · начальник) types ────── /** * Строка таблицы «Результативность менеджеров» (Task 6.2b). * status: 'active' — работает, 'vacation' — в отпуске. */ export interface SalesPerformanceRow { manager_id: number; name: string; email: string; clients_count: number; active_clients: number; leads_delivered: number; oborot_rub: number; paid_all_time_rub: number; earned_rub: number; status: 'active' | 'vacation'; } export interface SalesPerformanceParams { period: string; from?: string; to?: string; search?: string; } // ─── managers performance endpoint ───────────────────────────────────────────── /** * GET /api/sales/managers/performance?period=...&from=...&to=...&search=... (Bearer, начальник) * → { data: SalesPerformanceRow[] } */ export async function getSalesManagersPerformance(params: SalesPerformanceParams): Promise { const { data } = await axios.get<{ data: SalesPerformanceRow[] }>('/api/sales/managers/performance', { params, headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest', ...authHeaders(), }, }); return data.data; } // ─── managers (создание менеджера + список · начальник) types ────────────────── /** * Строка менеджера в таблице «Менеджеры отдела» (Task 7.1b). * is_active=false трактуется как «Отпуск». */ export interface SalesManagerRow { id: number; name: string; email: string; role: 'manager' | 'head'; is_active: boolean; clients_count: number; paid_all_time_rub: number; } /** Результат POST /api/sales/managers → 201. */ export interface SalesManagerCreateResult { id: number; name: string; email: string; role: 'manager' | 'head'; is_active: boolean; } // ─── managers endpoints ──────────────────────────────────────────────────────── /** * GET /api/sales/managers (Bearer, начальник) → { data: SalesManagerRow[] } */ export async function listSalesManagers(): Promise { const { data } = await axios.get<{ data: SalesManagerRow[] }>('/api/sales/managers', { headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest', ...authHeaders(), }, }); return data.data; } /** * POST /api/sales/managers (Bearer, начальник) → 201 SalesManagerCreateResult * Дубль email / невалидные данные → 422. */ export async function createSalesManager(payload: { name: string; email: string; password: string; role: 'manager' | 'head'; }): Promise { const { data } = await axios.post('/api/sales/managers', payload, { headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest', ...authHeaders(), }, }); return data; }