feat(client): 完成 P3 业务页联动闭环(D6/D8/D10/D11)

- 待办:列表/详情 + approve/reject/return/transfer(D6)
- 报销:列表/详情 + create/submit/withdraw/pay(D8)
- 票据:列表/上传/OCR轮询/绑定/预览(D10)
- 统一:Api types + 错误映射 + react-query缓存失效(D11)
这个提交包含在:
2026-02-08 23:08:41 +08:00
父节点 65e7b37ebd
当前提交 7a6f5573a6
共修改 16 个文件,包含 1762 行新增和 31 行删除
+130
查看文件
@@ -0,0 +1,130 @@
import { apiRequestData } from "./http";
export type InvoicePoolStatus = "pending_ocr" | "ocr_failed" | "available" | "bound" | "archived";
export type InvoicePoolListItem = {
id: string;
status: InvoicePoolStatus;
ocrType: string;
ocrProvider: string;
ocrAttempts: number;
ocrLastError: string | null;
ocrCompletedAt: string | null;
invoiceCode: string | null;
invoiceNumber: string | null;
invoiceDate: string | null;
amount: string | null;
taxAmount: string | null;
totalAmount: string | null;
linkedReimbursementId: string | null;
linkedItemId: string | null;
originalName: string;
mimeType: string;
fileSize: number;
owner: {
id: string;
employeeNo: string;
name: string;
} | null;
uploadedBy: {
id: string;
employeeNo: string;
name: string;
} | null;
createdAt: string | null;
updatedAt: string | null;
};
export type InvoicePoolListResponse = {
page: number;
pageSize: number;
total: number;
ownerScope: "own" | "all";
items: InvoicePoolListItem[];
};
export type InvoicePoolDetail = {
id: string;
status: InvoicePoolStatus;
ownerEmployeeId: string;
uploadedByEmployeeId: string;
ocrType: string;
ocrProvider: string;
ocrAttempts: number;
ocrLastError: string | null;
ocrCompletedAt: string | null;
invoiceCode: string | null;
invoiceNumber: string | null;
invoiceDate: string | null;
amount: string | null;
taxAmount: string | null;
totalAmount: string | null;
linkedReimbursementId: string | null;
linkedItemId: string | null;
createdAt: string | null;
updatedAt: string | null;
};
export type InvoicePoolListQuery = {
page?: number;
pageSize?: number;
status?: InvoicePoolStatus;
ownerScope?: "own" | "all";
};
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 fetchInvoicePoolList(query: InvoicePoolListQuery = {}) {
return apiRequestData<InvoicePoolListResponse>(
`/api/invoice-pool${toQueryString(query as Record<string, string | number | undefined>)}`,
);
}
export async function fetchInvoicePoolDetail(invoiceId: string) {
return apiRequestData<InvoicePoolDetail>(`/api/invoice-pool/${invoiceId}`);
}
export async function uploadInvoice(file: File, ocrType?: string) {
const formData = new FormData();
formData.append("file", file);
if (ocrType) {
formData.append("ocr_type", ocrType);
}
return apiRequestData<{ invoicePoolId: string; status: string; fileSha256: string }>(
"/api/invoice-pool/upload",
{
method: "POST",
body: formData,
},
);
}
export async function bindInvoice(invoiceId: string, reimbursementId: string, linkedItemId?: string) {
return apiRequestData<Record<string, unknown>>(`/api/invoice-pool/${invoiceId}/bind`, {
method: "POST",
body: JSON.stringify({ reimbursementId, linkedItemId }),
});
}
export async function fetchInvoiceSignedUrl(invoiceId: string) {
return apiRequestData<{ signedUrl: string; expiresAt: string }>(
`/api/invoice-pool/${invoiceId}/signed-url`,
{
method: "POST",
body: JSON.stringify({}),
},
);
}