02a8a90e4d
Backend: AdminDashboardController +leads/+supply эндпоинты, summary дополнен плитками leads/supply; сверка заказа вынесена в чистый сервис SupplyReconciliation (спрос → формула computeOrder=max(max,⌈Σ/3⌉) → факт → рассинхрон). Лиды: доставлено сегодня / зависшие 4ч+ / нераспределённые / % доставки — cross-tenant под pgsql_admin. Frontend: плитки Лиды и Заказ оживлены (убраны заглушки «Этап 2»), drill с KPI и таблицей групп спрос→формула→факт→совпадает. Тесты: SupplyReconciliation unit 3/3, Leads/Supply/Summary feature, admin-срез 87 зелёных, фронт 10/10. stan 0, pint/eslint/type-check/build чисто. phpstan-baseline перегенерирован (getJson false-positive на новых тестах). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
123 lines
3.3 KiB
TypeScript
123 lines
3.3 KiB
TypeScript
import { apiClient } from './client';
|
|
|
|
/**
|
|
* SaaS-admin «Командный центр» — типизированный клиент read-only агрегатов.
|
|
*
|
|
* Все 3 эндпоинта — GET под группой ['saas-admin','admin-db'] (cross-tenant
|
|
* через pgsql_admin). CSRF не нужен (только чтение).
|
|
* Backend: AdminDashboardController. Spec:
|
|
* docs/superpowers/specs/2026-06-27-admin-command-center-design.md
|
|
*/
|
|
|
|
export type Light = 'green' | 'amber' | 'red';
|
|
|
|
export interface DashboardSummary {
|
|
period: string;
|
|
finance: {
|
|
topups_rub: string;
|
|
charges_rub: string;
|
|
active_clients: number;
|
|
new_clients: number;
|
|
negative_balance_count: number;
|
|
light: Light;
|
|
};
|
|
health: {
|
|
light: Light;
|
|
open_incidents: number;
|
|
failed_jobs_24h: number;
|
|
last_sync_status: string;
|
|
last_sync_at: string | null;
|
|
};
|
|
leads: {
|
|
light: Light;
|
|
delivered_today: number;
|
|
stuck: number;
|
|
unrouted: number;
|
|
delivery_pct: number;
|
|
};
|
|
supply: {
|
|
light: Light;
|
|
demand: number;
|
|
formula: number;
|
|
ordered: number;
|
|
mismatches: number;
|
|
};
|
|
}
|
|
|
|
export interface LeadsDetail {
|
|
light: Light;
|
|
kpi: {
|
|
delivered_today: number;
|
|
stuck: number;
|
|
unrouted: number;
|
|
unassigned_deals: number;
|
|
delivery_pct: number;
|
|
};
|
|
}
|
|
|
|
export interface SupplyDetail {
|
|
snapshot_date: string | null;
|
|
light: Light;
|
|
totals: { demand: number; formula: number; ordered: number; mismatches: number };
|
|
groups: Array<{
|
|
signal_type: string;
|
|
identifier: string;
|
|
demand: number;
|
|
formula: number;
|
|
ordered: number;
|
|
in_sync: boolean;
|
|
}>;
|
|
}
|
|
|
|
export interface FinanceDetail {
|
|
period: string;
|
|
kpi: {
|
|
topups_rub: string;
|
|
charges_rub: string;
|
|
net_inflow_rub: string;
|
|
negative_balance_count: number;
|
|
};
|
|
attention: Array<{
|
|
id: number;
|
|
subdomain: string;
|
|
organization_name: string;
|
|
balance_rub: string;
|
|
state: string;
|
|
}>;
|
|
top_by_turnover: Array<{
|
|
id: number;
|
|
organization_name: string;
|
|
topped_rub: string;
|
|
}>;
|
|
}
|
|
|
|
export interface HealthDetail {
|
|
overall_light: Light;
|
|
subsystems: Array<{ key: string; light: Light; detail: string }>;
|
|
}
|
|
|
|
export async function getDashboardSummary(period: string): Promise<DashboardSummary> {
|
|
const { data } = await apiClient.get<DashboardSummary>('/api/admin/dashboard', { params: { period } });
|
|
return data;
|
|
}
|
|
|
|
export async function getDashboardFinance(period: string): Promise<FinanceDetail> {
|
|
const { data } = await apiClient.get<FinanceDetail>('/api/admin/dashboard/finance', { params: { period } });
|
|
return data;
|
|
}
|
|
|
|
export async function getDashboardHealth(): Promise<HealthDetail> {
|
|
const { data } = await apiClient.get<HealthDetail>('/api/admin/dashboard/health');
|
|
return data;
|
|
}
|
|
|
|
export async function getDashboardLeads(): Promise<LeadsDetail> {
|
|
const { data } = await apiClient.get<LeadsDetail>('/api/admin/dashboard/leads');
|
|
return data;
|
|
}
|
|
|
|
export async function getDashboardSupply(): Promise<SupplyDetail> {
|
|
const { data } = await apiClient.get<SupplyDetail>('/api/admin/dashboard/supply');
|
|
return data;
|
|
}
|