ATTV1 实现考勤统一入口与企微同步
这个提交包含在:
+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({}),
|
||||
});
|
||||
}
|
||||
|
||||
在新工单中引用
屏蔽一个用户