Quellcode durchsuchen

fix(timecard): pass manager when submitting repair hours

malk vor 3 Wochen
Ursprung
Commit
8645ea9098
3 geänderte Dateien mit 5317 neuen und 0 gelöschten Zeilen
  1. 28 0
      pages/README.md
  2. 5191 0
      pages/src/timecard-repair.oyd.jsx
  3. 98 0
      pages/tests/verify-repair-manager.cjs

+ 28 - 0
pages/README.md

@@ -0,0 +1,28 @@
+# TimeCard 补填页面源码
+
+`src/timecard-repair.oyd.jsx` 为阿科德斯 `Timecard_补填` 的维护源码,采用宜搭现有平台 JSX 模式。主管规则为内部员工取人员档案 Manager,外部员工取所选月份有效项目经理,只有业务数据缺失时才兜底 Raymond。
+
+其他工时补填请求必须携带 `otherGroup.managerId`。`buildTimecardRepairRequest` 和调用方需要完整等待主管解析,再发起后端请求。
+
+## 验证
+
+从仓库根运行,无需浏览器、登录态或后端启动:
+
+```sh
+node pages/tests/verify-repair-manager.cjs
+```
+
+8 个用例覆盖主管缓存、缓存缺失、内外部员工规则、业务兜底、纯项目工时、连接器实际请求及主管获取失败。也可验证回读 Schema 的源码或编译代码:
+
+```sh
+node pages/tests/verify-repair-manager.cjs /absolute/path/to/readback-schema.json compiled
+```
+
+## 发布边界
+
+- 目标应用:`APP_ZQ3I7XO2RSHDJ4QDEVNB`。
+- 补填页面:`FORM-C7FA6060445B4820AF93668DE6AD9C4FY0CY`。
+- 发布前回读最新线上 Schema,将修改合入线上基线,再执行 `openyida check-page`、`compile`、`publish --health-check`。发布后核对源码、原有功能与页面数据源。
+- 正式 `Timecard` 主页面仍由用户手工发布,本目录源码不能用于覆盖该页面。
+- 登录态沿用已绑定阿科德斯组织的项目环境,不提交凭据、Schema 快照、编译产物或缓存。
+- 技术说明、执行证据与归档记录保留在 `/Users/malk/Desktop/Tech/1. malk/宜搭/阿科德斯/TimeCard/`;该目录的 `程序` 链接指向本仓库。

Datei-Diff unterdrückt, da er zu groß ist
+ 5191 - 0
pages/src/timecard-repair.oyd.jsx


+ 98 - 0
pages/tests/verify-repair-manager.cjs

