feat(P15B-1,P15B-6): 完成请假V1闭环
这个提交包含在:
@@ -136,6 +136,42 @@ export function mapApiErrorMessage(
|
||||
return "当前报销金额超出预算,系统已阻断提交。";
|
||||
}
|
||||
|
||||
if (details?.leave === "conflict_range") {
|
||||
return "当前时间段与已有请假记录冲突,请调整后再提交。";
|
||||
}
|
||||
|
||||
if (details?.leave === "insufficient_balance") {
|
||||
return "当前假种可用额度不足,无法提交。";
|
||||
}
|
||||
|
||||
if (details?.leave === "invalid_range") {
|
||||
return "请假开始/结束日期与半天时段不合法。";
|
||||
}
|
||||
|
||||
if (details?.leave === "missing_reserve_ledger") {
|
||||
return "请假冻结账本状态异常,请联系管理员处理。";
|
||||
}
|
||||
|
||||
if (details?.file === "required") {
|
||||
return "当前假种要求上传附件后才能提交。";
|
||||
}
|
||||
|
||||
if (details?.file === "unsupported_mime_type") {
|
||||
return "附件类型不被当前假种接受,请重新选择。";
|
||||
}
|
||||
|
||||
if (details?.withdraw === "no_pending_instance") {
|
||||
return "当前请假单不存在可撤回的审批实例。";
|
||||
}
|
||||
|
||||
if (details?.withdraw === "already_processed_by_approver") {
|
||||
return "审批人已处理该请假单,当前不能撤回。";
|
||||
}
|
||||
|
||||
if (details?.leaveTypeId === "missing_active_policy") {
|
||||
return "当前假种未配置可用策略,请联系 HR。";
|
||||
}
|
||||
|
||||
if (details?.integration === "erp_sync_disabled") {
|
||||
return "ERP 同步功能未开启。";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
import { apiRequestData } from "./http";
|
||||
|
||||
export type LeaveSession = "am" | "pm";
|
||||
export type LeaveRequestStatus = "draft" | "reviewing" | "approved" | "rejected" | "cancelled";
|
||||
|
||||
export type LeaveType = {
|
||||
id: string;
|
||||
code: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
enabled: boolean;
|
||||
requiresAttachment: boolean;
|
||||
attachmentMimeTypes: string[];
|
||||
sortOrder: number;
|
||||
policy: {
|
||||
id: string;
|
||||
annualQuotaDays: number;
|
||||
enabled: boolean;
|
||||
description: string | null;
|
||||
} | null;
|
||||
createdAt: string | null;
|
||||
updatedAt: string | null;
|
||||
};
|
||||
|
||||
export type LeaveBalanceItem = {
|
||||
leaveType: LeaveType;
|
||||
policy: {
|
||||
id: string;
|
||||
annualQuotaDays: number;
|
||||
enabled: boolean;
|
||||
description: string | null;
|
||||
};
|
||||
granted: number;
|
||||
adjusted: number;
|
||||
reserved: number;
|
||||
used: number;
|
||||
available: number;
|
||||
periodStart: string | null;
|
||||
periodEnd: string | null;
|
||||
};
|
||||
|
||||
export type LeaveRequestItem = {
|
||||
id: string;
|
||||
ownerId: string;
|
||||
departmentId: string;
|
||||
status: LeaveRequestStatus;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
startSession: LeaveSession;
|
||||
endSession: LeaveSession;
|
||||
requestedDays: number;
|
||||
reason: string | null;
|
||||
attachment: {
|
||||
originalName: string | null;
|
||||
mimeType: string | null;
|
||||
} | null;
|
||||
leaveType: {
|
||||
id: string;
|
||||
code: string;
|
||||
name: string;
|
||||
requiresAttachment: boolean;
|
||||
attachmentMimeTypes: string[];
|
||||
} | null;
|
||||
owner: {
|
||||
id: string;
|
||||
employeeNo: string;
|
||||
name: string;
|
||||
} | null;
|
||||
createdAt: string | null;
|
||||
updatedAt: string | null;
|
||||
};
|
||||
|
||||
export type LeaveRequestListResponse = {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
scope: "own" | "all";
|
||||
items: LeaveRequestItem[];
|
||||
};
|
||||
|
||||
export type LeaveBalanceResponse = {
|
||||
employeeId: string;
|
||||
items: LeaveBalanceItem[];
|
||||
};
|
||||
|
||||
export type LeaveListQuery = {
|
||||
status?: LeaveRequestStatus;
|
||||
scope?: "own" | "all";
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
};
|
||||
|
||||
function toQueryString(query: Record<string, string | number | undefined>) {
|
||||
const params = new URLSearchParams();
|
||||
for (const [key, value] of Object.entries(query)) {
|
||||
if (value == null || value === "") {
|
||||
continue;
|
||||
}
|
||||
params.set(key, String(value));
|
||||
}
|
||||
const encoded = params.toString();
|
||||
return encoded ? `?${encoded}` : "";
|
||||
}
|
||||
|
||||
export async function fetchLeaveTypes(params?: { enabled?: boolean }) {
|
||||
return apiRequestData<{ items: LeaveType[] }>(
|
||||
`/api/leave-types${toQueryString({ enabled: params?.enabled == null ? undefined : String(params.enabled) })}`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function createLeaveType(payload: {
|
||||
code: string;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
enabled?: boolean;
|
||||
requiresAttachment?: boolean;
|
||||
attachmentMimeTypes?: string[];
|
||||
sortOrder?: number;
|
||||
annualQuotaDays: number;
|
||||
policyDescription?: string | null;
|
||||
}) {
|
||||
return apiRequestData<LeaveType>("/api/leave-types", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}
|
||||
|
||||
export async function patchLeaveType(
|
||||
leaveTypeId: string,
|
||||
payload: Partial<{
|
||||
code: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
enabled: boolean;
|
||||
requiresAttachment: boolean;
|
||||
attachmentMimeTypes: string[];
|
||||
sortOrder: number;
|
||||
annualQuotaDays: number;
|
||||
policyDescription: string | null;
|
||||
}>,
|
||||
) {
|
||||
return apiRequestData<LeaveType>(`/api/leave-types/${leaveTypeId}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchLeaveBalances(employeeId?: string) {
|
||||
return apiRequestData<LeaveBalanceResponse>(
|
||||
`/api/leave-balances${toQueryString({ employeeId })}`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchLeaveRequests(query: LeaveListQuery = {}) {
|
||||
return apiRequestData<LeaveRequestListResponse>(
|
||||
`/api/leave-requests${toQueryString(query as Record<string, string | number | undefined>)}`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchLeaveRequestDetail(leaveRequestId: string) {
|
||||
return apiRequestData<LeaveRequestItem>(`/api/leave-requests/${leaveRequestId}`);
|
||||
}
|
||||
|
||||
export async function createLeaveRequest(payload: {
|
||||
leaveTypeId: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
startSession: LeaveSession;
|
||||
endSession: LeaveSession;
|
||||
reason?: string;
|
||||
file?: File | null;
|
||||
}) {
|
||||
const formData = new FormData();
|
||||
formData.append("leaveTypeId", payload.leaveTypeId);
|
||||
formData.append("startDate", payload.startDate);
|
||||
formData.append("endDate", payload.endDate);
|
||||
formData.append("startSession", payload.startSession);
|
||||
formData.append("endSession", payload.endSession);
|
||||
if (payload.reason?.trim()) {
|
||||
formData.append("reason", payload.reason.trim());
|
||||
}
|
||||
if (payload.file) {
|
||||
formData.append("file", payload.file);
|
||||
}
|
||||
|
||||
return apiRequestData<LeaveRequestItem>("/api/leave-requests", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
}
|
||||
|
||||
export async function submitLeaveRequest(leaveRequestId: string) {
|
||||
return apiRequestData<Record<string, unknown>>(`/api/leave-requests/${leaveRequestId}/submit`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
}
|
||||
|
||||
export async function withdrawLeaveRequest(leaveRequestId: string, comment?: string) {
|
||||
return apiRequestData<Record<string, unknown>>(`/api/leave-requests/${leaveRequestId}/withdraw`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ comment }),
|
||||
});
|
||||
}
|
||||
@@ -53,6 +53,7 @@ export type WorkflowTaskDetail = {
|
||||
};
|
||||
workflowInstance: {
|
||||
id: string;
|
||||
entityType: string;
|
||||
currentStatus: string;
|
||||
currentStepId: string;
|
||||
startedAt: string | null;
|
||||
@@ -76,6 +77,30 @@ export type WorkflowTaskDetail = {
|
||||
name: string;
|
||||
} | null;
|
||||
} | null;
|
||||
leaveRequest: {
|
||||
id: string;
|
||||
status: string;
|
||||
startDate: string | null;
|
||||
endDate: string | null;
|
||||
startSession: "am" | "pm";
|
||||
endSession: "am" | "pm";
|
||||
requestedDays: number;
|
||||
reason: string | null;
|
||||
attachment: {
|
||||
originalName: string | null;
|
||||
mimeType: string | null;
|
||||
} | null;
|
||||
leaveType: {
|
||||
id: string;
|
||||
code: string;
|
||||
name: string;
|
||||
} | null;
|
||||
owner: {
|
||||
id: string;
|
||||
employeeNo: string;
|
||||
name: string;
|
||||
} | null;
|
||||
} | null;
|
||||
allowedActions: Array<"approve" | "reject" | "return" | "transfer">;
|
||||
history: WorkflowHistoryItem[];
|
||||
};
|
||||
|
||||
在新工单中引用
屏蔽一个用户