R53 A1-A6:ERP 集成运维可视化与业务同步状态接入
- ErpIntegrationPage Tab 化:主数据同步 / Outbox 队列管理 / ID 映射查看器 - Outbox 分页查询、详情、重试与统计端点 - IntegrationBinding 分页查询、routePath 推导与采购/资产深链 - 报销/采购/资产详情区接入 ERP 同步状态卡片 - 单测覆盖 Outbox 与 Binding 端点
这个提交包含在:
@@ -66,6 +66,99 @@ export type ReimbursementJournalPreview = {
|
||||
};
|
||||
};
|
||||
|
||||
export type IntegrationOutboxStatus =
|
||||
| "pending"
|
||||
| "processing"
|
||||
| "succeeded"
|
||||
| "failed"
|
||||
| "dead_letter";
|
||||
|
||||
export type IntegrationOutboxPayloadSummary = {
|
||||
entityType: string | null;
|
||||
entityId: string | null;
|
||||
sourceEventName: string | null;
|
||||
journalType: string | null;
|
||||
exportType: string | null;
|
||||
identityCode: string | null;
|
||||
};
|
||||
|
||||
export type IntegrationOutboxItem = {
|
||||
id: string;
|
||||
eventType: string;
|
||||
status: IntegrationOutboxStatus | string;
|
||||
payloadSummary: IntegrationOutboxPayloadSummary;
|
||||
attempts: number;
|
||||
maxAttempts: number;
|
||||
nextRetryAt: string | null;
|
||||
createdAt: string | null;
|
||||
processedAt: string | null;
|
||||
deadLetterAt: string | null;
|
||||
};
|
||||
|
||||
export type IntegrationOutboxDetail = IntegrationOutboxItem & {
|
||||
payload: Record<string, unknown> | null;
|
||||
lastError: string | null;
|
||||
};
|
||||
|
||||
export type IntegrationOutboxListResponse = {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
items: IntegrationOutboxItem[];
|
||||
};
|
||||
|
||||
export type IntegrationOutboxStats = {
|
||||
pending: number;
|
||||
processing: number;
|
||||
succeeded: number;
|
||||
failed: number;
|
||||
deadLetter: number;
|
||||
};
|
||||
|
||||
export type IntegrationOutboxQuery = {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
status?: IntegrationOutboxStatus;
|
||||
eventType?: string;
|
||||
};
|
||||
|
||||
export type IntegrationBindingItem = {
|
||||
id: string;
|
||||
provider: string;
|
||||
entityType: string;
|
||||
localId: string;
|
||||
externalId: string | null;
|
||||
externalCode: string | null;
|
||||
lastSyncedAt: string | null;
|
||||
routePath: string | null;
|
||||
};
|
||||
|
||||
export type IntegrationBindingListResponse = {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
items: IntegrationBindingItem[];
|
||||
};
|
||||
|
||||
export type IntegrationBindingQuery = {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
provider?: string;
|
||||
entityType?: string;
|
||||
};
|
||||
|
||||
function toQueryString(query: Record<string, string | number | boolean | undefined>) {
|
||||
const params = new URLSearchParams();
|
||||
for (const [key, value] of Object.entries(query)) {
|
||||
if (value == null || value === "") {
|
||||
continue;
|
||||
}
|
||||
params.set(key, typeof value === "boolean" ? (value ? "1" : "0") : String(value));
|
||||
}
|
||||
const encoded = params.toString();
|
||||
return encoded ? `?${encoded}` : "";
|
||||
}
|
||||
|
||||
export async function triggerErpSync(reimbursementId: string, runNow = true) {
|
||||
return apiRequestData<{
|
||||
outboxId: string;
|
||||
@@ -107,6 +200,45 @@ export async function exportReimbursementJournal(reimbursementId: string, runNow
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchIntegrationOutbox(query: IntegrationOutboxQuery = {}) {
|
||||
return apiRequestData<IntegrationOutboxListResponse>(
|
||||
`/api/integrations/outbox${toQueryString({
|
||||
page: query.page,
|
||||
pageSize: query.pageSize,
|
||||
status: query.status,
|
||||
event_type: query.eventType,
|
||||
})}`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchIntegrationOutboxDetail(outboxId: string) {
|
||||
const response = await apiRequestData<{ item: IntegrationOutboxDetail }>(`/api/integrations/outbox/${outboxId}`);
|
||||
return response.item;
|
||||
}
|
||||
|
||||
export async function retryIntegrationOutbox(outboxId: string) {
|
||||
const response = await apiRequestData<{ item: IntegrationOutboxDetail }>(`/api/integrations/outbox/${outboxId}/retry`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
return response.item;
|
||||
}
|
||||
|
||||
export async function fetchIntegrationOutboxStats() {
|
||||
return apiRequestData<IntegrationOutboxStats>("/api/integrations/outbox/stats");
|
||||
}
|
||||
|
||||
export async function fetchIntegrationBindings(query: IntegrationBindingQuery = {}) {
|
||||
return apiRequestData<IntegrationBindingListResponse>(
|
||||
`/api/integrations/bindings${toQueryString({
|
||||
page: query.page,
|
||||
pageSize: query.pageSize,
|
||||
provider: query.provider,
|
||||
entity_type: query.entityType,
|
||||
})}`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function importTravelOrders(payload: {
|
||||
orders: Array<{
|
||||
source: string;
|
||||
|
||||
在新工单中引用
屏蔽一个用户