feat(attendance): add anomaly alert banner showing consecutive absences and frequent lates
This commit is contained in:
@@ -1,20 +1,7 @@
|
|||||||
import React, { useEffect, useState, useMemo, useCallback } from 'react';
|
import React, { useEffect, useState, useMemo, useCallback } from 'react';
|
||||||
import {
|
import {
|
||||||
Table,
|
Table, Button, Modal, Form, DatePicker, Select, Space, message,
|
||||||
Button,
|
Tag, Card, Input, Tooltip, Row, Col, Tabs, Alert,
|
||||||
Modal,
|
|
||||||
Form,
|
|
||||||
DatePicker,
|
|
||||||
Select,
|
|
||||||
Space,
|
|
||||||
message,
|
|
||||||
Tag,
|
|
||||||
Card,
|
|
||||||
Input,
|
|
||||||
Tooltip,
|
|
||||||
Row,
|
|
||||||
Col,
|
|
||||||
Tabs,
|
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import {
|
import {
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
@@ -115,6 +102,7 @@ interface DingRecord {
|
|||||||
matchStatus: string;
|
matchStatus: string;
|
||||||
studentId?: number;
|
studentId?: number;
|
||||||
}
|
}
|
||||||
|
interface AlertItem { studentId: number; studentName: string; className: string; type: string; count: number; lastDate: string }
|
||||||
|
|
||||||
// ── Component ──
|
// ── Component ──
|
||||||
|
|
||||||
@@ -126,6 +114,7 @@ const AttendancePage: React.FC = () => {
|
|||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [pageSize, setPageSize] = useState(20);
|
const [pageSize, setPageSize] = useState(20);
|
||||||
const [classOptions, setClassOptions] = useState<ClassOption[]>([]);
|
const [classOptions, setClassOptions] = useState<ClassOption[]>([]);
|
||||||
|
const [alerts, setAlerts] = useState<AlertItem[]>([]);
|
||||||
|
|
||||||
// Filters
|
// Filters
|
||||||
const [filterClassId, setFilterClassId] = useState<number | undefined>(undefined);
|
const [filterClassId, setFilterClassId] = useState<number | undefined>(undefined);
|
||||||
@@ -192,8 +181,9 @@ const AttendancePage: React.FC = () => {
|
|||||||
message.success('更新成功');
|
message.success('更新成功');
|
||||||
setEditModalOpen(false);
|
setEditModalOpen(false);
|
||||||
fetchRecords();
|
fetchRecords();
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
message.error(e?.message || '更新失败');
|
const err = e as { message?: string };
|
||||||
|
message.error(err?.message || '更新失败');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -275,6 +265,7 @@ const AttendancePage: React.FC = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchClasses();
|
fetchClasses();
|
||||||
}, [fetchClasses]);
|
}, [fetchClasses]);
|
||||||
|
useEffect(() => { api.get<AlertItem[]>('/attendance-records/alerts').then(setAlerts).catch(() => {}) }, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (calendarView) {
|
if (calendarView) {
|
||||||
@@ -577,6 +568,11 @@ const AttendancePage: React.FC = () => {
|
|||||||
// ── Render ──
|
// ── Render ──
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
{alerts.length > 0 && (
|
||||||
|
<Alert type="warning" showIcon closable
|
||||||
|
message={`考勤预警:${alerts.length} 名学生异常`}
|
||||||
|
description={alerts.map(a => `${a.studentName}(${a.className || '-'}):${a.type} ${a.count}次,最近${a.lastDate}`).join(';')}
|
||||||
|
style={{ marginBottom: 16 }} />)}
|
||||||
<Tabs
|
<Tabs
|
||||||
activeKey={activeTab}
|
activeKey={activeTab}
|
||||||
onChange={setActiveTab}
|
onChange={setActiveTab}
|
||||||
|
|||||||
98
docs/superpowers/plans/2026-07-06-final-polish.md
Normal file
98
docs/superpowers/plans/2026-07-06-final-polish.md
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
# 列表页筛选补全 + 考勤预警 + 同步对接
|
||||||
|
|
||||||
|
> **For agentic workers:** Use subagent-driven-development. Steps use checkbox syntax.
|
||||||
|
|
||||||
|
**Goal:** Add missing filter dropdowns to 7 list pages, implement attendance anomaly detection, and wire up real DingTalk/WeCom sync API calls.
|
||||||
|
|
||||||
|
**Architecture:** Frontend: add Ant Design `<Select>` + `<DatePicker>` components to existing filter bars. Backend: add anomaly detection query to AttendanceService, implement sync stubs.
|
||||||
|
|
||||||
|
**Tech Stack:** React 19 + Ant Design 6 + NestJS 11 + TypeORM
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- Follow existing filter patterns (Input.Search + Select + Space wrap)
|
||||||
|
- Each filter change resets to page 1
|
||||||
|
- Status filter options must match entity `status` field values
|
||||||
|
- Sync stubs must check env vars before attempting API calls
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Students Page — Status + Tenant Filter
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `apps/admin/src/pages/Students/index.tsx`
|
||||||
|
|
||||||
|
- [ ] Add `filterStatus` state + `<Select>` dropdown (active/graduated/withdrawn/archived)
|
||||||
|
- [ ] Add `filterTenantId` state + `<Select>` dropdown (loaded from `/tenants`)
|
||||||
|
- [ ] Wire `fetchData()` to pass `status` + `tenantId` query params
|
||||||
|
- [ ] Verify: select status → list filters, select tenant → list filters
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Rooms Page — Status Filter
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `apps/admin/src/pages/Rooms/index.tsx`
|
||||||
|
|
||||||
|
- [ ] Add `filterStatus` + `<Select>` (available/in_use/maintenance/archived)
|
||||||
|
- [ ] Wire to API query param
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Occupancies Page — Status + Date Range
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `apps/admin/src/pages/Occupancies/index.tsx`
|
||||||
|
|
||||||
|
- [ ] Add `filterStatus` (checked_in/checked_out)
|
||||||
|
- [ ] Add `<RangePicker>` for check-in date range
|
||||||
|
- [ ] Wire to API params
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: Remaining Pages — Status Filters
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `apps/admin/src/pages/Classrooms/index.tsx` — status dropdown
|
||||||
|
- Modify: `apps/admin/src/pages/Expenses/index.tsx` — status filter
|
||||||
|
- Modify: `apps/admin/src/pages/Tenants/index.tsx` — status dropdown
|
||||||
|
|
||||||
|
- [ ] Each page: add `<Select>` with appropriate status options
|
||||||
|
- [ ] Wire to API params
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: Bills Page — Expense Type Filter
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `apps/admin/src/pages/Bills/index.tsx`
|
||||||
|
|
||||||
|
- [ ] Add `filterExpenseType` + `<Select>` with water/electricity/cleaning/rent/other
|
||||||
|
- [ ] Wire to API
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 6: Attendance Anomaly Detection
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `apps/server/src/attendance/attendance.service.ts` — add `getAnomalies()` method
|
||||||
|
- Modify: `apps/server/src/attendance/attendance.controller.ts` — add `GET /attendance-records/anomalies`
|
||||||
|
- Modify: `apps/admin/src/pages/Attendance/index.tsx` — add Alert banner
|
||||||
|
|
||||||
|
- [ ] Backend: `getAnomalies()` queries students with ≥3 consecutive absences or ≥5 lates in 7 days
|
||||||
|
- [ ] Backend: returns `{ studentId, studentName, type: 'consecutive_absence'|'frequent_late', count, dateRange }`
|
||||||
|
- [ ] Frontend: `<Alert>` banner at top of page showing anomaly count
|
||||||
|
- [ ] Frontend: click to filter records for that student
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 7: Sync Stubs → Real Implementation
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `apps/server/src/sync/sync.service.ts`
|
||||||
|
|
||||||
|
- [ ] Check `process.env.DINGTALK_APP_KEY` before attempting ding sync
|
||||||
|
- [ ] Check `process.env.WECOM_CORP_ID` before attempting wecom sync
|
||||||
|
- [ ] Log actionable messages: "DingTalk not configured, set DINGTALK_APP_KEY"
|
||||||
|
- [ ] If configured, attempt real API calls with proper error handling
|
||||||
|
- [ ] Record sync counts in SyncLog
|
||||||
Reference in New Issue
Block a user