(function () {
'use strict';
const API = '/api/tbg-purchase';
const state = {
monitorPage: 1, monitorPages: 1, monitorSize: 50,
recordsPage: 1, recordsPages: 1, recordsSize: 50
};
const STATUS = {
pending: ['待下推', 'warn'], running: ['执行中', 'blue'], failed: ['失败', 'bad'],
success: ['成功', 'ok'], skipped: ['已跳过', 'warn']
};
const STEP = {
scan: '扫描待处理', push_requisition: '下推采购申请',
submit_requisition: '提交采购申请', audit_requisition: '审核采购申请',
push_purchase_order: '下推采购订单', update_purchase_order: '更新采购订单字段',
detected: '已发现', requisition_created: '采购申请已下推',
requisition_submitted: '采购申请已提交', requisition_audited: '采购申请已审核',
purchase_orders_created: '采购订单已下推', purchase_orders_configured: '采购订单字段已更新',
completed: '已完成'
};
function html(value) {
return String(value == null ? '' : value).replace(/[&<>"']/g, function (char) {
return {'&': '&', '<': '<', '>': '>', '"': '"', "'": '''}[char];
});
}
function text(value, fallback) { return html(value == null || value === '' ? (fallback == null ? '-' : fallback) : value); }
function notify(message) { if (typeof window.toast === 'function') window.toast(message); }
function canOperate(resource) { return typeof window.hasOperateResource !== 'function' || window.hasOperateResource(resource); }
function statusTag(value) {
const item = STATUS[value] || [value || '未知', 'warn'];
return '' + html(item[0]) + '';
}
function pageText(page, pages, total) { return '第 ' + page + ' / ' + pages + ' 页,共 ' + total + ' 条'; }
function selectedIds(bodyId) {
return Array.from(document.querySelectorAll('#' + bodyId + ' .tbg-row-select:checked')).map(function (item) { return Number(item.value); }).filter(Boolean);
}
function toggleAll(bodyId, checked) {
document.querySelectorAll('#' + bodyId + ' .tbg-row-select').forEach(function (item) { item.checked = checked; });
}
function installStyles() {
if (document.getElementById('tbgPurchaseStyles')) return;
const style = document.createElement('style');
style.id = 'tbgPurchaseStyles';
style.textContent = [
'.tbg-metrics{display:grid;grid-template-columns:repeat(5,minmax(120px,1fr));gap:10px;margin-bottom:12px}',
'.tbg-metric{background:#fff;border:1px solid var(--border);padding:12px 14px;border-radius:4px}',
'.tbg-metric span{display:block;color:var(--muted);font-size:12px}.tbg-metric strong{display:block;font-size:22px;margin-top:4px;color:#172033}',
'.tbg-toolbar{display:grid;grid-template-columns:minmax(140px,180px) minmax(220px,1fr) auto;gap:12px;align-items:end}',
'.tbg-status{display:flex;gap:12px;align-items:center;flex-wrap:wrap;font-size:13px;margin:8px 0 12px}',
'.tbg-actions{display:flex;gap:8px;justify-content:flex-end;align-items:center;flex-wrap:wrap}',
'.tbg-config-actions{display:flex;gap:8px;justify-content:flex-end;margin-bottom:12px}',
'.tbg-message{max-width:320px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
'.tbg-detail-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:1px;background:var(--border);border:1px solid var(--border)}',
'.tbg-detail-grid>div{background:#fff;padding:12px}.tbg-detail-grid small{display:block;color:var(--muted);margin-bottom:5px}',
'.tbg-secret{font-family:Consolas,monospace}',
'.tbg-org-table input{min-width:130px}.tbg-org-empty{text-align:center;color:var(--muted);padding:18px}',
'@media(max-width:1100px){.tbg-metrics{grid-template-columns:repeat(2,1fr)}.tbg-toolbar{grid-template-columns:1fr}.tbg-detail-grid{grid-template-columns:repeat(2,1fr)}}'
].join('');
document.head.appendChild(style);
}
function installNavigation() {
const side = document.querySelector('.side');
if (!side || document.getElementById('navTbgPurchase')) return;
const group = document.createElement('div');
group.className = 'navgroup';
group.id = 'navTbgPurchase';
group.innerHTML = '🧾 TBG采购下推▸';
const sub = document.createElement('div');
sub.className = 'navsub';
sub.id = 'navTbgPurchaseSub';
sub.innerHTML = [
'
TBG采购下推监控
',
'TBG下推记录
',
'TBG任务配置
'
].join('');
const before = document.getElementById('navSystem');
side.insertBefore(group, before || null);
side.insertBefore(sub, before || null);
group.onclick = function () {
const open = sub.style.display === 'block';
sub.style.display = open ? 'none' : 'block';
group.classList.toggle('open', !open);
group.querySelector('.caret').textContent = open ? '▸' : '▾';
};
sub.querySelector('[data-s="tbg-purchase-monitor"]').onclick = function () {
openPage('tbg-purchase-monitor', 'TBG采购下推监控'); loadMonitor();
};
sub.querySelector('[data-s="tbg-purchase-records"]').onclick = function () {
openPage('tbg-purchase-records', 'TBG下推记录'); loadRecords();
};
sub.querySelector('[data-s="tbg-purchase-config"]').onclick = function () {
openPage('tbg-purchase-config', 'TBG任务配置'); loadConfig();
};
}
function installPages() {
const main = document.querySelector('main.main');
if (!main || document.getElementById('tbg-purchase-monitor')) return;
main.insertAdjacentHTML('beforeend', `
TBG采购下推监控
监控销售订单下推采购申请、审核、采购订单生成及字段更新。
TBG下推记录
查询订单执行结果、各阶段返回信息和历史操作记录。
| 销售订单编号 | 状态 | 当前步骤 | 采购申请 | 采购订单数 | 执行次数 | 执行信息 | 发现时间 | 完成时间 |
|---|
点击查询查看记录 |
`);
bindEvents();
}
function openPage(id, heading) {
document.querySelectorAll('.nav,.navchild,.navgrandchild').forEach(function (item) { item.classList.remove('active'); });
const nav = document.querySelector('[data-s="' + id + '"]'); if (nav) nav.classList.add('active');
document.querySelectorAll('.section').forEach(function (item) { item.classList.remove('active'); });
const section = document.getElementById(id); if (section) section.classList.add('active');
const title = document.getElementById('title'); if (title) title.textContent = heading;
if (typeof window.activateWorkspacePage === 'function') window.activateWorkspacePage(id);
}
function bindEvents() {
document.getElementById('tbgRefresh').onclick = loadMonitor;
document.getElementById('tbgScan').onclick = scan;
document.getElementById('tbgExecute').onclick = function () { execute(selectedIds('tbgMonitorBody')); };
document.getElementById('tbgRun').onclick = runNow;
document.getElementById('tbgMonitorQuery').onclick = function () { state.monitorPage = 1; loadMonitorRows(); };
document.getElementById('tbgMonitorAll').onchange = function () { toggleAll('tbgMonitorBody', this.checked); };
document.getElementById('tbgMonitorPrev').onclick = function () { if (state.monitorPage > 1) { state.monitorPage--; loadMonitorRows(); } };
document.getElementById('tbgMonitorNext').onclick = function () { if (state.monitorPage < state.monitorPages) { state.monitorPage++; loadMonitorRows(); } };
document.getElementById('tbgMonitorSize').onchange = function () { state.monitorSize = Number(this.value) || 50; state.monitorPage = 1; loadMonitorRows(); };
document.getElementById('tbgRecordsRefresh').onclick = loadRecords;
document.getElementById('tbgRecordsQuery').onclick = function () { state.recordsPage = 1; loadRecords(); };
document.getElementById('tbgRecordsPrev').onclick = function () { if (state.recordsPage > 1) { state.recordsPage--; loadRecords(); } };
document.getElementById('tbgRecordsNext').onclick = function () { if (state.recordsPage < state.recordsPages) { state.recordsPage++; loadRecords(); } };
document.getElementById('tbgRecordsSize').onchange = function () { state.recordsSize = Number(this.value) || 50; state.recordsPage = 1; loadRecords(); };
document.getElementById('tbgSaveConfig').onclick = saveConfig;
document.getElementById('tbgTestKingdee').onclick = function () { testConfig('/config/test-kingdee', '金蝶API'); };
document.getElementById('tbgAddOrgMapping').onclick = function () { addOrgMapping({}); };
}
function recordRow(row, selectable) {
const link = '' + text(row.source_bill_no || row.source_order_id) + '';
const selection = selectable ? ' | ' : '';
const action = row.status === 'success' || row.status === 'running' || !canOperate('tbg_purchase.monitor') ? '-' : '';
const message = row.error_message || row.message || '';
if (selectable) return '' + selection + '| ' + link + ' | ' + Number(row.entry_count || 0) + ' | ' + statusTag(row.status) + ' | ' + text(STEP[row.current_step] || row.current_step) + ' | ' + text(row.requisition_bill_no || row.requisition_id) + ' | ' + (row.purchase_order_numbers || []).map(function (item) { return text(item); }).join(' ') + ' | ' + text(message) + ' | ' + text(row.last_attempt_at) + ' | ' + action + ' |
';
return '| ' + link + ' | ' + statusTag(row.status) + ' | ' + text(STEP[row.current_step] || row.current_step) + ' | ' + text(row.requisition_bill_no || row.requisition_id) + ' | ' + Number(row.purchase_order_count || 0) + ' | ' + Number(row.attempt_count || 0) + ' | ' + text(message) + ' | ' + text(row.detected_at) + ' | ' + text(row.completed_at) + ' |
';
}
async function loadSummary() {
try {
const data = await apiGet(API + '/summary');
const labels = {pending: '待下推', running: '执行中', failed: '失败', success: '成功', skipped: '已跳过'};
document.getElementById('tbgMetrics').innerHTML = Object.keys(labels).map(function (key) { return '' + labels[key] + '' + Number(data.counts[key] || 0) + '
'; }).join('');
document.getElementById('tbgStatus').innerHTML = '' + (data.task_enabled ? '自动任务已启用' : '自动任务已停用') + '最近扫描:' + text(data.last_scan_at, '尚未扫描') + '最近执行:' + text(data.last_run_at, '尚未执行') + '' + text(data.last_run_message) + '';
} catch (error) { document.getElementById('tbgStatus').innerHTML = '读取失败' + html(error.message) + ''; }
}
async function loadMonitorRows() {
const status = document.getElementById('tbgMonitorStatus').value;
const bill = document.getElementById('tbgMonitorBill').value.trim();
let url = API + '/records?status=' + encodeURIComponent(status) + '&page=' + state.monitorPage + '&page_size=' + state.monitorSize;
if (bill) url += '&bill_no=' + encodeURIComponent(bill);
try {
const data = await apiGet(url); state.monitorPages = Math.max(1, Math.ceil(data.total / data.page_size)); state.monitorPage = data.page;
document.getElementById('tbgMonitorBody').innerHTML = (data.rows || []).map(function (row) { return recordRow(row, true); }).join('') || '没有匹配记录 |
';
document.getElementById('tbgMonitorPage').textContent = pageText(state.monitorPage, state.monitorPages, data.total || 0);
document.getElementById('tbgMonitorAll').checked = false;
} catch (error) { document.getElementById('tbgMonitorBody').innerHTML = '加载失败:' + html(error.message) + ' |
'; }
applyTbgPermissions();
}
async function loadMonitor() { await Promise.all([loadSummary(), loadMonitorRows()]); }
async function loadRecords() {
const status = document.getElementById('tbgRecordsStatus').value;
const bill = document.getElementById('tbgRecordsBill').value.trim();
let url = API + '/records?status=' + encodeURIComponent(status) + '&page=' + state.recordsPage + '&page_size=' + state.recordsSize;
if (bill) url += '&bill_no=' + encodeURIComponent(bill);
try {
const data = await apiGet(url); state.recordsPages = Math.max(1, Math.ceil(data.total / data.page_size)); state.recordsPage = data.page;
document.getElementById('tbgRecordsBody').innerHTML = (data.rows || []).map(function (row) { return recordRow(row, false); }).join('') || '没有匹配记录 |
';
document.getElementById('tbgRecordsPage').textContent = pageText(state.recordsPage, state.recordsPages, data.total || 0);
} catch (error) { document.getElementById('tbgRecordsBody').innerHTML = '加载失败:' + html(error.message) + ' |
'; }
}
async function scan() {
const button = document.getElementById('tbgScan'); button.disabled = true;
try { const result = await apiPost(API + '/scan', {}); notify(result.message); await loadMonitor(); }
catch (error) { notify('扫描失败:' + error.message); } finally { button.disabled = false; }
}
async function execute(ids) {
if (!ids.length) return notify('请先勾选要执行的订单');
if (!confirm('确认执行选中的 ' + ids.length + ' 张TBG销售订单吗?')) return;
try { const result = await apiPost(API + '/execute', {record_ids: ids}); notify(result.message); await loadMonitor(); }
catch (error) { notify('执行失败:' + error.message); }
}
async function retry(recordId, button) {
button.disabled = true;
try { const result = await apiPost(API + '/execute', {record_ids: [recordId]}); notify(result.message); await loadMonitor(); }
catch (error) { notify('重推失败:' + error.message); } finally { button.disabled = false; }
}
async function runNow() {
if (!confirm('确认立即扫描并执行全部待下推及失败订单吗?')) return;
const button = document.getElementById('tbgRun'); button.disabled = true;
try { const result = await apiPost(API + '/run', {}); notify(result.message); await loadMonitor(); }
catch (error) { notify('任务执行失败:' + error.message); } finally { button.disabled = false; }
}
async function openRecord(recordId) {
openPage('tbg-purchase-records', 'TBG下推记录');
try {
const data = await apiGet(API + '/records/' + recordId);
const orders = Object.keys(data.purchase_orders || {}).map(function (supplier) { const row = data.purchase_orders[supplier]; return '| ' + text(supplier) + ' | ' + text(row.purchase_order_no || row.purchase_order_id) + ' | ' + text(row.purchase_org_number) + ' | ' + (row.configured ? '已更新' : '待更新') + ' |
'; }).join('') || '暂无采购订单 |
';
const logs = (data.logs || []).map(function (row) { return '| ' + text(row.created_at) + ' | ' + text(STEP[row.step] || row.step) + ' | ' + statusTag(row.status === 'failed' ? 'failed' : row.status === 'skipped' ? 'skipped' : 'success') + ' | ' + text(row.operator_name) + ' | ' + text(row.message) + ' |
'; }).join('') || '暂无执行日志 |
';
document.getElementById('tbgRecordDetail').innerHTML = '' + text(data.source_bill_no || data.source_order_id) + '
' + (data.status !== 'success' && canOperate('tbg_purchase.monitor') ? '
' : '') + '
状态' + statusTag(data.status) + '
当前步骤' + text(STEP[data.current_step] || data.current_step) + '
采购申请' + text(data.requisition_bill_no || data.requisition_id) + '
执行次数' + Number(data.attempt_count || 0) + '
待下推明细' + Number((data.source_entry_ids || []).length) + '
最近执行' + text(data.last_attempt_at) + '
完成时间' + text(data.completed_at) + '
执行信息' + text(data.error_message || data.message) + '
| 供应商内码 | 采购订单 | 采购组织 | 字段状态 |
' + orders + '
| 时间 | 步骤 | 结果 | 执行人 | 返回信息 |
' + logs + '
';
} catch (error) { notify('读取记录失败:' + error.message); }
}
function configPayload() {
const orgMappings = Array.from(document.querySelectorAll('#tbgOrgMappings tr[data-org-mapping]')).map(function (row) {
return {
purchase_org_number: row.querySelector('[data-field="purchase_org_number"]').value.trim(),
department_number: row.querySelector('[data-field="department_number"]').value.trim(),
group_number: row.querySelector('[data-field="group_number"]').value.trim(),
purchaser_number: row.querySelector('[data-field="purchaser_number"]').value.trim()
};
});
return {
kingdee_base_url: document.getElementById('tbgKingdeeUrl').value.trim(), kingdee_acct_id: document.getElementById('tbgKingdeeAcct').value.trim(),
kingdee_username: document.getElementById('tbgKingdeeUsername').value.trim(), kingdee_app_id: document.getElementById('tbgKingdeeAppId').value.trim(), kingdee_app_secret: document.getElementById('tbgKingdeeSecret').value,
kingdee_lcid: Number(document.getElementById('tbgKingdeeLcid').value) || 2052, timeout: Number(document.getElementById('tbgTimeout').value) || 60,
task_enabled: Number(document.getElementById('tbgTaskEnabled').value), interval_minutes: Number(document.getElementById('tbgInterval').value) || 30,
source_since: document.getElementById('tbgSourceSince').value, sales_group: document.getElementById('tbgSalesGroup').value.trim(),
category_names: document.getElementById('tbgCategories').value.split(/[,,]/).map(function (x) { return x.trim(); }).filter(Boolean),
excluded_material_group_name: document.getElementById('tbgExcludedMaterial').value.trim(), org_mappings: orgMappings,
sales_to_req_rule_id: document.getElementById('tbgSalesRule').value.trim(), req_to_po_rule_id: document.getElementById('tbgPurchaseRule').value.trim()
};
}
function setValue(id, value) { const element = document.getElementById(id); if (element) element.value = value == null ? '' : value; }
function renderOrgMappingEmpty() {
const body = document.getElementById('tbgOrgMappings');
if (body && !body.querySelector('tr[data-org-mapping]')) body.innerHTML = '| 尚未配置采购组织字段映射 |
';
}
function addOrgMapping(mapping) {
const body = document.getElementById('tbgOrgMappings');
const empty = body.querySelector('.tbg-org-empty'); if (empty) empty.remove();
const row = document.createElement('tr'); row.dataset.orgMapping = '1';
row.innerHTML = ' | | | | | ';
row.querySelector('button').onclick = function () { row.remove(); renderOrgMappingEmpty(); };
body.appendChild(row);
}
function renderOrgMappings(mappings) {
const body = document.getElementById('tbgOrgMappings'); body.innerHTML = '';
(mappings || []).forEach(addOrgMapping); renderOrgMappingEmpty();
}
async function loadConfig() {
try {
const data = await apiGet(API + '/config');
setValue('tbgKingdeeUrl', data.kingdee_base_url); setValue('tbgKingdeeAcct', data.kingdee_acct_id); setValue('tbgKingdeeUsername', data.kingdee_username); setValue('tbgKingdeeAppId', data.kingdee_app_id); setValue('tbgKingdeeSecret', data.kingdee_app_secret); setValue('tbgKingdeeLcid', data.kingdee_lcid || 2052); setValue('tbgTimeout', data.timeout || 60);
setValue('tbgTaskEnabled', Number(data.task_enabled || 0)); setValue('tbgInterval', data.interval_minutes || 30); setValue('tbgSourceSince', data.source_since || '2022-03-01'); setValue('tbgSalesGroup', data.sales_group || 'TBG'); setValue('tbgCategories', (data.category_names || ['PD I', 'PD II', 'PD III']).join(',')); setValue('tbgExcludedMaterial', data.excluded_material_group_name || '费用'); setValue('tbgSalesRule', data.sales_to_req_rule_id); setValue('tbgPurchaseRule', data.req_to_po_rule_id); renderOrgMappings(data.org_mappings || []);
document.getElementById('tbgConfigResult').textContent = '';
} catch (error) { notify('加载TBG配置失败:' + error.message); }
}
async function saveConfig() {
try { const result = await apiPut(API + '/config', configPayload()); notify('TBG任务配置已保存'); await loadConfig(); return result; }
catch (error) { notify('保存失败:' + error.message); throw error; }
}
async function testConfig(path, label) {
const target = document.getElementById('tbgConfigResult');
try { await saveConfig(); const result = await apiPost(API + path, {}); target.innerHTML = '' + html(label) + '连接成功' + text(result.message) + ''; }
catch (error) { target.innerHTML = '' + html(label) + '连接失败' + html(error.message) + ''; }
}
function applyTbgPermissions() {
const resources = {
'tbg-purchase-monitor': 'tbg_purchase.monitor',
'tbg-purchase-records': 'tbg_purchase.records',
'tbg-purchase-config': 'tbg_purchase.config'
};
Object.keys(resources).forEach(function (id) {
const allowed = typeof window.hasResource !== 'function' || window.hasResource(resources[id]);
const nav = document.querySelector('[data-s="' + id + '"]'); if (nav) nav.style.display = allowed ? '' : 'none';
const section = document.getElementById(id); if (section && !allowed) section.classList.remove('active');
});
document.querySelectorAll('[data-resource-action^="tbg_purchase."]').forEach(function (button) { button.style.display = canOperate(button.dataset.resourceAction) ? '' : 'none'; });
const sub = document.getElementById('navTbgPurchaseSub'), group = document.getElementById('navTbgPurchase');
if (sub && group) group.style.display = Array.from(sub.querySelectorAll('[data-s]')).some(function (item) { return item.style.display !== 'none'; }) ? '' : 'none';
}
function installPermissions() {
if (window._tbgPermissionWrapped || typeof window.applyNavPermissions !== 'function') return;
const original = window.applyNavPermissions;
window.applyNavPermissions = function () { original(); applyTbgPermissions(); };
window._tbgPermissionWrapped = true;
}
window.openTbgRecord = openRecord;
window.retryTbgRecord = retry;
function init() {
installStyles(); installNavigation(); installPages(); installPermissions(); applyTbgPermissions();
if (typeof window.applyNavPermissions === 'function') window.applyNavPermissions();
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); else init();
})();