--- +++ @@ -5,3 +5,3 @@ import { - el, mount, clear, card, button, toggleSwitch, field, fieldValue, modal, + sectionTabs, el, mount, clear, card, button, toggleSwitch, field, fieldValue, modal, toast, confirmDialog, copyText, busy, loadingBlock, errorBlock, emptyBlock, @@ -14,2 +14,3 @@ import { loadRules } from './payment_conditions.js'; +import { openPaymentProvider, providerForMethod } from './payment_providers.js'; @@ -155,3 +156,2 @@ { key: 'cashback', label: 'pay_tab_cashback', icon: '🎁' }, - { key: 'crypto', label: 'pay_tab_crypto', icon: '🪙' }, { key: 'rules', label: 'pay_tab_rules', icon: '🔓' }, @@ -177,3 +177,4 @@ export function render(view, parts) { - drawShell(view, (parts && parts[0]) || 'cards'); + const active=(parts && parts[0]) || 'cards'; + drawShell(view,active === 'crypto' ? 'methods' : active); } @@ -182,10 +183,3 @@ const host = el('div', { class: 'page' }); - const tabbar = el('div', { class: 'subtabs' }); - TABS.forEach(function (tb) { - tabbar.appendChild(el('button', { - class: 'subtab' + (tb.key === active ? ' active' : ''), - onClick: function () { drawShell(view, tb.key); }, - }, [el('span', { text: tb.icon }), ' ', t(tb.label)])); - }); - const content = el('div', { class: 'subcontent' }, [loadingBlock()]); + const { tabbar, content } = sectionTabs(TABS, active, key => drawShell(view, key)); mount(host, [tabbar, content]); @@ -193,3 +187,3 @@ - const loaders = { cards: loadCards, methods: loadMethods, safe: loadSafeMode, cashback: loadCashback, crypto: loadCrypto, rules: loadRules }; + const loaders = { cards: loadCards, methods: loadMethods, safe: loadSafeMode, cashback: loadCashback, rules: loadRules }; (loaders[active] || loadCards)(content); @@ -276,10 +270,15 @@ const actions = el('div', { class: 'card-actions-row' }, [ - toggleSwitch({ - checked: c.enabled, + el('label',{class:'inline-toggle'},[el('span',{text:t('enabled')}),toggleSwitch({ + label: 'وضعیت کارت ' + (c.holder || c.bank), checked: c.enabled, onChange: function (v) { return api('/cards/' + c.id + '/toggle', { method: 'POST', body: { field: 'enabled' } }) - .then(function () { toast(t('saved_ok'), 'success'); }) + .then(function () { + c.enabled=v; + statusChip.textContent=t(v?'enabled':'disabled'); + statusChip.className='bc-status ' + (v?'on':'off'); + toast(t('saved_ok'), 'success'); + }) .catch(function (e) { toast(errText(e) || t('error_generic'), 'error'); throw e; }); }, - }), + })]), el('div', { class: 'grow' }), @@ -383,2 +382,9 @@ const wrap = el('div'); + const providers=methods.filter(providerForMethod); + if(providers.length)wrap.appendChild(el('div',{class:'gateway-shortcuts'},[ + el('span',{class:'hint',text:'تنظیمات اتصال درگاه‌ها'}), + el('div',{class:'gateway-shortcut-grid'},providers.map(method=>button({ + label:method.title,icon:'⚙️',kind:'ghost',onClick:()=>openPaymentProvider(providerForMethod(method)), + }))), + ])); if (!methods.length) wrap.appendChild(emptyBlock()); @@ -402,4 +408,4 @@ const actions = [ - toggleSwitch({ - checked: mth.enabled, onChange: function (v) { + el('label',{class:'inline-toggle'},[el('span',{text:t('enabled')}),toggleSwitch({ + label:'وضعیت ' + mth.title, checked: mth.enabled, onChange: function (v) { return api('/payment-methods/' + mth.id, { method: 'PATCH', body: { enabled: v } }) @@ -408,3 +414,4 @@ }, - }), + })]), + el('div',{class:'grow'}), button({ label: t('edit'), icon: '✏️', kind: 'ghost', small: true, onClick: function () { openMethodModal(host, mth); } }), @@ -412,8 +419,8 @@ // One settings button per wallet (availability + overrides + cashback rules). - const walletRow = el('div', { class: 'card-actions-row' }, + const walletRow = el('div', { class: 'wallet-options' }, walletIds().map(function (wid) { const link = (mth.wallets || []).find(function (w) { return w.wallet_id === wid; }); - const state = (link && link.enabled) ? '✅' : '❌'; - return button({ - label: state + ' ' + walletLabel(wid), icon: '👛', kind: 'ghost', + const enabled = !!(link && link.enabled); + const option = button({ + label: walletLabel(wid), icon: '👛', kind: 'ghost', small: true, @@ -421,5 +428,11 @@ }); + option.classList.add('wallet-option',enabled?'enabled':'disabled'); + option.setAttribute('aria-label',walletLabel(wid) + '، ' + t(enabled?'enabled':'disabled')); + return option; })); return card({ + class:'method-card', title: mth.title, icon: mth.is_card ? '💳' : (mth.is_nowpayments ? '🪙' : '💠'), + actions:providerForMethod(mth)?[button({label:'تنظیمات درگاه',kind:'ghost',small:true, + onClick:()=>openPaymentProvider(providerForMethod(mth))})]:null, children: [meta, walletRow, el('div', { class: 'card-actions-row' }, actions)], @@ -618,126 +631,2 @@ -// =========================================================== CRYPTO -const NOWP_GROUPS = [ - { label: 'pay_grp_credentials', keys: ['NOWPAYMENTS_API_KEY', 'NOWPAYMENTS_IPN_SECRET', 'NOWPAYMENTS_SANDBOX_API_KEY', 'NOWPAYMENTS_SANDBOX_IPN_SECRET'] }, - { label: 'pay_grp_sandbox', keys: ['NOWPAYMENTS_SANDBOX_MODE', 'NOWPAYMENTS_TEST_USER_IDS', 'NOWPAYMENTS_API_BASE_URL', 'NOWPAYMENTS_SANDBOX_API_BASE_URL'] }, - { label: 'pay_grp_currency', keys: ['NOWPAYMENTS_PRICE_CURRENCY', 'NOWPAYMENTS_DEFAULT_PAY_CURRENCY', 'NOWPAYMENTS_OUTCOME_CURRENCY', 'NOWPAYMENTS_FEE_PAID_BY_USER', 'NOWPAYMENTS_USD_TOMAN_MANUAL_RATE', 'NOWPAYMENTS_USD_TOMAN_RATE_SOURCE', 'NOWPAYMENTS_USD_TOMAN_RATE_URL', 'NOWPAYMENTS_RATE_MARKUP_PERCENT', 'NOWPAYMENTS_RATE_CACHE_TTL_SECONDS', 'NOWPAYMENTS_MIN_AMOUNT_CACHE_TTL_SECONDS', 'NOWPAYMENTS_HTTP_TIMEOUT_SECONDS'] }, - { label: 'pay_grp_callbacks', keys: ['WEBHOOK_PUBLIC_BASE_URL', 'NOWPAYMENTS_IPN_CALLBACK_URL', 'NOWPAYMENTS_SUCCESS_URL', 'NOWPAYMENTS_CANCEL_URL'] }, - { label: 'pay_grp_polling', keys: ['NOWPAYMENTS_POLLING_ENABLED', 'NOWPAYMENTS_POLLING_INTERVAL_SECONDS', 'NOWPAYMENTS_POLLING_MAX_AGE_HOURS', 'NOWPAYMENTS_ADMIN_NOTIFICATIONS_ENABLED', 'NOWPAYMENTS_OVERPAYMENT_ADMIN_REVIEW_ENABLED', 'NOWPAYMENTS_PARTIAL_PAYMENT_ADMIN_REVIEW_ENABLED'] }, -]; - -function loadCrypto(host) { - mount(host, loadingBlock()); - Promise.all([api('/payment-settings'), api('/nowpayments')]).then(function (res) { - const settings = res[0], nowp = res[1]; - const wrap = el('div'); - wrap.appendChild(tronadoCard(settings)); - wrap.appendChild(nowpaymentsCard(nowp)); - mount(host, wrap); - }).catch(function () { mount(host, errorBlock(function () { loadCrypto(host); })); }); -} - -function tronadoCard(settings) { - const fKey = field({ label: t('pay_api_key') + (settings.tronado_api_key_masked ? ' (' + settings.tronado_api_key_masked + ')' : ''), value: '', ltr: true, placeholder: t('pay_api_key_change') }); - const fWallet = field({ label: t('pay_wallet'), value: settings.tronado_wallet_address || '', ltr: true, placeholder: 'T...' }); - return card({ - title: t('pay_crypto_tronado'), icon: '🪙', - children: [fKey, fWallet, button({ - label: t('pay_save_settings'), onClick: function (ev) { - const body = {}; - if (fieldValue(fKey).trim()) body.api_key = fieldValue(fKey).trim(); - if (fieldValue(fWallet).trim()) body.wallet_address = fieldValue(fWallet).trim(); - const btn = ev.currentTarget; busy(btn, true); - api('/payment-settings/tronado', { method: 'POST', body: body }) - .then(function () { toast(t('saved_ok'), 'success'); busy(btn, false); }) - .catch(function (e) { busy(btn, false); toast(errText(e) || t('error_generic'), 'error'); }); - }, - })], - }); -} - -function nowpaymentsCard(nowp) { - const fields = nowp.fields || []; - const byKey = {}; - fields.forEach(function (f) { byKey[f.key] = f; }); - const pending = {}; // text/secret edits to save - const body = el('div'); - - NOWP_GROUPS.forEach(function (grp) { - const rows = []; - grp.keys.forEach(function (k) { - const f = byKey[k]; - if (!f) return; - rows.push(nowpRow(f, pending)); - delete byKey[k]; - }); - if (rows.length) { - body.appendChild(el('div', { class: 'group-label', text: t(grp.label) })); - rows.forEach(function (r) { body.appendChild(r); }); - } - }); - // leftovers - const rest = Object.keys(byKey); - if (rest.length) { - body.appendChild(el('div', { class: 'group-label', text: t('pay_grp_other') })); - rest.forEach(function (k) { body.appendChild(nowpRow(byKey[k], pending)); }); - } - - // resolved callbacks - if (nowp.callbacks) { - body.appendChild(el('div', { class: 'group-label', text: t('pay_callbacks') })); - Object.keys(nowp.callbacks).forEach(function (name) { - const url = nowp.callbacks[name]; - body.appendChild(el('div', { class: 'copyable' }, [ - el('span', { class: 'cp-url', dir: 'ltr', text: url }), - button({ label: t('pay_copy'), icon: '📋', kind: 'ghost', small: true, onClick: function () { copyText(url); } }), - ])); - }); - } - - body.appendChild(button({ - label: t('pay_save_settings'), onClick: function (ev) { - const updates = {}; - Object.keys(pending).forEach(function (k) { updates[k] = pending[k]; }); - if (!Object.keys(updates).length) { toast(t('saved_ok'), 'success'); return; } - const btn = ev.currentTarget; busy(btn, true); - api('/nowpayments', { method: 'PATCH', body: { updates: updates } }) - .then(function () { toast(t('saved_ok'), 'success'); busy(btn, false); for (const k in pending) delete pending[k]; }) - .catch(function (e) { busy(btn, false); toast(errText(e) || t('error_generic'), 'error'); }); - }, - })); - - return card({ title: t('pay_crypto_nowp'), icon: '🌐', children: [body] }); -} - -function nowpRow(f, pending) { - if (f.kind === 'bool') { - return el('div', { class: 'setting-row' }, [ - el('span', { class: 'sr-label', text: f.label }), - toggleSwitch({ - checked: f.value, onChange: function (v) { - return api('/nowpayments', { method: 'PATCH', body: { updates: { [f.key]: v } } }) - .then(function () { toast(t('saved_ok'), 'success'); }) - .catch(function (e) { toast(errText(e) || t('error_generic'), 'error'); throw e; }); - }, - }), - ]); - } - if (f.kind === 'secret') { - const input = el('input', { type: 'text', dir: 'ltr', placeholder: f.has_value ? (f.display || t('pay_secret_set')) : t('pay_secret_unset') }); - input.addEventListener('input', function () { - if (input.value.trim()) pending[f.key] = input.value.trim(); else delete pending[f.key]; - }); - return el('div', { class: 'setting-row col' }, [ - el('span', { class: 'sr-label', text: f.label + (f.has_value ? ' 🔒✅' : ' 🔒') }), - input, - ]); - } - const input = el('input', { type: 'text', dir: 'ltr', value: f.value != null ? f.value : '' }); - input.addEventListener('input', function () { pending[f.key] = input.value; }); - return el('div', { class: 'setting-row col' }, [ - el('span', { class: 'sr-label', text: f.label }), - input, - ]); -} - // =========================================================== SAFE MODE @@ -776,5 +665,5 @@ try { - return new Date(iso).toLocaleString('fa-IR', { + return new Date(iso).toLocaleString('en-CA', { timeZone: 'Asia/Tehran', year: 'numeric', month: '2-digit', - day: '2-digit', hour: '2-digit', minute: '2-digit', + day: '2-digit', hour: '2-digit', minute: '2-digit', hourCycle: 'h23', }); @@ -941,4 +830,4 @@ kv(t('sm_interval'), fmtNum(s.interval_minutes)), - kv(t('sm_last_run'), smFmtDate(s.last_run_at)), - kv(t('sm_updated_at'), smFmtDate(s.updated_at)), + kv(t('sm_last_run'), smFmtDate(s.last_run_at), 'ltr'), + kv(t('sm_updated_at'), smFmtDate(s.updated_at), 'ltr'), ]), @@ -946,3 +835,3 @@ el('span', { class: 'muted', text: t('sm_next_run') + ': ' }), - el('span', { text: smFmtDate(s.next_run_at) }), + el('span', { text: smFmtDate(s.next_run_at), dir: 'ltr' }), ' ', cd, @@ -1134,3 +1023,3 @@ pill(t('sm_ev_' + ev.event_type), kindMap[ev.event_type] || ''), - el('span', { class: 'sm-ev-date muted', text: smFmtDate(ev.created_at) }), + el('span', { class: 'sm-ev-date muted', text: smFmtDate(ev.created_at), dir: 'ltr' }), ]), @@ -1144,3 +1033,3 @@ // =========================================================== small helpers -function kv(label, value) { +function kv(label, value, direction) { // Missing values render as an em dash — never "undefined"/"null" text. @@ -1150,3 +1039,3 @@ el('div', { class: 'kv-label', text: label }), - el('div', { class: 'kv-value', text: shown }), + el('div', { class: 'kv-value', text: shown, dir: direction }), ]);