R59 A1-A6:极简考勤打卡
- 新增 AttendanceRecord 与 AttendancePolicy 模型 - 上下班打卡、今日状态、月度统计端点 - 考勤管理页面(我的/团队/设置三 Tab) - 移动端快速打卡入口 - 管理员补卡与审计日志 - 单测覆盖打卡幂等、迟到计算、工时计算、权限边界
这个提交包含在:
@@ -0,0 +1,168 @@
|
||||
import { apiRequestData } from "./http";
|
||||
|
||||
export type AttendanceExemptionReason =
|
||||
| "leave"
|
||||
| "travel"
|
||||
| "holiday"
|
||||
| "weekend"
|
||||
| "policy_disabled";
|
||||
|
||||
export type AttendanceStatus =
|
||||
| "normal"
|
||||
| "late"
|
||||
| "early_leave"
|
||||
| "late_early_leave"
|
||||
| "pending_clock_out"
|
||||
| "missed"
|
||||
| "leave"
|
||||
| "travel"
|
||||
| "holiday"
|
||||
| "weekend";
|
||||
|
||||
export type AttendanceRecordItem = {
|
||||
id: string | null;
|
||||
employeeId: string;
|
||||
workDate: string;
|
||||
clockInAt: string | null;
|
||||
clockOutAt: string | null;
|
||||
workMinutes: number | null;
|
||||
isLate: boolean;
|
||||
lateMinutes: number;
|
||||
isEarlyLeave: boolean;
|
||||
earlyMinutes: number;
|
||||
note: string | null;
|
||||
source: "web" | "mobile" | "admin" | null;
|
||||
updatedAt: string | null;
|
||||
};
|
||||
|
||||
export type AttendanceTodayResponse = {
|
||||
workDate: string;
|
||||
needClockIn: boolean;
|
||||
exemptionReason: AttendanceExemptionReason | null;
|
||||
item: AttendanceRecordItem | null;
|
||||
};
|
||||
|
||||
export type AttendanceMonthItem = AttendanceRecordItem & {
|
||||
attendanceStatus: AttendanceStatus;
|
||||
needClockIn: boolean;
|
||||
exemptionReason: AttendanceExemptionReason | null;
|
||||
};
|
||||
|
||||
export type AttendanceMonthSummary = {
|
||||
totalWorkDays: number;
|
||||
clockedDays: number;
|
||||
lateDays: number;
|
||||
earlyLeaveDays: number;
|
||||
missedDays: number;
|
||||
totalWorkMinutes: number;
|
||||
};
|
||||
|
||||
export type AttendanceMonthResponse = {
|
||||
year: number;
|
||||
month: number;
|
||||
summary: AttendanceMonthSummary;
|
||||
items: AttendanceMonthItem[];
|
||||
};
|
||||
|
||||
export type AttendanceTeamItem = {
|
||||
employeeId: string;
|
||||
employeeName: string;
|
||||
employeeNo: string;
|
||||
title: string;
|
||||
departmentId: string | null;
|
||||
departmentName: string | null;
|
||||
attendanceStatus: AttendanceStatus;
|
||||
needClockIn: boolean;
|
||||
exemptionReason: AttendanceExemptionReason | null;
|
||||
clockInAt: string | null;
|
||||
clockOutAt: string | null;
|
||||
workMinutes: number | null;
|
||||
note: string | null;
|
||||
source: "web" | "mobile" | "admin" | null;
|
||||
};
|
||||
|
||||
export type AttendancePolicy = {
|
||||
id: string | null;
|
||||
workStartTime: string;
|
||||
workEndTime: string;
|
||||
lunchStartTime: string;
|
||||
lunchEndTime: string;
|
||||
lateToleranceMinutes: number;
|
||||
enabled: boolean;
|
||||
createdAt: string | null;
|
||||
updatedAt: string | null;
|
||||
updatedBy: string | null;
|
||||
};
|
||||
|
||||
export async function clockInAttendance(note?: string | null) {
|
||||
return apiRequestData<{ item: AttendanceRecordItem }>("/api/attendance/clock-in", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(note ? { note } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
export async function clockOutAttendance(note?: string | null) {
|
||||
return apiRequestData<{ item: AttendanceRecordItem }>("/api/attendance/clock-out", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(note ? { note } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchMyTodayAttendance() {
|
||||
return apiRequestData<AttendanceTodayResponse>("/api/attendance/me/today");
|
||||
}
|
||||
|
||||
export async function fetchMyMonthAttendance(params?: { year?: number; month?: number }) {
|
||||
const search = new URLSearchParams();
|
||||
if (params?.year) {
|
||||
search.set("year", String(params.year));
|
||||
}
|
||||
if (params?.month) {
|
||||
search.set("month", String(params.month));
|
||||
}
|
||||
const query = search.toString();
|
||||
return apiRequestData<AttendanceMonthResponse>(`/api/attendance/me/month${query ? `?${query}` : ""}`);
|
||||
}
|
||||
|
||||
export async function fetchTeamAttendance(params?: { date?: string; departmentId?: string }) {
|
||||
const search = new URLSearchParams();
|
||||
if (params?.date) {
|
||||
search.set("date", params.date);
|
||||
}
|
||||
if (params?.departmentId) {
|
||||
search.set("departmentId", params.departmentId);
|
||||
}
|
||||
const query = search.toString();
|
||||
return apiRequestData<{ date: string; items: AttendanceTeamItem[] }>(`/api/attendance/team${query ? `?${query}` : ""}`);
|
||||
}
|
||||
|
||||
export async function fetchAttendancePolicy() {
|
||||
return apiRequestData<{ item: AttendancePolicy }>("/api/attendance/policy");
|
||||
}
|
||||
|
||||
export async function updateAttendancePolicy(payload: {
|
||||
workStartTime: string;
|
||||
workEndTime: string;
|
||||
lunchStartTime: string;
|
||||
lunchEndTime: string;
|
||||
lateToleranceMinutes: number;
|
||||
enabled?: boolean;
|
||||
}) {
|
||||
return apiRequestData<{ item: AttendancePolicy }>("/api/attendance/policy", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}
|
||||
|
||||
export async function adminBackfillAttendance(payload: {
|
||||
employeeId: string;
|
||||
workDate: string;
|
||||
clockInAt?: string | null;
|
||||
clockOutAt?: string | null;
|
||||
note?: string | null;
|
||||
}) {
|
||||
return apiRequestData<{ item: AttendanceRecordItem }>("/api/attendance/admin/backfill", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}
|
||||
@@ -12,6 +12,7 @@ const FEATURE_FLAG_DISABLED_MESSAGES: Record<string, string> = {
|
||||
portal_home_v1_disabled: "Portal 首页当前未启用,请联系管理员检查环境开关与功能开关配置。",
|
||||
announcements_v1_disabled: "公告模块当前未启用,请联系管理员检查环境开关与功能开关配置。",
|
||||
presence_board_v1_disabled: "团队状态模块当前未启用,请联系管理员检查环境开关与功能开关配置。",
|
||||
attendance_v1_disabled: "考勤模块当前未启用,请联系管理员检查环境开关与功能开关配置。",
|
||||
weekly_report_v1_disabled: "周报模块当前未启用,请联系管理员检查环境开关与功能开关配置。",
|
||||
employee_lifecycle_v1_disabled: "人员生命周期模块当前未启用,请联系管理员检查环境开关与功能开关配置。",
|
||||
todo_board_v1_disabled: "待办模块当前未启用,请联系管理员检查环境开关与功能开关配置。",
|
||||
|
||||
@@ -13,6 +13,7 @@ export type FeatureFlagKey =
|
||||
| "workflow_definition_admin_v1"
|
||||
| "cutover_console_v1"
|
||||
| "presence_board_v1"
|
||||
| "attendance_v1"
|
||||
| "weekly_report_v1"
|
||||
| "employee_lifecycle_v1"
|
||||
| "todo_board_v1"
|
||||
|
||||
在新工单中引用
屏蔽一个用户