文件
hyb-oa/client/src/api/reimbursements.ts
T

530 行
14 KiB
TypeScript

import { apiRequestBlob, apiRequestData } from "./http";
import type { ExpenseStandardCheck, PrecheckResponse, RiskFlag } from "./feeControl";
export type ReimbursementStatus =
| "draft"
| "reviewing"
| "approved"
| "paid"
| "rejected"
| "cancelled";
export type ReimbursementItem = {
id: string;
expenseTypeId: string;
expenseDate: string | null;
amount: string;
description: string | null;
expenseType: {
id: string;
code: string;
name: string;
category: string;
enabled: boolean;
} | null;
};
export type LinkedTravelRequestSummary = {
id: string;
requestNo: string | null;
purpose: string | null;
startDate: string | null;
endDate: string | null;
estimatedAmount: string;
};
export type ReimbursementListItem = {
id: string;
formNumber: string | null;
status: ReimbursementStatus;
reimbursementDate: string | null;
totalAmount: string | null;
description: string | null;
payment: {
paymentVoucherNo: string | null;
paymentDate: string | null;
paymentChannel: string | null;
paidAt: string | null;
paidBy: string | null;
};
owner: {
id: string;
employeeNo: string;
name: string;
} | null;
submitter: {
id: string;
employeeNo: string;
name: string;
} | null;
project: {
id: string;
projectNo: string;
name: string;
} | null;
pendingWorkflow: {
instanceId: string;
stepId: string;
allowedActions: Array<"approve" | "reject" | "return" | "transfer">;
task: {
id: string;
stepName: string;
assigneeId: string | null;
assigneeRole: string | null;
} | null;
} | null;
createdAt: string | null;
updatedAt: string | null;
};
export type ReimbursementListResponse = {
page: number;
pageSize: number;
total: number;
scope: "own" | "all";
items: ReimbursementListItem[];
};
export type ReimbursementBudgetHint = {
budgetId: string;
budgetCode: string;
budgetName: string;
controlMode: "warn" | "block";
warningThresholdPercent: number;
allocatedAmount: string;
usedAmount: string;
remainingAmount: string;
currentAmount: string;
nextUsedAmount: string;
nextUsagePercent: number;
exceedsThreshold: boolean;
exceedsBudget: boolean;
};
export type ReimbursementWorkflowProgress = {
currentStepIndex: number;
totalSteps: number;
steps: Array<{
id: string;
name: string;
status: "completed" | "current" | "pending";
}>;
};
export type ReimbursementApprovalHistoryItem = {
id: string;
stepName: string | null;
action: string;
actorName: string;
comment: string | null;
createdAt: string | null;
};
export type ReimbursementDetail = {
id: string;
formNumber: string | null;
submitterId: string;
ownerId: string;
departmentId: string;
department: {
id: string;
name: string;
} | null;
status: ReimbursementStatus;
createdAt: string | null;
submittedAt: string | null;
reimbursementDate: string | null;
totalAmount: string | null;
description: string | null;
payment: {
paymentVoucherNo: string | null;
paymentDate: string | null;
paymentChannel: string | null;
paidAt: string | null;
paidBy: {
id: string;
employeeNo: string;
name: string;
} | null;
};
projectId: string | null;
linkedTravelRequestId: string | null;
linkedTravelRequest: LinkedTravelRequestSummary | null;
project: {
id: string;
projectNo: string;
name: string;
} | null;
owner: {
id: string;
employee_no: string;
name: string;
} | null;
submitter: {
id: string;
employee_no: string;
name: string;
} | null;
invoices: Array<{
id: string;
status: string;
invoiceCode: string | null;
invoiceNumber: string | null;
invoiceDate: string | null;
sellerName: string | null;
totalAmount: string | null;
originalName: string | null;
linkedItemId: string | null;
}>;
attachments: Array<{
id: string;
originalName: string;
mimeType: string;
fileSize: number;
fileSha256: string;
uploadedBy: {
id: string;
name: string;
employeeNo: string;
} | null;
createdAt: string | null;
}>;
budgetHint: ReimbursementBudgetHint | null;
pendingWorkflow: {
instanceId: string;
stepId: string;
task: {
id: string;
step_name: string;
assignee_id: string | null;
assignee_role: string | null;
} | null;
} | null;
workflowProgress: ReimbursementWorkflowProgress | null;
approvalHistory: ReimbursementApprovalHistoryItem[];
riskFlags: RiskFlag[];
items: ReimbursementItem[];
};
export type ReimbursementListQuery = {
page?: number;
pageSize?: number;
status?: ReimbursementStatus;
scope?: "own" | "all";
};
export type ReimbursementStatsQuery = {
dateFrom?: string;
dateTo?: string;
departmentId?: string;
};
export type ReimbursementStatsGranularity = "month" | "week";
export type ReimbursementStatsSummary = {
totalCount: number;
totalAmount: string;
byStatus: Record<
ReimbursementStatus,
{
count: number;
totalAmount: string;
}
>;
avgProcessingDays: number | null;
};
export type ReimbursementStatsTrendItem = {
period: string | null;
count: number;
amount: string;
paidCount: number;
paidAmount: string;
};
export type ReimbursementTopExpenseTypeItem = {
expenseTypeId: string;
expenseTypeName: string;
count: number;
totalAmount: string;
};
export type ReimbursementApprovalBottleneckItem = {
assigneeId: string | null;
assigneeName: string;
pendingCount: number;
oldestPendingDays: number;
};
export type ReimbursementPayPayload = {
paymentVoucherNo?: string;
paymentDate?: string;
paymentChannel?: "bank" | "alipay" | "wechat" | "cash" | "other";
paymentNote?: string;
};
export type ReimbursementCreatePayload = {
ownerEmployeeId?: string;
reimbursementDate?: string;
description?: string;
templateId?: string;
linkedTravelRequestId?: string;
};
export type ReimbursementCreateResponse = {
id: string;
formNumber: string | null;
submitterId: string;
ownerId: string;
reimbursementDate: string | null;
status: ReimbursementStatus;
description: string | null;
projectId: string | null;
project: {
id: string;
projectNo: string;
name: string;
} | null;
totalAmount: string;
linkedTravelRequestId: string | null;
linkedTravelRequest: LinkedTravelRequestSummary | null;
items: ReimbursementItem[];
};
export type LinkableTravelRequestSummary = {
id: string;
requestNo: string | null;
purpose: string;
startDate: string | null;
endDate: string | null;
estimatedAmount: string;
};
export type LinkableTravelRequestListResponse = {
ownerEmployeeId: string;
items: LinkableTravelRequestSummary[];
};
export type ReimbursementItemPayload = {
expenseTypeId: string;
expenseDate: string;
amount: string;
description?: string;
};
export type ReimbursementItemMutationResponse = {
item: {
id: string;
expenseTypeId: string;
expenseDate: string;
amount: string;
description: string | null;
};
totalAmount: string;
};
export type ReimbursementItemDeleteResponse = {
outcome: "deleted";
totalAmount: string;
};
export type ReimbursementDeleteResponse = {
reimbursementId: string;
outcome: "deleted";
unboundInvoiceCount: number;
};
export type ReimbursementSubmitResponse = {
reimbursementId: string;
formNumber: string;
workflowInstanceId: string;
currentTaskId: string;
status: "reviewing";
budget: {
budgetId: string;
budgetCode: string;
controlMode: "warn" | "block" | "manual_review";
usagePercent: number;
exceededThreshold: boolean;
exceededBudget: boolean;
} | null;
precheckDecision: "pass" | "warn" | "manual_review";
standardWarnings: ExpenseStandardCheck[];
openedRiskFlagCount: 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 fetchReimbursements(query: ReimbursementListQuery = {}) {
return apiRequestData<ReimbursementListResponse>(
`/api/reimbursements${toQueryString(query as Record<string, string | number | undefined>)}`,
);
}
export async function fetchReimbursementStatsSummary(query: ReimbursementStatsQuery = {}) {
return apiRequestData<ReimbursementStatsSummary>(
`/api/reimbursements/stats/summary${toQueryString(query as Record<string, string | number | undefined>)}`,
);
}
export async function fetchReimbursementStatsTrend(
query: ReimbursementStatsQuery & { granularity?: ReimbursementStatsGranularity } = {},
) {
return apiRequestData<{ items: ReimbursementStatsTrendItem[] }>(
`/api/reimbursements/stats/trend${toQueryString(query as Record<string, string | number | undefined>)}`,
);
}
export async function fetchReimbursementTopExpenseTypes(
query: ReimbursementStatsQuery & { limit?: number } = {},
) {
return apiRequestData<{ items: ReimbursementTopExpenseTypeItem[] }>(
`/api/reimbursements/stats/top-expense-types${toQueryString(query as Record<string, string | number | undefined>)}`,
);
}
export async function fetchReimbursementApprovalBottleneck(query: ReimbursementStatsQuery = {}) {
return apiRequestData<{ items: ReimbursementApprovalBottleneckItem[] }>(
`/api/reimbursements/stats/approval-bottleneck${toQueryString(query as Record<string, string | number | undefined>)}`,
);
}
export async function fetchReimbursementDetail(reimbursementId: string) {
return apiRequestData<ReimbursementDetail>(`/api/reimbursements/${reimbursementId}`);
}
export async function createReimbursement(payload: ReimbursementCreatePayload = {}) {
return apiRequestData<ReimbursementCreateResponse>(`/api/reimbursements`, {
method: "POST",
body: JSON.stringify(payload),
});
}
export async function fetchLinkableTravelRequests(query: { ownerEmployeeId?: string } = {}) {
return apiRequestData<LinkableTravelRequestListResponse>(
`/api/reimbursements/linkable-travel-requests${toQueryString(query as Record<string, string | number | undefined>)}`,
);
}
export async function precheckReimbursement(reimbursementId: string) {
return apiRequestData<PrecheckResponse>(`/api/reimbursements/${reimbursementId}/precheck`, {
method: "POST",
body: JSON.stringify({}),
});
}
export async function submitReimbursement(reimbursementId: string) {
return apiRequestData<ReimbursementSubmitResponse>(`/api/reimbursements/${reimbursementId}/submit`, {
method: "POST",
body: JSON.stringify({}),
});
}
export async function withdrawReimbursement(reimbursementId: string, comment?: string) {
return apiRequestData<Record<string, unknown>>(`/api/reimbursements/${reimbursementId}/withdraw`, {
method: "POST",
body: JSON.stringify({ comment }),
});
}
export async function payReimbursement(reimbursementId: string, payload: ReimbursementPayPayload = {}) {
return apiRequestData<Record<string, unknown>>(`/api/reimbursements/${reimbursementId}/pay`, {
method: "POST",
body: JSON.stringify(payload),
});
}
export async function deleteReimbursement(reimbursementId: string) {
return apiRequestData<ReimbursementDeleteResponse>(`/api/reimbursements/${reimbursementId}`, {
method: "DELETE",
});
}
export async function createReimbursementItem(
reimbursementId: string,
payload: ReimbursementItemPayload,
) {
return apiRequestData<ReimbursementItemMutationResponse>(
`/api/reimbursements/${reimbursementId}/items`,
{
method: "POST",
body: JSON.stringify(payload),
},
);
}
export async function updateReimbursementItem(
reimbursementId: string,
itemId: string,
payload: Partial<ReimbursementItemPayload>,
) {
return apiRequestData<ReimbursementItemMutationResponse>(
`/api/reimbursements/${reimbursementId}/items/${itemId}`,
{
method: "PATCH",
body: JSON.stringify(payload),
},
);
}
export async function deleteReimbursementItem(reimbursementId: string, itemId: string) {
return apiRequestData<ReimbursementItemDeleteResponse>(
`/api/reimbursements/${reimbursementId}/items/${itemId}`,
{
method: "DELETE",
},
);
}
export async function uploadReimbursementAttachments(reimbursementId: string, files: File[]) {
const formData = new FormData();
for (const file of files) {
formData.append("files", file);
}
return apiRequestData<{ items: ReimbursementDetail["attachments"] }>(
`/api/reimbursements/${reimbursementId}/attachments`,
{
method: "POST",
body: formData,
},
);
}
export async function deleteReimbursementAttachment(reimbursementId: string, attachmentId: string) {
return apiRequestData<{ attachmentId: string; outcome: string }>(
`/api/reimbursements/${reimbursementId}/attachments/${attachmentId}`,
{
method: "DELETE",
},
);
}
export function reimbursementAttachmentFileUrl(reimbursementId: string, attachmentId: string) {
return `/api/reimbursements/${reimbursementId}/attachments/${attachmentId}/file`;
}
export async function downloadReimbursementAttachment(reimbursementId: string, attachmentId: string) {
return apiRequestBlob(reimbursementAttachmentFileUrl(reimbursementId, attachmentId));
}
export async function linkReimbursementProject(reimbursementId: string, projectId: string | null) {
return apiRequestData<{ projectId: string | null; project: { id: string; projectNo: string; name: string } | null }>(`/api/reimbursements/${reimbursementId}/link-project`, {
method: "POST",
body: JSON.stringify({ projectId }),
});
}