feat(client): 完成 P3 业务页联动闭环(D6/D8/D10/D11)
- 待办:列表/详情 + approve/reject/return/transfer(D6) - 报销:列表/详情 + create/submit/withdraw/pay(D8) - 票据:列表/上传/OCR轮询/绑定/预览(D10) - 统一:Api types + 错误映射 + react-query缓存失效(D11)
这个提交包含在:
@@ -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({}),
|
||||
},
|
||||
);
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户