From f299ceeb74f502e4505801f0135443921bc36b9b Mon Sep 17 00:00:00 2001 From: shunli Date: Sat, 30 May 2026 23:06:02 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=20ERP=20=E8=BE=B9=E7=95=8C?= =?UTF-8?q?=E4=B8=8E=E8=B5=84=E4=BA=A7=E9=A1=B5=E9=9D=A2=E4=B8=80=E8=87=B4?= =?UTF-8?q?=E6=80=A7=EF=BC=8805-30-fix-erp-boundary-asset-consistency?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../spec/hyb-oa/frontend-react-vite-design.md | 42 +- .../check.jsonl | 3 + .../implement.jsonl | 3 + .../implement.md | 13 + .../prd.md | 67 ++ .../task.json | 33 + client/src/api/errorMessages.ts | 2 +- client/src/components/navigation/NavLinks.tsx | 1 - client/src/components/navigation/Topbar.tsx | 8 +- .../travel/TravelOrderMarkPaidDialog.tsx | 2 +- .../travel/TravelPaymentTrackCard.tsx | 6 +- .../travel/TravelReconciliationTab.tsx | 12 +- client/src/features/navigation/schema.ts | 1 - client/src/pages/AssetRegistryPage.tsx | 545 +++++--------- client/src/pages/ErpIntegrationPage.tsx | 686 ------------------ client/src/pages/FeatureFlagsPage.tsx | 5 +- client/src/pages/PurchaseRequestsPage.tsx | 251 ------- client/src/pages/ReimbursementDetailPage.tsx | 174 ----- client/src/pages/SystemSettingsPage.tsx | 3 +- client/src/pages/WorkspaceSetupPage.tsx | 8 +- client/src/router/index.tsx | 10 - client/src/router/lazy/ErpIntegrationPage.ts | 1 - client/src/router/lazy/index.ts | 1 - .../e2e/announcements-policy-ack.spec.ts | 6 +- .../tests/e2e/announcements-presence.spec.ts | 2 +- client/tests/e2e/assets-flow.spec.ts | 20 +- .../tests/e2e/confidential-boundary.spec.ts | 2 +- client/tests/e2e/helpers/auth.ts | 16 +- client/tests/e2e/leave-team-calendar.spec.ts | 2 +- client/tests/e2e/login-portal-ux.spec.ts | 43 +- client/tests/e2e/project-workbench.spec.ts | 2 +- client/tests/e2e/r100-design-baseline.spec.ts | 2 +- .../e2e/r101-mobile-wip-closeout.spec.ts | 2 +- .../e2e/r102-reimbursement-main-flow.spec.ts | 2 +- .../r103-cross-page-ui-consistency.spec.ts | 4 +- .../e2e/r89-mobile-login-navigation.spec.ts | 8 +- .../e2e/r92-mobile-invoice-usability.spec.ts | 14 +- client/tests/e2e/visual-portal.spec.ts | 12 +- client/tests/e2e/visual-shell.spec.ts | 10 +- .../e2e/workflow-action-confirm-ui.spec.ts | 4 +- .../tests/e2e/workflow-quick-approve.spec.ts | 2 +- docs/产品设计规则.md | 10 +- .../scripts/export-small-team-observation.js | 2 +- server/src/scripts/seed-config.js | 7 - server/src/scripts/seed-dev.js | 50 +- server/src/scripts/seed-trialrun.js | 7 +- server/src/services/workspaceSettings.js | 3 - 47 files changed, 476 insertions(+), 1633 deletions(-) create mode 100644 .trellis/tasks/05-30-fix-erp-boundary-asset-consistency/check.jsonl create mode 100644 .trellis/tasks/05-30-fix-erp-boundary-asset-consistency/implement.jsonl create mode 100644 .trellis/tasks/05-30-fix-erp-boundary-asset-consistency/implement.md create mode 100644 .trellis/tasks/05-30-fix-erp-boundary-asset-consistency/prd.md create mode 100644 .trellis/tasks/05-30-fix-erp-boundary-asset-consistency/task.json delete mode 100644 client/src/pages/ErpIntegrationPage.tsx delete mode 100644 client/src/router/lazy/ErpIntegrationPage.ts diff --git a/.trellis/spec/hyb-oa/frontend-react-vite-design.md b/.trellis/spec/hyb-oa/frontend-react-vite-design.md index cd938f09..94fb1d8b 100644 --- a/.trellis/spec/hyb-oa/frontend-react-vite-design.md +++ b/.trellis/spec/hyb-oa/frontend-react-vite-design.md @@ -361,8 +361,9 @@ point and prevents each route from inventing a different title/action layout. - `个人设置` stays in the topbar account menu. Notifications stay in the topbar bell and notification center. - Finance labels should use work names such as `付款`, `应付账款` and `票据`; - internal platform words like `AP 运营`, `ERP 集成` and `功能开关` should be - hidden behind management wording or renamed for users. + internal platform words like `AP 运营` and `功能开关` should be hidden behind + management wording or renamed for users. `ERP 集成` is not a hyb-oa product + entry. - `更多` is a lightweight collapsed row for low-frequency, frozen, staged or occasional entries; do not add a sidebar explanation card, subtitle or onboarding copy around it. @@ -652,7 +653,7 @@ screen. - Procurement ledgers in V1 are request, approval, attachment, light acceptance/archive and relation surfaces for project, contract and payment/AP context. Do not design `/purchase-requests` as purchase-order lifecycle, - inventory, three-way matching, supplier-lifecycle, tendering or ERP software. + inventory, three-way matching, supplier-lifecycle or tendering software. Approved procurement means the request was approved; it does not imply paid, received into inventory or converted into a fixed asset. - Project pages in V1 are light project ledgers for status, owner/member, @@ -665,10 +666,11 @@ screen. - Finance ledgers in V1 are payment-preparation, payment-batch, receipt-audit, AP-status, invoice/OCR-status and agency-accounting handover summaries. Do not design payment, AP or invoice pages as online banking, bank-statement - ingestion, ERP/general-ledger, voucher-generation, auto-reconciliation, tax - filing or full accounting software. Real payment stays in offline banking or - existing finance/accounting workflows; OA records status, evidence and - handover summaries. + ingestion, general-ledger, voucher-generation, auto-reconciliation, tax filing + or full accounting software. Real payment stays in offline banking or existing + finance/accounting workflows; OA records status, evidence and handover + summaries. hyb-oa does not connect to ERP now and does not plan ERP + integration later. - Payment, AP and invoice pages default to task/exception visibility: `付款` shows review, payment-marking, receipt and exception attention before batch creation; `应付账款` shows pending payment, exceptions, risks and differences @@ -710,7 +712,8 @@ subsystem. - The default `/assets` view answers where the asset is, who uses it, who is responsible for it, what state it is in and whether it needs inventory or - handover. Do not lead with depreciation, accounting subjects or ERP sync. + handover. Do not lead with depreciation, accounting subjects or external + finance sync. - Register physical items that must be returned, transferred, inventoried or protected, such as computers, monitors, disks, equipment and security media. Do not create `FA` cards for ordinary consumables. @@ -738,9 +741,10 @@ subsystem. - Move depreciation, residual value, accounting subject and monthly depreciation voucher concepts out of the OA primary UI unless a later Trellis task explicitly scopes accounting-ledger work. -- Keep classification maintenance, ERP sync, raw metadata and advanced filters - behind management, collapsed or advanced surfaces so they do not dominate the - asset list/detail first screen. +- Keep classification maintenance, raw metadata and advanced filters behind + management, collapsed or advanced surfaces so they do not dominate the asset + list/detail first screen. Do not show ERP sync state or ERP asset numbers in + the asset UI. - Mobile asset lookup should prefer QR scan and fall back to asset number or serial number input. Do not require heavy photo inventory unless a later task explicitly scopes it. @@ -1141,9 +1145,10 @@ request, frontend click or message attempt as final business completion. - Enterprise WeChat owns daily HR/attendance actions: check-in, leave, missed punch, outing and overtime. OA shows mirror, history, exception, audit and emergency-takeover records for those domains. -- ERP, agency accounting, bank, OCR and Enterprise WeChat messages are external +- Agency accounting, bank, OCR and Enterprise WeChat messages are external capabilities or delivery channels. They do not replace OA server-side - permissions or state machines. + permissions or state machines. ERP is outside hyb-oa's product boundary and + must not appear as a current or future user-facing integration. - Show final labels such as `已同步`, `已发送`, `已付款` and `识别完成` only when the relevant external system has returned a clear success acknowledgement. - Use in-progress labels such as `同步中`, `等待回执`, `银行处理中`, `识别中` and @@ -1152,8 +1157,8 @@ request, frontend click or message attempt as final business completion. such as `银行回执未返回` or `OCR 识别失败,需重新上传 PDF`. - Do not expose raw external error codes, payloads, full URLs, secrets, tokens or debug traces to ordinary users. -- External writes, payment execution, ERP sync, Enterprise WeChat resend and OCR - retry must be controlled by permissions and state-driven actions. +- External writes, payment execution, Enterprise WeChat resend and OCR retry + must be controlled by permissions and state-driven actions. - Retry confirmation should explain which object will be retried, whether it may send duplicate external messages, external-system impact and audit effect. - External sync state should not block unrelated primary flows. If it blocks a @@ -1165,8 +1170,9 @@ request, frontend click or message attempt as final business completion. unless OA emergency takeover is explicitly enabled. - Enterprise WeChat messages carry desensitized summary and OA deeplink only; they do not carry sensitive attachment text or final payment/approval actions. -- If bank, ERP, agency accounting or another integration is not connected or not - confirmed, UI copy must not imply the capability already completed. +- If bank, agency accounting or another enabled integration is not connected or + not confirmed, UI copy must not imply the capability already completed. Do not + use ERP as a placeholder for future integration copy. **Why**: External integrations are asynchronous and failure-prone. Conservative copy prevents users from trusting a state the system has not actually verified. @@ -1176,7 +1182,7 @@ copy prevents users from trusting a state the system has not actually verified. - `cd client && npm run typecheck` - `cd client && npm run build` - Focused Playwright coverage when changing Enterprise WeChat boundary copy, - OCR status, bank/payment callbacks, ERP/accounting sync status, retry + OCR status, bank/payment callbacks, accounting handover status, retry controls, external failure messages or integration diagnostics. ## Convention: Verification And User-Testable Claims diff --git a/.trellis/tasks/05-30-fix-erp-boundary-asset-consistency/check.jsonl b/.trellis/tasks/05-30-fix-erp-boundary-asset-consistency/check.jsonl new file mode 100644 index 00000000..8734c461 --- /dev/null +++ b/.trellis/tasks/05-30-fix-erp-boundary-asset-consistency/check.jsonl @@ -0,0 +1,3 @@ +{"file": ".trellis/spec/hyb-oa/frontend-react-vite-design.md", "reason": "检查列表/详情/动作区是否符合统一成熟页面规则"} +{"file": "docs/产品设计规则.md", "reason": "检查无 ERP、员工姓名编号、资产业务边界是否沉淀"} +{"file": ".trellis/spec/hyb-oa/security-privacy.md", "reason": "检查敏感信息和外部系统文案没有误暴露"} diff --git a/.trellis/tasks/05-30-fix-erp-boundary-asset-consistency/implement.jsonl b/.trellis/tasks/05-30-fix-erp-boundary-asset-consistency/implement.jsonl new file mode 100644 index 00000000..4747b567 --- /dev/null +++ b/.trellis/tasks/05-30-fix-erp-boundary-asset-consistency/implement.jsonl @@ -0,0 +1,3 @@ +{"file": ".trellis/spec/hyb-oa/frontend-react-vite-design.md", "reason": "统一前端页面基线、资产台账和详情页动作区规则"} +{"file": "docs/产品设计规则.md", "reason": "产品边界、无 ERP 口径、资产管理业务事实"} +{"file": ".trellis/spec/hyb-oa/security-privacy.md", "reason": "资产敏感字段与外部系统文案清理的隐私边界"} diff --git a/.trellis/tasks/05-30-fix-erp-boundary-asset-consistency/implement.md b/.trellis/tasks/05-30-fix-erp-boundary-asset-consistency/implement.md new file mode 100644 index 00000000..0a444cfd --- /dev/null +++ b/.trellis/tasks/05-30-fix-erp-boundary-asset-consistency/implement.md @@ -0,0 +1,13 @@ +# 实施说明 + +## 设计决策 + +- ERP 是产品边界外能力:当前 UI、导航、设置、功能开关和默认业务详情不再暴露 ERP 入口、同步状态或未来接入口径。 +- 资产首屏按业务台账处理:列表只负责查找、状态和进入详情,详情采用主内容区加 PC 右侧操作面板。 +- 资产操作区集中承接保存、领用、归还和调拨,按钮横向排列,普通解释文案不默认展示。 +- 账号默认展示中文姓名和员工编号;已有本地账号的 `employee_no` 不强制更新,因为数据库约束把员工号视为不可变字段。 + +## 验证限制 + +- 本地真实资产 E2E 受数据库迁移漂移阻断,当前库缺少 `asset_cards.asset_type` 和 `purchase_requests.project_id` 等列;本任务不擅自执行数据库迁移。 +- 资产页面已用 mock API 做浏览器检查,验证无 ERP 文案、右侧操作区、中文姓名和员工号展示。 diff --git a/.trellis/tasks/05-30-fix-erp-boundary-asset-consistency/prd.md b/.trellis/tasks/05-30-fix-erp-boundary-asset-consistency/prd.md new file mode 100644 index 00000000..4e5b411a --- /dev/null +++ b/.trellis/tasks/05-30-fix-erp-boundary-asset-consistency/prd.md @@ -0,0 +1,67 @@ +# 修正 ERP 边界与资产页面一致性 + +## Goal + +把用户反馈的产品边界和资产页面一致性问题收口到同一批改造:OA 不再出现 ERP 作为当前或未来接入对象;账号展示使用中文姓名和员工编号;资产列表、资产详情和资产操作区回到全系统统一的成熟企业台账/详情页规则。 + +## What I already know + +- 目标用户是国内、小于 10 人的科技初创公司,页面应优先采用成熟 OA / Feishu / Ant Design Pro 式常规交互,不做创新型操作命名或解释性堆砌。 +- 用户明确确认:系统中没有 ERP,以后也不会有 ERP。 +- 员工账号应有中文姓名和员工编号,不能在真实可见路径里默认展示 `Admin`、`Employee`、`Finance Manager` 这类英文角色名。 +- `client/src/pages/AssetRegistryPage.tsx` 的资产列表仍使用页面本地 `Table` 组件结构,视觉密度和报销等列表不一致。 +- 资产详情页仍是单列大卡堆叠,存在基础信息、责任归档、流转、ERP 同步、AI 建议、保存动作等多处操作与说明,和统一详情工作区的右侧状态/动作面板不一致。 +- `server/src/scripts/seed-dev.js` 默认开发账号姓名为英文角色名;`client/tests/e2e/helpers/auth.ts` 和多条 E2E fixture 也有英文名。 +- `docs/产品设计规则.md` 和 `.trellis/spec/hyb-oa/frontend-react-vite-design.md` 已有部分 no-ERP 事实,但仍把 ERP 写成“外部能力/外部系统”或资产高级轨道。 + +## Requirements + +- 用户可见 UI、导航、设置、功能开关和资产/采购/报销详情的默认路径不得出现 ERP 文案、ERP 入口或 ERP 未来接入口径。 +- 后端 API、数据库表、历史迁移或内部代码中的 `erp_*` 技术遗留不在本任务大拆;但不能通过前端入口、菜单或说明暴露为产品能力。 +- 开发和自动化默认账号改为中文姓名,并保留员工编号。测试代码中需要登录识别的用户对象也应同步中文显示名。 +- 资产列表采用与报销列表一致的业务台账样式:单列列表首屏、统一 `PrimaryTable` + `table-row/table-td` 密度、右侧操作列、移动端卡片尾部动作。 +- 资产台账工具只承接低频管理能力,例如分类维护、高级筛选和新建资产;首屏不把工具详情铺在左侧或常驻在列表旁。 +- 资产详情采用统一详情工作区:顶部摘要、主内容区信息/证据、PC 右侧状态与操作面板,移动端保持单列和底部/就近主动作。 +- 资产流转动作集中到详情操作区,按钮横向排列;基础信息保存、领用、归还、调拨等动作位置稳定,不分散在多个大卡下方。 +- 资产详情默认减少解释性句子,仅保留安全/权限/高敏字段等必要边界提示。 + +## Scope + +- 前端页面:资产台账/详情、报销详情财务导出暴露面、顶部账号展示、导航/设置/功能开关中的 ERP 入口隐藏。 +- 开发与测试账号:开发种子和常用 E2E fixture 的默认显示名改为中文,并保留员工编号展示。 +- 事实源:更新 `docs/产品设计规则.md` 和 `.trellis/spec/hyb-oa/frontend-react-vite-design.md` 中的 no-ERP 和资产详情操作区规则。 +- 验证:前端类型检查、构建、聚焦 Playwright/mock 视觉检查、Trellis task 校验、governance 和 diff 检查。 + +## Acceptance Criteria + +- [x] `/assets` PC 首屏资产列表和报销等台账使用一致的表格密度、右侧操作列和单列首屏结构。 +- [x] 资产详情 PC 端出现统一右侧状态/动作面板;流转、保存、返回等动作不再分散在页面底部或独立大块中。 +- [x] 资产详情默认不显示 ERP 同步状态、ERP 资产编号或 ERP 重试操作。 +- [x] 管理入口、功能开关和普通业务页面不再向用户展示 ERP 作为系统能力。 +- [x] 开发/E2E 默认账号中文显示名与员工编号同步,顶部账号、人员选择和测试 fixture 不再默认依赖英文角色名。 +- [x] 文档和 Trellis spec 固化:OA 无 ERP,未来也不规划接入;资产页不以 ERP/会计固定资产账为产品口径。 +- [x] 前端类型检查、构建、相关 Playwright 或截图检查通过;Trellis governance 和 `git diff --check` 通过。真实资产 E2E 因本地数据库 schema 漂移阻断,已记录在 `implement.md`。 + +## Out of Scope + +- 不删除历史数据库迁移、已有后端 ERP 命名服务、历史 API 路径或老数据表,以免扩大破坏面;本任务只清理产品暴露面和当前默认 UI。 +- 不引入新的外部系统、财务软件、总账、银行直连、自动凭证或折旧模块。 +- 不重做资产数据模型、盘点任务引擎或二维码贴标全流程。 +- 不修改公网真实数据,除非后续部署/验证步骤明确需要并获得批准。 + +## Test Plan + +- `cd client && npm run typecheck` +- `cd client && npm run build` +- 聚焦 Playwright:资产列表/详情、跨页面 UI 一致性、登录/顶部账号显示相关用例。 +- `python3 ./.trellis/scripts/task.py validate 05-30-fix-erp-boundary-asset-consistency` +- `node scripts/qa/check-trellis-governance.mjs` +- `git diff --check` +- 若需要告知线上可测,部署后再跑公网 `/ready`、bundle copy 扫描和可用凭据范围内的 live smoke。 + +## Technical Notes + +- 主要前端文件:`client/src/pages/AssetRegistryPage.tsx`、`client/src/pages/ErpIntegrationPage.tsx`、`client/src/pages/SystemSettingsPage.tsx`、`client/src/pages/FeatureFlagsPage.tsx`、`client/src/features/navigation/schema.ts`。 +- 账号/fixture:`server/src/scripts/seed-dev.js`、`server/src/scripts/seed-trialrun.js`、`client/tests/e2e/helpers/auth.ts` 和涉及 mock `me.name` 的 E2E。 +- 事实源:`docs/产品设计规则.md`、`.trellis/spec/hyb-oa/frontend-react-vite-design.md`。 +- 设计裁决按 `frontend-design` skill 和现有成熟企业页面基线执行。 diff --git a/.trellis/tasks/05-30-fix-erp-boundary-asset-consistency/task.json b/.trellis/tasks/05-30-fix-erp-boundary-asset-consistency/task.json new file mode 100644 index 00000000..439e4218 --- /dev/null +++ b/.trellis/tasks/05-30-fix-erp-boundary-asset-consistency/task.json @@ -0,0 +1,33 @@ +{ + "id": "fix-erp-boundary-asset-consistency", + "name": "fix-erp-boundary-asset-consistency", + "title": "修正 ERP 边界与资产页面一致性", + "description": "清理用户可见 ERP 产品暴露,统一资产台账/详情交互,并同步中文姓名与员工编号展示。", + "status": "in_progress", + "dev_type": null, + "scope": "client,server,docs,trellis-spec", + "package": null, + "priority": "P2", + "creator": "shunli", + "assignee": "shunli", + "createdAt": "2026-05-30", + "completedAt": null, + "branch": null, + "base_branch": "main", + "worktree_path": null, + "commit": null, + "pr_url": null, + "subtasks": [], + "children": [], + "parent": null, + "relatedFiles": [ + "client/src/pages/AssetRegistryPage.tsx", + "client/src/pages/ReimbursementDetailPage.tsx", + "client/src/components/navigation/Topbar.tsx", + "server/src/scripts/seed-dev.js", + "docs/产品设计规则.md", + ".trellis/spec/hyb-oa/frontend-react-vite-design.md" + ], + "notes": "", + "meta": {} +} diff --git a/client/src/api/errorMessages.ts b/client/src/api/errorMessages.ts index 79205d7d..0ee4d34e 100644 --- a/client/src/api/errorMessages.ts +++ b/client/src/api/errorMessages.ts @@ -358,7 +358,7 @@ export function mapApiErrorMessage( } if (details?.integration === "erp_sync_disabled") { - return "ERP 同步功能未开启。"; + return "该同步功能未开启。"; } if (details?.integration === "travel_sync_disabled") { diff --git a/client/src/components/navigation/NavLinks.tsx b/client/src/components/navigation/NavLinks.tsx index 72849e61..2333e33c 100644 --- a/client/src/components/navigation/NavLinks.tsx +++ b/client/src/components/navigation/NavLinks.tsx @@ -56,7 +56,6 @@ const MANAGEMENT_ACTIVE_PATHS = [ "/system/reimbursement-templates", "/system/master-data", "/system/notification-channels", - "/system/integrations/erp", "/system/users", "/system/departments", "/system/cutover", diff --git a/client/src/components/navigation/Topbar.tsx b/client/src/components/navigation/Topbar.tsx index 2afaa3ef..f2eec6e5 100644 --- a/client/src/components/navigation/Topbar.tsx +++ b/client/src/components/navigation/Topbar.tsx @@ -178,7 +178,9 @@ export function Topbar() { ) : null} -
{me ? `${me.name} · 当前工作区` : "海音达 OA"}
+
+ {me ? `${me.name} · ${me.employeeNo}` : "海音达 OA"} +
@@ -293,8 +295,8 @@ export function Topbar() { - diff --git a/client/src/components/travel/TravelOrderMarkPaidDialog.tsx b/client/src/components/travel/TravelOrderMarkPaidDialog.tsx index 94bc65c3..0f973372 100644 --- a/client/src/components/travel/TravelOrderMarkPaidDialog.tsx +++ b/client/src/components/travel/TravelOrderMarkPaidDialog.tsx @@ -58,7 +58,7 @@ export function TravelOrderMarkPaidDialog({ 标记差旅订单已支付 - 确认订单 `{order?.externalOrderNo ?? "-"}` 已完成对公支付;系统会按幂等键复用现有 ERP journal 出站记录。 + 确认订单 `{order?.externalOrderNo ?? "-"}` 已完成对公支付;系统会按幂等键复用现有财务交接记录。 diff --git a/client/src/components/travel/TravelPaymentTrackCard.tsx b/client/src/components/travel/TravelPaymentTrackCard.tsx index f9598bac..e4f2056b 100644 --- a/client/src/components/travel/TravelPaymentTrackCard.tsx +++ b/client/src/components/travel/TravelPaymentTrackCard.tsx @@ -66,7 +66,7 @@ function erpStageLabel(order: TravelPaymentOrder) { if (order.erpStage.binding?.externalId) { return "已同步"; } - return "ERP 出站未开启"; + return "摘要导出未开启"; } type TravelPaymentTrackCardProps = { @@ -178,7 +178,7 @@ export function TravelPaymentTrackCard({ travelRequestId }: TravelPaymentTrackCa

对公支付轨道

-

授权、交易、发票绑定与 ERP 支付出站在此统一查看。

+

授权、交易、发票绑定与财务交接导出在此统一查看。

{canManagePayments ? 具备支付管理权限 : null}
@@ -257,7 +257,7 @@ export function TravelPaymentTrackCard({ travelRequestId }: TravelPaymentTrackCa
金额:{order.amount} {order.currency}
付款凭证:{order.paymentVoucherNo ?? "-"}
付款日期:{order.paymentDate ?? "-"} · 渠道:{paymentChannelLabel(order.paymentChannel)}
-
ERP 状态:{erpStageLabel(order)}
+
导出状态:{erpStageLabel(order)}
{canManagePayments ? ( diff --git a/client/src/components/travel/TravelReconciliationTab.tsx b/client/src/components/travel/TravelReconciliationTab.tsx index badb0337..a57d3f9b 100644 --- a/client/src/components/travel/TravelReconciliationTab.tsx +++ b/client/src/components/travel/TravelReconciliationTab.tsx @@ -244,7 +244,7 @@ export function TravelReconciliationTab({ selectedBatchId, onSelectBatch }: Trav }, onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: ['travel-reconciliation', 'erp-track', selectedBatchId] }); - toast.success('ERP 摘要桥已重新入队'); + toast.success('交接摘要已重新入队'); }, onError: (error) => { toast.error(resolveError(error)); @@ -418,7 +418,7 @@ export function TravelReconciliationTab({ selectedBatchId, onSelectBatch }: Trav ) : null} {!selectedBatchId ? ( - + ) : null} {detail ? ( @@ -445,7 +445,7 @@ export function TravelReconciliationTab({ selectedBatchId, onSelectBatch }: Trav ) : null} {track?.permissions.canRetry ? ( ) : null} @@ -473,14 +473,14 @@ export function TravelReconciliationTab({ selectedBatchId, onSelectBatch }: Trav
-

ERP 摘要桥

-

批次级 `erp.travel.export` 轨道,不会回改订单、申请或预算原值。

+

交接摘要

+

批次级摘要导出轨道,不会回改订单、申请或预算原值。

{trackQuery.isLoading ? : null} {trackQuery.isError ? (

- ERP 轨道加载失败:{resolveError(trackQuery.error)} + 摘要轨道加载失败:{resolveError(trackQuery.error)}

) : null} diff --git a/client/src/features/navigation/schema.ts b/client/src/features/navigation/schema.ts index dabe6482..cfabf7e7 100644 --- a/client/src/features/navigation/schema.ts +++ b/client/src/features/navigation/schema.ts @@ -46,7 +46,6 @@ const PAGE_TITLES: Array<{ path: string; title: string; group: string }> = [ { path: "/system/settings", title: "管理", group: "管理" }, { path: "/system/feature-flags", title: "功能管理", group: "管理" }, { path: "/system/cutover", title: "切换控制台", group: "更多" }, - { path: "/system/integrations/erp", title: "外部系统", group: "更多" }, { path: "/dashboard", title: "数据看板", group: "更多" }, { path: "/audit-logs", title: "审计日志", group: "管理" }, ]; diff --git a/client/src/pages/AssetRegistryPage.tsx b/client/src/pages/AssetRegistryPage.tsx index 8e2de483..442e4460 100644 --- a/client/src/pages/AssetRegistryPage.tsx +++ b/client/src/pages/AssetRegistryPage.tsx @@ -10,16 +10,12 @@ import { fetchAssetAssignees, fetchAssetCategories, fetchAssetDetail, - fetchAssetErpTrack, fetchAssets, patchAsset, patchAssetCategory, - requeueAssetErpSync, returnAsset, transferAsset, type AssetCategoryItem, - type AssetErpStage, - type AssetErpTrack, type AssetItem, type AssetSensitivityLevel, type AssetStatus, @@ -30,11 +26,12 @@ import { fetchCostCenters, fetchDocumentCategories, fetchLegalEntities } from ". import { isApiError } from "../api/types"; import { useAuth } from "../auth/AuthContext"; import { hasAnyPermission, hasPermission } from "../auth/permissions"; -import { AiSuggestionCard } from "../components/ai/AiSuggestionCard"; import { DateTextInput } from "../components/common/DateTextInput"; import { StatusBadge } from "../components/common/StatusBadge"; import { TableSkeleton } from "../components/common/TableSkeleton"; +import { DetailSideMeta } from "../components/patterns/DetailSideMeta"; import { FilterBar, ListWorkspace, PrimaryTable, WorkspaceHeader } from "../components/patterns/ListWorkspace"; +import { SectionPanel } from "../components/patterns/SectionPanel"; import { Button } from "../components/ui/button"; import { Input } from "../components/ui/input"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table"; @@ -117,16 +114,6 @@ function assetStatusLabel(value: AssetStatus | string | null | undefined) { return assetStatusOptions().find((item) => item.value === value)?.label ?? "未知状态"; } -function assetErpChangeTypeLabel(value: string | null | undefined) { - const labels: Record = { - created: "新建资产卡", - updated: "资产信息更新", - retired: "资产退役", - archived: "资产归档", - }; - return value ? labels[value] ?? "资产变更" : "-"; -} - function sensitivityOptions(): Array<{ value: AssetSensitivityLevel | ""; label: string }> { return [ { value: "", label: "全部敏感级别" }, @@ -137,38 +124,12 @@ function sensitivityOptions(): Array<{ value: AssetSensitivityLevel | ""; label: ]; } -function typeHint(assetType: AssetType) { - switch (assetType) { - case "electronic_document": - return "电子资料只登记摘要、hash、敏感级别和归档位置;S3/S4 原文和完整路径不上传公网 OA。"; - case "electronic_system": - return "电子系统登记负责人、入口、到期和备份策略;账号密码、token、Secret 不进 OA。"; - case "code_repository": - return "代码仓库继续由 Git 管理,OA 只登记仓库元数据、负责人和备份策略。"; - case "backup_media": - return "备份介质记录保管人、加密状态、备份范围和最近恢复验证时间。"; - default: - return "固定资产支持领用、归还、调拨、盘点和移动扫码。"; - } -} - function isElectronicAssetType(assetType: AssetType) { return assetType !== "fixed_asset"; } -function assetNextStep(item: Pick) { - if (item.status === "archived") return "已归档留存"; - if (item.status === "retired") return "核对处置记录"; - if (item.status === "under_repair") return "跟进维修结果"; - if (item.assetType === "fixed_asset") { - if (!item.assignee) return "待领用或确认保管"; - if (!item.lastVerifiedAt) return "待首次盘点"; - return "按季度盘点"; - } - if (!item.owner) return "补负责人"; - if (!item.lastVerifiedAt) return "待验证责任/备份"; - if (item.expiresAt && item.expiresAt < new Date().toISOString().slice(0, 10)) return "检查到期续费"; - return "按期验证权限"; +function formatEmployeeSummary(employee: { name: string; employeeNo: string } | null | undefined) { + return employee ? `${employee.name} · ${employee.employeeNo}` : "-"; } function CompactAssetNo({ value, copyable = true }: { value: string; copyable?: boolean }) { @@ -231,37 +192,6 @@ function unsafeAssetTextReason(value: string) { return ""; } -function formatAssetErpReason(reason: string | null) { - switch (reason) { - case "erp_sync_disabled": - case "env_hard_off": - return "ERP 总闸当前关闭"; - case "flag_force_off": - return "ERP 功能开关当前强制关闭"; - case "missing_flag_record": - return "ERP 功能开关记录缺失"; - default: - return reason ?? null; - } -} - -function resolveAssetErpTimestamp(stage: AssetErpStage) { - return stage.outbox?.updatedAt ?? stage.outbox?.processedAt ?? stage.binding?.lastSyncedAt ?? null; -} - -function resolveAssetErpActionLabel(actionHint: AssetErpTrack["stage"]["actionHint"]) { - switch (actionHint) { - case "backfill": - return "补发"; - case "retry": - return "重试同步"; - case "resync": - return "重新同步摘要"; - default: - return null; - } -} - export function AssetRegistryPage() { const navigate = useNavigate(); const [searchParams, setSearchParams] = useSearchParams(); @@ -270,7 +200,6 @@ export function AssetRegistryPage() { const canManage = hasPermission(permissions, "asset.manage"); const canTransfer = hasPermission(permissions, "asset.transfer"); - const canIntegrationManage = hasPermission(permissions, "integration.manage"); const canViewAll = hasAnyPermission(permissions, ["asset.view_all", "asset.manage", "asset.transfer", "asset.inventory"]); const canReadProcurementSources = canManage && hasAnyPermission(permissions, ["procurement.view_all", "procurement.manage"]); @@ -324,12 +253,6 @@ export function AssetRegistryPage() { queryFn: () => fetchAssetDetail(selectedAssetId), }); - const assetErpTrackQuery = useQuery({ - enabled: canIntegrationManage && Boolean(selectedAssetId) && !isCreatingAsset, - queryKey: ["assets", "erp-track", selectedAssetId], - queryFn: () => fetchAssetErpTrack(selectedAssetId), - }); - const legalEntitiesQuery = useQuery({ queryKey: ["assets", "legal-entities"], queryFn: () => fetchLegalEntities({ enabled: true }), @@ -442,9 +365,6 @@ export function AssetRegistryPage() { }, [costCentersQuery.data?.items, legalEntityFilter]); const currentAsset = assetDetailQuery.data?.asset ?? null; - const assetErpTrack = assetErpTrackQuery.data ?? null; - const assetErpStage = assetErpTrack?.stage ?? null; - const assetErpActionLabel = assetErpStage ? resolveAssetErpActionLabel(assetErpStage.actionHint) : null; const canEditCurrent = canManage && (isCreatingAsset || currentAsset?.permissions.canEdit); function resetCategoryEditor(category?: AssetCategoryItem) { @@ -612,7 +532,6 @@ export function AssetRegistryPage() { await Promise.all([ queryClient.invalidateQueries({ queryKey: ["assets", "list"] }), queryClient.invalidateQueries({ queryKey: ["assets", "detail"] }), - queryClient.invalidateQueries({ queryKey: ["assets", "erp-track"] }), ]); setAssetFormError(""); setSelectedAssetId(data.asset.id); @@ -627,30 +546,6 @@ export function AssetRegistryPage() { }); - const requeueAssetErpMutation = useMutation({ - mutationFn: async () => { - if (!currentAsset) { - throw new Error("请先选择资产卡片"); - } - return requeueAssetErpSync(currentAsset.id, true); - }, - onSuccess: async (data) => { - await Promise.all([ - queryClient.invalidateQueries({ queryKey: ["assets", "erp-track"] }), - queryClient.invalidateQueries({ queryKey: ["assets", "detail"] }), - ]); - const message = data.duplicated - ? "ERP 摘要已是最新状态" - : data.requeued - ? "资产 ERP 同步已重新入队" - : "资产 ERP 同步已入队"; - toast.success(message); - }, - onError: (error) => { - toast.error(resolveError(error)); - }, - }); - const assignAssetMutation = useMutation({ mutationFn: async () => { if (!currentAsset) { @@ -819,6 +714,11 @@ export function AssetRegistryPage() { ) : ( <> + {canManage ? ( + + ) : null} @@ -835,15 +735,6 @@ export function AssetRegistryPage() {
{showLedgerTools ? (
-
-
台账工具
- {canManage ? ( - - ) : null} -
-

资产分类

{canManage ? ( @@ -1087,58 +978,61 @@ export function AssetRegistryPage() {
暂无资产卡片
)}
- - - - - 资产 - 类型 - 责任链 - 位置 - 状态 - 操作 - - - - {orderedAssetItems.length ? ( - orderedAssetItems.map((item) => ( - { - selectAsset(item.id); - }} className="cursor-pointer"> - -
- -
-
- {formatTextWithCompactLongNumbers(item.name)} -
-
- {assetTypeLabel(item.assetType)} - -
{item.owner?.name ?? item.assignee?.name ?? "-"}
-
保管 {item.keeper?.name ?? "-"}
-
- -
{formatTextWithCompactLongNumbers(item.location ?? "-")}
-
盘点 {item.lastVerifiedAt ? formatDisplayDateTime(item.lastVerifiedAt) : "未登记"}
-
- - - - -
- )) - ) : ( - - - 暂无资产卡片 - - - )} -
-
+ + + + + + + + + + + + + + {orderedAssetItems.length ? ( + orderedAssetItems.map((item) => ( + + + + + + + + + )) + ) : ( + + + + )} + +
资产状态责任人保管/位置盘点操作
+
+ +
+
+ {formatTextWithCompactLongNumbers(item.name)} +
+
{assetTypeLabel(item.assetType)}
+
+ + +
{formatEmployeeSummary(item.owner ?? item.assignee)}
+
归属 {formatEmployeeSummary(item.assignee)}
+
+
{formatEmployeeSummary(item.keeper)}
+
{formatTextWithCompactLongNumbers(item.location ?? "-")}
+
+ {item.lastVerifiedAt ? formatDisplayDateTime(item.lastVerifiedAt) : "未登记"} + + +
+ 暂无资产卡片 +
) : null} @@ -1146,7 +1040,8 @@ export function AssetRegistryPage() {
) : ( -
+
+
{selectedAssetId && !isCreatingAsset && assetDetailQuery.isLoading ? : null} {selectedAssetId && !isCreatingAsset && assetDetailQuery.isError ? (

{resolveError(assetDetailQuery.error)}

@@ -1182,19 +1077,19 @@ export function AssetRegistryPage() {
责任人
-
{currentAsset.owner?.name ?? currentAsset.assignee?.name ?? "未指定"}
+
{formatEmployeeSummary(currentAsset.owner ?? currentAsset.assignee)}
保管/位置
-
{currentAsset.keeper?.name ?? currentAsset.location ?? "待确认"}
+
{currentAsset.keeper ? formatEmployeeSummary(currentAsset.keeper) : currentAsset.location ?? "待确认"}
{isElectronicAssetType(currentAsset.assetType) ? "最近验证" : "最近盘点"}
{currentAsset.lastVerifiedAt ? formatDisplayDateTime(currentAsset.lastVerifiedAt) : "未登记"}
-
下一步
-
{assetNextStep(currentAsset)}
+
状态
+
) : null} @@ -1203,7 +1098,6 @@ export function AssetRegistryPage() {

基础信息

-

维护资产主标识、类型、分类和组织归属。

@@ -1219,7 +1113,6 @@ export function AssetRegistryPage() { ))} -

{typeHint(assetForm.assetType)}

@@ -1300,7 +1193,6 @@ export function AssetRegistryPage() {

责任与归档

-

明确领用、负责、保管、位置和生命周期日期。

@@ -1371,7 +1263,7 @@ export function AssetRegistryPage() {

电子资产安全字段

-

只登记必要摘要,密码、密钥、源码包、原始正文和完整路径不得进入 OA。

+

不填写密码、密钥、源码包、原始正文或完整路径。

@@ -1403,183 +1295,130 @@ export function AssetRegistryPage() {
) : null}
- - - {currentAsset && canTransfer && !isCreatingAsset && currentAsset.assetType === "fixed_asset" ? ( -
-
-
-

资产流转

-

通过受控动作执行领用、归还和调拨,并写入审计记录。

-
-
- 当前归属人:{currentAsset.assignee?.name ?? "未分配"} · 位置:{currentAsset.location ?? "未设置"} -
-
- - {movementError ? ( -

{movementError}

- ) : null} - -
-
- - -
-
- - setMovementLocation(event.target.value)} - /> -
-
- - setMovementNote(event.target.value)} - /> -
-
- -
- - - -
-
- ) : null} - - {selectedAssetId && !isCreatingAsset && canIntegrationManage ? ( -
- -
-
ERP 同步状态
-
高级轨道,默认收起。
-
- 展开 -
-
- {assetErpTrackQuery.isLoading ? ( -

ERP 同步轨道加载中...

- ) : assetErpTrackQuery.isError ? ( -

{resolveError(assetErpTrackQuery.error)}

- ) : assetErpStage ? ( -
-
-
-
- 最近更新时间:{resolveAssetErpTimestamp(assetErpStage) ? formatDisplayDateTime(resolveAssetErpTimestamp(assetErpStage)) : "尚无同步记录"} -
-
变更来源:{assetErpStage.preview.sourceEventName ?? "-"}
-
- -
- -
-
资产编号:{formatTextWithCompactLongNumbers(assetErpTrack?.asset.assetNo ?? currentAsset?.assetNo ?? "-")}
-
最近变更:{assetErpChangeTypeLabel(assetErpStage.preview.changeType)}
-
分类:{formatTextWithCompactLongNumbers(assetErpStage.preview.categoryCode ?? "-")}
-
资产状态:{assetStatusLabel(assetErpStage.preview.status ?? assetErpTrack?.asset.status)}
-
法体:{assetErpStage.preview.legalEntityCode ?? "-"}
-
成本中心:{assetErpStage.preview.costCenterCode ?? "-"}
-
- - {formatAssetErpReason(assetErpStage.reason) ? ( -
{formatAssetErpReason(assetErpStage.reason)}
- ) : null} - - {assetErpStage.outbox?.lastError ? ( -
{assetErpStage.outbox.lastError}
- ) : null} - - {assetErpTrack?.permissions.canSeeExternalRefs && (assetErpStage.binding?.externalCode || assetErpStage.binding?.externalId) ? ( -
- ERP 资产编号:{assetErpStage.binding?.externalCode ?? "-"} - {assetErpStage.binding?.externalId ? ` · ${assetErpStage.binding.externalId}` : ""} -
- ) : null} - - {assetErpTrack?.permissions.canRetry && assetErpActionLabel ? ( -
- -
- ) : null} -
- ) : ( -

暂无 ERP 轨道信息

- )} -
-
- ) : null} - - {selectedAssetId && !isCreatingAsset && currentAsset ? ( - - ) : null} - -
- {canManage ? ( - - ) : null} - {currentAsset?.purchaseRequest ? ( - - 采购来源:{formatTextWithCompactLongNumbers(currentAsset.purchaseRequest.requestNo ?? currentAsset.purchaseRequest.id)} - - ) : null} -
) : null}
+ {(isCreatingAsset || currentAsset) && !assetDetailQuery.isLoading ? ( +
+ +
+ {canManage ? ( + + ) : null} + + {currentAsset && canTransfer && !isCreatingAsset && currentAsset.assetType === "fixed_asset" ? ( +
+
资产流转
+
+ 归属人:{formatEmployeeSummary(currentAsset.assignee)} · 位置:{currentAsset.location ?? "未设置"} +
+ + {movementError ? ( +

{movementError}

+ ) : null} + +
+
+ + +
+
+ + setMovementLocation(event.target.value)} + /> +
+
+ + setMovementNote(event.target.value)} + /> +
+
+ +
+ + + +
+
+ ) : null} +
+
+ + +
状态:{isCreatingAsset ? "新建中" : assetStatusLabel(currentAsset?.status)}
+
类型:{assetTypeLabel(assetForm.assetType)}
+
责任人:{formatEmployeeSummary(currentAsset?.owner ?? currentAsset?.assignee)}
+
归属人:{formatEmployeeSummary(currentAsset?.assignee)}
+
保管人:{formatEmployeeSummary(currentAsset?.keeper)}
+
位置:{(currentAsset?.location ?? assetForm.location) || "-"}
+
+ {isElectronicAssetType(assetForm.assetType) ? "最近验证" : "最近盘点"}: + {currentAsset?.lastVerifiedAt ? formatDisplayDateTime(currentAsset.lastVerifiedAt) : assetForm.lastVerifiedAt || "-"} +
+
+ 采购来源: + {currentAsset?.purchaseRequest + ? formatTextWithCompactLongNumbers(currentAsset.purchaseRequest.requestNo ?? currentAsset.purchaseRequest.id) + : "-"} +
+
+
+ ) : null} +
)} ); diff --git a/client/src/pages/ErpIntegrationPage.tsx b/client/src/pages/ErpIntegrationPage.tsx deleted file mode 100644 index 3c80e621..00000000 --- a/client/src/pages/ErpIntegrationPage.tsx +++ /dev/null @@ -1,686 +0,0 @@ -import { useMemo, useState } from "react"; -import { Link } from "react-router-dom"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { toast } from "sonner"; -import { - fetchErpMasterDataStatus, - fetchIntegrationBindings, - fetchIntegrationOutbox, - fetchIntegrationOutboxDetail, - fetchIntegrationOutboxStats, - retryIntegrationOutbox, - syncErpMasterData, - type ErpMasterDataResourceType, - type ErpMasterDataSyncItem, - type IntegrationBindingItem, - type IntegrationOutboxItem, - type IntegrationOutboxStatus, -} from "../api/integrations"; -import { mapApiErrorMessage } from "../api/errorMessages"; -import { isApiError } from "../api/types"; -import { PageHeader } from "../components/common/PageHeader"; -import { Pagination } from "../components/common/Pagination"; -import { TableSkeleton } from "../components/common/TableSkeleton"; -import { WorkspaceNotice } from "../components/patterns/ListWorkspace"; -import { Button } from "../components/ui/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, -} from "../components/ui/dialog"; -import { Input } from "../components/ui/input"; -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "../components/ui/tabs"; - -const RESOURCE_LABELS: Record = { - legal_entity: "法体", - cost_center: "成本中心", - supplier: "供应商", -}; - -const OUTBOX_STATUS_OPTIONS: Array<{ value: IntegrationOutboxStatus | ""; label: string }> = [ - { value: "", label: "全部状态" }, - { value: "pending", label: "待处理" }, - { value: "processing", label: "处理中" }, - { value: "succeeded", label: "已成功" }, - { value: "failed", label: "失败" }, - { value: "dead_letter", label: "死信" }, -]; - -const BINDING_ENTITY_OPTIONS: Array<{ value: string; label: string }> = [ - { value: "", label: "全部类型" }, - { value: "reimbursement_journal", label: "报销凭证" }, - { value: "purchase_request_commitment_journal", label: "采购 Commitment" }, - { value: "purchase_order_accrual_journal", label: "采购 Accrual" }, - { value: "purchase_order_po", label: "采购 PO 映射" }, - { value: "purchase_order_contract", label: "采购合同映射" }, - { value: "asset_erp_card", label: "资产台账摘要" }, - { value: "travel_payable_summary", label: "差旅应付摘要" }, - { value: "travel_settlement_batch", label: "差旅结算批次" }, -]; - -type ErpTab = "master-data" | "outbox" | "bindings"; - -function resolveError(error: unknown) { - if (isApiError(error)) { - return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`; - } - if (error instanceof Error) { - return error.message; - } - return "请求失败,请稍后重试。"; -} - -function formatDateTime(value: string | null) { - if (!value) return "-"; - return value.slice(0, 19).replace("T", " "); -} - -function formatRelativeTime(value: string | null) { - if (!value) return "-"; - const target = new Date(value).getTime(); - if (!Number.isFinite(target)) return "-"; - const diffSeconds = Math.round((target - Date.now()) / 1000); - const abs = Math.abs(diffSeconds); - if (abs < 60) { - return diffSeconds >= 0 ? `${abs} 秒后` : `${abs} 秒前`; - } - const diffMinutes = Math.round(diffSeconds / 60); - if (Math.abs(diffMinutes) < 60) { - return diffMinutes >= 0 ? `${diffMinutes} 分钟后` : `${Math.abs(diffMinutes)} 分钟前`; - } - const diffHours = Math.round(diffMinutes / 60); - if (Math.abs(diffHours) < 24) { - return diffHours >= 0 ? `${diffHours} 小时后` : `${Math.abs(diffHours)} 小时前`; - } - const diffDays = Math.round(diffHours / 24); - return diffDays >= 0 ? `${diffDays} 天后` : `${Math.abs(diffDays)} 天前`; -} - -function formatGateReason(reason: string) { - if (reason === "env_hard_off") return "Env 总闸关闭"; - if (reason === "flag_force_off") return "功能开关强制关闭"; - if (reason === "department_deferred") return "部门灰度在管理端按全局集成动作放行"; - if (reason === "env_inherit_global_on") return "Env 继承开启"; - if (reason === "flag_force_on_global") return "功能开关强制开启"; - return reason; -} - -function outboxStatusLabel(status: string) { - switch (status) { - case "pending": - return "待处理"; - case "processing": - return "处理中"; - case "succeeded": - return "已成功"; - case "failed": - return "失败"; - case "dead_letter": - return "死信"; - default: - return status; - } -} - -function outboxStatusClassName(status: string) { - if (status === "failed" || status === "dead_letter") { - return "border-red-200 bg-red-50 text-red-700"; - } - if (status === "succeeded") { - return "border-emerald-200 bg-emerald-50 text-emerald-700"; - } - if (status === "processing") { - return "border-amber-200 bg-amber-50 text-amber-700"; - } - return "border-zinc-200 bg-zinc-50 text-zinc-700"; -} - -function renderOutboxSummary(item: IntegrationOutboxItem) { - const summary = item.payloadSummary; - return summary.identityCode ?? summary.entityId ?? "-"; -} - -function bindingEntityLabel(value: string) { - return BINDING_ENTITY_OPTIONS.find((item) => item.value === value)?.label ?? value; -} - -function BindingLocalIdCell({ item }: { item: IntegrationBindingItem }) { - if (!item.routePath) { - return {item.localId}; - } - - return ( - - {item.localId} - - ); -} - -export function ErpIntegrationPage() { - const queryClient = useQueryClient(); - const [tab, setTab] = useState("master-data"); - const [error, setError] = useState(""); - - const [outboxPage, setOutboxPage] = useState(1); - const [outboxStatus, setOutboxStatus] = useState(""); - const [outboxEventType, setOutboxEventType] = useState(""); - const [selectedOutboxId, setSelectedOutboxId] = useState(""); - - const [bindingsPage, setBindingsPage] = useState(1); - const [bindingProvider, setBindingProvider] = useState(""); - const [bindingEntityType, setBindingEntityType] = useState(""); - - const statusQuery = useQuery({ - queryKey: ["erp-master-data-status"], - queryFn: () => fetchErpMasterDataStatus(), - }); - - const outboxStatsQuery = useQuery({ - enabled: tab === "outbox", - queryKey: ["integration-outbox-stats"], - queryFn: () => fetchIntegrationOutboxStats(), - }); - - const outboxQuery = useQuery({ - enabled: tab === "outbox", - queryKey: ["integration-outbox", outboxPage, outboxStatus, outboxEventType], - queryFn: () => - fetchIntegrationOutbox({ - page: outboxPage, - pageSize: 20, - status: outboxStatus || undefined, - eventType: outboxEventType.trim() || undefined, - }), - }); - - const outboxDetailQuery = useQuery({ - enabled: Boolean(selectedOutboxId), - queryKey: ["integration-outbox-detail", selectedOutboxId], - queryFn: () => fetchIntegrationOutboxDetail(selectedOutboxId), - }); - - const bindingsQuery = useQuery({ - enabled: tab === "bindings", - queryKey: ["integration-bindings", bindingsPage, bindingProvider, bindingEntityType], - queryFn: () => - fetchIntegrationBindings({ - page: bindingsPage, - pageSize: 20, - provider: bindingProvider || undefined, - entityType: bindingEntityType || undefined, - }), - }); - - const syncMutation = useMutation({ - mutationFn: async (resources?: ErpMasterDataResourceType[]) => syncErpMasterData({ resources }), - onSuccess: async (result) => { - await queryClient.invalidateQueries({ queryKey: ["erp-master-data-status"] }); - setError(""); - const finished = result.items - .map((item) => `${RESOURCE_LABELS[item.resourceType]}:${item.status}`) - .join(" / "); - toast.success(`主数据同步完成:${finished}`); - }, - onError: (mutationError) => { - const message = resolveError(mutationError); - setError(message); - toast.error(message); - }, - }); - - const retryMutation = useMutation({ - mutationFn: retryIntegrationOutbox, - onSuccess: async () => { - await Promise.all([ - queryClient.invalidateQueries({ queryKey: ["integration-outbox"] }), - queryClient.invalidateQueries({ queryKey: ["integration-outbox-stats"] }), - queryClient.invalidateQueries({ queryKey: ["integration-outbox-detail"] }), - ]); - toast.success("同步任务已重置为待处理"); - }, - onError: (mutationError) => { - const message = resolveError(mutationError); - toast.error(message); - }, - }); - - const itemsByType = useMemo(() => { - const map = new Map(); - for (const item of statusQuery.data?.items ?? []) { - map.set(item.resourceType, item); - } - return map; - }, [statusQuery.data?.items]); - - const gate = statusQuery.data?.gate ?? null; - const outboxTotalPages = Math.max(1, Math.ceil((outboxQuery.data?.total ?? 0) / 20)); - const bindingsTotalPages = Math.max(1, Math.ceil((bindingsQuery.data?.total ?? 0) / 20)); - - return ( -
- syncMutation.mutate(undefined)} disabled={syncMutation.isPending}> - {syncMutation.isPending ? "同步中..." : "同步全部"} - - ) : null - } - /> - - 试运行期间不把 ERP 写入作为主路径;本页用于管理员查看同步状态和队列排障。 - - - {error ? ( -

{error}

- ) : null} - - setTab(value as ErpTab)}> - - 主数据同步 - 同步队列 - ID 映射 - - - -
-
-

ERP 状态

- {statusQuery.isLoading ?

加载中...

: null} - {statusQuery.isError ? ( -

{resolveError(statusQuery.error)}

- ) : null} - {gate ? ( -
-
-
Env 总闸
-
{gate.envEnabled ? "已开启" : "已关闭"}
-
-
-
功能开关
-
{gate.effectiveEnabled ? "允许" : "阻断"}
-
{formatGateReason(gate.effectiveReason)}
-
-
-
同步服务 / 主数据端点
-
{gate.provider}
-
- {gate.masterDataEndpointConfigured ? "主数据端点已配置" : "主数据端点未配置"} -
-
-
-
凭证端点 / 自动同步
-
- {gate.journalEndpointConfigured ? "凭证端点已配置" : "凭证端点未配置"} -
-
{gate.masterDataSyncCron || "未启用自动同步"}
-
-
- ) : null} -
- - {(["legal_entity", "cost_center", "supplier"] as const).map((resourceType) => { - const item = itemsByType.get(resourceType) ?? null; - return ( -
-
-
-

{RESOURCE_LABELS[resourceType]}

-

状态:{item?.status ?? "idle"}

-
- -
- -
-
最近开始:{formatDateTime(item?.lastStartedAt ?? null)}
-
最近完成:{formatDateTime(item?.lastFinishedAt ?? null)}
-
最近成功:{formatDateTime(item?.lastSuccessAt ?? null)}
-
最近错误:{item?.lastError ?? "-"}
-
- - {item?.lastResult ? ( -
-
- 请求 {item.lastResult.requested} / 新增 {item.lastResult.created} / 更新 {item.lastResult.updated} -
-
- 归档 {item.lastResult.archived} / 跳过 {item.lastResult.skipped} -
- {item.lastResult.sampleSkipped.length ? ( -
- 示例跳过: - {item.lastResult.sampleSkipped - .map((sample) => `${sample.key}:${sample.reason}`) - .join(";")} -
- ) : null} -
- ) : ( -

暂无同步记录。

- )} -
- ); - })} -
- -
-

说明

-

- 本页只负责主数据同步、业务同步队列和 ID 映射。

-

- 报销、采购和资产的同步动作在对应业务详情区执行。

-

- 当前仍使用演示同步服务。

-
-
- - -
- {OUTBOX_STATUS_OPTIONS.filter((item) => item.value).map((item) => { - const status = item.value as IntegrationOutboxStatus; - const count = - status === "pending" - ? outboxStatsQuery.data?.pending - : status === "processing" - ? outboxStatsQuery.data?.processing - : status === "succeeded" - ? outboxStatsQuery.data?.succeeded - : status === "failed" - ? outboxStatsQuery.data?.failed - : outboxStatsQuery.data?.deadLetter; - - return ( -
-
{outboxStatusLabel(status)}
-
- {outboxStatsQuery.isLoading ? "-" : count ?? 0} -
-
- ); - })} -
- -
-
-
-

同步任务列表

-

失败任务可重试。

-
-
- - -
-
- - {outboxQuery.isLoading ? : null} - {outboxQuery.isError ? ( -

- {resolveError(outboxQuery.error)} -

- ) : null} - - {outboxQuery.data ? ( - <> - - - - 事件类型 - 摘要标识 - 状态 - 尝试 - 下次重试 - 创建时间 - 处理时间 - 操作 - - - - {outboxQuery.data.items.length ? ( - outboxQuery.data.items.map((item) => ( - - {item.eventType} - -
-
{renderOutboxSummary(item)}
-
- {item.payloadSummary.entityType ?? "-"} - {item.payloadSummary.sourceEventName ? ` · ${item.payloadSummary.sourceEventName}` : ""} -
-
-
- - - {outboxStatusLabel(item.status)} - - - - {item.attempts} / {item.maxAttempts} - - {formatDateTime(item.nextRetryAt)} - {formatDateTime(item.createdAt)} - {formatDateTime(item.processedAt)} - -
- - {item.status === "failed" || item.status === "dead_letter" ? ( - - ) : null} -
-
-
- )) - ) : ( - - - 当前筛选下暂无同步任务 - - - )} -
-
- - - - ) : null} -
-
- - -
-
-
-

IntegrationBinding 列表

-

查看本地单据与 ERP 外部 ID/编号的映射关系,并支持深链回到业务页。

-
-
- - -
-
- - {bindingsQuery.isLoading ? : null} - {bindingsQuery.isError ? ( -

- {resolveError(bindingsQuery.error)} -

- ) : null} - - {bindingsQuery.data ? ( - <> - - - - 同步服务 - 实体类型 - 本地 ID - 外部 ID - 外部编号 - 最后同步 - - - - {bindingsQuery.data.items.length ? ( - bindingsQuery.data.items.map((item) => ( - - {item.provider} - {bindingEntityLabel(item.entityType)} - - - - {item.externalId ?? "-"} - {item.externalCode ?? "-"} - {formatRelativeTime(item.lastSyncedAt)} - - )) - ) : ( - - - 当前筛选下暂无 ID 映射记录 - - - )} - -
- - - - ) : null} -
-
-
- - { - if (!open) { - setSelectedOutboxId(""); - } - }} - > - - - 同步任务详情 - 查看数据内容、错误摘要和当前队列状态。 - - - {outboxDetailQuery.isLoading ?

详情加载中...

: null} - {outboxDetailQuery.isError ? ( -

- {resolveError(outboxDetailQuery.error)} -

- ) : null} - - {outboxDetailQuery.data ? ( -
-
-
队列 ID:{outboxDetailQuery.data.id}
-
事件类型:{outboxDetailQuery.data.eventType}
-
状态:{outboxStatusLabel(outboxDetailQuery.data.status)}
-
尝试次数:{outboxDetailQuery.data.attempts} / {outboxDetailQuery.data.maxAttempts}
-
创建时间:{formatDateTime(outboxDetailQuery.data.createdAt)}
-
处理时间:{formatDateTime(outboxDetailQuery.data.processedAt)}
-
下次重试:{formatDateTime(outboxDetailQuery.data.nextRetryAt)}
-
死信时间:{formatDateTime(outboxDetailQuery.data.deadLetterAt)}
-
- -
-
最近错误
-
- {outboxDetailQuery.data.lastError ?? "-"} -
-
- -
-
数据内容
-
-                  {JSON.stringify(outboxDetailQuery.data.payload ?? null, null, 2)}
-                
-
-
- ) : null} -
-
-
- ); -} diff --git a/client/src/pages/FeatureFlagsPage.tsx b/client/src/pages/FeatureFlagsPage.tsx index 1600697d..8810f3f0 100644 --- a/client/src/pages/FeatureFlagsPage.tsx +++ b/client/src/pages/FeatureFlagsPage.tsx @@ -46,7 +46,6 @@ const ROLLOUT_TYPE_OPTIONS: Array<{ value: FeatureFlagRolloutType; label: string function toDisplayName(flagKey: string) { if (flagKey === "ai_assist") return "AI 辅助"; - if (flagKey === "erp_sync") return "ERP 同步"; if (flagKey === "invoice_verification") return "发票验真"; if (flagKey === "leave_holiday_calendar") return "请假日历增强"; if (flagKey === "budget_precheck_v1") return "预算预校验"; @@ -259,7 +258,7 @@ export function FeatureFlagsPage() { return map; }, [departmentsQuery.data?.items]); - const items = flagsQuery.data?.items ?? []; + const items = (flagsQuery.data?.items ?? []).filter((item) => item.flagKey !== "erp_sync"); const flagStats = { total: items.length, effectiveOn: items.filter((item) => item.effectiveEnabled).length, @@ -325,7 +324,7 @@ export function FeatureFlagsPage() { Env 仍是硬闸;批量开启、关闭或灰度变更会影响试运行边界,执行前必须保留变更说明。 diff --git a/client/src/pages/PurchaseRequestsPage.tsx b/client/src/pages/PurchaseRequestsPage.tsx index e6bd4082..dc92482f 100644 --- a/client/src/pages/PurchaseRequestsPage.tsx +++ b/client/src/pages/PurchaseRequestsPage.tsx @@ -11,19 +11,14 @@ import { deletePurchaseAttachment, fetchPurchaseOrderArchive, fetchPurchaseRequestDetail, - fetchPurchaseRequestErpTrack, fetchPurchaseRequests, issuePurchaseAttachmentSignedUrl, precheckPurchaseRequest, - requeuePurchaseRequestErpSync, submitPurchaseRequest, updatePurchaseRequest, uploadPurchaseAttachment, withdrawPurchaseRequest, - type PurchaseErpBinding, type PurchaseRequestDetail, - type PurchaseRequestErpRequeueTarget, - type PurchaseRequestErpTrack, type PurchaseRequestItemPayload, type PurchaseRequestStatus, } from "../api/purchaseRequests"; @@ -265,106 +260,6 @@ function CompactBusinessCode({ value, copyable = true }: { value: string; copyab ); } -type PurchaseErpStageKey = keyof PurchaseRequestErpTrack["stages"]; - -const PURCHASE_ERP_STAGE_META: Record< - PurchaseErpStageKey, - { title: string; target: PurchaseRequestErpRequeueTarget } -> = { - commitmentJournal: { title: "Commitment", target: "commitment_journal" }, - accrualJournal: { title: "Accrual", target: "accrual_journal" }, - archiveSummary: { title: "PO/合同同步", target: "archive_summary" }, -}; - -function formatStageReason(reason: string | null) { - switch (reason) { - case "purchase_request_not_approved": - return "当前采购单尚未处于已通过状态"; - case "purchase_order_archive_missing": - return "尚未完成 PO/合同归档"; - default: - return reason ?? null; - } -} - -function resolveStageTimestamp(stage: PurchaseRequestErpTrack["stages"][PurchaseErpStageKey]) { - const binding = stage.binding as PurchaseErpBinding | null | undefined; - const poBinding = stage.bindings?.po ?? null; - const contractBinding = stage.bindings?.contract ?? null; - - return ( - stage.outbox?.updatedAt ?? - stage.outbox?.processedAt ?? - binding?.lastSyncedAt ?? - poBinding?.lastSyncedAt ?? - contractBinding?.lastSyncedAt ?? - null - ); -} - -function resolveStageExternalCode(stage: PurchaseRequestErpTrack["stages"][PurchaseErpStageKey]) { - const binding = stage.binding as PurchaseErpBinding | null | undefined; - return binding?.externalCode ?? stage.bindings?.po?.externalCode ?? stage.bindings?.contract?.externalCode ?? null; -} - -function resolveStageExternalId(stage: PurchaseRequestErpTrack["stages"][PurchaseErpStageKey]) { - const binding = stage.binding as PurchaseErpBinding | null | undefined; - return binding?.externalId ?? stage.bindings?.po?.externalId ?? stage.bindings?.contract?.externalId ?? null; -} - -function resolveStageSummary(stageKey: PurchaseErpStageKey, stage: PurchaseRequestErpTrack["stages"][PurchaseErpStageKey]) { - const preview = stage.preview as { - documentNo?: string | null; - requestNo?: string | null; - poNo?: string | null; - contractNo?: string | null; - totalAmount?: string | null; - supplier?: { - code?: string | null; - }; - dimensions?: { - legalEntityCode?: string | null; - costCenterCode?: string | null; - supplierCode?: string | null; - }; - }; - - return { - documentNo: - preview.documentNo ?? - preview.requestNo ?? - (stageKey === "archiveSummary" ? preview.poNo ?? null : null), - totalAmount: preview.totalAmount ?? null, - legalEntityCode: preview.dimensions?.legalEntityCode ?? null, - costCenterCode: preview.dimensions?.costCenterCode ?? null, - supplierCode: preview.dimensions?.supplierCode ?? preview.supplier?.code ?? null, - poNo: preview.poNo ?? null, - contractNo: preview.contractNo ?? null, - }; -} - -function resolveStageAction(stageKey: PurchaseErpStageKey, stage: PurchaseRequestErpTrack["stages"][PurchaseErpStageKey]) { - if (!stage.eligible) { - return null; - } - - if (stage.status === "failed" || stage.status === "dead_letter") { - return { - label: stageKey === "archiveSummary" ? "重试同步摘要" : "重试同步", - target: PURCHASE_ERP_STAGE_META[stageKey].target, - }; - } - - if (stage.status === "not_triggered" || stage.status === "skipped") { - return { - label: stageKey === "archiveSummary" ? "补发摘要" : "补发", - target: PURCHASE_ERP_STAGE_META[stageKey].target, - }; - } - - return null; -} - export function PurchaseRequestsPage() { const queryClient = useQueryClient(); const { me, permissions } = useAuth(); @@ -372,7 +267,6 @@ export function PurchaseRequestsPage() { const canCreate = hasPermission(permissions, "procurement.create"); const canManage = hasPermission(permissions, "procurement.manage"); - const canIntegrationManage = hasPermission(permissions, "integration.manage"); const canViewAll = hasPermission(permissions, "procurement.view_all") || canManage; @@ -402,7 +296,6 @@ export function PurchaseRequestsPage() { const [archiveContractNo, setArchiveContractNo] = useState(""); const [selectedAttachmentName, setSelectedAttachmentName] = useState(""); const [submitPrecheckConfirm, setSubmitPrecheckConfirm] = useState(null); - const [erpRequeueConfirm, setErpRequeueConfirm] = useState<{ target: PurchaseRequestErpRequeueTarget; label: string } | null>(null); const attachmentInputRef = useRef(null); const suppliersQuery = useQuery({ @@ -456,15 +349,6 @@ export function PurchaseRequestsPage() { queryFn: () => fetchPurchaseOrderArchive(selectedId), }); - const erpTrackQuery = useQuery({ - enabled: - canIntegrationManage && - Boolean(selectedId) && - (detailQuery.data?.status === "approved" || Boolean(detailQuery.data?.archiveOrder)), - queryKey: ["purchase-requests", "erp-track", selectedId], - queryFn: () => fetchPurchaseRequestErpTrack(selectedId), - }); - useEffect(() => { const searchSelectedId = searchParams.get("id") ?? ""; if (searchSelectedId && searchSelectedId !== selectedId) { @@ -687,7 +571,6 @@ export function PurchaseRequestsPage() { queryClient.invalidateQueries({ queryKey: ["purchase-requests"] }), queryClient.invalidateQueries({ queryKey: ["purchase-requests", "detail", selectedId] }), queryClient.invalidateQueries({ queryKey: ["workflow", "tasks"] }), - queryClient.invalidateQueries({ queryKey: ["purchase-requests", "erp-track", selectedId] }), ]); setFormError(""); toast.success( @@ -715,7 +598,6 @@ export function PurchaseRequestsPage() { queryClient.invalidateQueries({ queryKey: ["purchase-requests"] }), queryClient.invalidateQueries({ queryKey: ["workflow", "tasks"] }), queryClient.invalidateQueries({ queryKey: ["purchase-requests", "archive-order", selectedId] }), - queryClient.invalidateQueries({ queryKey: ["purchase-requests", "erp-track", selectedId] }), ]); setFormError(""); toast.success("采购申请已撤回到草稿"); @@ -780,7 +662,6 @@ export function PurchaseRequestsPage() { await Promise.all([ queryClient.invalidateQueries({ queryKey: ["purchase-requests", "detail", selectedId] }), queryClient.invalidateQueries({ queryKey: ["purchase-requests", "archive-order", selectedId] }), - queryClient.invalidateQueries({ queryKey: ["purchase-requests", "erp-track", selectedId] }), ]); toast.success("PO/合同归档已保存"); }, @@ -790,39 +671,6 @@ export function PurchaseRequestsPage() { }, }); - const requeueErpMutation = useMutation({ - mutationFn: async (payload: { target: PurchaseRequestErpRequeueTarget; label: string }) => { - if (!selectedId) { - throw new Error("请先选择采购单"); - } - return { - label: payload.label, - response: await requeuePurchaseRequestErpSync(selectedId, { targets: [payload.target] }), - }; - }, - onSuccess: async ({ label, response }) => { - await Promise.all([ - queryClient.invalidateQueries({ queryKey: ["purchase-requests", "detail", selectedId] }), - queryClient.invalidateQueries({ queryKey: ["purchase-requests", "archive-order", selectedId] }), - queryClient.invalidateQueries({ queryKey: ["purchase-requests", "erp-track", selectedId] }), - ]); - const firstItem = response.items[0]; - if (firstItem?.duplicated) { - toast.success(`${label} 已存在待处理记录`); - return; - } - if (firstItem?.requeued) { - toast.success(`${label} 已重新入队`); - return; - } - toast.success(`${label} 已加入同步队列`); - }, - onError: (error) => { - const message = resolveError(error); - toast.error(message); - }, - }); - const uploadMutation = useMutation({ mutationFn: async (file: File) => { if (!selectedId) { @@ -916,8 +764,6 @@ export function PurchaseRequestsPage() { } }; - const erpTrack = erpTrackQuery.data ?? null; - const canRetryErp = Boolean(canIntegrationManage && erpTrack?.permissions.canRetry); const showArchiveSection = Boolean(detail && (detail.status === "approved" || archiveOrder)); const canEditDraft = Boolean(detail?.permissions.canEditDraft); const canSubmit = Boolean(detail?.permissions.canSubmit); @@ -1963,89 +1809,6 @@ export function PurchaseRequestsPage() {
) : null} - {showArchiveSection && canIntegrationManage ? ( - - {erpTrackQuery.isLoading ? ( -

ERP 同步轨道加载中...

- ) : erpTrackQuery.isError ? ( -

{resolveError(erpTrackQuery.error)}

- ) : erpTrack ? ( -
- {(Object.entries(PURCHASE_ERP_STAGE_META) as Array< - [PurchaseErpStageKey, (typeof PURCHASE_ERP_STAGE_META)[PurchaseErpStageKey]] - >).map(([stageKey, meta]) => { - const stage = erpTrack.stages[stageKey]; - const summary = resolveStageSummary(stageKey, stage); - const action = canRetryErp ? resolveStageAction(stageKey, stage) : null; - const stageTime = resolveStageTimestamp(stage); - const externalCode = resolveStageExternalCode(stage); - const externalId = resolveStageExternalId(stage); - const reasonText = formatStageReason(stage.reason); - const isSubmittingAction = requeueErpMutation.isPending && requeueErpMutation.variables?.target === action?.target; - - return ( -
-
-
-
{meta.title}
-
- {stageTime ? `最近更新时间:${formatDisplayDateTime(stageTime)}` : "尚无同步记录"} -
-
- -
- -
-
单号:{summary.documentNo ?? "-"}
-
金额:{summary.totalAmount ?? "-"}
-
法体 / 成本中心:{summary.legalEntityCode ?? "-"} / {summary.costCenterCode ?? "-"}
-
供应商:{summary.supplierCode ?? "-"}
- {stageKey !== "commitmentJournal" ? ( -
PO / 合同:{summary.poNo ?? "-"} / {summary.contractNo ?? "-"}
- ) : null} -
- - {reasonText ? ( -
{reasonText}
- ) : null} - - {stage.outbox?.lastError ? ( -
{stage.outbox.lastError}
- ) : null} - - {externalCode || externalId ? ( -
- ERP 编号:{externalCode ?? "-"} - {externalId ? ` · ${externalId}` : ""} -
- ) : null} - - {action ? ( -
- -
- ) : null} -
- ); - })} -
- ) : ( -

暂无 ERP 轨道信息

- )} -
- ) : null} - {detailApprovalHistory.length ? (
@@ -2083,20 +1846,6 @@ export function PurchaseRequestsPage() { submitMutation.mutate(); }} /> - { - if (!open) setErpRequeueConfirm(null); - }} - kind="retry" - targetName={erpRequeueConfirm?.label} - description="确认后会重新补发或重试 ERP 同步消息,并追加新的同步处理记录。" - onConfirm={async () => { - if (erpRequeueConfirm) { - requeueErpMutation.mutate(erpRequeueConfirm); - } - }} - /> ); } diff --git a/client/src/pages/ReimbursementDetailPage.tsx b/client/src/pages/ReimbursementDetailPage.tsx index fe0daeb9..ce69ff66 100644 --- a/client/src/pages/ReimbursementDetailPage.tsx +++ b/client/src/pages/ReimbursementDetailPage.tsx @@ -5,7 +5,6 @@ import { isApiError } from "../api/types"; import { mapApiErrorMessage } from "../api/errorMessages"; import { fetchExpenseTypes } from "../api/expenseTypes"; import type { ExpenseStandardCheck, PrecheckResponse } from "../api/feeControl"; -import { exportReimbursementJournal, previewReimbursementJournal } from "../api/integrations"; import { fetchProjects } from "../api/projects"; import { createReimbursementItem, @@ -108,32 +107,6 @@ function workflowActionLabel(action: string) { return labels[action] ?? action; } -function resolveErpSyncStatus( - exportState: - | { - exportedAt: string | null; - lastOutboxStatus: string | null; - } - | null, -) { - if (!exportState) { - return "未导出"; - } - if (exportState.lastOutboxStatus === "pending") { - return "待导出"; - } - if (exportState.lastOutboxStatus === "processing") { - return "导出中"; - } - if (exportState.lastOutboxStatus === "failed" || exportState.lastOutboxStatus === "dead_letter") { - return "导出失败"; - } - if (exportState.lastOutboxStatus === "succeeded" || exportState.exportedAt) { - return "已导出"; - } - return "未导出"; -} - function budgetControlModeLabel(controlMode: string | null | undefined) { const labels: Record = { block: "超额阻断", @@ -180,7 +153,6 @@ export function ReimbursementDetailPage() { const [itemError, setItemError] = useState(""); const [confirmDeleteItemId, setConfirmDeleteItemId] = useState(""); const [confirmDeleteReimbursement, setConfirmDeleteReimbursement] = useState(false); - const [isErpPreviewOpen, setIsErpPreviewOpen] = useState(false); const [showSecondaryDetail, setShowSecondaryDetail] = useState(false); const [submitConfirmPrecheck, setSubmitConfirmPrecheck] = useState(null); const [isInvoiceDialogOpen, setIsInvoiceDialogOpen] = useState(false); @@ -215,8 +187,6 @@ export function ReimbursementDetailPage() { queryFn: () => fetchProjects({ mine: true }), }); - const showExternalSyncUi = import.meta.env.VITE_SHOW_ERP_UI === "true"; - const canManageIntegration = hasPermission(permissions, "integration.manage") && showExternalSyncUi; const canSubmitExpense = hasPermission(permissions, "expense.submit"); const canUploadInvoice = hasPermission(permissions, "invoice.upload"); const canViewOwnInvoice = hasPermission(permissions, "invoice.view_own"); @@ -229,12 +199,6 @@ export function ReimbursementDetailPage() { queryFn: () => precheckReimbursement(reimbursementId), }); - const erpPreviewQuery = useQuery({ - enabled: Boolean(reimbursementId) && canManageIntegration && detailQuery.data?.status === "paid", - queryKey: ["reimbursements", reimbursementId, "integration-journal-preview"], - queryFn: () => previewReimbursementJournal(reimbursementId), - }); - const bindableInvoicesQuery = useQuery({ enabled: Boolean(reimbursementId) && @@ -461,41 +425,6 @@ export function ReimbursementDetailPage() { }, }); - const erpExportMutation = useMutation({ - mutationFn: async () => { - if (!reimbursementId) { - throw new Error("missing reimbursementId"); - } - return exportReimbursementJournal(reimbursementId, true); - }, - onSuccess: async (result) => { - await Promise.all([ - queryClient.invalidateQueries({ queryKey: ["reimbursements", reimbursementId, "integration-journal-preview"] }), - queryClient.invalidateQueries({ queryKey: ["reimbursements", reimbursementId] }), - ]); - setActionError(""); - const suffix = result.requeued ? "已重入队" : result.duplicated ? "已复用现有队列" : "已入队"; - toast.success(`财务桥接出队${suffix}`); - }, - onError: (error) => { - if (isApiError(error)) { - const msg = `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`; - setActionError(msg); - toast.error(msg); - return; - } - - if (error instanceof Error) { - setActionError(error.message); - toast.error(error.message); - return; - } - - setActionError("财务桥接出队失败,请稍后重试"); - toast.error("财务桥接出队失败,请稍后重试"); - }, - }); - const bindInvoicesMutation = useMutation({ mutationFn: async (invoiceSelections: Array<{ invoiceId: string; linkedItemId?: string }>) => { if (!reimbursementId) { @@ -809,8 +738,6 @@ export function ReimbursementDetailPage() { paidBy: null, }; const displayCode = resolveReimbursementDisplayCode(detail); - const isPaidReimbursement = detail.status === "paid"; - const erpSyncStatus = resolveErpSyncStatus(erpPreviewQuery.data?.exportState ?? null); const budgetHint = detail.budgetHint; const canPay = hasPermission(permissions, "expense.finance.pay"); const canEditItems = ["draft", "rejected"].includes(detail.status); @@ -1732,72 +1659,6 @@ export function ReimbursementDetailPage() { - - {canManageIntegration ? ( - - - -
- } - > - {!isPaidReimbursement ? ( -

- 未满足导出条件,仅已付款报销可生成财务导出记录。 -

- ) : null} - - {erpPreviewQuery.isLoading ?

正在加载财务凭证预览...

: null} - {erpPreviewQuery.isError ? ( -

- {isApiError(erpPreviewQuery.error) - ? `${erpPreviewQuery.error.code}: ${mapApiErrorMessage( - erpPreviewQuery.error.code, - erpPreviewQuery.error.details, - erpPreviewQuery.error.message, - )}` - : "加载财务凭证预览失败"} -

- ) : null} - -
-
-
导出状态
-
{erpSyncStatus}
-
-
-
最近导出
-
{formatDateTime(erpPreviewQuery.data?.exportState.exportedAt ?? null)}
-
-
-
导出任务
-
{erpPreviewQuery.data?.exportState.lastOutboxId ?? "-"}
-
-
-
外部凭证号
-
{erpPreviewQuery.data?.exportState.externalId ?? "-"}
-
-
- - {erpPreviewQuery.data?.exportState.lastError ? ( -
- 最近错误:{erpPreviewQuery.data.exportState.lastError} -
- ) : null} -
- ) : null}
@@ -2058,41 +1919,6 @@ export function ReimbursementDetailPage() { - - - - 财务凭证预览 - 查看凭证内容和最近导出状态。 - - - {erpPreviewQuery.data ? ( -
-
-

凭证切片

-
类型:{erpPreviewQuery.data.journal.journalType}
-
单号:{erpPreviewQuery.data.journal.documentNo ?? "-"}
-
金额:{erpPreviewQuery.data.journal.totalAmount ?? "-"}
-
法体:{erpPreviewQuery.data.journal.dimensions.legalEntityCode ?? "-"}
-
成本中心:{erpPreviewQuery.data.journal.dimensions.costCenterCode ?? "-"}
-
付款凭证号:{erpPreviewQuery.data.journal.paymentVoucherNo ?? "-"}
-
付款日期:{erpPreviewQuery.data.journal.paymentDate ?? "-"}
-
-
-

导出状态

-
导出状态:{erpSyncStatus}
-
外部凭证号:{erpPreviewQuery.data.exportState.externalId ?? "-"}
-
任务编号:{erpPreviewQuery.data.exportState.lastOutboxId ?? "-"}
-
任务状态:{erpSyncStatus}
-
最近导出:{formatDateTime(erpPreviewQuery.data.exportState.exportedAt)}
-
最近错误:{erpPreviewQuery.data.exportState.lastError ?? "-"}
-
-
- ) : ( -

暂无可预览的财务凭证数据。

- )} -
-
- { diff --git a/client/src/pages/SystemSettingsPage.tsx b/client/src/pages/SystemSettingsPage.tsx index 73f0db16..4e8000ed 100644 --- a/client/src/pages/SystemSettingsPage.tsx +++ b/client/src/pages/SystemSettingsPage.tsx @@ -26,7 +26,6 @@ const ITEMS: ManagementItem[] = [ { to: "/system/master-data", icon: "dataset", title: "主数据", description: "法体、成本中心、供应商等基础数据。", group: "功能管理", anyOf: ["masterdata.view", "masterdata.manage"] }, { to: "/system/notification-channels", icon: "notifications_active", title: "通知渠道", description: "企业微信、邮件等补充通知。", group: "功能管理", anyOf: ["notification.channel.manage"] }, { to: "/system/feature-flags", icon: "toggle_on", title: "功能管理", description: "功能开关、灰度和试运行边界。", group: "功能管理", anyOf: ["system.settings"] }, - { to: "/system/integrations/erp", icon: "hub", title: "外部系统", description: "财务、ERP 和外部系统连接。", group: "功能管理", anyOf: ["integration.manage"] }, ]; const GROUPS: ManagementItem["group"][] = ["人员与组织", "流程与规则", "系统与审计", "功能管理"]; @@ -43,7 +42,7 @@ export function SystemSettingsPage() {
diff --git a/client/src/pages/WorkspaceSetupPage.tsx b/client/src/pages/WorkspaceSetupPage.tsx index c1ec3df9..2ee09d45 100644 --- a/client/src/pages/WorkspaceSetupPage.tsx +++ b/client/src/pages/WorkspaceSetupPage.tsx @@ -42,7 +42,6 @@ const MODULE_LABELS: Record = { invoicePool: "票据", inventoryChecks: "资产盘点", cutover: "切换控制台", - erpIntegrations: "外部系统", profile: "个人设置", }; @@ -83,9 +82,12 @@ export function WorkspaceSetupPage() { const workspace = workspaceQuery.data; const rolePresets = navigationQuery.data?.recommendedRolePresets ?? []; - const moduleEntries = useMemo(() => Object.entries(workspace?.moduleVisibility ?? {}), [workspace?.moduleVisibility]); + const moduleEntries = useMemo( + () => Object.entries(workspace?.moduleVisibility ?? {}).filter(([key]) => key !== "erpIntegrations"), + [workspace?.moduleVisibility], + ); const workspaceStats = useMemo(() => { - const entries = Object.entries(workspace?.moduleVisibility ?? {}); + const entries = Object.entries(workspace?.moduleVisibility ?? {}).filter(([key]) => key !== "erpIntegrations"); return { total: entries.length, main: entries.filter(([, mode]) => mode === "main").length, diff --git a/client/src/router/index.tsx b/client/src/router/index.tsx index d9a56c6d..ee889d08 100644 --- a/client/src/router/index.tsx +++ b/client/src/router/index.tsx @@ -22,7 +22,6 @@ import { EmployeeProfilePage, DelegationsPage, DepartmentsPage, - ErpIntegrationPage, ExpenseStandardsPage, FeatureFlagsPage, InventoryChecksPage, @@ -563,15 +562,6 @@ export function AppRouter() { } /> - - {withRouteFallback()} - - } - /> - import("./AssetRegistryPage")); export const CutoverConsolePage = lazy(() => import("./CutoverConsolePage")); export const DelegationsPage = lazy(() => import("./DelegationsPage")); export const DepartmentsPage = lazy(() => import("./DepartmentsPage")); -export const ErpIntegrationPage = lazy(() => import("./ErpIntegrationPage")); export const ExpenseStandardsPage = lazy(() => import("./ExpenseStandardsPage")); export const FeatureFlagsPage = lazy(() => import("./FeatureFlagsPage")); export const InventoryChecksPage = lazy(() => import("./InventoryChecksPage")); diff --git a/client/tests/e2e/announcements-policy-ack.spec.ts b/client/tests/e2e/announcements-policy-ack.spec.ts index 8211b661..0343bd16 100644 --- a/client/tests/e2e/announcements-policy-ack.spec.ts +++ b/client/tests/e2e/announcements-policy-ack.spec.ts @@ -39,7 +39,7 @@ function buildMe(user: MockUserKey) { return { id: "admin-policy-ack", employeeNo: "A001", - name: "Admin", + name: "王顺利", phone: "mock-phone-admin", email: "admin@example.test", departmentId: "dept-admin", @@ -55,7 +55,7 @@ function buildMe(user: MockUserKey) { return { id: "employee-policy-ack", employeeNo: "E001", - name: "Employee", + name: "陈工程", phone: "mock-phone-employee", email: "employee@example.test", departmentId: "dept-employee", @@ -85,7 +85,7 @@ function mapAnnouncementForUser(item: MockAnnouncement, currentUser: MockUserKey function mapAnnouncementForAdmin(item: MockAnnouncement) { const employees = [ - { id: "employee-policy-ack", employeeNo: "E001", name: "Employee", status: "pending" }, + { id: "employee-policy-ack", employeeNo: "E001", name: "陈工程", status: "pending" }, { id: "employee-policy-ack-2", employeeNo: "E002", name: "员工二", status: "pending" }, ]; const pendingEmployees = item.requiresAcknowledgement diff --git a/client/tests/e2e/announcements-presence.spec.ts b/client/tests/e2e/announcements-presence.spec.ts index 16fbc1fd..b18de512 100644 --- a/client/tests/e2e/announcements-presence.spec.ts +++ b/client/tests/e2e/announcements-presence.spec.ts @@ -6,7 +6,7 @@ import { TEST_IDS } from "./helpers/testIds"; const ADMIN = { identifier: "admin@hyinda.com", password: "AdminPassword1234", - name: "Admin", + name: "王顺利", }; test("E2E 公告与团队状态:公告可见 + 今日状态更新 + Portal 汇总", async ({ page }) => { diff --git a/client/tests/e2e/assets-flow.spec.ts b/client/tests/e2e/assets-flow.spec.ts index 2e4a6da9..0cfefb99 100644 --- a/client/tests/e2e/assets-flow.spec.ts +++ b/client/tests/e2e/assets-flow.spec.ts @@ -2,8 +2,8 @@ import { expect, test, type Locator, type Page } from "@playwright/test"; import { openNavLink } from "./helpers/navigation"; -const FINANCE_MGR = { identifier: "finance_mgr@hyinda.com", password: "FinanceMgrPassword1234", name: "Finance Manager" }; -const ADMINISTRATION = { identifier: "administration@hyinda.com", password: "AdministrationPassword1234", name: "Administration" }; +const FINANCE_MGR = { identifier: "finance_mgr@hyinda.com", password: "FinanceMgrPassword1234", name: "钱主管", employeeNo: "HYD000005" }; +const ADMINISTRATION = { identifier: "administration@hyinda.com", password: "AdministrationPassword1234", name: "周行政", employeeNo: "HYD000008" }; async function login(page: Page, user: { identifier: string; password: string }) { await page.goto("/login"); @@ -130,26 +130,26 @@ test("E2E 资产流转主链路:建卡→领用归还→调拨→盘点", asyn await page.goto(`/assets?id=${assetId}`); await expect(page.getByTestId("asset-name")).toHaveValue(assetName, { timeout: 30_000 }); - await selectOptionByText(page.getByTestId("asset-movement-assignee"), /Employee/); + await selectOptionByText(page.getByTestId("asset-movement-assignee"), /陈工程/); await page.getByTestId("asset-movement-note").fill("Playwright 首次领用"); - if (await page.getByText(/当前归属人:未分配/).isVisible().catch(() => false)) { + if (await page.getByText(/归属人:-/).isVisible().catch(() => false)) { await page.getByTestId("asset-assign-submit").click(); } else { await page.getByTestId("asset-transfer-submit").click(); } - await expect(page.getByText(/当前归属人:Employee/)).toBeVisible({ timeout: 30_000 }); + await expect(page.getByText(/归属人:陈工程/)).toBeVisible({ timeout: 30_000 }); await page.getByTestId("asset-return-submit").click(); - await expect(page.getByText(/当前归属人:未分配/)).toBeVisible({ timeout: 30_000 }); + await expect(page.getByText(/归属人:-/)).toBeVisible({ timeout: 30_000 }); - await selectOptionByText(page.getByTestId("asset-movement-assignee"), /Employee/); + await selectOptionByText(page.getByTestId("asset-movement-assignee"), /陈工程/); await page.getByTestId("asset-assign-submit").click(); - await expect(page.getByText(/当前归属人:Employee/)).toBeVisible({ timeout: 30_000 }); + await expect(page.getByText(/归属人:陈工程/)).toBeVisible({ timeout: 30_000 }); - await selectOptionByText(page.getByTestId("asset-movement-assignee"), /Finance Manager/); + await selectOptionByText(page.getByTestId("asset-movement-assignee"), /钱主管/); await page.getByTestId("asset-movement-location").fill("深圳办公室 B-02"); await page.getByTestId("asset-transfer-submit").click(); - await expect(page.getByText(/当前归属人:Finance Manager/)).toBeVisible({ timeout: 30_000 }); + await expect(page.getByText(/归属人:钱主管/)).toBeVisible({ timeout: 30_000 }); await expect(page.getByText(/位置:深圳办公室 B-02/)).toBeVisible({ timeout: 30_000 }); await logout(page, ADMINISTRATION.name); diff --git a/client/tests/e2e/confidential-boundary.spec.ts b/client/tests/e2e/confidential-boundary.spec.ts index 38dbeea7..4eea6047 100644 --- a/client/tests/e2e/confidential-boundary.spec.ts +++ b/client/tests/e2e/confidential-boundary.spec.ts @@ -5,7 +5,7 @@ import { apiGetData, apiPostData, createAuthenticatedApiClient, login } from "./ const ADMIN = { identifier: "admin@hyinda.com", password: "AdminPassword1234", - name: "Admin", + name: "王顺利", }; type FeatureFlagItem = { diff --git a/client/tests/e2e/helpers/auth.ts b/client/tests/e2e/helpers/auth.ts index e5a5df26..4a0a014b 100644 --- a/client/tests/e2e/helpers/auth.ts +++ b/client/tests/e2e/helpers/auth.ts @@ -8,36 +8,42 @@ export type E2EUser = { identifier: string; password: string; name: string; + employeeNo?: string; }; export const EMPLOYEE: E2EUser = { identifier: "employee@hyinda.com", password: "EmployeePassword1234", - name: "Employee", + name: "陈工程", + employeeNo: "HYD000003", }; export const MANAGER: E2EUser = { identifier: "manager@hyinda.com", password: "ManagerPassword1234", - name: "Manager", + name: "李研发", + employeeNo: "HYD000002", }; export const FINANCE: E2EUser = { identifier: "finance@hyinda.com", password: "FinancePassword1234", - name: "Finance", + name: "赵财务", + employeeNo: "HYD000004", }; export const FINANCE_MGR: E2EUser = { identifier: "finance_mgr@hyinda.com", password: "FinanceMgrPassword1234", - name: "Finance Manager", + name: "钱主管", + employeeNo: "HYD000005", }; export const ADMIN: E2EUser = { identifier: "admin@hyinda.com", password: "AdminPassword1234", - name: "Admin", + name: "王顺利", + employeeNo: "HYD000006", }; export type MeResponse = { diff --git a/client/tests/e2e/leave-team-calendar.spec.ts b/client/tests/e2e/leave-team-calendar.spec.ts index 117f24aa..299a847f 100644 --- a/client/tests/e2e/leave-team-calendar.spec.ts +++ b/client/tests/e2e/leave-team-calendar.spec.ts @@ -5,7 +5,7 @@ import { EMPLOYEE, MANAGER, login, type E2EUser } from "./helpers/auth"; const LEAVE_CALENDAR_HR: E2EUser = { identifier: "hr@hyinda.com", password: "HrPassword1234", - name: "HR", + name: "孙人事", }; function envelope(data: T) { diff --git a/client/tests/e2e/login-portal-ux.spec.ts b/client/tests/e2e/login-portal-ux.spec.ts index a17170ab..acfc5566 100644 --- a/client/tests/e2e/login-portal-ux.spec.ts +++ b/client/tests/e2e/login-portal-ux.spec.ts @@ -5,8 +5,8 @@ import { TEST_IDS } from "./helpers/testIds"; test.use({ serviceWorkers: "block" }); -const ADMIN = { identifier: "admin@hyinda.com", password: "AdminPassword1234", name: "Admin" }; -const MANAGER = { identifier: "manager@hyinda.com", password: "ManagerPassword1234", name: "Manager" }; +const ADMIN = { identifier: "admin@hyinda.com", password: "AdminPassword1234", name: "王顺利", employeeNo: "HYD000006" }; +const MANAGER = { identifier: "manager@hyinda.com", password: "ManagerPassword1234", name: "李研发", employeeNo: "HYD000002" }; function envelope(data: unknown) { return { @@ -148,16 +148,23 @@ function mockNotificationList(page: import("@playwright/test").Page) { } function mockMe(page: import("@playwright/test").Page, roleCodes: string[], name: string) { + const employeeNoByName: Record = { + 王顺利: "HYD000006", + 李研发: "HYD000002", + 陈工程: "HYD000003", + 周行政: "HYD000008", + }; + const employeeNo = employeeNoByName[name] ?? "HYD000003"; return page.route("**/api/me", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(envelope({ id: "employee-mock-1", - employeeNo: "E001", + employeeNo, name, phone: "13800000000", - email: `${name.toLowerCase()}@hyinda.com`, + email: `mock-${employeeNo.toLowerCase()}@hyinda.com`, departmentId: "dept-1", directManagerId: null, status: "active", @@ -260,7 +267,7 @@ function mockPortalHome(page: import("@playwright/test").Page, mode: "employee" const data = mode === "manager" ? { workspace: { id: "workspace-1", persisted: true, profile: "small_team_internal" }, - greeting: { name: "Manager" }, + greeting: { name: "李研发" }, cards: [ { key: "pending_approvals", title: "待我处理", value: 3, href: "/workflow/tasks", severity: "warning" }, { key: "team_presence", title: "今日团队状态", value: "6/8 在岗", href: "/team", severity: "warning" }, @@ -298,7 +305,7 @@ function mockPortalHome(page: import("@playwright/test").Page, mode: "employee" } : { workspace: { id: "workspace-1", persisted: true, profile: "small_team_internal" }, - greeting: { name: "Employee" }, + greeting: { name: "陈工程" }, cards: [ { key: "my_todos", title: "我的待办", value: 4, href: "/todos", severity: "warning" }, { key: "today_presence", title: "今日状态", value: "在岗", href: "/team", severity: "normal" }, @@ -325,10 +332,10 @@ function mockPortalHome(page: import("@playwright/test").Page, mode: "employee" peopleLifecycle: null, adoptionSummary: null, recentActivities: [ - { id: "activity-1", type: "leave.submit", actorName: "Employee", summary: "Employee 提交了请假单", createdAt: "2026-03-22T09:00:00.000Z", href: "/leave-requests" }, + { id: "activity-1", type: "leave.submit", actorName: "陈工程", summary: "陈工程 提交了请假单", createdAt: "2026-03-22T09:00:00.000Z", href: "/leave-requests" }, ], teamStatusBoard: [ - { employeeId: "employee-1", name: "Employee", avatarUrl: null, presenceStatus: "office", attendanceClockIn: "2026-03-22T00:55:00.000Z", attendanceClockOut: null, onLeave: false, leaveType: null }, + { employeeId: "employee-1", name: "陈工程", avatarUrl: null, presenceStatus: "office", attendanceClockIn: "2026-03-22T00:55:00.000Z", attendanceClockOut: null, onLeave: false, leaveType: null }, { employeeId: "employee-2", name: "同事", avatarUrl: null, presenceStatus: "remote", attendanceClockIn: null, attendanceClockOut: null, onLeave: false, leaveType: null }, ], }; @@ -386,7 +393,7 @@ test("邮箱验证码登录可发送验证码并进入工作台", async ({ page const verifyPayloads: unknown[] = []; await mockGuestAuthChrome(page); - await mockMe(page, ["employee"], "Employee"); + await mockMe(page, ["employee"], "陈工程"); await page.route("**/api/auth/email-login/code", async (route) => { codeRequests.push(route.request().postDataJSON()); @@ -507,11 +514,11 @@ test("Portal 员工视角优先显示今日动作顺序", async ({ page }) => { await mockPortalHome(page, "employee"); await mockAttendanceToday(page); await mockNotificationList(page); - await mockMe(page, ["employee"], "Employee"); + await mockMe(page, ["employee"], "陈工程"); await login(page, EMPLOYEE); - await expect(page.getByTestId("portal-work-summary")).toContainText("今天有 7 件待处理"); + await expect(page.getByTestId("portal-work-summary")).toContainText("待处理 7"); await expect(page.getByRole("heading", { name: "今日待处理" })).toBeVisible(); await expect(page.getByRole("link", { name: /待办事项 4 条.*查看待办/ })).toBeVisible(); await expect(page.getByRole("link", { name: /本周周报待填写.*填写周报/ })).toBeVisible(); @@ -527,7 +534,7 @@ test("Portal 管理视角按角色分发待处理和团队卡点", async ({ page await mockPortalHome(page, "manager"); await mockAttendanceToday(page); await mockNotificationList(page); - await mockMe(page, ["manager"], "Manager"); + await mockMe(page, ["manager"], "李研发"); await login(page, MANAGER); @@ -552,7 +559,7 @@ test("Portal 新员工只显示轻量欢迎入口,不再显示快速上手清 await mockPortalHome(page, "employee"); await mockAttendanceToday(page); await mockNotificationList(page); - await mockMe(page, ["employee"], "Employee"); + await mockMe(page, ["employee"], "陈工程"); await login(page, EMPLOYEE); @@ -584,7 +591,7 @@ test("个人设置默认只展示账号通知和企业微信,不展示权限 c await mockPortalHome(page, "employee"); await mockAttendanceToday(page); await mockNotificationList(page); - await mockMe(page, ["employee"], "Employee"); + await mockMe(page, ["employee"], "陈工程"); await login(page, EMPLOYEE); await page.goto("/settings/profile"); @@ -646,7 +653,7 @@ test("Portal bootstrap refresh 首次瞬时失败后重试成功仍保留登录 await mockPortalHome(page, "employee"); await mockAttendanceToday(page); await mockNotificationList(page); - await mockMe(page, ["employee"], "Employee"); + await mockMe(page, ["employee"], "陈工程"); await page.goto("/portal"); @@ -661,7 +668,7 @@ test("Portal 域管理角色也能看到团队卡点", async ({ page }) => { await mockPortalHome(page, "manager"); await mockAttendanceToday(page); await mockNotificationList(page); - await mockMe(page, ["administration"], "Administration"); + await mockMe(page, ["administration"], "周行政"); await login(page, EMPLOYEE); @@ -676,7 +683,7 @@ test("Portal 管理提醒正常时不显示空壳", async ({ page }) => { await mockPortalHome(page, "manager"); await mockAttendanceToday(page); await mockNotificationList(page); - await mockMe(page, ["admin"], "Admin"); + await mockMe(page, ["admin"], "王顺利"); await login(page, ADMIN); @@ -697,7 +704,7 @@ test("企微 WebView 下默认进入移动工作台", async ({ page }) => { await mockPortalHome(page, "employee"); await mockAttendanceToday(page); await mockNotificationList(page); - await mockMe(page, ["employee"], "Employee"); + await mockMe(page, ["employee"], "陈工程"); await page.goto("/login"); await page.getByTestId(TEST_IDS.login.identifier).fill(EMPLOYEE.identifier); diff --git a/client/tests/e2e/project-workbench.spec.ts b/client/tests/e2e/project-workbench.spec.ts index 5adcc5eb..10f0cc71 100644 --- a/client/tests/e2e/project-workbench.spec.ts +++ b/client/tests/e2e/project-workbench.spec.ts @@ -5,7 +5,7 @@ import { apiGetData, apiPostData, createAuthenticatedApiClient, login } from "./ const ADMIN = { identifier: "admin@hyinda.com", password: "AdminPassword1234", - name: "Admin", + name: "王顺利", }; type FeatureFlagItem = { diff --git a/client/tests/e2e/r100-design-baseline.spec.ts b/client/tests/e2e/r100-design-baseline.spec.ts index 5e7ad21b..262787bf 100644 --- a/client/tests/e2e/r100-design-baseline.spec.ts +++ b/client/tests/e2e/r100-design-baseline.spec.ts @@ -15,7 +15,7 @@ async function fulfillJson(route: Route, data: unknown, status = 200) { const me = { id: "employee-r100", employeeNo: "E100", - name: "Employee", + name: "陈工程", phone: "13800000000", email: "employee@hyinda.com", departmentId: "dept-r100", diff --git a/client/tests/e2e/r101-mobile-wip-closeout.spec.ts b/client/tests/e2e/r101-mobile-wip-closeout.spec.ts index 1b5f05e4..25c8a90e 100644 --- a/client/tests/e2e/r101-mobile-wip-closeout.spec.ts +++ b/client/tests/e2e/r101-mobile-wip-closeout.spec.ts @@ -15,7 +15,7 @@ async function fulfillJson(route: Route, data: unknown, status = 200) { const me = { id: "employee-r101", employeeNo: "E101", - name: "Employee", + name: "陈工程", phone: "13800000000", email: "employee@hyinda.com", departmentId: "dept-r101", diff --git a/client/tests/e2e/r102-reimbursement-main-flow.spec.ts b/client/tests/e2e/r102-reimbursement-main-flow.spec.ts index 02b82cd1..57341693 100644 --- a/client/tests/e2e/r102-reimbursement-main-flow.spec.ts +++ b/client/tests/e2e/r102-reimbursement-main-flow.spec.ts @@ -15,7 +15,7 @@ async function fulfillJson(route: Route, data: unknown, status = 200) { const me = { id: "employee-r102", employeeNo: "E102", - name: "Employee", + name: "陈工程", phone: "13800000000", email: "employee@hyinda.com", departmentId: "dept-r102", diff --git a/client/tests/e2e/r103-cross-page-ui-consistency.spec.ts b/client/tests/e2e/r103-cross-page-ui-consistency.spec.ts index faf6a5b7..e49a5c52 100644 --- a/client/tests/e2e/r103-cross-page-ui-consistency.spec.ts +++ b/client/tests/e2e/r103-cross-page-ui-consistency.spec.ts @@ -23,8 +23,8 @@ function screenshotPath(fileName: string) { const me = { id: "employee-r103", - employeeNo: "E103", - name: "Employee", + employeeNo: "HYD000103", + name: "陈工程", phone: "13800000000", email: "employee@hyinda.com", departmentId: "dept-r103", diff --git a/client/tests/e2e/r89-mobile-login-navigation.spec.ts b/client/tests/e2e/r89-mobile-login-navigation.spec.ts index e78366f5..fffe8efa 100644 --- a/client/tests/e2e/r89-mobile-login-navigation.spec.ts +++ b/client/tests/e2e/r89-mobile-login-navigation.spec.ts @@ -37,8 +37,8 @@ async function attachScreenshot(page: Page, testInfo: TestInfo, name: string) { const mockMe = { id: "employee-mock-1", - employeeNo: "M001", - name: "Manager", + employeeNo: "HYD000002", + name: "李研发", phone: "13800000000", email: "manager@hyinda.com", departmentId: "dept-1", @@ -105,8 +105,8 @@ const pendingTaskDetail = { reimbursementDate: "2026-05-05", totalAmount: "1683.00", description: "R89 移动审批验证", - owner: { id: "employee-mock-2", employeeNo: "E002", name: "Employee" }, - submitter: { id: "employee-mock-2", employeeNo: "E002", name: "Employee" }, + owner: { id: "employee-mock-2", employeeNo: "HYD000003", name: "陈工程" }, + submitter: { id: "employee-mock-2", employeeNo: "HYD000003", name: "陈工程" }, }, leaveRequest: null, purchaseRequest: null, diff --git a/client/tests/e2e/r92-mobile-invoice-usability.spec.ts b/client/tests/e2e/r92-mobile-invoice-usability.spec.ts index 79f1738e..180aff8f 100644 --- a/client/tests/e2e/r92-mobile-invoice-usability.spec.ts +++ b/client/tests/e2e/r92-mobile-invoice-usability.spec.ts @@ -60,8 +60,8 @@ async function mockAuthenticatedShell(page: Page) { await page.route((url) => url.pathname === "/api/me", (route) => fulfillJson(route, { id: "employee-mock-1", - employeeNo: "E001", - name: "Smoke Employee", + employeeNo: "HYD000003", + name: "陈工程", phone: "13800000000", email: "smoke@hyinda.com", departmentId: "dept-1", @@ -113,7 +113,7 @@ async function mockAuthenticatedShell(page: Page) { await page.route((url) => url.pathname === "/api/portal/home", (route) => fulfillJson(route, { workspace: { id: "workspace-1", persisted: true, profile: "small_team_internal" }, - greeting: { name: "Smoke Employee" }, + greeting: { name: "陈工程" }, cards: [], pendingTasks: { count: 0, href: "/todos" }, myApplications: [], @@ -149,8 +149,8 @@ function reimbursementListItem() { paidAt: null, paidBy: null, }, - owner: { id: "employee-mock-1", employeeNo: "E001", name: "Smoke Employee" }, - submitter: { id: "employee-mock-1", employeeNo: "E001", name: "Smoke Employee" }, + owner: { id: "employee-mock-1", employeeNo: "HYD000003", name: "陈工程" }, + submitter: { id: "employee-mock-1", employeeNo: "HYD000003", name: "陈工程" }, project: null, pendingWorkflow: null, createdAt: null, @@ -197,8 +197,8 @@ const invoiceListItem = { mimeType: "application/pdf", fileSize: 128, verification: null, - owner: { id: "employee-mock-1", employeeNo: "E001", name: "Smoke Employee" }, - uploadedBy: { id: "employee-mock-1", employeeNo: "E001", name: "Smoke Employee" }, + owner: { id: "employee-mock-1", employeeNo: "HYD000003", name: "陈工程" }, + uploadedBy: { id: "employee-mock-1", employeeNo: "HYD000003", name: "陈工程" }, createdAt: null, updatedAt: null, }; diff --git a/client/tests/e2e/visual-portal.spec.ts b/client/tests/e2e/visual-portal.spec.ts index 78e1e18c..f3b54546 100644 --- a/client/tests/e2e/visual-portal.spec.ts +++ b/client/tests/e2e/visual-portal.spec.ts @@ -7,7 +7,8 @@ import { EMPLOYEE, FINANCE, login, type E2EUser } from "./helpers/auth"; const ADMIN: E2EUser = { identifier: "admin@hyinda.com", password: "AdminPassword1234", - name: "Admin", + name: "王顺利", + employeeNo: "HYD000006", }; const SCREENSHOT_DIR = path.resolve(process.cwd(), "../output/playwright/portal-task-first-redesign"); @@ -25,9 +26,10 @@ function envelope(data: T) { } function portalPayload(mode: "employee" | "admin" | "finance") { + const displayName = mode === "employee" ? "陈工程" : mode === "finance" ? "赵财务" : "王顺利"; const base = { workspace: { id: "workspace-visual", persisted: true, profile: "small_team_internal" }, - greeting: { name: mode === "employee" ? "Employee" : mode === "finance" ? "Finance" : "Admin" }, + greeting: { name: displayName }, myApplications: [{ domain: "reimbursement", count: 1, href: "/reimbursements" }], riskSummary: null, announcements: [], @@ -134,13 +136,15 @@ async function mockVisualPortal(page: Page, mode: "employee" | "admin" | "financ return; } if (pathName === "/api/me") { + const displayName = mode === "employee" ? "陈工程" : mode === "finance" ? "赵财务" : "王顺利"; + const employeeNo = mode === "employee" ? "HYD000003" : mode === "finance" ? "HYD000004" : "HYD000006"; await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(envelope({ id: `visual-${mode}`, - employeeNo: "VISUAL", - name: mode === "employee" ? "Employee" : mode === "finance" ? "Finance" : "Admin", + employeeNo, + name: displayName, email: `${mode}@hyinda.com`, roleCodes: mode === "employee" ? ["employee"] : mode === "finance" ? ["finance"] : ["admin"], notificationPushEnabled: true, diff --git a/client/tests/e2e/visual-shell.spec.ts b/client/tests/e2e/visual-shell.spec.ts index b5d77e2f..aaa657e1 100644 --- a/client/tests/e2e/visual-shell.spec.ts +++ b/client/tests/e2e/visual-shell.spec.ts @@ -19,7 +19,7 @@ async function fulfillJson(route: Route, data: unknown, status = 200) { const visualMe = { id: "employee-r96", employeeNo: "R96001", - name: "Employee", + name: "陈工程", phone: "13800000000", email: "employee@hyinda.com", departmentId: "dept-r96", @@ -261,12 +261,12 @@ async function mockVisualApis(page: Page) { owner: { id: "employee-r96", employeeNo: "R96001", - name: "Employee", + name: "陈工程", }, submitter: { id: "employee-r96", employeeNo: "R96001", - name: "Employee", + name: "陈工程", }, project: null, pendingWorkflow: { @@ -300,12 +300,12 @@ async function mockVisualApis(page: Page) { owner: { id: "employee-r96", employeeNo: "R96001", - name: "Employee", + name: "陈工程", }, submitter: { id: "employee-r96", employeeNo: "R96001", - name: "Employee", + name: "陈工程", }, project: null, pendingWorkflow: null, diff --git a/client/tests/e2e/workflow-action-confirm-ui.spec.ts b/client/tests/e2e/workflow-action-confirm-ui.spec.ts index 69f663f9..f84764b6 100644 --- a/client/tests/e2e/workflow-action-confirm-ui.spec.ts +++ b/client/tests/e2e/workflow-action-confirm-ui.spec.ts @@ -21,7 +21,7 @@ async function fulfillJson(route: Route, data: T) { const mockMe = { id: "employee-confirm-manager", employeeNo: "M-confirm", - name: "Manager", + name: "李研发", email: "manager@hyinda.com", phone: null, roleCodes: ["manager"], @@ -98,7 +98,7 @@ const pendingTaskDetail = { reason: "R7E 审批动作确认", attachment: null, leaveType: { id: "annual", code: "annual", name: "年假" }, - owner: { id: "employee-confirm-applicant", employeeNo: "E-confirm", name: "Employee" }, + owner: { id: "employee-confirm-applicant", employeeNo: "E-confirm", name: "陈工程" }, }, purchaseRequest: null, travelRequest: null, diff --git a/client/tests/e2e/workflow-quick-approve.spec.ts b/client/tests/e2e/workflow-quick-approve.spec.ts index fa258474..13a2052e 100644 --- a/client/tests/e2e/workflow-quick-approve.spec.ts +++ b/client/tests/e2e/workflow-quick-approve.spec.ts @@ -21,7 +21,7 @@ async function fulfillJson(route: Route, data: T) { const manager = { id: "manager-wecom-boundary", employeeNo: "M-WECOM", - name: "Manager", + name: "李研发", email: "manager@hyinda.com", phone: null, roleCodes: ["manager"], diff --git a/docs/产品设计规则.md b/docs/产品设计规则.md index 5000eb93..e69135e3 100644 --- a/docs/产品设计规则.md +++ b/docs/产品设计规则.md @@ -54,11 +54,11 @@ A 类页面优先保证真实试运行路径顺畅;B 类页面优先保证台 合同台账 V1 只做登记、归档、到期/续签提醒,以及关联项目、采购和付款。合同页面不做完整合同生命周期管理,不自建电子签、复杂法务审查、条款库、版本协同或 CLM;如需合同审批,走 OA 统一审批中心或另建后续 Trellis task,合同台账页只展示状态、摘要和关联入口。 -采购台账 V1 只做申请、审批、附件、轻量验收/归档,以及关联合同、项目和付款/AP。采购页面不做完整采购 ERP,不承接采购订单全生命周期、库存、三单匹配、供应商生命周期管理、招采评标或 ERP 入账;审批通过不代表已付款、已入库或已形成固定资产。 +采购台账 V1 只做申请、审批、附件、轻量验收/归档,以及关联合同、项目和付款/AP。采购页面不承接采购订单全生命周期、库存、三单匹配、供应商生命周期管理、招采评标或自动入账;审批通过不代表已付款、已入库或已形成固定资产。 项目模块 V1 只做轻项目台账:项目状态、负责人/成员、里程碑/任务、风险、交付记录,以及关联报销、采购、合同和付款。项目页面不做完整项目管理软件,不承接甘特图、复杂任务依赖、工时、资源排期、OKR、项目组合管理或大型协同平台;项目页主要负责业务归集、风险提醒和跳转到对应业务详情。 -财务台账 V1 只做付款准备、付款批次、回单留痕、AP 状态、票据/OCR 状态和代账交接摘要。付款、应付账款和票据页面不做完整财务软件,不承接银行直连真实支付、银行流水自动导入、ERP/总账、凭证生成、自动对账或税务申报;真实付款继续在线下银行或既有财务/代账流程完成,OA 只记录状态、证据和交接摘要。 +财务台账 V1 只做付款准备、付款批次、回单留痕、AP 状态、票据/OCR 状态和代账交接摘要。付款、应付账款和票据页面不做完整财务软件,不承接银行直连真实支付、银行流水自动导入、总账、凭证生成、自动对账或税务申报;真实付款继续在线下银行或既有财务/代账流程完成,OA 只记录状态、证据和交接摘要。OA 不接 ERP,未来也不规划 ERP 接入。 付款相关动作集中在 `付款` / 财务处理详情执行。报销、采购、合同、项目等列表和普通详情只显示付款状态、回单状态、`查看付款`、`补回单` 或进入付款记录入口,不直接放 `标记已付款`、`取消付款`、`批量付款`、`执行付款`;付款动作前必须核对金额、对象、线下付款说明、证据和回单。 @@ -80,7 +80,7 @@ A 类页面优先保证真实试运行路径顺畅;B 类页面优先保证台 危险动作必须二次确认且展示对象和后果。删除、付款、发布、停用、作废、驳回、退回、切换配置、批量处理等不可逆或高风险动作,确认面板必须包含对象摘要、后果、影响范围、必要理由输入或确认输入,并写入审计;不允许只弹一句 `确定吗?`。 -集成和外部系统状态必须保守表达。OA 主办报销、采购、合同、项目、付款、AP、票据、资产和高敏附件;企业微信主办打卡、请假、补卡、外出、加班等 HR/考勤日常动作;ERP、银行、代账、OCR、企业微信消息等外部系统只展示真实同步状态、失败原因和下一步,不在界面上承诺无法确认的 `已同步`、`已付款` 或 `已审批`。 +集成和外部系统状态必须保守表达。OA 主办报销、采购、合同、项目、付款、AP、票据、资产和高敏附件;企业微信主办打卡、请假、补卡、外出、加班等 HR/考勤日常动作;银行、代账、OCR、企业微信消息等外部能力只展示真实同步状态、失败原因和下一步,不在界面上承诺无法确认的 `已同步`、`已付款` 或 `已审批`。ERP 不作为当前或未来外部能力。 试运行和验收口径必须准确。本地构建、Playwright 和截图通过只代表本地验证通过;只有完成部署、公网访问检查、关键路径冒烟和必要截图证据后,才可告知用户 `可以线上测试`。未部署时只能说明 `本地验证通过,尚未线上可测`。 @@ -129,8 +129,8 @@ A 类页面优先保证真实试运行路径顺畅;B 类页面优先保证台 - PC 请假入口默认表达为 `请假记录`;提交请假、补卡、外出、加班仍优先走企业微信或移动入口,只有 OA 应急接管或指定管理场景才突出提交入口。 - 移动请假默认是记录查看和状态确认;普通员工不显示 OA 新建请假表单、编辑和提交动作。HR/管理员应急接管时才显示 `OA 应急` 表单和 `提交应急审批`。 - 考勤页普通员工只看月度摘要、异常解释、同步/留痕状态和企微处理说明;`OA 应急修正` 只给 HR/管理员处理企微不可用时的应急接管。 -- 管理配置类能力在侧边栏合并为 `管理`,具体人员、组织、流程、规则、审计、功能开关和外部系统入口进入管理页后分组承接。 -- 财务入口应使用真实工作名,例如 `付款`、`应付账款`、`票据`;`AP 运营`、`ERP 集成`、`功能开关` 等内部平台词不作为默认一级菜单文案。 +- 管理配置类能力在侧边栏合并为 `管理`,具体人员、组织、流程、规则、审计、功能开关和真实已启用外部能力进入管理页后分组承接。 +- 财务入口应使用真实工作名,例如 `付款`、`应付账款`、`票据`;`AP 运营`、`功能开关` 等内部平台词不作为默认一级菜单文案,`ERP 集成` 不作为产品入口。 - `更多` 只承载低频、冻结、阶段性或偶尔查看的入口,不使用说明型副标题、教学卡片或历史模块解释文案;不得把常用业务台账作为默认二级入口。 ## 业务体验事实 diff --git a/server/src/scripts/export-small-team-observation.js b/server/src/scripts/export-small-team-observation.js index da2a4061..0dc936a8 100644 --- a/server/src/scripts/export-small-team-observation.js +++ b/server/src/scripts/export-small-team-observation.js @@ -158,7 +158,7 @@ function renderMarkdown(report) { lines.push("- 阻塞 3:"); lines.push(""); lines.push("## 下周调整"); - lines.push("- 是否继续隐藏考勤、采购、差旅、预算、AP、ERP、AI 等备用模块:"); + lines.push("- 是否继续隐藏考勤、采购、差旅、预算、AP、AI 等备用模块:"); lines.push("- 是否需要补充培训、公告提醒或管理员代配置:"); lines.push("- 是否达到试点预期:是 / 否"); return `${lines.join("\n")}\n`; diff --git a/server/src/scripts/seed-config.js b/server/src/scripts/seed-config.js index 75ed6615..38a4c29d 100644 --- a/server/src/scripts/seed-config.js +++ b/server/src/scripts/seed-config.js @@ -255,13 +255,6 @@ const DEFAULT_FEATURE_FLAGS = [ department_ids: [], note: "bootstrap default: force_off", }, - { - flag_key: "erp_sync", - override_mode: "force_off", - rollout_type: "global", - department_ids: [], - note: "bootstrap default: force_off", - }, { flag_key: "invoice_verification", override_mode: "force_off", diff --git a/server/src/scripts/seed-dev.js b/server/src/scripts/seed-dev.js index 1cd59111..fe17a678 100644 --- a/server/src/scripts/seed-dev.js +++ b/server/src/scripts/seed-dev.js @@ -317,7 +317,6 @@ async function ensureWorkspaceSetting(prisma) { paymentBatches: "admin", apOperations: "admin", cutover: "admin", - erpIntegrations: "admin", }, }; @@ -344,7 +343,6 @@ async function ensureEmployee(prisma, payload) { const employee = await prisma.employee.update({ where: { id: existing.id }, data: { - employee_no: payload.employeeNo, name: payload.name, phone: payload.phone, department_id: payload.departmentId, @@ -480,8 +478,8 @@ async function main() { } const ceo = await ensureEmployee(prisma, { - employeeNo: process.env.SEED_CEO_EMPLOYEE_NO ?? "E0001", - name: "CEO", + employeeNo: process.env.SEED_CEO_EMPLOYEE_NO ?? "HYD000001", + name: "张总", phone: process.env.SEED_CEO_PHONE ?? "13900000000", email: process.env.SEED_CEO_EMAIL ?? "ceo@hyinda.com", password: process.env.SEED_CEO_PASSWORD ?? "CeoPassword1234", @@ -494,8 +492,8 @@ async function main() { }); const admin = await ensureEmployee(prisma, { - employeeNo: process.env.SEED_ADMIN_EMPLOYEE_NO ?? "E0000", - name: "Admin", + employeeNo: process.env.SEED_ADMIN_EMPLOYEE_NO ?? "HYD000006", + name: "王顺利", phone: process.env.SEED_ADMIN_PHONE ?? "13800000000", email: process.env.SEED_ADMIN_EMAIL ?? "admin@hyinda.com", password: process.env.SEED_ADMIN_PASSWORD ?? "AdminPassword1234", @@ -508,8 +506,8 @@ async function main() { }); const manager = await ensureEmployee(prisma, { - employeeNo: process.env.SEED_MANAGER_EMPLOYEE_NO ?? "E0100", - name: "Manager", + employeeNo: process.env.SEED_MANAGER_EMPLOYEE_NO ?? "HYD000002", + name: "李研发", phone: process.env.SEED_MANAGER_PHONE ?? "13700000000", email: process.env.SEED_MANAGER_EMAIL ?? "manager@hyinda.com", password: process.env.SEED_MANAGER_PASSWORD ?? "ManagerPassword1234", @@ -522,8 +520,8 @@ async function main() { }); const employee = await ensureEmployee(prisma, { - employeeNo: process.env.SEED_EMPLOYEE_EMPLOYEE_NO ?? "E0101", - name: "Employee", + employeeNo: process.env.SEED_EMPLOYEE_EMPLOYEE_NO ?? "HYD000003", + name: "陈工程", phone: process.env.SEED_EMPLOYEE_PHONE ?? "13600000000", email: process.env.SEED_EMPLOYEE_EMAIL ?? "employee@hyinda.com", password: process.env.SEED_EMPLOYEE_PASSWORD ?? "EmployeePassword1234", @@ -536,8 +534,8 @@ async function main() { }); const finance = await ensureEmployee(prisma, { - employeeNo: process.env.SEED_FINANCE_EMPLOYEE_NO ?? "E0200", - name: "Finance", + employeeNo: process.env.SEED_FINANCE_EMPLOYEE_NO ?? "HYD000004", + name: "赵财务", phone: process.env.SEED_FINANCE_PHONE ?? "13500000000", email: process.env.SEED_FINANCE_EMAIL ?? "finance@hyinda.com", password: process.env.SEED_FINANCE_PASSWORD ?? "FinancePassword1234", @@ -550,8 +548,8 @@ async function main() { }); const financeMgr = await ensureEmployee(prisma, { - employeeNo: process.env.SEED_FINANCE_MGR_EMPLOYEE_NO ?? "E0201", - name: "Finance Manager", + employeeNo: process.env.SEED_FINANCE_MGR_EMPLOYEE_NO ?? "HYD000005", + name: "钱主管", phone: process.env.SEED_FINANCE_MGR_PHONE ?? "13400000000", email: process.env.SEED_FINANCE_MGR_EMAIL ?? "finance_mgr@hyinda.com", password: process.env.SEED_FINANCE_MGR_PASSWORD ?? "FinanceMgrPassword1234", @@ -564,8 +562,8 @@ async function main() { }); const hr = await ensureEmployee(prisma, { - employeeNo: process.env.SEED_HR_EMPLOYEE_NO ?? "E0300", - name: "HR", + employeeNo: process.env.SEED_HR_EMPLOYEE_NO ?? "HYD000007", + name: "孙人事", phone: process.env.SEED_HR_PHONE ?? "13300000000", email: process.env.SEED_HR_EMAIL ?? "hr@hyinda.com", password: process.env.SEED_HR_PASSWORD ?? "HrPassword1234", @@ -578,8 +576,8 @@ async function main() { }); const administration = await ensureEmployee(prisma, { - employeeNo: process.env.SEED_ADMINISTRATION_EMPLOYEE_NO ?? "E0400", - name: "Administration", + employeeNo: process.env.SEED_ADMINISTRATION_EMPLOYEE_NO ?? "HYD000008", + name: "周行政", phone: process.env.SEED_ADMINISTRATION_PHONE ?? "13200000000", email: process.env.SEED_ADMINISTRATION_EMAIL ?? "administration@hyinda.com", password: process.env.SEED_ADMINISTRATION_PASSWORD ?? "AdministrationPassword1234", @@ -664,21 +662,21 @@ async function main() { // eslint-disable-next-line no-console console.log("Seeded dev users:"); // eslint-disable-next-line no-console - console.log(`- CEO: ${ceo.email} / ${process.env.SEED_CEO_PASSWORD ?? "CeoPassword1234"}`); + console.log(`- 负责人: ${ceo.email} / ${process.env.SEED_CEO_PASSWORD ?? "CeoPassword1234"}`); // eslint-disable-next-line no-console - console.log(`- Admin: ${admin.email} / ${process.env.SEED_ADMIN_PASSWORD ?? "AdminPassword1234"}`); + console.log(`- 管理员: ${admin.email} / ${process.env.SEED_ADMIN_PASSWORD ?? "AdminPassword1234"}`); // eslint-disable-next-line no-console - console.log(`- Manager: ${manager.email} / ${process.env.SEED_MANAGER_PASSWORD ?? "ManagerPassword1234"}`); + console.log(`- 研发负责人: ${manager.email} / ${process.env.SEED_MANAGER_PASSWORD ?? "ManagerPassword1234"}`); // eslint-disable-next-line no-console - console.log(`- Employee: ${employee.email} / ${process.env.SEED_EMPLOYEE_PASSWORD ?? "EmployeePassword1234"}`); + console.log(`- 员工: ${employee.email} / ${process.env.SEED_EMPLOYEE_PASSWORD ?? "EmployeePassword1234"}`); // eslint-disable-next-line no-console - console.log(`- Finance: ${finance.email} / ${process.env.SEED_FINANCE_PASSWORD ?? "FinancePassword1234"}`); + console.log(`- 财务: ${finance.email} / ${process.env.SEED_FINANCE_PASSWORD ?? "FinancePassword1234"}`); // eslint-disable-next-line no-console - console.log(`- Finance Manager: ${financeMgr.email} / ${process.env.SEED_FINANCE_MGR_PASSWORD ?? "FinanceMgrPassword1234"}`); + console.log(`- 财务主管: ${financeMgr.email} / ${process.env.SEED_FINANCE_MGR_PASSWORD ?? "FinanceMgrPassword1234"}`); // eslint-disable-next-line no-console - console.log(`- HR: ${hr.email} / ${process.env.SEED_HR_PASSWORD ?? "HrPassword1234"}`); + console.log(`- 人事: ${hr.email} / ${process.env.SEED_HR_PASSWORD ?? "HrPassword1234"}`); // eslint-disable-next-line no-console - console.log(`- Administration: ${administration.email} / ${process.env.SEED_ADMINISTRATION_PASSWORD ?? "AdministrationPassword1234"}`); + console.log(`- 行政: ${administration.email} / ${process.env.SEED_ADMINISTRATION_PASSWORD ?? "AdministrationPassword1234"}`); // eslint-disable-next-line no-console console.log(`- Legal Entity: ${legalEntity.code} / ${legalEntity.name}`); // eslint-disable-next-line no-console diff --git a/server/src/scripts/seed-trialrun.js b/server/src/scripts/seed-trialrun.js index b94d91b2..91f9757b 100644 --- a/server/src/scripts/seed-trialrun.js +++ b/server/src/scripts/seed-trialrun.js @@ -24,7 +24,6 @@ const DEFAULT_FORCE_ON_FLAGS = Object.freeze([ ]); const OPTIONAL_FORCE_ON_FLAGS = new Set([ - "erp_sync", "budget_precheck_v1", "payment_batch_v1", "workflow_definition_admin_v1", @@ -164,7 +163,6 @@ async function ensureWorkspaceSetting(prisma) { paymentBatches: "admin", apOperations: "admin", cutover: "admin", - erpIntegrations: "admin", }, }, }); @@ -199,7 +197,6 @@ async function ensureWorkspaceSetting(prisma) { paymentBatches: "admin", apOperations: "admin", cutover: "admin", - erpIntegrations: "admin", }, }, }); @@ -326,8 +323,8 @@ async function main() { email: adminEmail, phone: adminPhone, password: adminPassword || null, - employeeNo: process.env.TRIALRUN_ADMIN_EMPLOYEE_NO ?? "T0000", - name: process.env.TRIALRUN_ADMIN_NAME ?? "Trialrun Admin", + employeeNo: process.env.TRIALRUN_ADMIN_EMPLOYEE_NO ?? "HYD000006", + name: process.env.TRIALRUN_ADMIN_NAME ?? "王顺利", title: process.env.TRIALRUN_ADMIN_TITLE ?? "系统管理员", departmentId: management.id, hireDate: new Date(process.env.TRIALRUN_ADMIN_HIRE_DATE ?? "2020-01-01"), diff --git a/server/src/services/workspaceSettings.js b/server/src/services/workspaceSettings.js index 4160a59a..0684f0cb 100644 --- a/server/src/services/workspaceSettings.js +++ b/server/src/services/workspaceSettings.js @@ -31,7 +31,6 @@ const MANAGEMENT_CHILD_MODULES = new Set([ "masterData", "notificationChannels", "cutover", - "erpIntegrations", ]); const BACKUP_NAVIGATION_MODULES = new Set([ "weeklyReports", @@ -104,7 +103,6 @@ const NAVIGATION_REGISTRY = Object.freeze({ invoicePool: { key: "invoicePool", label: "票据", icon: "receipt_long", href: "/invoice-pool", anyOf: ["invoice.manage"] }, inventoryChecks: { key: "inventoryChecks", label: "资产盘点", icon: "inventory", href: "/inventory-checks", anyOf: ["asset.inventory"] }, cutover: { key: "cutover", label: "切换控制台", icon: "swap_calls", href: "/system/cutover", anyOf: ["system.settings"], featureFlagKey: FEATURE_FLAG_KEYS.cutoverConsole }, - erpIntegrations: { key: "erpIntegrations", label: "外部系统", icon: "hub", href: "/system/integrations/erp", anyOf: ["integration.manage"] }, profile: { key: "profile", label: "个人设置", icon: "manage_accounts", href: "/settings/profile", anyOf: [] }, }); @@ -144,7 +142,6 @@ const DEFAULT_MODULE_VISIBILITY = Object.freeze({ invoicePool: "admin", inventoryChecks: "admin", cutover: "admin", - erpIntegrations: "admin", profile: "hidden", });