feat(P25-2/P25-3/P26-2/P28-1/P24-2/P29-3): 落地内部OA够用版前端入口与运营页面

关联 Issue: P25-2, P25-3, P25-4, P26-2, P26-4, P28-1, P28-2, P28-3, P28-4, P24-2, P29-3
这个提交包含在:
2026-03-21 18:03:48 +08:00
父节点 e39e41287e
当前提交 9905c1c79d
共修改 44 个文件,包含 2029 行新增和 169 行删除
+56
查看文件
@@ -0,0 +1,56 @@
import { apiRequestData } from "./http";
export type PortalAnnouncement = {
id: string;
title: string;
summary: string;
linkUrl: string | null;
startsAt: string | null;
endsAt: string | null;
enabled: boolean;
priority: number;
createdAt: string | null;
updatedAt: string | null;
};
export async function fetchAnnouncements(manage = false) {
const suffix = manage ? "?manage=1" : "";
return apiRequestData<{ items: PortalAnnouncement[] }>(`/api/announcements${suffix}`);
}
export async function createAnnouncement(payload: {
title: string;
summary: string;
linkUrl?: string | null;
startsAt?: string | null;
endsAt?: string | null;
enabled?: boolean;
priority?: number;
}) {
return apiRequestData<{ item: PortalAnnouncement }>("/api/announcements", {
method: "POST",
body: JSON.stringify(payload),
});
}
export async function updateAnnouncement(id: string, payload: Partial<{
title: string;
summary: string;
linkUrl: string | null;
startsAt: string | null;
endsAt: string | null;
enabled: boolean;
priority: number;
}>) {
return apiRequestData<{ item: PortalAnnouncement }>(`/api/announcements/${id}`, {
method: "PATCH",
body: JSON.stringify(payload),
});
}
export async function archiveAnnouncement(id: string) {
return apiRequestData<{ item: PortalAnnouncement }>(`/api/announcements/${id}/archive`, {
method: "POST",
body: JSON.stringify({}),
});
}
+19
查看文件
@@ -0,0 +1,19 @@
import { apiRequestData } from "./http";
import type { RiskFlag } from "./feeControl";
export type ApOperationsSummary = {
summary: {
openPaymentBatchCount: number;
failedPaymentBatchItemCount: number;
openRiskFlagCount: number;
reconciliationAnomalyCount: number;
};
openPaymentBatches: Array<{ id: string; batchNo: string; entityType: string; name: string; status: string; createdAt: string | null }>;
failedPaymentBatchItems: Array<{ id: string; paymentBatchId: string; entityId: string; entityCode: string; entityTitle: string; amount: string; currencyCode: string; failureCode: string | null; failureMessage: string | null; createdAt: string | null }>;
openRiskFlags: RiskFlag[];
reconciliationAnomalies: Array<{ id: string; travelReconciliationBatchId: string; travelOrderId: string | null; travelRequestId: string | null; matchStatus: string; resolutionStatus: string; anomalyCode: string | null; orderAmount: string | null; transactionAmount: string | null; currency: string; createdAt: string | null }>;
};
export async function fetchApOperations() {
return apiRequestData<ApOperationsSummary>("/api/ap-operations");
}
+6
查看文件
@@ -17,3 +17,9 @@ export async function changePassword(payload: { oldPassword: string; newPassword
});
}
export type SsoProviderInfo = { id: "oidc"; label: string; enabled: boolean; loginUrl: string };
export async function fetchSsoProviders() {
return apiRequestData<SsoProviderInfo[]>("/api/auth/sso/providers", { auth: false });
}
+72
查看文件
@@ -0,0 +1,72 @@
import { apiRequestData } from "./http";
export type MigrationRun = {
id: string;
runType: "employees" | "departments" | "budgets" | "masterdata_validation";
status: string;
sourceFilename: string;
summaryJson: Record<string, unknown> | null;
errorJson: Record<string, unknown> | null;
createdAt: string | null;
createdBy: string | null;
};
export type CutoverScope = {
id: string;
scopeType: "department" | "legal_entity" | "domain";
scopeKey: string;
enabled: boolean;
startsAt: string | null;
notes: string | null;
createdAt: string | null;
updatedAt: string | null;
updatedBy: string | null;
};
export async function fetchMigrationRuns() {
return apiRequestData<{ items: MigrationRun[] }>("/api/migration-runs");
}
export async function fetchMigrationRun(id: string) {
return apiRequestData<{ item: MigrationRun }>(`/api/migration-runs/${id}`);
}
export async function dryRunMigration(payload: { runType: MigrationRun["runType"]; file: File }) {
const formData = new FormData();
formData.append("runType", payload.runType);
formData.append("file", payload.file);
return apiRequestData<{ item: MigrationRun }>("/api/migration-runs/dry-run", {
method: "POST",
body: formData,
});
}
export async function fetchCutoverScopes() {
return apiRequestData<{ items: CutoverScope[] }>("/api/cutover/scopes");
}
export async function createCutoverScope(payload: {
scopeType: CutoverScope["scopeType"];
scopeKey: string;
enabled?: boolean;
startsAt?: string | null;
notes?: string | null;
}) {
return apiRequestData<{ item: CutoverScope }>("/api/cutover/scopes", {
method: "POST",
body: JSON.stringify(payload),
});
}
export async function updateCutoverScope(id: string, payload: Partial<{
scopeType: CutoverScope["scopeType"];
scopeKey: string;
enabled: boolean;
startsAt: string | null;
notes: string | null;
}>) {
return apiRequestData<{ item: CutoverScope }>(`/api/cutover/scopes/${id}`, {
method: "PATCH",
body: JSON.stringify(payload),
});
}
+1 -1
查看文件
@@ -1,6 +1,6 @@
import { apiRequestData } from "./http";
export type FeatureFlagKey = "erp_sync" | "invoice_verification" | "leave_holiday_calendar";
export type FeatureFlagKey = "erp_sync" | "invoice_verification" | "leave_holiday_calendar" | "budget_precheck_v1" | "payment_batch_v1" | "portal_home_v1" | "global_search_v1" | "announcements_v1" | "workflow_definition_admin_v1" | "cutover_console_v1";
export type FeatureFlagOverrideMode = "inherit" | "force_on" | "force_off";
export type FeatureFlagRolloutType = "global" | "department_allowlist";
+34
查看文件
@@ -0,0 +1,34 @@
export type RiskFlag = {
id: string;
entityType: string;
entityId: string;
riskCode: string;
level: "low" | "medium" | "high";
title: string;
detail: string | null;
source: string;
status: "open" | "resolved" | "dismissed";
payloadJson: Record<string, unknown> | null;
createdAt: string | null;
createdBy: string | null;
resolvedAt: string | null;
resolvedBy: string | null;
};
export type PrecheckDecision = "pass" | "warn" | "manual_review" | "block";
export type PrecheckResponse = {
entityType: "reimbursement" | "purchase_request" | "travel_request";
entityId: string;
decision: PrecheckDecision;
matchedBudgetControlRule: {
id: string;
name: string;
controlMode: "warn" | "block" | "manual_review";
} | null;
budgetHint: Record<string, unknown> | null;
warnings: Array<{ code: string; title: string; detail: string }>;
blockingReasons: Array<{ code: string; title: string; detail: string }>;
riskFlags: Array<{ code: string; level: "low" | "medium" | "high"; title: string; detail: string; willOpenOnSubmit: boolean }>;
nextActions: string[];
};
+109
查看文件
@@ -0,0 +1,109 @@
import { apiRequestData } from "./http";
import type { RiskFlag } from "./feeControl";
export type PaymentBatchEntityType = "reimbursement" | "travel_order";
export type PaymentBatchStatus = "draft" | "reviewed" | "executed" | "failed" | "cancelled";
export type PaymentChannel = "bank" | "alipay" | "wechat" | "cash" | "other";
export type PaymentBatchItem = {
id: string;
entityId: string;
entityCode: string;
entityTitle: string;
amount: string;
currencyCode: string;
paymentVoucherNoOverride: string | null;
paymentDateOverride: string | null;
paymentChannelOverride: PaymentChannel | null;
status: string;
failureCode: string | null;
failureMessage: string | null;
snapshotJson: Record<string, unknown>;
createdAt: string | null;
openRiskFlags: RiskFlag[];
};
export type PaymentBatch = {
id: string;
batchNo: string;
entityType: PaymentBatchEntityType;
name: string;
status: PaymentBatchStatus;
defaultPaymentDate: string | null;
defaultPaymentChannel: PaymentChannel | null;
voucherPrefix: string | null;
reviewComment: string | null;
executionSummaryJson: Record<string, unknown> | null;
createdAt: string | null;
createdBy: string | null;
reviewedAt: string | null;
reviewedBy: string | null;
executedAt: string | null;
executedBy: string | null;
cancelledAt: string | null;
cancelledBy: string | null;
items: PaymentBatchItem[];
};
export type PaymentBatchListResponse = { items: PaymentBatch[] };
export async function fetchPaymentBatches(query: { entityType?: PaymentBatchEntityType; status?: PaymentBatchStatus } = {}) {
const params = new URLSearchParams();
if (query.entityType) params.set("entityType", query.entityType);
if (query.status) params.set("status", query.status);
const suffix = params.toString() ? `?${params.toString()}` : "";
return apiRequestData<PaymentBatchListResponse>(`/api/payment-batches${suffix}`);
}
export async function fetchPaymentBatchDetail(batchId: string) {
return apiRequestData<{ item: PaymentBatch }>(`/api/payment-batches/${batchId}`);
}
export async function createPaymentBatch(payload: {
entityType: PaymentBatchEntityType;
name: string;
entityIds: string[];
defaultPaymentDate?: string;
defaultPaymentChannel?: PaymentChannel;
voucherPrefix?: string;
}) {
return apiRequestData<{ item: PaymentBatch }>("/api/payment-batches", {
method: "POST",
body: JSON.stringify(payload),
});
}
export async function updatePaymentBatchItem(batchId: string, itemId: string, payload: {
paymentVoucherNoOverride?: string | null;
paymentDateOverride?: string | null;
paymentChannelOverride?: PaymentChannel | null;
}) {
return apiRequestData<{ item: Pick<PaymentBatchItem, "id" | "paymentVoucherNoOverride" | "paymentDateOverride" | "paymentChannelOverride"> }>(
`/api/payment-batches/${batchId}/items/${itemId}`,
{
method: "PATCH",
body: JSON.stringify(payload),
},
);
}
export async function reviewPaymentBatch(batchId: string, payload: { comment?: string; resolveRiskFlagIds?: string[] } = {}) {
return apiRequestData<{ item: PaymentBatch }>(`/api/payment-batches/${batchId}/review`, {
method: "POST",
body: JSON.stringify(payload),
});
}
export async function executePaymentBatch(batchId: string) {
return apiRequestData<{ item: PaymentBatch }>(`/api/payment-batches/${batchId}/execute`, {
method: "POST",
body: JSON.stringify({}),
});
}
export async function cancelPaymentBatch(batchId: string) {
return apiRequestData<{ item: PaymentBatch }>(`/api/payment-batches/${batchId}/cancel`, {
method: "POST",
body: JSON.stringify({}),
});
}
+15
查看文件
@@ -0,0 +1,15 @@
import { apiRequestData } from "./http";
export type PortalHomePayload = {
greeting: { name: string };
cards: Array<{ key: string; title: string; value: string | number; href: string; severity?: "normal" | "warning" | "high" }>;
pendingTasks: { count: number; href: string };
myApplications: Array<{ domain: string; count: number; href: string }>;
riskSummary: { openCount: number; href: string } | null;
announcements: Array<{ id: string; title: string; summary: string; href: string | null }>;
quickLinks: Array<{ key: string; title: string; href: string }>;
};
export async function fetchPortalHome() {
return apiRequestData<PortalHomePayload>("/api/portal/home");
}
+11
查看文件
@@ -1,4 +1,5 @@
import { apiRequestData } from "./http";
import type { PrecheckResponse, RiskFlag } from "./feeControl";
export type PurchaseRequestStatus = "draft" | "reviewing" | "approved" | "rejected" | "cancelled";
@@ -205,6 +206,7 @@ export type PurchaseRequestDetail = PurchaseRequestSummary & {
canWithdraw: boolean;
canArchiveOrder: boolean;
};
riskFlags: RiskFlag[];
};
export type PurchaseRequestListResponse = {
@@ -283,6 +285,13 @@ export async function updatePurchaseRequest(
});
}
export async function precheckPurchaseRequest(purchaseRequestId: string) {
return apiRequestData<PrecheckResponse>(`/api/purchase-requests/${purchaseRequestId}/precheck`, {
method: "POST",
body: JSON.stringify({}),
});
}
export async function submitPurchaseRequest(purchaseRequestId: string) {
return apiRequestData<{
purchaseRequestId: string;
@@ -296,6 +305,8 @@ export async function submitPurchaseRequest(purchaseRequestId: string) {
applied: boolean;
reservedAmount: string;
};
precheckDecision: "pass" | "warn" | "manual_review";
openedRiskFlagCount: number;
}>(`/api/purchase-requests/${purchaseRequestId}/submit`, {
method: "POST",
body: JSON.stringify({}),
+12 -1
查看文件
@@ -1,4 +1,5 @@
import { apiRequestData } from "./http";
import type { PrecheckResponse, RiskFlag } from "./feeControl";
export type ReimbursementStatus =
| "draft"
@@ -113,6 +114,7 @@ export type ReimbursementDetail = {
assignee_role: string | null;
} | null;
} | null;
riskFlags: RiskFlag[];
items: ReimbursementItem[];
};
@@ -161,11 +163,13 @@ export type ReimbursementSubmitResponse = {
budget: {
budgetId: string;
budgetCode: string;
controlMode: "warn" | "block";
controlMode: "warn" | "block" | "manual_review";
usagePercent: number;
exceededThreshold: boolean;
exceededBudget: boolean;
} | null;
precheckDecision: "pass" | "warn" | "manual_review";
openedRiskFlagCount: number;
};
function toQueryString(query: Record<string, string | number | undefined>) {
@@ -203,6 +207,13 @@ export async function createReimbursement(payload: {
});
}
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",
+17
查看文件
@@ -0,0 +1,17 @@
import { apiRequestData } from "./http";
export type GlobalSearchHit = {
entityType: "reimbursement" | "leave" | "purchase_request" | "travel_request" | "asset";
entityId: string;
title: string;
subtitle: string | null;
status: string | null;
href: string;
matchedBy: "exact" | "prefix" | "contains";
updatedAt: string | null;
};
export async function searchGlobal(q: string, limit = 10) {
const params = new URLSearchParams({ q, limit: String(limit) });
return apiRequestData<{ items: GlobalSearchHit[] }>(`/api/search?${params.toString()}`);
}
+11
查看文件
@@ -1,4 +1,5 @@
import { apiRequestData } from "./http";
import type { PrecheckResponse, RiskFlag } from "./feeControl";
export type TravelRequestStatus = "draft" | "reviewing" | "approved" | "rejected";
export type TravelTripType = "domestic" | "international";
@@ -108,6 +109,7 @@ export type TravelRequestDetail = TravelRequestSummary & {
canWithdraw: boolean;
canManage: boolean;
};
riskFlags: RiskFlag[];
};
export type TravelRequestListResponse = {
@@ -160,6 +162,8 @@ export type TravelRequestSubmitResponse = {
reservedAmount?: string;
items?: Array<{ budgetPoolId: string; amount: string }>;
};
precheckDecision: "pass" | "warn" | "manual_review";
openedRiskFlagCount: number;
};
export type TravelRequestWithdrawResponse = {
@@ -209,6 +213,13 @@ export async function updateTravelRequest(travelRequestId: string, payload: Part
});
}
export async function precheckTravelRequest(travelRequestId: string) {
return apiRequestData<PrecheckResponse>(`/api/travel-requests/${travelRequestId}/precheck`, {
method: "POST",
body: JSON.stringify({}),
});
}
export async function submitTravelRequest(travelRequestId: string) {
return apiRequestData<TravelRequestSubmitResponse>(`/api/travel-requests/${travelRequestId}/submit`, {
method: "POST",
+45
查看文件
@@ -0,0 +1,45 @@
import { apiRequestData } from "./http";
export type WorkflowDefinitionEditorModel = {
id: string;
entityType: "reimbursement" | "leave" | "purchase_request" | "travel_request";
name: string;
version: number;
status: "draft" | "published" | "archived" | "derived_readonly";
steps: Array<{
id: string;
name: string;
assigneeRule:
| { type: "department_manager" }
| { type: "role"; value: string }
| { type: "specific_user"; value: string };
}>;
notes?: string;
source: "manual" | "approval_rules_projection";
createdAt: string | null;
updatedAt: string | null;
publishedAt: string | null;
archivedAt: string | null;
};
export async function fetchWorkflowDefinitions() {
return apiRequestData<{ items: WorkflowDefinitionEditorModel[] }>("/api/workflow-definitions");
}
export async function fetchWorkflowDefinition(id: string) {
return apiRequestData<{ item: WorkflowDefinitionEditorModel }>(`/api/workflow-definitions/${id}`);
}
export async function fetchActiveWorkflowDefinition(entityType: WorkflowDefinitionEditorModel["entityType"]) {
return apiRequestData<{ item: WorkflowDefinitionEditorModel | null }>(`/api/workflow-definitions/entity/${entityType}/active`);
}
export async function createWorkflowDefinition(payload: { entityType: WorkflowDefinitionEditorModel["entityType"]; name: string; steps: WorkflowDefinitionEditorModel["steps"]; notes?: string }) {
return apiRequestData<{ item: WorkflowDefinitionEditorModel }>("/api/workflow-definitions", { method: "POST", body: JSON.stringify(payload) });
}
export async function updateWorkflowDefinition(id: string, payload: Partial<{ name: string; steps: WorkflowDefinitionEditorModel["steps"]; notes?: string }>) {
return apiRequestData<{ item: WorkflowDefinitionEditorModel }>(`/api/workflow-definitions/${id}`, { method: "PATCH", body: JSON.stringify(payload) });
}
export async function publishWorkflowDefinition(id: string) {
return apiRequestData<{ item: WorkflowDefinitionEditorModel }>(`/api/workflow-definitions/${id}/publish`, { method: "POST", body: JSON.stringify({}) });
}
export async function archiveWorkflowDefinition(id: string) {
return apiRequestData<{ item: WorkflowDefinitionEditorModel }>(`/api/workflow-definitions/${id}/archive`, { method: "POST", body: JSON.stringify({}) });
}