修复变更、增加筛选条件
This commit is contained in:
1 parent
712ec51d68
commit
e06afc8c33
137 files changed
+35872
-275
No files matched your search
@@ -11,9 +11,18 @@ Chrome Manifest V3 扩展,在用户已登录的 LTJT ERP 页面内执行经过
|
||||
|
||||
## 当前版本
|
||||
|
||||
当前源码版本为 `0.5.173`。版本化 ZIP、文件哈希和 Skill/DOCX 基线见 [`../../dist/release-manifest.json`](../../dist/release-manifest.json)。旧版本实现流水已冻结在 [`../../archive/project-history/2026-08-16/chrome-extension-README.pre-governance.md`](../../archive/project-history/2026-08-16/chrome-extension-README.pre-governance.md)。
|
||||
当前源码版本为 `0.5.177`。版本化 ZIP、文件哈希和 Skill/DOCX 基线见 [`../../dist/release-manifest.json`](../../dist/release-manifest.json)。旧版本实现流水已冻结在 [`../../archive/project-history/2026-08-16/chrome-extension-README.pre-governance.md`](../../archive/project-history/2026-08-16/chrome-extension-README.pre-governance.md)。
|
||||
|
||||
0.5.173 当前重点:
|
||||
0.5.177 当前重点:
|
||||
|
||||
- 程序模式“安排大交通 / 安排大交通变更”新增选填车票说明(别名:车票说明搜索)。与结算单位搜索共同筛选同一条原生候选,填写后仅接受唯一结果;省略沿用首条匹配行为。完整说明与价格等字段仍由 ERP 原生选择联动。原有“车票/控位说明”兼容字段不变。需同步更新服务端,无新增迁移。
|
||||
|
||||
- 程序模式“安排用车 / 安排用车变更”新增选填用车类型(别名:车型)。以车型关键词过滤结算单位搜索候选,只接受唯一结果;不匹配或多匹配停止,保留原有省略行为。变更日期仅定位原安排,类型只筛选新车辆。需同步更新服务端,数据库仍为迁移 024,无新增迁移。
|
||||
|
||||
- 新增 Program-only“安排导游变更 / 安排用车变更 / 安排大交通变更 / 安排其他/备案变更”。日期仅定位原安排,资源和明确填写的数量、备注及备案号/口岸作为新值;保留原日期、ID、未填写字段和其他行。需同步部署服务端、执行迁移 `024_arrangement_update_routes` 并由管理员授予四项变更权限;没有自动授予权限。已完成本地回归,真实 ERP 验收待部署方执行。
|
||||
- 程序模式支持“安排酒店变更”:按原入住及离店日期唯一定位已有酒店行,使用 ERP 原生酒店选择联动更换酒店和房型,并修改间数;保留记录 ID、原日期、状态、备注及其他行。仅允许未安排或未确认、无付款及审核的记录;多条日期匹配时停止。已完成本地回归,真实 ERP 验收由部署方进行;需同步升级服务端。
|
||||
|
||||
继续保留 0.5.173 的名单修复:
|
||||
|
||||
- 名单 TSV 在派发校验、领队提取和原生名单合并时保留末尾空列的制表符,只统一换行并移除首尾空行。最后一条电话或备注为空时仍按完整 13 列处理;真正少列、多列及领队不一致仍阻断。本次需同步部署服务端的统一 operation 校验修复,不涉及数据库变更。
|
||||
|
||||
|
||||
@@ -2387,6 +2387,17 @@ function erpResolutionFailure(operation, report = {}) {
|
||||
|
||||
function lifecycleRouteFailure(operation, report = {}) {
|
||||
const blockers = Array.isArray(report.blockers) ? report.blockers.map(String) : [];
|
||||
const targetCount = blockers.find((item) => /^arrangement_update_target_candidate_count:\d+$/.test(item));
|
||||
if (targetCount) {
|
||||
const count = Number(targetCount.split(':').at(-1));
|
||||
const label = { arrangement_hotel: '入住及离店日期均匹配的酒店', arrangement_vehicle: '用车日期范围匹配的车辆', arrangement_transport: '交通日期匹配的大交通', arrangement_other: '业务日期匹配的其他/备案', arrangement_guide: '当前团队的导游' }[operation?.action] || '原';
|
||||
return {
|
||||
errorCode: 'arrangement_update_target_not_unique',
|
||||
message: count === 0
|
||||
? `ERP 中没有找到${label}安排,请核对原安排;未写入 ERP。`
|
||||
: `ERP 中找到 ${count} 条${label}安排,无法唯一确定可变更的记录,请核对原安排;未写入 ERP。`
|
||||
};
|
||||
}
|
||||
const noopBlocker = blockers.find((item) => item.startsWith('lifecycle_transition_target_already_current:'));
|
||||
if (noopBlocker) {
|
||||
const currentStatus = noopBlocker.slice('lifecycle_transition_target_already_current:'.length).trim();
|
||||
|
||||
@@ -2939,16 +2939,28 @@
|
||||
.map((row) => row.split('◆').map((cell) => String(cell || '').trim()));
|
||||
}
|
||||
|
||||
function uniqueLookupCandidate(rows, ref, { guide = false, hotel = false, transport = false, other = false, item = '', searchColumns = [] } = {}) {
|
||||
function candidateDescriptionMatches(item, query) {
|
||||
const text = normalizedLookupSearchValue(String(item || '').normalize('NFKC'));
|
||||
const keyword = normalizedLookupSearchValue(String(query || '').normalize('NFKC'));
|
||||
if (!keyword) return false;
|
||||
// Numeric fragments must not match larger numbers, e.g. 15 vs 115 or D88 vs D888.
|
||||
for (let at = text.indexOf(keyword); at >= 0; at = text.indexOf(keyword, at + 1)) {
|
||||
if (/\d/.test(keyword[0]) && /\d/.test(text[at - 1] || '')) continue;
|
||||
if (/\d/.test(keyword.at(-1)) && /\d/.test(text[at + keyword.length] || '')) continue;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function uniqueLookupCandidate(rows, ref, { guide = false, hotel = false, transport = false, other = false, item = '', vehicleType, ticketDescription, searchColumns = [] } = {}) {
|
||||
const combinedCandidateLookup = hotel || transport || other;
|
||||
const queries = hotel
|
||||
? hotelLookupQueries(ref)
|
||||
: combinedCandidateLookup
|
||||
? [...new Set([ref?.name, ref?.keyword].map(compactHotelLookupValue).filter(Boolean))]
|
||||
: [...new Set([ref?.name, ref?.keyword].map(normalizedLookupSearchValue).filter(Boolean))];
|
||||
// Hotel room type and transport ticket/control description are ERP-derived
|
||||
// values, never parser-side filters. The selected dropdown row supplies
|
||||
// them after the settlement-unit candidate is chosen.
|
||||
// Legacy item remains ERP-derived. Optional type/description keywords are
|
||||
// separate filters; the chosen native row still supplies the complete item.
|
||||
const expectedItem = combinedCandidateLookup ? '' : normalizedLookupSearchValue(item);
|
||||
const candidates = rows.map((row) => ({
|
||||
row,
|
||||
@@ -2956,7 +2968,10 @@
|
||||
name: String(guide ? row[2] : row[1] || '').trim(),
|
||||
item: String(guide ? '' : row[2] || '').trim(),
|
||||
raw: Array.isArray(row) ? row.join('◆') : String(row || '')
|
||||
})).filter((candidate) => candidate.id && candidate.name && (!expectedItem || normalizedLookupSearchValue(candidate.item) === expectedItem));
|
||||
})).filter((candidate) => candidate.id && candidate.name
|
||||
&& (!expectedItem || normalizedLookupSearchValue(candidate.item) === expectedItem)
|
||||
&& (vehicleType === undefined || candidateDescriptionMatches(candidate.item, vehicleType))
|
||||
&& (ticketDescription === undefined || candidateDescriptionMatches(candidate.item, ticketDescription)));
|
||||
const searchable = (candidate) => {
|
||||
const cells = combinedCandidateLookup
|
||||
// Hotel and transport SelectBox options are rendered as combined
|
||||
@@ -2995,10 +3010,10 @@
|
||||
: contains.length
|
||||
? contains
|
||||
: tokenized;
|
||||
const firstMatchDefaulted = combinedCandidateLookup && matches.length > 1;
|
||||
const firstMatchDefaulted = combinedCandidateLookup && ticketDescription === undefined && matches.length > 1;
|
||||
return {
|
||||
// The native dropdown is ordered; for hotel and transport, use its
|
||||
// first matching row when one settlement unit has multiple candidates.
|
||||
// Preserve ordered legacy selection only when no explicit description
|
||||
// filter is supplied. An explicit filter always requires a unique row.
|
||||
candidate: matches.length === 1 || firstMatchDefaulted ? matches[0] : null,
|
||||
match_count: matches.length,
|
||||
candidate_count: candidates.length,
|
||||
@@ -3047,7 +3062,8 @@
|
||||
|| form?.querySelector?.('#daoyou0, [name="daoyou0"]');
|
||||
const currentValue = controlCanonicalValue(form, 'daoguan');
|
||||
const rows = selectBoxRows(field);
|
||||
const guideRef = operation?.data?.arrangement?.resource || {};
|
||||
const guideArrangement = operation?.data?.arrangement || {};
|
||||
const guideRef = guideArrangement.changes?.resource || guideArrangement.resource || {};
|
||||
const guideResolution = uniqueLookupCandidate(selectBoxRows(lookup), guideRef, { guide: true });
|
||||
const linkedCandidate = guideResolution.candidate?.row || [];
|
||||
const linkedValue = String(linkedCandidate[5] || '').trim();
|
||||
@@ -3066,7 +3082,7 @@
|
||||
value_redacted: true
|
||||
};
|
||||
}
|
||||
if (currentValue && normalizedLookupValue(currentValue) !== normalizedLookupValue(linkedValue)) {
|
||||
if (guideArrangement.mode !== 'update' && currentValue && normalizedLookupValue(currentValue) !== normalizedLookupValue(linkedValue)) {
|
||||
return {
|
||||
value: currentValue,
|
||||
source: 'form_linked_value_mismatch',
|
||||
@@ -3126,20 +3142,105 @@
|
||||
const blockers = [];
|
||||
let evidence = null;
|
||||
|
||||
if (hotel && arrangement.mode === 'update') {
|
||||
const rowId = controlCanonicalValue(form, 'id0');
|
||||
const resourceId = controlCanonicalValue(form, 'danweiid0');
|
||||
const resourceName = controlCanonicalValue(form, 'danwei0');
|
||||
const roomType = controlCanonicalValue(form, 'fangxing0');
|
||||
const startDate = strictIsoDate(controlCanonicalValue(form, 'riqi0'));
|
||||
const endDate = strictIsoDate(controlCanonicalValue(form, 'riqis0'));
|
||||
const roomCount = Number(controlCanonicalValue(form, 'jianshu0'));
|
||||
const status = controlCanonicalValue(form, 'zt0');
|
||||
const remark = controlCanonicalValue(form, 'beizhu0');
|
||||
if (!rowId || !resourceId || !resourceName || !roomType) blockers.push('arrangement_update_current_row_reference_incomplete');
|
||||
if (!hotel && arrangement.mode === 'update') {
|
||||
let slotIndex = 0;
|
||||
const selection = arrangement.selection || {};
|
||||
if (guide) {
|
||||
const existing = Array.from(form.elements || []).filter((control) => /^daoyouid\d+$/.test(control.name || control.id || '') && String(control.value || '').trim() && String(control.value).trim() !== '0');
|
||||
if (existing.length !== 1 || (existing[0].name || existing[0].id) !== 'daoyouid0') return { ok: false, operation: resolved, blockers: [`arrangement_update_target_candidate_count:${existing.length}`], evidence: { value_redacted: true } };
|
||||
} else {
|
||||
const indices = [...new Set(Array.from(form.elements || []).map((control) => /^id(\d+)$/.exec(control.name || control.id || '')?.[1]).filter(Boolean).map(Number))];
|
||||
const matching = indices.filter((index) => {
|
||||
const id = controlCanonicalValue(form, `id${index}`);
|
||||
return id && id !== '0' && (action === 'arrangement_vehicle'
|
||||
? strictIsoDate(controlCanonicalValue(form, `riqi${index}`)) === selection.start_date && strictIsoDate(controlCanonicalValue(form, `riqis${index}`)) === selection.end_date
|
||||
: strictIsoDate(controlCanonicalValue(form, `riqi${index}`)) === selection.date);
|
||||
});
|
||||
if (matching.length !== 1) return { ok: false, operation: resolved, blockers: [`arrangement_update_target_candidate_count:${matching.length}`], evidence: { value_redacted: true } };
|
||||
slotIndex = matching[0];
|
||||
}
|
||||
const current = (name) => controlCanonicalValue(form, `${name}${slotIndex}`);
|
||||
const resourceId = current(guide ? 'daoyouid' : 'danweiid');
|
||||
const resourceName = current(guide ? 'daoyou' : 'danwei');
|
||||
const rowId = guide ? '' : current('id');
|
||||
if (!resourceId || resourceId === '0' || !resourceName || (!guide && (!rowId || rowId === '0'))) blockers.push('arrangement_update_current_row_reference_incomplete');
|
||||
if (!guide && Array.from(form.elements || []).filter((control) => /^id\d+$/.test(control.name || control.id || '') && String(control.value).trim() === rowId).length !== 1) blockers.push('arrangement_update_row_id_not_unique');
|
||||
arrangement.target = { slot_index: slotIndex, ...(!guide ? { row_id: rowId } : {}) };
|
||||
arrangement[guide ? 'resource' : 'supplier'] = { name: resourceName, id: resourceId, resolved: true };
|
||||
arrangement.remark = current('beizhu');
|
||||
arrangement.status = current('zt') || ((guide || other) ? '未确认' : '');
|
||||
arrangement.side_effect_policy = 'no_external';
|
||||
if (!guide) {
|
||||
arrangement.item = current(action === 'arrangement_vehicle' ? 'chehao' : 'shuoming');
|
||||
if (action === 'arrangement_vehicle') {
|
||||
arrangement.start_date = strictIsoDate(current('riqi'));
|
||||
arrangement.end_date = strictIsoDate(current('riqis'));
|
||||
} else arrangement.date = strictIsoDate(current('riqi'));
|
||||
const quantityControl = form.elements?.namedItem?.(`shuliang${slotIndex}`) || (action === 'arrangement_vehicle' ? form.elements?.namedItem?.(`tianshu${slotIndex}`) : null);
|
||||
if (!quantityControl) blockers.push('arrangement_update_quantity_field_missing');
|
||||
else {
|
||||
const quantity = Number(quantityControl.value);
|
||||
if (!Number.isInteger(quantity) || quantity < (other ? 0 : 1)) blockers.push('arrangement_update_current_quantity_invalid');
|
||||
arrangement.quantity = quantity;
|
||||
}
|
||||
if (!arrangement.item) blockers.push('arrangement_update_current_item_missing');
|
||||
}
|
||||
if (other) {
|
||||
const filing = {};
|
||||
for (const [key, name] of Object.entries({ number: 'beianhao', date: 'beianri', entry_port: 'rujingkou', exit_port: 'chujingkou' })) {
|
||||
const value = controlCanonicalValue(form, name);
|
||||
if (value) filing[key] = key === 'date' ? strictIsoDate(value) : value;
|
||||
}
|
||||
if (selection.filing_date && selection.filing_date !== filing.date) blockers.push('arrangement_update_original_filing_date_mismatch');
|
||||
arrangement.filing = filing;
|
||||
}
|
||||
const lookup = form.elements?.namedItem?.(`${guide ? 'daoyou' : 'danwei'}${slotIndex}`);
|
||||
const selected = uniqueLookupCandidate(selectBoxRows(lookup), arrangement.changes?.[guide ? 'resource' : 'supplier'], {
|
||||
guide, transport, other, vehicleType: action === 'arrangement_vehicle' ? arrangement.vehicle_type : undefined,
|
||||
ticketDescription: action === 'arrangement_transport' ? arrangement.ticket_description : undefined,
|
||||
searchColumns: action === 'arrangement_vehicle' ? [1, 2, 3] : []
|
||||
});
|
||||
if (!selected.candidate) blockers.push(`arrangement_resource_candidate_count:${selected.match_count}`);
|
||||
else {
|
||||
const ref = arrangement.changes[guide ? 'resource' : 'supplier'];
|
||||
arrangement.changes = { ...arrangement.changes, [guide ? 'resource' : 'supplier']: { ...ref, id: selected.candidate.id, name: selected.candidate.name, resolved: true }, ...(!guide ? { item: selected.candidate.item } : {}) };
|
||||
if (!guide && !selected.candidate.item) blockers.push('arrangement_update_new_item_missing');
|
||||
}
|
||||
if (guide) {
|
||||
const coordinator = guideCoordinatorResolution(form, operation, form.ownerDocument);
|
||||
if (!coordinator.value || coordinator.mismatch) blockers.push('arrangement_guide_linked_coordinator_unresolved');
|
||||
}
|
||||
delete arrangement.selection;
|
||||
evidence = { mode: 'update', slot_index: slotIndex, current_row_count: 1, value_redacted: true };
|
||||
} else if (hotel && arrangement.mode === 'update') {
|
||||
const selection = arrangement.selection;
|
||||
let slotIndex = 0;
|
||||
if (selection) {
|
||||
const indices = [...new Set(Array.from(form.elements || []).map((control) => /^id(\d+)$/.exec(control.name || control.id || '')?.[1]).filter(Boolean).map(Number))];
|
||||
const matching = indices.filter((index) => {
|
||||
const id = controlCanonicalValue(form, `id${index}`);
|
||||
return id && id !== '0'
|
||||
&& strictIsoDate(controlCanonicalValue(form, `riqi${index}`)) === selection.start_date
|
||||
&& strictIsoDate(controlCanonicalValue(form, `riqis${index}`)) === selection.end_date;
|
||||
});
|
||||
if (matching.length !== 1) return { ok: false, operation: resolved, blockers: [`arrangement_update_target_candidate_count:${matching.length}`], evidence: { match_count: matching.length, value_redacted: true } };
|
||||
slotIndex = matching[0];
|
||||
}
|
||||
const current = (name) => controlCanonicalValue(form, `${name}${slotIndex}`);
|
||||
const rowId = current('id');
|
||||
const resourceId = current('danweiid');
|
||||
const resourceName = current('danwei');
|
||||
const roomType = current('fangxing');
|
||||
const startDate = strictIsoDate(current('riqi'));
|
||||
const endDate = strictIsoDate(current('riqis'));
|
||||
const roomCount = Number(current('jianshu'));
|
||||
const status = current('zt');
|
||||
const remark = current('beizhu');
|
||||
if (!rowId || rowId === '0' || !resourceId || resourceId === '0' || !resourceName || !roomType) blockers.push('arrangement_update_current_row_reference_incomplete');
|
||||
if (Array.from(form.elements || []).filter((control) => /^id\d+$/.test(control.name || control.id || '') && String(control.value).trim() === rowId).length !== 1) blockers.push('arrangement_update_row_id_not_unique');
|
||||
if (!startDate || !endDate || !Number.isInteger(roomCount) || roomCount < 1) blockers.push('arrangement_update_current_business_snapshot_incomplete');
|
||||
if (!status) blockers.push('arrangement_update_current_status_missing');
|
||||
arrangement.target = { slot_index: 0, row_id: rowId };
|
||||
arrangement.target = { slot_index: slotIndex, row_id: rowId };
|
||||
arrangement.resource = { name: resourceName, keyword: resourceName, id: resourceId, resolved: true };
|
||||
arrangement.room_type = roomType;
|
||||
arrangement.start_date = startDate;
|
||||
@@ -3148,7 +3249,19 @@
|
||||
arrangement.status = status;
|
||||
arrangement.remark = remark;
|
||||
arrangement.side_effect_policy = 'no_external';
|
||||
evidence = { mode: 'update', current_row_count: rowId ? 1 : 0, status, value_redacted: true };
|
||||
delete arrangement.selection;
|
||||
if (arrangement.changes?.resource) {
|
||||
const lookup = form.elements?.namedItem?.(`danwei${slotIndex}`)
|
||||
|| form.querySelector?.(`[name="danwei${slotIndex}"]`);
|
||||
const selected = uniqueLookupCandidate(selectBoxRows(lookup), arrangement.changes.resource, { hotel: true });
|
||||
if (!selected.candidate) blockers.push(`arrangement_resource_candidate_count:${selected.match_count}`);
|
||||
else arrangement.changes = {
|
||||
...arrangement.changes,
|
||||
resource: { ...arrangement.changes.resource, name: selected.candidate.name, id: selected.candidate.id, resolved: true },
|
||||
room_type: selected.candidate.item
|
||||
};
|
||||
}
|
||||
evidence = { mode: 'update', slot_index: slotIndex, current_row_count: rowId ? 1 : 0, status, value_redacted: true };
|
||||
} else {
|
||||
const lookup = form?.elements?.namedItem?.(guide ? 'daoyou0' : 'danwei0')
|
||||
|| form?.querySelector?.(`#${guide ? 'daoyou0' : 'danwei0'}, [name="${guide ? 'daoyou0' : 'danwei0'}"]`);
|
||||
@@ -3166,6 +3279,8 @@
|
||||
const selected = uniqueLookupCandidate(rows, ref, {
|
||||
guide,
|
||||
item: requestedItem,
|
||||
vehicleType: action === 'arrangement_vehicle' ? arrangement.vehicle_type : undefined,
|
||||
ticketDescription: action === 'arrangement_transport' ? arrangement.ticket_description : undefined,
|
||||
hotel,
|
||||
transport,
|
||||
other,
|
||||
@@ -3294,7 +3409,7 @@
|
||||
});
|
||||
}
|
||||
const arrangement = operation.data?.arrangement || {};
|
||||
const lookupRequired = !(operation.action === 'arrangement_hotel' && arrangement.mode === 'update');
|
||||
const lookupRequired = !(operation.action === 'arrangement_hotel' && arrangement.mode === 'update' && !arrangement.changes?.resource);
|
||||
const lookupReadiness = lookupRequired
|
||||
? await waitForCondition(() => {
|
||||
const readiness = arrangementLookupReadiness(formDocument.querySelector('#InfoForm1, form[name="InfoForm1"]'), operation.action, formDocument, operation);
|
||||
@@ -6403,7 +6518,7 @@
|
||||
}
|
||||
|
||||
window.LTJTOrderAssistant = {
|
||||
version: '0.5.173',
|
||||
version: '0.5.177',
|
||||
resolveNativeListSearchValues,
|
||||
lookupKeywordMatchesText,
|
||||
inspectLifecycleSearchCriteria: lifecycleSearchCriteria,
|
||||
@@ -6439,6 +6554,8 @@
|
||||
inspectArrangementLookupReadiness(form, action, scopeDocument = null, operation = {}) {
|
||||
return arrangementLookupReadiness(form, action, scopeDocument, operation);
|
||||
},
|
||||
matchesVehicleType: candidateDescriptionMatches,
|
||||
matchesTicketDescription: candidateDescriptionMatches,
|
||||
inspectArrangementLookupCandidate(rows, ref, options = {}) {
|
||||
return uniqueLookupCandidate(rows, ref, options);
|
||||
},
|
||||
|
||||
@@ -29,11 +29,11 @@
|
||||
'arrangement_other'
|
||||
]);
|
||||
const ARRANGEMENT_UPDATE_KEYS = Object.freeze({
|
||||
arrangement_guide: new Set(),
|
||||
arrangement_vehicle: new Set(),
|
||||
arrangement_hotel: new Set(['end_date', 'room_count']),
|
||||
arrangement_transport: new Set(),
|
||||
arrangement_other: new Set()
|
||||
arrangement_guide: new Set(["resource", "remark"]),
|
||||
arrangement_vehicle: new Set(["supplier", "item", "quantity", "remark"]),
|
||||
arrangement_hotel: new Set(['end_date', 'room_count', 'resource', 'room_type', 'remark']),
|
||||
arrangement_transport: new Set(["supplier", "item", "quantity", "remark"]),
|
||||
arrangement_other: new Set(["supplier", "item", "quantity", "remark", "filing"])
|
||||
});
|
||||
const HOTEL_ARRANGEMENT_STATUSES = new Set(['未安排', '未确认']);
|
||||
const UPDATE_FIELDS = Object.freeze({
|
||||
@@ -331,7 +331,7 @@
|
||||
|
||||
function arrangementFinalState(arrangement = {}) {
|
||||
if (arrangement.mode !== 'update' || !isObject(arrangement.changes)) return arrangement;
|
||||
const finalState = { ...arrangement, ...arrangement.changes };
|
||||
const finalState = { ...arrangement, ...arrangement.changes, ...(arrangement.changes.filing ? { filing: { ...arrangement.filing, ...arrangement.changes.filing } } : {}) };
|
||||
delete finalState.changes;
|
||||
return finalState;
|
||||
}
|
||||
@@ -353,20 +353,34 @@
|
||||
if (ARRANGEMENT_ACTIONS.has(action)) {
|
||||
const arrangement = data.arrangement || {};
|
||||
const finalArrangement = arrangementFinalState(arrangement);
|
||||
if (arrangement.vehicle_type !== undefined && (action !== 'arrangement_vehicle'
|
||||
|| !['create', 'update'].includes(arrangement.mode)
|
||||
|| typeof arrangement.vehicle_type !== 'string' || !arrangement.vehicle_type.trim()
|
||||
|| !assistant.matchesVehicleType?.(finalArrangement.item, arrangement.vehicle_type))) blockers.push('arrangement_vehicle_type_mismatch');
|
||||
if (arrangement.ticket_description !== undefined && (action !== 'arrangement_transport'
|
||||
|| !['create', 'update'].includes(arrangement.mode)
|
||||
|| typeof arrangement.ticket_description !== 'string' || !arrangement.ticket_description.trim()
|
||||
|| !assistant.matchesTicketDescription?.(finalArrangement.item, arrangement.ticket_description))) blockers.push('arrangement_ticket_description_mismatch');
|
||||
if (!['create', 'update', 'clear'].includes(arrangement.mode)) blockers.push('arrangement_mode_must_be_create_update_or_clear');
|
||||
if (arrangement.mode === 'create' && arrangement.target !== undefined) blockers.push('arrangement_create_target_forbidden');
|
||||
if (arrangement.mode === 'update' || arrangement.mode === 'clear') {
|
||||
if (!isObject(arrangement.target) || arrangement.target.slot_index !== 0) blockers.push(`arrangement_${arrangement.mode}_target_slot_must_be_zero`);
|
||||
if (!isObject(arrangement.target) || (action !== 'arrangement_guide' && arrangement.mode === 'update'
|
||||
? !Number.isInteger(arrangement.target.slot_index) || arrangement.target.slot_index < 0
|
||||
: arrangement.target.slot_index !== 0)) blockers.push(`arrangement_${arrangement.mode}_target_slot_invalid`);
|
||||
if (action !== 'arrangement_guide' && (typeof arrangement.target?.row_id !== 'string' || !arrangement.target.row_id.trim())) blockers.push(`arrangement_${arrangement.mode}_row_id_missing`);
|
||||
}
|
||||
if (arrangement.mode === 'update') {
|
||||
const changes = arrangement.changes;
|
||||
const allowed = ARRANGEMENT_UPDATE_KEYS[action] || new Set();
|
||||
if (action !== 'arrangement_hotel') blockers.push('arrangement_update_action_not_live_validated');
|
||||
if (!isObject(changes) || Object.keys(changes).length === 0) blockers.push('arrangement_update_changes_missing');
|
||||
for (const key of Object.keys(changes || {})) {
|
||||
if (!allowed.has(key)) blockers.push(`arrangement_update_field_not_live_validated:${key}`);
|
||||
}
|
||||
if (changes?.room_type !== undefined && !changes.resource) blockers.push('arrangement_update_field_not_live_validated:room_type');
|
||||
if (changes?.resource && (changes.resource.resolved !== true || !nativeRef(changes.resource) || (action === 'arrangement_hotel' && !changes.room_type))) blockers.push('arrangement_update_new_resource_unresolved');
|
||||
if (changes?.supplier && (changes.supplier.resolved !== true || !nativeRef(changes.supplier) || !changes.item)) blockers.push('arrangement_update_new_supplier_unresolved');
|
||||
if (changes?.item !== undefined && !changes.supplier) blockers.push('arrangement_update_item_requires_native_supplier');
|
||||
if (changes?.filing && (!isObject(changes.filing) || Object.keys(changes.filing).some((key) => !['number', 'entry_port', 'exit_port'].includes(key)))) blockers.push('arrangement_update_filing_fields_invalid');
|
||||
if (action === 'arrangement_hotel'
|
||||
? !HOTEL_ARRANGEMENT_STATUSES.has(arrangement.status)
|
||||
: arrangement.status !== '未确认') blockers.push(action === 'arrangement_hotel'
|
||||
@@ -402,7 +416,7 @@
|
||||
if (['arrangement_vehicle', 'arrangement_transport'].includes(action)
|
||||
&& (!Number.isInteger(finalArrangement.quantity) || finalArrangement.quantity <= 0)) blockers.push('arrangement_quantity_must_be_positive');
|
||||
if (action === 'arrangement_other' && finalArrangement.quantity !== undefined
|
||||
&& (!Number.isInteger(finalArrangement.quantity) || finalArrangement.quantity <= 0)) blockers.push('arrangement_quantity_must_be_positive');
|
||||
&& (!Number.isInteger(finalArrangement.quantity) || (finalArrangement.quantity <= 0 && !(arrangement.mode === 'update' && arrangement.changes?.quantity === undefined && finalArrangement.quantity === 0)))) blockers.push('arrangement_quantity_must_be_positive');
|
||||
if (finalArrangement.filing?.date !== undefined
|
||||
&& (!validDate(finalArrangement.filing.date) || (testMode && !finalArrangement.filing.date.startsWith('2026-09-')))) blockers.push(testMode ? 'arrangement_filing_date_outside_2026_09' : 'arrangement_filing_date_invalid');
|
||||
}
|
||||
@@ -552,6 +566,20 @@
|
||||
return { control: null, name: '' };
|
||||
}
|
||||
|
||||
function arrangementFieldName(operation, name) {
|
||||
const arrangement = operation.data?.arrangement;
|
||||
if (!ARRANGEMENT_ACTIONS.has(operation.action) || arrangement?.mode !== 'update') return name;
|
||||
return name.replace(/^([a-zA-Z_]+)0$/, (_, prefix) => `${prefix}${arrangement.target?.slot_index ?? 0}`);
|
||||
}
|
||||
|
||||
function getArrangementControl(form, operation, name) {
|
||||
return getControl(form, arrangementFieldName(operation, name));
|
||||
}
|
||||
|
||||
function firstArrangementControl(form, operation, names) {
|
||||
return firstControl(form, names.map((name) => arrangementFieldName(operation, name)));
|
||||
}
|
||||
|
||||
function findForm(contract, targetDocument = document) {
|
||||
if (!contract?.form) return null;
|
||||
if (contract.form === 'DaoruText') return targetDocument.querySelector('#DaoruText, textarea[name="DaoruText"]');
|
||||
@@ -759,26 +787,26 @@
|
||||
arrangement_other: 'shuoming0'
|
||||
}[action];
|
||||
const quantityMatch = action === 'arrangement_hotel'
|
||||
? { name: 'jianshu0', control: getControl(form, 'jianshu0') }
|
||||
: action === 'arrangement_guide' ? { name: '', control: null } : firstControl(form, ['shuliang0', 'tianshu0']);
|
||||
? { name: 'jianshu0', control: getArrangementControl(form, operation, 'jianshu0') }
|
||||
: action === 'arrangement_guide' ? { name: '', control: null } : firstArrangementControl(form, operation, ['shuliang0', 'tianshu0']);
|
||||
return {
|
||||
slot_index: 0,
|
||||
row_id: action === 'arrangement_guide' ? '' : controlValue(getControl(form, 'id0')),
|
||||
resource_id: controlValue(getControl(form, resourceField)),
|
||||
slot_index: operation.data?.arrangement?.target?.slot_index ?? 0,
|
||||
row_id: action === 'arrangement_guide' ? '' : controlValue(getArrangementControl(form, operation, 'id0')),
|
||||
resource_id: controlValue(getArrangementControl(form, operation, resourceField)),
|
||||
expected_resource_id: nativeRef(ref),
|
||||
item_field: itemField || '',
|
||||
item_value: itemField ? controlValue(getControl(form, itemField)) : '',
|
||||
quantity_field: quantityMatch.name,
|
||||
item_field: itemField ? arrangementFieldName(operation, itemField) : '',
|
||||
item_value: itemField ? controlValue(getArrangementControl(form, operation, itemField)) : '',
|
||||
quantity_field: arrangementFieldName(operation, quantityMatch.name),
|
||||
quantity_value: quantityMatch.control ? controlValue(quantityMatch.control) : '',
|
||||
status_values: controlComparableValues(getControl(form, 'zt0')),
|
||||
marker_present: normalize(controlValue(getControl(form, 'beizhu0'))).includes(TEST_MARKER),
|
||||
payment_clear: action === 'arrangement_guide' || emptySlotValue(controlValue(getControl(form, 'yf0'))),
|
||||
status_values: controlComparableValues(getArrangementControl(form, operation, 'zt0')),
|
||||
marker_present: normalize(controlValue(getArrangementControl(form, operation, 'beizhu0'))).includes(TEST_MARKER),
|
||||
payment_clear: action === 'arrangement_guide' || emptySlotValue(controlValue(getArrangementControl(form, operation, 'yf0'))),
|
||||
audit_clear: action === 'arrangement_guide' || ['sh0', 'shenhe0'].every((name) => {
|
||||
const control = getControl(form, name);
|
||||
const control = getArrangementControl(form, operation, name);
|
||||
return !control || emptySlotValue(controlValue(control));
|
||||
}),
|
||||
realized_clear: ['ap_dy', 'ap_qt'].every((name) => {
|
||||
const control = getControl(form, name);
|
||||
const control = getArrangementControl(form, operation, name);
|
||||
return !control || emptySlotValue(controlValue(control));
|
||||
}),
|
||||
value_redacted: true
|
||||
@@ -1031,6 +1059,16 @@
|
||||
const mode = arrangement.mode;
|
||||
const testMode = isObject(contextFor(operation));
|
||||
const required = action === 'arrangement_guide' ? ['daoyou0', 'daoyouid0', 'daoguan'] : ['id0', 'danwei0', 'danweiid0', 'yf0'];
|
||||
if (action === 'arrangement_hotel' && mode === 'update') required.push('riqi0', 'riqis0', 'fangxing0', 'jianshu0', 'zt0', 'beizhu0', 'sh0');
|
||||
if (mode === 'update' && action !== 'arrangement_hotel') {
|
||||
if (action === 'arrangement_guide') required.push('dianhua0', 'dengji0', 'beizhu0', 'ap_dy');
|
||||
else {
|
||||
required.push('riqi0', 'beizhu0', 'sh0', action === 'arrangement_vehicle' ? 'chehao0' : 'shuoming0');
|
||||
if (action === 'arrangement_vehicle') required.push('riqis0', 'zt0');
|
||||
if (action === 'arrangement_transport') required.push('zt0');
|
||||
if (action === 'arrangement_other') required.push('beianhao', 'beianri', 'rujingkou', 'chujingkou', 'ap_qt');
|
||||
}
|
||||
}
|
||||
const itemField = {
|
||||
arrangement_vehicle: 'chehao0',
|
||||
arrangement_hotel: 'fangxing0',
|
||||
@@ -1041,13 +1079,13 @@
|
||||
? ['daoyou0', 'daoyouid0', 'beizhu0']
|
||||
: ['id0', 'danwei0', 'danweiid0', itemField, 'beizhu0'].filter(Boolean);
|
||||
const financialFields = action === 'arrangement_guide' ? [] : ['yf0', 'sh0', 'shenhe0'];
|
||||
const missingFields = required.filter((name) => !getControl(form, name));
|
||||
const missingFields = required.filter((name) => !getArrangementControl(form, operation, name));
|
||||
const blockingFields = mode === 'create' ? occupiedFields.filter((name) => {
|
||||
const control = getControl(form, name);
|
||||
const control = getArrangementControl(form, operation, name);
|
||||
return control && !emptySlotValue(controlValue(control));
|
||||
}) : [];
|
||||
const financialBlockers = financialFields.filter((name) => {
|
||||
const control = getControl(form, name);
|
||||
const control = getArrangementControl(form, operation, name);
|
||||
return control && !emptySlotValue(controlValue(control));
|
||||
});
|
||||
const filingMismatches = [];
|
||||
@@ -1060,7 +1098,7 @@
|
||||
};
|
||||
for (const [key, name] of Object.entries(filingFields)) {
|
||||
if (arrangement.filing[key] === undefined) continue;
|
||||
const existing = controlValue(getControl(form, name));
|
||||
const existing = controlValue(getArrangementControl(form, operation, name));
|
||||
const valuesMatch = key === 'date'
|
||||
? dateYyyyMD(existing) === dateYyyyMD(arrangement.filing[key])
|
||||
: normalize(existing) === normalize(arrangement.filing[key]);
|
||||
@@ -1074,31 +1112,38 @@
|
||||
if ((mode === 'clear' || mode === 'update') && missingFields.length === 0) {
|
||||
const ref = action === 'arrangement_guide' || action === 'arrangement_hotel' ? arrangement.resource : arrangement.supplier;
|
||||
const resourceField = action === 'arrangement_guide' ? 'daoyouid0' : 'danweiid0';
|
||||
if (controlValue(getControl(form, resourceField)) !== nativeRef(ref)) targetMismatches.push(resourceField);
|
||||
if (action !== 'arrangement_guide' && controlValue(getControl(form, 'id0')) !== String(arrangement.target?.row_id || '')) targetMismatches.push('id0');
|
||||
if (controlValue(getArrangementControl(form, operation, resourceField)) !== nativeRef(ref)) targetMismatches.push(resourceField);
|
||||
if (action !== 'arrangement_guide' && controlValue(getArrangementControl(form, operation, 'id0')) !== String(arrangement.target?.row_id || '')) targetMismatches.push('id0');
|
||||
if (itemField) {
|
||||
const expectedItem = action === 'arrangement_hotel' ? arrangement.room_type : arrangement.item;
|
||||
if (!controlMatches(getControl(form, itemField), expectedItem)) targetMismatches.push(itemField);
|
||||
if (!controlMatches(getArrangementControl(form, operation, itemField), expectedItem)) targetMismatches.push(itemField);
|
||||
}
|
||||
if (action === 'arrangement_hotel') {
|
||||
if (!dateControlMatches(getControl(form, 'riqi0'), arrangement.start_date)) targetMismatches.push('riqi0');
|
||||
if (!dateControlMatches(getControl(form, 'riqis0'), arrangement.end_date)) targetMismatches.push('riqis0');
|
||||
if (!controlMatches(getControl(form, 'jianshu0'), arrangement.room_count)) targetMismatches.push('jianshu0');
|
||||
if (!dateControlMatches(getArrangementControl(form, operation, 'riqi0'), arrangement.start_date)) targetMismatches.push('riqi0');
|
||||
if (!dateControlMatches(getArrangementControl(form, operation, 'riqis0'), arrangement.end_date)) targetMismatches.push('riqis0');
|
||||
if (!controlMatches(getArrangementControl(form, operation, 'jianshu0'), arrangement.room_count)) targetMismatches.push('jianshu0');
|
||||
} else if (action === 'arrangement_vehicle') {
|
||||
if (!dateControlMatches(getControl(form, 'riqi0'), arrangement.start_date)) targetMismatches.push('riqi0');
|
||||
if (!dateControlMatches(getControl(form, 'riqis0'), arrangement.end_date)) targetMismatches.push('riqis0');
|
||||
const quantity = firstControl(form, ['shuliang0', 'tianshu0']);
|
||||
if (!quantity.control || !controlMatches(quantity.control, arrangement.quantity)) targetMismatches.push(quantity.name || 'shuliang0|tianshu0');
|
||||
if (!dateControlMatches(getArrangementControl(form, operation, 'riqi0'), arrangement.start_date)) targetMismatches.push('riqi0');
|
||||
if (!dateControlMatches(getArrangementControl(form, operation, 'riqis0'), arrangement.end_date)) targetMismatches.push('riqis0');
|
||||
const quantity = firstArrangementControl(form, operation, ['shuliang0', 'tianshu0']);
|
||||
if (!quantity.control || !(action === 'arrangement_other' && arrangement.quantity === 0 ? emptySlotValue(controlValue(quantity.control)) : controlMatches(quantity.control, arrangement.quantity))) targetMismatches.push(quantity.name || 'shuliang0|tianshu0');
|
||||
} else if (action !== 'arrangement_guide') {
|
||||
if (!dateControlMatches(getControl(form, 'riqi0'), arrangement.date)) targetMismatches.push('riqi0');
|
||||
const quantity = firstControl(form, ['shuliang0', 'tianshu0']);
|
||||
if (!quantity.control || !controlMatches(quantity.control, arrangement.quantity)) targetMismatches.push(quantity.name || 'shuliang0|tianshu0');
|
||||
if (!dateControlMatches(getArrangementControl(form, operation, 'riqi0'), arrangement.date)) targetMismatches.push('riqi0');
|
||||
const quantity = firstArrangementControl(form, operation, ['shuliang0', 'tianshu0']);
|
||||
if (!quantity.control || !(action === 'arrangement_other' && arrangement.quantity === 0 ? emptySlotValue(controlValue(quantity.control)) : controlMatches(quantity.control, arrangement.quantity))) targetMismatches.push(quantity.name || 'shuliang0|tianshu0');
|
||||
}
|
||||
if (getControl(form, 'zt0') && !controlMatches(getControl(form, 'zt0'), arrangement.status)) targetMismatches.push('zt0');
|
||||
if (normalize(controlValue(getControl(form, 'beizhu0'))) !== normalize(arrangement.remark)
|
||||
if (getArrangementControl(form, operation, 'zt0') && !controlMatches(getArrangementControl(form, operation, 'zt0'), arrangement.status)) targetMismatches.push('zt0');
|
||||
if (normalize(controlValue(getArrangementControl(form, operation, 'beizhu0'))) !== normalize(arrangement.remark)
|
||||
|| (testMode && !normalize(arrangement.remark).includes(TEST_MARKER))) targetMismatches.push('beizhu0');
|
||||
const realizedField = action === 'arrangement_guide' ? 'ap_dy' : action === 'arrangement_other' ? 'ap_qt' : '';
|
||||
if (realizedField && getControl(form, realizedField) && !emptySlotValue(controlValue(getControl(form, realizedField)))) targetMismatches.push(realizedField);
|
||||
if (realizedField && getArrangementControl(form, operation, realizedField) && !emptySlotValue(controlValue(getArrangementControl(form, operation, realizedField)))) targetMismatches.push(realizedField);
|
||||
}
|
||||
if (mode === 'update' && action === 'arrangement_other') {
|
||||
for (const [key, name] of Object.entries({ number: 'beianhao', date: 'beianri', entry_port: 'rujingkou', exit_port: 'chujingkou' })) {
|
||||
const expected = arrangement.filing?.[key] || '';
|
||||
const control = getControl(form, name);
|
||||
if (!(key === 'date' && expected ? dateControlMatches(control, expected) : controlMatchesIncludingEmpty(control, expected))) filingMismatches.push(name);
|
||||
}
|
||||
}
|
||||
const blockers = [
|
||||
...missingFields.map((name) => `arrangement_slot_guard_field_missing:${name}`),
|
||||
@@ -1111,7 +1156,7 @@
|
||||
ready: blockers.length === 0,
|
||||
empty: mode === 'create' && blockers.length === 0,
|
||||
mode,
|
||||
slot_index: 0,
|
||||
slot_index: operation.data?.arrangement?.target?.slot_index ?? 0,
|
||||
target: arrangementSlotSnapshot(form, operation),
|
||||
missing_fields: missingFields,
|
||||
occupied_fields: blockingFields,
|
||||
@@ -1188,20 +1233,20 @@
|
||||
}
|
||||
|
||||
function arrangementResourceProof(form, operation) {
|
||||
const arrangement = operation.data?.arrangement || {};
|
||||
const arrangement = arrangementFinalState(operation.data?.arrangement || {});
|
||||
const guide = operation.action === 'arrangement_guide';
|
||||
const ref = guide || operation.action === 'arrangement_hotel' ? arrangement.resource : arrangement.supplier;
|
||||
const expectedId = nativeRef(ref);
|
||||
const expectedName = normalize(ref?.name);
|
||||
const expectedItem = guide ? '' : normalize(operation.action === 'arrangement_hotel' ? arrangement.room_type : arrangement.item);
|
||||
const lookup = getControl(form, guide ? 'daoyou0' : 'danwei0');
|
||||
const lookup = getArrangementControl(form, operation, guide ? 'daoyou0' : 'danwei0');
|
||||
const raw = String(lookup?.getAttribute?.('data') || '');
|
||||
const rows = raw.split('◇').map((row) => row.trim()).filter(Boolean).map((row) => row.split('◆').map(normalize));
|
||||
const matches = rows.filter((row) => {
|
||||
if (guide) return row[1] === expectedId && row[2] === expectedName;
|
||||
return row[0] === expectedId && row[1] === expectedName && row[2] === expectedItem;
|
||||
});
|
||||
const coordinator = guide ? getControl(form, 'daoguan') : null;
|
||||
const coordinator = guide ? getArrangementControl(form, operation, 'daoguan') : null;
|
||||
const linkedGuide = guide && matches.length === 1 ? matches[0] : [];
|
||||
const expectedCoordinator = guide ? normalize(linkedGuide[5]) : '';
|
||||
const coordinatorRaw = String(coordinator?.getAttribute?.('data') || '');
|
||||
@@ -1214,11 +1259,29 @@
|
||||
if (!lookup) blockers.push('arrangement_resource_lookup_missing');
|
||||
if (!raw) blockers.push('arrangement_resource_candidate_data_missing');
|
||||
if (raw && matches.length !== 1) blockers.push(`arrangement_resource_candidate_count:${matches.length}`);
|
||||
if (operation.action === 'arrangement_vehicle' && arrangement.vehicle_type !== undefined
|
||||
&& !assistant.matchesVehicleType?.(arrangement.item, arrangement.vehicle_type)) blockers.push('arrangement_vehicle_type_mismatch');
|
||||
if (operation.action === 'arrangement_transport' && arrangement.ticket_description !== undefined
|
||||
&& !assistant.matchesTicketDescription?.(arrangement.item, arrangement.ticket_description)) blockers.push('arrangement_ticket_description_mismatch');
|
||||
const filteredVehicle = operation.action === 'arrangement_vehicle' && arrangement.vehicle_type !== undefined;
|
||||
const filteredTransport = operation.action === 'arrangement_transport' && arrangement.ticket_description !== undefined;
|
||||
if (filteredVehicle || filteredTransport) {
|
||||
const filtered = assistant.inspectArrangementLookupCandidate?.(selectBoxRows(lookup), ref, {
|
||||
transport: filteredTransport,
|
||||
vehicleType: filteredVehicle ? arrangement.vehicle_type : undefined,
|
||||
ticketDescription: filteredTransport ? arrangement.ticket_description : undefined,
|
||||
searchColumns: filteredVehicle ? [1, 2, 3] : []
|
||||
});
|
||||
if (!filtered?.candidate || filtered.match_count !== 1
|
||||
|| normalize(filtered.candidate.id) !== expectedId || normalize(filtered.candidate.item) !== expectedItem) {
|
||||
blockers.push(`${operation.action}_filtered_candidate_changed:${filtered?.match_count || 0}`);
|
||||
}
|
||||
}
|
||||
if (guide && !expectedCoordinator) blockers.push('arrangement_guide_coordinator_missing');
|
||||
if (guide && !coordinator) blockers.push('arrangement_guide_coordinator_field_missing');
|
||||
if (guide && !coordinatorRaw) blockers.push('arrangement_guide_coordinator_candidate_data_missing');
|
||||
if (guide && coordinatorRaw && coordinatorMatches.length !== 1) blockers.push(`arrangement_guide_coordinator_candidate_count:${coordinatorMatches.length}`);
|
||||
if (guide && currentCoordinator && currentCoordinator !== expectedCoordinator) blockers.push('arrangement_guide_coordinator_current_value_mismatch');
|
||||
if (guide && arrangement.mode !== 'update' && currentCoordinator && currentCoordinator !== expectedCoordinator) blockers.push('arrangement_guide_coordinator_current_value_mismatch');
|
||||
return {
|
||||
matched: blockers.length === 0,
|
||||
lookup_field: guide ? 'daoyou0' : 'danwei0',
|
||||
@@ -1672,7 +1735,7 @@
|
||||
'id0', 'yf0', 'sh0', 'shenhe0', 'daoyou0', 'daoyouid0', 'dianhua0', 'dengji0', 'daoguan', 'danwei0', 'danweiid0', 'chehao0',
|
||||
'fangxing0', 'shuoming0', 'riqi0', 'riqis0', 'shuliang0', 'tianshu0', 'jianshu0',
|
||||
'zt0', 'beizhu0', 'beianhao', 'beianri', 'rujingkou', 'chujingkou', 'ap_dy', 'ap_qt'
|
||||
];
|
||||
].map((name) => arrangementFieldName(operation, name));
|
||||
}
|
||||
if (operation.action.startsWith('order_update_')) {
|
||||
const fields = Object.values(UPDATE_FIELDS[operation.action] || {});
|
||||
@@ -1949,8 +2012,8 @@
|
||||
.map((row) => row.split('◆').map((cell) => String(cell || '').trim()));
|
||||
}
|
||||
|
||||
function applyNativeHotelSelection(form, resource, changes, blockers) {
|
||||
const control = getControl(form, 'danwei0');
|
||||
function applyNativeHotelSelection(form, resource, changes, blockers, operation = {}) {
|
||||
const control = getArrangementControl(form, operation, 'danwei0');
|
||||
if (!control) {
|
||||
blockers.push('arrangement_hotel_native_selectbox_missing');
|
||||
return null;
|
||||
@@ -2004,7 +2067,7 @@
|
||||
blockers.push('arrangement_hotel_native_selectbox_setval_missing');
|
||||
return null;
|
||||
}
|
||||
const linkedFields = ['danwei0', 'danweiid0', 'fangxing0', 'diqu0', 'dianhua0', 'bizhong0', 'danjia0', 'fangshi0'];
|
||||
const linkedFields = ['danwei0', 'danweiid0', 'fangxing0', 'diqu0', 'dianhua0', 'bizhong0', 'danjia0', 'fangshi0'].map((name) => arrangementFieldName(operation, name));
|
||||
const before = Object.fromEntries(linkedFields.map((name) => [name, controlValue(getControl(form, name))]));
|
||||
try {
|
||||
// SetValToObj is the same ERP-native mapping used by a real hotel-row
|
||||
@@ -2018,11 +2081,11 @@
|
||||
}
|
||||
const after = Object.fromEntries(linkedFields.map((name) => [name, controlValue(getControl(form, name))]));
|
||||
changes.push({
|
||||
field: 'danwei0',
|
||||
field: control.name,
|
||||
target: 'arrangement_hotel_native_selectbox',
|
||||
operation: 'select_first_match',
|
||||
before: before.danwei0,
|
||||
after: after.danwei0,
|
||||
before: before[control.name],
|
||||
after: after[control.name],
|
||||
match_count: selected.match_count,
|
||||
selection: selected.selection,
|
||||
linked_fields_changed: linkedFields.filter((name) => before[name] !== after[name]),
|
||||
@@ -2031,11 +2094,10 @@
|
||||
return selected;
|
||||
}
|
||||
|
||||
function applyNativeSettlementSelection(form, supplier, changes, blockers, action = 'arrangement_transport') {
|
||||
const prefix = action === 'arrangement_other' ? 'arrangement_other' : 'arrangement_transport';
|
||||
// The concrete targets remain arrangement_transport_native_selectbox or
|
||||
// arrangement_other_native_selectbox so result evidence stays action-specific.
|
||||
const control = getControl(form, 'danwei0');
|
||||
function applyNativeSettlementSelection(form, supplier, changes, blockers, action = 'arrangement_transport', operation = {}) {
|
||||
const prefix = action;
|
||||
// Native selection evidence stays action-specific for vehicle, transport and other.
|
||||
const control = getArrangementControl(form, operation, 'danwei0');
|
||||
if (!control) {
|
||||
blockers.push(`${prefix}_native_selectbox_missing`);
|
||||
return null;
|
||||
@@ -2051,13 +2113,23 @@
|
||||
}
|
||||
const lookup = assistant.inspectArrangementLookupCandidate(rows, supplier, {
|
||||
transport: action === 'arrangement_transport',
|
||||
other: action === 'arrangement_other'
|
||||
other: action === 'arrangement_other',
|
||||
vehicleType: action === 'arrangement_vehicle' ? operation.data?.arrangement?.vehicle_type : undefined,
|
||||
ticketDescription: action === 'arrangement_transport' ? operation.data?.arrangement?.ticket_description : undefined,
|
||||
searchColumns: action === 'arrangement_vehicle' ? [1, 2, 3] : []
|
||||
});
|
||||
if (!lookup?.candidate) {
|
||||
blockers.push(`${prefix}_native_candidate_count:${lookup?.match_count || 0}`);
|
||||
return null;
|
||||
}
|
||||
const expectedId = nativeRef(supplier);
|
||||
const explicitlyFiltered = (action === 'arrangement_vehicle' && operation.data?.arrangement?.vehicle_type !== undefined)
|
||||
|| (action === 'arrangement_transport' && operation.data?.arrangement?.ticket_description !== undefined);
|
||||
if (explicitlyFiltered && (lookup.match_count !== 1 || String(lookup.candidate.id || '').trim() !== expectedId
|
||||
|| normalize(lookup.candidate.item) !== normalize(arrangementFinalState(operation.data.arrangement).item))) {
|
||||
blockers.push(`${action}_filtered_candidate_changed`);
|
||||
return null;
|
||||
}
|
||||
let selected = lookup;
|
||||
if (expectedId && String(selected.candidate.id || '').trim() !== expectedId) {
|
||||
// Route preparation resolves the first native row. Reuse its hidden id
|
||||
@@ -2091,7 +2163,7 @@
|
||||
blockers.push(`${prefix}_native_selectbox_setval_missing`);
|
||||
return null;
|
||||
}
|
||||
const linkedFields = ['danwei0', 'danweiid0', 'shuoming0', 'bizhong0', 'danjia0', 'fangshi0'];
|
||||
const linkedFields = ['danwei0', 'danweiid0', action === 'arrangement_vehicle' ? 'chehao0' : 'shuoming0', 'bizhong0', 'danjia0', 'fangshi0'].map((name) => arrangementFieldName(operation, name));
|
||||
const before = Object.fromEntries(linkedFields.map((name) => [name, controlValue(getControl(form, name))]));
|
||||
try {
|
||||
// The same native mapping used by a real settlement-unit row click fills
|
||||
@@ -2105,11 +2177,11 @@
|
||||
}
|
||||
const after = Object.fromEntries(linkedFields.map((name) => [name, controlValue(getControl(form, name))]));
|
||||
changes.push({
|
||||
field: 'danwei0',
|
||||
field: control.name,
|
||||
target: `${prefix}_native_selectbox`,
|
||||
operation: 'select_first_match',
|
||||
before: before.danwei0,
|
||||
after: after.danwei0,
|
||||
before: before[control.name],
|
||||
after: after[control.name],
|
||||
match_count: selected.match_count,
|
||||
selection: selected.selection,
|
||||
linked_fields_changed: linkedFields.filter((name) => before[name] !== after[name]),
|
||||
@@ -2118,8 +2190,8 @@
|
||||
return selected;
|
||||
}
|
||||
|
||||
function applyNativeTransportSelection(form, supplier, changes, blockers) {
|
||||
return applyNativeSettlementSelection(form, supplier, changes, blockers, 'arrangement_transport');
|
||||
function applyNativeTransportSelection(form, supplier, changes, blockers, operation) {
|
||||
return applyNativeSettlementSelection(form, supplier, changes, blockers, 'arrangement_transport', operation);
|
||||
}
|
||||
|
||||
function applyNativeOtherSelection(form, supplier, changes, blockers) {
|
||||
@@ -2227,39 +2299,73 @@
|
||||
return { changes, blockers, arrangementSlot: slot, clearedTarget: slot.target };
|
||||
}
|
||||
|
||||
async function applyGuideResource(form, ref, changes, blockers) {
|
||||
const refId = nativeRef(ref);
|
||||
const lookup = getControl(form, 'daoyou0');
|
||||
const candidateRows = String(lookup?.getAttribute?.('data') || '').split('◇')
|
||||
.map((row) => row.trim()).filter(Boolean).map((row) => row.split('◆').map(normalize));
|
||||
const linkedCandidates = candidateRows.filter((row) => row[1] === refId && row[2] === normalize(ref?.name));
|
||||
if (linkedCandidates.length !== 1) blockers.push(`arrangement_guide_linked_candidate_count:${linkedCandidates.length}`);
|
||||
const linkedCandidate = linkedCandidates[0] || [];
|
||||
const coordinator = normalize(linkedCandidate[5]);
|
||||
if (!coordinator) blockers.push('arrangement_guide_linked_coordinator_missing');
|
||||
if (blockers.length) return;
|
||||
applyExact(form, 'daoyou0', 'set', ref?.name, changes, blockers);
|
||||
applyExact(form, 'daoyouid0', 'set', refId, changes, blockers);
|
||||
applyExact(form, 'dianhua0', 'set', linkedCandidate[3] || '', changes, blockers);
|
||||
applyExact(form, 'dengji0', 'set', linkedCandidate[4] || '', changes, blockers);
|
||||
applyExact(form, 'daoguan', 'set', coordinator, changes, blockers);
|
||||
if (!await waitForStable(form, ['daoyou0', 'daoyouid0', 'dianhua0', 'dengji0', 'daoguan'])) blockers.push('linked_fields_not_stable');
|
||||
requireControlValue(form, 'daoyouid0', refId, blockers);
|
||||
requireControlValue(form, 'daoguan', coordinator, blockers);
|
||||
}
|
||||
|
||||
async function applyArrangementUpdate(form, operation, slot) {
|
||||
const action = operation.action;
|
||||
const arrangement = operation.data?.arrangement || {};
|
||||
const updates = arrangement.changes || {};
|
||||
const changes = [];
|
||||
const blockers = [];
|
||||
const otherRows = Array.from(form.elements || []).filter((control) => {
|
||||
const match = /^([a-zA-Z_]+)(\d+)$/.exec(control.name);
|
||||
return match && Number(match[2]) !== arrangement.target?.slot_index;
|
||||
}).map((control) => ({ control, value: controlValue(control) }));
|
||||
const annotate = (beforeCount, target) => {
|
||||
for (let index = beforeCount; index < changes.length; index += 1) changes[index].target = `arrangement.${target}`;
|
||||
};
|
||||
const applyField = (target, field, value, options = {}) => {
|
||||
const { date = false, ...applyOptions } = options;
|
||||
const control = getControl(form, field);
|
||||
const control = getArrangementControl(form, operation, field);
|
||||
const alreadyCurrent = control && (date
|
||||
? dateControlMatches(control, value)
|
||||
: controlMatches(control, value));
|
||||
if (alreadyCurrent) return;
|
||||
const beforeCount = changes.length;
|
||||
applyExact(form, field, 'set', value, changes, blockers, applyOptions);
|
||||
applyExact(form, arrangementFieldName(operation, field), 'set', value, changes, blockers, applyOptions);
|
||||
annotate(beforeCount, target);
|
||||
};
|
||||
const applyQuantity = (target, names, value) => {
|
||||
const current = firstControl(form, names);
|
||||
const current = firstArrangementControl(form, operation, names);
|
||||
if (current.control && controlMatches(current.control, value)) return;
|
||||
const beforeCount = changes.length;
|
||||
applyFirst(form, names, 'set', value, changes, blockers);
|
||||
applyFirst(form, names.map((name) => arrangementFieldName(operation, name)), 'set', value, changes, blockers);
|
||||
annotate(beforeCount, target);
|
||||
};
|
||||
|
||||
if (Object.hasOwn(updates, 'item')) {
|
||||
if (Object.hasOwn(updates, 'item') && !updates.supplier) {
|
||||
const field = action === 'arrangement_vehicle' ? 'chehao0' : 'shuoming0';
|
||||
applyField('item', field, updates.item);
|
||||
}
|
||||
if (Object.hasOwn(updates, 'room_type')) applyField('room_type', 'fangxing0', updates.room_type);
|
||||
if (updates.resource) {
|
||||
if (action === 'arrangement_guide') await applyGuideResource(form, updates.resource, changes, blockers);
|
||||
else applyNativeHotelSelection(form, updates.resource, changes, blockers, operation);
|
||||
if (blockers.length) return { changes, blockers, arrangementSlot: slot };
|
||||
}
|
||||
if (updates.supplier) {
|
||||
applyNativeSettlementSelection(form, updates.supplier, changes, blockers, action, operation);
|
||||
if (blockers.length) return { changes, blockers, arrangementSlot: slot };
|
||||
}
|
||||
if (Object.hasOwn(updates, 'room_type') && !updates.resource) applyField('room_type', 'fangxing0', updates.room_type);
|
||||
if (Object.hasOwn(updates, 'date')) {
|
||||
applyField('date', 'riqi0', updates.date, { date: true });
|
||||
applyField('date', 'riqis0', updates.date, { date: true, optional: true });
|
||||
@@ -2268,72 +2374,91 @@
|
||||
if (Object.hasOwn(updates, 'end_date')) applyField('end_date', 'riqis0', updates.end_date, { date: true });
|
||||
|
||||
const linkageFields = [
|
||||
...((updates.resource || updates.supplier) ? ['danwei0', 'danweiid0', 'daoyou0', 'daoyouid0', 'daoguan', 'bizhong0', 'danjia0', 'fangshi0'] : []),
|
||||
Object.hasOwn(updates, 'item') ? (action === 'arrangement_vehicle' ? 'chehao0' : 'shuoming0') : '',
|
||||
Object.hasOwn(updates, 'room_type') ? 'fangxing0' : '',
|
||||
Object.hasOwn(updates, 'date') || Object.hasOwn(updates, 'start_date') ? 'riqi0' : '',
|
||||
Object.hasOwn(updates, 'date') || Object.hasOwn(updates, 'end_date') ? 'riqis0' : ''
|
||||
].filter(Boolean);
|
||||
].filter(Boolean).map((name) => arrangementFieldName(operation, name));
|
||||
if (linkageFields.length && !await waitForStable(form, linkageFields)) blockers.push('arrangement_update_linked_fields_not_stable');
|
||||
|
||||
if (Object.hasOwn(updates, 'quantity')) {
|
||||
const names = action === 'arrangement_vehicle' ? ['shuliang0', 'tianshu0'] : ['shuliang0'];
|
||||
applyQuantity('quantity', names, updates.quantity);
|
||||
}
|
||||
if (action === 'arrangement_other' && !Object.hasOwn(updates, 'quantity') && arrangement.quantity !== undefined) {
|
||||
const quantity = firstArrangementControl(form, operation, ['shuliang0']);
|
||||
const unchanged = quantity.control && (arrangement.quantity === 0 ? emptySlotValue(controlValue(quantity.control)) : controlMatches(quantity.control, arrangement.quantity));
|
||||
if (!unchanged) applyQuantity('quantity', ['shuliang0'], arrangement.quantity);
|
||||
}
|
||||
if (Object.hasOwn(updates, 'room_count')) applyField('room_count', 'jianshu0', updates.room_count);
|
||||
if (updates.filing) {
|
||||
for (const [key, name] of Object.entries({ number: 'beianhao', entry_port: 'rujingkou', exit_port: 'chujingkou' })) {
|
||||
if (Object.hasOwn(updates.filing, key)) applyField(`filing.${key}`, name, updates.filing[key]);
|
||||
}
|
||||
}
|
||||
if (Object.hasOwn(updates, 'remark')) applyField('remark', 'beizhu0', updates.remark);
|
||||
else if (!controlMatchesIncludingEmpty(getControl(form, 'beizhu0'), arrangement.remark)) {
|
||||
else if (!controlMatchesIncludingEmpty(getArrangementControl(form, operation, 'beizhu0'), arrangement.remark)) {
|
||||
// Native update_CZR() clears the existing remark when linked date/quantity
|
||||
// controls blur. Restore the frozen value after all linked callbacks so an
|
||||
// unrelated field update cannot erase the business remark.
|
||||
const restored = setControl(getControl(form, 'beizhu0'), 'set', arrangement.remark);
|
||||
if (!restored.ok || !controlMatchesIncludingEmpty(getControl(form, 'beizhu0'), arrangement.remark)) {
|
||||
const restored = setControl(getArrangementControl(form, operation, 'beizhu0'), 'set', arrangement.remark);
|
||||
if (!restored.ok || !controlMatchesIncludingEmpty(getArrangementControl(form, operation, 'beizhu0'), arrangement.remark)) {
|
||||
blockers.push('arrangement_update_remark_restore_failed');
|
||||
}
|
||||
}
|
||||
|
||||
const finalState = arrangementFinalState(arrangement);
|
||||
const assertions = [];
|
||||
if (action === 'arrangement_hotel' && updates.resource) assertions.push(['danwei0', finalState.resource.name, false], ['riqi0', finalState.start_date, true], ['riqis0', finalState.end_date, true]);
|
||||
if (updates.supplier) {
|
||||
assertions.push(['danwei0', finalState.supplier.name, false], ['riqi0', finalState.start_date || finalState.date, true]);
|
||||
if (action === 'arrangement_vehicle') assertions.push(['riqis0', finalState.end_date, true]);
|
||||
}
|
||||
if (Object.hasOwn(updates, 'item')) assertions.push([action === 'arrangement_vehicle' ? 'chehao0' : 'shuoming0', finalState.item, false]);
|
||||
if (Object.hasOwn(updates, 'room_type')) assertions.push(['fangxing0', finalState.room_type, false]);
|
||||
if (Object.hasOwn(updates, 'date')) assertions.push(['riqi0', finalState.date, true]);
|
||||
if (Object.hasOwn(updates, 'start_date')) assertions.push(['riqi0', finalState.start_date, true]);
|
||||
if (Object.hasOwn(updates, 'end_date')) assertions.push(['riqis0', finalState.end_date, true]);
|
||||
if (Object.hasOwn(updates, 'quantity')) {
|
||||
const quantity = firstControl(form, action === 'arrangement_vehicle' ? ['shuliang0', 'tianshu0'] : ['shuliang0']);
|
||||
if (!quantity.control || !controlMatches(quantity.control, finalState.quantity)) blockers.push('arrangement_update_quantity_mismatch');
|
||||
if (finalState.quantity !== undefined) {
|
||||
const quantity = firstArrangementControl(form, operation, action === 'arrangement_vehicle' ? ['shuliang0', 'tianshu0'] : ['shuliang0']);
|
||||
if (!quantity.control || !(action === 'arrangement_other' && finalState.quantity === 0 ? emptySlotValue(controlValue(quantity.control)) : controlMatches(quantity.control, finalState.quantity))) blockers.push('arrangement_update_quantity_mismatch');
|
||||
}
|
||||
if (Object.hasOwn(updates, 'room_count')) assertions.push(['jianshu0', finalState.room_count, false]);
|
||||
if (Object.hasOwn(updates, 'remark')) assertions.push(['beizhu0', finalState.remark, false]);
|
||||
for (const [field, expected, isDate] of assertions) {
|
||||
const control = getControl(form, field);
|
||||
const control = getArrangementControl(form, operation, field);
|
||||
const matched = isDate ? dateControlMatches(control, expected) : controlMatches(control, expected);
|
||||
if (!matched) blockers.push(`arrangement_update_value_mismatch:${field}`);
|
||||
}
|
||||
const ref = action === 'arrangement_guide' || action === 'arrangement_hotel' ? finalState.resource : finalState.supplier;
|
||||
const resourceField = action === 'arrangement_guide' ? 'daoyouid0' : 'danweiid0';
|
||||
if (controlValue(getControl(form, resourceField)) !== nativeRef(ref)) blockers.push('arrangement_update_resource_changed');
|
||||
if (action !== 'arrangement_guide' && controlValue(getControl(form, 'id0')) !== String(arrangement.target?.row_id || '')) blockers.push('arrangement_update_row_id_changed');
|
||||
if (getControl(form, 'zt0')) {
|
||||
if (controlValue(getArrangementControl(form, operation, resourceField)) !== nativeRef(ref)) blockers.push('arrangement_update_resource_changed');
|
||||
if (action !== 'arrangement_guide' && controlValue(getArrangementControl(form, operation, 'id0')) !== String(arrangement.target?.row_id || '')) blockers.push('arrangement_update_row_id_changed');
|
||||
if (getArrangementControl(form, operation, 'zt0')) {
|
||||
const statusMatches = action === 'arrangement_hotel'
|
||||
? [...HOTEL_ARRANGEMENT_STATUSES].some((status) => controlMatches(getControl(form, 'zt0'), status))
|
||||
: controlMatches(getControl(form, 'zt0'), '未确认');
|
||||
? controlMatches(getArrangementControl(form, operation, 'zt0'), arrangement.status)
|
||||
: controlMatches(getArrangementControl(form, operation, 'zt0'), '未确认');
|
||||
if (!statusMatches) blockers.push('arrangement_update_status_changed');
|
||||
}
|
||||
if (!controlMatchesIncludingEmpty(getControl(form, 'beizhu0'), finalState.remark)) blockers.push('arrangement_update_remark_mismatch');
|
||||
if (!controlMatchesIncludingEmpty(getArrangementControl(form, operation, 'beizhu0'), finalState.remark)) blockers.push('arrangement_update_remark_mismatch');
|
||||
for (const name of action === 'arrangement_guide' ? [] : ['yf0', 'sh0', 'shenhe0']) {
|
||||
if (getControl(form, name) && !emptySlotValue(controlValue(getControl(form, name)))) blockers.push(`arrangement_update_financial_or_audit_state:${name}`);
|
||||
if (getArrangementControl(form, operation, name) && !emptySlotValue(controlValue(getArrangementControl(form, operation, name)))) blockers.push(`arrangement_update_financial_or_audit_state:${name}`);
|
||||
}
|
||||
if (action === 'arrangement_other' && isObject(arrangement.filing)) {
|
||||
if (action === 'arrangement_other' && isObject(finalState.filing)) {
|
||||
const filingFields = { number: 'beianhao', date: 'beianri', entry_port: 'rujingkou', exit_port: 'chujingkou' };
|
||||
for (const [key, name] of Object.entries(filingFields)) {
|
||||
if (arrangement.filing[key] === undefined) continue;
|
||||
const actual = controlValue(getControl(form, name));
|
||||
const matched = key === 'date'
|
||||
? dateYyyyMD(actual) === dateYyyyMD(arrangement.filing[key])
|
||||
: normalize(actual) === normalize(arrangement.filing[key]);
|
||||
const expected = finalState.filing[key] || '';
|
||||
const actual = controlValue(getArrangementControl(form, operation, name));
|
||||
const matched = key === 'date' && expected
|
||||
? dateYyyyMD(actual) === dateYyyyMD(expected)
|
||||
: normalize(actual) === normalize(expected);
|
||||
if (!matched) blockers.push(`arrangement_filing_changed_during_update:${name}`);
|
||||
}
|
||||
}
|
||||
const realized = action === 'arrangement_guide' ? 'ap_dy' : action === 'arrangement_other' ? 'ap_qt' : '';
|
||||
if (realized && !emptySlotValue(controlValue(getControl(form, realized)))) blockers.push('arrangement_update_realized_state_changed');
|
||||
if (otherRows.some(({ control, value }) => controlValue(control) !== value)) blockers.push('arrangement_update_other_row_changed');
|
||||
if (!changes.length) blockers.push('arrangement_update_no_effect');
|
||||
return { changes, blockers, arrangementSlot: slot, updatedTarget: slot.target };
|
||||
}
|
||||
@@ -2345,6 +2470,8 @@
|
||||
const action = operation.action;
|
||||
const slot = arrangementSlotEvidence(form, operation);
|
||||
if (!slot.ready) blockers.push(...slot.blockers);
|
||||
if ((action === 'arrangement_vehicle' && value.vehicle_type !== undefined)
|
||||
|| (action === 'arrangement_transport' && value.ticket_description !== undefined)) blockers.push(...arrangementResourceProof(form, operation).blockers);
|
||||
if (blockers.length) return { changes, blockers, arrangementSlot: slot };
|
||||
if (value.mode === 'clear') return await applyArrangementClear(form, operation, slot);
|
||||
if (value.mode === 'update') return await applyArrangementUpdate(form, operation, slot);
|
||||
@@ -2353,29 +2480,14 @@
|
||||
if (!refId) blockers.push('resolved_resource_id_missing');
|
||||
|
||||
if (action === 'arrangement_guide') {
|
||||
const lookup = getControl(form, 'daoyou0');
|
||||
const candidateRows = String(lookup?.getAttribute?.('data') || '').split('◇')
|
||||
.map((row) => row.trim()).filter(Boolean).map((row) => row.split('◆').map(normalize));
|
||||
const linkedCandidates = candidateRows.filter((row) => row[1] === refId && row[2] === normalize(ref?.name));
|
||||
if (linkedCandidates.length !== 1) blockers.push(`arrangement_guide_linked_candidate_count:${linkedCandidates.length}`);
|
||||
const linkedCandidate = linkedCandidates[0] || [];
|
||||
const coordinator = normalize(linkedCandidate[5]);
|
||||
if (!coordinator) blockers.push('arrangement_guide_linked_coordinator_missing');
|
||||
applyExact(form, 'daoyou0', 'set', ref?.name, changes, blockers);
|
||||
applyExact(form, 'daoyouid0', 'set', refId, changes, blockers);
|
||||
applyExact(form, 'dianhua0', 'set', linkedCandidate[3] || '', changes, blockers);
|
||||
applyExact(form, 'dengji0', 'set', linkedCandidate[4] || '', changes, blockers);
|
||||
applyExact(form, 'daoguan', 'set', coordinator, changes, blockers);
|
||||
if (!await waitForStable(form, ['daoyou0', 'daoyouid0', 'dianhua0', 'dengji0', 'daoguan'])) blockers.push('linked_fields_not_stable');
|
||||
requireControlValue(form, 'daoyouid0', refId, blockers);
|
||||
requireControlValue(form, 'daoguan', coordinator, blockers);
|
||||
await applyGuideResource(form, ref, changes, blockers);
|
||||
applyExact(form, 'beizhu0', 'set', value.remark, changes, blockers);
|
||||
applyExact(form, 'ap_dy', 'set', false, changes, blockers, { optional: true });
|
||||
return { changes, blockers, arrangementSlot: slot };
|
||||
}
|
||||
|
||||
if (action === 'arrangement_hotel') applyNativeHotelSelection(form, ref, changes, blockers);
|
||||
else if (action === 'arrangement_transport') applyNativeTransportSelection(form, ref, changes, blockers);
|
||||
else if (action === 'arrangement_transport') applyNativeTransportSelection(form, ref, changes, blockers, operation);
|
||||
else if (action === 'arrangement_other') applyNativeOtherSelection(form, ref, changes, blockers);
|
||||
else {
|
||||
applyExact(form, 'danwei0', 'set', ref?.name, changes, blockers);
|
||||
@@ -3553,12 +3665,13 @@
|
||||
const fields = [];
|
||||
const blockers = [];
|
||||
const add = (name, expected, options = {}) => {
|
||||
name = arrangementFieldName(operation, name);
|
||||
const control = getControl(form, name);
|
||||
if (!control) {
|
||||
if (!options.optional) blockers.push(`arrangement_requery_field_missing:${name}`);
|
||||
return;
|
||||
}
|
||||
fields.push({ name, expected, date: options.date === true, checkbox: options.checkbox === true });
|
||||
fields.push({ name, expected, date: options.date === true, checkbox: options.checkbox === true, numeric: options.numeric === true });
|
||||
};
|
||||
|
||||
if (action === 'arrangement_guide') {
|
||||
@@ -3587,6 +3700,7 @@
|
||||
return { fields, blockers };
|
||||
}
|
||||
|
||||
if (arrangement.mode === 'update') add('id0', arrangement.target?.row_id);
|
||||
const ref = action === 'arrangement_hotel' ? arrangement.resource : arrangement.supplier;
|
||||
add('danwei0', ref?.name);
|
||||
add('danweiid0', nativeRef(ref));
|
||||
@@ -3598,24 +3712,24 @@
|
||||
add('riqis0', arrangement.end_date, { date: true });
|
||||
if (action === 'arrangement_hotel') add('jianshu0', arrangement.room_count);
|
||||
else {
|
||||
const quantity = firstControl(form, ['shuliang0', 'tianshu0']);
|
||||
const quantity = firstArrangementControl(form, operation, ['shuliang0', 'tianshu0']);
|
||||
if (!quantity.control) blockers.push('arrangement_requery_quantity_field_missing');
|
||||
else add(quantity.name, arrangement.quantity);
|
||||
else add(quantity.name, arrangement.quantity, { numeric: action === 'arrangement_other' && arrangement.quantity === 0 });
|
||||
}
|
||||
} else {
|
||||
add('riqi0', arrangement.date, { date: true });
|
||||
const quantity = firstControl(form, ['shuliang0', 'tianshu0']);
|
||||
const quantity = firstArrangementControl(form, operation, ['shuliang0', 'tianshu0']);
|
||||
if (!quantity.control && action !== 'arrangement_other') blockers.push('arrangement_requery_quantity_field_missing');
|
||||
else if (quantity.control && (action !== 'arrangement_other' || arrangement.quantity !== undefined)) add(quantity.name, arrangement.quantity);
|
||||
else if (quantity.control && (action !== 'arrangement_other' || arrangement.quantity !== undefined)) add(quantity.name, arrangement.quantity, { numeric: action === 'arrangement_other' && arrangement.quantity === 0 });
|
||||
}
|
||||
add('zt0', arrangement.status, { optional: action === 'arrangement_other' });
|
||||
add('beizhu0', arrangement.remark);
|
||||
if (action === 'arrangement_other') {
|
||||
const filing = arrangement.filing || {};
|
||||
if (filing.number !== undefined) add('beianhao', filing.number);
|
||||
if (filing.date !== undefined) add('beianri', filing.date, { date: true });
|
||||
if (filing.entry_port !== undefined) add('rujingkou', filing.entry_port);
|
||||
if (filing.exit_port !== undefined) add('chujingkou', filing.exit_port);
|
||||
if (filing.number !== undefined || arrangement.mode === 'update') add('beianhao', filing.number || '');
|
||||
if (filing.date !== undefined || arrangement.mode === 'update') add('beianri', filing.date || '', { date: Boolean(filing.date) });
|
||||
if (filing.entry_port !== undefined || arrangement.mode === 'update') add('rujingkou', filing.entry_port || '');
|
||||
if (filing.exit_port !== undefined || arrangement.mode === 'update') add('chujingkou', filing.exit_port || '');
|
||||
add('ap_qt', false, { checkbox: true, optional: true });
|
||||
}
|
||||
return { fields, blockers };
|
||||
@@ -3632,7 +3746,9 @@
|
||||
const actual = field.checkbox ? Boolean(control?.checked) : controlValue(control);
|
||||
const matched = field.checkbox
|
||||
? actual === Boolean(field.expected)
|
||||
: field.date
|
||||
: field.numeric
|
||||
? Number(actual) === Number(field.expected)
|
||||
: field.date
|
||||
? dateYyyyMD(actual) === dateYyyyMD(field.expected)
|
||||
: normalize(actual) === normalize(field.expected);
|
||||
return { field: field.name, expected: String(field.expected ?? ''), actual: String(actual ?? ''), matched };
|
||||
@@ -3642,7 +3758,10 @@
|
||||
const testMode = isObject(contextFor(operation));
|
||||
const markerPresent = !testMode || page.text.includes(TEST_MARKER);
|
||||
const accountPresent = !testMode || Boolean(context.account && page.text.includes(context.account));
|
||||
const arrangementTarget = form ? arrangementSlotSnapshot(form, operation) : null;
|
||||
const arrangementTarget = form ? arrangementSlotSnapshot(form, {
|
||||
...operation,
|
||||
data: { ...operation.data, arrangement: arrangementFinalState(operation.data?.arrangement || {}) }
|
||||
}) : null;
|
||||
return {
|
||||
matched: page.response.ok && !page.login_timeout && !page.permission_error && Boolean(form)
|
||||
&& identity.matched && markerPresent && projection.blockers.length === 0
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "联泰下单助手",
|
||||
"version": "0.5.173",
|
||||
"version": "0.5.177",
|
||||
"description": "在已登录 LTJT ERP 页面内规划并受控执行联泰 ERP 业务操作。",
|
||||
"permissions": [
|
||||
"activeTab",
|
||||
|
||||
@@ -56,14 +56,14 @@
|
||||
label: '团队安排-导游',
|
||||
route: 'arrangement_guide',
|
||||
execution: 'browser_lifecycle',
|
||||
capabilityState: 'validated_unconfirmed_create_clear_only',
|
||||
capabilityState: 'native_create_clear_program_update_pending_live_acceptance',
|
||||
source: 'native-live-validated-2026-09'
|
||||
},
|
||||
arrangement_vehicle: {
|
||||
label: '团队安排-车辆',
|
||||
route: 'arrangement_vehicle',
|
||||
execution: 'browser_lifecycle',
|
||||
capabilityState: 'validated_unconfirmed_create_clear_only',
|
||||
capabilityState: 'native_create_clear_program_update_pending_live_acceptance',
|
||||
source: 'native-live-validated-2026-09'
|
||||
},
|
||||
arrangement_hotel: {
|
||||
@@ -77,14 +77,14 @@
|
||||
label: '团队安排-大交通',
|
||||
route: 'arrangement_transport',
|
||||
execution: 'browser_lifecycle',
|
||||
capabilityState: 'validated_unconfirmed_create_clear_only',
|
||||
capabilityState: 'native_create_clear_program_update_pending_live_acceptance',
|
||||
source: 'native-live-validated-2026-09'
|
||||
},
|
||||
arrangement_other: {
|
||||
label: '团队安排-其他/备案',
|
||||
route: 'arrangement_other',
|
||||
execution: 'browser_lifecycle',
|
||||
capabilityState: 'validated_unconfirmed_create_clear_preserve_filing_only',
|
||||
capabilityState: 'native_create_clear_program_update_preserve_filing_date_pending_live_acceptance',
|
||||
source: 'native-live-validated-2026-09'
|
||||
},
|
||||
order_update_independent: {
|
||||
@@ -234,11 +234,11 @@
|
||||
...['adult', 'child_bed', 'child_no_bed', 'infant', 'leader'].map((key) => `prices.${key}`)
|
||||
]));
|
||||
const ARRANGEMENT_UPDATE_KEYS = Object.freeze({
|
||||
arrangement_guide: new Set(),
|
||||
arrangement_vehicle: new Set(),
|
||||
arrangement_hotel: new Set(['end_date', 'room_count']),
|
||||
arrangement_transport: new Set(),
|
||||
arrangement_other: new Set()
|
||||
arrangement_guide: new Set(["resource", "remark"]),
|
||||
arrangement_vehicle: new Set(["supplier", "item", "quantity", "remark"]),
|
||||
arrangement_hotel: new Set(['end_date', 'room_count', 'resource', 'room_type', 'remark']),
|
||||
arrangement_transport: new Set(["supplier", "item", "quantity", "remark"]),
|
||||
arrangement_other: new Set(["supplier", "item", "quantity", "remark", "filing"])
|
||||
});
|
||||
const HOTEL_ARRANGEMENT_STATUSES = new Set(['未安排', '未确认']);
|
||||
const PASSENGER_HEADERS = Object.freeze([
|
||||
@@ -856,7 +856,11 @@
|
||||
blockers.push('安排 action 必须包含 data.arrangement 对象。');
|
||||
return;
|
||||
}
|
||||
const allowed = new Set(['mode', 'target', 'changes', 'resource', 'supplier', 'item', 'room_type', 'date', 'start_date', 'end_date', 'quantity', 'room_count', 'status', 'remark', 'side_effect_policy', 'marker', 'filing', 'preserve_filing']);
|
||||
if (value.vehicle_type !== undefined && (action !== 'arrangement_vehicle' || !['create', 'update'].includes(value.mode)
|
||||
|| typeof value.vehicle_type !== 'string' || !value.vehicle_type.trim())) blockers.push('用车类型只允许用于安排用车新增/变更,且必须是非空文本。');
|
||||
if (value.ticket_description !== undefined && (action !== 'arrangement_transport' || !['create', 'update'].includes(value.mode)
|
||||
|| typeof value.ticket_description !== 'string' || !value.ticket_description.trim())) blockers.push('车票说明只允许用于安排大交通新增/变更,且必须是非空文本。');
|
||||
const allowed = new Set(['mode', 'target', 'changes', 'resource', 'supplier', 'item', 'room_type', 'date', 'start_date', 'end_date', 'quantity', 'room_count', 'status', 'remark', 'side_effect_policy', 'marker', 'filing', 'preserve_filing', 'vehicle_type', 'ticket_description']);
|
||||
if (Object.keys(value).some((key) => !allowed.has(key))) blockers.push('data.arrangement 包含未声明字段。');
|
||||
if (!['create', 'update', 'clear'].includes(value.mode)) blockers.push('data.arrangement.mode 必须是 create、update 或 clear。');
|
||||
if (value.mode === 'create' && value.target !== undefined) blockers.push('create 模式必须由 ERP 页面证明空槽位,不能预填 target。');
|
||||
@@ -864,16 +868,26 @@
|
||||
if (!isPlainObject(value.target)) blockers.push(`${value.mode} 模式必须提供 data.arrangement.target。`);
|
||||
else {
|
||||
if (Object.keys(value.target).some((key) => !new Set(['slot_index', 'row_id']).has(key))) blockers.push('data.arrangement.target 只允许 slot_index 和 row_id。');
|
||||
if (value.target.slot_index !== 0) blockers.push(`当前安排 ${value.mode} 只允许精确槽位 0。`);
|
||||
if (action !== 'arrangement_guide' && value.mode === 'update'
|
||||
? !Number.isInteger(value.target.slot_index) || value.target.slot_index < 0
|
||||
: value.target.slot_index !== 0) blockers.push(`当前安排 ${value.mode} 的目标槽位无效。`);
|
||||
if (action !== 'arrangement_guide' && (typeof value.target.row_id !== 'string' || !value.target.row_id.trim())) blockers.push(`${action} ${value.mode} 必须提供原生 row_id。`);
|
||||
}
|
||||
}
|
||||
if (value.mode === 'update') {
|
||||
const changes = value.changes;
|
||||
const updateKeys = ARRANGEMENT_UPDATE_KEYS[action] || new Set();
|
||||
if (action !== 'arrangement_hotel') blockers.push(`${action} 尚未实测开放 update;当前只开放未确认酒店安排的离店日期和房间数。`);
|
||||
if (!isPlainObject(changes) || Object.keys(changes).length === 0) blockers.push('update 模式必须提供非空 data.arrangement.changes。');
|
||||
for (const key of Object.keys(changes || {})) if (!updateKeys.has(key)) blockers.push(`${action} 尚未实测开放安排变更字段:${key}。`);
|
||||
if (changes?.resource !== undefined) {
|
||||
validateResolvedNamedRef(changes.resource, 'data.arrangement.changes.resource', blockers, { required: true });
|
||||
if (action === 'arrangement_hotel' && (typeof changes.room_type !== 'string' || !changes.room_type.trim())) blockers.push('更换酒店必须由 ERP 联动解析新房型。');
|
||||
} else if (changes?.room_type !== undefined) blockers.push(`${action} 尚未实测开放安排变更字段:room_type。`);
|
||||
if (changes?.supplier !== undefined) {
|
||||
validateResolvedNamedRef(changes.supplier, 'arrangement.changes.supplier', blockers, { required: true });
|
||||
if (typeof changes.item !== 'string' || !changes.item.trim()) blockers.push('变更项目必须由 ERP 供应商候选联动带入。');
|
||||
} else if (changes?.item !== undefined) blockers.push('禁止单独修改联动项目。');
|
||||
if (changes?.filing !== undefined && (!isPlainObject(changes.filing) || !Object.keys(changes.filing).length || Object.keys(changes.filing).some((key) => !['number', 'entry_port', 'exit_port'].includes(key)) || Object.values(changes.filing).some((v) => typeof v !== 'string' || !v.trim()))) blockers.push('备案变更字段无效。');
|
||||
if (!arrangementStatusAllowed(action, value.mode, value.status, { testMode })) blockers.push(action === 'arrangement_hotel'
|
||||
? 'update 只能作用于 ERP 当前“未安排”或“未确认”酒店安排。'
|
||||
: 'update 只能作用于 ERP 当前“未确认”安排。');
|
||||
@@ -884,7 +898,7 @@
|
||||
if (action === 'arrangement_other' && !isPlainObject(value.filing)) blockers.push('arrangement_other clear 必须携带待保留备案快照。');
|
||||
}
|
||||
if (value.mode !== 'clear' && value.preserve_filing !== undefined) blockers.push('preserve_filing 只允许用于 arrangement_other clear。');
|
||||
const finalValue = value.mode === 'update' && isPlainObject(value.changes) ? { ...value, ...value.changes } : value;
|
||||
const finalValue = value.mode === 'update' && isPlainObject(value.changes) ? { ...value, ...value.changes, ...(value.changes.filing ? { filing: { ...value.filing, ...value.changes.filing } } : {}) } : value;
|
||||
if (action === 'arrangement_guide' || action === 'arrangement_hotel') {
|
||||
validateResolvedNamedRef(value.resource, 'data.arrangement.resource', blockers, { required: true });
|
||||
} else if (value.resource !== undefined) {
|
||||
@@ -927,7 +941,7 @@
|
||||
if (finalValue[key] !== undefined && (!isValidDate(finalValue[key]) || (testMode && !finalValue[key].startsWith('2026-09-')))) blockers.push(testMode ? `data.arrangement.${key} 必须是 2026 年 9 月有效日期。` : `data.arrangement.${key} 必须是有效的 YYYY-MM-DD 日期。`);
|
||||
}
|
||||
for (const key of ['quantity', 'room_count']) {
|
||||
if (finalValue[key] !== undefined && (!Number.isInteger(finalValue[key]) || finalValue[key] <= 0)) blockers.push(`data.arrangement.${key} 必须是正整数。`);
|
||||
if (finalValue[key] !== undefined && (!Number.isInteger(finalValue[key]) || (finalValue[key] <= 0 && !(key === 'quantity' && action === 'arrangement_other' && value.mode === 'update' && value.changes?.quantity === undefined && finalValue[key] === 0)))) blockers.push(`data.arrangement.${key} 必须是正整数。`);
|
||||
}
|
||||
if (action === 'arrangement_hotel' && isValidDate(finalValue.start_date) && isValidDate(finalValue.end_date)
|
||||
&& Date.parse(`${finalValue.end_date}T00:00:00Z`) <= Date.parse(`${finalValue.start_date}T00:00:00Z`)) blockers.push('酒店安排 end_date 必须晚于 start_date。');
|
||||
@@ -1158,26 +1172,75 @@
|
||||
if (manualReviewTargets.length) warnings.push(`已解析但尚未纳入当前自动写入白名单的字段将转人工复核:${[...new Set(manualReviewTargets)].join('、')}。`);
|
||||
}
|
||||
|
||||
function validateOriginalArrangementUpdate(value, action, errors) {
|
||||
const guide = action === 'arrangement_guide';
|
||||
const vehicle = action === 'arrangement_vehicle';
|
||||
const other = action === 'arrangement_other';
|
||||
const keys = new Set(guide ? ['resource', 'remark'] : ['supplier', 'quantity', 'remark', ...(other ? ['filing'] : [])]);
|
||||
if (Object.keys(value).some((key) => !['mode', 'selection', 'changes', ...(vehicle ? ['vehicle_type'] : []), ...(action === 'arrangement_transport' ? ['ticket_description'] : [])].includes(key))) errors.push('安排变更包含未声明字段。');
|
||||
const changes = value.changes;
|
||||
if (!isPlainObject(changes)) { errors.push('安排变更缺少新的目标值。'); return; }
|
||||
if (Object.keys(changes).some((key) => !keys.has(key))) errors.push('安排变更日期仅用于定位,不能修改日期或直接指定联动项目。');
|
||||
validateDispatchNamedRef(changes[guide ? 'resource' : 'supplier'], 'arrangement.changes.resource_or_supplier', errors, { required: true });
|
||||
if (changes.quantity !== undefined && (!Number.isInteger(changes.quantity) || changes.quantity < 1)) errors.push('安排变更数量必须为正整数。');
|
||||
if ((vehicle || action === 'arrangement_transport') && changes.quantity === undefined) errors.push('安排变更缺少数量。');
|
||||
if (changes.remark !== undefined && (typeof changes.remark !== 'string' || !changes.remark.trim())) errors.push('安排变更备注必须为非空文本。');
|
||||
if (guide) {
|
||||
if (value.selection !== undefined) errors.push('导游变更按团号定位唯一已有导游,不接受日期。');
|
||||
} else {
|
||||
const selection = value.selection;
|
||||
const dateKeys = vehicle ? ['start_date', 'end_date'] : ['date', ...(other ? ['filing_date'] : [])];
|
||||
if (!isPlainObject(selection) || Object.keys(selection).some((key) => !dateKeys.includes(key))) errors.push('安排变更需要原安排日期。');
|
||||
if (vehicle) {
|
||||
if (!isValidDate(selection?.start_date) || !isValidDate(selection?.end_date) || selection.end_date < selection.start_date) errors.push('原用车日期范围无效。');
|
||||
} else if (!isValidDate(selection?.date)) errors.push('原业务日期无效。');
|
||||
if (selection?.filing_date !== undefined && !isValidDate(selection.filing_date)) errors.push('原备案日期无效。');
|
||||
}
|
||||
if (changes.filing !== undefined) {
|
||||
if (!other || !isPlainObject(changes.filing) || !Object.keys(changes.filing).length
|
||||
|| Object.keys(changes.filing).some((key) => !['number', 'entry_port', 'exit_port'].includes(key))
|
||||
|| Object.values(changes.filing).some((v) => typeof v !== 'string' || !v.trim())) errors.push('备案变更只允许明确填写的备案号和入出境口岸;日期保持原值。');
|
||||
}
|
||||
}
|
||||
|
||||
function validateDispatchArrangement(value, action, blockers) {
|
||||
if (!isPlainObject(value)) {
|
||||
blockers.push('data.arrangement 必须是对象。');
|
||||
return;
|
||||
}
|
||||
if (value.vehicle_type !== undefined && (action !== 'arrangement_vehicle' || !['create', 'update'].includes(value.mode)
|
||||
|| typeof value.vehicle_type !== 'string' || !value.vehicle_type.trim())) blockers.push('用车类型只允许用于安排用车新增/变更,且必须是非空文本。');
|
||||
if (value.ticket_description !== undefined && (action !== 'arrangement_transport' || !['create', 'update'].includes(value.mode)
|
||||
|| typeof value.ticket_description !== 'string' || !value.ticket_description.trim())) blockers.push('车票说明只允许用于安排大交通新增/变更,且必须是非空文本。');
|
||||
if (value.mode === 'update' && action !== 'arrangement_hotel') {
|
||||
validateOriginalArrangementUpdate(value, action, blockers);
|
||||
return;
|
||||
}
|
||||
const common = {
|
||||
arrangement_guide: new Set(['mode', 'resource', 'remark']),
|
||||
arrangement_vehicle: new Set(['mode', 'supplier', 'start_date', 'end_date', 'quantity', 'remark']),
|
||||
arrangement_transport: new Set(['mode', 'supplier', 'item', 'quantity', 'date', 'remark']),
|
||||
arrangement_vehicle: new Set(['mode', 'supplier', 'start_date', 'end_date', 'quantity', 'remark', 'vehicle_type']),
|
||||
arrangement_transport: new Set(['mode', 'supplier', 'item', 'quantity', 'date', 'remark', 'ticket_description']),
|
||||
arrangement_other: new Set(['mode', 'supplier', 'item', 'quantity', 'date', 'remark', 'filing'])
|
||||
};
|
||||
const allowed = action === 'arrangement_hotel'
|
||||
? value.mode === 'update' ? new Set(['mode', 'changes']) : new Set(['mode', 'resource', 'room_type', 'start_date', 'end_date', 'room_count', 'remark'])
|
||||
? value.mode === 'update' ? new Set(['mode', 'changes', 'selection']) : new Set(['mode', 'resource', 'room_type', 'start_date', 'end_date', 'room_count', 'remark'])
|
||||
: common[action] || new Set();
|
||||
if (Object.keys(value).some((key) => !allowed.has(key))) blockers.push('data.arrangement 包含当前业务未开放或执行态字段。');
|
||||
if (action === 'arrangement_hotel' && value.mode === 'update') {
|
||||
const changes = value.changes;
|
||||
if (!isPlainObject(changes) || !Object.keys(changes).length || Object.keys(changes).some((key) => !new Set(['end_date', 'room_count']).has(key))) blockers.push('酒店安排 update 只允许非空 changes.end_date/room_count。');
|
||||
if (!isPlainObject(changes) || !Object.keys(changes).length || Object.keys(changes).some((key) => !new Set(['end_date', 'room_count', 'resource', 'remark']).has(key))) blockers.push('酒店安排 update 只允许非空 changes.end_date/room_count/resource。');
|
||||
if (value.selection !== undefined) {
|
||||
const selection = value.selection;
|
||||
if (!isPlainObject(selection) || Object.keys(selection).some((key) => !['start_date', 'end_date'].includes(key))
|
||||
|| !isValidDate(selection.start_date) || !isValidDate(selection.end_date) || selection.end_date <= selection.start_date) blockers.push('酒店变更必须提供有效的原安排日期。');
|
||||
}
|
||||
if (changes?.resource !== undefined) {
|
||||
validateDispatchNamedRef(changes.resource, 'data.arrangement.changes.resource', blockers, { required: true });
|
||||
if (!isPlainObject(value.selection)) blockers.push('更换酒店必须提供原安排的入住和离店日期。');
|
||||
}
|
||||
if (changes?.end_date !== undefined && !isValidDate(changes.end_date)) blockers.push('changes.end_date 必须是有效的 YYYY-MM-DD 日期。');
|
||||
if (changes?.room_count !== undefined && (!Number.isInteger(changes.room_count) || changes.room_count < 1)) blockers.push('changes.room_count 必须是正整数。');
|
||||
if (value.changes?.remark !== undefined && (typeof value.changes.remark !== 'string' || !value.changes.remark.trim())) blockers.push('变更备注必须为非空文本。');
|
||||
return;
|
||||
}
|
||||
if (value.mode !== 'create') blockers.push(`${action} 当前只允许 create${action === 'arrangement_hotel' ? ' 或已开放的 update' : ''}。`);
|
||||
|
||||
@@ -944,7 +944,7 @@
|
||||
|
||||
window.LTJTOrderAssistant = {
|
||||
...(window.LTJTOrderAssistant || {}),
|
||||
version: '0.5.173',
|
||||
version: '0.5.177',
|
||||
openTeamBatchForm,
|
||||
pingTeamBatchFrame,
|
||||
preflightTeamBatchNative,
|
||||
|
||||
Reference in new issue
Block a user