a85148555c
Task 1.3b: SalesClientsView — таблица 11 колонок по демо #page-clients (Клиент/Тип/Активность/Баланс/Запас/Проектов/Пришло/Оборот/Тариф/Заработал/Статус), бейдж типа лица, чипы статуса (Триал/Активен/Просрочка/Приостановлен), HelpHint на терминах, деньги ru-RU + JetBrains Mono, перезагрузка при смене периода, клик по строке → карточка. Заработал=«—» до Фазы 3. listSalesClients в api/sales.ts. Vitest 6/6, lint/type-check 0. Один эскейп на сессию. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
145 lines
4.8 KiB
TypeScript
145 lines
4.8 KiB
TypeScript
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: 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;
|
|
}
|