feat(management): 前端API client——管理端点typed契约(批次B冻结面)
对批次B as-built逐键核对后相对任务稿的修正(以真机响应为准): - DashboardResp 补 caliber_notes(总述口径说明) - DrilldownResp 补 page/page_size/scope(各分支均返回) - FactDrillRow 补 chat_hint(owner+talker 直达聊天记录) - LifecycleDrillRow.nickname 收窄为非空(后端 friend_id 兜底) - EmployeeRow.metrics 注明 user_id 键为字符串透传字段 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
// 经营管理板块 API(批次C)。契约以批次B终评冻结面为准:
|
||||
// None≠0(无样本分位/率=null);scope.channel 回显原值;drilldown 白名单六值。
|
||||
// 已对照批次B as-built(management.py/api_assembly.py)逐键核对:
|
||||
// - dashboard 另有 caliber_notes(总述口径说明,Record<string,string>)
|
||||
// - drilldown 各分支均返回 page/page_size/scope(与 items/total 同层)
|
||||
// - first_reply_* 行含 chat_hint(owner+talker 直达聊天记录)
|
||||
// - lifecycle 行 nickname 恒非空(后端 friend_nickname→remark→friend_id 兜底)
|
||||
import { getJson } from './client'
|
||||
import type { Channel } from './types'
|
||||
|
||||
export type MgmtChannel = 'all' | Channel
|
||||
export type CompareMode = 'previous' | 'personal_baseline' | 'team_median'
|
||||
export type DrilldownCode =
|
||||
| 'first_reply_p90' | 'first_reply_p50' | 'unreplied'
|
||||
| 'cooling_customers' | 'dormant_customers' | 'call_followup'
|
||||
|
||||
export interface MgmtScope { channel: MgmtChannel; start: number; end: number; compare?: CompareMode; view?: string; user_id?: string; metric_code?: string }
|
||||
export interface Kpis {
|
||||
active_customers: number; active_employees: number
|
||||
msg_total: number; cust_threads: number; threads_answered: number
|
||||
call_count: number; call_seconds: number; answered_call: number
|
||||
lifecycle: { new: number; cooling: number; dormant: number; reactivated: number }
|
||||
}
|
||||
export interface CommTrendPoint { day: number; msg_total: number; active_customers: number; call_cnt: number }
|
||||
export interface SvcTrendPoint { day: number; fr_p50_ms: number | null; fr_p90_ms: number | null; reply_2h_rate: number | null }
|
||||
export interface DimSummary { [dim: string]: { name: string; weight: number; scored_count: number; pending_count: number; insufficient_count: number; avg_score: number | null } }
|
||||
export interface AnomalyCard {
|
||||
rule_code: string; severity: 'critical' | 'warning'; employee_user_id: string
|
||||
title: string; current_value: number; baseline_value: number | null; unit: string
|
||||
sample_size: number; affected_count: number; reason: string
|
||||
drilldown: { type: string; query: { metric_code?: string; user_id?: string } }
|
||||
suggested_action: string
|
||||
}
|
||||
export interface DashboardResp {
|
||||
scope: MgmtScope; data_as_of: number | null; metric_version: string; config_source: 'db' | 'default'
|
||||
kpis: Kpis
|
||||
communication_trend: CommTrendPoint[]
|
||||
service_trend: SvcTrendPoint[]
|
||||
lifecycle: Kpis['lifecycle']
|
||||
dimension_summary: DimSummary
|
||||
anomalies: AnomalyCard[]
|
||||
action_summary: { unreplied_2h: number; churn_real_30d: number; sensitive_pending: number }
|
||||
caliber_notes: Record<string, string>
|
||||
}
|
||||
export interface EmployeeRow {
|
||||
user_id: string; name: string | null; is_on_job: boolean | null
|
||||
// metrics=PeriodMetrics._asdict():数值字段外的 user_id 键是字符串(透传
|
||||
// 字段,UI 取身份用顶层 row.user_id,动态取数只碰数值键)
|
||||
metrics: Record<string, number | null>
|
||||
comparisons: Record<string, { current: number | null; prev: number | null; baseline: number | null; median: number | null }>
|
||||
score: { total_score: number | null; grade: string | null; score_status: string; trial: boolean; dimensions: Record<string, { score: number | null; status: string }> }
|
||||
anomaly_count: number; anomaly_weight: number
|
||||
}
|
||||
export interface EmployeesResp { total: number; page: number; page_size: number; rows: EmployeeRow[]; scope: MgmtScope; data_as_of: number | null; metric_version: string; config_source: 'db' | 'default' }
|
||||
export interface FactDrillRow { day: number; channel: string; owner_account_id: string; peer_id: string; thread_end_ts: number; reply_ts: number; fr_ms: number; chat_hint: { owner_account_id: string; talker_id: string } }
|
||||
export interface LifecycleDrillRow { friend_id: string; channel: string; nickname: string; last_day: number; state: string }
|
||||
export interface CallDrillRow { call_id: string; channel: string; call_number: string | null; start_call_time: number; call_duration: number }
|
||||
export interface DrilldownResp { total: number; page: number; page_size: number; scope: MgmtScope; items: Record<string, unknown>[]; note?: string; truncated?: boolean; summary?: Record<string, number> }
|
||||
export interface MetricDefsResp {
|
||||
definitions: Record<string, { name: string; unit: string; direction: number; caliber?: string; formula?: string }>
|
||||
score_config: { version: string; name: string; trial: boolean; normalization: string; sample_guards: Record<string, number>; grade_bounds: Record<string, number>; dimensions: Record<string, { name: string; weight: number; status?: string }> }
|
||||
caliber_notes: string
|
||||
config_source: 'db' | 'default'
|
||||
}
|
||||
|
||||
export const getDashboard = (channel: MgmtChannel, start: number, end: number, compare: CompareMode = 'previous') =>
|
||||
getJson<DashboardResp>('/management/dashboard', { channel, start, end, compare })
|
||||
export const getEmployees = (params: Record<string, unknown>) =>
|
||||
getJson<EmployeesResp>('/management/employees', params)
|
||||
export const getEmployeeDrilldown = (userId: string, metricCode: DrilldownCode, start: number, end: number, channel: MgmtChannel, page = 1, pageSize = 20) =>
|
||||
getJson<DrilldownResp>(`/management/employees/${userId}/drilldown`, { metric_code: metricCode, start, end, channel, page, page_size: pageSize })
|
||||
export const getMetricDefs = () => getJson<MetricDefsResp>('/management/metric-definitions')
|
||||
Reference in New Issue
Block a user