ATTV1 实现考勤统一入口与企微同步

这个提交包含在:
2026-05-17 16:56:24 +08:00
父节点 d50b7db143
当前提交 916cc3b7f7
共修改 51 个文件,包含 3323 行新增和 1762 行删除
+166 -143
查看文件
@@ -1,168 +1,191 @@
import { apiRequestData } from "./http";
export type AttendanceExemptionReason =
| "leave"
| "travel"
| "holiday"
| "weekend"
| "policy_disabled";
export type AttendanceStatus =
export type AttendanceCorrectionKind = "missed_punch" | "out" | "overtime";
export type AttendanceCorrectionStatus = "reviewing" | "approved" | "rejected" | "cancelled";
export type AttendanceDailyStatus =
| "normal"
| "late"
| "early_leave"
| "late_early_leave"
| "pending_clock_out"
| "missed"
| "exception"
| "missing"
| "leave"
| "travel"
| "holiday"
| "corrected"
| "overtime"
| "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;
export type AttendanceEmployeeSummary = {
id: string;
employeeNo: string;
title: string;
name: string;
title?: string | null;
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;
export type WecomCheckinSummary = {
id: string;
checkinAt: string | null;
checkinDate: string | null;
checkinType: string;
result: string | null;
exceptionType: string | null;
sourceType: string | null;
};
export type AttendanceCorrectionItem = {
id: string;
kind: AttendanceCorrectionKind;
status: AttendanceCorrectionStatus;
workDate: string | null;
missedPunchAt: string | null;
startAt: string | null;
endAt: string | null;
reason: string;
locationSummary: string | null;
compensationPolicy: string | null;
owner: AttendanceEmployeeSummary | null;
createdAt: string | null;
updatedAt: string | null;
updatedBy: string | null;
workflow?: {
id: string;
status: string;
currentStepId: 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 type AttendanceMonthlyDay = {
date: string;
status: AttendanceDailyStatus;
explanation: string;
firstCheckinAt: string | null;
lastCheckinAt: string | null;
checkins: WecomCheckinSummary[];
corrections: AttendanceCorrectionItem[];
leaveRequests: Array<{
id: string;
leaveTypeName: string;
requestedDays: number;
reason: string | null;
}>;
};
export async function clockOutAttendance(note?: string | null) {
return apiRequestData<{ item: AttendanceRecordItem }>("/api/attendance/clock-out", {
method: "POST",
body: JSON.stringify(note ? { note } : {}),
});
}
export type AttendanceMonthlyEmployee = {
employee: AttendanceEmployeeSummary | null;
days: AttendanceMonthlyDay[];
};
export async function fetchMyTodayAttendance() {
return apiRequestData<AttendanceTodayResponse>("/api/attendance/me/today");
}
export type AttendanceMonthlyResponse = {
month: string;
summary: {
employeeCount: number;
workDays: number;
checkinDays: number;
missingDays: number;
approvedCorrectionCount: number;
};
employees: AttendanceMonthlyEmployee[];
};
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 type WecomCheckinSyncRun = {
id: string;
startDate: string | null;
endDate: string | null;
status: "running" | "success" | "skipped" | "failed" | string;
employeeCount: number;
recordCount: number;
errorCode: string | null;
errorMessage: string | null;
triggeredBy: AttendanceEmployeeSummary | null;
createdAt: string | null;
updatedAt: string | null;
};
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;
export type AttendanceCorrectionCreatePayload = {
kind: AttendanceCorrectionKind;
workDate: string;
clockInAt?: string | null;
clockOutAt?: string | null;
note?: string | null;
missedPunchAt?: string;
startAt?: string;
endAt?: string;
reason: string;
locationSummary?: string;
compensationPolicy?: string;
};
export async function fetchAttendanceMonthly(params?: { month?: string; employeeId?: string }) {
const search = new URLSearchParams();
if (params?.month) search.set("month", params.month);
if (params?.employeeId) search.set("employeeId", params.employeeId);
const query = search.toString();
return apiRequestData<AttendanceMonthlyResponse>(`/api/attendance/monthly${query ? `?${query}` : ""}`);
}
export async function triggerWecomAttendanceSync(payload: {
startDate: string;
endDate: string;
employeeIds?: string[];
}) {
return apiRequestData<{ item: AttendanceRecordItem }>("/api/attendance/admin/backfill", {
return apiRequestData<{
item: WecomCheckinSyncRun;
skipped: boolean;
code: string;
employeeCount: number;
boundUserCount: number;
recordCount: number;
}>("/api/attendance/wecom/sync", {
method: "POST",
body: JSON.stringify(payload),
});
}
export async function fetchWecomAttendanceSyncRuns(params?: { page?: number; pageSize?: number }) {
const search = new URLSearchParams();
if (params?.page) search.set("page", String(params.page));
if (params?.pageSize) search.set("pageSize", String(params.pageSize));
const query = search.toString();
return apiRequestData<{
page: number;
pageSize: number;
total: number;
items: WecomCheckinSyncRun[];
}>(`/api/attendance/wecom/sync-runs${query ? `?${query}` : ""}`);
}
export async function createAttendanceCorrection(payload: AttendanceCorrectionCreatePayload) {
return apiRequestData<{
item: AttendanceCorrectionItem;
workflowInstanceId: string;
currentTaskId: string;
}>("/api/attendance-corrections", {
method: "POST",
body: JSON.stringify(payload),
});
}
export async function fetchAttendanceCorrections(params?: {
month?: string;
employeeId?: string;
status?: AttendanceCorrectionStatus;
kind?: AttendanceCorrectionKind;
page?: number;
pageSize?: number;
}) {
const search = new URLSearchParams();
if (params?.month) search.set("month", params.month);
if (params?.employeeId) search.set("employeeId", params.employeeId);
if (params?.status) search.set("status", params.status);
if (params?.kind) search.set("kind", params.kind);
if (params?.page) search.set("page", String(params.page));
if (params?.pageSize) search.set("pageSize", String(params.pageSize));
const query = search.toString();
return apiRequestData<{
page: number;
pageSize: number;
total: number;
items: AttendanceCorrectionItem[];
}>(`/api/attendance-corrections${query ? `?${query}` : ""}`);
}
export async function cancelAttendanceCorrection(id: string) {
return apiRequestData<{ item: AttendanceCorrectionItem }>(`/api/attendance-corrections/${id}/cancel`, {
method: "POST",
body: JSON.stringify({}),
});
}