1951 lines
92 KiB
JavaScript
1951 lines
92 KiB
JavaScript
(function installLTJTOrderAssistant() {
|
||
'use strict';
|
||
|
||
function sleep(ms) {
|
||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||
}
|
||
|
||
function toNumber(value, fallback = 0) {
|
||
const num = Number(value);
|
||
return Number.isFinite(num) ? num : fallback;
|
||
}
|
||
|
||
function integerString(value) {
|
||
return String(Math.max(0, Math.trunc(toNumber(value))));
|
||
}
|
||
|
||
function compactNumber(value) {
|
||
const num = toNumber(value);
|
||
if (Number.isInteger(num)) return String(num);
|
||
return String(Math.round((num + Number.EPSILON) * 100) / 100);
|
||
}
|
||
|
||
function dateYyyyMD(value) {
|
||
if (typeof value !== 'string') return value;
|
||
const match = value.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/);
|
||
if (!match) return value;
|
||
return `${match[1]}-${Number(match[2])}-${Number(match[3])}`;
|
||
}
|
||
|
||
function passengerTotal(counts = {}) {
|
||
return ['adult', 'child_bed', 'child_no_bed', 'infant', 'leader']
|
||
.reduce((sum, key) => sum + Math.max(0, Math.trunc(toNumber(counts[key]))), 0);
|
||
}
|
||
|
||
function roomTotal(roomCounts = {}) {
|
||
return ['SGL', 'TWN', 'TRP', 'DBL', 'HNM', 'TL']
|
||
.reduce((sum, key) => sum + Math.max(0, Math.trunc(toNumber(roomCounts[key]))), 0);
|
||
}
|
||
|
||
function splitRows(text) {
|
||
const str = String(text || '');
|
||
if (!str) return [];
|
||
if (str.includes('◇')) return str.split('◇').map((row) => row.trim()).filter(Boolean);
|
||
if (/\r?\n/.test(str)) return str.split(/\r?\n/).map((row) => row.trim()).filter(Boolean);
|
||
return [str.trim()].filter(Boolean);
|
||
}
|
||
|
||
function splitColumns(row) {
|
||
return String(row || '').split('◆').map((part) => part.trim());
|
||
}
|
||
|
||
function rows(text) {
|
||
return splitRows(text).map((row, rowIndex) => ({ rowIndex, columns: splitColumns(row) }));
|
||
}
|
||
|
||
function summarizeText(text) {
|
||
const parsedRows = splitRows(text);
|
||
const histogram = {};
|
||
parsedRows.slice(0, 200).forEach((row) => {
|
||
const count = splitColumns(row).length;
|
||
histogram[count] = (histogram[count] || 0) + 1;
|
||
});
|
||
return {
|
||
byte_length: new Blob([String(text || '')]).size,
|
||
row_count: parsedRows.length,
|
||
column_count_histogram_first_200_rows: histogram,
|
||
value_redacted: true
|
||
};
|
||
}
|
||
|
||
function exactMatches(text, expected, column) {
|
||
const expectedText = String(expected || '').trim();
|
||
if (!expectedText) return [];
|
||
return rows(text).filter((row) => String(row.columns[column] || '').trim() === expectedText);
|
||
}
|
||
|
||
function containsMatches(text, expected, column) {
|
||
const expectedText = String(expected || '').trim();
|
||
if (!expectedText) return [];
|
||
return rows(text).filter((row) => String(row.columns[column] || '').trim().includes(expectedText));
|
||
}
|
||
|
||
function selectUniqueExistingOption(text, expected, column, name) {
|
||
const exact = exactMatches(text, expected, column);
|
||
const contains = containsMatches(text, expected, column);
|
||
if (exact.length === 1) {
|
||
return {
|
||
match: exact[0],
|
||
rule: 'exact',
|
||
exact_count: exact.length,
|
||
contains_count: contains.length,
|
||
blocker: ''
|
||
};
|
||
}
|
||
if (exact.length === 0 && contains.length === 1) {
|
||
return {
|
||
match: contains[0],
|
||
rule: 'unique_contains',
|
||
exact_count: 0,
|
||
contains_count: contains.length,
|
||
blocker: ''
|
||
};
|
||
}
|
||
return {
|
||
match: null,
|
||
rule: exact.length > 1 ? 'ambiguous_exact' : 'blocked',
|
||
exact_count: exact.length,
|
||
contains_count: contains.length,
|
||
blocker: exact.length > 1
|
||
? `${name}: expected exactly one existing LTJT option, found multiple exact matches`
|
||
: `${name}: no exact match and unique contains fallback did not produce exactly one option`
|
||
};
|
||
}
|
||
|
||
async function sha256(text) {
|
||
const buffer = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(text));
|
||
return Array.from(new Uint8Array(buffer)).map((byte) => byte.toString(16).padStart(2, '0')).join('');
|
||
}
|
||
|
||
function sha256Sync(ascii) {
|
||
function rightRotate(value, amount) {
|
||
return (value >>> amount) | (value << (32 - amount));
|
||
}
|
||
const mathPow = Math.pow;
|
||
const maxWord = mathPow(2, 32);
|
||
const lengthProperty = 'length';
|
||
let i;
|
||
let j;
|
||
const result = [];
|
||
const words = [];
|
||
const asciiBitLength = ascii[lengthProperty] * 8;
|
||
let hash = sha256Sync.h = sha256Sync.h || [];
|
||
let k = sha256Sync.k = sha256Sync.k || [];
|
||
let primeCounter = k[lengthProperty];
|
||
const isComposite = {};
|
||
for (let candidate = 2; primeCounter < 64; candidate += 1) {
|
||
if (!isComposite[candidate]) {
|
||
for (i = 0; i < 313; i += candidate) isComposite[i] = candidate;
|
||
hash[primeCounter] = (mathPow(candidate, 0.5) * maxWord) | 0;
|
||
k[primeCounter] = (mathPow(candidate, 1 / 3) * maxWord) | 0;
|
||
primeCounter += 1;
|
||
}
|
||
}
|
||
ascii += String.fromCharCode(0x80);
|
||
while (ascii[lengthProperty] % 64 - 56) ascii += String.fromCharCode(0);
|
||
for (i = 0; i < ascii[lengthProperty]; i += 1) {
|
||
j = ascii.charCodeAt(i);
|
||
if (j >> 8) throw new Error('sha256Sync only supports 8-bit input');
|
||
words[i >> 2] |= j << ((3 - i) % 4) * 8;
|
||
}
|
||
words[words[lengthProperty]] = ((asciiBitLength / maxWord) | 0);
|
||
words[words[lengthProperty]] = asciiBitLength;
|
||
for (j = 0; j < words[lengthProperty];) {
|
||
const w = words.slice(j, j += 16);
|
||
const oldHash = hash;
|
||
hash = hash.slice(0, 8);
|
||
for (i = 0; i < 64; i += 1) {
|
||
const w15 = w[i - 15];
|
||
const w2 = w[i - 2];
|
||
const a = hash[0];
|
||
const e = hash[4];
|
||
const temp1 = hash[7]
|
||
+ (rightRotate(e, 6) ^ rightRotate(e, 11) ^ rightRotate(e, 25))
|
||
+ ((e & hash[5]) ^ ((~e) & hash[6]))
|
||
+ k[i]
|
||
+ (w[i] = (i < 16) ? w[i] : (
|
||
w[i - 16]
|
||
+ (rightRotate(w15, 7) ^ rightRotate(w15, 18) ^ (w15 >>> 3))
|
||
+ w[i - 7]
|
||
+ (rightRotate(w2, 17) ^ rightRotate(w2, 19) ^ (w2 >>> 10))
|
||
) | 0);
|
||
const temp2 = (rightRotate(a, 2) ^ rightRotate(a, 13) ^ rightRotate(a, 22))
|
||
+ ((a & hash[1]) ^ (a & hash[2]) ^ (hash[1] & hash[2]));
|
||
hash = [(temp1 + temp2) | 0].concat(hash);
|
||
hash[4] = (hash[4] + temp1) | 0;
|
||
}
|
||
for (i = 0; i < 8; i += 1) hash[i] = (hash[i] + oldHash[i]) | 0;
|
||
}
|
||
for (i = 0; i < 8; i += 1) {
|
||
for (j = 3; j + 1; j -= 1) {
|
||
const b = (hash[i] >> (j * 8)) & 255;
|
||
result.push((b < 16 ? '0' : '') + b.toString(16));
|
||
}
|
||
}
|
||
return result.join('');
|
||
}
|
||
|
||
function isLoginText(text) {
|
||
return /登陆|登录|login/i.test(String(text || ''));
|
||
}
|
||
|
||
function visibleElement(el) {
|
||
const style = window.getComputedStyle(el);
|
||
return style.display !== 'none' && style.visibility !== 'hidden' && el.getClientRects().length > 0;
|
||
}
|
||
|
||
function loadingRoot(el) {
|
||
let current = el;
|
||
let root = el;
|
||
for (let depth = 0; depth < 5 && current?.parentElement; depth += 1) {
|
||
const parent = current.parentElement;
|
||
const className = String(parent.className || '');
|
||
const position = window.getComputedStyle(parent).position;
|
||
const parentText = String(parent.textContent || '').trim();
|
||
if (
|
||
/dialog|window|messager|loading|mask|aui|layui|panel/i.test(className)
|
||
|| (['absolute', 'fixed'].includes(position) && parentText.length <= 80)
|
||
) {
|
||
root = parent;
|
||
current = parent;
|
||
} else {
|
||
break;
|
||
}
|
||
}
|
||
return root;
|
||
}
|
||
|
||
function closeLingeringLoading(jq) {
|
||
const cleanup = {
|
||
attempted: [],
|
||
closed_by_api: 0,
|
||
removed_elements: 0,
|
||
errors: []
|
||
};
|
||
try {
|
||
if (jq?.messager?.progress) {
|
||
jq.messager.progress('close');
|
||
cleanup.attempted.push('jquery_easyui_messager_progress_close');
|
||
cleanup.closed_by_api += 1;
|
||
}
|
||
} catch (error) {
|
||
cleanup.errors.push(`messager.progress close failed: ${String(error.message || error).slice(0, 120)}`);
|
||
}
|
||
|
||
try {
|
||
if (jq?.dialog?.list && typeof jq.dialog.list === 'object') {
|
||
Object.values(jq.dialog.list).forEach((dialog) => {
|
||
const text = String(dialog?.DOM?.content?.[0]?.textContent || dialog?.content || '').trim();
|
||
if (/loading|加载|处理中|请稍候/i.test(text) && typeof dialog.close === 'function') {
|
||
dialog.close();
|
||
cleanup.closed_by_api += 1;
|
||
}
|
||
});
|
||
cleanup.attempted.push('jquery_dialog_list_close_loading');
|
||
}
|
||
} catch (error) {
|
||
cleanup.errors.push(`dialog list close failed: ${String(error.message || error).slice(0, 120)}`);
|
||
}
|
||
|
||
const selector = [
|
||
'.datagrid-mask',
|
||
'.datagrid-mask-msg',
|
||
'.window-mask',
|
||
'.window-shadow',
|
||
'.messager-window',
|
||
'.layui-layer-loading',
|
||
'.layui-layer-shade',
|
||
'.aui_state_lock',
|
||
'.aui_outer',
|
||
'.aui_dialog'
|
||
].join(',');
|
||
const roots = new Set();
|
||
document.querySelectorAll(selector).forEach((el) => {
|
||
if (visibleElement(el)) roots.add(el);
|
||
});
|
||
document.querySelectorAll('div, span, td').forEach((el) => {
|
||
const text = String(el.textContent || '').trim();
|
||
if (/^loading\.{0,3}$/i.test(text) || /^(加载中|处理中|请稍候)\.{0,3}$/i.test(text)) {
|
||
if (visibleElement(el)) roots.add(loadingRoot(el));
|
||
}
|
||
});
|
||
roots.forEach((el) => {
|
||
try {
|
||
el.remove();
|
||
cleanup.removed_elements += 1;
|
||
} catch (error) {
|
||
cleanup.errors.push(`remove loading element failed: ${String(error.message || error).slice(0, 120)}`);
|
||
}
|
||
});
|
||
if (roots.size) cleanup.attempted.push('remove_visible_loading_nodes');
|
||
return cleanup;
|
||
}
|
||
|
||
function returnToOrderList({ marker = '', dateFrom = '', dateTo = '' } = {}) {
|
||
closeLingeringLoading(window.jQuery || window.$);
|
||
const url = new URL('/System/Business/orders.asp', location.origin);
|
||
const normalizedFrom = dateYyyyMD(dateFrom || '');
|
||
const normalizedTo = dateYyyyMD(dateTo || dateFrom || '');
|
||
if (normalizedFrom) {
|
||
url.searchParams.set('riqi', 'chufari');
|
||
url.searchParams.set('S_chufariqi', normalizedFrom);
|
||
url.searchParams.set('S_chufarizhi', normalizedTo || normalizedFrom);
|
||
}
|
||
if (marker) url.searchParams.set('S_tuanxuhao', marker);
|
||
url.searchParams.set('wode', '0');
|
||
location.href = url.href;
|
||
return {
|
||
status: 'return_to_order_list_requested',
|
||
marker,
|
||
date_range: {
|
||
from: normalizedFrom,
|
||
to: normalizedTo || normalizedFrom
|
||
},
|
||
target_url_redacted: '/System/Business/orders.asp'
|
||
};
|
||
}
|
||
|
||
function operationIdentifier(operation = {}) {
|
||
const data = operation.data || {};
|
||
const refs = data.existing_refs || {};
|
||
return String(
|
||
operation.identifier
|
||
|| refs.identifier
|
||
|| refs.order_no
|
||
|| refs.group_no
|
||
|| refs.child_order_no
|
||
|| refs.parent_group_no
|
||
|| ''
|
||
).trim();
|
||
}
|
||
|
||
function extractGroupNumber(text) {
|
||
return String(text || '').match(/[A-Z]{1,4}-\d{6,8}[A-Z0-9]*(?:-[A-Z0-9][A-Z0-9_-]*)*/i)?.[0] || '';
|
||
}
|
||
|
||
function extractDdid(links = []) {
|
||
for (const link of links) {
|
||
const source = `${link.onclick || ''} ${link.href || ''}`;
|
||
const match = source.match(/OPEN_update\(['"]?([^,'")]+)|[?&]ddid=([^&#]+)/i);
|
||
const value = match?.[1] || match?.[2] || '';
|
||
if (value) return value;
|
||
}
|
||
return '';
|
||
}
|
||
|
||
function extractTid(text, links = []) {
|
||
const source = `${text || ''} ${links.map((link) => `${link.href || ''} ${link.onclick || ''}`).join(' ')}`;
|
||
return source.match(/[?&]tid=(\d+)/i)?.[1]
|
||
|| source.match(/OPEN_finance_list\(\s*[^,]+,\s*["']?(\d+)/i)?.[1]
|
||
|| source.match(/OPEN_List\(\s*["']?[^,"')]+["']?\s*,\s*["']?(\d+)/i)?.[1]
|
||
|| '';
|
||
}
|
||
|
||
function readVisibleOrderRows() {
|
||
return Array.from(document.querySelectorAll('tr')).map((tr) => {
|
||
const text = String(tr.innerText || tr.textContent || '').replace(/\s+/g, ' ').trim();
|
||
const links = Array.from(tr.querySelectorAll('a')).map((link) => ({
|
||
text: String(link.innerText || link.textContent || '').replace(/\s+/g, ' ').trim(),
|
||
href: link.getAttribute('href') || '',
|
||
onclick: link.getAttribute('onclick') || '',
|
||
}));
|
||
const groupNo = extractGroupNumber(text);
|
||
return {
|
||
group_no: groupNo,
|
||
ddid: extractDdid(links),
|
||
tid: extractTid(text, links),
|
||
row_text_length: text.length,
|
||
has_modify_link: links.some((link) => /修改|OPEN_update/i.test(`${link.text} ${link.onclick}`)),
|
||
has_detail_link: links.some((link) => /orders_view|查看|详情/i.test(`${link.text} ${link.href}`)),
|
||
};
|
||
}).filter((row) => row.row_text_length > 0 && (row.group_no || row.has_modify_link || row.has_detail_link));
|
||
}
|
||
|
||
function inspectOrderList(operation = {}, adapter = 'inspect_existing_order_update') {
|
||
const identifier = operationIdentifier(operation);
|
||
const path = location.pathname.toLowerCase();
|
||
if (!/\/system\/business\/orders\.asp$/i.test(path)) {
|
||
return {
|
||
status: 'erp_page_mismatch',
|
||
adapter,
|
||
expected_path: '/System/Business/orders.asp',
|
||
current_path: location.pathname,
|
||
identifier,
|
||
no_erp_write: true,
|
||
blockers: ['请在独立团计划表页面执行订单定位预检。']
|
||
};
|
||
}
|
||
const rows = readVisibleOrderRows();
|
||
const matches = identifier
|
||
? rows.filter((row) => row.group_no === identifier || row.group_no.includes(identifier))
|
||
: [];
|
||
return {
|
||
status: matches.length === 1 ? 'erp_order_candidate_unique' : (matches.length > 1 ? 'erp_order_candidate_ambiguous' : 'erp_order_candidate_not_found'),
|
||
adapter,
|
||
identifier,
|
||
candidate_count: matches.length,
|
||
visible_row_count: rows.length,
|
||
candidates: matches,
|
||
page: { path: location.pathname, title: document.title },
|
||
no_erp_write: true,
|
||
write_attempted: false,
|
||
};
|
||
}
|
||
|
||
function inspectSplitParentPage(operation = {}) {
|
||
const path = location.pathname.toLowerCase();
|
||
const supportedPage = /\/system\/business\/plan(?:_add)?\.asp$/i.test(path);
|
||
const controls = Array.from(document.querySelectorAll('input,select,textarea,button,a'));
|
||
return {
|
||
status: supportedPage ? 'split_parent_page_detected' : 'erp_page_mismatch',
|
||
adapter: 'inspect_split_parent_plan',
|
||
current_path: location.pathname,
|
||
expected_paths: ['/System/Business/plan.asp', '/System/Business/plan_add.asp'],
|
||
control_summary: {
|
||
input_count: controls.filter((el) => el.tagName.toLowerCase() === 'input').length,
|
||
select_count: controls.filter((el) => el.tagName.toLowerCase() === 'select').length,
|
||
textarea_count: controls.filter((el) => el.tagName.toLowerCase() === 'textarea').length,
|
||
add_plan_control_count: controls.filter((el) => /新增计划|新建计划/i.test(String(el.innerText || el.value || ''))).length,
|
||
submit_control_count: controls.filter((el) => /保存|提交|确定/i.test(String(el.innerText || el.value || ''))).length,
|
||
},
|
||
requested_dates: operation.data?.departure_dates || [],
|
||
no_erp_write: true,
|
||
write_attempted: false,
|
||
};
|
||
}
|
||
|
||
function inspectSplitChildPage(operation = {}) {
|
||
const path = location.pathname.toLowerCase();
|
||
const supportedPage = /\/system\/business\/(?:plan|plan_order|orders_add)\.asp$/i.test(path);
|
||
const identifier = operationIdentifier(operation);
|
||
const rows = readVisibleOrderRows();
|
||
const parent = identifier ? rows.filter((row) => row.group_no === identifier || row.group_no.includes(identifier)) : [];
|
||
return {
|
||
status: supportedPage ? 'split_child_page_detected' : 'erp_page_mismatch',
|
||
adapter: 'inspect_split_child_order',
|
||
current_path: location.pathname,
|
||
identifier,
|
||
parent_candidate_count: parent.length,
|
||
parent_candidates: parent,
|
||
no_erp_write: true,
|
||
write_attempted: false,
|
||
};
|
||
}
|
||
|
||
function inspectOperationContext(operation = {}) {
|
||
if (/login/i.test(location.href) || /登录|登陆/i.test(document.title || '')) {
|
||
return { status: 'login_required', no_erp_write: true, blockers: ['login_required'] };
|
||
}
|
||
switch (String(operation.action || '')) {
|
||
case 'order_update':
|
||
return inspectOrderList(operation, 'inspect_existing_order_update');
|
||
case 'passenger_list_import':
|
||
return {
|
||
...inspectOrderList(operation, 'inspect_existing_order_traveler'),
|
||
attachment_count: Array.isArray(operation.data?.attachments) ? operation.data.attachments.length : 0,
|
||
traveler_action_count: Array.isArray(operation.data?.updatePlan?.actions)
|
||
? operation.data.updatePlan.actions.filter((item) => /^travelerList/i.test(String(item?.target || ''))).length
|
||
: 0,
|
||
import_write_attempted: false,
|
||
};
|
||
case 'confirmation_export':
|
||
return inspectOrderList(operation, 'inspect_confirmation_sources');
|
||
case 'shared_plan_create':
|
||
return inspectSplitParentPage(operation);
|
||
case 'shared_child_order_create':
|
||
return inspectSplitChildPage(operation);
|
||
default:
|
||
return {
|
||
status: 'operation_context_not_required',
|
||
current_path: location.pathname,
|
||
action: operation.action || '',
|
||
no_erp_write: true,
|
||
write_attempted: false,
|
||
};
|
||
}
|
||
}
|
||
|
||
function sameOriginDocuments(rootDocument = document, depth = 0, output = []) {
|
||
if (!rootDocument || depth > 6) return output;
|
||
output.push(rootDocument);
|
||
Array.from(rootDocument.querySelectorAll?.('iframe,frame') || []).forEach((frame) => {
|
||
try {
|
||
if (frame.contentDocument) sameOriginDocuments(frame.contentDocument, depth + 1, output);
|
||
} catch (error) {
|
||
// Cross-origin or unloaded frames are intentionally ignored.
|
||
}
|
||
});
|
||
return output;
|
||
}
|
||
|
||
function documentPath(rootDocument = document, pattern) {
|
||
const documents = sameOriginDocuments(rootDocument);
|
||
return documents.find((candidate) => {
|
||
try {
|
||
return pattern.test(candidate.location.pathname);
|
||
} catch (error) {
|
||
return false;
|
||
}
|
||
}) || null;
|
||
}
|
||
|
||
function mainIframe() {
|
||
return document.getElementById('Iframe_Home')
|
||
|| document.querySelector('iframe[name="MainIframe"]');
|
||
}
|
||
|
||
function mainIframeDocument() {
|
||
try {
|
||
return mainIframe()?.contentDocument || null;
|
||
} catch (error) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
async function waitForDocumentPath(pattern, timeoutMs = 20000) {
|
||
const deadline = Date.now() + timeoutMs;
|
||
let last = null;
|
||
while (Date.now() < deadline) {
|
||
last = documentPath(document, pattern);
|
||
if (last?.readyState === 'complete' || last?.querySelector('body')) return last;
|
||
await sleep(250);
|
||
}
|
||
return last;
|
||
}
|
||
|
||
async function waitForCondition(condition, timeoutMs = 20000, intervalMs = 250) {
|
||
const deadline = Date.now() + timeoutMs;
|
||
let last = null;
|
||
while (Date.now() < deadline) {
|
||
last = await condition();
|
||
if (last) return last;
|
||
await sleep(intervalMs);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function setDomField(scopeDocument, idOrName, value, options = {}) {
|
||
const selector = idOrName.startsWith('#') || idOrName.startsWith('[')
|
||
? idOrName
|
||
: `#${idOrName}, [name="${idOrName}"]`;
|
||
const element = scopeDocument.querySelector(selector);
|
||
if (!element) {
|
||
if (options.optional) return false;
|
||
throw new Error(`missing ERP field: ${idOrName}`);
|
||
}
|
||
const next = String(value ?? '');
|
||
element.value = next;
|
||
const ownerWindow = scopeDocument.defaultView || window;
|
||
['input', 'change', 'blur'].forEach((eventName) => {
|
||
element.dispatchEvent(new ownerWindow.Event(eventName, { bubbles: true }));
|
||
});
|
||
return true;
|
||
}
|
||
|
||
function textOf(element) {
|
||
return String(element?.innerText || element?.textContent || '').replace(/\s+/g, ' ').trim();
|
||
}
|
||
|
||
function parseErpResponseText(value) {
|
||
return String(value || '')
|
||
.replace(/<br\s*\/?>/gi, '\n')
|
||
.replace(/<\/(p|div|li|tr)>/gi, '\n')
|
||
.replace(/<[^>]+>/g, '')
|
||
.replace(/ /gi, ' ')
|
||
.replace(/&/gi, '&')
|
||
.replace(/</gi, '<')
|
||
.replace(/>/gi, '>')
|
||
.trim();
|
||
}
|
||
|
||
function extractOrderNumbers(value) {
|
||
return [...new Set(String(value || '').match(/D\d{4,}/gi) || [])];
|
||
}
|
||
|
||
function isSuccessResponse(value) {
|
||
return /操作成功|下单成功|修改成功|成功|success|สำเร็จ/i.test(parseErpResponseText(value));
|
||
}
|
||
|
||
function captureAjaxSubmit(scopeDocument, endpointPattern, submitFunctionName, options = {}) {
|
||
const ownerWindow = scopeDocument.defaultView || window;
|
||
const jq = ownerWindow.jQuery || ownerWindow.$;
|
||
const capture = {
|
||
requests: [],
|
||
alerts: [],
|
||
native_alerts: [],
|
||
ajax_events: [],
|
||
invoked: false,
|
||
completed: false,
|
||
invoke_error: ''
|
||
};
|
||
if (!jq || typeof jq.ajax !== 'function') {
|
||
capture.invoke_error = 'jquery_ajax_not_found';
|
||
return Promise.resolve(capture);
|
||
}
|
||
const originalAjax = jq.ajax;
|
||
const originalDialogAlert = jq.dialog?.alert;
|
||
const originalNativeAlert = ownerWindow.alert;
|
||
const safeFieldNames = options.safeFieldNames || [];
|
||
const restore = () => {
|
||
jq.ajax = originalAjax;
|
||
if (jq.dialog && typeof originalDialogAlert === 'function') jq.dialog.alert = originalDialogAlert;
|
||
ownerWindow.alert = originalNativeAlert;
|
||
};
|
||
if (jq.dialog) {
|
||
jq.dialog.alert = (...args) => capture.alerts.push(parseErpResponseText(args[0] || ''));
|
||
}
|
||
ownerWindow.alert = (...args) => capture.native_alerts.push(parseErpResponseText(args[0] || ''));
|
||
jq.ajax = function ajaxCapture(optionsArg, ...rest) {
|
||
const config = typeof optionsArg === 'string'
|
||
? { ...(rest[0] || {}), url: optionsArg }
|
||
: { ...(optionsArg || {}) };
|
||
const url = String(config.url || '');
|
||
const requestData = String(config.data || '');
|
||
const isTarget = endpointPattern.test(url) && (!options.requestPrefix || requestData.startsWith(options.requestPrefix));
|
||
if (!isTarget) return originalAjax.call(this, optionsArg, ...rest);
|
||
const params = new URLSearchParams(requestData.replace(/^Act=[^&]+&/, ''));
|
||
const eventBase = () => ({
|
||
status: 0,
|
||
text_status: '',
|
||
response_text: '',
|
||
response_bytes: 0,
|
||
error: ''
|
||
});
|
||
capture.requests.push({
|
||
url: url.replace(/^https?:\/\/[^/]+/i, ''),
|
||
method: config.type || config.method || 'GET',
|
||
data_length: new Blob([requestData]).size,
|
||
field_count: [...params.keys()].length,
|
||
fields: Object.fromEntries(safeFieldNames.map((name) => [name, name === 'Act'
|
||
? [requestData.match(/^Act=([^&]+)/)?.[1] || '']
|
||
: params.getAll(name).slice(0, 6)]))
|
||
});
|
||
const originalSuccess = config.success;
|
||
const originalError = config.error;
|
||
const originalComplete = config.complete;
|
||
config.success = function successCapture(data, textStatus, jqXHR) {
|
||
const event = eventBase();
|
||
event.event = 'success';
|
||
event.status = Number(jqXHR?.status || 0);
|
||
event.text_status = String(textStatus || '');
|
||
event.response_text = String(jqXHR?.responseText || data || '').slice(0, 4000);
|
||
event.response_bytes = new Blob([event.response_text]).size;
|
||
capture.ajax_events.push(event);
|
||
capture.completed = true;
|
||
if (typeof originalSuccess === 'function') return originalSuccess.apply(this, arguments);
|
||
return undefined;
|
||
};
|
||
config.error = function errorCapture(jqXHR, textStatus, errorThrown) {
|
||
const event = eventBase();
|
||
event.event = 'error';
|
||
event.status = Number(jqXHR?.status || 0);
|
||
event.text_status = String(textStatus || '');
|
||
event.response_text = String(jqXHR?.responseText || '').slice(0, 4000);
|
||
event.response_bytes = new Blob([event.response_text]).size;
|
||
event.error = String(errorThrown?.message || errorThrown || '');
|
||
capture.ajax_events.push(event);
|
||
capture.completed = true;
|
||
if (typeof originalError === 'function') return originalError.apply(this, arguments);
|
||
return undefined;
|
||
};
|
||
config.complete = function completeCapture(jqXHR, textStatus) {
|
||
if (!capture.ajax_events.length || capture.ajax_events[capture.ajax_events.length - 1].event !== 'success') {
|
||
const event = eventBase();
|
||
event.event = 'complete';
|
||
event.status = Number(jqXHR?.status || 0);
|
||
event.text_status = String(textStatus || '');
|
||
event.response_text = String(jqXHR?.responseText || '').slice(0, 4000);
|
||
event.response_bytes = new Blob([event.response_text]).size;
|
||
capture.ajax_events.push(event);
|
||
}
|
||
capture.completed = true;
|
||
if (typeof originalComplete === 'function') return originalComplete.apply(this, arguments);
|
||
return undefined;
|
||
};
|
||
return originalAjax.call(this, config, ...rest);
|
||
};
|
||
try {
|
||
if (typeof ownerWindow[submitFunctionName] !== 'function') throw new Error(`${submitFunctionName}_not_found`);
|
||
capture.invoked = true;
|
||
ownerWindow[submitFunctionName]();
|
||
} catch (error) {
|
||
capture.invoke_error = String(error.message || error);
|
||
capture.completed = true;
|
||
}
|
||
const waitMs = Number(options.waitMs || 20000);
|
||
return (async () => {
|
||
const deadline = Date.now() + waitMs;
|
||
while (!capture.completed && Date.now() < deadline) await sleep(250);
|
||
if (!capture.completed) capture.timed_out = true;
|
||
restore();
|
||
const responseTexts = [
|
||
...capture.alerts,
|
||
...capture.native_alerts,
|
||
...capture.ajax_events.map((event) => event.response_text)
|
||
].filter(Boolean);
|
||
const joined = responseTexts.join('\n');
|
||
capture.success = isSuccessResponse(joined);
|
||
capture.login_timeout = isLoginText(joined);
|
||
capture.permission_error = /权限|permission/i.test(joined);
|
||
capture.order_numbers = extractOrderNumbers(joined);
|
||
capture.response_preview = parseErpResponseText(joined).slice(0, 1200);
|
||
return capture;
|
||
})();
|
||
}
|
||
|
||
function selectLookupFromInput(input, expected, valueColumn = 0) {
|
||
const expectedText = String(expected || '').trim();
|
||
const candidates = rows(input?.getAttribute('data') || '')
|
||
.filter((row) => String(row.columns[valueColumn] || '').trim() === expectedText);
|
||
if (candidates.length !== 1) {
|
||
return { ok: false, exact_count: candidates.length, expected: expectedText };
|
||
}
|
||
return { ok: true, row: candidates[0], exact_count: candidates.length, expected: expectedText };
|
||
}
|
||
|
||
function selectProductRadio(scopeDocument, data) {
|
||
const productName = String(data.product?.name || '').trim();
|
||
const cpid = String(data.product?.cpid || data.product?.id || '').trim();
|
||
let candidates = Array.from(scopeDocument.querySelectorAll('input[name="cp_id"]')).map((input) => {
|
||
const row = input.closest('tr') || input.parentElement?.parentElement || input.parentElement;
|
||
return { input, text: textOf(row), cpid: String(input.value || '').trim() };
|
||
});
|
||
if (cpid) candidates = candidates.filter((candidate) => candidate.cpid === cpid);
|
||
if (productName) candidates = candidates.filter((candidate) => candidate.text.includes(productName));
|
||
if (candidates.length !== 1) {
|
||
return { ok: false, blocker: `product lookup expected exactly one radio, found ${candidates.length}`, candidates: candidates.map((candidate) => candidate.cpid) };
|
||
}
|
||
candidates[0].input.click();
|
||
return { ok: true, cpid: candidates[0].cpid, label_length: candidates[0].text.length };
|
||
}
|
||
|
||
function fillCustomerLookup(scopeDocument, customer, fieldName = 'zutuanshe') {
|
||
const expected = String(customer?.name || '').trim();
|
||
const input = scopeDocument.querySelector(`#${fieldName}, [name="${fieldName}"]`);
|
||
if (!input) return { ok: false, blocker: `${fieldName}_field_missing` };
|
||
const selected = selectLookupFromInput(input, expected, 2);
|
||
if (!selected.ok) return { ok: false, blocker: `customer lookup expected exactly one option, found ${selected.exact_count}` };
|
||
const columns = selected.row.columns;
|
||
setDomField(scopeDocument, fieldName, columns[2] || expected);
|
||
setDomField(scopeDocument, `${fieldName}id`, columns[1] || customer.id || '', { optional: true });
|
||
setDomField(scopeDocument, `${fieldName}gzr`, columns[4] || '', { optional: true });
|
||
setDomField(scopeDocument, 'lianxiren', columns[3] || customer.contact || '', { optional: true });
|
||
setDomField(scopeDocument, 'bizhong', columns[0] || 'CNY', { optional: true });
|
||
return { ok: true, column_count: columns.length };
|
||
}
|
||
|
||
function fillStaffLookup(scopeDocument, fieldName, expected) {
|
||
const input = scopeDocument.querySelector(`#${fieldName}, [name="${fieldName}"]`);
|
||
if (!input) return { ok: false, blocker: `${fieldName}_field_missing` };
|
||
const selected = selectLookupFromInput(input, expected, 0);
|
||
if (!selected.ok) return { ok: false, blocker: `${fieldName} lookup expected exactly one option, found ${selected.exact_count}` };
|
||
setDomField(scopeDocument, fieldName, selected.row.columns[0] || expected);
|
||
return { ok: true, column_count: selected.row.columns.length };
|
||
}
|
||
|
||
async function ensureMainPage(path, pattern, query = {}) {
|
||
const frame = mainIframe();
|
||
if (!frame) throw new Error('main_iframe_not_found');
|
||
let current = mainIframeDocument();
|
||
if (!current || !pattern.test(current.location.pathname)) {
|
||
const url = new URL(path, location.origin);
|
||
Object.entries(query).forEach(([key, value]) => {
|
||
if (value !== undefined && value !== null && String(value) !== '') url.searchParams.set(key, String(value));
|
||
});
|
||
url.searchParams.set('_', String(Date.now()));
|
||
frame.src = url.href;
|
||
current = await waitForCondition(async () => {
|
||
const candidate = mainIframeDocument();
|
||
return candidate && pattern.test(candidate.location.pathname) ? candidate : null;
|
||
}, 30000, 300);
|
||
}
|
||
if (!current) throw new Error(`main_page_not_ready:${path}`);
|
||
return current;
|
||
}
|
||
|
||
async function searchMainPage(scopeDocument, values = {}) {
|
||
const setIfPresent = (name, value) => {
|
||
const element = scopeDocument.querySelector(`#${name}, [name="${name}"]`);
|
||
if (!element) return false;
|
||
setDomField(scopeDocument, name, value);
|
||
return true;
|
||
};
|
||
Object.entries(values).forEach(([name, value]) => setIfPresent(name, value));
|
||
const button = scopeDocument.querySelector('#SearchButton, [name="SearchButton"]');
|
||
if (button) button.click();
|
||
await sleep(1000);
|
||
return scopeDocument;
|
||
}
|
||
|
||
function rowForIdentifier(scopeDocument, identifier) {
|
||
const expected = String(identifier || '').trim();
|
||
return Array.from(scopeDocument.querySelectorAll('tr')).find((row) => {
|
||
const text = textOf(row);
|
||
return expected && (text.includes(expected) || extractGroupNumber(text) === expected);
|
||
}) || null;
|
||
}
|
||
|
||
function closeDialogDocument(scopeDocument) {
|
||
try {
|
||
if (scopeDocument?.defaultView?.frameElement?.api?.close) {
|
||
scopeDocument.defaultView.frameElement.api.close();
|
||
return true;
|
||
}
|
||
} catch (error) {
|
||
return false;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
async function createSplitParentLive(operation = {}) {
|
||
const data = operation.data || {};
|
||
const dates = Array.isArray(data.departure_dates) ? data.departure_dates : [];
|
||
const blockers = [];
|
||
if (dates.length !== 1) blockers.push('split parent live adapter currently requires exactly one departure date');
|
||
if (Number(data.groups_per_date || data.plans_per_date || 1) !== 1) blockers.push('split parent live adapter currently requires one plan per date');
|
||
if (!data.product?.name) blockers.push('split parent product is missing');
|
||
if (!data.customer?.name) blockers.push('split parent customer is missing');
|
||
if (blockers.length) return { status: 'split_parent_live_blocked', blockers, no_erp_write: true };
|
||
let listDocument = await ensureMainPage('/System/Business/plan.asp', /\/system\/business\/plan\.asp$/i, {
|
||
S_chufariqi: dateYyyyMD(dates[0]),
|
||
S_chufarizhi: dateYyyyMD(dates[0])
|
||
});
|
||
const addButton = listDocument.querySelector('#NewBuildB, [name="NewBuildB"]');
|
||
if (!addButton) return { status: 'split_parent_live_blocked', blockers: ['split parent add button not found'], no_erp_write: true };
|
||
addButton.click();
|
||
const formDocument = await waitForDocumentPath(/\/system\/business\/plan_add\.asp$/i, 30000);
|
||
if (!formDocument) return { status: 'split_parent_live_blocked', blockers: ['split parent form did not load'], no_erp_write: true };
|
||
await waitForCondition(() => {
|
||
const productInputs = formDocument.querySelectorAll('input[name="cp_id"]');
|
||
const customerInput = formDocument.querySelector('#zutuanshe, [name="zutuanshe"]');
|
||
return productInputs.length > 0 && String(customerInput?.getAttribute('data') || '').length > 0 ? formDocument : null;
|
||
}, 15000, 300);
|
||
const form = formDocument.querySelector('#InfoForm1, form');
|
||
if (!form) return { status: 'split_parent_live_blocked', blockers: ['split parent form missing'], no_erp_write: true };
|
||
const lookupChecks = [];
|
||
setDomField(formDocument, 'Riqi1', dateYyyyMD(dates[0]));
|
||
setDomField(formDocument, 'Riqi2', dateYyyyMD(dates[0]));
|
||
setDomField(formDocument, 'jihuashu', data.planned_capacity || 0);
|
||
const customer = fillCustomerLookup(formDocument, data.customer);
|
||
lookupChecks.push({ name: 'customer', ok: customer.ok, exact_match_count: customer.ok ? 1 : 0 });
|
||
const op = fillStaffLookup(formDocument, 'jiedairen', data.op_user?.name);
|
||
const sales = fillStaffLookup(formDocument, 'xiaoshouren', data.sales_user?.name);
|
||
lookupChecks.push({ name: 'op_user', ok: op.ok, exact_match_count: op.ok ? 1 : 0 });
|
||
lookupChecks.push({ name: 'sales_user', ok: sales.ok, exact_match_count: sales.ok ? 1 : 0 });
|
||
const product = selectProductRadio(formDocument, data);
|
||
if (!customer.ok || !op.ok || !sales.ok || !product.ok) {
|
||
closeDialogDocument(formDocument);
|
||
return { status: 'split_parent_live_blocked', blockers: [customer.blocker, op.blocker, sales.blocker, product.blocker].filter(Boolean), lookup_checks: lookupChecks, no_erp_write: true };
|
||
}
|
||
if (typeof formDocument.defaultView.ShowFoucsBox === 'function') formDocument.defaultView.ShowFoucsBox();
|
||
await sleep(100);
|
||
const dateBoxes = Array.from(formDocument.querySelectorAll('input[name="zhidingzhouqi"]'));
|
||
dateBoxes.forEach((input, index) => { input.checked = index === 0; });
|
||
setDomField(formDocument, 'tuanxuhao1', formDocument.querySelector('#tuanxuhao1')?.value || 'LW', { optional: true });
|
||
setDomField(formDocument, 'tuanxuhao2', data.test_marker || 'CDP-SPLIT-PARENT-TEST');
|
||
const capture = await captureAjaxSubmit(formDocument, /\/System\/DAT\/plan\.asp/i, 'SubmitInfoForm', {
|
||
requestPrefix: 'Act=DoInfoSPs&',
|
||
safeFieldNames: ['Act', 'Riqi1', 'Riqi2', 'jihuashu', 'tuanxuhao1', 'tuanxuhao2', 'zutuanshe', 'zutuansheid', 'jiedairen', 'xiaoshouren', 'cp_id', 'zhidingzhouqi'],
|
||
waitMs: 15000
|
||
});
|
||
const groupNumber = capture.order_numbers.find((value) => /[A-Z]{1,4}-\d{6,}/i.test(value)) || capture.response_preview.match(/[A-Z]{1,4}-\d{6,}[A-Z0-9_-]*/i)?.[0] || '';
|
||
closeDialogDocument(formDocument);
|
||
listDocument = mainIframeDocument() || listDocument;
|
||
if (groupNumber) {
|
||
await searchMainPage(listDocument, { S_chufariqi: dateYyyyMD(dates[0]), S_chufarizhi: dateYyyyMD(dates[0]), S_tuanxuhao: data.test_marker || groupNumber });
|
||
}
|
||
const row = groupNumber ? rowForIdentifier(listDocument, groupNumber) : null;
|
||
const verified = Boolean(groupNumber && row && textOf(row).includes(groupNumber));
|
||
return {
|
||
status: verified && capture.success ? 'split_parent_completed' : (capture.timed_out ? 'split_parent_live_uncertain' : 'split_parent_live_failed'),
|
||
no_erp_write: false,
|
||
write_attempted: true,
|
||
lookup_checks: lookupChecks,
|
||
submit: capture,
|
||
erp_receipt: groupNumber ? { group_number: groupNumber, departure_date: dateYyyyMD(dates[0]), planned_capacity: Number(data.planned_capacity || 0) } : undefined,
|
||
verification: { status: verified ? 'parent_group_found' : 'parent_group_not_found', row_text_length: row ? textOf(row).length : 0 },
|
||
blockers: verified && capture.success ? [] : ['split parent save was not confirmed by success response and list requery']
|
||
};
|
||
}
|
||
|
||
async function createSplitChildLive(operation = {}) {
|
||
const data = operation.data || {};
|
||
const parentGroup = String(data.parent_group_no || data.existing_refs?.parent_group_no || '').trim();
|
||
const dates = Array.isArray(data.departure_dates) ? data.departure_dates : [];
|
||
const blockers = [];
|
||
if (!parentGroup) blockers.push('split child parent_group_no is missing');
|
||
if (dates.length !== 1) blockers.push('split child live adapter currently requires exactly one departure date');
|
||
if (!data.customer?.name) blockers.push('split child customer is missing');
|
||
if (passengerTotal(data.passenger_counts || {}) <= 0) blockers.push('split child passenger total must be greater than zero');
|
||
if (blockers.length) return { status: 'split_child_live_blocked', blockers, no_erp_write: true };
|
||
let listDocument = await ensureMainPage('/System/Business/plan.asp', /\/system\/business\/plan\.asp$/i, {
|
||
S_chufariqi: dateYyyyMD(dates[0]),
|
||
S_chufarizhi: dateYyyyMD(dates[0]),
|
||
S_tuanxuhao: parentGroup
|
||
});
|
||
await searchMainPage(listDocument, { S_chufariqi: dateYyyyMD(dates[0]), S_chufarizhi: dateYyyyMD(dates[0]), S_tuanxuhao: parentGroup });
|
||
const parentRow = rowForIdentifier(listDocument, parentGroup);
|
||
if (!parentRow) return { status: 'split_child_live_blocked', blockers: ['parent group not found on split plan list'], no_erp_write: true };
|
||
const childButton = Array.from(parentRow.querySelectorAll('a')).find((link) => /OPEN_update\([^,]+,\s*['"]?0/i.test(link.getAttribute('onclick') || ''));
|
||
if (!childButton) return { status: 'split_child_live_blocked', blockers: ['split child entry link not found on parent row'], no_erp_write: true };
|
||
const parentTid = extractTid(textOf(parentRow), Array.from(parentRow.querySelectorAll('a')).map((link) => ({ text: textOf(link), href: link.getAttribute('href') || '', onclick: link.getAttribute('onclick') || '' })));
|
||
childButton.click();
|
||
const formDocument = await waitForDocumentPath(/\/system\/business\/plan_order\.asp$/i, 30000);
|
||
if (!formDocument) return { status: 'split_child_live_blocked', blockers: ['split child form did not load'], no_erp_write: true };
|
||
await waitForCondition(() => {
|
||
const lookupInputs = ['zutuanshe', 'gendanren', 'xiaoshouren']
|
||
.map((name) => formDocument.querySelector(`#${name}, [name="${name}"]`));
|
||
return lookupInputs.every((input) => String(input?.getAttribute('data') || '').length > 0) ? formDocument : null;
|
||
}, 15000, 300);
|
||
const lookupChecks = [];
|
||
const customer = fillCustomerLookup(formDocument, data.customer);
|
||
const op = fillStaffLookup(formDocument, 'gendanren', data.op_user?.name);
|
||
const sales = fillStaffLookup(formDocument, 'xiaoshouren', data.sales_user?.name);
|
||
lookupChecks.push({ name: 'customer', ok: customer.ok, exact_match_count: customer.ok ? 1 : 0 });
|
||
lookupChecks.push({ name: 'op_user', ok: op.ok, exact_match_count: op.ok ? 1 : 0 });
|
||
lookupChecks.push({ name: 'sales_user', ok: sales.ok, exact_match_count: sales.ok ? 1 : 0 });
|
||
if (!customer.ok || !op.ok || !sales.ok) {
|
||
closeDialogDocument(formDocument);
|
||
return { status: 'split_child_live_blocked', blockers: [customer.blocker, op.blocker, sales.blocker].filter(Boolean), lookup_checks: lookupChecks, no_erp_write: true };
|
||
}
|
||
const counts = data.passenger_counts || {};
|
||
[['darenshu', counts.adult], ['xiaorenshu', counts.child_bed], ['ertrenshu', counts.child_no_bed], ['yingrenshu', counts.infant], ['quanrenshu', counts.leader]].forEach(([name, value]) => setDomField(formDocument, name, integerString(value)));
|
||
if (typeof formDocument.defaultView.GetVisitorsHtml === 'function') formDocument.defaultView.GetVisitorsHtml();
|
||
const prices = data.prices || {};
|
||
const priceRows = [
|
||
['adult', 0, counts.adult],
|
||
['child_bed', 1, counts.child_bed],
|
||
['child_no_bed', 2, counts.child_no_bed],
|
||
['infant', 3, counts.infant],
|
||
['leader', 5, counts.leader]
|
||
];
|
||
priceRows.forEach(([key, index, quantity]) => {
|
||
setDomField(formDocument, `ys_danwei${index}`, data.customer.name, { optional: true });
|
||
setDomField(formDocument, `ys_danweiid${index}`, data.customer.id || '', { optional: true });
|
||
setDomField(formDocument, `ys_shuliang${index}`, integerString(quantity), { optional: true });
|
||
setDomField(formDocument, `ys_danjia${index}`, compactNumber(prices[key] || 0), { optional: true });
|
||
setDomField(formDocument, `ys_jine${index}`, compactNumber(toNumber(quantity) * toNumber(prices[key] || 0)), { optional: true });
|
||
});
|
||
if (typeof formDocument.defaultView.sum_jiesuan === 'function') formDocument.defaultView.sum_jiesuan('ys');
|
||
setDomField(formDocument, 'xiadanbeizhu', data.test_marker || data.special_requests || 'CDP-SPLIT-CHILD-TEST', { optional: true });
|
||
const capture = await captureAjaxSubmit(formDocument, /\/System\/DAT\/plan\.asp/i, 'SubmitInfoForm', {
|
||
requestPrefix: 'Act=DoInfo_order&',
|
||
safeFieldNames: ['Act', 'tdid', 'ddid', 'zutuanshe', 'zutuansheid', 'gendanren', 'xiaoshouren', 'darenshu', 'xiaorenshu', 'ertrenshu', 'yingrenshu', 'quanrenshu', 'xiadanbeizhu'],
|
||
waitMs: 20000
|
||
});
|
||
const childOrder = capture.order_numbers.find((value) => /^D\d{4,}$/i.test(value)) || '';
|
||
closeDialogDocument(formDocument);
|
||
listDocument = mainIframeDocument() || listDocument;
|
||
if (parentTid && typeof listDocument.defaultView?.AjaxLoadDataOne === 'function') listDocument.defaultView.AjaxLoadDataOne(parentTid);
|
||
await sleep(1000);
|
||
const refreshedParentRow = rowForIdentifier(listDocument, parentGroup);
|
||
const verified = Boolean(childOrder && refreshedParentRow && textOf(refreshedParentRow).includes(childOrder));
|
||
return {
|
||
status: verified && capture.success ? 'split_child_completed' : (capture.timed_out ? 'split_child_live_uncertain' : 'split_child_live_failed'),
|
||
no_erp_write: false,
|
||
write_attempted: true,
|
||
lookup_checks: lookupChecks,
|
||
submit: capture,
|
||
erp_receipt: childOrder ? { order_number: childOrder, parent_group_no: parentGroup } : undefined,
|
||
verification: { status: verified ? 'child_order_found_on_parent' : 'child_order_not_found_on_parent', row_text_length: refreshedParentRow ? textOf(refreshedParentRow).length : 0 },
|
||
blockers: verified && capture.success ? [] : ['split child save was not confirmed by success response and parent requery']
|
||
};
|
||
}
|
||
|
||
function normalizeExportType(value) {
|
||
const key = String(value || '').trim().toLowerCase();
|
||
return ({
|
||
confirmation: 'xingyou-confirm',
|
||
confirm: 'xingyou-confirm',
|
||
xingyou: 'xingyou-confirm',
|
||
'xingyou-confirm': 'xingyou-confirm',
|
||
liantai: 'liantai-confirm',
|
||
'liantai-confirm': 'liantai-confirm',
|
||
job: 'job-order',
|
||
beian: 'job-order',
|
||
'job-order': 'job-order'
|
||
})[key] || key;
|
||
}
|
||
|
||
async function exportConfirmationSources(operation = {}) {
|
||
const data = operation.data || {};
|
||
const identifier = operationIdentifier(operation);
|
||
const dates = Array.isArray(data.departure_dates) ? data.departure_dates : [];
|
||
const listDocument = await ensureMainPage('/System/Business/orders.asp', /\/system\/business\/orders\.asp$/i, {
|
||
S_chufariqi: dateYyyyMD(dates[0] || ''),
|
||
S_chufarizhi: dateYyyyMD(dates[0] || ''),
|
||
S_tuanxuhao: identifier
|
||
});
|
||
await searchMainPage(listDocument, {
|
||
S_chufariqi: dateYyyyMD(dates[0] || ''),
|
||
S_chufarizhi: dateYyyyMD(dates[0] || ''),
|
||
S_tuanxuhao: identifier
|
||
});
|
||
const row = rowForIdentifier(listDocument, identifier);
|
||
if (!row) return { status: 'export_source_blocked', blockers: ['order row not found for export'], no_erp_write: true };
|
||
const links = Array.from(row.querySelectorAll('a')).map((link) => ({ text: textOf(link), href: link.getAttribute('href') || '', onclick: link.getAttribute('onclick') || '' }));
|
||
const ddid = extractDdid(links) || data.existing_refs?.ddid || data.existing_refs?.ltjt_ddid || '';
|
||
const tid = extractTid(textOf(row), links) || data.existing_refs?.tid || data.existing_refs?.ltjt_tdid || '';
|
||
if (!ddid) return { status: 'export_source_blocked', blockers: ['order row did not expose ddid'], no_erp_write: true };
|
||
const requested = (Array.isArray(operation.exportTypes) && operation.exportTypes.length
|
||
? operation.exportTypes
|
||
: (Array.isArray(data.exportTypes) && data.exportTypes.length ? data.exportTypes : ['xingyou-confirm']))
|
||
.map(normalizeExportType);
|
||
const specs = requested.map((type) => {
|
||
if (type === 'xingyou-confirm') return { type, path: `/System/Business/orders_confirm_news.asp?did=${encodeURIComponent(ddid)}&tid=${encodeURIComponent(tid)}` };
|
||
if (type === 'liantai-confirm') return { type, path: `/System/Business/orders_confirm_new.asp?did=${encodeURIComponent(ddid)}&tid=${encodeURIComponent(tid)}` };
|
||
if (type === 'job-order') return { type, path: `/System/Business/teams_beian1.asp?did=${encodeURIComponent(ddid)}` };
|
||
return { type, path: '' };
|
||
});
|
||
const artifacts = [];
|
||
for (const spec of specs) {
|
||
if (!spec.path) {
|
||
artifacts.push({ type: spec.type, status: 'unsupported_export_type' });
|
||
continue;
|
||
}
|
||
const response = await fetch(new URL(spec.path, location.origin).href, { credentials: 'same-origin' });
|
||
const buffer = await response.arrayBuffer();
|
||
const preview = new TextDecoder('utf-8', { fatal: false }).decode(buffer.slice(0, 1000));
|
||
artifacts.push({
|
||
type: spec.type,
|
||
path: spec.path,
|
||
http_status: response.status,
|
||
ok: response.ok,
|
||
content_type: response.headers.get('content-type') || '',
|
||
content_disposition: response.headers.get('content-disposition') || '',
|
||
bytes: buffer.byteLength,
|
||
login_timeout: isLoginText(preview),
|
||
permission_error: /权限|permission/i.test(preview),
|
||
word_like: /application\/vnd\.ms-word|Word\.Document|schemas-microsoft-com/i.test(`${response.headers.get('content-type') || ''} ${preview}`)
|
||
});
|
||
}
|
||
const failed = artifacts.filter((artifact) => !artifact.ok || artifact.login_timeout || artifact.permission_error || artifact.status === 'unsupported_export_type');
|
||
return {
|
||
status: failed.length ? 'export_source_failed' : 'export_source_completed',
|
||
no_erp_write: true,
|
||
write_attempted: false,
|
||
identifier,
|
||
source_record: { ddid, tid, group_no: extractGroupNumber(textOf(row)) },
|
||
artifacts,
|
||
export_only: true,
|
||
never_resave: true,
|
||
blockers: failed.map((artifact) => `${artifact.type}: export source response was not valid`)
|
||
};
|
||
}
|
||
|
||
function getOrderForm() {
|
||
const form = document.querySelector('#ListForm');
|
||
if (!form || !String(location.href).includes('/orders_add.asp')) return null;
|
||
return form;
|
||
}
|
||
|
||
function getFrameVisibility() {
|
||
try {
|
||
if (!window.frameElement) return true;
|
||
return Boolean(window.frameElement.offsetParent || window.frameElement.getClientRects().length);
|
||
} catch (error) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
function getValue(form, name) {
|
||
const el = form.elements[name];
|
||
if (!el) return '';
|
||
if (el.length && !el.tagName) return Array.from(el).find((item) => item.checked)?.value || el[0]?.value || '';
|
||
return el.value ?? '';
|
||
}
|
||
|
||
function setValue(form, blockers, setResults, name, value, source) {
|
||
const elements = Array.from(form.elements).filter((el) => el.name === name);
|
||
if (!elements.length) {
|
||
blockers.push(`Cannot set missing form field ${name} from ${source}`);
|
||
return;
|
||
}
|
||
const textValue = String(value ?? '');
|
||
const type = String(elements[0].type || '').toLowerCase();
|
||
if (type === 'radio') {
|
||
const matched = elements.find((el) => String(el.value) === textValue) || elements[0];
|
||
elements.forEach((el) => { el.checked = el === matched; });
|
||
} else if (type === 'checkbox') {
|
||
elements[0].checked = Boolean(value);
|
||
if (textValue) elements[0].value = textValue;
|
||
} else {
|
||
elements[0].value = textValue;
|
||
}
|
||
setResults.push({ name, source, type, value_length: textValue.length, value_redacted: true });
|
||
}
|
||
|
||
function summarizeValues(values) {
|
||
const normalized = values.map((value) => String(value ?? ''));
|
||
return {
|
||
value_count: normalized.length,
|
||
nonempty_count: normalized.filter((value) => value !== '').length,
|
||
total_value_length: normalized.reduce((sum, value) => sum + value.length, 0),
|
||
value_redacted: true
|
||
};
|
||
}
|
||
|
||
function snapshotForm(form) {
|
||
const snapshot = {};
|
||
Array.from(form.elements).forEach((el, index) => {
|
||
const name = el.name || el.id || `__element_${index}`;
|
||
const type = String(el.type || '').toLowerCase();
|
||
const value = (type === 'checkbox' || type === 'radio') ? (el.checked ? el.value : '') : (el.value ?? '');
|
||
if (!snapshot[name]) snapshot[name] = [];
|
||
snapshot[name].push(String(value));
|
||
});
|
||
return snapshot;
|
||
}
|
||
|
||
function compareSnapshots(before, after) {
|
||
const names = Array.from(new Set([...Object.keys(before), ...Object.keys(after)])).sort();
|
||
return names
|
||
.filter((name) => JSON.stringify(before[name] || []) !== JSON.stringify(after[name] || []))
|
||
.map((name) => ({ name, before: summarizeValues(before[name] || []), after: summarizeValues(after[name] || []) }));
|
||
}
|
||
|
||
function groupChangedFields(changes) {
|
||
const groups = {};
|
||
for (const change of changes) {
|
||
let group = 'other';
|
||
if (/^(Text|shuoming|oldxingcheng|xingcheng)/.test(change.name)) group = 'itinerary_text';
|
||
else if (/^(zao|zhong|wan|zhusu|jingdian)/.test(change.name)) group = 'resources';
|
||
else if (/^(ys_|youyingshou|baojia|yingfu)/.test(change.name)) group = 'pricing_receivable';
|
||
else if (/^(TianShu|chanpinming|tuanxuhao|zhuanxianming|zutuanshe|bizhong|lianxiren)/.test(change.name)) group = 'core';
|
||
groups[group] = (groups[group] || 0) + 1;
|
||
}
|
||
return groups;
|
||
}
|
||
|
||
function serializeForm(form) {
|
||
if (window.jQuery) return window.jQuery(form).serialize();
|
||
return Array.from(new FormData(form).entries())
|
||
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
|
||
.join('&');
|
||
}
|
||
|
||
async function fetchText(path) {
|
||
const url = new URL(path, location.href).href;
|
||
const res = await fetch(url, {
|
||
method: 'POST',
|
||
credentials: 'same-origin',
|
||
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }
|
||
});
|
||
const text = await res.text();
|
||
return { ok: res.ok, status: res.status, text };
|
||
}
|
||
|
||
function localValidate(operation) {
|
||
const blockers = [];
|
||
const data = operation?.data || {};
|
||
const counts = data.passenger_counts || {};
|
||
if (operation?.action !== 'team_order_create') blockers.push('Unsupported action; expected team_order_create.');
|
||
if (operation?.submit_mode !== 'dry_run') blockers.push('submit_mode must be dry_run before approved live submit.');
|
||
if (operation?.order_nature !== 'test') blockers.push('Extension v0.1 only allows test orders for live submit.');
|
||
if (!data.test_marker) blockers.push('data.test_marker is required.');
|
||
if (!data.product?.name) blockers.push('data.product.name is required.');
|
||
if (!Array.isArray(data.departure_dates) || data.departure_dates.length !== 1 || !data.departure_dates[0]) blockers.push('Exactly one departure date is required.');
|
||
const total = passengerTotal(counts);
|
||
if (total <= 0) blockers.push('Passenger total must be greater than zero.');
|
||
if (counts.expected_total != null && Math.trunc(toNumber(counts.expected_total)) !== total) blockers.push('expected_total does not equal computed passenger total.');
|
||
if (!data.op_user?.name) blockers.push('data.op_user.name is required.');
|
||
if (!data.sales_user?.name) blockers.push('data.sales_user.name is required.');
|
||
return blockers;
|
||
}
|
||
|
||
async function openOrderForm({ fabudanwei = '老挝联泰' } = {}) {
|
||
if (/login/i.test(location.href) || /云智办公i-5.1/.test(document.title || '')) {
|
||
return { status: 'login_required', blockers: ['login_required'], top: { url: location.href, title: document.title } };
|
||
}
|
||
const frame = document.getElementById('Iframe_Home') || document.querySelector('iframe[name="MainIframe"], iframe');
|
||
if (!frame) return { status: 'main_iframe_not_found', blockers: ['main_iframe_not_found'] };
|
||
const entryUrl = `https://ltjt.yunzhi.run/System/Business/orders_add.asp?fabudanwei=${encodeURIComponent(fabudanwei)}©=0&ddid=0&_=${Date.now()}`;
|
||
const entryStamp = new URL(entryUrl).searchParams.get('_');
|
||
frame.src = entryUrl;
|
||
return {
|
||
status: 'order_form_open_requested',
|
||
entry_stamp: entryStamp,
|
||
frame_id_hint: frame.id || frame.name || '',
|
||
url_redacted: true
|
||
};
|
||
}
|
||
|
||
async function pingOrderFrame() {
|
||
const form = getOrderForm();
|
||
if (!form) return { status: 'not_order_frame', url: location.href, title: document.title };
|
||
return {
|
||
status: form.elements.length >= 800 ? 'order_frame_ready' : 'order_frame_loading',
|
||
url: location.href,
|
||
title: document.title,
|
||
entry_stamp: new URL(location.href).searchParams.get('_') || '',
|
||
form_element_count: form.elements.length,
|
||
visible: getFrameVisibility()
|
||
};
|
||
}
|
||
|
||
function readOrderEditSnapshot() {
|
||
const form = getOrderForm();
|
||
if (!form) return { status: 'not_order_frame', no_erp_write: true };
|
||
const readNumber = (names) => {
|
||
for (const name of names) {
|
||
const element = form.elements[name];
|
||
const value = element?.value;
|
||
if (value !== undefined && value !== '') return toNumber(value);
|
||
}
|
||
return 0;
|
||
};
|
||
const readText = (names) => {
|
||
for (const name of names) {
|
||
const element = form.elements[name];
|
||
const value = element?.value;
|
||
if (value !== undefined && value !== '') return String(value);
|
||
}
|
||
return '';
|
||
};
|
||
const params = new URL(location.href).searchParams;
|
||
const remark = readText(['dingdanbeizhu', 'xiadanbeizhu', 'ddbeizhu', 'beizhu', 'beizhu1', 'remark']);
|
||
return {
|
||
status: 'order_edit_snapshot_ready',
|
||
mode: params.get('ddid') && params.get('ddid') !== '0' ? 'edit' : 'create',
|
||
identifier: params.get('ddid') || '',
|
||
fields: {
|
||
pax: {
|
||
adult: readNumber(['darenshu']),
|
||
child_bed: readNumber(['xiaorenshu']),
|
||
child_no_bed: readNumber(['ertrenshu']),
|
||
infant: readNumber(['yingrenshu']),
|
||
leader: readNumber(['quanrenshu']),
|
||
},
|
||
rooms: {
|
||
SGL: readNumber(['frenshu0']),
|
||
TWN: readNumber(['frenshu1']),
|
||
TRP: readNumber(['frenshu2']),
|
||
DBL: readNumber(['frenshu3']),
|
||
HNM: readNumber(['frenshu4']),
|
||
TL: readNumber(['frenshu5']),
|
||
},
|
||
prices: {
|
||
adult: readNumber(['ys_danjia0']),
|
||
child_bed: readNumber(['ys_danjia1']),
|
||
child_no_bed: readNumber(['ys_danjia2']),
|
||
infant: readNumber(['ys_danjia3']),
|
||
leader: readNumber(['ys_danjia4']),
|
||
},
|
||
remark: {
|
||
present: Boolean(remark),
|
||
length: remark.length,
|
||
},
|
||
},
|
||
form_element_count: form.elements.length,
|
||
no_erp_write: true,
|
||
write_attempted: false,
|
||
};
|
||
}
|
||
|
||
async function preflightRawInstruction(operation, options = {}) {
|
||
const form = getOrderForm();
|
||
if (!form) return { status: 'not_order_frame' };
|
||
if (form.elements.length < 800) {
|
||
return {
|
||
status: 'raw_instruction_test_blocked',
|
||
blockers: ['orders_add_form_not_ready'],
|
||
page: { url: location.href, title: document.title, form_element_count: form.elements.length }
|
||
};
|
||
}
|
||
|
||
const blockers = localValidate(operation);
|
||
const warnings = [];
|
||
const setResults = [];
|
||
const ajaxRecords = [];
|
||
const data = operation.data || {};
|
||
const marker = String(data.test_marker || '').trim();
|
||
const markerSuffix = `${marker}-${Date.now()}`;
|
||
const fabudanwei = form.elements.fabudanwei?.value || data.system_defaults?.fabudanwei || '老挝联泰';
|
||
|
||
const endpoints = {
|
||
product: `../dat/AjaxPublicFun.asp?Act=ProGetProductname&fls=1&fabudanwei=${encodeURIComponent(fabudanwei)}`,
|
||
route: `../dat/AjaxPublicFun.asp?Act=GetInformation&fl=3&fls=1&fabudanwei=${encodeURIComponent(fabudanwei)}`,
|
||
customer: `../dat/AjaxPublicFun.asp?Act=ProTravel&fabudanwei=${encodeURIComponent(fabudanwei)}`,
|
||
staff: `../dat/AjaxPublicFun.asp?Act=ProDanwei_Yuangong&fabudanwei=${encodeURIComponent(fabudanwei)}`
|
||
};
|
||
const fetched = {};
|
||
for (const [key, path] of Object.entries(endpoints)) {
|
||
fetched[key] = await fetchText(path);
|
||
if (!fetched[key].ok) blockers.push(`${key} lookup endpoint returned HTTP ${fetched[key].status}`);
|
||
if (isLoginText(fetched[key].text)) blockers.push(`${key} lookup response looked like login timeout`);
|
||
}
|
||
|
||
const productSelection = selectUniqueExistingOption(fetched.product?.text || '', data.product?.name, 1, 'product');
|
||
if (!productSelection.match) blockers.push(productSelection.blocker);
|
||
const productInitialCheck = {
|
||
name: 'product_initial_selection',
|
||
endpoint_key: 'product',
|
||
match_rule: productSelection.rule,
|
||
exact_match_count: productSelection.exact_count,
|
||
contains_match_count: productSelection.contains_count,
|
||
match_shape: productSelection.match ? { row_index: productSelection.match.rowIndex, column_count: productSelection.match.columns.length, set_value_targets: ['chanpinming'] } : null,
|
||
summary: summarizeText(fetched.product?.text || '')
|
||
};
|
||
|
||
const jq = window.jQuery || window.$;
|
||
const originalAjax = jq?.ajax;
|
||
if (!jq || typeof originalAjax !== 'function') blockers.push('jQuery.ajax is not available in order form');
|
||
if (typeof window.Find_product !== 'function') blockers.push('Find_product() is not available');
|
||
|
||
if (!blockers.length) {
|
||
jq.ajax = function ajaxWrapper(optionsArg, ...rest) {
|
||
const config = typeof optionsArg === 'string' ? { url: optionsArg } : { ...(optionsArg || {}) };
|
||
const isProductEffect = String(config.url || '').includes('Act=GetProduct');
|
||
if (!isProductEffect) return originalAjax.call(this, optionsArg, ...rest);
|
||
const record = {
|
||
url_path_redacted: '../dat/AjaxPublicFun.asp?Act=GetProduct&cpm=[redacted]',
|
||
method: config.type || config.method || 'GET',
|
||
dataType: config.dataType || '',
|
||
completed: false,
|
||
ok: false,
|
||
text_status: '',
|
||
response_byte_length: 0,
|
||
contains_login_timeout_text: false
|
||
};
|
||
ajaxRecords.push(record);
|
||
const originalSuccess = config.success;
|
||
const originalError = config.error;
|
||
const originalComplete = config.complete;
|
||
config.success = function successWrapper(responseData, textStatus) {
|
||
const text = String(responseData || '');
|
||
record.ok = true;
|
||
record.text_status = textStatus || '';
|
||
record.response_byte_length = new Blob([text]).size;
|
||
record.contains_login_timeout_text = isLoginText(text);
|
||
return originalSuccess ? originalSuccess.apply(this, arguments) : undefined;
|
||
};
|
||
config.error = function errorWrapper(jqXHR, textStatus, errorThrown) {
|
||
record.ok = false;
|
||
record.text_status = textStatus || '';
|
||
record.error_text_redacted = errorThrown ? String(errorThrown).slice(0, 80) : '';
|
||
return originalError ? originalError.apply(this, arguments) : undefined;
|
||
};
|
||
config.complete = function completeWrapper() {
|
||
record.completed = true;
|
||
return originalComplete ? originalComplete.apply(this, arguments) : undefined;
|
||
};
|
||
return originalAjax.call(this, config, ...rest);
|
||
};
|
||
}
|
||
|
||
const beforeProduct = snapshotForm(form);
|
||
if (!blockers.length) {
|
||
setValue(form, blockers, setResults, 'chanpinming', productSelection.match.columns[1], 'product_lookup_selection');
|
||
try {
|
||
window.Find_product();
|
||
} catch (error) {
|
||
blockers.push(`Find_product() threw: ${String(error.message || error).slice(0, 160)}`);
|
||
}
|
||
}
|
||
const effectStarted = Date.now();
|
||
while (Date.now() - effectStarted < 15000) {
|
||
if (ajaxRecords.some((record) => record.completed)) break;
|
||
await sleep(250);
|
||
}
|
||
await sleep(800);
|
||
if (jq && originalAjax) jq.ajax = originalAjax;
|
||
|
||
const afterProduct = snapshotForm(form);
|
||
const productChanges = compareSnapshots(beforeProduct, afterProduct);
|
||
const productAjax = ajaxRecords.find((record) => record.url_path_redacted.includes('GetProduct')) || null;
|
||
if (!productAjax) blockers.push('GetProduct ajax request was not observed');
|
||
else if (!productAjax.ok) blockers.push('GetProduct ajax request did not complete successfully');
|
||
else if (productAjax.contains_login_timeout_text) blockers.push('GetProduct response looked like login-timeout script');
|
||
if (!productChanges.length) blockers.push('Find_product/GetProduct completed without changing serialized form fields');
|
||
|
||
const core = {
|
||
product: getValue(form, 'chanpinming'),
|
||
trip: getValue(form, 'TianShu'),
|
||
route: getValue(form, 'zhuanxianming'),
|
||
routePrefix: getValue(form, 'tuanxuhao1'),
|
||
customer: getValue(form, 'zutuanshe'),
|
||
customerId: getValue(form, 'zutuansheid'),
|
||
currency: getValue(form, 'bizhong')
|
||
};
|
||
|
||
const staticTrip = '1D◇2D1N◇3D2N◇4D3N◇5D4N◇6D5N◇7D6N◇8D7N◇9D8N◇10D9N';
|
||
const productAfterMatches = exactMatches(fetched.product?.text || '', core.product, 1);
|
||
const tripMatches = exactMatches(staticTrip, core.trip, 0);
|
||
const routeMatches = exactMatches(fetched.route?.text || '', core.route, 1);
|
||
const customerMatches = exactMatches(fetched.customer?.text || '', core.customer, 2);
|
||
const opSelection = selectUniqueExistingOption(fetched.staff?.text || '', data.op_user?.name, 0, 'op_user');
|
||
const salesSelection = selectUniqueExistingOption(fetched.staff?.text || '', data.sales_user?.name, 0, 'sales_user');
|
||
if (!opSelection.match) blockers.push(opSelection.blocker);
|
||
if (!salesSelection.match) blockers.push(salesSelection.blocker);
|
||
|
||
const lookupChecks = [
|
||
productInitialCheck,
|
||
{ name: 'product_after_effect', endpoint_key: 'product', exact_match_count: productAfterMatches.length, match_shape: productAfterMatches.length === 1 ? { row_index: productAfterMatches[0].rowIndex, column_count: productAfterMatches[0].columns.length } : null, summary: summarizeText(fetched.product?.text || '') },
|
||
{ name: 'trip_after_effect', endpoint_key: '', exact_match_count: tripMatches.length, match_shape: tripMatches.length === 1 ? { row_index: tripMatches[0].rowIndex, column_count: tripMatches[0].columns.length } : null, summary: summarizeText(staticTrip) },
|
||
{ name: 'route_after_effect', endpoint_key: 'route', exact_match_count: routeMatches.length, match_shape: routeMatches.length === 1 ? { row_index: routeMatches[0].rowIndex, column_count: routeMatches[0].columns.length } : null, summary: summarizeText(fetched.route?.text || '') },
|
||
{ name: 'customer_after_effect', endpoint_key: 'customer', exact_match_count: customerMatches.length, match_shape: customerMatches.length === 1 ? { row_index: customerMatches[0].rowIndex, column_count: customerMatches[0].columns.length } : null, summary: summarizeText(fetched.customer?.text || '') },
|
||
{ name: 'op_user', endpoint_key: 'staff', exact_match_count: opSelection.exact_count, contains_match_count: opSelection.contains_count, match_rule: opSelection.rule, match_shape: opSelection.match ? { row_index: opSelection.match.rowIndex, column_count: opSelection.match.columns.length, set_value_targets: ['gendanren'] } : null, summary: summarizeText(fetched.staff?.text || '') },
|
||
{ name: 'sales_user', endpoint_key: 'staff', exact_match_count: salesSelection.exact_count, contains_match_count: salesSelection.contains_count, match_rule: salesSelection.rule, match_shape: salesSelection.match ? { row_index: salesSelection.match.rowIndex, column_count: salesSelection.match.columns.length, set_value_targets: ['xiaoshouren'] } : null, summary: summarizeText(fetched.staff?.text || '') }
|
||
];
|
||
for (const check of lookupChecks.slice(1, 5)) {
|
||
if (check.exact_match_count !== 1) blockers.push(`${check.name}: expected exactly one existing LTJT option, found ${check.exact_match_count}`);
|
||
}
|
||
|
||
const consistencyChecks = [
|
||
['product_template_product_nonempty', 'chanpinming', core.product],
|
||
['product_template_trip_nonempty', 'TianShu', core.trip],
|
||
['product_template_route_nonempty', 'zhuanxianming', core.route],
|
||
['product_template_route_prefix_nonempty', 'tuanxuhao1', core.routePrefix],
|
||
['product_template_customer_nonempty', 'zutuanshe', core.customer],
|
||
['product_template_customer_id_nonempty', 'zutuansheid', core.customerId]
|
||
].map(([name, field, value]) => {
|
||
const passed = Boolean(String(value || '').trim());
|
||
if (!passed) blockers.push(`${name}: product template did not populate ${field}`);
|
||
return { name, field, passed, required: true, actual_redacted: true, actual_length: String(value || '').length };
|
||
});
|
||
if (customerMatches.length === 1 && String(customerMatches[0].columns[1] || '').trim() !== String(core.customerId || '').trim()) {
|
||
blockers.push('customer_after_effect: product template customer id does not match existing customer option id');
|
||
}
|
||
if (routeMatches.length === 1 && String(routeMatches[0].columns[3] || '').trim() !== String(core.routePrefix || '').trim()) {
|
||
blockers.push('route_after_effect: product template route prefix does not match existing route option prefix');
|
||
}
|
||
|
||
const counts = data.passenger_counts || {};
|
||
const rooms = data.room_counts || {};
|
||
const prices = data.prices || {};
|
||
const passengerSum = passengerTotal(counts);
|
||
const expectedPassengerTotal = counts.expected_total == null ? passengerSum : Math.trunc(toNumber(counts.expected_total));
|
||
const computedRoomTotal = roomTotal(rooms);
|
||
const logicalChecks = [
|
||
{ name: 'passenger_total', passed: passengerSum > 0 && passengerSum === expectedPassengerTotal, expected: expectedPassengerTotal, actual: passengerSum },
|
||
{ name: 'room_total', passed: computedRoomTotal >= 0, actual: computedRoomTotal },
|
||
{ name: 'departure_date_count', passed: Array.isArray(data.departure_dates) && data.departure_dates.length === 1, actual: Array.isArray(data.departure_dates) ? data.departure_dates.length : 0 }
|
||
];
|
||
logicalChecks.forEach((check) => {
|
||
if (!check.passed) blockers.push(`${check.name}: raw instruction logical validation failed`);
|
||
});
|
||
|
||
const receivableRows = [];
|
||
[
|
||
['adult', '成人团费'],
|
||
['child_bed', '小童占床'],
|
||
['child_no_bed', '小童不占床'],
|
||
['infant', '婴儿'],
|
||
['leader', '领队']
|
||
].forEach(([key, label]) => {
|
||
const quantity = toNumber(counts[key]);
|
||
const unitPrice = toNumber(prices[key]);
|
||
if (quantity > 0 && unitPrice > 0) receivableRows.push({ key, label, quantity, unit_price: unitPrice, amount: quantity * unitPrice });
|
||
else if (quantity > 0 && unitPrice <= 0) blockers.push(`price for ${key} must be greater than zero when quantity is greater than zero`);
|
||
});
|
||
if (!receivableRows.length) blockers.push('No receivable rows were generated from passenger counts and prices');
|
||
|
||
if (!blockers.length) {
|
||
const note = `${marker}|测试订单|${String(data.special_requests || '')}`;
|
||
const currency = core.currency || prices.currency || data.system_defaults?.currency || 'USD';
|
||
setValue(form, blockers, setResults, 'chufa_ri', dateYyyyMD(data.departure_dates[0]), 'raw_instruction_departure_date');
|
||
setValue(form, blockers, setResults, 'tuanxuhao2', markerSuffix, 'raw_instruction_obvious_test_suffix');
|
||
setValue(form, blockers, setResults, 'gendanren', opSelection.match.columns[0], 'raw_instruction_existing_op_user');
|
||
setValue(form, blockers, setResults, 'xiaoshouren', salesSelection.match.columns[0], 'raw_instruction_existing_sales_user');
|
||
setValue(form, blockers, setResults, 'darenshu', integerString(counts.adult), 'raw_instruction_passenger_counts');
|
||
setValue(form, blockers, setResults, 'xiaorenshu', integerString(counts.child_bed), 'raw_instruction_passenger_counts');
|
||
setValue(form, blockers, setResults, 'ertrenshu', integerString(counts.child_no_bed), 'raw_instruction_passenger_counts');
|
||
setValue(form, blockers, setResults, 'yingrenshu', integerString(counts.infant), 'raw_instruction_passenger_counts');
|
||
setValue(form, blockers, setResults, 'quanrenshu', integerString(counts.leader), 'raw_instruction_passenger_counts');
|
||
setValue(form, blockers, setResults, 'frenshu0', integerString(rooms.SGL), 'raw_instruction_room_counts');
|
||
setValue(form, blockers, setResults, 'frenshu1', integerString(rooms.TWN), 'raw_instruction_room_counts');
|
||
setValue(form, blockers, setResults, 'frenshu2', integerString(rooms.TRP), 'raw_instruction_room_counts');
|
||
setValue(form, blockers, setResults, 'frenshu3', integerString(rooms.DBL), 'raw_instruction_room_counts');
|
||
setValue(form, blockers, setResults, 'frenshu4', integerString(rooms.HNM), 'raw_instruction_room_counts');
|
||
setValue(form, blockers, setResults, 'frenshu5', integerString(rooms.TL), 'raw_instruction_room_counts');
|
||
setValue(form, blockers, setResults, 'frenshu6', integerString(computedRoomTotal), 'raw_instruction_room_counts');
|
||
setValue(form, blockers, setResults, 'danzhuangtai', data.system_defaults?.business_status || '预订', 'raw_instruction_status');
|
||
setValue(form, blockers, setResults, 'yaobeian', data.system_defaults?.filing_required === false ? '' : '要备案', 'raw_instruction_checkbox');
|
||
setValue(form, blockers, setResults, 'xiadanbeizhu', note, 'raw_instruction_obvious_test_note');
|
||
|
||
const receivableTargets = ['ys_danwei','ys_danweiid','ys_shuoming','ys_zhanwei','ys_xiangmu','ys_fangshi','ys_bizhong','ys_shuliang','ys_danjia','ys_jine','ys_yishoufu','ys_beizhu','ys_id','ys_shoufulei','ys_caozuoren','ys_shenheren'];
|
||
for (let index = 0; index < 10; index += 1) {
|
||
receivableTargets.forEach((prefix) => setValue(form, blockers, setResults, `${prefix}${index}`, '', 'raw_instruction_clear_receivables'));
|
||
}
|
||
receivableRows.forEach((row, index) => {
|
||
setValue(form, blockers, setResults, `ys_danwei${index}`, core.customer, 'raw_instruction_receivable_customer');
|
||
setValue(form, blockers, setResults, `ys_danweiid${index}`, core.customerId, 'raw_instruction_receivable_customer_id');
|
||
setValue(form, blockers, setResults, `ys_xiangmu${index}`, `${marker} ${row.label}`, 'raw_instruction_receivable_item');
|
||
setValue(form, blockers, setResults, `ys_shuoming${index}`, '人', 'raw_instruction_receivable_unit');
|
||
setValue(form, blockers, setResults, `ys_bizhong${index}`, currency, 'raw_instruction_receivable_currency');
|
||
setValue(form, blockers, setResults, `ys_shuliang${index}`, compactNumber(row.quantity), 'raw_instruction_receivable_quantity');
|
||
setValue(form, blockers, setResults, `ys_danjia${index}`, compactNumber(row.unit_price), 'raw_instruction_receivable_unit_price');
|
||
setValue(form, blockers, setResults, `ys_jine${index}`, compactNumber(row.amount), 'raw_instruction_receivable_amount');
|
||
setValue(form, blockers, setResults, `ys_yishoufu${index}`, '0', 'raw_instruction_receivable_paid');
|
||
setValue(form, blockers, setResults, `ys_beizhu${index}`, note, 'raw_instruction_receivable_remark');
|
||
setValue(form, blockers, setResults, `ys_shoufulei${index}`, '0', 'raw_instruction_receivable_payment_type');
|
||
setValue(form, blockers, setResults, `ys_caozuoren${index}`, opSelection.match.columns[0], 'raw_instruction_receivable_operator');
|
||
});
|
||
}
|
||
|
||
const requiredTargets = ['chufa_ri','zhuanxianming','TianShu','zutuanshe','chanpinming','gendanren','tuanxuhao1','tuanxuhao2','darenshu','xiaorenshu','ertrenshu','yingrenshu','quanrenshu','xiaoshouren'];
|
||
const requiredMissing = requiredTargets.filter((name) => String(getValue(form, name) || '').trim() === '');
|
||
if (requiredMissing.length) blockers.push(`Required fields still blank: ${requiredMissing.join(', ')}`);
|
||
|
||
const serializedForm = serializeForm(form);
|
||
const serializedParams = new URLSearchParams(serializedForm);
|
||
let submitIntercept = null;
|
||
if (options.interceptSubmit && !blockers.length) {
|
||
submitIntercept = await interceptSubmit(form, jq);
|
||
if (submitIntercept.status !== 'submit_intercept_captured') blockers.push('submit intercept did not capture exactly one clean DoInfoJH branch');
|
||
}
|
||
|
||
const receivableAmountTotal = receivableRows.reduce((sum, row) => sum + row.amount, 0);
|
||
const output = {
|
||
ok: true,
|
||
status: blockers.length ? 'raw_instruction_test_blocked' : 'raw_instruction_test_passed',
|
||
page: { url: location.href, title: document.title, form_element_count: form.elements.length },
|
||
parsed_instruction: {
|
||
action: operation.action,
|
||
order_nature: operation.order_nature,
|
||
order_mode: data.order_mode || '',
|
||
departure_date: dateYyyyMD(data.departure_dates?.[0] || ''),
|
||
passenger_total: passengerSum,
|
||
room_total: computedRoomTotal,
|
||
receivable_row_count: receivableRows.length,
|
||
receivable_amount_total: receivableAmountTotal,
|
||
values_are_test_only: true
|
||
},
|
||
lookup_checks: lookupChecks,
|
||
product_side_effect: {
|
||
ajax_records: ajaxRecords,
|
||
changed_field_count: productChanges.length,
|
||
changed_field_groups: groupChangedFields(productChanges),
|
||
changed_field_names: productChanges.map((change) => change.name)
|
||
},
|
||
consistency_checks: consistencyChecks,
|
||
core_after_product: Object.fromEntries(Object.entries(core).map(([key, value]) => [key, { nonempty: Boolean(String(value || '').trim()), value_length: String(value || '').length, value_redacted: true }])),
|
||
logical_checks: logicalChecks,
|
||
test_markers: {
|
||
suffix_marker: marker,
|
||
note_marker: marker,
|
||
full_suffix_marker: markerSuffix,
|
||
values_are_test_only: true
|
||
},
|
||
set_summary: {
|
||
set_field_count: setResults.length,
|
||
set_field_names: Array.from(new Set(setResults.map((item) => item.name))).sort(),
|
||
set_results: setResults
|
||
},
|
||
final_form: {
|
||
serialized_field_count: Array.from(serializedParams.keys()).length,
|
||
serialized_form_sha256: await sha256(`Act=DoInfoJH&${serializedForm}`),
|
||
serialized_form_redacted: true,
|
||
required_missing: requiredMissing
|
||
},
|
||
submit_intercept: submitIntercept,
|
||
blockers,
|
||
warnings,
|
||
submit_safety: {
|
||
live_submit_attempted: false,
|
||
DoInfoJH_network_prevented: Boolean(submitIntercept?.DoInfoJH_network_prevented),
|
||
note: 'Chrome extension preflight intercepted DoInfoJH before network.'
|
||
}
|
||
};
|
||
if (submitIntercept?.intercepted_submits?.[0]?.payload_sha256) {
|
||
output.final_form.submit_payload_sha256 = submitIntercept.intercepted_submits[0].payload_sha256;
|
||
output.final_form.submit_function_mutates_payload = output.final_form.submit_payload_sha256 !== output.final_form.serialized_form_sha256;
|
||
}
|
||
return output;
|
||
}
|
||
|
||
async function interceptSubmit(form, jq) {
|
||
const originalAjaxForSubmit = jq.ajax;
|
||
const originalAlert = window.alert;
|
||
const originalDialogAlert = jq.dialog?.alert;
|
||
const intercepted = [];
|
||
const alerts = [];
|
||
window.alert = function interceptedAlert(message) {
|
||
alerts.push({ type: 'alert', message_length: String(message || '').length, message_redacted: true });
|
||
return undefined;
|
||
};
|
||
if (jq.dialog && typeof jq.dialog.alert === 'function') {
|
||
jq.dialog.alert = function interceptedDialogAlert(message) {
|
||
alerts.push({ type: 'dialog.alert', message_length: String(message || '').length, message_redacted: true });
|
||
return undefined;
|
||
};
|
||
}
|
||
jq.ajax = function ajaxIntercept(optionsArg, ...rest) {
|
||
const config = typeof optionsArg === 'string' ? { url: optionsArg } : { ...(optionsArg || {}) };
|
||
const url = String(config.url || '');
|
||
const requestData = String(config.data || '');
|
||
const isSubmit = /\/System\/DAT\/orders\.asp/i.test(url) && requestData.startsWith('Act=DoInfoJH&');
|
||
if (!isSubmit) return originalAjaxForSubmit.call(this, optionsArg, ...rest);
|
||
const params = new URLSearchParams(requestData.slice('Act=DoInfoJH&'.length));
|
||
intercepted.push({
|
||
url_redacted: '/System/DAT/orders.asp',
|
||
method: config.type || config.method || 'GET',
|
||
dataType: config.dataType || '',
|
||
prevented_from_network: true,
|
||
payload: requestData,
|
||
payload_summary: {
|
||
starts_with_DoInfoJH: true,
|
||
byte_length: new Blob([requestData]).size,
|
||
field_count: Array.from(params.keys()).length,
|
||
field_names: Array.from(new Set(Array.from(params.keys()))).sort(),
|
||
value_redacted: true
|
||
}
|
||
});
|
||
return { readyState: 4, status: 0, statusText: 'intercepted_by_ltjt_order_assistant', abort() {} };
|
||
};
|
||
let thrown = '';
|
||
try {
|
||
window.SubmitInfoForm();
|
||
} catch (error) {
|
||
thrown = error.message || String(error);
|
||
}
|
||
await sleep(1000);
|
||
jq.ajax = originalAjaxForSubmit;
|
||
window.alert = originalAlert;
|
||
if (jq.dialog && typeof originalDialogAlert === 'function') jq.dialog.alert = originalDialogAlert;
|
||
const loadingCleanup = closeLingeringLoading(jq);
|
||
for (const item of intercepted) {
|
||
item.payload_sha256 = await sha256(item.payload || '');
|
||
item.payload_redacted = true;
|
||
delete item.payload;
|
||
}
|
||
return {
|
||
status: intercepted.length === 1 && !alerts.length && !thrown ? 'submit_intercept_captured' : 'submit_intercept_blocked',
|
||
intercepted_submit_count: intercepted.length,
|
||
intercepted_submits: intercepted,
|
||
alerts,
|
||
ui_cleanup: {
|
||
loading_cleanup: loadingCleanup
|
||
},
|
||
thrown_redacted: Boolean(thrown),
|
||
thrown_length: String(thrown || '').length,
|
||
DoInfoJH_network_prevented: true
|
||
};
|
||
}
|
||
|
||
async function liveSubmitApproved({ expectedPayloadSha256, expectedFieldCount } = {}) {
|
||
const form = getOrderForm();
|
||
if (!form) return { status: 'not_order_frame' };
|
||
if (form.elements.length < 800) {
|
||
return {
|
||
status: 'live_submit_blocked',
|
||
blockers: ['orders_add_form_not_ready'],
|
||
form_element_count: form.elements.length,
|
||
submit_safety: { live_submit_attempted: false }
|
||
};
|
||
}
|
||
const jq = window.jQuery || window.$;
|
||
if (!jq || typeof jq.ajax !== 'function') return { status: 'live_submit_blocked', blockers: ['jquery_ajax_not_found'] };
|
||
if (typeof window.SubmitInfoForm !== 'function') return { status: 'live_submit_blocked', blockers: ['SubmitInfoForm_not_found'] };
|
||
if (!expectedPayloadSha256) return { status: 'live_submit_blocked', blockers: ['expectedPayloadSha256_missing'] };
|
||
|
||
const serializedForm = serializeForm(form);
|
||
const requestBody = `Act=DoInfoJH&${serializedForm}`;
|
||
const currentHash = await sha256(requestBody);
|
||
const params = new URLSearchParams(serializedForm);
|
||
const blockers = [];
|
||
if (expectedFieldCount && Array.from(params.keys()).length !== expectedFieldCount) blockers.push('current browser field count does not match approved report field count');
|
||
if (blockers.length) {
|
||
return {
|
||
status: 'live_submit_blocked',
|
||
current_payload: { payload_sha256: currentHash, field_count: Array.from(params.keys()).length, value_redacted: true },
|
||
blockers,
|
||
submit_safety: { live_submit_attempted: false }
|
||
};
|
||
}
|
||
|
||
const originalAjax = jq.ajax;
|
||
const ajaxRecords = [];
|
||
const alerts = [];
|
||
const deferredDialogCallbacks = [];
|
||
const originalAlert = window.alert;
|
||
const originalDialogAlert = jq.dialog?.alert;
|
||
const originalMessagerAlert = jq.messager?.alert;
|
||
function parseErpSubmitReceipt(message) {
|
||
const raw = typeof message === 'object' && message
|
||
? String(message.content || message.message || message.msg || message.text || '')
|
||
: String(message || '');
|
||
const text = raw
|
||
.replace(/\r/g, '')
|
||
.replace(/<br\s*\/?>/gi, '\n')
|
||
.replace(/<\/(p|div|li|tr)>/gi, '\n')
|
||
.replace(/<[^>]+>/g, '')
|
||
.replace(/ /gi, ' ')
|
||
.replace(/&/gi, '&')
|
||
.replace(/</gi, '<')
|
||
.replace(/>/gi, '>')
|
||
.trim();
|
||
if (!text) return null;
|
||
const lines = text.split('\n').map((line) => line.trim()).filter(Boolean);
|
||
const valueAfterLabel = (label) => {
|
||
const pattern = new RegExp(`${label}\\s*[::]\\s*([^\\n]+)`, 'i');
|
||
const line = lines.find((item) => pattern.test(item));
|
||
const source = line || text;
|
||
const match = source.match(pattern);
|
||
const value = match ? match[1].trim() : '';
|
||
return label === '团号' ? value.replace(/[\s。;;,,].*$/, '').trim() : value;
|
||
};
|
||
const groupNumber = valueAfterLabel('团号');
|
||
const product = valueAfterLabel('产品');
|
||
const passengerText = valueAfterLabel('人数');
|
||
const success = /下单成功|成功|success|ສໍາເລັດ/i.test(text);
|
||
if (!success && !groupNumber && !product && !passengerText) return null;
|
||
return {
|
||
status: success ? 'submit_success_receipt' : 'submit_receipt',
|
||
success,
|
||
title: lines[0] || '',
|
||
group_number: groupNumber,
|
||
product,
|
||
passenger_text: passengerText,
|
||
line_count: lines.length,
|
||
received_at: new Date().toISOString()
|
||
};
|
||
}
|
||
function alertRecord(type, message) {
|
||
const text = typeof message === 'object' && message
|
||
? String(message.content || message.message || message.msg || message.text || '')
|
||
: String(message || '');
|
||
const receipt = parseErpSubmitReceipt(text);
|
||
return {
|
||
type,
|
||
message_length: text.length,
|
||
message_redacted: !receipt,
|
||
message: receipt ? text.slice(0, 500) : undefined,
|
||
erp_receipt: receipt || undefined
|
||
};
|
||
}
|
||
function captureVisibleDialogReceipts() {
|
||
const selector = [
|
||
'.messager-window',
|
||
'.messager-body',
|
||
'.window.messager-window',
|
||
'.aui_dialog',
|
||
'.ui-dialog',
|
||
'.dialog',
|
||
'[role="dialog"]'
|
||
].join(',');
|
||
Array.from(document.querySelectorAll(selector)).forEach((node) => {
|
||
const text = String(node.textContent || '').trim();
|
||
if (!text) return;
|
||
const receipt = parseErpSubmitReceipt(text);
|
||
if (receipt && !alerts.some((alert) => alert.erp_receipt?.group_number === receipt.group_number)) {
|
||
alerts.push({
|
||
type: 'visible_dialog',
|
||
message_length: text.length,
|
||
message_redacted: false,
|
||
message: text.slice(0, 500),
|
||
erp_receipt: receipt
|
||
});
|
||
}
|
||
});
|
||
}
|
||
window.alert = function liveSubmitAlert(message) {
|
||
alerts.push(alertRecord('alert', message));
|
||
return undefined;
|
||
};
|
||
if (jq.dialog && typeof jq.dialog.alert === 'function') {
|
||
jq.dialog.alert = function liveSubmitDialogAlert(message) {
|
||
alerts.push(alertRecord('dialog.alert', message));
|
||
Array.from(arguments).forEach((arg) => {
|
||
if (typeof arg === 'function') deferredDialogCallbacks.push(arg);
|
||
});
|
||
return undefined;
|
||
};
|
||
}
|
||
if (jq.messager && typeof jq.messager.alert === 'function') {
|
||
jq.messager.alert = function liveSubmitMessagerAlert(title, message) {
|
||
alerts.push(alertRecord('messager.alert', [title, message].filter(Boolean).join('\n')));
|
||
Array.from(arguments).forEach((arg) => {
|
||
if (typeof arg === 'function') deferredDialogCallbacks.push(arg);
|
||
});
|
||
return undefined;
|
||
};
|
||
}
|
||
jq.ajax = function liveSubmitAjaxWrapper(optionsArg, ...rest) {
|
||
const config = typeof optionsArg === 'string' ? { url: optionsArg } : { ...(optionsArg || {}) };
|
||
const url = String(config.url || '');
|
||
const requestData = String(config.data || '');
|
||
const isSubmit = /\/System\/DAT\/orders\.asp/i.test(url) && requestData.startsWith('Act=DoInfoJH&');
|
||
if (!isSubmit) return originalAjax.call(this, optionsArg, ...rest);
|
||
const record = {
|
||
url_redacted: '/System/DAT/orders.asp',
|
||
method: config.type || config.method || 'GET',
|
||
dataType: config.dataType || '',
|
||
request_payload_sha256: sha256Sync(requestData),
|
||
request_field_count: expectedFieldCount,
|
||
live_submit_attempted: true,
|
||
prevented_from_network: false,
|
||
completed: false,
|
||
ok: false,
|
||
text_status: '',
|
||
response_byte_length: 0,
|
||
response_contains_login_timeout: false,
|
||
response_contains_permission_text: false,
|
||
response_contains_success_hint: false,
|
||
response_contains_error_hint: false,
|
||
response_redacted: true
|
||
};
|
||
ajaxRecords.push(record);
|
||
if (record.request_payload_sha256 !== expectedPayloadSha256) {
|
||
record.prevented_from_network = true;
|
||
record.completed = true;
|
||
record.ok = false;
|
||
record.text_status = 'blocked_hash_mismatch_before_network';
|
||
return { readyState: 4, status: 0, statusText: 'blocked_hash_mismatch_before_network', abort() {} };
|
||
}
|
||
const originalSuccess = config.success;
|
||
const originalError = config.error;
|
||
const originalComplete = config.complete;
|
||
config.success = function successWrapper(responseText, textStatus) {
|
||
const text = String(responseText || '');
|
||
record.ok = true;
|
||
record.text_status = textStatus || '';
|
||
record.response_byte_length = new Blob([text]).size;
|
||
record.response_contains_login_timeout = isLoginText(text);
|
||
record.response_contains_permission_text = /权限|permission/i.test(text);
|
||
record.response_contains_success_hint = /成功|保存|添加|完成|ok|success/i.test(text);
|
||
record.response_contains_error_hint = /失败|错误|异常|error|alert\(/i.test(text) && !record.response_contains_success_hint;
|
||
return originalSuccess ? originalSuccess.apply(this, arguments) : undefined;
|
||
};
|
||
config.error = function errorWrapper(jqXHR, textStatus, errorThrown) {
|
||
record.ok = false;
|
||
record.text_status = textStatus || '';
|
||
record.error_text_redacted = errorThrown ? String(errorThrown).slice(0, 80) : '';
|
||
return originalError ? originalError.apply(this, arguments) : undefined;
|
||
};
|
||
config.complete = function completeWrapper() {
|
||
record.completed = true;
|
||
return originalComplete ? originalComplete.apply(this, arguments) : undefined;
|
||
};
|
||
return originalAjax.call(this, config, ...rest);
|
||
};
|
||
|
||
let thrown = '';
|
||
try {
|
||
window.SubmitInfoForm();
|
||
} catch (error) {
|
||
thrown = error.message || String(error);
|
||
}
|
||
const started = Date.now();
|
||
while (Date.now() - started < 30000) {
|
||
if (ajaxRecords.some((record) => record.completed)) break;
|
||
await sleep(250);
|
||
}
|
||
jq.ajax = originalAjax;
|
||
captureVisibleDialogReceipts();
|
||
window.alert = originalAlert;
|
||
if (jq.dialog && typeof originalDialogAlert === 'function') jq.dialog.alert = originalDialogAlert;
|
||
if (jq.messager && typeof originalMessagerAlert === 'function') jq.messager.alert = originalMessagerAlert;
|
||
const immediateLoadingCleanup = closeLingeringLoading(jq);
|
||
if (deferredDialogCallbacks.length) {
|
||
// Native dialog callbacks may dereference a popup/opener that no longer
|
||
// exists after the extension intercepts the dialog. Verification and
|
||
// return-to-list are handled explicitly by the executor, so replaying
|
||
// those callbacks is both unnecessary and able to disrupt reconciliation.
|
||
window.setTimeout(() => {
|
||
closeLingeringLoading(window.jQuery || window.$);
|
||
}, 100);
|
||
} else {
|
||
window.setTimeout(() => closeLingeringLoading(window.jQuery || window.$), 800);
|
||
}
|
||
|
||
const submitBlockers = [];
|
||
if (thrown) submitBlockers.push('SubmitInfoForm threw before ajax');
|
||
if (!ajaxRecords.length) submitBlockers.push('SubmitInfoForm did not attempt DoInfoJH ajax submit');
|
||
if (ajaxRecords.length !== 1) submitBlockers.push('Expected exactly one DoInfoJH ajax submit attempt');
|
||
if (ajaxRecords[0]?.prevented_from_network) submitBlockers.push('DoInfoJH ajax was blocked before network because payload hash mismatched');
|
||
if (ajaxRecords[0] && !ajaxRecords[0].completed) submitBlockers.push('DoInfoJH ajax did not complete before timeout');
|
||
if (ajaxRecords[0]?.response_contains_login_timeout) submitBlockers.push('DoInfoJH response looks like login timeout');
|
||
if (ajaxRecords[0]?.response_contains_permission_text) submitBlockers.push('DoInfoJH response contains permission text');
|
||
const erpReceipt = alerts.map((alert) => alert.erp_receipt).find(Boolean) || null;
|
||
|
||
return {
|
||
status: submitBlockers.length ? 'live_submit_uncertain_or_failed' : 'live_submit_completed',
|
||
page: { url: location.href, title: document.title, form_element_count: form.elements.length },
|
||
current_payload: { payload_sha256: currentHash, field_count: Array.from(params.keys()).length, value_redacted: true },
|
||
ajax_records: ajaxRecords,
|
||
alerts,
|
||
erp_receipt: erpReceipt,
|
||
blockers: submitBlockers,
|
||
ui_cleanup: {
|
||
immediate_loading_cleanup: immediateLoadingCleanup,
|
||
deferred_dialog_callback_count: deferredDialogCallbacks.length,
|
||
deferred_dialog_callbacks_suppressed: deferredDialogCallbacks.length > 0,
|
||
followup_loading_cleanup_scheduled: true
|
||
},
|
||
submit_safety: {
|
||
live_submit_attempted: ajaxRecords.some((record) => record.live_submit_attempted),
|
||
approved_payload_sha256_matched: currentHash === expectedPayloadSha256 || ajaxRecords[0]?.request_payload_sha256 === expectedPayloadSha256
|
||
}
|
||
};
|
||
}
|
||
|
||
async function verifyOrderMarker({ marker, dateFrom, dateTo } = {}) {
|
||
const body = new URLSearchParams({
|
||
Act: 'JH_OrderList',
|
||
Tpage: '1',
|
||
P_Size: '50',
|
||
riqi: 'chufari',
|
||
S_chufariqi: dateYyyyMD(dateFrom || ''),
|
||
S_chufarizhi: dateYyyyMD(dateTo || dateFrom || ''),
|
||
S_fabudanwei: '老挝联泰',
|
||
S_tuanxuhao: marker || '',
|
||
S_kehuming: '',
|
||
S_chanpinming: '',
|
||
S_gendanren: '',
|
||
S_youkexinxi: '',
|
||
S_daoyou: '',
|
||
S_lingdui: '',
|
||
S_querenshu: '',
|
||
S_zhuanxianming: '',
|
||
S_zhuangtai: ''
|
||
});
|
||
const res = await fetch('/System/dat/orders.asp', {
|
||
method: 'POST',
|
||
credentials: 'same-origin',
|
||
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
|
||
body: body.toString()
|
||
});
|
||
const text = await res.text();
|
||
const markerOccurrences = marker ? Math.max(0, text.split(marker).length - 1) : 0;
|
||
return {
|
||
status: markerOccurrences ? 'order_marker_found' : 'order_marker_not_found',
|
||
request: {
|
||
endpoint: '/System/dat/orders.asp',
|
||
action: 'JH_OrderList',
|
||
date_range: { from: dateYyyyMD(dateFrom || ''), to: dateYyyyMD(dateTo || dateFrom || '') },
|
||
marker_redacted: false,
|
||
marker
|
||
},
|
||
response: {
|
||
http_status: res.status,
|
||
byte_length: new Blob([text]).size,
|
||
marker_occurrences: markerOccurrences,
|
||
contains_login_timeout_text: isLoginText(text),
|
||
contains_permission_text: /权限|permission/i.test(text),
|
||
looks_empty_or_no_match: /无数据|没有|暂无|empty/i.test(text) || markerOccurrences === 0,
|
||
value_redacted: true
|
||
},
|
||
submit_safety: {
|
||
live_submit_attempted: false,
|
||
verification_only: true
|
||
}
|
||
};
|
||
}
|
||
|
||
window.LTJTOrderAssistant = {
|
||
version: '0.2.14',
|
||
openOrderForm,
|
||
pingOrderFrame,
|
||
preflightRawInstruction,
|
||
liveSubmitApproved,
|
||
verifyOrderMarker,
|
||
returnToOrderList,
|
||
inspectOperationContext,
|
||
readOrderEditSnapshot,
|
||
createSplitParentLive,
|
||
createSplitChildLive,
|
||
exportConfirmationSources
|
||
};
|
||
})();
|