Files
portal/app/resources/js/api/sales.ts
T
Дмитрий daa41bcecf feat(sales): пересмотр состава тарифов портала продаж — 2 вида
Решение заказчика 03.07.2026: два вида тарифа вместо трёх — суточный
оклад daily_salary и процент от пополнений topup_step. Убраны
percent_oborot и fix_per_client.

Миграция 2026_07_03_120000 меняет CHECK sales_tariffs.kind, освобождает
FK sales_users.current_tariff_id и sales_client_assignments.tariff_id,
удаляет тарифы уходящих видов. SalesEarningsService для уходящих видов
возвращает 0 по default-ветке. Снимки привязок не трогаются.

Обновлены контроллеры тарифов и дохода, сервисы Earnings и Metrics,
фронт SalesTariffsView и api/sales.ts, демо-сид, тесты бэка и фронта,
CHANGELOG схемы v8.61 и спека портала.

Не на проде: ветка feat/sales-portal-demo.

Проверка: Sales Feature 169/169, Vitest SalesTariffs 10/10, Larastan 0.

Co-Authored-By: Claude Opus 4.8 1M context <noreply@anthropic.com>
2026-07-05 19:25:19 +03:00

914 lines
30 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.
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<string, string> {
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<string, string[]> } | 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<SalesLoginResponse> {
const { data } = await axios.post<SalesLoginResponse>(
'/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<SalesUser> {
const { data } = await axios.get<SalesUser>('/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<void> {
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<SalesClientRow[]> {
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<SalesOverview> {
const { data } = await axios.get<SalesOverview>('/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<SalesClientCard> {
const { data } = await axios.get<SalesClientCard>(`/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<SalesAttachmentSubmitResult> {
const { data } = await axios.post<SalesAttachmentSubmitResult>(
'/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<SalesAttachmentRow[]> {
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<SalesAttachmentQueue> {
const { data } = await axios.get<SalesAttachmentQueue>('/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<SalesAttachmentSubmitResult> {
const { data } = await axios.post<SalesAttachmentSubmitResult>(
`/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<string, unknown>;
}
/** Строка менеджера в таблице назначения тарифа (для новых клиентов). */
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<SalesTariffsResponse> {
const { data } = await axios.get<SalesTariffsResponse>('/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<string, unknown>;
}): Promise<SalesTariff> {
const { data } = await axios.post<SalesTariff>('/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<string, unknown> },
): Promise<SalesTariff> {
const { data } = await axios.put<SalesTariff>(`/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<SalesTariffAssignResult> {
const { data } = await axios.post<SalesTariffAssignResult>('/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<SalesPayoutRow[]> {
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<SalesRemainingRow[]> {
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<SalesPayoutRow> {
const { data } = await axios.post<SalesPayoutRow>('/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<SalesIncomeResponse> {
const { data } = await axios.get<SalesIncomeResponse>('/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<SalesInvoicesResponse> {
const { data } = await axios.get<SalesInvoicesResponse>('/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<SalesDashboardOverview> {
const { data } = await axios.get<SalesDashboardOverview>('/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<SalesPerformanceRow[]> {
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<SalesManagerRow[]> {
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<SalesManagerCreateResult> {
const { data } = await axios.post<SalesManagerCreateResult>('/api/sales/managers', payload, {
headers: {
Accept: 'application/json',
'X-Requested-With': 'XMLHttpRequest',
...authHeaders(),
},
});
return data;
}