267 lines
10 KiB
TypeScript
267 lines
10 KiB
TypeScript
import { useEffect, useMemo, useState } from 'react';
|
|
import { getRevenueStatistics, getTodayExamStatistics, getUserOwnedMenus, getB1ServiceBoard, getNorth3ServiceBoard } from '../../api';
|
|
import type { OutputB1ServiceBoard, OutputNorth3ServiceBoard } from '../../api/types';
|
|
import { Card, CardContent, CardHeader, InfoCard } from '../ui';
|
|
import { cleanLocalStorageIfNeeded } from '../../utils/clean';
|
|
|
|
const APP_ID = 'b2b49e91d21446aeb14579930f732985';
|
|
|
|
type HomeStatItem = { label: string; value: number };
|
|
type RevenueStatItem = { label: string; value: string | number };
|
|
|
|
export const HomeSection = () => {
|
|
const [homeStats, setHomeStats] = useState<HomeStatItem[]>([
|
|
{ label: '今日预约', value: 0 },
|
|
{ label: '签到人数', value: 0 },
|
|
{ label: '打印导检单', value: 0 },
|
|
{ label: '在检人数', value: 0 },
|
|
{ label: '已完成人数', value: 0 },
|
|
]);
|
|
const [revenueStats, setRevenueStats] = useState<RevenueStatItem[]>([
|
|
{ label: '体检收入', value: '¥ 0' },
|
|
{ label: '加项收入', value: '¥ 0' },
|
|
{ label: '整体收入', value: '¥ 0' },
|
|
{ label: '当日目标', value: '¥ 0' },
|
|
{ label: '完成百分比', value: '0%' },
|
|
{ label: '当日缺口', value: '¥ 0' },
|
|
]);
|
|
const [showRevenueStats, setShowRevenueStats] = useState(false);
|
|
const [b1Data, setB1Data] = useState<OutputB1ServiceBoard | null>(null);
|
|
const [b1Loading, setB1Loading] = useState(false);
|
|
const [north3Data, setNorth3Data] = useState<OutputNorth3ServiceBoard | null>(null);
|
|
const [north3Loading, setNorth3Loading] = useState(false);
|
|
|
|
const currencyFormatter = useMemo(() => new Intl.NumberFormat('zh-CN', {
|
|
style: 'currency',
|
|
currency: 'CNY',
|
|
minimumFractionDigits: 0,
|
|
maximumFractionDigits: 0,
|
|
}), []);
|
|
|
|
useEffect(() => {
|
|
|
|
// 清理本地存储
|
|
cleanLocalStorageIfNeeded();
|
|
|
|
getTodayExamStatistics({})
|
|
.then((res) => {
|
|
const d = res.Data;
|
|
setHomeStats([
|
|
{ label: '今日预约', value: Number(d?.today_appointment_count ?? 0) },
|
|
{ label: '签到人数', value: Number(d?.today_signin_count ?? 0) },
|
|
{ label: '打印导检单', value: Number(d?.today_print_guide_count ?? 0) },
|
|
{ label: '在检人数', value: Number(d?.today_in_exam_count ?? 0) },
|
|
{ label: '已完成人数', value: Number(d?.today_completed_count ?? 0) },
|
|
]);
|
|
})
|
|
.catch((err) => {
|
|
console.error('获取今日体检统计失败', err);
|
|
});
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
getRevenueStatistics({})
|
|
.then((res) => {
|
|
const d = res.Data;
|
|
const fmt = (n?: number | null) => currencyFormatter.format(Number(n ?? 0));
|
|
setRevenueStats([
|
|
{ label: '体检收入', value: fmt(d?.physical_exam_income) },
|
|
{ label: '加项收入', value: fmt(d?.add_item_income) },
|
|
{ label: '整体收入', value: fmt(d?.total_income) },
|
|
{ label: '当日目标', value: fmt(d?.target_income) },
|
|
{ label: '完成百分比', value: d?.completion_percentage ?? '0%' },
|
|
{ label: '当日缺口', value: fmt(d?.gap_amount) },
|
|
]);
|
|
})
|
|
.catch((err) => {
|
|
console.error('获取营收数据统计失败', err);
|
|
});
|
|
}, [currencyFormatter]);
|
|
|
|
useEffect(() => {
|
|
getUserOwnedMenus({ app_id: APP_ID })
|
|
.then((res) => {
|
|
console.log('获取用户菜单权限返回内容:', res);
|
|
|
|
// 检查菜单 children 中是否有 authCode === 'HisTijianPad_Btn_Tongji' 的项
|
|
const menus = res.Data?.menus || [];
|
|
let hasPermission = false;
|
|
|
|
for (const menu of menus) {
|
|
if (menu.children && Array.isArray(menu.children)) {
|
|
for (const child of menu.children) {
|
|
if (typeof child === 'object' && child.authCode === 'HisTijianPad_Btn_Tongji') {
|
|
hasPermission = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (hasPermission) break;
|
|
}
|
|
|
|
setShowRevenueStats(hasPermission);
|
|
})
|
|
.catch((err) => {
|
|
console.error('获取用户菜单权限失败', err);
|
|
setShowRevenueStats(false);
|
|
});
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
setB1Loading(true);
|
|
getB1ServiceBoard({})
|
|
.then((res) => {
|
|
if (res.Status === 200 && res.Data) {
|
|
setB1Data(res.Data);
|
|
}
|
|
})
|
|
.catch((err) => {
|
|
console.error('获取B1服务看板失败', err);
|
|
setB1Data(null);
|
|
})
|
|
.finally(() => {
|
|
setB1Loading(false);
|
|
});
|
|
}, []);
|
|
|
|
const b1ServiceList = b1Data?.b1_service_info_list || [];
|
|
|
|
useEffect(() => {
|
|
setNorth3Loading(true);
|
|
getNorth3ServiceBoard({})
|
|
.then((res) => {
|
|
if (res.Status === 200 && res.Data) {
|
|
setNorth3Data(res.Data);
|
|
}
|
|
})
|
|
.catch((err) => {
|
|
console.error('获取北3服务看板失败', err);
|
|
setNorth3Data(null);
|
|
})
|
|
.finally(() => {
|
|
setNorth3Loading(false);
|
|
});
|
|
}, []);
|
|
|
|
return (
|
|
<div className='space-y-6'>
|
|
<Card>
|
|
<CardHeader>今日体检统计</CardHeader>
|
|
<CardContent>
|
|
<div className='grid grid-cols-5 gap-3'>
|
|
{homeStats.map(({ label, value }) => (
|
|
<InfoCard key={label} label={label} value={value} />
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{showRevenueStats && (
|
|
<Card>
|
|
<CardHeader>今日营收统计</CardHeader>
|
|
<CardContent>
|
|
<div className='grid grid-cols-3 gap-3'>
|
|
{revenueStats.map(({ label, value }) => (
|
|
<InfoCard key={label} label={label} value={value} />
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
<div className='grid grid-cols-2 gap-4'>
|
|
<Card>
|
|
<CardHeader>B1 服务看板</CardHeader>
|
|
<CardContent>
|
|
<div className='grid grid-cols-3 gap-3 mb-3'>
|
|
<InfoCard label='客户总数' value={b1Data?.total_customer_count ?? 0} />
|
|
<InfoCard label='待检人数' value={b1Data?.waiting_exam_count ?? 0} />
|
|
<InfoCard label='在检人数' value={b1Data?.in_exam_count ?? 0} />
|
|
</div>
|
|
{b1Loading ? (
|
|
<div className='text-center text-gray-500 py-4'>加载中...</div>
|
|
) : b1ServiceList.length === 0 ? (
|
|
<div className='text-center text-gray-500 py-4'>暂无数据</div>
|
|
) : (
|
|
<table className='w-full text-xs'>
|
|
<thead>
|
|
<tr className='text-gray-500 border-b'>
|
|
<th className='py-2 text-left'>科室</th>
|
|
<th className='py-2 text-left'>医生</th>
|
|
<th className='py-2 text-right'>已检人数</th>
|
|
<th className='py-2 text-right'>已检部位数</th>
|
|
<th className='py-2 text-right'>总时长</th>
|
|
<th className='py-2 text-right'>平均时长</th>
|
|
<th className='py-2 text-right'>在检</th>
|
|
<th className='py-2 text-right'>待检</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{b1ServiceList.map((item, index) => (
|
|
<tr key={`${item.dept_name}-${item.doctor_name}-${index}`} className='border-b last:border-b-0'>
|
|
<td className='py-2'>{item.dept_name || '-'}</td>
|
|
<td className='py-2'>{item.doctor_name || '-'}</td>
|
|
<td className='py-2 text-right'>{item.completed_customer_count ?? '-'}</td>
|
|
<td className='py-2 text-right'>{item.exam_part_count ?? '-'}</td>
|
|
<td className='py-2 text-right'>{item.total_duration_minutes !== null && item.total_duration_minutes !== undefined ? `${item.total_duration_minutes}分钟` : '-'}</td>
|
|
<td className='py-2 text-right'>{item.average_duration_minutes !== null && item.average_duration_minutes !== undefined ? `${item.average_duration_minutes}分钟` : '-'}</td>
|
|
<td className='py-2 text-right'>-</td>
|
|
<td className='py-2 text-right'>-</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>北3服务看板</CardHeader>
|
|
<CardContent>
|
|
<div className='grid grid-cols-3 gap-3 mb-3'>
|
|
<InfoCard label='今日家医数' value={north3Data?.today_family_doctor_count ?? 0} />
|
|
<InfoCard label='分配客户数' value={north3Data?.assigned_customer_count ?? 0} />
|
|
<InfoCard label='面诊数' value={north3Data?.face_to_face_count ?? 0} />
|
|
</div>
|
|
{north3Loading ? (
|
|
<div className='text-center text-gray-500 py-4'>加载中...</div>
|
|
) : !north3Data || north3Data.north3_service_list.length === 0 ? (
|
|
<div className='text-center text-gray-500 py-4'>暂无数据</div>
|
|
) : (
|
|
<table className='w-full text-xs'>
|
|
<thead>
|
|
<tr className='text-gray-500 border-b'>
|
|
<th className='py-2 text-left'>家医</th>
|
|
<th className='py-2 text-right'>分配客户数</th>
|
|
<th className='py-2 text-right'>面诊数</th>
|
|
<th className='py-2 text-right'>面诊率</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{north3Data.north3_service_list.map((item, index) => (
|
|
<tr key={`${item.family_doctor_name}-${index}`} className='border-b last:border-b-0'>
|
|
<td className='py-2'>{item.family_doctor_name || '-'}</td>
|
|
<td className='py-2 text-right'>{item.assigned_customer_count ?? '-'}</td>
|
|
<td className='py-2 text-right'>{item.face_to_face_count ?? '-'}</td>
|
|
<td className='py-2 text-right'>
|
|
{item.face_to_face_rate !== undefined
|
|
? `${(item.face_to_face_rate * 100).toFixed(2)}%`
|
|
: item.assigned_customer_count && item.assigned_customer_count > 0
|
|
? `${((item.face_to_face_count ?? 0) / item.assigned_customer_count * 100).toFixed(2)}%`
|
|
: '-'
|
|
}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
|