@@ -0,0 +1,98 @@
+'use strict';
+
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const path = require('node:path');
+const vm = require('node:vm');
+
+/** @param {string} source @param {string} name @returns {string} */
+function extractFunction(source, name) {
+  const pattern = new RegExp('export (?:async )?function ' + name + '\\(');
+  const match = pattern.exec(source);
+  assert.ok(match, '缺少方法 ' + name);
+  const end = source.indexOf('\n/**', match.index + match[0].length);
+  return source.slice(match.index, end < 0 ? source.length : end).replace(/^export /, '');
+}
+
+/** @param {object} options @returns {object} */
+function createContext(options = {}) {
+  const state = {
+    selectedYear: 2026, selectedMonth: 8, userId: '626968159',
+    userInfo: { userName: '冷志刚/Zhigang Leng', staffType: '内部', managerId: '640891109' },
+    projectRows: [], ...options.state,
+  };
+  return {
+    state,
+    collectApprovalData: () => options.projectGroups || {},
+    collectOtherApprovalData: () => ({
+      otherRows: [{ dateStr: '20260813', activityName: 'Annual Leave', hours: 8 }],
+      otherTotal: 8, ...options.otherGroup,
+    }),
+    getRepairMonthText: (year, month) => String(year) + String(month).padStart(2, '0'),
+    fetchDeptManagerId: methods.fetchDeptManagerId,
+    buildTimecardRepairRequest: methods.buildTimecardRepairRequest,
+    getTexts: () => ({ repairBackendPending: '补填连接器未配置' }),
+    unwrapConnectorResponse: (response) => response.data,
+  };
+}
+
+const sourcePath = path.resolve(__dirname, '../src/timecard-repair.oyd.jsx');
+const schemaFile = process.argv[2];
+const schema = schemaFile ? JSON.parse(fs.readFileSync(schemaFile, 'utf8')).content : null;
+const compiled = process.argv[3] === 'compiled';
+const source = schema ? schema.actions.module[compiled ? 'compiled' : 'source'] : fs.readFileSync(sourcePath, 'utf8');
+const methods = vm.runInNewContext(
+  (compiled ? source : ['buildTimecardRepairRequest', 'submitTimecardRepairRequest', 'fetchDeptManagerId']
+    .map((name) => extractFunction(source, name)).join('\n'))
+    + '\n({buildTimecardRepairRequest, submitTimecardRepairRequest, fetchDeptManagerId})',
+  { exports: {}, REPAIR_DATA_SOURCES: { submit: 'startTimecardRepair' } },
+);
+
+/** @returns {Promise<void>} */
+async function main() {
+  const cases = [
+    ['内部员工缓存主管', { state: { deptManagerInfo: { managerId: '640891109', isFallback: false } } }, '640891109'],
+    ['缓存缺失时从内部人员档案获取', {}, '640891109'],
+    ['外部员工从有效项目获取', { state: {
+      userInfo: { staffType: '外部' },
+      projectRows: [{ managerId: '634735129' }, { managerId: 'project-manager' }],
+    } }, 'project-manager'],
+    ['内部员工未配置主管时保留业务兜底', { state: { userInfo: { staffType: '内部' } } }, '634735129'],
+    ['外部员工无有效项目经理时保留业务兜底', { state: { userInfo: { staffType: '外部' } } }, '634735129'],
+  ];
+  for (const [name, options, expected] of cases) {
+    const context = createContext(options);
+    const request = await methods.buildTimecardRepairRequest.call(context);
+    assert.equal(request.otherGroup.managerId, expected, name);
+    assert.equal(request.otherGroup.otherTotal, 8, name);
+    assert.equal(request.otherGroup.otherRows[0].hours, 8, name);
+    assert.equal(request.sourceMonthText, '202608', name);
+    assert.equal(request.submitterUserId, '626968159', name);
+    console.log('PASS ' + name);
+  }
+  const projectGroups = { pm: { managerId: 'original-pm', billableTotal: 8, nonBillableTotal: 0 } };
+  const projectOnly = createContext({ projectGroups, otherGroup: { otherRows: [], otherTotal: 0 } });
+  projectOnly.fetchDeptManagerId = () => { throw new Error('纯项目补填无需获取其他工时主管'); };
+  assert.equal((await methods.buildTimecardRepairRequest.call(projectOnly)).projectGroups, projectGroups);
+  console.log('PASS 项目工时分组保持原样');
+
+  const context = createContext();
+  let submitted;
+  context.getCallableDataSource = () => ({ load: async (payload) => {
+    submitted = JSON.parse(payload.inputs).Body;
+    return { data: { status: '审批中' } };
+  } });
+  const result = await methods.submitTimecardRepairRequest.call(context);
+  assert.equal(submitted.otherGroup.managerId, '640891109');
+  assert.equal(submitted.otherGroup.otherRows[0].activityName, 'Annual Leave');
+  assert.equal(result.status, '审批中');
+  console.log('PASS 连接器实际请求携带 Kevin ID');
+
+  const failedContext = createContext();
+  failedContext.fetchDeptManagerId = async () => { throw new Error('主管获取失败'); };
+  failedContext.getCallableDataSource = () => ({ load: () => assert.fail('主管获取失败后不得提交') });
+  await assert.rejects(methods.submitTimecardRepairRequest.call(failedContext), /主管获取失败/);
+  console.log('PASS 主管获取失败阻止提交');
+}
+
+main().catch((error) => { console.error(error); process.exitCode = 1; });