weaver-work-cli 0.1.21 → 0.1.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/shortcuts/invoice/host.js +5 -4
- package/dist/shortcuts/meeting/operations/change.js +7 -4
- package/dist/shortcuts/meeting/operations/create.js +5 -3
- package/dist/shortcuts/meeting/operations/eb/association.js +151 -0
- package/dist/shortcuts/meeting/operations/eb/attendance-summary.js +153 -0
- package/dist/shortcuts/meeting/operations/eb/catalog.js +185 -13
- package/dist/shortcuts/meeting/operations/eb/conflict.js +17 -11
- package/dist/shortcuts/meeting/operations/eb/decision.js +405 -0
- package/dist/shortcuts/meeting/operations/eb/factory.js +222 -35
- package/dist/shortcuts/meeting/operations/eb/index.js +18 -1
- package/dist/shortcuts/meeting/operations/eb/link.js +212 -0
- package/dist/shortcuts/meeting/operations/eb/task-progress.js +123 -0
- package/dist/shortcuts/meeting/operations/env.js +78 -61
- package/dist/shortcuts/meeting/operations/lifecycle.js +8 -6
- package/dist/shortcuts/meeting/operations/link.js +125 -0
- package/dist/shortcuts/meeting/operations/receipts.js +54 -21
- package/dist/shortcuts/meeting/operations/registry.js +2 -0
- package/dist/shortcuts/meeting/operations/shared.js +41 -0
- package/dist/shortcuts/meeting/operations/signs.js +14 -4
- package/dist/shortcuts/project/operations/read.js +7 -6
- package/docs/e10-auth.md +4 -0
- package/docs/invoice.md +3 -0
- package/package.json +1 -1
- package/skills/weaver-e10-meeting/SKILL.md +28 -11
- package/skills/weaver-e10-meeting/references/meeting-eb.md +172 -7
- package/skills/weaver-e10-meeting/references/meeting-receipts-signs.md +7 -2
- package/skills/weaver-e10-meeting/references/meeting-verification.md +271 -0
- package/skills/weaver-e10-meeting.zip +0 -0
- package/skills/weaver-e10-shijingran/SKILL.md +68 -3
- package/skills/weaver-e10-shijingran/references/disabled-capabilities.md +22 -0
- package/skills/weaver-e10-shijingran/references/source-manifest.json +12 -4
|
@@ -135,10 +135,11 @@ export function createInvoiceHost(ctx, dependencies = {}) {
|
|
|
135
135
|
if (typeof token !== 'string' || !token.trim()) {
|
|
136
136
|
throw invoiceAuthError('invoice_token_missing', '未取得业票通业务 Token');
|
|
137
137
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
138
|
+
// Headless Web/Linux hosts may not provide a system keyring (for example,
|
|
139
|
+
// Secret Service over D-Bus). Persistence is only an optimization: keep
|
|
140
|
+
// the token in this process so the current business request can proceed,
|
|
141
|
+
// without falling back to an insecure plaintext cache.
|
|
142
|
+
await tokenStore.save(token);
|
|
142
143
|
invoiceToken = token;
|
|
143
144
|
return token;
|
|
144
145
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { meetingConfirmationError, meetingPartialError, meetingValidationError } from '../errors.js';
|
|
3
3
|
import { meetingSuccess, SCHEMA_DATETIME_MINUTE, SCHEMA_ID } from './types.js';
|
|
4
|
-
import { assertDateTimeMinute, extractRows, toIdCsv, toIdString } from './shared.js';
|
|
4
|
+
import { assertDateTimeMinute, extractRows, mergeSnapshot, toIdCsv, toIdString } from './shared.js';
|
|
5
5
|
const CONTINUATION_TTL_SECONDS = 600;
|
|
6
6
|
const LIST_PATH = '/api/meeting/search/getMeetingList';
|
|
7
7
|
function fingerprint(value) {
|
|
@@ -202,15 +202,18 @@ const meetingChangeApply = {
|
|
|
202
202
|
caller: SCHEMA_ID,
|
|
203
203
|
contacter: SCHEMA_ID,
|
|
204
204
|
},
|
|
205
|
-
required: ['confirm', 'continuation'
|
|
205
|
+
required: ['confirm', 'continuation'],
|
|
206
206
|
},
|
|
207
207
|
handler: async (args, env) => {
|
|
208
208
|
requireConfirm(args, 'meeting.change.apply');
|
|
209
209
|
const lease = await verifyLease(args, 'meeting.change.apply', env);
|
|
210
|
-
|
|
210
|
+
// 只传 confirm+continuation 时用 prepare 快照(含 prepare 时算好的 id / 字段);
|
|
211
|
+
// 传入字段则覆盖快照,仍由 fingerprint 决定是否 target_changed。
|
|
212
|
+
const merged = mergeSnapshot(lease.payload, args);
|
|
213
|
+
const { operateType: leaseOperateType, ...mergedBody } = merged;
|
|
214
|
+
const body = normalizeChangeInput(mergedBody);
|
|
211
215
|
const id = String(body.id);
|
|
212
216
|
const { operateType, mtStatus } = await resolveOperateType(id, env, 'meeting.change.apply');
|
|
213
|
-
const leaseOperateType = lease.payload?.operateType;
|
|
214
217
|
if (leaseOperateType && leaseOperateType !== operateType) {
|
|
215
218
|
throw meetingValidationError('target_changed', `会议状态已变化(prepare 时按 ${leaseOperateType}、当前按 ${operateType},当前状态:${mtStatus}),请重新执行 prepare 并向用户确认`);
|
|
216
219
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { meetingConflictError, meetingConfirmationError, meetingPartialError, meetingValidationError } from '../errors.js';
|
|
3
3
|
import { meetingSuccess, SCHEMA_DATETIME_MINUTE, SCHEMA_ID } from './types.js';
|
|
4
|
-
import { assertDateTimeMinute, extractConflict, toIdCsv, toIdString, } from './shared.js';
|
|
4
|
+
import { assertDateTimeMinute, extractConflict, mergeSnapshot, toIdCsv, toIdString, } from './shared.js';
|
|
5
5
|
const CONTINUATION_TTL_SECONDS = 600;
|
|
6
6
|
function fingerprint(value) {
|
|
7
7
|
return createHash('sha256').update(JSON.stringify(value ?? null)).digest('hex').slice(0, 32);
|
|
@@ -309,13 +309,15 @@ const meetingCreateApply = {
|
|
|
309
309
|
repeatDesc: { type: 'string' },
|
|
310
310
|
serviceItems: { type: 'string' },
|
|
311
311
|
},
|
|
312
|
-
required: ['confirm', 'continuation'
|
|
312
|
+
required: ['confirm', 'continuation'],
|
|
313
313
|
},
|
|
314
314
|
handler: async (args, env) => {
|
|
315
315
|
requireConfirm(args, 'meeting.create.apply');
|
|
316
316
|
const lease = await verifyLease(args, 'meeting.create.apply', env);
|
|
317
317
|
const session = await env.host.getSession();
|
|
318
|
-
|
|
318
|
+
// 只传 confirm+continuation 时用 prepare 快照(用户已确认的内容);传入其它字段则作为覆盖,
|
|
319
|
+
// 由下面的 fingerprint 校验决定是否 target_changed。
|
|
320
|
+
const body = normalizeCreateInput(mergeSnapshot(lease.payload, args), session.userId);
|
|
319
321
|
if (lease.fingerprint !== fingerprint(body)) {
|
|
320
322
|
throw meetingValidationError('target_changed', '会议内容与 prepare 时的快照不一致,请重新执行 prepare');
|
|
321
323
|
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EB 建会后的**关联数据补建**(占用 / 回执 / 签到)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么必须补(2026-09-21 真机确认,应用设置 `open_mt_approve=0` + `only_flow_create=0`):
|
|
5
|
+
* eBuilder 的**自定义接口** `create_meeting` 只写会议主表,**不会**像页面建会那样
|
|
6
|
+
* 自动生成占用视图 / 回执 / 签到记录;而这三样恰好是后续能力的硬依赖:
|
|
7
|
+
*
|
|
8
|
+
* - 没有**占用**(roomusage)⇒ 会议室冲突检测(`query_roomusage_list`)查不到这场会,
|
|
9
|
+
* 以后别人在同一时段订同一间会议室不会被拦住;
|
|
10
|
+
* - 没有**回执**(receipt)⇒ 参会人冲突检测(以回执表 member_id 为准)对本场会议失效;
|
|
11
|
+
* - 没有**签到**记录(sign)⇒ `meeting.eb.sign.mark`(补签)报「未找到签到记录」。
|
|
12
|
+
*
|
|
13
|
+
* 因此建会成功后由本模块按「会议 + 参会人」逐条补齐,语义与页面建会一致。
|
|
14
|
+
*
|
|
15
|
+
* 失败语义(重要):主写入(建会)**已经成功**,这里任何一步失败都**不抛错**,
|
|
16
|
+
* 只把失败原因汇总进 `warnings` 并标明后果 —— 否则调用方会以为建会失败而重试,导致重复建会。
|
|
17
|
+
*/
|
|
18
|
+
import { EB_BASE, ebBusinessSuccess, normalizeEbWrite, writeBody } from './factory.js';
|
|
19
|
+
import { EB_ENDPOINTS } from './catalog.js';
|
|
20
|
+
/** 只在建会后补建关联数据;其他写操作直接跳过。 */
|
|
21
|
+
const CREATE_MEETING_PK = 'create_meeting';
|
|
22
|
+
/** 回执人员类型:1 = 参会人员(参会人冲突检测只统计该类型)。 */
|
|
23
|
+
const MEMBER_TYPE_ATTENDEE = '1';
|
|
24
|
+
function requireEndpoint(interfacePk) {
|
|
25
|
+
const endpoint = EB_ENDPOINTS.find((item) => item.interfacePk === interfacePk);
|
|
26
|
+
if (!endpoint)
|
|
27
|
+
throw new Error(`EB catalog 缺少 ${interfacePk},请检查 catalog.ts 声明`);
|
|
28
|
+
return endpoint;
|
|
29
|
+
}
|
|
30
|
+
/** 逗号分隔 id 串(`hrm_members` 等)→ 去重去空后的数组。 */
|
|
31
|
+
function splitIds(value) {
|
|
32
|
+
if (value === undefined || value === null)
|
|
33
|
+
return [];
|
|
34
|
+
const text = String(value).trim();
|
|
35
|
+
if (text === '')
|
|
36
|
+
return [];
|
|
37
|
+
const seen = new Set();
|
|
38
|
+
for (const part of text.split(',')) {
|
|
39
|
+
const id = part.trim();
|
|
40
|
+
if (id !== '')
|
|
41
|
+
seen.add(id);
|
|
42
|
+
}
|
|
43
|
+
return [...seen];
|
|
44
|
+
}
|
|
45
|
+
function errorText(error) {
|
|
46
|
+
return error instanceof Error ? error.message : String(error);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* 单条 EB 写入。**不抛错**:返回失败原因交由调用方汇总(主写入已成功,不可中断)。
|
|
50
|
+
* 复用 `normalizeEbWrite` 以保证字段白名单 / 必填 / 日期格式与手动调用完全一致。
|
|
51
|
+
*/
|
|
52
|
+
async function writeOne(env, interfacePk, args) {
|
|
53
|
+
try {
|
|
54
|
+
const session = await env.host.getSession();
|
|
55
|
+
const payload = normalizeEbWrite(requireEndpoint(interfacePk), args, session.userId);
|
|
56
|
+
const response = await env.host.request({
|
|
57
|
+
method: 'POST',
|
|
58
|
+
path: `${EB_BASE}/saveFormData/${interfacePk}`,
|
|
59
|
+
operation: 'meeting.eb.meeting.create.apply',
|
|
60
|
+
body: writeBody(session.userId, payload),
|
|
61
|
+
acceptBusiness: ebBusinessSuccess,
|
|
62
|
+
});
|
|
63
|
+
if (response?.datajson?.status === false) {
|
|
64
|
+
return { ok: false, error: `${interfacePk} 返回 status=false` };
|
|
65
|
+
}
|
|
66
|
+
return { ok: true };
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
return { ok: false, error: `${interfacePk}: ${errorText(error)}` };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export const ebMeetingAssociation = async (ctx) => {
|
|
73
|
+
if (ctx.endpoint.interfacePk !== CREATE_MEETING_PK)
|
|
74
|
+
return undefined;
|
|
75
|
+
const mtId = ctx.dataIds[0];
|
|
76
|
+
if (!mtId) {
|
|
77
|
+
return {
|
|
78
|
+
status: 'SKIPPED',
|
|
79
|
+
reason: 'create_meeting 未返回会议 id,无法补建占用/回执/签到,请到会议详情页确认。',
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const main = ctx.payload.mainTable;
|
|
83
|
+
const warnings = [];
|
|
84
|
+
const created = {};
|
|
85
|
+
// ① 会议室占用:仅在指定了会议室时写入。
|
|
86
|
+
const address = main.address === undefined || main.address === null ? '' : String(main.address).trim();
|
|
87
|
+
if (address === '') {
|
|
88
|
+
warnings.push('会议未指定会议室(address),已跳过占用写入。');
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
const result = await writeOne(ctx.env, 'create_roomusage', {
|
|
92
|
+
mt_id: mtId,
|
|
93
|
+
room_id: address,
|
|
94
|
+
startdate: main.begin_datetime,
|
|
95
|
+
enddate: main.end_datetime,
|
|
96
|
+
});
|
|
97
|
+
if (result.ok)
|
|
98
|
+
created.roomUsage = [address];
|
|
99
|
+
else {
|
|
100
|
+
warnings.push(`会议室占用写入失败(${result.error}):该会议不会被会议室冲突检测识别,`
|
|
101
|
+
+ '建议到会议详情页确认,或按需重新创建占用。');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// ② 回执 + ③ 签到:按 hrm_members 逐人补齐。
|
|
105
|
+
const members = splitIds(main.hrm_members);
|
|
106
|
+
if (members.length === 0) {
|
|
107
|
+
warnings.push('会议未提供具体参会人员(hrm_members 为空),已跳过回执与签到写入:'
|
|
108
|
+
+ '参会人冲突检测与签到/补签都依赖这些记录;若通过 org_members 指定了部门,本 CLI 不展开部门成员,'
|
|
109
|
+
+ '请到会议详情页确认参会人后按需补建。');
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
const receiptFailed = [];
|
|
113
|
+
const signFailed = [];
|
|
114
|
+
for (const memberId of members) {
|
|
115
|
+
const receipt = await writeOne(ctx.env, 'create_meeting_receipt', {
|
|
116
|
+
mt_id: mtId,
|
|
117
|
+
member_type: MEMBER_TYPE_ATTENDEE,
|
|
118
|
+
member_id: memberId,
|
|
119
|
+
member_manager: memberId,
|
|
120
|
+
});
|
|
121
|
+
if (receipt.ok)
|
|
122
|
+
(created.receipt ??= []).push(memberId);
|
|
123
|
+
else
|
|
124
|
+
receiptFailed.push(`${memberId}(${receipt.error})`);
|
|
125
|
+
const sign = await writeOne(ctx.env, 'create_meeting_sign', {
|
|
126
|
+
mt_id: mtId,
|
|
127
|
+
user_id: memberId,
|
|
128
|
+
});
|
|
129
|
+
if (sign.ok)
|
|
130
|
+
(created.sign ??= []).push(memberId);
|
|
131
|
+
else
|
|
132
|
+
signFailed.push(`${memberId}(${sign.error})`);
|
|
133
|
+
}
|
|
134
|
+
if (receiptFailed.length > 0) {
|
|
135
|
+
warnings.push(`回执写入失败 ${receiptFailed.length}/${members.length} 人:${receiptFailed.join('、')}。`
|
|
136
|
+
+ '参会人冲突检测以回执表为准,缺失将导致该会议的参会人冲突漏检。');
|
|
137
|
+
}
|
|
138
|
+
if (signFailed.length > 0) {
|
|
139
|
+
warnings.push(`签到记录写入失败 ${signFailed.length}/${members.length} 人:${signFailed.join('、')}。`
|
|
140
|
+
+ '这些人将无法通过 meeting.eb.sign.mark 补签(无签到记录可定位)。');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
status: warnings.length === 0 ? 'COMPLETE' : 'PARTIAL',
|
|
145
|
+
mtId,
|
|
146
|
+
created,
|
|
147
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
148
|
+
note: 'eBuilder 自定义接口 create_meeting 不会像页面建会那样自动生成占用/回执/签到,'
|
|
149
|
+
+ '本步骤由 CLI 按「会议 + 参会人」补齐,供会议室/参会人冲突检测与签到补签使用。',
|
|
150
|
+
};
|
|
151
|
+
};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 签到 / 回执列表的**出席情况汇总**(供 `meeting.eb.sign.list` 与 `meeting.eb.receipt.list`
|
|
3
|
+
* 声明式挂载,见 catalog 的 `summarizeRows`)。机制与 `task-progress.ts` 一致:
|
|
4
|
+
* 只**增**一个 `data.progress` 字段,原始 `rows` 原样返回。
|
|
5
|
+
*
|
|
6
|
+
* 真机确立的取值口径(2026-09-21/22,实现前务必看清):
|
|
7
|
+
* - `is_sign` / `attend_status` 回的是**数字字符串**(`"1"` / `""`),不是中文渲染值;
|
|
8
|
+
* - **"未签到 / 未回执" = 空字符串**,而 `is_sign="0"` 是**缺席**(是"已表态"而非"没签")——
|
|
9
|
+
* 两者语义不同,混在一起会得出错误的出席率;
|
|
10
|
+
* - `sign_out_status`:`0` 已签退 / `1` 未签退 / `2` 早退 / 空 = 未签到;
|
|
11
|
+
* - ⚠️ **两张表的 `mt_id` 语义不同**:签到表回的是**会议名称文本**,回执表回的是**会议 id** ——
|
|
12
|
+
* 故汇总里统一命名为 `mtRef`(原样透出),不硬称为 id 或 name。
|
|
13
|
+
*/
|
|
14
|
+
function toText(value) {
|
|
15
|
+
return value === undefined || value === null ? '' : String(value).trim();
|
|
16
|
+
}
|
|
17
|
+
/** 百分比字符串(保留 1 位小数);分母为 0 时返回 "0.0%"。 */
|
|
18
|
+
function rate(part, whole) {
|
|
19
|
+
if (whole <= 0)
|
|
20
|
+
return '0.0%';
|
|
21
|
+
return `${((part / whole) * 100).toFixed(1)}%`;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* `is_sign` 归一化。
|
|
25
|
+
*
|
|
26
|
+
* 数字字符串为主(实测 `"1"` / `""`),同时兼容中文渲染值与"已签退但未填 is_sign"的兜底:
|
|
27
|
+
* **空值一律算「未签到」**(不是缺席)。
|
|
28
|
+
*/
|
|
29
|
+
export function normalizeSignState(isSign, signTime) {
|
|
30
|
+
const text = toText(isSign);
|
|
31
|
+
if (text === '') {
|
|
32
|
+
// is_sign 为空:只要 sign_time 有值说明实际签过,按正常处理,避免误判成"未签到"。
|
|
33
|
+
return toText(signTime) === '' ? 'notSigned' : 'normal';
|
|
34
|
+
}
|
|
35
|
+
if (text.includes('正常'))
|
|
36
|
+
return 'normal';
|
|
37
|
+
if (text.includes('请假'))
|
|
38
|
+
return 'leave';
|
|
39
|
+
if (text.includes('迟到早退'))
|
|
40
|
+
return 'lateAndEarlyLeave';
|
|
41
|
+
if (text.includes('迟到未签退'))
|
|
42
|
+
return 'lateAndNotSignedOut';
|
|
43
|
+
if (text.includes('迟到'))
|
|
44
|
+
return 'late';
|
|
45
|
+
if (text.includes('早退'))
|
|
46
|
+
return 'earlyLeave';
|
|
47
|
+
if (text.includes('未签退'))
|
|
48
|
+
return 'notSignedOut';
|
|
49
|
+
if (text === '0' || text.includes('缺席'))
|
|
50
|
+
return 'absent';
|
|
51
|
+
switch (text) {
|
|
52
|
+
case '1': return 'normal';
|
|
53
|
+
case '2': return 'leave';
|
|
54
|
+
case '3': return 'late';
|
|
55
|
+
case '4': return 'earlyLeave';
|
|
56
|
+
case '5': return 'notSignedOut';
|
|
57
|
+
case '6': return 'lateAndNotSignedOut';
|
|
58
|
+
case '7': return 'lateAndEarlyLeave';
|
|
59
|
+
default: return 'unknown';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const SIGN_STATES = [
|
|
63
|
+
'normal', 'leave', 'late', 'earlyLeave', 'notSignedOut', 'lateAndNotSignedOut',
|
|
64
|
+
'lateAndEarlyLeave', 'absent', 'notSigned', 'unknown',
|
|
65
|
+
];
|
|
66
|
+
/** `meeting.eb.sign.list` 的结果后处理。 */
|
|
67
|
+
export function summarizeSignRows(rows) {
|
|
68
|
+
const byState = Object.fromEntries(SIGN_STATES.map((key) => [key, 0]));
|
|
69
|
+
const items = rows.map((row) => {
|
|
70
|
+
const main = (row?.mainTable ?? row ?? {});
|
|
71
|
+
const state = normalizeSignState(main.is_sign, main.sign_time);
|
|
72
|
+
byState[state] += 1;
|
|
73
|
+
return {
|
|
74
|
+
id: toText(main.id),
|
|
75
|
+
userId: toText(main.user_id),
|
|
76
|
+
mtRef: toText(main.mt_id),
|
|
77
|
+
state,
|
|
78
|
+
signTime: toText(main.sign_time),
|
|
79
|
+
signBackTime: toText(main.signback_time),
|
|
80
|
+
signOutStatus: toText(main.sign_out_status),
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
const total = items.length;
|
|
84
|
+
const notSigned = byState.notSigned;
|
|
85
|
+
const signed = total - notSigned;
|
|
86
|
+
return {
|
|
87
|
+
summary: {
|
|
88
|
+
total,
|
|
89
|
+
signed,
|
|
90
|
+
notSigned,
|
|
91
|
+
signedOut: items.filter((item) => item.signOutStatus === '0').length,
|
|
92
|
+
// 出席率按"已签到"口径(未签到 = 没来),比把"缺席"也算作出席更直观。
|
|
93
|
+
attendanceRate: rate(signed, total),
|
|
94
|
+
byState,
|
|
95
|
+
},
|
|
96
|
+
items,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/** `attend_status` 归一化:空 = 未回执;`0` = 不参加;`1` = 参加。 */
|
|
100
|
+
export function normalizeReceiptState(raw) {
|
|
101
|
+
const text = toText(raw);
|
|
102
|
+
if (text === '')
|
|
103
|
+
return 'noReply';
|
|
104
|
+
if (text === '1' || text.includes('参加') && !text.includes('不参加'))
|
|
105
|
+
return 'attend';
|
|
106
|
+
if (text === '0' || text.includes('不参加'))
|
|
107
|
+
return 'decline';
|
|
108
|
+
return 'unknown';
|
|
109
|
+
}
|
|
110
|
+
/** `meeting.eb.receipt.list` 的结果后处理。 */
|
|
111
|
+
export function summarizeReceiptRows(rows) {
|
|
112
|
+
const byMemberType = { attendee: 0, customer: 0, other: 0, unknown: 0 };
|
|
113
|
+
const items = rows.map((row) => {
|
|
114
|
+
const main = (row?.mainTable ?? row ?? {});
|
|
115
|
+
const memberType = toText(main.member_type);
|
|
116
|
+
if (memberType === '1')
|
|
117
|
+
byMemberType.attendee += 1;
|
|
118
|
+
else if (memberType === '2')
|
|
119
|
+
byMemberType.customer += 1;
|
|
120
|
+
else if (memberType === '3')
|
|
121
|
+
byMemberType.other += 1;
|
|
122
|
+
else
|
|
123
|
+
byMemberType.unknown += 1;
|
|
124
|
+
return {
|
|
125
|
+
id: toText(main.id),
|
|
126
|
+
memberId: toText(main.member_id),
|
|
127
|
+
mtRef: toText(main.mt_id),
|
|
128
|
+
state: normalizeReceiptState(main.attend_status),
|
|
129
|
+
memberType,
|
|
130
|
+
attendStatus: toText(main.attend_status),
|
|
131
|
+
bookRoom: toText(main.book_room),
|
|
132
|
+
bookTicket: toText(main.book_ticket),
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
const total = items.length;
|
|
136
|
+
const attend = items.filter((item) => item.state === 'attend').length;
|
|
137
|
+
const decline = items.filter((item) => item.state === 'decline').length;
|
|
138
|
+
const replied = attend + decline;
|
|
139
|
+
return {
|
|
140
|
+
summary: {
|
|
141
|
+
total,
|
|
142
|
+
attend,
|
|
143
|
+
decline,
|
|
144
|
+
noReply: total - replied,
|
|
145
|
+
replyRate: rate(replied, total),
|
|
146
|
+
attendRate: rate(attend, total),
|
|
147
|
+
byMemberType,
|
|
148
|
+
bookRoom: items.filter((item) => item.bookRoom !== '').length,
|
|
149
|
+
bookTicket: items.filter((item) => item.bookTicket !== '').length,
|
|
150
|
+
},
|
|
151
|
+
items,
|
|
152
|
+
};
|
|
153
|
+
}
|