@steedos-labs/plugin-workflow 3.0.55 → 3.0.56
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/designer/dist/amis-renderer/amis-renderer.css +1 -1
- package/designer/dist/amis-renderer/amis-renderer.js +1 -1
- package/designer/dist/assets/{index-CBj3S4BT.js → index-B9naSD1C.js} +174 -174
- package/designer/dist/assets/index-DEEcIiu0.css +1 -0
- package/designer/dist/index.html +2 -2
- package/main/default/manager/uuflow_manager.js +149 -45
- package/main/default/objects/categories/buttons/badge_recalc.button.yml +44 -0
- package/main/default/objects/instance_tasks/listviews/inbox.listview.yml +1 -1
- package/main/default/objects/instance_tasks/listviews/outbox.listview.yml +1 -2
- package/main/default/objects/instances/listviews/completed.listview.yml +1 -2
- package/main/default/objects/instances/listviews/draft.listview.yml +1 -2
- package/main/default/objects/instances/listviews/monitor.listview.yml +1 -1
- package/main/default/objects/instances/listviews/pending.listview.yml +1 -2
- package/main/default/pages/page_instance_print.page.amis.json +6 -2
- package/main/default/routes/api_workflow_ai_form_design.router.js +9 -3
- package/main/default/routes/api_workflow_ai_form_design_stream.router.js +9 -3
- package/main/default/routes/api_workflow_instance_forward.router.js +15 -1
- package/main/default/routes/api_workflow_instance_permissions.router.js +2 -2
- package/main/default/routes/api_workflow_nav.router.js +7 -1
- package/main/default/services/instance.service.js +1 -1
- package/main/default/utils/business_hours.js +210 -0
- package/main/default/utils/business_timeout.js +211 -0
- package/package.json +1 -1
- package/package.service.js +9 -1
- package/public/amis-renderer/amis-renderer.css +1 -1
- package/public/amis-renderer/amis-renderer.js +1 -1
- package/public/workflow/index.css +7 -2
- package/src/rests/badgeRecalcConsole.js +593 -0
- package/src/rests/badgeRecalcExecute.js +308 -0
- package/src/rests/index.js +2 -0
- package/src/timeout_auto_submit.js +81 -0
- package/designer/dist/assets/index-BNulYl_s.css +0 -1
|
@@ -16,11 +16,10 @@ filters: !!js/function |
|
|
|
16
16
|
}
|
|
17
17
|
sort: [['modified','desc']]
|
|
18
18
|
searchable_fields:
|
|
19
|
+
- field: flow
|
|
19
20
|
- field: name
|
|
20
21
|
- field: submitter_name
|
|
21
|
-
- field: flow
|
|
22
22
|
- field: applicant_organization_name
|
|
23
23
|
- field: submit_date
|
|
24
24
|
- field: state
|
|
25
|
-
- field: is_archived
|
|
26
25
|
disableSwitch: true
|
|
@@ -25,12 +25,12 @@ filters: !!js/function |
|
|
|
25
25
|
}
|
|
26
26
|
sort: [['submit_date','desc']]
|
|
27
27
|
searchable_fields:
|
|
28
|
+
- field: flow
|
|
28
29
|
- field: name
|
|
29
30
|
- field: submitter_name
|
|
30
31
|
- field: applicant_organization_name
|
|
31
32
|
- field: submit_date
|
|
32
33
|
- field: state
|
|
33
|
-
- field: is_archived
|
|
34
34
|
extra_columns:
|
|
35
35
|
- extras
|
|
36
36
|
- values
|
|
@@ -25,13 +25,12 @@ filters: !!js/function |
|
|
|
25
25
|
}
|
|
26
26
|
sort: [['submit_date','desc']]
|
|
27
27
|
searchable_fields:
|
|
28
|
+
- field: flow
|
|
28
29
|
- field: name
|
|
29
30
|
- field: submitter_name
|
|
30
|
-
- field: flow
|
|
31
31
|
- field: applicant_organization_name
|
|
32
32
|
- field: submit_date
|
|
33
33
|
- field: state
|
|
34
|
-
- field: is_archived
|
|
35
34
|
extra_columns:
|
|
36
35
|
- extras
|
|
37
36
|
disableSwitch: true
|
|
@@ -106,7 +106,7 @@
|
|
|
106
106
|
"actions": [
|
|
107
107
|
{
|
|
108
108
|
"actionType": "custom",
|
|
109
|
-
"script": "doAction({actionType: 'setValue', componentId: 'u:print-width', args: {value: Number(event.data.value)}}); $('.steedos-instance-related-view-wrapper .instance-form').css('width', event.data.value + 'mm');"
|
|
109
|
+
"script": "doAction({actionType: 'setValue', componentId: 'u:print-width', args: {value: Number(event.data.value)}}); $('.steedos-instance-related-view-wrapper .instance-form').css('width', event.data.value + 'mm'); $('.steedos-instance-related-view-wrapper .instance-approve-history').css('width', event.data.value + 'mm');"
|
|
110
110
|
}
|
|
111
111
|
]
|
|
112
112
|
}
|
|
@@ -125,7 +125,7 @@
|
|
|
125
125
|
"actions": [
|
|
126
126
|
{
|
|
127
127
|
"actionType": "custom",
|
|
128
|
-
"script": "$('.steedos-instance-related-view-wrapper .instance-form').css('width', event.data.value + 'mm');"
|
|
128
|
+
"script": "$('.steedos-instance-related-view-wrapper .instance-form').css('width', event.data.value + 'mm'); $('.steedos-instance-related-view-wrapper .instance-approve-history').css('width', event.data.value + 'mm');"
|
|
129
129
|
}
|
|
130
130
|
]
|
|
131
131
|
}
|
|
@@ -241,6 +241,10 @@
|
|
|
241
241
|
"width": "190mm",
|
|
242
242
|
"transition": "width 0.5s ease-in-out"
|
|
243
243
|
},
|
|
244
|
+
".steedos-instance-related-view-wrapper .instance-approve-history": {
|
|
245
|
+
"width": "190mm",
|
|
246
|
+
"transition": "width 0.5s ease-in-out"
|
|
247
|
+
},
|
|
244
248
|
".steedos-instance-related-view-wrapper .steedos-amis-instance-view-content": {
|
|
245
249
|
"display": "inline-block"
|
|
246
250
|
},
|
|
@@ -174,7 +174,10 @@ router.post('/am/ai/form-design', async function auth(req, res, next) {
|
|
|
174
174
|
- reference_to: 字符串,关联对象的 API 名称,如 "contracts"、"accounts"
|
|
175
175
|
- lookupLabelField: 字符串,用于搜索和显示的名称字段,默认 "name"
|
|
176
176
|
- pickerMultiple: 布尔值,是否允许多选,默认 false
|
|
177
|
-
- lookupFilters: 字符串,
|
|
177
|
+
- lookupFilters: 字符串,DevExpress JSON 格式的过滤表达式,支持 {fieldName} 占位符引用其他表单字段值(运行时自动替换为实际值)
|
|
178
|
+
- 静态过滤示例:'[["enablestate","=","2"],"and",["hidden","<>",true]]'
|
|
179
|
+
- 动态过滤示例(引用其他字段值):'[["company_id","=","{company_id}"],"and",["enablestate","=","2"]]' — 当表单中 company_id 字段值变化时,{company_id} 会被自动替换为该字段的实际值
|
|
180
|
+
- 格式规则:eq → "=",ne → "<>",gt → ">",ge → ">=",lt → "<",le → "<=",contains → "contains",startswith → "startswith"。布尔值不加引号(true/false)
|
|
178
181
|
- lookupDisplayFields: 数组,关联数据后额外展示的字段,格式 [{ "field": "字段API名", "label": "显示标签" }]
|
|
179
182
|
- lookupFillRules: 数组,填充规则,选择关联记录后将源字段值自动填充到表单其他字段。格式 [{ "sourceField": "关联对象的字段API名", "targetField": "当前表单中的目标字段name" }]
|
|
180
183
|
- 示例:选择合同后自动填充合同金额到表单的金额字段:lookupFillRules: [{ "sourceField": "amount", "targetField": "contract_amount" }]
|
|
@@ -252,6 +255,8 @@ if (field.name === 'quantity' || field.name === 'unit_price') {
|
|
|
252
255
|
10. 修改已有 grid 字段时,必须保留其完整的 gridData 和 children 数据,不要丢弃未被修改的子字段。如果用户未要求改动 grid 内容,应原样返回 grid 的所有属性
|
|
253
256
|
11. 每个字段的类型特有属性必须完整返回:number 类型的 min/max/precision/thousandSeparator、text/textarea 的 maxLength/minLength、lookup 的 reference_to/lookupLabelField/lookupFillRules、member/org 的 pickerFillRules、reference 的 reference_to/displayFields 等。不要遗漏这些属性
|
|
254
257
|
12. ⚠️ **type 与 formula 属性的严格对应**:只要字段包含公式表达式(formula 属性),其 type 就**必须**是 "formula",绝对不能是 "number"、"text" 或其他类型。"number" 类型仅用于用户手工输入数字的字段(无公式)。错误示例:{ type: "number", formula: "${a + b}" },正确示例:{ type: "formula", formula: "${a + b}", precision: 2 }。grid/table 的 children 子字段同样适用此规则
|
|
258
|
+
13. ⚠️ **升级场景禁止自动设置配色**:当用户提供了 instance_template、form_script、flow_events 等旧版数据进行升级转换时,section(分组)、grid(网格)、table(子表)字段**不要自动添加** colorScheme、sectionColor、titleColor 等配色属性。配色属性只有在用户明确要求设置颜色/配色/主题色时才添加。升级的目标是忠实还原旧版表单的结构和逻辑,不要擅自美化
|
|
259
|
+
14. **原样保留字段的 is_wide 属性**:is_wide 用于控制字段是否占满整行宽度。输出时必须保持每个字段的 is_wide 与输入完全一致:输入为 true 则输出 true,输入为 false 则输出 false,输入中没有该属性则不要添加。禁止擅自修改或统一设置 is_wide 的值
|
|
255
260
|
|
|
256
261
|
## 额外待转换的数据(升级场景)
|
|
257
262
|
|
|
@@ -377,15 +382,16 @@ if (field.name === 'quantity' || field.name === 'unit_price') {
|
|
|
377
382
|
|
|
378
383
|
### 旧公式字段跨对象引用迁移
|
|
379
384
|
- 旧版表单中,公式字段可能通过 \`{applicant.organization.name}\`、\`{applicant.name}\` 等语法引用成员的关联属性。这种跨对象引用在新版中不再通过公式实现,而是通过**成员字段的 pickerFillRules(填充规则)**来替代。
|
|
385
|
+
- ⚠️ **重要:applicant(申请人)是系统内置字段**,name 固定为 \`__applicant\`,不要创建新的 member 字段。在 fields 数组中输出 { "name": "__applicant", "label": "申请人", "type": "member", "pickerFillRules": [...] } 即可覆盖系统默认配置。该字段在拟稿状态下可编辑(用户可更换申请人),pickerFillRules 会在选人后自动触发填充。
|
|
380
386
|
- 迁移方式:
|
|
381
387
|
1. 将引用跨对象属性的公式字段改为**普通文本字段**(type: "text")作为目标字段
|
|
382
|
-
2.
|
|
388
|
+
2. 在 \`__applicant\` 字段上配置 **pickerFillRules**,使用点号表示法的 sourceField(如 "organization.name"),targetField 指向该文本字段
|
|
383
389
|
3. 如果 targetField 字段应为只读展示(即旧版是纯展示的公式字段),设置 readonly: true
|
|
384
390
|
- 迁移示例:
|
|
385
391
|
旧版公式字段:\`{applicant.organization.name}\` 用于显示申请人所属部门名称
|
|
386
392
|
新版迁移方式:
|
|
387
393
|
1. 创建文本字段 { "name": "dept_name", "label": "所属部门", "type": "text", "readonly": true }
|
|
388
|
-
2.
|
|
394
|
+
2. 输出 __applicant 字段:{ "name": "__applicant", "label": "申请人", "type": "member", "pickerFillRules": [{ "sourceField": "organization.name", "targetField": "dept_name" }] }
|
|
389
395
|
- 常见映射:
|
|
390
396
|
- \`{applicant.organization.name}\` → sourceField: "organization.name"
|
|
391
397
|
- \`{applicant.organization.fullname}\` → sourceField: "organization.fullname"
|
|
@@ -527,7 +527,10 @@ function buildSystemPrompt() {
|
|
|
527
527
|
- reference_to: 字符串,关联对象的 API 名称,如 "contracts"、"accounts"
|
|
528
528
|
- lookupLabelField: 字符串,用于搜索和显示的名称字段,默认 "name"
|
|
529
529
|
- pickerMultiple: 布尔值,是否允许多选,默认 false
|
|
530
|
-
- lookupFilters: 字符串,
|
|
530
|
+
- lookupFilters: 字符串,DevExpress JSON 格式的过滤表达式,支持 {fieldName} 占位符引用其他表单字段值(运行时自动替换为实际值)
|
|
531
|
+
- 静态过滤示例:'[["enablestate","=","2"],"and",["hidden","<>",true]]'
|
|
532
|
+
- 动态过滤示例(引用其他字段值):'[["company_id","=","{company_id}"],"and",["enablestate","=","2"]]' — 当表单中 company_id 字段值变化时,{company_id} 会被自动替换为该字段的实际值
|
|
533
|
+
- 格式规则:eq → "=",ne → "<>",gt → ">",ge → ">=",lt → "<",le → "<=",contains → "contains",startswith → "startswith"。布尔值不加引号(true/false)
|
|
531
534
|
- lookupDisplayFields: 数组,关联数据后额外展示的字段,格式 [{ "field": "字段API名", "label": "显示标签" }]
|
|
532
535
|
- lookupFillRules: 数组,填充规则,选择关联记录后将源字段值自动填充到表单其他字段。格式 [{ "sourceField": "关联对象的字段API名", "targetField": "当前表单中的目标字段name" }]
|
|
533
536
|
- 示例:选择合同后自动填充合同金额到表单的金额字段:lookupFillRules: [{ "sourceField": "amount", "targetField": "contract_amount" }]
|
|
@@ -612,6 +615,8 @@ if (field.name === 'quantity' || field.name === 'unit_price') {
|
|
|
612
615
|
7. 如果用户只要求修改字段,则 events 保持原样不变
|
|
613
616
|
8. 返回完整的 fields 数组和 events 对象
|
|
614
617
|
9. ⚠️ **type 与 formula 属性的严格对应**:只要字段包含公式表达式(formula 属性),其 type 就**必须**是 "formula",绝对不能是 "number"、"text" 或其他类型。"number" 类型仅用于用户手工输入数字的字段(无公式)。错误示例:{ type: "number", formula: "\${a + b}" },正确示例:{ type: "formula", formula: "\${a + b}", valueType: "number", precision: 2 }。grid/table 的 children 子字段同样适用此规则
|
|
618
|
+
10. ⚠️ **升级场景禁止自动设置配色**:当用户提供了 instance_template、form_script、flow_events 等旧版数据进行升级转换时,section(分组)、grid(网格)、table(子表)字段**不要自动添加** colorScheme、sectionColor、titleColor 等配色属性。配色属性只有在用户明确要求设置颜色/配色/主题色时才添加。升级的目标是忠实还原旧版表单的结构和逻辑,不要擅自美化
|
|
619
|
+
11. **原样保留字段的 is_wide 属性**:is_wide 用于控制字段是否占满整行宽度。输出时必须保持每个字段的 is_wide 与输入完全一致:输入为 true 则输出 true,输入为 false 则输出 false,输入中没有该属性则不要添加。禁止擅自修改或统一设置 is_wide 的值
|
|
615
620
|
|
|
616
621
|
## 额外待转换的数据(升级场景)
|
|
617
622
|
|
|
@@ -749,15 +754,16 @@ if (field.name === 'quantity' || field.name === 'unit_price') {
|
|
|
749
754
|
|
|
750
755
|
### 旧公式字段跨对象引用迁移
|
|
751
756
|
- 旧版表单中,公式字段可能通过 \`{applicant.organization.name}\`、\`{applicant.name}\` 等语法引用成员的关联属性。这种跨对象引用在新版中不再通过公式实现,而是通过**成员字段的 pickerFillRules(填充规则)**来替代。
|
|
757
|
+
- ⚠️ **重要:applicant(申请人)是系统内置字段**,name 固定为 \`__applicant\`,不要创建新的 member 字段。在 fields 数组中输出 { "name": "__applicant", "label": "申请人", "type": "member", "pickerFillRules": [...] } 即可覆盖系统默认配置。该字段在拟稿状态下可编辑(用户可更换申请人),pickerFillRules 会在选人后自动触发填充。
|
|
752
758
|
- 迁移方式:
|
|
753
759
|
1. 将引用跨对象属性的公式字段改为**普通文本字段**(type: "text")作为目标字段
|
|
754
|
-
2.
|
|
760
|
+
2. 在 \`__applicant\` 字段上配置 **pickerFillRules**,使用点号表示法的 sourceField(如 "organization.name"),targetField 指向该文本字段
|
|
755
761
|
3. 如果 targetField 字段应为只读展示(即旧版是纯展示的公式字段),设置 readonly: true
|
|
756
762
|
- 迁移示例:
|
|
757
763
|
旧版公式字段:\`{applicant.organization.name}\` 用于显示申请人所属部门名称
|
|
758
764
|
新版迁移方式:
|
|
759
765
|
1. 创建文本字段 { "name": "dept_name", "label": "所属部门", "type": "text", "readonly": true }
|
|
760
|
-
2.
|
|
766
|
+
2. 输出 __applicant 字段:{ "name": "__applicant", "label": "申请人", "type": "member", "pickerFillRules": [{ "sourceField": "organization.name", "targetField": "dept_name" }] }
|
|
761
767
|
- 常见映射:
|
|
762
768
|
- \`{applicant.organization.name}\` → sourceField: "organization.name"
|
|
763
769
|
- \`{applicant.organization.fullname}\` → sourceField: "organization.fullname"
|
|
@@ -155,7 +155,18 @@ router.post("/api/workflow/v2/instance/forward", requireAuthentication, async fu
|
|
|
155
155
|
const old_values = ins.values;
|
|
156
156
|
let new_values = {};
|
|
157
157
|
const form = await UUFlowManager.getForm(flow.form, { fields: { historys: 0 } });
|
|
158
|
-
const
|
|
158
|
+
const rawFields = form.current.fields || [];
|
|
159
|
+
|
|
160
|
+
// Normalize fields: ensure every field (and sub-fields) has `code` (fallback to `name`)
|
|
161
|
+
const normalizeFields = (list) => (list || []).map(f => {
|
|
162
|
+
const normalized = f.code ? f : { ...f, code: f.name };
|
|
163
|
+
if (['table', 'section'].includes(normalized.type) && _.isArray(normalized.fields)) {
|
|
164
|
+
normalized.fields = normalizeFields(normalized.fields);
|
|
165
|
+
}
|
|
166
|
+
return normalized;
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const fields = normalizeFields(rawFields);
|
|
159
170
|
|
|
160
171
|
const formCollection = await getCollection('forms')
|
|
161
172
|
|
|
@@ -176,6 +187,9 @@ router.post("/api/workflow/v2/instance/forward", requireAuthentication, async fu
|
|
|
176
187
|
}
|
|
177
188
|
}
|
|
178
189
|
|
|
190
|
+
// Normalize old_fields using the same helper
|
|
191
|
+
old_fields = normalizeFields(old_fields);
|
|
192
|
+
|
|
179
193
|
// Process fields
|
|
180
194
|
for (const field of fields) {
|
|
181
195
|
const exists_field = _.find(old_fields, f => f.type == field.type && f.code == field.code);
|
|
@@ -20,8 +20,8 @@ router.get('/api/workflow/v2/instance/:instanceId/permission', requireAuthentica
|
|
|
20
20
|
}
|
|
21
21
|
const user = await db.users.findOne({_id: current_user_id});
|
|
22
22
|
const instance = await db.instances.findOne({_id: instanceId})
|
|
23
|
-
console.log(`user`, user)
|
|
24
|
-
console.log(`instance`, instance)
|
|
23
|
+
// console.log(`user`, user)
|
|
24
|
+
// console.log(`instance`, instance)
|
|
25
25
|
const permission = await WorkflowManager.hasInstancePermissions(user, instance)
|
|
26
26
|
res.status(200).send({
|
|
27
27
|
data: {
|
|
@@ -147,10 +147,16 @@ const getCategoriesMonitor = async (userSession, req, currentUrl) => {
|
|
|
147
147
|
// Only the string 'false' will disable the filtering
|
|
148
148
|
const enableCategoryFilter = process.env.STEEDOS_WORKFLOW_ENABLE_CATEGORY_FILTER !== 'false';
|
|
149
149
|
|
|
150
|
+
// When set to 'false', skip monitor children (category/flow) computation, only show root node
|
|
151
|
+
// Defaults to the value of STEEDOS_WORKFLOW_ENABLE_CATEGORY_FILTER
|
|
152
|
+
const enableMonitorCategoryFilter = process.env.STEEDOS_WORKFLOW_ENABLE_MONITOR_CATEGORY_FILTER
|
|
153
|
+
? process.env.STEEDOS_WORKFLOW_ENABLE_MONITOR_CATEGORY_FILTER !== 'false'
|
|
154
|
+
: enableCategoryFilter;
|
|
155
|
+
|
|
150
156
|
try {
|
|
151
157
|
let flows = [];
|
|
152
158
|
|
|
153
|
-
if (!
|
|
159
|
+
if (!enableMonitorCategoryFilter) {
|
|
154
160
|
// When category filtering is disabled, return empty array (no sub-navigation)
|
|
155
161
|
// Only box-level items will be shown
|
|
156
162
|
// Still need to check permissions for hasFlowsPer (used to control monitor box visibility)
|
|
@@ -156,7 +156,7 @@ module.exports = {
|
|
|
156
156
|
break;
|
|
157
157
|
case 'pending':
|
|
158
158
|
filter.push(['state', '=', 'pending']);
|
|
159
|
-
filter.push([
|
|
159
|
+
filter.push(['submitterOrApplicant', '=', userId]);
|
|
160
160
|
break;
|
|
161
161
|
case 'completed':
|
|
162
162
|
filter.push(['submitter', '=', userId]);
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Business hours calculation utilities.
|
|
4
|
+
* Ported from steedos-platform packages/core/src/holidays/business_hours.ts
|
|
5
|
+
*/
|
|
6
|
+
const moment = require('moment');
|
|
7
|
+
|
|
8
|
+
// BusinessHoursCheckedType enum values
|
|
9
|
+
const BusinessHoursCheckedType = {
|
|
10
|
+
onAm: 1, // 上午班
|
|
11
|
+
onPm: 2, // 下午班
|
|
12
|
+
offDay: 0, // 非工作日,即节假日、周未等
|
|
13
|
+
offAm: -1, // 工作日,下班时间,非午休,早上0点到上午上班时间
|
|
14
|
+
offPm: -2, // 工作日,下班时间,非午休,下午下班时间到第二天早上0点
|
|
15
|
+
offLunch: -3 // 工作日,午休时间
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 把09:33这种字符串解析出来对应的时间值
|
|
20
|
+
* @param {string} str
|
|
21
|
+
* @param {number} digitsForHours
|
|
22
|
+
* @returns {object} { hours, minutes, valueToHours, valueToMinutes }
|
|
23
|
+
*/
|
|
24
|
+
function getStringTimeValue(str, digitsForHours = 2) {
|
|
25
|
+
str = str.trim();
|
|
26
|
+
if (!/^\d{1,2}:\d{1,2}$/.test(str)) {
|
|
27
|
+
throw new Error("getStringTimeValue:Time format error, please enter HH:MM this format of 24 hours time character.");
|
|
28
|
+
}
|
|
29
|
+
const splits = str.split(":");
|
|
30
|
+
const h = parseInt(splits[0]);
|
|
31
|
+
const m = parseInt(splits[1]);
|
|
32
|
+
if (h > 24 || m > 60) {
|
|
33
|
+
throw new Error("getStringTimeValue:Time format error, please enter HH:MM this format of 24 hours time character.");
|
|
34
|
+
} else if (h === 24 && m > 0) {
|
|
35
|
+
throw new Error("getStringTimeValue:Time format error, please enter HH:MM this format of 24 hours time character.");
|
|
36
|
+
}
|
|
37
|
+
const valueToMinutes = h * 60 + m;
|
|
38
|
+
const valueToHours = Number((valueToMinutes / 60).toFixed(digitsForHours));
|
|
39
|
+
return { hours: h, minutes: m, valueToHours, valueToMinutes };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 计算09:00-18:00这种开始时间结束时间代表的每天工作时间长度
|
|
44
|
+
*/
|
|
45
|
+
function computeBusinessHoursPerDay(start, end, lunch_start, lunch_end, digitsForHours = 2) {
|
|
46
|
+
const startValue = getStringTimeValue(start, digitsForHours);
|
|
47
|
+
const endValue = getStringTimeValue(end, digitsForHours);
|
|
48
|
+
const lunchStartValue = getStringTimeValue(lunch_start, digitsForHours);
|
|
49
|
+
const lunchEndValue = getStringTimeValue(lunch_end, digitsForHours);
|
|
50
|
+
if (startValue && endValue && lunchStartValue && lunchEndValue) {
|
|
51
|
+
let computedMinutes = endValue.valueToMinutes - startValue.valueToMinutes;
|
|
52
|
+
const computedAmMinutes = lunchStartValue.valueToMinutes - startValue.valueToMinutes;
|
|
53
|
+
const computedPmMinutes = endValue.valueToMinutes - lunchEndValue.valueToMinutes;
|
|
54
|
+
const computedLunchMinutes = lunchEndValue.valueToMinutes - lunchStartValue.valueToMinutes;
|
|
55
|
+
computedMinutes = computedMinutes - computedLunchMinutes;
|
|
56
|
+
if (computedMinutes <= 0 || computedLunchMinutes <= 0) {
|
|
57
|
+
throw new Error("computeBusinessHoursPerDay:The end or lunch_end time value must be later than the start or lunch_start time.");
|
|
58
|
+
} else if (lunchStartValue.valueToMinutes <= startValue.valueToMinutes || lunchEndValue.valueToMinutes >= endValue.valueToMinutes) {
|
|
59
|
+
throw new Error("computeBusinessHoursPerDay:The lunch time must between the working time.");
|
|
60
|
+
} else {
|
|
61
|
+
const computedHours = Number((computedMinutes / 60).toFixed(digitsForHours));
|
|
62
|
+
const computedAmHours = Number((computedAmMinutes / 60).toFixed(digitsForHours));
|
|
63
|
+
const computedPmHours = Number((computedPmMinutes / 60).toFixed(digitsForHours));
|
|
64
|
+
const computedLunchHours = Number((computedLunchMinutes / 60).toFixed(digitsForHours));
|
|
65
|
+
return {
|
|
66
|
+
computedHours, computedMinutes,
|
|
67
|
+
computedAmHours, computedAmMinutes,
|
|
68
|
+
computedPmHours, computedPmMinutes,
|
|
69
|
+
computedLunchHours, computedLunchMinutes,
|
|
70
|
+
startValue, endValue, lunchStartValue, lunchEndValue
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
throw new Error("computeBusinessHoursPerDay:start or end is not valid.");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 根据businessHours取其对应的每天工作时间长度,并把值缓存到其computedPerDay属性中
|
|
80
|
+
*/
|
|
81
|
+
function getBusinessHoursPerDay(businessHours, digitsForHours = 2) {
|
|
82
|
+
if (businessHours.computedPerDay) {
|
|
83
|
+
return businessHours.computedPerDay;
|
|
84
|
+
}
|
|
85
|
+
const computedPerDay = computeBusinessHoursPerDay(
|
|
86
|
+
businessHours.start, businessHours.end,
|
|
87
|
+
businessHours.lunch_start, businessHours.lunch_end,
|
|
88
|
+
digitsForHours
|
|
89
|
+
);
|
|
90
|
+
businessHours.computedPerDay = computedPerDay;
|
|
91
|
+
return computedPerDay;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 计算某个日期是不是工作日
|
|
96
|
+
*/
|
|
97
|
+
function computeIsBusinessDay(date, holidays, workingDays) {
|
|
98
|
+
const value = moment.utc(date);
|
|
99
|
+
value.hours(0);
|
|
100
|
+
value.minutes(0);
|
|
101
|
+
value.seconds(0);
|
|
102
|
+
value.milliseconds(0);
|
|
103
|
+
const holiday = holidays.find((item) => {
|
|
104
|
+
return item.date && item.date.getTime() === value.toDate().getTime();
|
|
105
|
+
});
|
|
106
|
+
if (holiday) {
|
|
107
|
+
switch (holiday.type) {
|
|
108
|
+
case "adjusted_working_day":
|
|
109
|
+
return true;
|
|
110
|
+
case "adjusted_holiday":
|
|
111
|
+
return false;
|
|
112
|
+
case "public":
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
return workingDays.indexOf(value.day().toString()) > -1;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 计算某个时间点是不是工作时间(只判断时间点,不判断日期)
|
|
122
|
+
*/
|
|
123
|
+
function computeIsBusinessHours(date, businessHours, digitsForHours = 2) {
|
|
124
|
+
const utcOffset = businessHours.utc_offset;
|
|
125
|
+
const businessHoursPerDay = getBusinessHoursPerDay(businessHours, digitsForHours);
|
|
126
|
+
const startMoment = moment.utc(date);
|
|
127
|
+
startMoment.hours(businessHoursPerDay.startValue.hours - utcOffset);
|
|
128
|
+
startMoment.minutes(businessHoursPerDay.startValue.minutes);
|
|
129
|
+
const endMoment = moment.utc(date);
|
|
130
|
+
endMoment.hours(businessHoursPerDay.endValue.hours - utcOffset);
|
|
131
|
+
endMoment.minutes(businessHoursPerDay.endValue.minutes);
|
|
132
|
+
const lunchStartMoment = moment.utc(date);
|
|
133
|
+
lunchStartMoment.hours(businessHoursPerDay.lunchStartValue.hours - utcOffset);
|
|
134
|
+
lunchStartMoment.minutes(businessHoursPerDay.lunchStartValue.minutes);
|
|
135
|
+
const lunchEndMoment = moment.utc(date);
|
|
136
|
+
lunchEndMoment.hours(businessHoursPerDay.lunchEndValue.hours - utcOffset);
|
|
137
|
+
lunchEndMoment.minutes(businessHoursPerDay.lunchEndValue.minutes);
|
|
138
|
+
const startTimeValue = startMoment.toDate().getTime();
|
|
139
|
+
const endTimeValue = endMoment.toDate().getTime();
|
|
140
|
+
const lunchStartTimeValue = lunchStartMoment.toDate().getTime();
|
|
141
|
+
const lunchEndTimeValue = lunchEndMoment.toDate().getTime();
|
|
142
|
+
const dateTimeValue = date.getTime();
|
|
143
|
+
if (dateTimeValue <= endTimeValue && dateTimeValue >= startTimeValue) {
|
|
144
|
+
if (dateTimeValue <= lunchStartTimeValue) {
|
|
145
|
+
return BusinessHoursCheckedType.onAm;
|
|
146
|
+
} else if (dateTimeValue >= lunchEndTimeValue) {
|
|
147
|
+
return BusinessHoursCheckedType.onPm;
|
|
148
|
+
} else {
|
|
149
|
+
return BusinessHoursCheckedType.offLunch;
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
if (dateTimeValue > endTimeValue) {
|
|
153
|
+
return BusinessHoursCheckedType.offPm;
|
|
154
|
+
} else {
|
|
155
|
+
return BusinessHoursCheckedType.offAm;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* 计算某个时间点是不是工作时间,包括节假日及非工作时间都要排除
|
|
162
|
+
*/
|
|
163
|
+
function computeIsBusinessDate(date, holidays, businessHours, digitsForHours = 2) {
|
|
164
|
+
if (computeIsBusinessDay(date, holidays, businessHours.working_days)) {
|
|
165
|
+
return computeIsBusinessHours(date, businessHours, digitsForHours);
|
|
166
|
+
} else {
|
|
167
|
+
return BusinessHoursCheckedType.offDay;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 根据来源时间所在日期计算下一个工作日的开始时间
|
|
173
|
+
*/
|
|
174
|
+
function computeNextBusinessDate(source, holidays, businessHours, digitsForHours = 2) {
|
|
175
|
+
const utcOffset = businessHours.utc_offset;
|
|
176
|
+
const businessHoursPerDay = getBusinessHoursPerDay(businessHours, digitsForHours);
|
|
177
|
+
const workingDays = businessHours.working_days;
|
|
178
|
+
const sourceMoment = moment.utc(source);
|
|
179
|
+
sourceMoment.hours(businessHoursPerDay.startValue.hours - utcOffset);
|
|
180
|
+
sourceMoment.minutes(businessHoursPerDay.startValue.minutes);
|
|
181
|
+
let startMoment = null;
|
|
182
|
+
const maxCount = 365;
|
|
183
|
+
for (let i = 0; i < maxCount; i++) {
|
|
184
|
+
sourceMoment.add(1, 'd');
|
|
185
|
+
if (computeIsBusinessDay(sourceMoment.toDate(), holidays, workingDays)) {
|
|
186
|
+
startMoment = sourceMoment;
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (startMoment) {
|
|
191
|
+
const start = startMoment.toDate();
|
|
192
|
+
startMoment.hours(businessHoursPerDay.endValue.hours - utcOffset);
|
|
193
|
+
startMoment.minutes(businessHoursPerDay.endValue.minutes);
|
|
194
|
+
const end = startMoment.toDate();
|
|
195
|
+
return { start, end };
|
|
196
|
+
} else {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
module.exports = {
|
|
202
|
+
BusinessHoursCheckedType,
|
|
203
|
+
getStringTimeValue,
|
|
204
|
+
computeBusinessHoursPerDay,
|
|
205
|
+
getBusinessHoursPerDay,
|
|
206
|
+
computeIsBusinessDay,
|
|
207
|
+
computeIsBusinessHours,
|
|
208
|
+
computeIsBusinessDate,
|
|
209
|
+
computeNextBusinessDate
|
|
210
|
+
};
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Business timeout calculation utilities.
|
|
4
|
+
* Ported from steedos-platform packages/core/src/holidays/business_timeout.ts
|
|
5
|
+
*
|
|
6
|
+
* Uses @steedos/objectql (already a project dependency) to access holidays and
|
|
7
|
+
* business_hours objects, instead of @steedos/core.
|
|
8
|
+
*/
|
|
9
|
+
const moment = require('moment');
|
|
10
|
+
const _ = require('lodash');
|
|
11
|
+
const { getObject } = require('@steedos/objectql');
|
|
12
|
+
const {
|
|
13
|
+
BusinessHoursCheckedType,
|
|
14
|
+
getBusinessHoursPerDay,
|
|
15
|
+
computeIsBusinessDate,
|
|
16
|
+
computeNextBusinessDate
|
|
17
|
+
} = require('./business_hours');
|
|
18
|
+
|
|
19
|
+
// Simple in-memory cache for holidays and business hours with TTL
|
|
20
|
+
const CACHE_TTL_MS = 10 * 60 * 1000; // 10 minutes
|
|
21
|
+
const _holidaysCache = {}; // key: `${spaceId}-${year}`, value: { values: [...], timestamp }
|
|
22
|
+
const _businessHoursCache = {}; // key: spaceId, value: { records: [...], timestamp }
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 从数据库中取出指定工作区内的节假日数据,支持缓存
|
|
26
|
+
* @param {string} spaceId
|
|
27
|
+
* @param {Array<number>} years
|
|
28
|
+
*/
|
|
29
|
+
async function getHolidays(spaceId, years) {
|
|
30
|
+
const holidaysFields = ["name", "type", "date", "adjusted_to", "space"];
|
|
31
|
+
for (const year of years) {
|
|
32
|
+
const cacheKey = `${spaceId}-${year}`;
|
|
33
|
+
const cached = _holidaysCache[cacheKey];
|
|
34
|
+
if (!cached || (Date.now() - cached.timestamp > CACHE_TTL_MS)) {
|
|
35
|
+
const yearStartDate = moment.utc(`${year}-01-01T00:00:00Z`).toDate();
|
|
36
|
+
const yearEndDate = moment.utc(`${year}-12-31T23:59:59Z`).toDate();
|
|
37
|
+
const objectHolidays = getObject("holidays");
|
|
38
|
+
const holidaysRecords = await objectHolidays.find({
|
|
39
|
+
filters: [["space", "=", spaceId], ["date", "between", [yearStartDate, yearEndDate]]],
|
|
40
|
+
fields: holidaysFields
|
|
41
|
+
});
|
|
42
|
+
_holidaysCache[cacheKey] = {
|
|
43
|
+
space: spaceId,
|
|
44
|
+
year: year,
|
|
45
|
+
values: (holidaysRecords && holidaysRecords.length) ? holidaysRecords : [],
|
|
46
|
+
timestamp: Date.now()
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const result = [];
|
|
51
|
+
for (const year of years) {
|
|
52
|
+
const cacheKey = `${spaceId}-${year}`;
|
|
53
|
+
if (_holidaysCache[cacheKey] && _holidaysCache[cacheKey].values) {
|
|
54
|
+
result.push(..._holidaysCache[cacheKey].values);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 从数据库中取出指定工作区内的默认工作时间数据,支持缓存
|
|
62
|
+
* @param {string} spaceId
|
|
63
|
+
*/
|
|
64
|
+
async function getDefaultBusinessHours(spaceId) {
|
|
65
|
+
const cached = _businessHoursCache[spaceId];
|
|
66
|
+
if (cached && (Date.now() - cached.timestamp <= CACHE_TTL_MS)) {
|
|
67
|
+
return cached.records;
|
|
68
|
+
}
|
|
69
|
+
const objectBusinessHours = getObject("business_hours");
|
|
70
|
+
const records = await objectBusinessHours.find({
|
|
71
|
+
filters: [["space", "=", spaceId], ["is_default", "=", true]],
|
|
72
|
+
fields: ["name", "start", "end", "lunch_start", "lunch_end", "utc_offset", "working_days", "space"]
|
|
73
|
+
});
|
|
74
|
+
if (records && records.length) {
|
|
75
|
+
_businessHoursCache[spaceId] = { records, timestamp: Date.now() };
|
|
76
|
+
}
|
|
77
|
+
return records || [];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 计算以某个时间点开始,超时几个小时后,超时时间点是什么时候 (纯计算,不查数据库)
|
|
82
|
+
* @param {Date} start
|
|
83
|
+
* @param {number} timeoutHours
|
|
84
|
+
* @param {Array} holidays
|
|
85
|
+
* @param {object} businessHours
|
|
86
|
+
* @param {number} digitsForHours
|
|
87
|
+
* @returns {Date}
|
|
88
|
+
*/
|
|
89
|
+
function computeTimeoutDateWithoutHolidays(start, timeoutHours, holidays, businessHours, digitsForHours = 2) {
|
|
90
|
+
if (timeoutHours <= 0) {
|
|
91
|
+
return start;
|
|
92
|
+
}
|
|
93
|
+
const utcOffset = businessHours.utc_offset;
|
|
94
|
+
const businessHoursPerDay = getBusinessHoursPerDay(businessHours, digitsForHours);
|
|
95
|
+
const startBhct = computeIsBusinessDate(start, holidays, businessHours, digitsForHours);
|
|
96
|
+
let offsetMinutes = 0;
|
|
97
|
+
let startMoment = moment.utc(start);
|
|
98
|
+
let startClosingMoment = moment.utc(start);
|
|
99
|
+
if (startBhct <= 0) {
|
|
100
|
+
if (startBhct === BusinessHoursCheckedType.offLunch) {
|
|
101
|
+
startMoment.hours(businessHoursPerDay.lunchEndValue.hours - utcOffset);
|
|
102
|
+
startMoment.minutes(businessHoursPerDay.lunchEndValue.minutes);
|
|
103
|
+
start = startMoment.toDate();
|
|
104
|
+
} else if (startBhct === BusinessHoursCheckedType.offAm) {
|
|
105
|
+
startMoment.hours(businessHoursPerDay.startValue.hours - utcOffset);
|
|
106
|
+
startMoment.minutes(businessHoursPerDay.startValue.minutes);
|
|
107
|
+
start = startMoment.toDate();
|
|
108
|
+
startClosingMoment = moment.utc(start);
|
|
109
|
+
startClosingMoment.hours(businessHoursPerDay.lunchStartValue.hours - utcOffset);
|
|
110
|
+
startClosingMoment.minutes(businessHoursPerDay.lunchStartValue.minutes);
|
|
111
|
+
offsetMinutes = startClosingMoment.diff(startMoment, 'minute');
|
|
112
|
+
startMoment.hours(businessHoursPerDay.lunchEndValue.hours - utcOffset);
|
|
113
|
+
startMoment.minutes(businessHoursPerDay.lunchEndValue.minutes);
|
|
114
|
+
start = startMoment.toDate();
|
|
115
|
+
} else {
|
|
116
|
+
const nextBusinessDate = computeNextBusinessDate(start, holidays, businessHours, digitsForHours);
|
|
117
|
+
start = nextBusinessDate.start;
|
|
118
|
+
startMoment = moment.utc(start);
|
|
119
|
+
startClosingMoment = moment.utc(start);
|
|
120
|
+
startClosingMoment.hours(businessHoursPerDay.lunchStartValue.hours - utcOffset);
|
|
121
|
+
startClosingMoment.minutes(businessHoursPerDay.lunchStartValue.minutes);
|
|
122
|
+
offsetMinutes = startClosingMoment.diff(startMoment, 'minute');
|
|
123
|
+
startMoment.hours(businessHoursPerDay.lunchEndValue.hours - utcOffset);
|
|
124
|
+
startMoment.minutes(businessHoursPerDay.lunchEndValue.minutes);
|
|
125
|
+
start = startMoment.toDate();
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
if (startBhct === BusinessHoursCheckedType.onAm) {
|
|
129
|
+
startClosingMoment.hours(businessHoursPerDay.lunchStartValue.hours - utcOffset);
|
|
130
|
+
startClosingMoment.minutes(businessHoursPerDay.lunchStartValue.minutes);
|
|
131
|
+
offsetMinutes = startClosingMoment.diff(startMoment, 'minute');
|
|
132
|
+
startMoment.hours(businessHoursPerDay.lunchEndValue.hours - utcOffset);
|
|
133
|
+
startMoment.minutes(businessHoursPerDay.lunchEndValue.minutes);
|
|
134
|
+
start = startMoment.toDate();
|
|
135
|
+
} else {
|
|
136
|
+
startMoment = moment.utc(start);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
startClosingMoment.hours(businessHoursPerDay.endValue.hours - utcOffset);
|
|
140
|
+
startClosingMoment.minutes(businessHoursPerDay.endValue.minutes);
|
|
141
|
+
offsetMinutes += startClosingMoment.diff(startMoment, 'minute');
|
|
142
|
+
const timeoutMinutes = timeoutHours * 60;
|
|
143
|
+
if (timeoutMinutes <= offsetMinutes) {
|
|
144
|
+
let subMinutes = offsetMinutes - timeoutMinutes;
|
|
145
|
+
if (subMinutes > 0) {
|
|
146
|
+
if (subMinutes >= businessHoursPerDay.computedPmMinutes) {
|
|
147
|
+
subMinutes += businessHoursPerDay.computedLunchMinutes;
|
|
148
|
+
}
|
|
149
|
+
startClosingMoment.subtract(subMinutes, 'm');
|
|
150
|
+
}
|
|
151
|
+
return startClosingMoment.toDate();
|
|
152
|
+
} else {
|
|
153
|
+
let nextMoment = startClosingMoment;
|
|
154
|
+
for (let i = 0; offsetMinutes < timeoutMinutes; i++) {
|
|
155
|
+
const nextBusinessDate = computeNextBusinessDate(nextMoment.toDate(), holidays, businessHours, digitsForHours);
|
|
156
|
+
if (nextBusinessDate) {
|
|
157
|
+
nextMoment = moment.utc(nextBusinessDate.end);
|
|
158
|
+
offsetMinutes += businessHoursPerDay.computedMinutes;
|
|
159
|
+
} else {
|
|
160
|
+
throw new Error("computeTimeoutDateWithoutHolidays:Maximum number of calls, The number of days in holidays may exceed 365.");
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (offsetMinutes > timeoutMinutes) {
|
|
164
|
+
let subMinutes = offsetMinutes - timeoutMinutes;
|
|
165
|
+
if (subMinutes >= businessHoursPerDay.computedPmMinutes) {
|
|
166
|
+
subMinutes += businessHoursPerDay.computedLunchMinutes;
|
|
167
|
+
}
|
|
168
|
+
nextMoment.subtract(subMinutes, 'm');
|
|
169
|
+
return nextMoment.toDate();
|
|
170
|
+
}
|
|
171
|
+
return nextMoment.toDate();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* 计算在某个工作区下的节假日、工作时间数据基础上,以某个时间点开始,超时几个小时后,超时时间点是什么时候
|
|
177
|
+
* @param {Date} start
|
|
178
|
+
* @param {number} timeoutHours
|
|
179
|
+
* @param {string} spaceId
|
|
180
|
+
* @param {number} digitsForHours
|
|
181
|
+
* @returns {Promise<Date>}
|
|
182
|
+
*/
|
|
183
|
+
async function getTimeoutDateWithoutHolidays(start, timeoutHours, spaceId, digitsForHours = 2) {
|
|
184
|
+
let enableHolidays = false;
|
|
185
|
+
try {
|
|
186
|
+
const { getSteedosConfig } = require('@steedos/objectql');
|
|
187
|
+
const config = getSteedosConfig();
|
|
188
|
+
enableHolidays = !!config.enable_holidays;
|
|
189
|
+
} catch (e) {
|
|
190
|
+
// If config is not available, treat as holidays not enabled
|
|
191
|
+
}
|
|
192
|
+
if (!enableHolidays) {
|
|
193
|
+
return moment(start).add(timeoutHours, 'h').toDate();
|
|
194
|
+
}
|
|
195
|
+
const defaultBusinessHoursRecords = await getDefaultBusinessHours(spaceId);
|
|
196
|
+
const defaultBusinessHoursRecord = defaultBusinessHoursRecords && defaultBusinessHoursRecords[0];
|
|
197
|
+
if (!defaultBusinessHoursRecord) {
|
|
198
|
+
// 未配置工作时间,直接按未开启enable_holidays处理
|
|
199
|
+
return moment(start).add(timeoutHours, 'h').toDate();
|
|
200
|
+
}
|
|
201
|
+
const startUTCYear = start.getUTCFullYear();
|
|
202
|
+
const holidaysRecords = await getHolidays(spaceId, [startUTCYear, startUTCYear + 1]);
|
|
203
|
+
return computeTimeoutDateWithoutHolidays(start, timeoutHours, holidaysRecords, defaultBusinessHoursRecord, digitsForHours);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
module.exports = {
|
|
207
|
+
getHolidays,
|
|
208
|
+
getDefaultBusinessHours,
|
|
209
|
+
getTimeoutDateWithoutHolidays,
|
|
210
|
+
computeTimeoutDateWithoutHolidays
|
|
211
|
+
};
|