/* Shopify Payouts extensions: mapping pages, push controls and mapping status. */ (function () { 'use strict'; const escHtml = (v) => (window.esc ? window.esc(v) : String(v == null ? '' : v).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]))); const val = (id) => (document.getElementById(id) || {}).value || ''; const notify = (msg) => window.toast ? window.toast(msg) : alert(msg); let payoutPageRows = []; function stores() { return apiGet('/api/shopify/stores').then(r => Array.isArray(r) ? r : (r.stores || [])); } function storeOptions(rows) { return '' + rows.map(s => '').join(''); } async function populateStoreOptions(ids) { const rows = await stores(); ids.forEach(id => { const el = document.getElementById(id); if (!el) return; const current = el.value; el.innerHTML = storeOptions(rows); if (rows.some(row => row.code === current)) el.value = current; }); return rows; } const payoutNavResources = { 'shopify-payout-detail': 'shopify.payout_detail', 'shopify-payout-raw': 'shopify.payout_raw', 'shopify-payout-mapping': 'shopify.payout_mapping', 'shopify-payout-field-map': 'shopify.payout_field_mapping' }; function addNav(id, label) { const sub = document.getElementById('navShopifySub'); if (!sub || sub.querySelector('[data-s="' + id + '"]')) return; const n = document.createElement('div'); n.className = 'nav navchild'; n.dataset.s = id; n.dataset.resource = payoutNavResources[id] || 'shopify'; n.textContent = label; n.onclick = () => window.showSection(id); sub.appendChild(n); } function installPayoutPermissions() { if (window._shopifyPayoutPermissionWrapped || typeof window.applyNavPermissions !== 'function') return; const original = window.applyNavPermissions; window.applyNavPermissions = function () { original(); Object.keys(payoutNavResources).forEach(id => { const resource = payoutNavResources[id]; const allowed = typeof window.hasResource !== 'function' || window.hasResource(resource); const nav = document.querySelector('#navShopifySub [data-s="' + id + '"]'); if (nav) nav.style.display = allowed ? '' : 'none'; const section = document.getElementById(id); if (section && !allowed) section.classList.remove('active'); }); const sub = document.getElementById('navShopifySub'); const group = document.getElementById('navShopify'); if (sub && group) { const visible = Array.prototype.some.call( sub.querySelectorAll('.nav[data-s]'), item => item.style.display !== 'none' ); group.style.display = visible ? '' : 'none'; } }; window._shopifyPayoutPermissionWrapped = true; } function payoutTypeHint(type) { const raw = canonicalType(type); const hints = { reserve: 'Reserved Funds(页面名称)', dispute: 'Chargeback / Chargeback Hold Release(按金额正负显示)', debit: 'Debit;若 source_type=adjustment 且 adjustment_reason=chargeback_fee,页面显示 Chargeback Fee', credit: 'Credit;若 source_type=adjustment 且 adjustment_reason=chargeback_fee,页面显示 Chargeback Fee Refund', adjustment: 'Adjustment(具体名称取决于 adjustment_reason)', charge: 'Charge', refund: 'Refund', payout_failure: 'Payout failure', refund_failure: 'Refund failure', payout: 'Payout(汇总行,通常不参与明细映射)', }; return hints[raw] || ''; } function canonicalType(type) { const raw = String(type || '').trim().toLowerCase().replace(/[- ]/g, '_'); return ({ reserved_funds: 'reserve', chargeback: 'dispute', chargeback_hold_release: 'dispute', chargeback_fee: 'debit', chargeback_fee_refund: 'credit' })[raw] || raw; } async function refreshPayoutTypeFilter(storeCode) { const select = document.getElementById('shopifyPayoutDetailTypeFilter'); if (!select || !storeCode) return; const current = select.value; try { const rawTypes = (await apiGet('/api/shopify/payouts/types?store_code=' + encodeURIComponent(storeCode))).rows || []; const types = [...new Set(rawTypes.map(x => String(x || '').trim()).filter(Boolean))]; select.innerHTML = '' + types.map(type => '').join(''); if (types.includes(current)) select.value = current; } catch (e) { // The detail request itself remains usable if the optional filter refresh fails. } } function mappingPageHtml() { return '

Payouts 映射关系

全部 Shopify 店铺共用一套原始 API Type + 实际非零金额类型映射。

原始 API Type系统类型金额类型金蝶费用项编码费用项字段(可多个)金额字段(可多个)启用操作
'; } function fieldPageHtml() { return `

金蝶单据字段映射

按结算单字段映射界面维护 Payouts 单据头和分录字段。

说明:区分单据头/分录;取值方式可选源列、固定值或金蝶自动生成;基础资料字段会自动按编码封装。分录 Key 与父分录 Key 的填写方式与“结算单字段映射”一致。
金蝶字段标识金蝶名称字段层级取值方式取值(来源列/固定值)分录 Key父分录 Key基础资料启用操作
`; } function rawPageHtml() { return '

Payouts API 原始明细

仅展示 Shopify API 原始返回的 balance transaction,不参与现有业务归类或金蝶推送。

原始 Type 用于一比一判断和金蝶映射;例如 reserve、debit、dispute 不会改名。数据来源为已同步到本地的 API 报文。
Processed atPayout IDTransaction ID原始 TypeSource typeAdjustment reasonSource order IDAmountFeeNetCurrency
每页条
'; } function addFieldRow(row) { row = row || {}; const tb = document.getElementById('spfTb'); const emptyRow = tb?.querySelector('.empty')?.closest('tr'); if (emptyRow) emptyRow.remove(); const tr = document.createElement('tr'); tr.innerHTML = ''; tr.querySelector('[data-k="field_scope"]').value = row.field_scope || 'entry'; tr.querySelector('[data-k="value_type"]').value = row.value_type || 'source'; tr.querySelector('button').onclick = () => tr.remove(); tb.appendChild(tr); } function collectFields() { return [...document.querySelectorAll('#spfTb tr')].map(tr => { const o = {}; tr.querySelectorAll('[data-k]').forEach(x => { o[x.dataset.k] = x.type === 'checkbox' ? (x.checked ? 1 : 0) : x.value.trim(); }); if (o.value_type === 'const') { o.const_value = o.source_col; o.source_col = ''; } return o; }).filter(x => x.target_key); } async function loadFields() { const storeCode = val('spmFieldStore'); const tb = document.getElementById('spfTb'); if (!storeCode) { if (tb) tb.innerHTML = '
请选择店铺后查看对应字段映射
'; return; } const d = await apiGet('/api/shopify/payouts/field-mapping?store_code=' + encodeURIComponent(storeCode)); if (tb) { tb.innerHTML = ''; (d.rows || []).forEach(addFieldRow); if (!(d.rows || []).length) tb.innerHTML = '
当前店铺暂无字段映射,可从金蝶拉取或手动添加
'; } } async function saveFields() { const storeCode = val('spmFieldStore'); if (!storeCode) return notify('请先选择要维护字段映射的店铺'); await apiPost('/api/shopify/payouts/field-mapping', { store_code: storeCode, mappings: collectFields() }); notify('已保存 ' + (document.getElementById('spmFieldStore')?.selectedOptions[0]?.textContent || storeCode) + ' 的字段映射'); await loadFields(); } async function pullFields() { const storeCode = val('spmFieldStore'); if (!storeCode) return notify('请先选择要维护字段映射的店铺'); try { const d = await apiGet('/api/shopify/payouts/kingdee-fields'); if (!d.ok) throw new Error(d.message || '拉取失败'); const tb = document.getElementById('spfTb'); if (tb) tb.innerHTML = ''; (d.fields || []).forEach(f => addFieldRow({ target_key: f.key, target_name: f.name || '', field_scope: f.scope === 'head' ? 'head' : 'entry', entry_key: f.entry_key || 'FEntity', parent_entry_key: f.parent_entry_key || '', value_type: 'source', enabled: 1 })); notify('已为当前店铺加载 ' + (d.fields || []).length + ' 个金蝶字段,确认后请保存映射'); } catch (e) { notify('拉取金蝶字段失败:' + e.message); } } async function refreshFieldCopyTargets() { const source = val('spmFieldStore'), target = document.getElementById('spmCopyTarget'); if (!target) return; const current = target.value; const rows = (await stores()).filter(row => row.code !== source); target.innerHTML = '' + rows.map(row => '').join(''); if (rows.some(row => row.code === current)) target.value = current; } async function copyFields() { const source = val('spmFieldStore'), target = val('spmCopyTarget'); if (!source) return notify('请先选择已有映射的源店铺'); if (!target) return notify('请选择要接收映射的目标店铺'); const sourceName = document.getElementById('spmFieldStore')?.selectedOptions[0]?.textContent || source, targetName = document.getElementById('spmCopyTarget')?.selectedOptions[0]?.textContent || target; if (!confirm('确认用 ' + sourceName + ' 已保存的字段映射覆盖 ' + targetName + ' 当前映射?')) return; const result = await apiPost('/api/shopify/payouts/field-mapping/copy', { source_store_code: source, target_store_code: target }); notify('已复制 ' + Number(result.copied || 0) + ' 条字段映射到 ' + targetName); } async function openFieldMappingPage() { try { await populateStoreOptions(['spmFieldStore']); await refreshFieldCopyTargets(); await loadFields(); } catch (e) { notify('加载店铺字段映射失败:' + e.message); } } async function loadMappingFieldOptions() { const button = document.getElementById('spmKingdeeFields'); if (button) button.disabled = true; try { const d = await apiGet('/api/shopify/payouts/kingdee-fields'); if (!d.ok) throw new Error(d.message || '拉取失败'); const fields = (d.fields || []).filter(f => f.scope === 'entry'); const optionHtml = fields.map(f => '').join(''); const fee = document.getElementById('spmFeeFieldOptions'), amount = document.getElementById('spmAmountFieldOptions'); if (fee) fee.innerHTML = optionHtml; if (amount) amount.innerHTML = optionHtml; notify('已加载 ' + fields.length + ' 个金蝶分录字段;多个目标字段请用逗号分隔'); } catch (e) { notify('加载金蝶字段失败:' + e.message); } finally { if (button) button.disabled = false; } } function addMappingRow(row) { row = row || {}; const tr = document.createElement('tr'); tr.dataset.id = row.id || ''; const rawType = String(row.transaction_type || ''); const systemType = payoutTypeHint(rawType) || rawType || '-'; tr.innerHTML = '' + escHtml(systemType) + ''; tr.querySelector('[data-k="amount_field"]').value = row.amount_field || 'amount'; tr.querySelector('button').onclick = async () => { if (tr.dataset.id) { try { await apiDelete('/api/shopify/payouts/mapping/' + tr.dataset.id); } catch (e) { notify(e.message); return; } } tr.remove(); }; document.getElementById('spmTb').appendChild(tr); } function collectMappings() { return [...document.querySelectorAll('#spmTb tr')].map(tr => { const o = {}; tr.querySelectorAll('[data-k]').forEach(x => { o[x.dataset.k] = x.type === 'checkbox' ? (x.checked ? 1 : 0) : x.value.trim(); }); return o; }); } function updateMappingSummary(sourceTypeCount, addedRowCount, combinationCount) { const rows = [...document.querySelectorAll('#spmTb tr')]; const typeCount = new Set(rows.map(tr => valFrom(tr, 'transaction_type').trim()).filter(Boolean)).size; const el = document.getElementById('spmTypeSummary'); if (!el) return; if (Number.isFinite(sourceTypeCount)) { el.textContent = '全部店铺合计 ' + sourceTypeCount + ' 个原始 Type、' + combinationCount + ' 个非零金额组合;当前显示 ' + typeCount + ' 个原始 Type、' + rows.length + ' 条共用映射,本次新增 ' + addedRowCount + ' 条'; } else { el.textContent = '当前已保存 ' + typeCount + ' 个原始 Type、' + rows.length + ' 条共用映射'; } } async function loadMappings() { const d = await apiGet('/api/shopify/payouts/mapping?store_code=' + encodeURIComponent(val('spmStore'))); const tb = document.getElementById('spmTb'); if (tb) { tb.innerHTML = ''; (d.rows || []).forEach(addMappingRow); updateMappingSummary(); } } async function saveMappings() { await apiPost('/api/shopify/payouts/mapping', { store_code: '', rows: collectMappings() }); notify('共用映射已保存'); await loadMappings(); } async function loadTypes() { const button = document.getElementById('spmTypes'); if (button) button.disabled = true; try { const data = await apiGet('/api/shopify/payouts/types'); const types = [...new Set((data.rows || []).map(type => String(type || '').trim()).filter(Boolean))]; const combinations = data.combinations || []; const existing = new Set([...document.querySelectorAll('#spmTb tr')].map(tr => valFrom(tr, 'transaction_type').trim().toLowerCase() + '+' + valFrom(tr, 'amount_field'))); let added = 0; combinations.forEach(item => { const type = String(item.transaction_type || '').trim(), amount = String(item.amount_field || '').trim().toLowerCase(), key = type.toLowerCase() + '+' + amount; if (type && ['amount', 'fee', 'net'].includes(amount) && !existing.has(key)) { addMappingRow({ transaction_type: type, amount_field: amount, enabled: 1 }); existing.add(key); added += 1; } }); updateMappingSummary(types.length, added, combinations.length); notify('已合并检索全部店铺 ' + combinations.length + ' 个非零金额组合,新增 ' + added + ' 条共用映射'); } catch (e) { notify('合并获取有金额的 Type 失败:' + e.message); } finally { if (button) button.disabled = false; } } const rawState = { page: 1, size: 100, pages: 1 }; async function loadRawTypes() { const store = val('spRawStore'); const select = document.getElementById('spRawType'); if (!select || !store) return; const current = select.value; try { const d = await apiGet('/api/shopify/payouts/types?store_code=' + encodeURIComponent(store)); const types = [...new Set((d.rows || []).map(x => String(x || '').trim()).filter(Boolean))]; select.innerHTML = '' + types.map(x => '').join(''); if (types.includes(current)) select.value = current; } catch (e) {} } async function renderRawTransactions() { const store = val('spRawStore'), payoutId = val('spRawPayoutId').trim(), type = val('spRawType'); let u = '/api/shopify/payouts/raw-transactions?page=' + rawState.page + '&page_size=' + rawState.size; if (store) u += '&store_code=' + encodeURIComponent(store); if (payoutId) u += '&payout_id=' + encodeURIComponent(payoutId); if (type) u += '&transaction_type=' + encodeURIComponent(type); try { const d = await apiGet(u); rawState.pages = Math.max(1, Math.ceil(Number(d.total || 0) / Number(d.page_size || rawState.size))); const tb = document.getElementById('spRawTb'); if (tb) tb.innerHTML = (d.rows || []).map(r => '' + escHtml(r.processed_at || '-') + '' + escHtml(r.payout_id) + '' + escHtml(r.transaction_id) + '' + escHtml(r.transaction_type || '-') + '' + escHtml(r.source_type || '-') + '' + escHtml(r.adjustment_reason || '-') + '' + escHtml(r.source_order_id || '-') + '' + money(r.amount) + '' + money(r.fee) + '' + money(r.net) + '' + escHtml(r.currency || '-') + '').join('') || '
暂无原始 API 明细,请先同步 Payout
'; const info = document.getElementById('spRawPageInfo'); if (info) info.textContent = '第 ' + d.page + ' / ' + rawState.pages + ' 页,共 ' + d.total + ' 条'; } catch (e) { notify('加载 API 原始明细失败:' + e.message); } } async function syncRawPayout() { const store = val('spRawStore'), payoutId = val('spRawPayoutId').trim(); if (!store || !payoutId || !/^\d+$/.test(payoutId)) return notify('请选择店铺并填写有效的 Payout ID'); try { const r = await apiPost('/api/shopify/payouts/sync', { store_codes: [store], payout_id: Number(payoutId), mode: 'sync' }); notify(r.accepted ? '已提交该 Payout 的 API 原始同步任务' : (r.message || '已有任务正在运行')); } catch (e) { notify('提交 API 同步失败:' + e.message); } } function setupRawPage() { const main = document.querySelector('main.main') || document.querySelector('.main') || document.body; if (!document.getElementById('shopify-payout-raw')) main.insertAdjacentHTML('beforeend', rawPageHtml()); const bind = (id, ev, fn) => { const el = document.getElementById(id); if (el && !el.dataset.bound) { el.dataset.bound = '1'; el.addEventListener(ev, fn); } }; bind('spRawQuery', 'click', () => { rawState.page = 1; renderRawTransactions(); }); bind('spRawSync', 'click', syncRawPayout); bind('spRawPrev', 'click', () => { rawState.page = Math.max(1, rawState.page - 1); renderRawTransactions(); }); bind('spRawNext', 'click', () => { rawState.page = Math.min(rawState.pages, rawState.page + 1); renderRawTransactions(); }); bind('spRawPageSize', 'change', () => { rawState.size = Number(val('spRawPageSize')) || 100; rawState.page = 1; renderRawTransactions(); }); } function valFrom(tr, key) { const x = tr.querySelector('[data-k="' + key + '"]'); return x ? x.value : ''; } // Keep the original global handlers available for existing buttons/bookmarks. window.addSpmRow = addMappingRow; window.loadSpm = loadMappings; window.saveSpm = saveMappings; window.addSpfRow = addFieldRow; window.saveSpf = saveFields; window.pullShopifyKingdeeFields = pullFields; window.refreshSpmTypes = loadTypes; function setupPages() { addNav('shopify-payout-detail', 'Payouts 打款记录明细'); addNav('shopify-payout-raw', 'Payouts API 原始明细'); addNav('shopify-payout-mapping', '映射关系'); addNav('shopify-payout-field-map', '金蝶单据字段映射'); const main = document.querySelector('main.main') || document.querySelector('.main') || document.body; if (!document.getElementById('shopify-payout-mapping')) main.insertAdjacentHTML('beforeend', mappingPageHtml()); if (!document.getElementById('shopify-payout-field-map')) main.insertAdjacentHTML('beforeend', fieldPageHtml()); setupRawPage(); const bind = (id, ev, fn) => { const el = document.getElementById(id); if (el && !el.dataset.bound) { el.dataset.bound = '1'; el.addEventListener(ev, fn); } }; bind('spmLoad', 'click', loadMappings); bind('spmSave', 'click', saveMappings); bind('spmTypes', 'click', loadTypes); bind('spmKingdeeFields', 'click', loadMappingFieldOptions); bind('spmAdd', 'click', () => addMappingRow()); bind('spmFieldStore', 'change', () => { loadFields().catch(e => notify('切换店铺字段映射失败:' + e.message)); refreshFieldCopyTargets().catch(e => notify('加载目标店铺失败:' + e.message)); }); bind('spmCopyFields', 'click', () => copyFields().catch(e => notify('复制字段映射失败:' + e.message))); bind('spmPullFields', 'click', pullFields); bind('spmSaveFields', 'click', saveFields); bind('spmSaveFieldsBottom', 'click', saveFields); bind('spmAddField', 'click', () => addFieldRow()); installPayoutPermissions(); if (typeof window.applyNavPermissions === 'function') window.applyNavPermissions(); } function ensurePayoutHeader() { const table = document.querySelector('#shopifyPayoutTb')?.closest('table'); if (!table) return; table.querySelector('thead tr').innerHTML = '店铺单据编号Payout date交易日期StatusAmountCurrency同步时间推送状态金蝶单据号单据内码推送信息推送金蝶'; const all = document.getElementById('spPayoutAll'); if (all) all.onchange = () => document.querySelectorAll('.sp-payout-row-check').forEach(x => x.checked = all.checked); const sec = document.getElementById('shopify-payouts'); const title = sec?.querySelector('.section-title'); const syncStatus = document.getElementById('shopifyPayoutSyncStatus'); const actions = syncStatus?.parentElement || (title?.children.length > 1 ? title.lastElementChild : null); if (actions) actions.classList.add('section-actions', 'payout-actions'); let refresh = title?.querySelector('.sp-refresh-payouts'); if (title && !refresh) { refresh = document.createElement('button'); refresh.className = 'btn2 sp-refresh-payouts'; refresh.textContent = '刷新列表'; refresh.onclick = refreshPayoutsAndClearFailures; } if (refresh && actions) actions.insertBefore(refresh, syncStatus?.nextSibling || actions.firstChild); if (title && !title.querySelector('.sp-push-header')) { const b = document.createElement('button'); b.className = 'btn3 sp-push-header'; b.textContent = '推送金蝶'; b.onclick = pushSelectedPayouts; (actions || title).appendChild(b); } } function payoutPushStatusHtml(value) { const status = String(value || 'pending').trim().toLowerCase(); const labels = { pending: '待推送', success: '推送成功', partial: '部分推送', failed: '推送失败', skipped: '已跳过', pushing: '推送中', running: '推送中', error: '推送失败' }; const tone = status === 'success' ? 'success' : (status === 'failed' || status === 'error') ? 'failed' : status === 'partial' ? 'warning' : 'pending'; return '' + escHtml(labels[status] || value || '待推送') + ''; } function payoutActionHtml(r) { const store = escHtml(r.store_code), record = Number(r.id), documentNumber = escHtml(r.document_number || r.payout_id); const pushed = ['success', 'partial'].includes(String(r.push_status || '').toLowerCase()) || r.kingdee_bill_no || r.kingdee_id; if (pushed) return ''; return ''; } function payoutMessageHtml(message) { const full = String(message || '').trim(); if (!full) return '-'; const tooltip = full.replace(/\s+/g, ' '); return ''; } window.copyPayoutMessage = async function (button) { const message = button?.dataset?.message || ''; if (!message) return; try { if (!navigator.clipboard?.writeText) throw new Error('clipboard unavailable'); await navigator.clipboard.writeText(message); } catch (e) { const textarea = document.createElement('textarea'); textarea.value = message; textarea.style.position = 'fixed'; textarea.style.opacity = '0'; document.body.appendChild(textarea); textarea.select(); document.execCommand('copy'); textarea.remove(); } notify('完整推送信息已复制'); }; function payoutRow(r) { const tx = r.transaction_date_min && r.transaction_date_max ? r.transaction_date_min + ' ~ ' + r.transaction_date_max : '源数据无 processed_at'; const documentNumber = r.document_number || r.payout_id; return '' + escHtml(r.store_code) + '' + shopifyPayoutDocumentLink(r) + '' + escHtml(r.date || '') + '' + escHtml(tx) + '' + escHtml(window.shopifyPayoutStatusLabel ? shopifyPayoutStatusLabel(r.status) : r.status) + '' + (window.money ? money(r.amount) : r.amount) + '' + escHtml(r.currency) + '' + escHtml(r.sync_at || '') + '' + payoutPushStatusHtml(r.push_status) + '' + escHtml(r.kingdee_bill_no || '') + '' + escHtml(r.kingdee_id || '') + '' + payoutMessageHtml(r.push_message) + '' + payoutActionHtml(r) + ''; } window.renderShopifyPayouts = async function () { try { await ensureShopifyStores(); const f = st.shopifyPayoutFilters; let u = '/api/shopify/payouts?page=' + st.shopifyPayoutPage.page + '&page_size=' + st.shopifyPayoutPage.size; [['store_code', f.store], ['date_min', f.date_min], ['date_max', f.date_max], ['document_number', f.document_number], ['status', f.status && f.status !== 'all' ? f.status : '']].forEach(([k, v]) => { if (v) u += '&' + k + '=' + encodeURIComponent(v); }); const d = await apiGet(u); payoutPageRows = d.rows || []; ensurePayoutHeader(); document.getElementById('shopifyPayoutTb').innerHTML = payoutPageRows.map(payoutRow).join('') || '
暂无数据
'; document.getElementById('shopifyPayoutPageInfo').innerText = '第 ' + d.page + ' / ' + Math.max(1, Math.ceil(d.total / d.page_size)) + ' 页,共 ' + d.total + ' 条'; } catch (e) { notify('加载 Payouts 失败:' + e.message); } }; async function refreshPayoutsAndClearFailures() { const button = document.querySelector('#shopify-payouts .sp-refresh-payouts'); const ids = payoutPageRows .filter(row => ['failed', 'error'].includes(String(row.push_status || '').toLowerCase())) .map(row => Number(row.id)) .filter(Boolean); if (button) button.disabled = true; try { let cleared = 0; if (ids.length) { const result = await apiPost('/api/shopify/payouts/clear-failed-push-messages', { ids }); cleared = Number(result.cleared || 0); } await window.renderShopifyPayouts(); notify('列表已刷新' + (cleared ? '(已清理 ' + cleared + ' 条失败信息)' : ',当前页没有失败信息')); } catch (e) { notify('刷新列表失败:' + e.message); } finally { if (button) button.disabled = false; } } async function submitPayoutPush(store, record, transactionIds, force) { const body = { store_code: store, payout_record_id: Number(record), force: force ? 1 : 0 }; if (transactionIds) body.transaction_ids = transactionIds; return apiPost('/api/shopify/payouts/push', body); } window.pushShopifyPayout = async function (store, record, btn, documentNumber) { const label = documentNumber || record; if (!confirm('确认推送 Payout ' + label + ' 到金蝶?')) return; if (btn) btn.disabled = true; try { const r = await submitPayoutPush(store, record, null, false); notify((r.ok ? '' : label + ':') + (r.message || (r.ok ? '推送成功' : '推送失败'))); await window.renderShopifyPayouts(); } catch (e) { notify(label + ':' + e.message); } finally { if (btn) btn.disabled = false; } }; window.checkShopifyPayoutKingdee = async function (store, record, btn, documentNumber) { const label = documentNumber || record; if (btn) btn.disabled = true; try { const r = await apiPost('/api/shopify/payouts/push/' + Number(record) + '/check?store_code=' + encodeURIComponent(store), {}); notify(label + ':' + (r.message || (r.ok ? '检查完成' : '检查失败'))); await window.renderShopifyPayouts(); } catch (e) { notify(label + ':' + e.message); } finally { if (btn) btn.disabled = false; } }; async function pushSelectedPayouts() { const rows = [...document.querySelectorAll('.sp-payout-row-check:checked')]; if (!rows.length) return notify('请先勾选要推送的 Payout'); if (!confirm('确认将选中的 ' + rows.length + ' 条 Payout 推送到金蝶?')) return; const button = document.querySelector('#shopify-payouts .sp-push-header'); if (button) button.disabled = true; const results = []; try { for (const x of rows) { try { const r = await submitPayoutPush(x.dataset.store, x.dataset.record, null, false); results.push({ document: x.dataset.document || x.dataset.record, ok: !!r.ok, message: r.message || (r.ok ? '推送成功' : '推送失败') }); } catch (e) { results.push({ document: x.dataset.document || x.dataset.record, ok: false, message: e.message }); } } const success = results.filter(x => x.ok).length, failed = results.filter(x => !x.ok); notify('选中 ' + rows.length + ' 条,推送成功 ' + success + ' 条,未推送 ' + failed.length + ' 条' + (failed.length ? ';' + failed.map(x => x.document + ':' + x.message).join(';') : '')); await window.renderShopifyPayouts(); } finally { if (button) button.disabled = false; } } window.renderShopifyPayoutDetail = async function () { try { const d = st.shopifyPayoutDetail; await refreshPayoutTypeFilter(d.store_code); let u = '/api/shopify/payouts/' + d.payout_id + '/transactions?page=' + d.page + '&page_size=' + d.size + '&store_code=' + encodeURIComponent(d.store_code) + '&payout_record_id=' + d.payout_record_id; if (d.type) u += '&transaction_type=' + encodeURIComponent(d.type); const data = await apiGet(u); const table = document.querySelector('#shopifyPayoutDetailTb')?.closest('table'); if (table) table.querySelector('thead tr').innerHTML = 'DateOrder原始 API Type系统类型Payment methodAmountFeeNetCurrency映射状态(原始 Type+金额类型)'; document.getElementById('shopifyPayoutDetailTb').innerHTML = (data.rows || []).map(r => { const details = r.mapping_details || {}; const mapText = Object.keys(details).map(k => escHtml(k) + ':' + escHtml(details[k].status) + (details[k].message ? '(' + escHtml(details[k].message) + ')' : '')).join('
') || '未配置'; const displayType = shopifyTxTypeLabel(r); const rawType = String(r.transaction_type || ''); return '' + escHtml(shopifyDateOnly(r.processed_at)) + '' + shopifyOrderDisplay(r) + '' + escHtml(rawType || '-') + '' + escHtml(displayType || '-') + '' + escHtml(r.payment_method || '-') + '' + money(r.amount) + '' + money(-Number(r.fee || 0)) + '' + money(r.net) + '' + escHtml(r.currency || '') + '' + mapText + ''; }).join('') || '
暂无明细数据
'; const all = document.getElementById('spTxAll'); if (all) all.onchange = () => document.querySelectorAll('.spTxCheck').forEach(x => x.checked = all.checked); const sec = document.getElementById('shopify-payout-detail'), title = sec?.querySelector('.section-title'), actions = title?.children.length > 1 ? title.lastElementChild : null; if (title && !title.querySelector('.sp-push-selected')) { const b = document.createElement('button'); b.className = 'btn3 sp-push-selected'; b.textContent = '推送选中明细到金蝶'; b.onclick = async () => { const current = st.shopifyPayoutDetail; const ids = [...document.querySelectorAll('.spTxCheck:checked')].map(x => Number(x.dataset.id)).filter(Boolean); if (!ids.length) return notify('请至少选择一条明细'); if (!confirm('确认只推送当前勾选的 ' + ids.length + ' 条 Payout 明细到金蝶?')) return; b.disabled = true; try { const r = await submitPayoutPush(current.store_code, current.payout_record_id, ids, false); notify(r.message || (r.ok ? '明细推送成功' : '明细未推送')); if (r.ok) { await window.renderShopifyPayoutDetail(); await window.renderShopifyPayouts(); } } catch (e) { notify('明细推送失败:' + e.message); } finally { b.disabled = false; } }; if (actions) actions.insertBefore(b, actions.firstElementChild); else title.appendChild(b); } if (window.renderShopifyPayoutTotals) renderShopifyPayoutTotals(data); document.getElementById('shopifyPayoutDetailPageInfo').innerText = '第 ' + data.page + ' / ' + Math.max(1, Math.ceil(data.total / data.page_size)) + ' 页,共 ' + data.total + ' 条'; } catch (e) { notify('加载明细失败:' + e.message); } }; const originalShow = window.showSection; window.showSection = function (id) { setupPages(); if (id === 'shopify-payout-mapping' || id === 'shopify-payout-field-map' || id === 'shopify-payout-detail' || id === 'shopify-payout-raw') { document.querySelectorAll('.section').forEach(x => x.classList.remove('active')); const sec = document.getElementById(id); if (sec) sec.classList.add('active'); document.querySelectorAll('[data-s]').forEach(x => x.classList.toggle('active', x.dataset.s === id)); const title = document.getElementById('title'), desc = document.getElementById('desc'); if (title) title.innerText = id === 'shopify-payout-mapping' ? 'Payouts 映射关系' : id === 'shopify-payout-field-map' ? '金蝶单据字段映射' : id === 'shopify-payout-raw' ? 'Payouts API 原始明细' : 'Payouts 打款记录明细'; if (desc) desc.innerText = 'Shopify Payouts 数据管理'; if (id === 'shopify-payout-raw') populateStoreOptions(['spRawStore']).catch(() => {}); if (id === 'shopify-payout-field-map') openFieldMappingPage(); return; } if (typeof originalShow === 'function') originalShow(id); }; document.addEventListener('DOMContentLoaded', setupPages); })();