From 4a48a65a48418c9853c5c610d5eae8d79e526b46 Mon Sep 17 00:00:00 2001 From: wangziqi Date: Mon, 6 Jul 2026 00:49:20 +0800 Subject: [PATCH] feat: enhance dashboard with attendance count + Gantt chart --- apps/admin/src/pages/Dashboard/index.tsx | 212 ++++++++++++++++-- .../server/src/dashboard/dashboard.service.ts | 2 +- 2 files changed, 200 insertions(+), 14 deletions(-) diff --git a/apps/admin/src/pages/Dashboard/index.tsx b/apps/admin/src/pages/Dashboard/index.tsx index 58dc596..bdd5041 100644 --- a/apps/admin/src/pages/Dashboard/index.tsx +++ b/apps/admin/src/pages/Dashboard/index.tsx @@ -19,10 +19,24 @@ const COLORS = [ ]; interface BillStatRow { status: string; count: string; total: string } +interface ClassAttendanceRank { className: string; present: number; total: number; rate: number } +interface ClassroomOccupancy { name: string; building: string; capacity: number; scheduleDays: number; rentalCount: number; occupancy: number } interface AttendanceTrendRow { date: string; rate: string } interface IncomeTrendRow { month: string; amount: number } interface OccupancyByBuildingRow { building: string; count: string } interface ExpenseByTypeRow { type: string; total: string } +interface GanttOccupancy { + studentName: string; + studentId?: string; + checkInDate: string; + checkOutDate: string | null; + billingStartDate?: string; + billingEndDate?: string; +} +interface GanttRoom { + roomNumber: string; + occupancies: GanttOccupancy[]; +} interface DashboardStats { totalRooms: number; @@ -39,6 +53,7 @@ interface DashboardStats { teacherCount: number; pendingDeposits: number; activeRentals: number; + todayPresent: number; occupancyByBuilding: OccupancyByBuildingRow[]; attendanceByStatus: Record; expenseByType: ExpenseByTypeRow[]; @@ -52,6 +67,9 @@ const DashboardPage: React.FC = () => { const screens = Grid.useBreakpoint(); const isMobile = !screens.sm; const [stats, setStats] = useState(null); + const [classRanking, setClassRanking] = useState<{ top: ClassAttendanceRank[]; bottom: ClassAttendanceRank[] }>({ top: [], bottom: [] }); + const [classroomOccupancy, setClassroomOccupancy] = useState([]); + const [ganttData, setGanttData] = useState([]); const [roomRanking, setRoomRanking] = useState>([]); const [loading, setLoading] = useState(true); const [period, setPeriod] = useState<[string, string]>([ @@ -62,14 +80,21 @@ const DashboardPage: React.FC = () => { const fetchData = async () => { setLoading(true); try { - const [s, r] = await Promise.all([ + const [s, r, cr, g] = await Promise.all([ api.get('/dashboard/stats'), api.get('/dashboard/room-ranking', { params: { periodStart: period[0], periodEnd: period[1] }, }), + api.get('/dashboard/class-attendance-ranking'), + api.get('/dashboard/gantt', { + params: { periodStart: period[0], periodEnd: period[1] }, + }), ]); setStats(s); - setRoomRanking(r as unknown as Array<{ roomNumber: string; total: string }>); + setClassRanking(cr as unknown as { top: ClassAttendanceRank[]; bottom: ClassAttendanceRank[] }); + setGanttData(g as GanttRoom[]); + const co = await api.get('/dashboard/classroom-occupancy'); + setClassroomOccupancy(co as ClassroomOccupancy[]); } catch (e) { console.error(e); } @@ -80,17 +105,15 @@ const DashboardPage: React.FC = () => { fetchData(); }, [period]); - const expenseTypeMap: Record = { - water: '水费', - electricity: '电费', - cleaning: '保洁费', - damage: '损坏赔偿', - penalty: '罚款', - key: '钥匙费', - remote: '空调遥控器', - deposit_deduction: '押金扣除', - other: '其他', - }; + const [expenseTypeMap, setExpenseTypeMap] = useState>({}); + + useEffect(() => { + api.get('/expense-types').then((types: any[]) => { + const map: Record = {}; + for (const t of types) map[t.code] = t.name; + setExpenseTypeMap(map); + }).catch(() => {}); + }, []); const attendanceLabelMap: Record = { present: '出勤', @@ -139,6 +162,46 @@ const DashboardPage: React.FC = () => { ], }; + // 班级考勤排行 - 前5 + const classRankingTopOption = { + tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' }, valueFormatter: (v: number) => `${v}%` }, + grid: { left: 80, right: 30, bottom: 30, top: 10 }, + xAxis: { type: 'value', max: 100, axisLabel: { formatter: '{value}%' } }, + yAxis: { + type: 'category', + data: classRanking.top.map((r) => r.className), + inverse: true, + }, + series: [ + { + type: 'bar', + data: classRanking.top.map((r) => r.rate), + itemStyle: { color: '#34C759', borderRadius: [0, 4, 4, 0] }, + label: { show: true, position: 'right', formatter: '{c}%' }, + }, + ], + }; + + // 班级考勤排行 - 后5 + const classRankingBottomOption = { + tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' }, valueFormatter: (v: number) => `${v}%` }, + grid: { left: 80, right: 30, bottom: 30, top: 10 }, + xAxis: { type: 'value', max: 100, axisLabel: { formatter: '{value}%' } }, + yAxis: { + type: 'category', + data: classRanking.bottom.map((r) => r.className), + inverse: true, + }, + series: [ + { + type: 'bar', + data: classRanking.bottom.map((r) => r.rate), + itemStyle: { color: '#FF3B30', borderRadius: [0, 4, 4, 0] }, + label: { show: true, position: 'right', formatter: '{c}%' }, + }, + ], + }; + // 考勤趋势折线图 const attendanceLineOption = { tooltip: { trigger: 'axis' }, @@ -182,6 +245,57 @@ const DashboardPage: React.FC = () => { ], }; + // 入住时间线(甘特图) + const ganttOption = { + tooltip: { + formatter: (p: { data: { name: string; value: [string, string, string, boolean] } }) => + `${p.data.name}
入住: ${p.data.value[1]}
退宿: ${p.data.value[2]}`, + }, + grid: { left: 100, right: 30, bottom: 40, top: 20 }, + xAxis: { type: 'time' }, + yAxis: { type: 'category', data: ganttData.map((r) => r.roomNumber), inverse: true }, + dataZoom: [ + { type: 'slider', xAxisIndex: 0, bottom: 10, height: 20 }, + { type: 'inside', xAxisIndex: 0 }, + ], + series: [{ + type: 'custom', + renderItem: (_params: unknown, api: { + value: (i: number) => string | boolean; + coord: (p: [string | number, string | number]) => [number, number]; + size: (p: [number, number]) => [number, number]; + }) => { + const cat = api.value(0); + const start = api.coord([api.value(1), cat]); + const end = api.coord([api.value(2), cat]); + const height = api.size([0, 1])[1] * 0.6; + const rectShape = { + x: start[0], + y: start[1] - height / 2, + width: Math.max(end[0] - start[0], 2), + height, + }; + return { + type: 'rect' as const, + shape: rectShape, + style: { fill: api.value(3) ? '#34C759' : '#FF9500', stroke: '#fff', lineWidth: 1 }, + }; + }, + encode: { x: [1, 2], y: 0 }, + data: ganttData.flatMap((r) => + (r.occupancies || []).map((o) => ({ + name: o.studentName, + value: [ + r.roomNumber, + o.checkInDate, + o.checkOutDate || new Date().toISOString().slice(0, 10), + !o.checkOutDate, + ] as [string, string, string, boolean], + })) + ), + }], + }; + if (loading && !stats) return ; @@ -307,6 +421,16 @@ const DashboardPage: React.FC = () => { } /> + + + } + /> + + @@ -330,6 +454,56 @@ const DashboardPage: React.FC = () => { + + + + {classRanking.top.length > 0 ? ( + + ) : ( +
暂无考勤数据
+ )} +
+ + + + {classRanking.bottom.length > 0 ? ( + + ) : ( +
暂无考勤数据
+ )} +
+ +
+ + + + + {classroomOccupancy.length > 0 ? ( + `${p.name}
排课: ${p.data.scheduleDays}天 租赁: ${p.data.rentalCount}个 占用率: ${(p.data.occupancy * 100).toFixed(0)}%`, + }, + grid: { left: 100, right: 20, bottom: 30, top: 10 }, + xAxis: { type: 'value', max: 1 }, + yAxis: { type: 'category', data: classroomOccupancy.map((r) => r.name), inverse: true }, + visualMap: { + min: 0, max: 1, orient: 'horizontal', left: 'center', bottom: 0, + inRange: { color: ['#e6f4ff', '#91caff', '#40a9ff', '#0050b3', '#002c8c'] }, + }, + series: [{ + type: 'bar', + data: classroomOccupancy.map((r) => ({ name: r.name, value: r.occupancy, scheduleDays: r.scheduleDays, rentalCount: r.rentalCount, occupancy: r.occupancy })), + itemStyle: { borderRadius: [0, 4, 4, 0] }, + label: { show: true, position: 'right', formatter: (p: any) => `${(p.data.occupancy * 100).toFixed(0)}%` }, + }], + }} style={{ width: '100%', height: isMobile ? 300 : 400 }} /> + ) : ( +
暂无教室数据
+ )} +
+ +
+ @@ -367,6 +541,18 @@ const DashboardPage: React.FC = () => { + + + + + {ganttData.length > 0 ? ( + + ) : ( +
暂无入住数据
+ )} +
+ +
); }; diff --git a/apps/server/src/dashboard/dashboard.service.ts b/apps/server/src/dashboard/dashboard.service.ts index 051bd35..75d61cb 100644 --- a/apps/server/src/dashboard/dashboard.service.ts +++ b/apps/server/src/dashboard/dashboard.service.ts @@ -159,7 +159,7 @@ export class DashboardService { billStats, classroomCount, classroomOccupancyRate, - todayAttendanceRate, + todayPresent, monthlyIncome, classCount, teacherCount,