feat(health): 工作台遗留项修复 — UNION ALL 聚合 + 团队概览 + 较昨日对比
1. 待办列表 UNION ALL 聚合:list_action_items 现从 ai_suggestion + alerts + follow_up_task 三表查询, ActionType 扩展为 AiSuggestion/Alert/Followup/DataAnomaly 四种类型, get_action_thread 按类型构建不同线程时间线(AI 建议/告警/随访) 2. 真实团队概览:get_team_overview 从 doctor_profile + follow_up_task + alerts 聚合成员统计和风险分布 3. 统计卡片较昨日描述:PersonalStatsResp 新增 6 个 yesterday_* 字段, Home.tsx 统计卡片底部渲染"较昨日+N"绿色/红色描述 4. 前端 ActionDetailDrawer 改用 item.id(action_type:uuid 格式)调用线程 API
This commit is contained in:
@@ -155,6 +155,12 @@ export interface PersonalStats {
|
||||
vital_signs_reported: number;
|
||||
vital_signs_total: number;
|
||||
pending_lab_reviews: number;
|
||||
yesterday_my_patients?: number;
|
||||
yesterday_today_appointments?: number;
|
||||
yesterday_consultations_this_month?: number;
|
||||
yesterday_follow_up_rate?: number;
|
||||
yesterday_today_follow_ups?: number;
|
||||
yesterday_overdue_follow_ups?: number;
|
||||
}
|
||||
|
||||
export interface OverviewStatistics {
|
||||
@@ -221,6 +227,51 @@ export interface HealthDataStats {
|
||||
|
||||
// --- API ---
|
||||
|
||||
export interface PointsAccountDetail {
|
||||
id: string;
|
||||
patient_id: string;
|
||||
balance: number;
|
||||
total_earned: number;
|
||||
total_spent: number;
|
||||
total_expired: number;
|
||||
}
|
||||
|
||||
export interface PointsTransactionDetail {
|
||||
id: string;
|
||||
account_id: string;
|
||||
transaction_type: string;
|
||||
amount: number;
|
||||
remaining_amount: number;
|
||||
status: string;
|
||||
expires_at: string | null;
|
||||
balance_after: number;
|
||||
description: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export const pointsAdminApi = {
|
||||
getPatientAccount: async (patientId: string) => {
|
||||
const { data } = await client.get<{
|
||||
success: boolean;
|
||||
data: PointsAccountDetail;
|
||||
}>(`/health/admin/points/patients/${patientId}/account`);
|
||||
return data.data;
|
||||
},
|
||||
|
||||
listPatientTransactions: async (
|
||||
patientId: string,
|
||||
params: { page?: number; page_size?: number },
|
||||
) => {
|
||||
const { data } = await client.get<{
|
||||
success: boolean;
|
||||
data: PaginatedResponse<PointsTransactionDetail>;
|
||||
}>(`/health/admin/points/patients/${patientId}/transactions`, { params });
|
||||
return data.data;
|
||||
},
|
||||
};
|
||||
|
||||
// --- API (original) ---
|
||||
|
||||
export const pointsApi = {
|
||||
// Rules
|
||||
listRules: async () => {
|
||||
|
||||
@@ -78,6 +78,7 @@ interface StatCardDef {
|
||||
key: string;
|
||||
title: string;
|
||||
getValue: (p: PersonalStats | null, s: ReturnType<typeof useStatsData>) => number;
|
||||
getDiff?: (p: PersonalStats | null) => number | undefined;
|
||||
icon: React.ReactNode;
|
||||
suffix?: string;
|
||||
path: string;
|
||||
@@ -106,15 +107,15 @@ const STAT_TEXT_COLORS: string[] = ['#2563EB', '#7C3AED', '#DC2626', '#D97706'];
|
||||
|
||||
const ROLE_STATS: Record<DashboardRole, StatCardDef[]> = {
|
||||
doctor: [
|
||||
{ key: 'my-patients', title: '我的患者', getValue: (p) => p?.my_patients ?? 0, icon: <TeamOutlined />, path: '/health/patients' },
|
||||
{ key: 'today-appointments', title: '今日预约', getValue: (p) => p?.today_appointments ?? 0, icon: <CalendarOutlined />, path: '/health/appointments' },
|
||||
{ key: 'consultations', title: '本月咨询', getValue: (p) => p?.consultations_this_month ?? 0, icon: <MessageOutlined />, path: '/health/consultations' },
|
||||
{ key: 'my-patients', title: '我的患者', getValue: (p) => p?.my_patients ?? 0, getDiff: (p) => { const c = p?.my_patients, y = p?.yesterday_my_patients; return c != null && y != null ? c - y : undefined; }, icon: <TeamOutlined />, path: '/health/patients' },
|
||||
{ key: 'today-appointments', title: '今日预约', getValue: (p) => p?.today_appointments ?? 0, getDiff: (p) => { const c = p?.today_appointments, y = p?.yesterday_today_appointments; return c != null && y != null ? c - y : undefined; }, icon: <CalendarOutlined />, path: '/health/appointments' },
|
||||
{ key: 'consultations', title: '本月咨询', getValue: (p) => p?.consultations_this_month ?? 0, getDiff: (p) => { const c = p?.consultations_this_month, y = p?.yesterday_consultations_this_month; return c != null && y != null ? c - y : undefined; }, icon: <MessageOutlined />, path: '/health/consultations' },
|
||||
{ key: 'followup-rate', title: '随访完成率', getValue: (p) => p?.follow_up_rate ?? 0, icon: <HeartOutlined />, suffix: '%', path: '/health/follow-ups' },
|
||||
],
|
||||
nurse: [
|
||||
{ key: 'today-appointments', title: '今日预约', getValue: (p) => p?.today_appointments ?? 0, icon: <CalendarOutlined />, path: '/health/appointments' },
|
||||
{ key: 'today-followups', title: '今日随访', getValue: (p) => p?.today_follow_ups ?? 0, icon: <HeartOutlined />, path: '/health/follow-ups' },
|
||||
{ key: 'overdue', title: '逾期随访', getValue: (p) => p?.overdue_follow_ups ?? 0, icon: <AlertOutlined />, path: '/health/follow-ups' },
|
||||
{ key: 'today-appointments', title: '今日预约', getValue: (p) => p?.today_appointments ?? 0, getDiff: (p) => { const c = p?.today_appointments, y = p?.yesterday_today_appointments; return c != null && y != null ? c - y : undefined; }, icon: <CalendarOutlined />, path: '/health/appointments' },
|
||||
{ key: 'today-followups', title: '今日随访', getValue: (p) => p?.today_follow_ups ?? 0, getDiff: (p) => { const c = p?.today_follow_ups, y = p?.yesterday_today_follow_ups; return c != null && y != null ? c - y : undefined; }, icon: <HeartOutlined />, path: '/health/follow-ups' },
|
||||
{ key: 'overdue', title: '逾期随访', getValue: (p) => p?.overdue_follow_ups ?? 0, getDiff: (p) => { const c = p?.overdue_follow_ups, y = p?.yesterday_overdue_follow_ups; return c != null && y != null ? c - y : undefined; }, icon: <AlertOutlined />, path: '/health/follow-ups' },
|
||||
{ key: 'vital-rate', title: '体征上报率', getValue: (p) => p?.vital_signs_report_rate ?? 0, icon: <MedicineBoxOutlined />, suffix: '%', path: '/health/vital-signs' },
|
||||
],
|
||||
admin: [
|
||||
@@ -249,6 +250,7 @@ export default function Home() {
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 16, marginBottom: 24 }}>
|
||||
{statDefs.map((def, i) => {
|
||||
const value = def.getValue(personalStats, statsData);
|
||||
const diff = def.getDiff?.(personalStats);
|
||||
return (
|
||||
<div
|
||||
key={def.key}
|
||||
@@ -275,6 +277,11 @@ export default function Home() {
|
||||
<StatValue value={value} loading={loading} />
|
||||
{def.suffix && <span style={{ fontSize: 14, marginLeft: 2 }}>{def.suffix}</span>}
|
||||
</div>
|
||||
{diff != null && (
|
||||
<div style={{ fontSize: 11, marginTop: 4, color: diff > 0 ? '#16A34A' : diff < 0 ? '#DC2626' : '#94A3B8' }}>
|
||||
{diff === 0 ? '与昨日持平' : `较昨日 ${diff > 0 ? '+' : ''}${diff}`}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -57,7 +57,7 @@ export default function ActionDetailDrawer({
|
||||
}
|
||||
setLoading(true);
|
||||
actionInboxApi
|
||||
.getThread(item.source_ref)
|
||||
.getThread(item.id)
|
||||
.then(setThread)
|
||||
.finally(() => setLoading(false));
|
||||
}, [item, open]);
|
||||
|
||||
Reference in New Issue
Block a user