feat(mobile-oa-h5): port OA kit runtime and wire duration sync on oa-start
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
b638c787b1
commit
e83d925994
19 files changed
+1941
-14
No files matched your search
@@ -52,4 +52,5 @@
|
|||||||
- [x] `GET /api/mini/menus` + SQL 种子 `docs/sql/mobile_menu_oa_start.sql` + 首页拉菜单(失败时 fallback)
|
- [x] `GET /api/mini/menus` + SQL 种子 `docs/sql/mobile_menu_oa_start.sql` + 首页拉菜单(失败时 fallback)
|
||||||
- [ ] 测试库执行 SQL 并在角色中勾选「发起审批」
|
- [ ] 测试库执行 SQL 并在角色中勾选「发起审批」
|
||||||
- [ ] 真机:列表 → 填写 → 草稿/提交 全链路
|
- [ ] 真机:列表 → 填写 → 草稿/提交 全链路
|
||||||
- [ ] Task 8 套件运行时(请假/加班时长等)与 Web 对齐(分期)
|
- [x] Task 8 套件运行时(请假/加班/出差/转正/离职/owner 默认值)已移植并接入 `/oa-start`(节假日走 `/cn-holiday/range`)
|
||||||
|
- [ ] 真机:含套件模板与 Web 对照回归
|
||||||
@@ -79,4 +79,12 @@ export function fetchStartOaInstance(data) {
|
|||||||
return post('/web/admin/oa-approval/instances', data)
|
return post('/web/admin/oa-approval/instances', data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function fetchCurrentUser() {
|
||||||
|
return get('/web/auth/current-user')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchCnHolidayRange(start, end) {
|
||||||
|
return get('/cn-holiday/range', { start, end })
|
||||||
|
}
|
||||||
|
|
||||||
export { post, put }
|
export { post, put }
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/** 套件运行时 re-export(自 web-admin 移植) */
|
||||||
|
export {
|
||||||
|
applyLeaveKitDurationsToFormData,
|
||||||
|
ensureLeaveKitRuleProps,
|
||||||
|
LEAVE_MANUAL_PREFIX,
|
||||||
|
listLeaveKitBindings,
|
||||||
|
markLeaveDurationManual
|
||||||
|
} from './kit/oa-leave-kit-runtime.js'
|
||||||
|
|
||||||
|
export {
|
||||||
|
applyOvertimeKitDefaults,
|
||||||
|
applyOvertimeKitDurationsToFormData,
|
||||||
|
ensureOvertimeKitRuleProps,
|
||||||
|
listOvertimeKitBindings,
|
||||||
|
markOvertimeDurationManual,
|
||||||
|
OVERTIME_MANUAL_PREFIX
|
||||||
|
} from './kit/oa-overtime-kit-runtime.js'
|
||||||
|
|
||||||
|
export {
|
||||||
|
applyTripKitDaysToFormData,
|
||||||
|
listTripKits,
|
||||||
|
TRIP_MANUAL_PREFIX
|
||||||
|
} from './kit/oa-trip-kit-runtime.js'
|
||||||
|
|
||||||
|
export {
|
||||||
|
applyConfirmationKitDateLinkage,
|
||||||
|
applyConfirmationKitDefaults,
|
||||||
|
ensureConfirmationKitVisibility,
|
||||||
|
listConfirmationKitBindings,
|
||||||
|
listManualConfirmationDateFields,
|
||||||
|
markConfirmationDateManual,
|
||||||
|
CONFIRMATION_MANUAL_PREFIX
|
||||||
|
} from './kit/oa-confirmation-kit-runtime.js'
|
||||||
|
|
||||||
|
export { applyResignationKitDefaults } from './kit/oa-resignation-kit-runtime.js'
|
||||||
|
|
||||||
|
export {
|
||||||
|
applyOwnerFieldDefaults,
|
||||||
|
ensureOwnerFieldRuleProps
|
||||||
|
} from './kit/oa-owner-field-defaults.js'
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
function addMonthsYmd(hireYmd, months) {
|
||||||
|
const s = String(hireYmd || "").trim();
|
||||||
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(s)) return null;
|
||||||
|
if (!Number.isInteger(months) || months < 0) return null;
|
||||||
|
const [y, m, d] = s.split("-").map((x) => Number(x));
|
||||||
|
if (!y || !m || !d) return null;
|
||||||
|
const total = y * 12 + (m - 1) + months;
|
||||||
|
const ny = Math.floor(total / 12);
|
||||||
|
const nm = total % 12 + 1;
|
||||||
|
const lastDay = new Date(ny, nm, 0).getDate();
|
||||||
|
const nd = Math.min(d, lastDay);
|
||||||
|
return `${ny}-${String(nm).padStart(2, "0")}-${String(nd).padStart(2, "0")}`;
|
||||||
|
}
|
||||||
|
export {
|
||||||
|
addMonthsYmd
|
||||||
|
};
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
let _uidSeq = 0;
|
||||||
|
function uniqueId() {
|
||||||
|
const num = 370 + ++_uidSeq;
|
||||||
|
return "F" + Math.random().toString(36).slice(3, 6) + Number(`${Date.now()}`).toString(36) + num.toString(36) + "r";
|
||||||
|
}
|
||||||
|
const CONFIRMATION_KIT_FIELD_DEFS = [
|
||||||
|
{ key: "hireDate", title: "\u5165\u804C\u65E5\u671F", showProp: "showHireDate" },
|
||||||
|
{ key: "probationMonths", title: "\u8BD5\u7528\u671F\uFF08\u6708\uFF09", showProp: "showProbationMonths" },
|
||||||
|
{ key: "confirmationDate", title: "\u8F6C\u6B63\u65E5\u671F", showProp: "showConfirmationDate" },
|
||||||
|
{ key: "post", title: "\u804C\u4F4D", showProp: "showPost" },
|
||||||
|
{ key: "jobLevel", title: "\u5C97\u4F4D\u804C\u7EA7", showProp: "showJobLevel" },
|
||||||
|
{ key: "name", title: "\u59D3\u540D", showProp: "showName" },
|
||||||
|
{ key: "email", title: "\u90AE\u7BB1", showProp: "showEmail" },
|
||||||
|
{ key: "dept", title: "\u90E8\u95E8", showProp: "showDept" },
|
||||||
|
{ key: "phone", title: "\u624B\u673A\u53F7", showProp: "showPhone" }
|
||||||
|
];
|
||||||
|
function buildConfirmationKitChildren() {
|
||||||
|
const hireDateField = uniqueId();
|
||||||
|
const probationMonthsField = uniqueId();
|
||||||
|
const confirmationDateField = uniqueId();
|
||||||
|
const postField = uniqueId();
|
||||||
|
const jobLevelField = uniqueId();
|
||||||
|
const nameField = uniqueId();
|
||||||
|
const emailField = uniqueId();
|
||||||
|
const deptField = uniqueId();
|
||||||
|
const phoneField = uniqueId();
|
||||||
|
return {
|
||||||
|
hireDateField,
|
||||||
|
probationMonthsField,
|
||||||
|
confirmationDateField,
|
||||||
|
postField,
|
||||||
|
jobLevelField,
|
||||||
|
nameField,
|
||||||
|
emailField,
|
||||||
|
deptField,
|
||||||
|
phoneField,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: "datePicker",
|
||||||
|
field: hireDateField,
|
||||||
|
title: "\u5165\u804C\u65E5\u671F",
|
||||||
|
info: "",
|
||||||
|
$required: true,
|
||||||
|
props: {
|
||||||
|
_fc_kit_field: "hireDate",
|
||||||
|
type: "date",
|
||||||
|
format: "YYYY-MM-DD",
|
||||||
|
valueFormat: "YYYY-MM-DD",
|
||||||
|
placeholder: "\u8BF7\u9009\u62E9\u5165\u804C\u65E5\u671F"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "inputNumber",
|
||||||
|
field: probationMonthsField,
|
||||||
|
title: "\u8BD5\u7528\u671F\uFF08\u6708\uFF09",
|
||||||
|
info: "",
|
||||||
|
$required: true,
|
||||||
|
props: {
|
||||||
|
_fc_kit_field: "probationMonths",
|
||||||
|
min: 0,
|
||||||
|
precision: 0,
|
||||||
|
controlsPosition: "right"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "datePicker",
|
||||||
|
field: confirmationDateField,
|
||||||
|
title: "\u8F6C\u6B63\u65E5\u671F",
|
||||||
|
info: "\u53EF\u6839\u636E\u5165\u804C\u65E5\u671F\u4E0E\u8BD5\u7528\u671F\u81EA\u52A8\u5EFA\u8BAE\uFF1B\u624B\u6539\u540E\u4E0D\u518D\u81EA\u52A8\u8054\u52A8",
|
||||||
|
$required: false,
|
||||||
|
props: {
|
||||||
|
_fc_kit_field: "confirmationDate",
|
||||||
|
type: "date",
|
||||||
|
format: "YYYY-MM-DD",
|
||||||
|
valueFormat: "YYYY-MM-DD",
|
||||||
|
placeholder: "\u8BF7\u9009\u62E9\u8F6C\u6B63\u65E5\u671F"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "post",
|
||||||
|
field: postField,
|
||||||
|
title: "\u804C\u4F4D",
|
||||||
|
info: "",
|
||||||
|
$required: false,
|
||||||
|
props: {
|
||||||
|
_fc_kit_field: "post",
|
||||||
|
multiple: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "input",
|
||||||
|
field: jobLevelField,
|
||||||
|
title: "\u5C97\u4F4D\u804C\u7EA7",
|
||||||
|
info: "",
|
||||||
|
$required: false,
|
||||||
|
props: {
|
||||||
|
_fc_kit_field: "jobLevel",
|
||||||
|
placeholder: "\u8BF7\u8F93\u5165\u5C97\u4F4D\u804C\u7EA7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "input",
|
||||||
|
field: nameField,
|
||||||
|
title: "\u59D3\u540D",
|
||||||
|
info: "",
|
||||||
|
$required: true,
|
||||||
|
props: {
|
||||||
|
_fc_kit_field: "name",
|
||||||
|
placeholder: "\u8BF7\u8F93\u5165\u59D3\u540D"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "input",
|
||||||
|
field: emailField,
|
||||||
|
title: "\u90AE\u7BB1",
|
||||||
|
info: "",
|
||||||
|
$required: false,
|
||||||
|
props: {
|
||||||
|
_fc_kit_field: "email",
|
||||||
|
placeholder: "\u8BF7\u8F93\u5165\u90AE\u7BB1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "dept",
|
||||||
|
field: deptField,
|
||||||
|
title: "\u90E8\u95E8",
|
||||||
|
info: "",
|
||||||
|
$required: true,
|
||||||
|
props: {
|
||||||
|
_fc_kit_field: "dept",
|
||||||
|
multiple: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "input",
|
||||||
|
field: phoneField,
|
||||||
|
title: "\u624B\u673A\u53F7",
|
||||||
|
info: "",
|
||||||
|
$required: false,
|
||||||
|
props: {
|
||||||
|
_fc_kit_field: "phone",
|
||||||
|
placeholder: "\u8BF7\u8F93\u5165\u624B\u673A\u53F7"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const confirmationKitDragRule = {
|
||||||
|
menu: "kit",
|
||||||
|
icon: "icon-idcard",
|
||||||
|
label: "\u8F6C\u6B63",
|
||||||
|
name: "confirmationKit",
|
||||||
|
input: false,
|
||||||
|
drag: true,
|
||||||
|
dragBtn: true,
|
||||||
|
mask: false,
|
||||||
|
inside: true,
|
||||||
|
rule() {
|
||||||
|
const built = buildConfirmationKitChildren();
|
||||||
|
return {
|
||||||
|
type: "confirmationKit",
|
||||||
|
props: {
|
||||||
|
showHireDate: true,
|
||||||
|
showProbationMonths: true,
|
||||||
|
showConfirmationDate: true,
|
||||||
|
showPost: true,
|
||||||
|
showJobLevel: true,
|
||||||
|
showName: true,
|
||||||
|
showEmail: true,
|
||||||
|
showDept: true,
|
||||||
|
showPhone: true,
|
||||||
|
hireDateField: built.hireDateField,
|
||||||
|
probationMonthsField: built.probationMonthsField,
|
||||||
|
confirmationDateField: built.confirmationDateField,
|
||||||
|
postField: built.postField,
|
||||||
|
jobLevelField: built.jobLevelField,
|
||||||
|
nameField: built.nameField,
|
||||||
|
emailField: built.emailField,
|
||||||
|
deptField: built.deptField,
|
||||||
|
phoneField: built.phoneField
|
||||||
|
},
|
||||||
|
children: built.children
|
||||||
|
};
|
||||||
|
},
|
||||||
|
props() {
|
||||||
|
const switches = CONFIRMATION_KIT_FIELD_DEFS.map((d) => ({
|
||||||
|
type: "switch",
|
||||||
|
field: d.showProp,
|
||||||
|
title: `\u663E\u793A\uFF1A${d.title}`,
|
||||||
|
value: true,
|
||||||
|
props: { activeText: "\u663E\u793A", inactiveText: "\u9690\u85CF" }
|
||||||
|
}));
|
||||||
|
return [
|
||||||
|
...switches,
|
||||||
|
{
|
||||||
|
type: "input",
|
||||||
|
field: "formCreateInfo",
|
||||||
|
title: "\u63D0\u793A",
|
||||||
|
value: "\u9690\u85CF\u5B57\u6BB5\u4ECD\u4F1A\u5199\u5165\u9ED8\u8BA4\u503C\uFF1B\u5BA1\u6279\u901A\u8FC7\u540E\u53EF\u80FD\u56DE\u5199\u7528\u6237\u4E3B\u6570\u636E\u3002",
|
||||||
|
props: { disabled: true, type: "textarea", rows: 2 }
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export {
|
||||||
|
CONFIRMATION_KIT_FIELD_DEFS,
|
||||||
|
confirmationKitDragRule
|
||||||
|
};
|
||||||
@@ -0,0 +1,235 @@
|
|||||||
|
import { addMonthsYmd } from "./oa-confirmation-date.js";
|
||||||
|
import {
|
||||||
|
CONFIRMATION_KIT_FIELD_DEFS
|
||||||
|
} from "./oa-confirmation-kit-rule.js";
|
||||||
|
import { resolveCurrentOwnerDept } from "./oa-owner-field-defaults.js";
|
||||||
|
const CONFIRMATION_MANUAL_PREFIX = "__manual_";
|
||||||
|
function asRules(schema) {
|
||||||
|
if (Array.isArray(schema)) return schema.filter((r) => r && typeof r === "object");
|
||||||
|
if (schema && typeof schema === "object") {
|
||||||
|
const s = schema;
|
||||||
|
const rules = s.rule || s.rules || s.fields;
|
||||||
|
if (Array.isArray(rules)) return rules.filter((r) => r && typeof r === "object");
|
||||||
|
if (s.type || s.field) return [s];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
function walk(rules, out) {
|
||||||
|
for (const r of rules) {
|
||||||
|
out.push(r);
|
||||||
|
const children = r.children;
|
||||||
|
if (Array.isArray(children)) {
|
||||||
|
walk(
|
||||||
|
children.filter((c) => c && typeof c === "object"),
|
||||||
|
out
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const props = r.props;
|
||||||
|
if (props && Array.isArray(props.rule)) {
|
||||||
|
walk(
|
||||||
|
props.rule.filter((c) => c && typeof c === "object"),
|
||||||
|
out
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function kitChildren(kit) {
|
||||||
|
const props = kit.props && typeof kit.props === "object" ? kit.props : {};
|
||||||
|
if (Array.isArray(kit.children)) return kit.children;
|
||||||
|
if (Array.isArray(props.rule)) return props.rule;
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
function resolveBinding(kit) {
|
||||||
|
const props = kit.props && typeof kit.props === "object" ? kit.props : {};
|
||||||
|
const fields = {};
|
||||||
|
const propFieldMap = {
|
||||||
|
hireDateField: "hireDate",
|
||||||
|
probationMonthsField: "probationMonths",
|
||||||
|
confirmationDateField: "confirmationDate",
|
||||||
|
postField: "post",
|
||||||
|
jobLevelField: "jobLevel",
|
||||||
|
nameField: "name",
|
||||||
|
emailField: "email",
|
||||||
|
deptField: "dept",
|
||||||
|
phoneField: "phone"
|
||||||
|
};
|
||||||
|
for (const [propKey, sem] of Object.entries(propFieldMap)) {
|
||||||
|
const f = props[propKey];
|
||||||
|
if (f) fields[sem] = String(f);
|
||||||
|
}
|
||||||
|
for (const c of kitChildren(kit)) {
|
||||||
|
if (!c?.field) continue;
|
||||||
|
const sem = String(c.props?._fc_kit_field || "");
|
||||||
|
if (CONFIRMATION_KIT_FIELD_DEFS.some((d) => d.key === sem) && !fields[sem]) {
|
||||||
|
fields[sem] = String(c.field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const visible = {};
|
||||||
|
for (const d of CONFIRMATION_KIT_FIELD_DEFS) {
|
||||||
|
const v = props[d.showProp];
|
||||||
|
visible[d.key] = v === void 0 || v === null ? true : !!v;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
fields,
|
||||||
|
visible,
|
||||||
|
confirmationDateField: fields.confirmationDate,
|
||||||
|
hireDateField: fields.hireDate,
|
||||||
|
probationMonthsField: fields.probationMonths
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function listConfirmationKitBindings(schema) {
|
||||||
|
const flat = [];
|
||||||
|
walk(asRules(schema), flat);
|
||||||
|
const out = [];
|
||||||
|
for (const r of flat) {
|
||||||
|
if (String(r.type || "") !== "confirmationKit") continue;
|
||||||
|
out.push(resolveBinding(r));
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
function isEmptyValue(raw) {
|
||||||
|
if (raw == null || raw === "") return true;
|
||||||
|
if (Array.isArray(raw)) return raw.length === 0;
|
||||||
|
if (typeof raw === "object") {
|
||||||
|
const o = raw;
|
||||||
|
return !String(o.id || "").trim();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
function ensureConfirmationKitVisibility(rules) {
|
||||||
|
if (!Array.isArray(rules)) return [];
|
||||||
|
return rules.map((raw) => {
|
||||||
|
if (!raw || typeof raw !== "object") return raw;
|
||||||
|
const rule = { ...raw };
|
||||||
|
if (String(rule.type || "") === "confirmationKit") {
|
||||||
|
const binding = resolveBinding(rule);
|
||||||
|
const patchChildren = (list) => list.map((c) => {
|
||||||
|
if (!c || typeof c !== "object") return c;
|
||||||
|
const child = { ...c };
|
||||||
|
const sem = String(child.props?._fc_kit_field || "");
|
||||||
|
const show = sem && CONFIRMATION_KIT_FIELD_DEFS.some((d) => d.key === sem) ? binding.visible[sem] !== false : true;
|
||||||
|
child.hidden = !show;
|
||||||
|
if (!show) {
|
||||||
|
child.$required = false;
|
||||||
|
if (child.validate) child.validate = [];
|
||||||
|
}
|
||||||
|
if (Array.isArray(child.children)) {
|
||||||
|
child.children = patchChildren(child.children);
|
||||||
|
}
|
||||||
|
return child;
|
||||||
|
});
|
||||||
|
if (Array.isArray(rule.children)) {
|
||||||
|
rule.children = patchChildren(rule.children);
|
||||||
|
}
|
||||||
|
if (Array.isArray(rule.props?.rule)) {
|
||||||
|
rule.props = {
|
||||||
|
...rule.props,
|
||||||
|
rule: patchChildren(rule.props.rule)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Array.isArray(rule.children)) {
|
||||||
|
rule.children = ensureConfirmationKitVisibility(rule.children);
|
||||||
|
}
|
||||||
|
if (Array.isArray(rule.props?.rule)) {
|
||||||
|
rule.props = {
|
||||||
|
...rule.props,
|
||||||
|
rule: ensureConfirmationKitVisibility(rule.props.rule)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return rule;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async function applyConfirmationKitDefaults(schema, formData, user) {
|
||||||
|
const data = { ...formData };
|
||||||
|
const kits = listConfirmationKitBindings(schema);
|
||||||
|
if (!kits.length || !user) return data;
|
||||||
|
const name = String(user.realName || user.nickName || user.userName || "").trim();
|
||||||
|
const email = String(user.email || "").trim();
|
||||||
|
const phone = String(user.phone || user.phoneNo || "").trim();
|
||||||
|
const hireDate = String(user.hireDate || "").trim();
|
||||||
|
const probationRaw = user.probationMonths;
|
||||||
|
const probationMonths = probationRaw === null || probationRaw === void 0 || probationRaw === "" ? null : Number(probationRaw);
|
||||||
|
const dept = await resolveCurrentOwnerDept(user);
|
||||||
|
for (const kit of kits) {
|
||||||
|
const setIfEmpty = (sem, value) => {
|
||||||
|
const field = kit.fields[sem];
|
||||||
|
if (!field) return;
|
||||||
|
if (!isEmptyValue(data[field])) return;
|
||||||
|
if (value === null || value === void 0 || value === "") return;
|
||||||
|
data[field] = value;
|
||||||
|
};
|
||||||
|
setIfEmpty("hireDate", hireDate || null);
|
||||||
|
if (probationMonths !== null && Number.isFinite(probationMonths) && probationMonths >= 0) {
|
||||||
|
setIfEmpty("probationMonths", Math.trunc(probationMonths));
|
||||||
|
}
|
||||||
|
setIfEmpty("name", name || null);
|
||||||
|
setIfEmpty("email", email || null);
|
||||||
|
setIfEmpty("phone", phone || null);
|
||||||
|
if (dept) setIfEmpty("dept", dept);
|
||||||
|
const confField = kit.confirmationDateField;
|
||||||
|
const hireField = kit.hireDateField;
|
||||||
|
const probField = kit.probationMonthsField;
|
||||||
|
if (confField && isEmptyValue(data[confField]) && hireField && probField) {
|
||||||
|
const suggested = addMonthsYmd(
|
||||||
|
String(data[hireField] || ""),
|
||||||
|
Number(data[probField])
|
||||||
|
);
|
||||||
|
if (suggested) data[confField] = suggested;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
function applyConfirmationKitDateLinkage(schema, formData) {
|
||||||
|
const data = { ...formData };
|
||||||
|
let changed = false;
|
||||||
|
for (const kit of listConfirmationKitBindings(schema)) {
|
||||||
|
const confField = kit.confirmationDateField;
|
||||||
|
const hireField = kit.hireDateField;
|
||||||
|
const probField = kit.probationMonthsField;
|
||||||
|
if (!confField || !hireField || !probField) continue;
|
||||||
|
if (data[CONFIRMATION_MANUAL_PREFIX + confField]) continue;
|
||||||
|
const suggested = addMonthsYmd(String(data[hireField] || ""), Number(data[probField]));
|
||||||
|
if (suggested == null) continue;
|
||||||
|
if (data[confField] !== suggested) {
|
||||||
|
data[confField] = suggested;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { data, changed };
|
||||||
|
}
|
||||||
|
function markConfirmationDateManual(formData, confirmationDateField, manual = true) {
|
||||||
|
const data = { ...formData };
|
||||||
|
const key = CONFIRMATION_MANUAL_PREFIX + confirmationDateField;
|
||||||
|
if (manual) data[key] = true;
|
||||||
|
else delete data[key];
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
function clearConfirmationDateManualAndRelink(schema, formData, confirmationDateField) {
|
||||||
|
let data = markConfirmationDateManual(formData, confirmationDateField, false);
|
||||||
|
const linked = applyConfirmationKitDateLinkage(schema, data);
|
||||||
|
return linked.data;
|
||||||
|
}
|
||||||
|
function listManualConfirmationDateFields(schema, formData) {
|
||||||
|
return listConfirmationKitBindings(schema).map((k) => k.confirmationDateField).filter((f) => !!f && !!formData[CONFIRMATION_MANUAL_PREFIX + f]);
|
||||||
|
}
|
||||||
|
function isConfirmationDateField(schema, field) {
|
||||||
|
return listConfirmationKitBindings(schema).some((k) => k.confirmationDateField === field);
|
||||||
|
}
|
||||||
|
function findConfirmationKitByHireOrProbation(schema, field) {
|
||||||
|
return listConfirmationKitBindings(schema).find(
|
||||||
|
(k) => k.hireDateField === field || k.probationMonthsField === field
|
||||||
|
) || null;
|
||||||
|
}
|
||||||
|
export {
|
||||||
|
CONFIRMATION_MANUAL_PREFIX,
|
||||||
|
applyConfirmationKitDateLinkage,
|
||||||
|
applyConfirmationKitDefaults,
|
||||||
|
clearConfirmationDateManualAndRelink,
|
||||||
|
ensureConfirmationKitVisibility,
|
||||||
|
findConfirmationKitByHireOrProbation,
|
||||||
|
isConfirmationDateField,
|
||||||
|
listConfirmationKitBindings,
|
||||||
|
listManualConfirmationDateFields,
|
||||||
|
markConfirmationDateManual
|
||||||
|
};
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
function isDeptValueItem(v) {
|
||||||
|
if (!v || typeof v !== "object") return false;
|
||||||
|
const o = v;
|
||||||
|
return String(o.id || "").trim() !== "" && String(o.name || "").trim() !== "";
|
||||||
|
}
|
||||||
|
function normalizeDeptModelValue(raw, multiple) {
|
||||||
|
if (multiple) {
|
||||||
|
if (Array.isArray(raw)) {
|
||||||
|
return raw.filter(isDeptValueItem).map((x) => ({
|
||||||
|
id: String(x.id).trim(),
|
||||||
|
name: String(x.name).trim()
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
if (isDeptValueItem(raw)) {
|
||||||
|
return [{ id: String(raw.id).trim(), name: String(raw.name).trim() }];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (isDeptValueItem(raw)) {
|
||||||
|
return { id: String(raw.id).trim(), name: String(raw.name).trim() };
|
||||||
|
}
|
||||||
|
if (Array.isArray(raw) && raw.length === 1 && isDeptValueItem(raw[0])) {
|
||||||
|
return { id: String(raw[0].id).trim(), name: String(raw[0].name).trim() };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function formatDeptDisplay(raw) {
|
||||||
|
if (Array.isArray(raw)) {
|
||||||
|
return raw.filter(isDeptValueItem).map((x) => x.name).filter(Boolean).join("\u3001");
|
||||||
|
}
|
||||||
|
if (isDeptValueItem(raw)) return raw.name;
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
export {
|
||||||
|
formatDeptDisplay,
|
||||||
|
isDeptValueItem,
|
||||||
|
normalizeDeptModelValue
|
||||||
|
};
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
const DEFAULT_LEAVE_WORK_HOURS = {
|
||||||
|
morningStart: "08:30",
|
||||||
|
morningEnd: "12:00",
|
||||||
|
afternoonStart: "13:00",
|
||||||
|
afternoonEnd: "17:30"
|
||||||
|
};
|
||||||
|
const DT_RE = /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})$/;
|
||||||
|
function parseHm(hm) {
|
||||||
|
const m = String(hm || "").trim().match(/^(\d{1,2}):(\d{2})$/);
|
||||||
|
if (!m) return null;
|
||||||
|
const h = Number(m[1]);
|
||||||
|
const min = Number(m[2]);
|
||||||
|
if (h < 0 || h > 23 || min < 0 || min > 59) return null;
|
||||||
|
return h * 60 + min;
|
||||||
|
}
|
||||||
|
function parseDt(s) {
|
||||||
|
const m = String(s || "").trim().match(DT_RE);
|
||||||
|
if (!m) return null;
|
||||||
|
const d = new Date(
|
||||||
|
Number(m[1]),
|
||||||
|
Number(m[2]) - 1,
|
||||||
|
Number(m[3]),
|
||||||
|
Number(m[4]),
|
||||||
|
Number(m[5]),
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
return Number.isNaN(d.getTime()) ? null : d;
|
||||||
|
}
|
||||||
|
function overlapMinutesOnDay(day, rangeStart, rangeEnd, workHours) {
|
||||||
|
const y = day.getFullYear();
|
||||||
|
const mo = day.getMonth();
|
||||||
|
const da = day.getDate();
|
||||||
|
const day0 = new Date(y, mo, da, 0, 0, 0, 0).getTime();
|
||||||
|
const amA = parseHm(workHours.morningStart);
|
||||||
|
const amB = parseHm(workHours.morningEnd);
|
||||||
|
const pmA = parseHm(workHours.afternoonStart);
|
||||||
|
const pmB = parseHm(workHours.afternoonEnd);
|
||||||
|
if (amA == null || amB == null || pmA == null || pmB == null) return 0;
|
||||||
|
if (amA >= amB || pmA >= pmB) return 0;
|
||||||
|
const segments = [
|
||||||
|
[day0 + amA * 6e4, day0 + amB * 6e4],
|
||||||
|
[day0 + pmA * 6e4, day0 + pmB * 6e4]
|
||||||
|
];
|
||||||
|
const rs = rangeStart.getTime();
|
||||||
|
const re = rangeEnd.getTime();
|
||||||
|
let total = 0;
|
||||||
|
for (const [a, b] of segments) {
|
||||||
|
const lo = Math.max(rs, a);
|
||||||
|
const hi = Math.min(re, b);
|
||||||
|
if (hi > lo) total += (hi - lo) / 6e4;
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
function calcLeaveHours(startStr, endStr, workHours = DEFAULT_LEAVE_WORK_HOURS) {
|
||||||
|
const start = parseDt(startStr);
|
||||||
|
const end = parseDt(endStr);
|
||||||
|
if (!start || !end || end.getTime() <= start.getTime()) return null;
|
||||||
|
let minutes = 0;
|
||||||
|
const cursor = new Date(start.getFullYear(), start.getMonth(), start.getDate());
|
||||||
|
const last = new Date(end.getFullYear(), end.getMonth(), end.getDate());
|
||||||
|
while (cursor.getTime() <= last.getTime()) {
|
||||||
|
minutes += overlapMinutesOnDay(cursor, start, end, workHours);
|
||||||
|
cursor.setDate(cursor.getDate() + 1);
|
||||||
|
}
|
||||||
|
return Math.round(minutes / 60 * 10) / 10;
|
||||||
|
}
|
||||||
|
export {
|
||||||
|
DEFAULT_LEAVE_WORK_HOURS,
|
||||||
|
calcLeaveHours
|
||||||
|
};
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
import {
|
||||||
|
calcLeaveHours,
|
||||||
|
DEFAULT_LEAVE_WORK_HOURS
|
||||||
|
} from "./oa-leave-hours.js";
|
||||||
|
const LEAVE_MANUAL_PREFIX = "__manual_";
|
||||||
|
function asRules(schema) {
|
||||||
|
if (Array.isArray(schema)) return schema.filter((r) => r && typeof r === "object");
|
||||||
|
if (schema && typeof schema === "object") {
|
||||||
|
const s = schema;
|
||||||
|
const rules = s.rule || s.rules || s.fields;
|
||||||
|
if (Array.isArray(rules)) return rules.filter((r) => r && typeof r === "object");
|
||||||
|
if (s.type || s.field) return [s];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
function walk(rules, out) {
|
||||||
|
for (const r of rules) {
|
||||||
|
out.push(r);
|
||||||
|
const children = r.children;
|
||||||
|
if (Array.isArray(children)) {
|
||||||
|
walk(
|
||||||
|
children.filter((c) => c && typeof c === "object"),
|
||||||
|
out
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const props = r.props;
|
||||||
|
if (props && Array.isArray(props.rule)) {
|
||||||
|
walk(
|
||||||
|
props.rule.filter((c) => c && typeof c === "object"),
|
||||||
|
out
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function resolveFields(kit) {
|
||||||
|
const props = kit.props && typeof kit.props === "object" ? kit.props : {};
|
||||||
|
let startField = props.startField;
|
||||||
|
let endField = props.endField;
|
||||||
|
let durationField = props.durationField;
|
||||||
|
const children = Array.isArray(kit.children) ? kit.children : Array.isArray(props.rule) ? props.rule : [];
|
||||||
|
if (!startField || !endField || !durationField) {
|
||||||
|
const dateFields = [];
|
||||||
|
let dur;
|
||||||
|
for (const c of children) {
|
||||||
|
if (!c?.field) continue;
|
||||||
|
const t = String(c.type || "");
|
||||||
|
if (t === "datePicker" || t === "date" || t === "timePicker" || t === "time") {
|
||||||
|
dateFields.push(String(c.field));
|
||||||
|
} else if ((t === "inputNumber" || t === "number") && !dur) {
|
||||||
|
dur = String(c.field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
startField = startField || dateFields[0];
|
||||||
|
endField = endField || dateFields[1];
|
||||||
|
durationField = durationField || dur;
|
||||||
|
}
|
||||||
|
if (!startField || !endField || !durationField) return null;
|
||||||
|
return {
|
||||||
|
startField,
|
||||||
|
endField,
|
||||||
|
durationField,
|
||||||
|
workHours: {
|
||||||
|
morningStart: String(props.morningStart || DEFAULT_LEAVE_WORK_HOURS.morningStart),
|
||||||
|
morningEnd: String(props.morningEnd || DEFAULT_LEAVE_WORK_HOURS.morningEnd),
|
||||||
|
afternoonStart: String(props.afternoonStart || DEFAULT_LEAVE_WORK_HOURS.afternoonStart),
|
||||||
|
afternoonEnd: String(props.afternoonEnd || DEFAULT_LEAVE_WORK_HOURS.afternoonEnd)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function listLeaveKitBindings(schema) {
|
||||||
|
const flat = [];
|
||||||
|
walk(asRules(schema), flat);
|
||||||
|
const out = [];
|
||||||
|
for (const r of flat) {
|
||||||
|
if (String(r.type || "") !== "leaveKit") continue;
|
||||||
|
const b = resolveFields(r);
|
||||||
|
if (b) out.push(b);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
function applyLeaveKitDurationsToFormData(schema, formData) {
|
||||||
|
const data = { ...formData };
|
||||||
|
let changed = false;
|
||||||
|
for (const kit of listLeaveKitBindings(schema)) {
|
||||||
|
const manualKey = LEAVE_MANUAL_PREFIX + kit.durationField;
|
||||||
|
if (data[manualKey]) continue;
|
||||||
|
const hours = calcLeaveHours(
|
||||||
|
String(data[kit.startField] || ""),
|
||||||
|
String(data[kit.endField] || ""),
|
||||||
|
kit.workHours
|
||||||
|
);
|
||||||
|
if (hours == null) continue;
|
||||||
|
if (data[kit.durationField] !== hours) {
|
||||||
|
data[kit.durationField] = hours;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { data, changed };
|
||||||
|
}
|
||||||
|
function markLeaveDurationManual(formData, durationField, manual = true) {
|
||||||
|
const data = { ...formData };
|
||||||
|
const key = LEAVE_MANUAL_PREFIX + durationField;
|
||||||
|
if (manual) data[key] = true;
|
||||||
|
else delete data[key];
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
function isLeaveDurationField(schema, field) {
|
||||||
|
return listLeaveKitBindings(schema).some((k) => k.durationField === field);
|
||||||
|
}
|
||||||
|
function findLeaveKitByStartOrEnd(schema, field) {
|
||||||
|
return listLeaveKitBindings(schema).find(
|
||||||
|
(k) => k.startField === field || k.endField === field
|
||||||
|
) || null;
|
||||||
|
}
|
||||||
|
function ensureLeaveKitRuleProps(rules) {
|
||||||
|
if (!Array.isArray(rules)) return [];
|
||||||
|
return rules.map((raw) => {
|
||||||
|
if (!raw || typeof raw !== "object") return raw;
|
||||||
|
const rule = { ...raw };
|
||||||
|
if (String(rule.type || "") === "leaveKit") {
|
||||||
|
const binding = resolveFields(rule);
|
||||||
|
const patchChildren = (list) => list.map((c) => {
|
||||||
|
if (!c || typeof c !== "object") return c;
|
||||||
|
const child = { ...c };
|
||||||
|
const t = String(child.type || "");
|
||||||
|
const isDuration = binding && child.field === binding.durationField || (t === "inputNumber" || t === "number") && String(child.title || "").includes("\u65F6\u957F");
|
||||||
|
if (isDuration) {
|
||||||
|
child.props = { ...child.props || {}, align: "left" };
|
||||||
|
const cls = String(child.class || child.className || "");
|
||||||
|
if (!cls.includes("oa-leave-duration")) {
|
||||||
|
child.class = (cls ? cls + " " : "") + "oa-leave-duration";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Array.isArray(child.children)) {
|
||||||
|
child.children = patchChildren(child.children);
|
||||||
|
}
|
||||||
|
return child;
|
||||||
|
});
|
||||||
|
if (Array.isArray(rule.children)) {
|
||||||
|
rule.children = patchChildren(rule.children);
|
||||||
|
}
|
||||||
|
if (Array.isArray(rule.props?.rule)) {
|
||||||
|
rule.props = {
|
||||||
|
...rule.props,
|
||||||
|
rule: patchChildren(rule.props.rule)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Array.isArray(rule.children)) {
|
||||||
|
rule.children = ensureLeaveKitRuleProps(rule.children);
|
||||||
|
}
|
||||||
|
if (Array.isArray(rule.props?.rule)) {
|
||||||
|
rule.props = {
|
||||||
|
...rule.props,
|
||||||
|
rule: ensureLeaveKitRuleProps(rule.props.rule)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return rule;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export {
|
||||||
|
LEAVE_MANUAL_PREFIX,
|
||||||
|
applyLeaveKitDurationsToFormData,
|
||||||
|
ensureLeaveKitRuleProps,
|
||||||
|
findLeaveKitByStartOrEnd,
|
||||||
|
isLeaveDurationField,
|
||||||
|
listLeaveKitBindings,
|
||||||
|
markLeaveDurationManual
|
||||||
|
};
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
const DEFAULT_OVERTIME_WINDOWS = {
|
||||||
|
workdayOvertimeStart: "17:30",
|
||||||
|
workdayOvertimeEnd: "08:30",
|
||||||
|
restBreakStart: "11:30",
|
||||||
|
restBreakEnd: "13:30"
|
||||||
|
};
|
||||||
|
const DT_RE = /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})$/;
|
||||||
|
function parseHm(hm) {
|
||||||
|
const m = String(hm || "").trim().match(/^(\d{1,2}):(\d{2})$/);
|
||||||
|
if (!m) return null;
|
||||||
|
const h = Number(m[1]);
|
||||||
|
const min = Number(m[2]);
|
||||||
|
if (h < 0 || h > 23 || min < 0 || min > 59) return null;
|
||||||
|
return h * 60 + min;
|
||||||
|
}
|
||||||
|
function parseDt(s) {
|
||||||
|
const m = String(s || "").trim().match(DT_RE);
|
||||||
|
if (!m) return null;
|
||||||
|
const d = new Date(
|
||||||
|
Number(m[1]),
|
||||||
|
Number(m[2]) - 1,
|
||||||
|
Number(m[3]),
|
||||||
|
Number(m[4]),
|
||||||
|
Number(m[5]),
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
return Number.isNaN(d.getTime()) ? null : d;
|
||||||
|
}
|
||||||
|
function formatYmd(d) {
|
||||||
|
const y = d.getFullYear();
|
||||||
|
const mo = String(d.getMonth() + 1).padStart(2, "0");
|
||||||
|
const da = String(d.getDate()).padStart(2, "0");
|
||||||
|
return `${y}-${mo}-${da}`;
|
||||||
|
}
|
||||||
|
function dayStartMs(d) {
|
||||||
|
return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
|
||||||
|
}
|
||||||
|
function dayAtMinutes(d, minutes) {
|
||||||
|
return dayStartMs(d) + minutes * 6e4;
|
||||||
|
}
|
||||||
|
function mergeIntervals(intervals) {
|
||||||
|
if (intervals.length === 0) return [];
|
||||||
|
const sorted = [...intervals].sort((a, b) => a[0] - b[0]);
|
||||||
|
const merged = [[sorted[0][0], sorted[0][1]]];
|
||||||
|
for (let i = 1; i < sorted.length; i++) {
|
||||||
|
const last = merged[merged.length - 1];
|
||||||
|
const [s, e] = sorted[i];
|
||||||
|
if (s <= last[1]) {
|
||||||
|
last[1] = Math.max(last[1], e);
|
||||||
|
} else {
|
||||||
|
merged.push([s, e]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
function intersectInterval(a, b) {
|
||||||
|
const lo = Math.max(a[0], b[0]);
|
||||||
|
const hi = Math.min(a[1], b[1]);
|
||||||
|
return hi > lo ? [lo, hi] : null;
|
||||||
|
}
|
||||||
|
function subtractInterval(base, sub) {
|
||||||
|
const [bs, be] = base;
|
||||||
|
const [ss, se] = sub;
|
||||||
|
if (se <= bs || ss >= be) return [base];
|
||||||
|
const out = [];
|
||||||
|
if (ss > bs) out.push([bs, ss]);
|
||||||
|
if (se < be) out.push([se, be]);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
function subtractIntervals(base, subs) {
|
||||||
|
let result = [base];
|
||||||
|
for (const sub of subs) {
|
||||||
|
const next = [];
|
||||||
|
for (const r of result) {
|
||||||
|
next.push(...subtractInterval(r, sub));
|
||||||
|
}
|
||||||
|
result = next;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
function intervalMinutes(intervals) {
|
||||||
|
return intervals.reduce((sum, [a, b]) => sum + (b - a) / 6e4, 0);
|
||||||
|
}
|
||||||
|
function buildWorkdayWindows(anchorDay, windows) {
|
||||||
|
const startMin = parseHm(windows.workdayOvertimeStart);
|
||||||
|
const endMin = parseHm(windows.workdayOvertimeEnd);
|
||||||
|
if (startMin == null || endMin == null) return null;
|
||||||
|
const anchorStart = dayAtMinutes(anchorDay, startMin);
|
||||||
|
const nextDay = new Date(anchorDay);
|
||||||
|
nextDay.setDate(nextDay.getDate() + 1);
|
||||||
|
const anchorEnd = dayAtMinutes(nextDay, endMin);
|
||||||
|
if (anchorEnd <= anchorStart) return null;
|
||||||
|
return [anchorStart, anchorEnd];
|
||||||
|
}
|
||||||
|
function buildRestDaySegments(day, windows) {
|
||||||
|
const breakStart = parseHm(windows.restBreakStart);
|
||||||
|
const breakEnd = parseHm(windows.restBreakEnd);
|
||||||
|
if (breakStart == null || breakEnd == null) return [];
|
||||||
|
const day0 = dayStartMs(day);
|
||||||
|
const nextDay = new Date(day);
|
||||||
|
nextDay.setDate(nextDay.getDate() + 1);
|
||||||
|
const dayEnd = dayStartMs(nextDay);
|
||||||
|
const segments = [];
|
||||||
|
if (breakStart > 0) segments.push([day0, dayAtMinutes(day, breakStart)]);
|
||||||
|
if (breakEnd < 24 * 60) segments.push([dayAtMinutes(day, breakEnd), dayEnd]);
|
||||||
|
return segments.filter(([a, b]) => b > a);
|
||||||
|
}
|
||||||
|
function calcOvertimeHours(startStr, endStr, windows = DEFAULT_OVERTIME_WINDOWS, isRestDay) {
|
||||||
|
const start = parseDt(startStr);
|
||||||
|
const end = parseDt(endStr);
|
||||||
|
if (!start || !end || end.getTime() <= start.getTime()) return null;
|
||||||
|
const query = [start.getTime(), end.getTime()];
|
||||||
|
const startDay = new Date(start.getFullYear(), start.getMonth(), start.getDate());
|
||||||
|
const endDay = new Date(end.getFullYear(), end.getMonth(), end.getDate());
|
||||||
|
const workdayIntervals = [];
|
||||||
|
const anchor = new Date(startDay);
|
||||||
|
anchor.setDate(anchor.getDate() - 1);
|
||||||
|
const lastAnchor = new Date(endDay);
|
||||||
|
while (anchor.getTime() <= lastAnchor.getTime()) {
|
||||||
|
const ymd = formatYmd(anchor);
|
||||||
|
if (!isRestDay(ymd)) {
|
||||||
|
const win = buildWorkdayWindows(anchor, windows);
|
||||||
|
if (win) {
|
||||||
|
const hit = intersectInterval(win, query);
|
||||||
|
if (hit) workdayIntervals.push(hit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
anchor.setDate(anchor.getDate() + 1);
|
||||||
|
}
|
||||||
|
const workdayMerged = mergeIntervals(workdayIntervals);
|
||||||
|
const restIntervals = [];
|
||||||
|
const day = new Date(startDay);
|
||||||
|
while (day.getTime() <= endDay.getTime()) {
|
||||||
|
const ymd = formatYmd(day);
|
||||||
|
if (isRestDay(ymd)) {
|
||||||
|
for (const seg of buildRestDaySegments(day, windows)) {
|
||||||
|
const hit = intersectInterval(seg, query);
|
||||||
|
if (hit) {
|
||||||
|
const remaining = subtractIntervals(hit, workdayMerged);
|
||||||
|
restIntervals.push(...remaining);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
day.setDate(day.getDate() + 1);
|
||||||
|
}
|
||||||
|
const totalMinutes = intervalMinutes(workdayMerged) + intervalMinutes(restIntervals);
|
||||||
|
return Math.round(totalMinutes / 60 * 10) / 10;
|
||||||
|
}
|
||||||
|
export {
|
||||||
|
DEFAULT_OVERTIME_WINDOWS,
|
||||||
|
calcOvertimeHours
|
||||||
|
};
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
import {
|
||||||
|
buildCurrentOwnerPerson
|
||||||
|
} from "./oa-owner-field-defaults.js";
|
||||||
|
import {
|
||||||
|
calcOvertimeHours,
|
||||||
|
DEFAULT_OVERTIME_WINDOWS
|
||||||
|
} from "./oa-overtime-hours.js";
|
||||||
|
const OVERTIME_MANUAL_PREFIX = "__manual_";
|
||||||
|
function asRules(schema) {
|
||||||
|
if (Array.isArray(schema)) return schema.filter((r) => r && typeof r === "object");
|
||||||
|
if (schema && typeof schema === "object") {
|
||||||
|
const s = schema;
|
||||||
|
const rules = s.rule || s.rules || s.fields;
|
||||||
|
if (Array.isArray(rules)) return rules.filter((r) => r && typeof r === "object");
|
||||||
|
if (s.type || s.field) return [s];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
function walk(rules, out) {
|
||||||
|
for (const r of rules) {
|
||||||
|
out.push(r);
|
||||||
|
if (Array.isArray(r.children)) {
|
||||||
|
walk(r.children, out);
|
||||||
|
}
|
||||||
|
const props = r.props;
|
||||||
|
if (props && Array.isArray(props.rule)) walk(props.rule, out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function resolveBinding(kit) {
|
||||||
|
const props = kit.props && typeof kit.props === "object" ? kit.props : {};
|
||||||
|
let personField = props.personField ? String(props.personField) : void 0;
|
||||||
|
let startField = props.startField ? String(props.startField) : void 0;
|
||||||
|
let endField = props.endField ? String(props.endField) : void 0;
|
||||||
|
let durationField = props.durationField ? String(props.durationField) : void 0;
|
||||||
|
const children = Array.isArray(kit.children) ? kit.children : Array.isArray(props.rule) ? props.rule : [];
|
||||||
|
for (const c of children) {
|
||||||
|
if (!c?.field) continue;
|
||||||
|
const sem = String(c.props?._fc_kit_field || "");
|
||||||
|
if (sem === "overtimePerson" && !personField) personField = String(c.field);
|
||||||
|
if (sem === "overtimeStart" && !startField) startField = String(c.field);
|
||||||
|
if (sem === "overtimeEnd" && !endField) endField = String(c.field);
|
||||||
|
if (sem === "overtimeDuration" && !durationField) durationField = String(c.field);
|
||||||
|
}
|
||||||
|
if (!startField || !endField || !durationField) {
|
||||||
|
const dateFields = [];
|
||||||
|
let dur;
|
||||||
|
for (const c of children) {
|
||||||
|
if (!c?.field) continue;
|
||||||
|
const t = String(c.type || "");
|
||||||
|
if (t === "datePicker" || t === "date" || t === "timePicker" || t === "time") {
|
||||||
|
dateFields.push(String(c.field));
|
||||||
|
} else if ((t === "inputNumber" || t === "number") && (String(c.title || "").includes("\u65F6\u957F") || String(c.props?._fc_kit_field || "") === "overtimeDuration")) {
|
||||||
|
dur = String(c.field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
startField = startField || dateFields[0];
|
||||||
|
endField = endField || dateFields[1];
|
||||||
|
durationField = durationField || dur;
|
||||||
|
}
|
||||||
|
if (!startField || !endField || !durationField) return null;
|
||||||
|
return {
|
||||||
|
personField,
|
||||||
|
startField,
|
||||||
|
endField,
|
||||||
|
durationField,
|
||||||
|
windows: {
|
||||||
|
workdayOvertimeStart: String(
|
||||||
|
props.workdayOvertimeStart || DEFAULT_OVERTIME_WINDOWS.workdayOvertimeStart
|
||||||
|
),
|
||||||
|
workdayOvertimeEnd: String(
|
||||||
|
props.workdayOvertimeEnd || DEFAULT_OVERTIME_WINDOWS.workdayOvertimeEnd
|
||||||
|
),
|
||||||
|
restBreakStart: String(props.restBreakStart || DEFAULT_OVERTIME_WINDOWS.restBreakStart),
|
||||||
|
restBreakEnd: String(props.restBreakEnd || DEFAULT_OVERTIME_WINDOWS.restBreakEnd)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function listOvertimeKitBindings(schema) {
|
||||||
|
const flat = [];
|
||||||
|
walk(asRules(schema), flat);
|
||||||
|
const out = [];
|
||||||
|
for (const r of flat) {
|
||||||
|
if (String(r.type || "") !== "overtimeKit") continue;
|
||||||
|
const b = resolveBinding(r);
|
||||||
|
if (b) out.push(b);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
function isEmptyPersonValue(raw) {
|
||||||
|
if (raw == null || raw === "") return true;
|
||||||
|
if (Array.isArray(raw)) return raw.length === 0;
|
||||||
|
if (typeof raw === "object") {
|
||||||
|
const o = raw;
|
||||||
|
return !String(o.id || "").trim();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
function applyOvertimeKitDefaults(schema, formData, user) {
|
||||||
|
const data = { ...formData };
|
||||||
|
const person = buildCurrentOwnerPerson(user);
|
||||||
|
if (!person) return data;
|
||||||
|
for (const kit of listOvertimeKitBindings(schema)) {
|
||||||
|
const f = kit.personField;
|
||||||
|
if (f && isEmptyPersonValue(data[f])) {
|
||||||
|
data[f] = [person];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
function applyOvertimeKitDurationsToFormData(schema, formData, isRestDay) {
|
||||||
|
const data = { ...formData };
|
||||||
|
let changed = false;
|
||||||
|
for (const kit of listOvertimeKitBindings(schema)) {
|
||||||
|
const manualKey = OVERTIME_MANUAL_PREFIX + kit.durationField;
|
||||||
|
if (data[manualKey]) continue;
|
||||||
|
const hours = calcOvertimeHours(
|
||||||
|
String(data[kit.startField] || ""),
|
||||||
|
String(data[kit.endField] || ""),
|
||||||
|
kit.windows,
|
||||||
|
isRestDay
|
||||||
|
);
|
||||||
|
if (hours == null) continue;
|
||||||
|
if (data[kit.durationField] !== hours) {
|
||||||
|
data[kit.durationField] = hours;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { data, changed };
|
||||||
|
}
|
||||||
|
function markOvertimeDurationManual(formData, durationField, manual = true) {
|
||||||
|
const data = { ...formData };
|
||||||
|
const key = OVERTIME_MANUAL_PREFIX + durationField;
|
||||||
|
if (manual) data[key] = true;
|
||||||
|
else delete data[key];
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
function isOvertimeDurationField(schema, field) {
|
||||||
|
return listOvertimeKitBindings(schema).some((k) => k.durationField === field);
|
||||||
|
}
|
||||||
|
function findOvertimeKitByStartOrEnd(schema, field) {
|
||||||
|
return listOvertimeKitBindings(schema).find(
|
||||||
|
(k) => k.startField === field || k.endField === field
|
||||||
|
) || null;
|
||||||
|
}
|
||||||
|
function ensureOvertimeKitRuleProps(rules) {
|
||||||
|
if (!Array.isArray(rules)) return [];
|
||||||
|
return rules.map((raw) => {
|
||||||
|
if (!raw || typeof raw !== "object") return raw;
|
||||||
|
const rule = { ...raw };
|
||||||
|
if (String(rule.type || "") === "overtimeKit") {
|
||||||
|
const binding = resolveBinding(rule);
|
||||||
|
const patchChildren = (list) => list.map((c) => {
|
||||||
|
if (!c || typeof c !== "object") return c;
|
||||||
|
const child = { ...c };
|
||||||
|
const t = String(child.type || "");
|
||||||
|
const isDuration = binding && child.field === binding.durationField || (t === "inputNumber" || t === "number") && String(child.title || "").includes("\u65F6\u957F");
|
||||||
|
if (isDuration) {
|
||||||
|
child.props = { ...child.props || {}, align: "left" };
|
||||||
|
const cls = String(child.class || child.className || "");
|
||||||
|
if (!cls.includes("oa-overtime-duration")) {
|
||||||
|
child.class = (cls ? cls + " " : "") + "oa-overtime-duration";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Array.isArray(child.children)) {
|
||||||
|
child.children = patchChildren(child.children);
|
||||||
|
}
|
||||||
|
return child;
|
||||||
|
});
|
||||||
|
if (Array.isArray(rule.children)) {
|
||||||
|
rule.children = patchChildren(rule.children);
|
||||||
|
}
|
||||||
|
if (Array.isArray(rule.props?.rule)) {
|
||||||
|
rule.props = {
|
||||||
|
...rule.props,
|
||||||
|
rule: patchChildren(rule.props.rule)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Array.isArray(rule.children)) {
|
||||||
|
rule.children = ensureOvertimeKitRuleProps(rule.children);
|
||||||
|
}
|
||||||
|
if (Array.isArray(rule.props?.rule)) {
|
||||||
|
rule.props = {
|
||||||
|
...rule.props,
|
||||||
|
rule: ensureOvertimeKitRuleProps(rule.props.rule)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return rule;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function hasOvertimeKit(schema) {
|
||||||
|
const flat = [];
|
||||||
|
const walk2 = (rules) => {
|
||||||
|
for (const r of rules) {
|
||||||
|
flat.push(r);
|
||||||
|
if (Array.isArray(r.children)) walk2(r.children);
|
||||||
|
const props = r.props;
|
||||||
|
if (props && Array.isArray(props.rule)) walk2(props.rule);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
walk2(asRules(schema));
|
||||||
|
return flat.some((r) => String(r.type || "") === "overtimeKit");
|
||||||
|
}
|
||||||
|
export {
|
||||||
|
OVERTIME_MANUAL_PREFIX,
|
||||||
|
applyOvertimeKitDefaults,
|
||||||
|
applyOvertimeKitDurationsToFormData,
|
||||||
|
ensureOvertimeKitRuleProps,
|
||||||
|
findOvertimeKitByStartOrEnd,
|
||||||
|
hasOvertimeKit,
|
||||||
|
isOvertimeDurationField,
|
||||||
|
listOvertimeKitBindings,
|
||||||
|
markOvertimeDurationManual
|
||||||
|
};
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import { fetchDeptTree } from "../../api/oa.js";
|
||||||
|
import { isDeptValueItem } from "./oa-dept-value.js";
|
||||||
|
import { isPersonValueItem } from "./oa-person-value.js";
|
||||||
|
import { buildPathLabelMapFromTreeRoots } from "./tree-path.js";
|
||||||
|
function walkRules(rules, out) {
|
||||||
|
for (const r of rules) {
|
||||||
|
if (!r || typeof r !== "object") continue;
|
||||||
|
const rule = r;
|
||||||
|
out.push(rule);
|
||||||
|
const children = rule.children || rule.props?.rule || [];
|
||||||
|
if (Array.isArray(children)) walkRules(children, out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function flattenRules(schemaOrRules) {
|
||||||
|
if (Array.isArray(schemaOrRules)) {
|
||||||
|
const out = [];
|
||||||
|
walkRules(schemaOrRules, out);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
if (schemaOrRules && typeof schemaOrRules === "object") {
|
||||||
|
const root = schemaOrRules;
|
||||||
|
const rules = root.rule || root.rules || [];
|
||||||
|
if (Array.isArray(rules)) {
|
||||||
|
const out = [];
|
||||||
|
walkRules(rules, out);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
function isEmptyOwnerValue(raw) {
|
||||||
|
if (raw == null || raw === "") return true;
|
||||||
|
if (Array.isArray(raw)) return raw.length === 0;
|
||||||
|
if (typeof raw === "object") {
|
||||||
|
const o = raw;
|
||||||
|
return !String(o.id || "").trim();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
function buildCurrentOwnerPerson(user) {
|
||||||
|
if (!user) return null;
|
||||||
|
const id = String(user.id || user.userId || "").trim();
|
||||||
|
if (!id) return null;
|
||||||
|
const name = String(user.realName || user.userName || user.username || "").trim() || id;
|
||||||
|
return { id, name };
|
||||||
|
}
|
||||||
|
function buildCurrentOwnerDeptSync(user) {
|
||||||
|
if (!user) return null;
|
||||||
|
const id = String(user.regionId || user.deptId || "").trim();
|
||||||
|
if (!id) return null;
|
||||||
|
const name = String(user.regionName || user.deptName || "").trim() || id;
|
||||||
|
return { id, name };
|
||||||
|
}
|
||||||
|
async function resolveCurrentOwnerDept(user) {
|
||||||
|
const base = buildCurrentOwnerDeptSync(user);
|
||||||
|
if (!base) return null;
|
||||||
|
try {
|
||||||
|
const tree = await fetchDeptTree() || [];
|
||||||
|
const map = buildPathLabelMapFromTreeRoots(tree, { nameKey: "deptName" });
|
||||||
|
const path = map.get(base.id);
|
||||||
|
if (path) return { id: base.id, name: path };
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
async function applyOwnerFieldDefaults(schemaOrRules, formData, user) {
|
||||||
|
const rules = flattenRules(schemaOrRules);
|
||||||
|
const hasPerson = rules.some((r) => String(r.type || "") === "ownerPerson");
|
||||||
|
const hasDept = rules.some((r) => String(r.type || "") === "ownerDept");
|
||||||
|
if (!hasPerson && !hasDept) return { ...formData };
|
||||||
|
const person = hasPerson ? buildCurrentOwnerPerson(user) : null;
|
||||||
|
const dept = hasDept ? await resolveCurrentOwnerDept(user) : null;
|
||||||
|
const next = { ...formData };
|
||||||
|
for (const rule of rules) {
|
||||||
|
const type = String(rule.type || "");
|
||||||
|
const field = String(rule.field || rule.name || "").trim();
|
||||||
|
if (!field) continue;
|
||||||
|
const allowModify = rule.props?.allowModify === true;
|
||||||
|
if (type === "ownerPerson" && person) {
|
||||||
|
if (!allowModify || isEmptyOwnerValue(next[field])) {
|
||||||
|
next[field] = person;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (type === "ownerDept" && dept) {
|
||||||
|
if (!allowModify || isEmptyOwnerValue(next[field])) {
|
||||||
|
next[field] = dept;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
function ensureOwnerFieldRuleProps(rules) {
|
||||||
|
if (!Array.isArray(rules)) return [];
|
||||||
|
return rules.map((raw) => {
|
||||||
|
if (!raw || typeof raw !== "object") return raw;
|
||||||
|
const rule = { ...raw };
|
||||||
|
const type = String(rule.type || "");
|
||||||
|
if (type === "ownerPerson" || type === "ownerDept") {
|
||||||
|
const props = { ...rule.props || {} };
|
||||||
|
if (props.allowModify !== true) props.allowModify = false;
|
||||||
|
rule.props = props;
|
||||||
|
}
|
||||||
|
if (Array.isArray(rule.children)) {
|
||||||
|
rule.children = ensureOwnerFieldRuleProps(rule.children);
|
||||||
|
}
|
||||||
|
if (rule.props && Array.isArray(rule.props.rule)) {
|
||||||
|
rule.props = {
|
||||||
|
...rule.props,
|
||||||
|
rule: ensureOwnerFieldRuleProps(rule.props.rule)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return rule;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function isOwnerPersonValue(v) {
|
||||||
|
return isPersonValueItem(v);
|
||||||
|
}
|
||||||
|
function isOwnerDeptValue(v) {
|
||||||
|
return isDeptValueItem(v);
|
||||||
|
}
|
||||||
|
export {
|
||||||
|
applyOwnerFieldDefaults,
|
||||||
|
buildCurrentOwnerDeptSync,
|
||||||
|
buildCurrentOwnerPerson,
|
||||||
|
ensureOwnerFieldRuleProps,
|
||||||
|
isOwnerDeptValue,
|
||||||
|
isOwnerPersonValue,
|
||||||
|
resolveCurrentOwnerDept
|
||||||
|
};
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
function isPersonValueItem(v) {
|
||||||
|
if (!v || typeof v !== "object") return false;
|
||||||
|
const o = v;
|
||||||
|
return String(o.id || "").trim() !== "" && String(o.name || "").trim() !== "";
|
||||||
|
}
|
||||||
|
function normalizePersonModelValue(raw, multiple) {
|
||||||
|
if (multiple) {
|
||||||
|
if (Array.isArray(raw)) {
|
||||||
|
return raw.filter(isPersonValueItem).map((x) => ({
|
||||||
|
id: String(x.id).trim(),
|
||||||
|
name: String(x.name).trim()
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
if (isPersonValueItem(raw)) {
|
||||||
|
return [{ id: String(raw.id).trim(), name: String(raw.name).trim() }];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (isPersonValueItem(raw)) {
|
||||||
|
return { id: String(raw.id).trim(), name: String(raw.name).trim() };
|
||||||
|
}
|
||||||
|
if (Array.isArray(raw) && raw.length === 1 && isPersonValueItem(raw[0])) {
|
||||||
|
return { id: String(raw[0].id).trim(), name: String(raw[0].name).trim() };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function formatPersonDisplay(raw) {
|
||||||
|
if (Array.isArray(raw)) {
|
||||||
|
return raw.filter(isPersonValueItem).map((x) => x.name).filter(Boolean).join("\u3001");
|
||||||
|
}
|
||||||
|
if (isPersonValueItem(raw)) return raw.name;
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
function extractPersonIds(raw) {
|
||||||
|
const n = Array.isArray(raw) ? raw.filter(isPersonValueItem) : isPersonValueItem(raw) ? [raw] : [];
|
||||||
|
const out = [];
|
||||||
|
const seen = /* @__PURE__ */ new Set();
|
||||||
|
for (const x of n) {
|
||||||
|
const id = String(x.id).trim();
|
||||||
|
if (!id || seen.has(id)) continue;
|
||||||
|
seen.add(id);
|
||||||
|
out.push(id);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
export {
|
||||||
|
extractPersonIds,
|
||||||
|
formatPersonDisplay,
|
||||||
|
isPersonValueItem,
|
||||||
|
normalizePersonModelValue
|
||||||
|
};
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
function asRules(schema) {
|
||||||
|
if (Array.isArray(schema)) return schema.filter((r) => r && typeof r === "object");
|
||||||
|
if (schema && typeof schema === "object") {
|
||||||
|
const s = schema;
|
||||||
|
const rules = s.rule || s.rules || s.fields;
|
||||||
|
if (Array.isArray(rules)) return rules.filter((r) => r && typeof r === "object");
|
||||||
|
if (s.type || s.field) return [s];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
function walk(rules, out) {
|
||||||
|
for (const r of rules) {
|
||||||
|
out.push(r);
|
||||||
|
if (Array.isArray(r.children)) {
|
||||||
|
walk(r.children, out);
|
||||||
|
}
|
||||||
|
const props = r.props;
|
||||||
|
if (props && Array.isArray(props.rule)) walk(props.rule, out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function resolveBinding(kit) {
|
||||||
|
const props = kit.props && typeof kit.props === "object" ? kit.props : {};
|
||||||
|
let hireDateField = props.hireDateField ? String(props.hireDateField) : void 0;
|
||||||
|
let lastWorkDayField = props.lastWorkDayField ? String(props.lastWorkDayField) : void 0;
|
||||||
|
const children = Array.isArray(kit.children) ? kit.children : Array.isArray(props.rule) ? props.rule : [];
|
||||||
|
for (const c of children) {
|
||||||
|
if (!c?.field) continue;
|
||||||
|
const sem = String(c.props?._fc_kit_field || "");
|
||||||
|
if (sem === "hireDate" && !hireDateField) hireDateField = String(c.field);
|
||||||
|
if (sem === "lastWorkDay" && !lastWorkDayField) lastWorkDayField = String(c.field);
|
||||||
|
}
|
||||||
|
return { hireDateField, lastWorkDayField };
|
||||||
|
}
|
||||||
|
function listResignationKitBindings(schema) {
|
||||||
|
const flat = [];
|
||||||
|
walk(asRules(schema), flat);
|
||||||
|
return flat.filter((r) => String(r.type || "") === "resignationKit").map(resolveBinding);
|
||||||
|
}
|
||||||
|
function isEmpty(raw) {
|
||||||
|
return raw == null || raw === "";
|
||||||
|
}
|
||||||
|
function applyResignationKitDefaults(schema, formData, user) {
|
||||||
|
const data = { ...formData };
|
||||||
|
if (!user) return data;
|
||||||
|
const hireDate = String(user.hireDate || "").trim();
|
||||||
|
if (!hireDate) return data;
|
||||||
|
for (const kit of listResignationKitBindings(schema)) {
|
||||||
|
const f = kit.hireDateField;
|
||||||
|
if (f && isEmpty(data[f])) data[f] = hireDate;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
export {
|
||||||
|
applyResignationKitDefaults,
|
||||||
|
listResignationKitBindings
|
||||||
|
};
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
const DATE_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
|
||||||
|
function dayIndex(dateStr) {
|
||||||
|
const m = DATE_RE.exec(String(dateStr || "").trim());
|
||||||
|
if (!m) return null;
|
||||||
|
const y = Number(m[1]), mo = Number(m[2]), d = Number(m[3]);
|
||||||
|
const t = Date.UTC(y, mo - 1, d);
|
||||||
|
if (Number.isNaN(t)) return null;
|
||||||
|
return t / 864e5;
|
||||||
|
}
|
||||||
|
function startOffset(half) {
|
||||||
|
if (half === "\u4E0A\u5348") return 0;
|
||||||
|
if (half === "\u4E0B\u5348") return 0.5;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function endOffset(half) {
|
||||||
|
if (half === "\u4E0A\u5348") return 0.5;
|
||||||
|
if (half === "\u4E0B\u5348") return 1;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function calcTripLegDays(startDate, startHalf, endDate, endHalf) {
|
||||||
|
const ds = dayIndex(startDate);
|
||||||
|
const de = dayIndex(endDate);
|
||||||
|
const so = startOffset(String(startHalf || "").trim());
|
||||||
|
const eo = endOffset(String(endHalf || "").trim());
|
||||||
|
if (ds == null || de == null || so == null || eo == null) return null;
|
||||||
|
const duration = de + eo - (ds + so);
|
||||||
|
if (!(duration > 0)) return null;
|
||||||
|
return Math.round(duration * 2) / 2;
|
||||||
|
}
|
||||||
|
function sumTripLegDays(legs) {
|
||||||
|
let sum = 0;
|
||||||
|
for (const leg of legs || []) {
|
||||||
|
const n = Number(leg?.duration);
|
||||||
|
if (Number.isFinite(n) && n >= 0) sum += n;
|
||||||
|
}
|
||||||
|
return Math.round(sum * 2) / 2;
|
||||||
|
}
|
||||||
|
function isHalfDayMultiple(n) {
|
||||||
|
const v = Number(n);
|
||||||
|
if (!Number.isFinite(v) || v < 0) return false;
|
||||||
|
return Math.abs(v * 2 - Math.round(v * 2)) < 1e-9;
|
||||||
|
}
|
||||||
|
function emptyTripLeg() {
|
||||||
|
return {
|
||||||
|
transport: "",
|
||||||
|
tripType: "",
|
||||||
|
fromCity: "",
|
||||||
|
toCity: "",
|
||||||
|
startDate: "",
|
||||||
|
startHalf: "\u4E0A\u5348",
|
||||||
|
endDate: "",
|
||||||
|
endHalf: "\u4E0B\u5348",
|
||||||
|
duration: null,
|
||||||
|
durationManual: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export {
|
||||||
|
calcTripLegDays,
|
||||||
|
emptyTripLeg,
|
||||||
|
isHalfDayMultiple,
|
||||||
|
sumTripLegDays
|
||||||
|
};
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import { calcTripLegDays, sumTripLegDays } from "./oa-trip-days.js";
|
||||||
|
const TRIP_MANUAL_PREFIX = "__manual_";
|
||||||
|
function asRules(schema) {
|
||||||
|
if (Array.isArray(schema)) return schema.filter((r) => r && typeof r === "object");
|
||||||
|
if (schema && typeof schema === "object") {
|
||||||
|
const s = schema;
|
||||||
|
const rules = s.rule || s.rules || s.fields;
|
||||||
|
if (Array.isArray(rules)) return rules.filter((r) => r && typeof r === "object");
|
||||||
|
if (s.type || s.field) return [s];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
function walk(rules, out) {
|
||||||
|
for (const r of rules) {
|
||||||
|
out.push(r);
|
||||||
|
const children = r.children;
|
||||||
|
if (Array.isArray(children)) {
|
||||||
|
walk(
|
||||||
|
children.filter((c) => c && typeof c === "object"),
|
||||||
|
out
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const props = r.props;
|
||||||
|
if (props && Array.isArray(props.rule)) {
|
||||||
|
walk(
|
||||||
|
props.rule.filter((c) => c && typeof c === "object"),
|
||||||
|
out
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function resolveTripKitFields(kit) {
|
||||||
|
const props = kit.props && typeof kit.props === "object" ? kit.props : {};
|
||||||
|
let listField = props.tripListField;
|
||||||
|
let daysField = props.tripDaysField;
|
||||||
|
if (listField && daysField) {
|
||||||
|
return { listField: String(listField), daysField: String(daysField) };
|
||||||
|
}
|
||||||
|
const children = Array.isArray(kit.children) ? kit.children : Array.isArray(props.rule) ? props.rule : [];
|
||||||
|
for (const c of children) {
|
||||||
|
if (!c?.field) continue;
|
||||||
|
const kitKey = String(c.props?._fc_kit_field || "");
|
||||||
|
if (kitKey === "tripList") listField = String(c.field);
|
||||||
|
else if (kitKey === "tripDays") daysField = String(c.field);
|
||||||
|
}
|
||||||
|
if (!listField || !daysField) return null;
|
||||||
|
return { listField, daysField };
|
||||||
|
}
|
||||||
|
function listTripKits(schema) {
|
||||||
|
const flat = [];
|
||||||
|
walk(asRules(schema), flat);
|
||||||
|
const out = [];
|
||||||
|
for (const r of flat) {
|
||||||
|
if (String(r.type || "") !== "tripKit") continue;
|
||||||
|
const ref = resolveTripKitFields(r);
|
||||||
|
if (ref) out.push(ref);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
function applyTripKitDaysToFormData(schema, formData) {
|
||||||
|
const data = { ...formData };
|
||||||
|
let changed = false;
|
||||||
|
for (const kit of listTripKits(schema)) {
|
||||||
|
const legsRaw = data[kit.listField];
|
||||||
|
if (!Array.isArray(legsRaw)) continue;
|
||||||
|
const prevLegsJson = JSON.stringify(data[kit.listField]);
|
||||||
|
const legs = [];
|
||||||
|
for (const leg of legsRaw) {
|
||||||
|
if (!leg || typeof leg !== "object") {
|
||||||
|
legs.push(leg);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const row = { ...leg };
|
||||||
|
if (!row.durationManual) {
|
||||||
|
const days = calcTripLegDays(
|
||||||
|
String(row.startDate || ""),
|
||||||
|
String(row.startHalf || ""),
|
||||||
|
String(row.endDate || ""),
|
||||||
|
String(row.endHalf || "")
|
||||||
|
);
|
||||||
|
if (days != null && row.duration !== days) {
|
||||||
|
row.duration = days;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
legs.push(row);
|
||||||
|
}
|
||||||
|
data[kit.listField] = legs;
|
||||||
|
if (JSON.stringify(legs) !== prevLegsJson) {
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
const manualKey = TRIP_MANUAL_PREFIX + kit.daysField;
|
||||||
|
if (data[manualKey]) continue;
|
||||||
|
const sum = sumTripLegDays(legs);
|
||||||
|
if (data[kit.daysField] !== sum) {
|
||||||
|
data[kit.daysField] = sum;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { data, changed };
|
||||||
|
}
|
||||||
|
export {
|
||||||
|
TRIP_MANUAL_PREFIX,
|
||||||
|
applyTripKitDaysToFormData,
|
||||||
|
listTripKits
|
||||||
|
};
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
const DEFAULT_TREE_PATH_SEPARATOR = " / ";
|
||||||
|
function buildTreePathLabelFromFlat(nodeId, flatList, separatorOrOpts) {
|
||||||
|
const separator = typeof separatorOrOpts === "string" ? separatorOrOpts : separatorOrOpts?.separator ?? DEFAULT_TREE_PATH_SEPARATOR;
|
||||||
|
const nameKey = typeof separatorOrOpts === "string" ? "name" : separatorOrOpts?.nameKey ?? "name";
|
||||||
|
const cid = String(nodeId ?? "").trim();
|
||||||
|
if (!cid) return "";
|
||||||
|
const byId = /* @__PURE__ */ new Map();
|
||||||
|
for (const c of flatList || []) {
|
||||||
|
const id = String(c?.id ?? "").trim();
|
||||||
|
if (id) byId.set(id, c);
|
||||||
|
}
|
||||||
|
const names = [];
|
||||||
|
let cur = cid;
|
||||||
|
const seen = /* @__PURE__ */ new Set();
|
||||||
|
while (cur && byId.has(cur) && !seen.has(cur)) {
|
||||||
|
seen.add(cur);
|
||||||
|
const cat = byId.get(cur);
|
||||||
|
const nm = String(cat?.[nameKey] ?? "").trim();
|
||||||
|
if (nm) names.unshift(nm);
|
||||||
|
const pid = String(cat?.parentId ?? cat?.parent_id ?? "").trim();
|
||||||
|
cur = pid || null;
|
||||||
|
}
|
||||||
|
return names.length ? names.join(separator) : "";
|
||||||
|
}
|
||||||
|
function buildPathLabelMapFromTreeRoots(roots, options) {
|
||||||
|
const idKey = options?.idKey ?? "id";
|
||||||
|
const nameKey = options?.nameKey ?? "name";
|
||||||
|
const childrenKey = options?.childrenKey ?? "children";
|
||||||
|
const sep = options?.separator ?? DEFAULT_TREE_PATH_SEPARATOR;
|
||||||
|
const map = /* @__PURE__ */ new Map();
|
||||||
|
const walk = (nodes, pathNames) => {
|
||||||
|
for (const raw of nodes || []) {
|
||||||
|
const n = raw;
|
||||||
|
const id = n?.[idKey] != null ? String(n[idKey]).trim() : "";
|
||||||
|
const curName = String(n?.[nameKey] ?? "").trim();
|
||||||
|
const nextPath = curName ? [...pathNames, curName] : [...pathNames];
|
||||||
|
if (id) map.set(id, nextPath.join(sep));
|
||||||
|
const ch = n?.[childrenKey];
|
||||||
|
if (Array.isArray(ch) && ch.length) walk(ch, nextPath);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
walk(roots || [], []);
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
function resolveTreePathDisplay(nodeId, flatList, fallbackName, separator = DEFAULT_TREE_PATH_SEPARATOR, emptyPlaceholder = "\u2014", nameKey) {
|
||||||
|
const path = buildTreePathLabelFromFlat(String(nodeId ?? "").trim() || void 0, flatList, {
|
||||||
|
separator,
|
||||||
|
nameKey
|
||||||
|
});
|
||||||
|
if (path) return path;
|
||||||
|
const fb = String(fallbackName ?? "").trim();
|
||||||
|
if (fb) return fb;
|
||||||
|
return emptyPlaceholder;
|
||||||
|
}
|
||||||
|
function findTreeSelectShortLabel(nodes, nodeId) {
|
||||||
|
const id = String(nodeId ?? "").trim();
|
||||||
|
if (!id) return "";
|
||||||
|
const walk = (arr) => {
|
||||||
|
for (const n of arr || []) {
|
||||||
|
if (!n) continue;
|
||||||
|
if (String(n.value ?? "") === id) return String(n.label ?? "").trim();
|
||||||
|
const hit = walk(n.children);
|
||||||
|
if (hit) return hit;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
return walk(nodes);
|
||||||
|
}
|
||||||
|
export {
|
||||||
|
DEFAULT_TREE_PATH_SEPARATOR,
|
||||||
|
buildPathLabelMapFromTreeRoots,
|
||||||
|
buildTreePathLabelFromFlat,
|
||||||
|
findTreeSelectShortLabel,
|
||||||
|
resolveTreePathDisplay
|
||||||
|
};
|
||||||
@@ -1,4 +1,10 @@
|
|||||||
import { oaFormOption } from './form-option.js'
|
import { oaFormOption } from './form-option.js'
|
||||||
|
import {
|
||||||
|
ensureLeaveKitRuleProps,
|
||||||
|
ensureOvertimeKitRuleProps,
|
||||||
|
ensureOwnerFieldRuleProps,
|
||||||
|
ensureConfirmationKitVisibility
|
||||||
|
} from './kit-runtime.js'
|
||||||
|
|
||||||
function walkRules(rules, fn) {
|
function walkRules(rules, fn) {
|
||||||
const list = Array.isArray(rules) ? rules : []
|
const list = Array.isArray(rules) ? rules : []
|
||||||
@@ -28,7 +34,11 @@ export function prepareMobileFormSchema(formSchemaJson) {
|
|||||||
: Array.isArray(schema)
|
: Array.isArray(schema)
|
||||||
? schema
|
? schema
|
||||||
: []
|
: []
|
||||||
const rule = ensureOaUploadRuleProps(rawRule)
|
const rule = ensureConfirmationKitVisibility(
|
||||||
|
ensureOwnerFieldRuleProps(
|
||||||
|
ensureOvertimeKitRuleProps(ensureLeaveKitRuleProps(ensureOaUploadRuleProps(rawRule)))
|
||||||
|
)
|
||||||
|
)
|
||||||
const option = {
|
const option = {
|
||||||
...oaFormOption,
|
...oaFormOption,
|
||||||
...(schema.option || {}),
|
...(schema.option || {}),
|
||||||
|
|||||||
@@ -49,9 +49,30 @@ import {
|
|||||||
fetchOaInstanceDraftDetail,
|
fetchOaInstanceDraftDetail,
|
||||||
fetchSubmitOaInstanceDraft,
|
fetchSubmitOaInstanceDraft,
|
||||||
fetchStartOaInstance,
|
fetchStartOaInstance,
|
||||||
fetchUserList
|
fetchUserList,
|
||||||
|
fetchCurrentUser,
|
||||||
|
fetchCnHolidayRange
|
||||||
} from '../api/oa.js'
|
} from '../api/oa.js'
|
||||||
import { prepareMobileFormSchema } from '../oa/schema-prep.js'
|
import { prepareMobileFormSchema } from '../oa/schema-prep.js'
|
||||||
|
import {
|
||||||
|
applyLeaveKitDurationsToFormData,
|
||||||
|
LEAVE_MANUAL_PREFIX,
|
||||||
|
listLeaveKitBindings,
|
||||||
|
markLeaveDurationManual,
|
||||||
|
applyOvertimeKitDefaults,
|
||||||
|
applyOvertimeKitDurationsToFormData,
|
||||||
|
listOvertimeKitBindings,
|
||||||
|
markOvertimeDurationManual,
|
||||||
|
OVERTIME_MANUAL_PREFIX,
|
||||||
|
applyTripKitDaysToFormData,
|
||||||
|
listTripKits,
|
||||||
|
TRIP_MANUAL_PREFIX,
|
||||||
|
applyConfirmationKitDateLinkage,
|
||||||
|
applyConfirmationKitDefaults,
|
||||||
|
listConfirmationKitBindings,
|
||||||
|
applyResignationKitDefaults,
|
||||||
|
applyOwnerFieldDefaults
|
||||||
|
} from '../oa/kit-runtime.js'
|
||||||
import OaFlowPreview from '../oa/components/OaFlowPreview.vue'
|
import OaFlowPreview from '../oa/components/OaFlowPreview.vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -70,9 +91,19 @@ const flowResolved = ref(false)
|
|||||||
const flowPendingMessage = ref('必填信息填写后,流程将自动显示')
|
const flowPendingMessage = ref('必填信息填写后,流程将自动显示')
|
||||||
const initiatorSelectByNode = ref({})
|
const initiatorSelectByNode = ref({})
|
||||||
const userOptions = ref([])
|
const userOptions = ref([])
|
||||||
|
const currentUser = ref(null)
|
||||||
|
|
||||||
let previewTimer = null
|
let previewTimer = null
|
||||||
let skipPreviewWatch = false
|
let skipPreviewWatch = false
|
||||||
|
let leaveKitSyncing = false
|
||||||
|
let leaveKitPrevSnapshot = {}
|
||||||
|
let tripKitSyncing = false
|
||||||
|
let tripKitPrevSnapshot = {}
|
||||||
|
let confirmationKitSyncing = false
|
||||||
|
let confirmationKitPrevSnapshot = {}
|
||||||
|
let overtimeKitSyncing = false
|
||||||
|
let overtimeKitPrevSnapshot = {}
|
||||||
|
const overtimeRestMap = {}
|
||||||
|
|
||||||
function closeToMini(payload) {
|
function closeToMini(payload) {
|
||||||
try {
|
try {
|
||||||
@@ -87,6 +118,14 @@ function closeToMini(payload) {
|
|||||||
showToast((payload && payload.message) || '已完成(非小程序环境)')
|
showToast((payload && payload.message) || '已完成(非小程序环境)')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadCurrentUser() {
|
||||||
|
try {
|
||||||
|
currentUser.value = (await fetchCurrentUser()) || null
|
||||||
|
} catch {
|
||||||
|
currentUser.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function loadUsers() {
|
async function loadUsers() {
|
||||||
try {
|
try {
|
||||||
const pageSize = 100
|
const pageSize = 100
|
||||||
@@ -101,11 +140,13 @@ async function loadUsers() {
|
|||||||
page += 1
|
page += 1
|
||||||
if (page > 50) break
|
if (page > 50) break
|
||||||
}
|
}
|
||||||
userOptions.value = raw.map((u) => ({
|
userOptions.value = raw
|
||||||
id: String(u.id || ''),
|
.map((u) => ({
|
||||||
nickName: u.nickName || '',
|
id: String(u.id || ''),
|
||||||
userNo: u.userNo || ''
|
nickName: u.nickName || '',
|
||||||
})).filter((u) => u.id)
|
userNo: u.userNo || ''
|
||||||
|
}))
|
||||||
|
.filter((u) => u.id)
|
||||||
} catch {
|
} catch {
|
||||||
userOptions.value = []
|
userOptions.value = []
|
||||||
}
|
}
|
||||||
@@ -155,7 +196,227 @@ function scheduleFlowPreview() {
|
|||||||
}, 300)
|
}, 300)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadByAppId(appId) {
|
function syncLeaveKitDurations() {
|
||||||
|
if (leaveKitSyncing) return
|
||||||
|
if (!formRule.value.length) return
|
||||||
|
const schema = { rule: formRule.value }
|
||||||
|
const bindings = listLeaveKitBindings(schema)
|
||||||
|
if (!bindings.length) return
|
||||||
|
|
||||||
|
const curr = { ...formData.value }
|
||||||
|
for (const kit of bindings) {
|
||||||
|
const prevDur = leaveKitPrevSnapshot[kit.durationField]
|
||||||
|
const currDur = curr[kit.durationField]
|
||||||
|
const startChanged = leaveKitPrevSnapshot[kit.startField] !== curr[kit.startField]
|
||||||
|
const endChanged = leaveKitPrevSnapshot[kit.endField] !== curr[kit.endField]
|
||||||
|
const durChanged = prevDur !== currDur
|
||||||
|
if (durChanged && !startChanged && !endChanged && currDur !== undefined && currDur !== '') {
|
||||||
|
Object.assign(curr, markLeaveDurationManual(curr, kit.durationField, true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data, changed } = applyLeaveKitDurationsToFormData(schema, curr)
|
||||||
|
leaveKitPrevSnapshot = { ...data }
|
||||||
|
const manualKeysChanged = bindings.some(
|
||||||
|
(k) =>
|
||||||
|
!!data[LEAVE_MANUAL_PREFIX + k.durationField] !==
|
||||||
|
!!formData.value[LEAVE_MANUAL_PREFIX + k.durationField]
|
||||||
|
)
|
||||||
|
const durationChanged = bindings.some(
|
||||||
|
(k) => data[k.durationField] !== formData.value[k.durationField]
|
||||||
|
)
|
||||||
|
if (changed || manualKeysChanged || durationChanged) {
|
||||||
|
leaveKitSyncing = true
|
||||||
|
formData.value = data
|
||||||
|
queueMicrotask(() => {
|
||||||
|
leaveKitSyncing = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncTripKitDurations() {
|
||||||
|
if (tripKitSyncing) return
|
||||||
|
if (!formRule.value.length) return
|
||||||
|
const schema = { rule: formRule.value }
|
||||||
|
const kits = listTripKits(schema)
|
||||||
|
if (!kits.length) return
|
||||||
|
|
||||||
|
const curr = { ...formData.value }
|
||||||
|
for (const kit of kits) {
|
||||||
|
const prevDays = tripKitPrevSnapshot[kit.daysField]
|
||||||
|
const currDays = curr[kit.daysField]
|
||||||
|
const daysChanged = prevDays !== currDays
|
||||||
|
const listChanged =
|
||||||
|
JSON.stringify(tripKitPrevSnapshot[kit.listField]) !== JSON.stringify(curr[kit.listField])
|
||||||
|
if (daysChanged && !listChanged && currDays !== undefined && currDays !== '') {
|
||||||
|
curr[TRIP_MANUAL_PREFIX + kit.daysField] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data, changed } = applyTripKitDaysToFormData(schema, curr)
|
||||||
|
tripKitPrevSnapshot = { ...data }
|
||||||
|
const manualKeysChanged = kits.some(
|
||||||
|
(k) =>
|
||||||
|
!!data[TRIP_MANUAL_PREFIX + k.daysField] !==
|
||||||
|
!!formData.value[TRIP_MANUAL_PREFIX + k.daysField]
|
||||||
|
)
|
||||||
|
const daysFieldChanged = kits.some((k) => data[k.daysField] !== formData.value[k.daysField])
|
||||||
|
if (changed || manualKeysChanged || daysFieldChanged) {
|
||||||
|
tripKitSyncing = true
|
||||||
|
formData.value = data
|
||||||
|
queueMicrotask(() => {
|
||||||
|
tripKitSyncing = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncConfirmationKitDates() {
|
||||||
|
if (confirmationKitSyncing) return
|
||||||
|
if (!formRule.value.length) return
|
||||||
|
const schema = { rule: formRule.value }
|
||||||
|
if (!listConfirmationKitBindings(schema).length) return
|
||||||
|
const curr = { ...formData.value }
|
||||||
|
const { data, changed } = applyConfirmationKitDateLinkage(schema, curr)
|
||||||
|
confirmationKitPrevSnapshot = { ...data }
|
||||||
|
if (changed) {
|
||||||
|
confirmationKitSyncing = true
|
||||||
|
formData.value = data
|
||||||
|
queueMicrotask(() => {
|
||||||
|
confirmationKitSyncing = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureOvertimeHolidayRange(startYmd, endYmd) {
|
||||||
|
const start = String(startYmd || '').trim().slice(0, 10)
|
||||||
|
const end = String(endYmd || '').trim().slice(0, 10)
|
||||||
|
if (!start || !end) return
|
||||||
|
let lo = start
|
||||||
|
let hi = end
|
||||||
|
if (lo > hi) {
|
||||||
|
const t = lo
|
||||||
|
lo = hi
|
||||||
|
hi = t
|
||||||
|
}
|
||||||
|
const begin = new Date(lo + 'T00:00:00')
|
||||||
|
const finish = new Date(hi + 'T00:00:00')
|
||||||
|
if (Number.isNaN(begin.getTime()) || Number.isNaN(finish.getTime())) return
|
||||||
|
|
||||||
|
let allPresent = true
|
||||||
|
for (let d = new Date(begin); d <= finish; d.setDate(d.getDate() + 1)) {
|
||||||
|
const y = d.getFullYear()
|
||||||
|
const m = String(d.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(d.getDate()).padStart(2, '0')
|
||||||
|
const key = `${y}-${m}-${day}`
|
||||||
|
if (!(key in overtimeRestMap)) {
|
||||||
|
allPresent = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (allPresent) return
|
||||||
|
try {
|
||||||
|
const res = await fetchCnHolidayRange(lo, hi)
|
||||||
|
const days = res?.days || []
|
||||||
|
for (const day of days) {
|
||||||
|
const key = String(day.dayDate || '').slice(0, 10)
|
||||||
|
if (key) overtimeRestMap[key] = !!day.isRestDay
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* 失败保留已有 map,计算回退周末规则 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRestDayResolver(ymd) {
|
||||||
|
if (ymd in overtimeRestMap) return overtimeRestMap[ymd]
|
||||||
|
const d = new Date(ymd + 'T00:00:00')
|
||||||
|
const w = d.getDay()
|
||||||
|
return w === 0 || w === 6
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncOvertimeKitDurations() {
|
||||||
|
if (overtimeKitSyncing) return
|
||||||
|
if (!formRule.value.length) return
|
||||||
|
const schema = { rule: formRule.value }
|
||||||
|
const bindings = listOvertimeKitBindings(schema)
|
||||||
|
if (!bindings.length) return
|
||||||
|
|
||||||
|
overtimeKitSyncing = true
|
||||||
|
try {
|
||||||
|
let minYmd = ''
|
||||||
|
let maxYmd = ''
|
||||||
|
for (const kit of bindings) {
|
||||||
|
for (const field of [kit.startField, kit.endField]) {
|
||||||
|
const ymd = String(formData.value[field] || '')
|
||||||
|
.trim()
|
||||||
|
.slice(0, 10)
|
||||||
|
if (ymd.length < 10) continue
|
||||||
|
if (!minYmd || ymd < minYmd) minYmd = ymd
|
||||||
|
if (!maxYmd || ymd > maxYmd) maxYmd = ymd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (minYmd && maxYmd) {
|
||||||
|
await ensureOvertimeHolidayRange(minYmd, maxYmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
const curr = { ...formData.value }
|
||||||
|
for (const kit of bindings) {
|
||||||
|
const prevDur = overtimeKitPrevSnapshot[kit.durationField]
|
||||||
|
const currDur = curr[kit.durationField]
|
||||||
|
const startChanged = overtimeKitPrevSnapshot[kit.startField] !== curr[kit.startField]
|
||||||
|
const endChanged = overtimeKitPrevSnapshot[kit.endField] !== curr[kit.endField]
|
||||||
|
const durChanged = prevDur !== currDur
|
||||||
|
if (
|
||||||
|
durChanged &&
|
||||||
|
!startChanged &&
|
||||||
|
!endChanged &&
|
||||||
|
currDur !== undefined &&
|
||||||
|
currDur !== ''
|
||||||
|
) {
|
||||||
|
Object.assign(curr, markOvertimeDurationManual(curr, kit.durationField, true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data, changed } = applyOvertimeKitDurationsToFormData(
|
||||||
|
schema,
|
||||||
|
curr,
|
||||||
|
isRestDayResolver
|
||||||
|
)
|
||||||
|
overtimeKitPrevSnapshot = { ...data }
|
||||||
|
const manualKeysChanged = bindings.some(
|
||||||
|
(k) =>
|
||||||
|
!!data[OVERTIME_MANUAL_PREFIX + k.durationField] !==
|
||||||
|
!!formData.value[OVERTIME_MANUAL_PREFIX + k.durationField]
|
||||||
|
)
|
||||||
|
const durationChanged = bindings.some(
|
||||||
|
(k) => data[k.durationField] !== formData.value[k.durationField]
|
||||||
|
)
|
||||||
|
if (changed || manualKeysChanged || durationChanged) {
|
||||||
|
formData.value = data
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
queueMicrotask(() => {
|
||||||
|
overtimeKitSyncing = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyKitDefaults(baseData) {
|
||||||
|
let data = { ...(baseData || {}) }
|
||||||
|
const user = currentUser.value
|
||||||
|
data = await applyOwnerFieldDefaults(formRule.value, data, user)
|
||||||
|
data = await applyConfirmationKitDefaults({ rule: formRule.value }, data, user)
|
||||||
|
data = applyResignationKitDefaults({ rule: formRule.value }, data, user)
|
||||||
|
data = applyOvertimeKitDefaults({ rule: formRule.value }, data, user)
|
||||||
|
const linked = applyConfirmationKitDateLinkage({ rule: formRule.value }, data)
|
||||||
|
data = linked.data
|
||||||
|
confirmationKitPrevSnapshot = { ...data }
|
||||||
|
overtimeKitPrevSnapshot = { ...data }
|
||||||
|
leaveKitPrevSnapshot = { ...data }
|
||||||
|
tripKitPrevSnapshot = { ...data }
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadByAppId(appId, { seedData = null, applyDefaults = true } = {}) {
|
||||||
const detail = await fetchOaTemplateDetail(appId)
|
const detail = await fetchOaTemplateDetail(appId)
|
||||||
appMeta.value = {
|
appMeta.value = {
|
||||||
id: appId,
|
id: appId,
|
||||||
@@ -164,6 +425,16 @@ async function loadByAppId(appId) {
|
|||||||
const prepared = prepareMobileFormSchema(detail?.formSchemaJson)
|
const prepared = prepareMobileFormSchema(detail?.formSchemaJson)
|
||||||
formRule.value = prepared.rule
|
formRule.value = prepared.rule
|
||||||
formOption.value = prepared.option
|
formOption.value = prepared.option
|
||||||
|
if (applyDefaults) {
|
||||||
|
formData.value = await applyKitDefaults(seedData || {})
|
||||||
|
await syncOvertimeKitDurations()
|
||||||
|
} else if (seedData) {
|
||||||
|
formData.value = { ...seedData }
|
||||||
|
leaveKitPrevSnapshot = { ...formData.value }
|
||||||
|
tripKitPrevSnapshot = { ...formData.value }
|
||||||
|
confirmationKitPrevSnapshot = { ...formData.value }
|
||||||
|
overtimeKitPrevSnapshot = { ...formData.value }
|
||||||
|
}
|
||||||
await loadFlowPreview()
|
await loadFlowPreview()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,6 +442,7 @@ async function bootstrap() {
|
|||||||
detailLoading.value = true
|
detailLoading.value = true
|
||||||
skipPreviewWatch = true
|
skipPreviewWatch = true
|
||||||
try {
|
try {
|
||||||
|
await loadCurrentUser()
|
||||||
const appId = String(route.query.appId || '').trim()
|
const appId = String(route.query.appId || '').trim()
|
||||||
const draftId = String(route.query.draftId || '').trim()
|
const draftId = String(route.query.draftId || '').trim()
|
||||||
if (draftId) {
|
if (draftId) {
|
||||||
@@ -178,17 +450,18 @@ async function bootstrap() {
|
|||||||
editingDraftId.value = draftId
|
editingDraftId.value = draftId
|
||||||
const aid = String(d?.appId || '').trim()
|
const aid = String(d?.appId || '').trim()
|
||||||
if (!aid) throw new Error('草稿缺少 appId')
|
if (!aid) throw new Error('草稿缺少 appId')
|
||||||
formData.value = d.formData && typeof d.formData === 'object' ? { ...d.formData } : {}
|
|
||||||
initiatorSelectByNode.value =
|
initiatorSelectByNode.value =
|
||||||
d.initiatorSelectByNode && typeof d.initiatorSelectByNode === 'object'
|
d.initiatorSelectByNode && typeof d.initiatorSelectByNode === 'object'
|
||||||
? { ...d.initiatorSelectByNode }
|
? { ...d.initiatorSelectByNode }
|
||||||
: {}
|
: {}
|
||||||
await loadByAppId(aid)
|
const seed =
|
||||||
|
d.formData && typeof d.formData === 'object' ? { ...d.formData } : {}
|
||||||
|
// 草稿:保留已填值,仅补空的 owner 等默认
|
||||||
|
await loadByAppId(aid, { seedData: seed, applyDefaults: true })
|
||||||
} else if (appId) {
|
} else if (appId) {
|
||||||
formData.value = {}
|
|
||||||
initiatorSelectByNode.value = {}
|
initiatorSelectByNode.value = {}
|
||||||
editingDraftId.value = ''
|
editingDraftId.value = ''
|
||||||
await loadByAppId(appId)
|
await loadByAppId(appId, { seedData: {}, applyDefaults: true })
|
||||||
} else {
|
} else {
|
||||||
showToast('缺少 appId')
|
showToast('缺少 appId')
|
||||||
}
|
}
|
||||||
@@ -288,7 +561,20 @@ async function submit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
[formData, initiatorSelectByNode],
|
formData,
|
||||||
|
() => {
|
||||||
|
if (skipPreviewWatch) return
|
||||||
|
syncLeaveKitDurations()
|
||||||
|
syncTripKitDurations()
|
||||||
|
syncConfirmationKitDates()
|
||||||
|
void syncOvertimeKitDurations()
|
||||||
|
scheduleFlowPreview()
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
initiatorSelectByNode,
|
||||||
() => {
|
() => {
|
||||||
if (skipPreviewWatch) return
|
if (skipPreviewWatch) return
|
||||||
scheduleFlowPreview()
|
scheduleFlowPreview()
|
||||||
|
|||||||
Reference in new issue
Block a user