kapital_bank/kapital_bank/public/js/kapital_bank_import.bundle.js

758 lines
25 KiB
JavaScript

const KBBTImport = {
loadingErrors: [],
};
// ======= ERROR MANAGEMENT =======
KBBTImport.errors = {
add: function(txnData, errorType, errorMessage) {
const errorEntry = {
ref_no: txnData.ref_no || 'Unknown',
date: txnData.date || 'Unknown',
counterparty: txnData.counterparty || 'Unknown',
amount: txnData.amount || 0,
source_label: txnData.source_label || '',
error_type: errorType,
error_message: errorMessage,
timestamp: new Date().toISOString(),
};
KBBTImport.loadingErrors.push(errorEntry);
console.error('KB BT Import Error:', errorEntry);
},
clear: function() {
frappe.confirm(
__('Are you sure you want to clear the error log?'),
function() {
KBBTImport.loadingErrors = [];
frappe.show_alert({
message: __('Error log cleared'),
indicator: 'green'
}, 2);
}
);
},
showDetails: function() {
if (KBBTImport.loadingErrors.length === 0) {
frappe.msgprint(__('No errors to display'));
return;
}
let errorsHtml = '<div class="error-log-container">';
KBBTImport.loadingErrors.forEach(function(error) {
let documentDate = 'No date';
if (error.date && error.date !== 'Unknown') {
try {
documentDate = moment(error.date).format('DD.MM.YYYY');
} catch (e) {
documentDate = 'Invalid date';
}
}
errorsHtml += '<div class="error-item" style="margin-bottom: 20px; padding: 15px; border: 1px solid var(--border-color); border-radius: 6px; background: var(--bg-color);">';
// Header
errorsHtml += '<div class="error-header" style="margin-bottom: 10px; padding-bottom: 8px; border-bottom: 1px solid var(--border-color);">';
errorsHtml += '<div style="display: flex; justify-content: space-between; align-items: center;">';
errorsHtml += '<h5 style="margin: 0; color: var(--text-color);">' +
'<strong>' + error.ref_no + '</strong></h5>';
errorsHtml += '<span class="label label-danger">' + error.error_type + '</span>';
errorsHtml += '</div>';
errorsHtml += '<small style="color: var(--text-muted);">' + error.counterparty + '</small>';
if (error.source_label) {
errorsHtml += ' <small style="color: var(--text-muted);">(' + error.source_label + ')</small>';
}
errorsHtml += '</div>';
// Error message
errorsHtml += '<div class="error-message" style="margin-bottom: 10px;">';
errorsHtml += '<strong style="color: var(--text-color);">' + error.error_message + '</strong>';
errorsHtml += '</div>';
errorsHtml += '<div style="text-align: right; margin-top: 10px;">';
errorsHtml += '<small style="color: var(--text-muted);"><em>' + documentDate + '</em></small>';
errorsHtml += '</div>';
errorsHtml += '</div>';
});
errorsHtml += '</div>';
// Statistics
const errorTypes = {};
KBBTImport.loadingErrors.forEach(function(error) {
errorTypes[error.error_type] = (errorTypes[error.error_type] || 0) + 1;
});
let statsHtml = '<div style="margin-bottom: 20px; padding: 15px; background: var(--bg-light); border-radius: 6px; border: 1px solid var(--border-color);">';
statsHtml += '<div style="display: flex; justify-content: space-between; align-items: center;">';
statsHtml += '<div>';
statsHtml += '<strong style="color: var(--text-color);">' + __('Total Errors:') + '</strong> ' + KBBTImport.loadingErrors.length + '<br>';
statsHtml += '<span style="color: var(--text-muted);">';
Object.keys(errorTypes).forEach(function(type, index) {
if (index > 0) statsHtml += ' &bull; ';
statsHtml += type + ': ' + errorTypes[type];
});
statsHtml += '</span>';
statsHtml += '</div>';
statsHtml += '<div>';
statsHtml += '<button class="btn btn-default btn-sm" id="kb-bt-export-csv-dialog-btn" style="margin-right: 5px;">' +
'<i class="fa fa-download"></i> ' + __('Export CSV') + '</button>';
statsHtml += '<button class="btn btn-warning btn-sm" id="kb-bt-clear-log-dialog-btn">' +
'<i class="fa fa-trash"></i> ' + __('Clear') + '</button>';
statsHtml += '</div></div>';
const d = new frappe.ui.Dialog({
title: __('Error Details') + ' (' + KBBTImport.loadingErrors.length + ')',
size: 'large',
fields: [
{
fieldname: 'stats_html',
fieldtype: 'HTML',
options: statsHtml
},
{
fieldname: 'errors_html',
fieldtype: 'HTML',
options: '<div style="max-height: 500px; overflow-y: auto;">' + errorsHtml + '</div>'
}
],
primary_action_label: __('Close'),
primary_action: function() {
d.hide();
}
});
d.show();
setTimeout(function() {
$('#kb-bt-export-csv-dialog-btn').off('click').on('click', function() {
KBBTImport.errors.exportCSV();
});
$('#kb-bt-clear-log-dialog-btn').off('click').on('click', function() {
KBBTImport.errors.clear();
d.hide();
});
}, 200);
},
exportCSV: function() {
if (KBBTImport.loadingErrors.length === 0) {
frappe.msgprint(__('No errors to export'));
return;
}
try {
let csvContent = "data:text/csv;charset=utf-8,";
csvContent += "Ref No,Date,Counterparty,Amount,Source,Error Type,Error Message,Timestamp\n";
KBBTImport.loadingErrors.forEach(function(error) {
const cleanMessage = error.error_message.replace(/"/g, '""').replace(/\n/g, ' ').replace(/\r/g, ' ');
const cleanCounterparty = (error.counterparty || '').replace(/"/g, '""');
const row = [
error.ref_no,
error.date,
cleanCounterparty,
error.amount,
error.source_label || '',
error.error_type,
cleanMessage,
moment(error.timestamp).format('DD.MM.YYYY HH:mm:ss')
].map(function(field) {
return '"' + (field || '') + '"';
}).join(',');
csvContent += row + "\n";
});
const encodedUri = encodeURI(csvContent);
const link = document.createElement("a");
link.setAttribute("href", encodedUri);
link.setAttribute("download", "kb_bt_import_errors_" + moment().format('YYYY-MM-DD_HH-mm-ss') + ".csv");
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
frappe.show_alert({
message: __('Error log exported successfully'),
indicator: 'green'
}, 3);
} catch (e) {
console.error('Export error:', e);
frappe.show_alert({
message: __('Failed to export error log'),
indicator: 'red'
}, 3);
}
},
showSummary: function(totalTransactions, processedCount, errorsCount) {
if (errorsCount === 0) {
frappe.msgprint({
title: __('Import Completed Successfully'),
indicator: 'green',
message: __('All ') + totalTransactions + __(' transactions were imported successfully.')
});
} else {
const title = errorsCount === totalTransactions ? __('Import Failed') : __('Import Completed with Errors');
const indicator = processedCount > 0 ? 'orange' : 'red';
let message = '<div style="margin-bottom: 15px;">';
if (processedCount > 0) {
message += __('Successfully imported: ') + '<strong>' + processedCount + '</strong>' + __(' transactions') + '<br>';
}
message += __('Failed: ') + '<strong>' + errorsCount + '</strong>' + __(' transactions');
message += '</div>';
message += '<div style="text-align: center;">' +
'<button class="btn btn-primary btn-sm" id="kb-bt-view-error-details-btn" style="margin-right: 10px;">' +
'<i class="fa fa-list"></i> ' + __('View Error Details') + '</button>' +
'<button class="btn btn-default btn-sm" id="kb-bt-export-error-log-btn">' +
'<i class="fa fa-download"></i> ' + __('Export CSV') + '</button>' +
'</div>';
const summaryDialog = frappe.msgprint({
title: title,
indicator: indicator,
message: message
});
setTimeout(function() {
$('#kb-bt-view-error-details-btn').off('click').on('click', function() {
summaryDialog.hide();
KBBTImport.errors.showDetails();
});
$('#kb-bt-export-error-log-btn').off('click').on('click', function() {
KBBTImport.errors.exportCSV();
});
}, 200);
}
}
};
// ======= IMPORT MODULE =======
KBBTImport.import = {
showDialog: function() {
// Load sources: try settings mappings first, fall back to registry doctypes
frappe.call({
method: 'frappe.client.get',
args: { doctype: 'Kapital Bank Settings', name: 'Kapital Bank Settings' },
callback: function(r) {
if (!r.message) {
frappe.msgprint({ title: __('Error'), indicator: 'red', message: __('Could not load Kapital Bank Settings') });
return;
}
const accountMappings = r.message.account_mappings || [];
const cardMappings = r.message.card_mappings || [];
if (accountMappings.length > 0 || cardMappings.length > 0) {
// Use mappings as source list
KBBTImport.import._showDialogWithSources(
accountMappings.map(function(m) {
return { type: 'account', id: m.iban, label: m.account_label ? (m.iban + ' — ' + m.account_label) : m.iban };
}).filter(function(s) { return s.id; }),
cardMappings.map(function(m) {
return { type: 'card', id: m.account_number, label: m.card_label ? (m.account_number + ' — ' + m.card_label) : m.account_number };
}).filter(function(s) { return s.id; })
);
} else {
// Fall back to registry doctypes
KBBTImport.import._loadRegistrySources();
}
}
});
},
_loadRegistrySources: function() {
let accounts = [];
let cards = [];
let pending = 2;
function onDone() {
pending--;
if (pending > 0) return;
if (accounts.length === 0 && cards.length === 0) {
frappe.msgprint({
title: __('No Sources'),
indicator: 'orange',
message: __('No bank accounts or cards found. Please load registry data in Kapital Bank Settings first.')
});
return;
}
KBBTImport.import._showDialogWithSources(accounts, cards);
}
frappe.call({
method: 'frappe.client.get_list',
args: {
doctype: 'Kapital Bank Account',
fields: ['iban', 'account_desc', 'currency'],
filters: { status: 'Active' },
limit_page_length: 0
},
callback: function(r) {
if (r.message) {
accounts = r.message.map(function(a) {
const label = a.account_desc ? (a.iban + ' — ' + a.account_desc) : a.iban;
return { type: 'account', id: a.iban, label: label };
});
}
onDone();
},
error: function() { onDone(); }
});
frappe.call({
method: 'frappe.client.get_list',
args: {
doctype: 'Kapital Bank Card',
fields: ['account_number', 'card_type', 'pan', 'currency'],
filters: { status: 'Active' },
limit_page_length: 0
},
callback: function(r) {
if (r.message) {
cards = r.message.map(function(c) {
const label = c.pan ? (c.account_number + ' — ' + c.pan) : c.account_number;
return { type: 'card', id: c.account_number, label: label };
});
}
onDone();
},
error: function() { onDone(); }
});
},
_showDialogWithSources: function(accounts, cards) {
let sourcesHtml = '<div style="max-height: 300px; overflow-y: auto;">';
sourcesHtml += '<div style="margin-bottom: 10px;">';
sourcesHtml += '<label><input type="checkbox" class="kb-bt-select-all-sources"> <strong>' + __('Select All') + '</strong></label>';
sourcesHtml += '</div>';
if (accounts.length > 0) {
sourcesHtml += '<div style="margin-bottom: 8px; font-weight: 600; color: var(--text-color);">' + __('Accounts') + '</div>';
accounts.forEach(function(a) {
sourcesHtml += '<div style="margin-left: 20px; margin-bottom: 4px;">';
sourcesHtml += '<label><input type="checkbox" class="kb-bt-source-check" data-type="' + a.type + '" data-id="' + a.id + '"> ' + a.label + '</label>';
sourcesHtml += '</div>';
});
}
if (cards.length > 0) {
sourcesHtml += '<div style="margin-bottom: 8px; margin-top: 10px; font-weight: 600; color: var(--text-color);">' + __('Cards') + '</div>';
cards.forEach(function(c) {
sourcesHtml += '<div style="margin-left: 20px; margin-bottom: 4px;">';
sourcesHtml += '<label><input type="checkbox" class="kb-bt-source-check" data-type="' + c.type + '" data-id="' + c.id + '"> ' + c.label + '</label>';
sourcesHtml += '</div>';
});
}
sourcesHtml += '</div>';
const today = frappe.datetime.get_today();
const first_day = frappe.datetime.month_start(today);
const d = new frappe.ui.Dialog({
title: __('Import Bank Transactions'),
fields: [
{ fieldname: 'sec_period', fieldtype: 'Section Break', label: __('Period') },
{ fieldname: 'date_from', fieldtype: 'Date', label: __('Date From'), reqd: 1, default: first_day },
{ fieldname: 'col_break', fieldtype: 'Column Break' },
{ fieldname: 'date_to', fieldtype: 'Date', label: __('Date To'), reqd: 1, default: today },
{ fieldname: 'sec_sources', fieldtype: 'Section Break', label: __('Sources') },
{
fieldname: 'sources_html',
fieldtype: 'HTML',
options: sourcesHtml
}
],
primary_action_label: __('Search'),
primary_action: function() {
const values = d.get_values();
const selectedSources = [];
d.$wrapper.find('.kb-bt-source-check:checked').each(function() {
selectedSources.push({
type: $(this).data('type'),
id: $(this).data('id')
});
});
if (selectedSources.length === 0) {
frappe.msgprint({
title: __('Warning'),
indicator: 'orange',
message: __('Please select at least one account or card')
});
return;
}
// Card statements are limited to 90 days by the API
const hasCards = selectedSources.some(s => s.type === 'card');
if (hasCards) {
const diffDays = frappe.datetime.get_diff(values.date_to, values.date_from);
if (diffDays > 90) {
frappe.msgprint({
title: __('Date Range Too Large'),
indicator: 'orange',
message: __('Card statements are limited to 90 days. Please shorten the date range or deselect cards.')
});
return;
}
}
d.hide();
KBBTImport.import.loadTransactionsMulti(values.date_from, values.date_to, selectedSources);
}
});
d.show();
// Select All handler
d.$wrapper.find('.kb-bt-select-all-sources').on('change', function() {
const isChecked = $(this).prop('checked');
d.$wrapper.find('.kb-bt-source-check').prop('checked', isChecked);
});
},
loadTransactionsMulti: function(fromDate, toDate, sources) {
const totalSources = sources.length;
let currentSourceIdx = 0;
let allTransactions = [];
let totalSkipped = 0;
let seenRefNos = new Set();
let crossAccountDuplicates = 0;
let skippedCrossCurrency = 0;
function fetchNext() {
if (currentSourceIdx >= totalSources) {
frappe.hide_progress();
if (allTransactions.length === 0) {
let msg = __('No new transactions found for the specified period.');
if (totalSkipped > 0) {
msg += ' ' + __('({0} already imported)', [totalSkipped]);
}
frappe.msgprint({
title: __('Information'),
indicator: 'blue',
message: msg
});
return;
}
if (totalSkipped > 0 || crossAccountDuplicates > 0 || skippedCrossCurrency > 0) {
let parts = [];
if (totalSkipped > 0) parts.push(__('already imported: {0}', [totalSkipped]));
if (crossAccountDuplicates > 0) parts.push(__('cross-account duplicates: {0}', [crossAccountDuplicates]));
if (skippedCrossCurrency > 0) parts.push(__('other currency account: {0}', [skippedCrossCurrency]));
frappe.show_alert({
message: __('Skipped: {0}', [parts.join(', ')]),
indicator: 'blue'
}, 5);
}
KBBTImport.import.showTransactionSelection(allTransactions);
return;
}
const source = sources[currentSourceIdx];
const sourceLabel = source.type === 'account'
? source.id
: __('Card: {0}', [source.id]);
frappe.show_progress(
__('Fetching Statements'),
currentSourceIdx,
totalSources,
__('Fetching statement {0} of {1}: {2}', [currentSourceIdx + 1, totalSources, sourceLabel])
);
const method = source.type === 'account'
? 'kapital_bank.bank_api.get_statement_transactions'
: 'kapital_bank.bank_api.get_card_statement_transactions';
const args = source.type === 'account'
? { from_date: fromDate, to_date: toDate, account_iban: source.id }
: { from_date: fromDate, to_date: toDate, card_account_number: source.id };
frappe.call({
method: method,
args: args,
callback: function(r) {
if (r.message && r.message.success) {
const txns = (r.message.transactions || []).map(function(txn) {
txn.source_id = source.id;
return txn;
});
// Deduplicate across multiple statements (cross-account transfers)
txns.forEach(function(txn) {
if (txn.ref_no && seenRefNos.has(txn.ref_no)) {
crossAccountDuplicates++;
} else {
if (txn.ref_no) seenRefNos.add(txn.ref_no);
allTransactions.push(txn);
}
});
totalSkipped += (r.message.skipped_duplicates || 0);
skippedCrossCurrency += (r.message.skipped_cross_currency || 0);
} else {
frappe.show_alert({
message: __('Error fetching {0}: {1}', [sourceLabel, r.message ? r.message.message : 'unknown']),
indicator: 'red'
}, 5);
}
currentSourceIdx++;
setTimeout(fetchNext, 100);
},
error: function() {
frappe.show_alert({
message: __('Network error fetching {0}', [sourceLabel]),
indicator: 'red'
}, 5);
currentSourceIdx++;
setTimeout(fetchNext, 100);
}
});
}
fetchNext();
},
showTransactionSelection: function(txns) {
const hasCardTxns = txns.some(t => t.source_type === 'card');
let txnTable = '';
if (hasCardTxns) {
txnTable += '<div style="margin-bottom: 10px; padding: 10px 12px; background: var(--red-50, #fff5f5); border: 1px solid var(--red-200, #fecaca); border-radius: var(--border-radius); font-size: var(--text-base); color: var(--red-600, #dc2626);">';
txnTable += '⚠️ ' + __('Card transactions may already be imported from other accounts. The bank uses different reference numbers for card and account statements, so duplicates cannot be detected automatically.');
txnTable += '</div>';
}
txnTable += '<div style="max-height: 500px; overflow-y: auto;"><table class="table table-bordered kb-bt-txn-table" style="width: 100%; table-layout: fixed;">';
txnTable += '<thead style="position: sticky; top: 0; z-index: 1; background: var(--bg-color, #fff);"><tr>' +
'<th style="width: 4%;"><input type="checkbox" class="kb-bt-select-all-txns"></th>' +
'<th style="width: 14%;">' + __('Ref No') + '</th>' +
'<th style="width: 10%;">' + __('Date') + '</th>' +
'<th style="width: 14%;">' + __('Account') + '</th>' +
'<th style="width: 20%;">' + __('Counterparty') + '</th>' +
'<th style="width: 12%; text-align: right;">' + __('Amount') + '</th>' +
'<th style="width: 6%; text-align: center;">' + __('Type') + '</th>' +
'<th style="width: 20%;">' + __('Purpose') + '</th>' +
'</tr></thead><tbody>';
txns.forEach(function(txn, idx) {
const displayDate = txn.date ? moment(txn.date).format('DD.MM.YYYY') : '';
const amountFmt = parseFloat(txn.amount || 0).toFixed(2);
const drcrLabel = txn.drcr === 'D'
? '<span class="label label-danger">' + __('Pay') + '</span>'
: '<span class="label label-success">' + __('Receive') + '</span>';
const sourceLabel = txn.source_label || '';
txnTable += '<tr>' +
'<td><input type="checkbox" class="kb-bt-select-txn" data-idx="' + idx + '"></td>' +
'<td style="word-break: break-word;">' + (txn.ref_no || '') + '</td>' +
'<td>' + displayDate + '</td>' +
'<td style="word-break: break-word; font-size: 0.85em;">' + sourceLabel + '</td>' +
'<td style="word-break: break-word;">' + (txn.counterparty || '') + '</td>' +
'<td style="text-align: right;">' + amountFmt + ' ' + (txn.currency || '') + '</td>' +
'<td style="text-align: center;">' + drcrLabel + '</td>' +
'<td style="word-break: break-word; font-size: 0.9em;">' + (txn.full_description || txn.purpose || '') + '</td>' +
'</tr>';
});
txnTable += '</tbody></table></div>';
const infoMessage = '<div class="alert alert-info">' +
__('Transactions found: ') + txns.length +
'</div>';
const d = new frappe.ui.Dialog({
title: __('Select Transactions to Import'),
size: 'large',
fields: [
{
fieldname: 'info_html',
fieldtype: 'HTML',
options: infoMessage
},
{
fieldname: 'txns_html',
fieldtype: 'HTML',
options: txnTable
}
],
primary_action_label: __('Load selected'),
primary_action: function() {
const selectedTxns = [];
d.$wrapper.find('.kb-bt-select-txn:checked').each(function() {
const idx = $(this).data('idx');
selectedTxns.push(txns[idx]);
});
if (selectedTxns.length === 0) {
frappe.msgprint({
title: __('Warning'),
indicator: 'orange',
message: __('No transactions selected')
});
return;
}
d.hide();
KBBTImport.import.loadSelectedTransactions(selectedTxns);
},
secondary_action_label: __('Cancel'),
secondary_action: function() {
d.hide();
}
});
d.$wrapper.find('.modal-dialog').css({
'max-width': '90%',
'width': '90%',
'margin': '30px auto'
});
d.$wrapper.find('.modal-body').css({
'padding': '15px'
});
d.show();
d.$wrapper.find('.kb-bt-select-all-txns').on('change', function() {
const isChecked = $(this).prop('checked');
d.$wrapper.find('.kb-bt-select-txn').prop('checked', isChecked);
});
},
loadSelectedTransactions: function(selectedTxns) {
KBBTImport.loadingErrors = [];
const total = selectedTxns.length;
frappe.show_progress(
__('Importing Transactions'), 0, total,
__('Starting import of {0} transactions...', [total])
);
// Listen for realtime progress from background job
frappe.realtime.off('kb_bt_import_progress');
frappe.realtime.on('kb_bt_import_progress', function(data) {
frappe.show_progress(
__('Importing Transactions'), data.current, data.total,
__('Importing transaction {0} of {1}', [data.current, data.total])
);
});
// Listen for completion from background job
frappe.realtime.off('kb_bt_import_complete');
frappe.realtime.on('kb_bt_import_complete', function(data) {
frappe.realtime.off('kb_bt_import_progress');
frappe.realtime.off('kb_bt_import_complete');
KBBTImport.import._onBulkComplete(data);
});
frappe.call({
method: 'kapital_bank.bank_api.import_bulk_bank_transactions',
args: { txn_list: JSON.stringify(selectedTxns) },
callback: function(r) {
// Job enqueued — progress & completion come via realtime events
if (!r.message || !r.message.enqueued) {
frappe.realtime.off('kb_bt_import_progress');
frappe.realtime.off('kb_bt_import_complete');
KBBTImport.import._closeProgress();
frappe.msgprint({
title: __('Error'),
indicator: 'red',
message: __('Failed to start import job')
});
}
},
error: function() {
frappe.realtime.off('kb_bt_import_progress');
frappe.realtime.off('kb_bt_import_complete');
KBBTImport.import._closeProgress();
frappe.msgprint({
title: __('Error'),
indicator: 'red',
message: __('Network error starting import')
});
}
});
},
_onBulkComplete: function(data) {
KBBTImport.import._closeProgress();
// Collect errors for the error panel
(data.errors || []).forEach(function(err) {
const txnData = err.txn_data || {};
const errorType = err.error_type === 'unmapped_bank_account' ? 'Unmapped Bank Account'
: 'Import Error';
KBBTImport.errors.add(txnData, errorType, err.message);
});
KBBTImport.errors.showSummary(data.total, data.imported, data.errors ? data.errors.length : 0);
if (cur_list) {
cur_list.refresh();
}
},
_closeProgress: function() {
if (frappe.cur_progress) {
try {
frappe.cur_progress.$wrapper.modal('hide');
frappe.cur_progress.$wrapper.remove();
} catch(e) {}
frappe.cur_progress = null;
}
if ($('.modal:visible').length === 0) {
$('.modal-backdrop').remove();
$('body').removeClass('modal-open');
}
}
};
// ───────────────────────────────────────────────────────────────────────────────
// Register Kapital Bank as a bank-import source in jey_erp's shared "Import
// From..." menu (Bank Transaction list, Payment Entry list, Bank Reconciliation
// Tool). Loaded globally via app_include_js, so KBBTImport is available wherever
// the menu is shown — including the BRT, where the per-doctype script is not.
// ───────────────────────────────────────────────────────────────────────────────
window.KBBTImport = KBBTImport;
frappe.provide('jey_erp');
jey_erp.bank_sources = jey_erp.bank_sources || [];
if (!jey_erp.bank_sources.some((s) => s.key === 'kapital')) {
jey_erp.bank_sources.push({
key: 'kapital',
label: __('Kapital Bank'),
importLabel: __('Kapital Bank (API)'),
settingsLabel: __('Kapital Bank Settings'),
runImport(ctx) {
ctx = ctx || {};
// Let the caller (e.g. the Bank Reconciliation Tool) react when the
// API import finishes — KBBTImport runs its own progress/list refresh,
// but the BRT needs to reload its reconciliation table afterwards.
if (typeof ctx.onComplete === 'function') {
const handler = (d) => {
frappe.realtime.off('kb_bt_import_complete', handler);
ctx.onComplete({ imported: (d && d.imported) || 0, errors: (d && d.errors) || [], transactions: [] });
};
frappe.realtime.on('kb_bt_import_complete', handler);
}
KBBTImport.import.showDialog();
},
goToSettings() { frappe.set_route('Form', 'Kapital Bank Settings'); },
});
}