invoice_az/invoice_az/client/employee.js

1568 lines
68 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Load employees from ƏMAS (e-social.gov.az) directly into ERPNext Employee
// Pattern: same as e-taxes import — button on list → API call → selection → create
frappe.ui.form.on('Employee', {
refresh: function(frm) {
// Add "View AMAS Contract" button if employee was imported from AMAS
if (frm.doc.custom_amas_employee) {
frm.add_custom_button(__('View AMAS Contract'), function() {
frappe.set_route('Form', 'Amas Employees', frm.doc.custom_amas_employee);
}, __('AMAS'));
// Load and display AMAS data dynamically
load_amas_data(frm);
}
}
});
// Load AMAS data from linked record and populate HTML fields
function load_amas_data(frm) {
if (!frm.doc.custom_amas_employee) return;
frappe.call({
method: 'frappe.client.get',
args: {
doctype: 'Amas Employees',
name: frm.doc.custom_amas_employee
},
callback: function(r) {
if (r.message) {
const amas_data = r.message;
populate_amas_contract_tab(amas_data);
populate_amas_position_tab(amas_data);
populate_amas_salary_tab(amas_data);
populate_amas_schedule_tab(amas_data);
populate_amas_personal_tab(amas_data);
populate_amas_documents_tab(amas_data);
populate_amas_integration_tab(amas_data);
}
}
});
}
// Helper function to create a field row
function field_row(label, value) {
if (!value) return '';
return `<tr><td style="font-weight: 600; width: 40%; padding: 8px;">${label}</td><td style="padding: 8px;">${frappe.utils.escape_html(value)}</td></tr>`;
}
// Populate Contract tab
function populate_amas_contract_tab(data) {
let html = '<table class="table table-bordered" style="margin-top: 15px;">';
html += '<thead><tr><th colspan="2" style="background-color: #f5f5f5;">Contract Information</th></tr></thead><tbody>';
html += field_row('Document No', data.doc_no);
html += field_row('Document Date', data.doc_date);
html += field_row('Document Type', data.doc_type);
html += field_row('Contract Type', data.contract_type);
html += field_row('Contract Status', data.contract_status);
html += field_row('Contract Number', data.contract_number);
html += field_row('Is Signed', data.is_signed ? 'Yes' : 'No');
html += field_row('Contract Validity', data.contract_validity);
html += '</tbody></table>';
html += '<table class="table table-bordered" style="margin-top: 15px;">';
html += '<thead><tr><th colspan="2" style="background-color: #f5f5f5;">Contract Dates</th></tr></thead><tbody>';
html += field_row('Begin Date', data.begin_date);
html += field_row('End Date', data.end_date);
html += field_row('Main Contract Begin Date', data.main_contract_begin_date);
html += field_row('Contract Begin Date', data.contract_begin_date);
html += field_row('Contract End Date', data.contract_end_date);
html += '</tbody></table>';
html += '<table class="table table-bordered" style="margin-top: 15px;">';
html += '<thead><tr><th colspan="2" style="background-color: #f5f5f5;">Contract Details</th></tr></thead><tbody>';
html += field_row('Previous Contract', data.prev_contract);
html += field_row('Contract Reason', data.contract_reason);
html += field_row('From Migration', data.from_migration ? 'Yes' : 'No');
html += field_row('Staff OID', data.staff_oid);
html += field_row('Structure Unit OID', data.structure_unit_oid);
html += field_row('Entity OID', data.entity_oid);
html += field_row('Entity Details OID', data.entity_details_oid);
html += '</tbody></table>';
$('#amas_contract_data').html(html);
}
// Populate Position tab
function populate_amas_position_tab(data) {
let html = '<table class="table table-bordered" style="margin-top: 15px;">';
html += '<thead><tr><th colspan="2" style="background-color: #f5f5f5;">Position</th></tr></thead><tbody>';
html += field_row('Work Position', data.work_position);
html += field_row('Position ID', data.position_id);
html += field_row('Position Text', data.position_text);
html += field_row('Activity Code', data.activity_code);
html += field_row('Activity Name', data.activity_name);
html += field_row('Staff Unit', data.staff_unit);
html += field_row('Structure Path', data.structure_path);
html += field_row('Workplace Sector', data.workplace_sector);
html += '</tbody></table>';
html += '<table class="table table-bordered" style="margin-top: 15px;">';
html += '<thead><tr><th colspan="2" style="background-color: #f5f5f5;">Work Mode</th></tr></thead><tbody>';
html += field_row('Workplace Type', data.workplace_type);
html += field_row('Form Size', data.form_size);
html += field_row('Flow', data.flow);
html += field_row('Flow Alias', data.flow_alias);
html += field_row('View Type', data.view_type);
html += '</tbody></table>';
$('#amas_position_data').html(html);
}
// Populate Salary tab
function populate_amas_salary_tab(data) {
let html = '<table class="table table-bordered" style="margin-top: 15px;">';
html += '<thead><tr><th colspan="2" style="background-color: #f5f5f5;">Base Salary</th></tr></thead><tbody>';
html += field_row('Base Salary', data.base_salary);
html += field_row('Monthly Salary', data.monthly_salary);
html += field_row('Salary Add', data.salary_add);
html += field_row('Salary Add HW', data.salary_add_hw);
html += field_row('Currency Type', data.currency_type);
html += field_row('Payment Type', data.payment_type);
html += field_row('Payment First Day', data.payment_first_day);
html += field_row('Bank OID', data.bank_oid);
html += '</tbody></table>';
html += '<table class="table table-bordered" style="margin-top: 15px;">';
html += '<thead><tr><th colspan="2" style="background-color: #f5f5f5;">Additional Payments</th></tr></thead><tbody>';
html += field_row('Award Amount', data.award_amount);
html += field_row('Award Type', data.award_type);
html += field_row('Overtime Amount', data.overtime_amount);
html += field_row('Bad Condition Amount', data.bad_condition_amount);
html += field_row('Bad Condition ID', data.bad_condition_id);
html += field_row('Qualification Degree Salary Add', data.qualification_degree_salary_add);
html += '</tbody></table>';
$('#amas_salary_data').html(html);
}
// Populate Schedule tab
function populate_amas_schedule_tab(data) {
let html = '<table class="table table-bordered" style="margin-top: 15px;">';
html += '<thead><tr><th colspan="2" style="background-color: #f5f5f5;">Work Schedule</th></tr></thead><tbody>';
html += field_row('Work Mode Type', data.work_mode_type);
html += field_row('Is Full Time', data.is_full_time);
html += field_row('Work Time', data.work_time);
html += field_row('Working Time Cumulative', data.working_time_cumulative ? 'Yes' : 'No');
html += field_row('First Shift Start', data.first_shift_start);
html += field_row('First Shift End', data.first_shift_end);
html += field_row('Second Shift Start', data.second_shift_start);
html += field_row('Second Shift End', data.second_shift_end);
html += field_row('Lunch Start', data.lunch_start);
html += field_row('Lunch End', data.lunch_end);
html += '</tbody></table>';
html += '<table class="table table-bordered" style="margin-top: 15px;">';
html += '<thead><tr><th colspan="2" style="background-color: #f5f5f5;">Vacation</th></tr></thead><tbody>';
html += field_row('Vacation Days Count', data.vacation_days_count);
html += field_row('Vacation General Duration', data.vacation_general_duration);
html += field_row('Vacation Main Duration', data.vacation_main_duration);
html += field_row('Vacation Add Duration 1', data.vacation_add_dur_one);
html += field_row('Vacation Add Duration 2', data.vacation_add_dur_two);
html += field_row('Vacation Add Duration 4', data.vacation_add_dur_four);
html += field_row('Vacation Amount', data.vacation_amount);
html += field_row('Profession Employee For Vacation', data.profession_employee_for_vacation);
html += '</tbody></table>';
$('#amas_schedule_data').html(html);
}
// Populate Personal tab
function populate_amas_personal_tab(data) {
let html = '<table class="table table-bordered" style="margin-top: 15px;">';
html += '<thead><tr><th colspan="2" style="background-color: #f5f5f5;">Personal Information</th></tr></thead><tbody>';
html += field_row('FIN', data.identification_number);
html += field_row('SSN', data.ssn);
html += field_row('Father Name', data.father_name);
html += field_row('Birthday', data.birthday);
html += field_row('Gender', data.gender);
html += field_row('Nationality', data.nationality);
html += field_row('Blood Type', data.blood_type);
html += field_row('Marital Status', data.marital_status);
html += field_row('Military Status', data.military_status);
html += '</tbody></table>';
html += '<table class="table table-bordered" style="margin-top: 15px;">';
html += '<thead><tr><th colspan="2" style="background-color: #f5f5f5;">Address</th></tr></thead><tbody>';
html += field_row('Personal Address', data.personal_address);
html += field_row('Personal City', data.personal_city);
html += field_row('Personal District', data.personal_district);
html += field_row('Work Address OID', data.work_address_oid);
html += field_row('Work Address Street', data.work_address_street);
html += field_row('Work Address District', data.work_address_district);
html += '</tbody></table>';
html += '<table class="table table-bordered" style="margin-top: 15px;">';
html += '<thead><tr><th colspan="2" style="background-color: #f5f5f5;">Contact</th></tr></thead><tbody>';
html += field_row('Contact Phone', data.contact_phone);
html += field_row('Contact Email', data.contact_email);
html += '</tbody></table>';
html += '<table class="table table-bordered" style="margin-top: 15px;">';
html += '<thead><tr><th colspan="2" style="background-color: #f5f5f5;">Education</th></tr></thead><tbody>';
html += field_row('Education', data.education);
html += field_row('Scientific Degree', data.scientific_degree);
html += field_row('Speciality Degree', data.speciality_degree);
html += field_row('State Speciality Degree', data.state_speciality_degree);
html += '</tbody></table>';
$('#amas_personal_data').html(html);
}
// Populate Documents tab
function populate_amas_documents_tab(data) {
let html = '<table class="table table-bordered" style="margin-top: 15px;">';
html += '<thead><tr><th colspan="2" style="background-color: #f5f5f5;">Document Information</th></tr></thead><tbody>';
html += field_row('Document Number', data.document_number);
html += field_row('Document Issue Date', data.document_issue_date);
html += field_row('Document Expiry Date', data.document_expiry_date);
html += field_row('Document Issued By', data.document_issued_by);
html += '</tbody></table>';
html += '<table class="table table-bordered" style="margin-top: 15px;">';
html += '<thead><tr><th colspan="2" style="background-color: #f5f5f5;">Employer Information</th></tr></thead><tbody>';
html += field_row('Employer PIN', data.employer_pin);
html += field_row('Employer TIN', data.employer_tin);
html += field_row('Chairman PIN', data.chairman_pin);
html += '</tbody></table>';
html += '<table class="table table-bordered" style="margin-top: 15px;">';
html += '<thead><tr><th colspan="2" style="background-color: #f5f5f5;">Additional Metadata</th></tr></thead><tbody>';
html += field_row('Document State', data.doc_state);
html += field_row('Created Date', data.created_date);
html += field_row('Labour Function', data.labour_function);
html += field_row('Activity Name Detail', data.activity_name_detail);
html += field_row('Protection Tools', data.protection_tools);
html += field_row('Payment Other Conditions', data.payment_other_cond);
html += field_row('Rule Text', data.rule_text);
html += '</tbody></table>';
html += '<table class="table table-bordered" style="margin-top: 15px;">';
html += '<thead><tr><th colspan="2" style="background-color: #f5f5f5;">System Fields</th></tr></thead><tbody>';
html += field_row('Job Start Date', data.emp_job_start_date);
html += field_row('Sign Type', data.sign_type);
html += field_row('Is Valid', data.is_valid);
html += field_row('Duration State', data.dur_state);
html += field_row('Operation', data.operation);
html += field_row('After Execution Label', data.after_exec_label);
html += field_row('Tree Path Name', data.tree_path_name);
html += field_row('Document OID', data.doc_oid);
html += '</tbody></table>';
$('#amas_documents_data').html(html);
}
// Populate Integration tab
function populate_amas_integration_tab(data) {
let html = '<table class="table table-bordered" style="margin-top: 15px;">';
html += '<thead><tr><th colspan="2" style="background-color: #f5f5f5;">Integration Information</th></tr></thead><tbody>';
html += field_row('AMAS Employee Record', data.name);
html += field_row('Import Date', data.import_date);
html += field_row('Organization Name', data.organization_name);
html += field_row('Organization VOEN', data.organization_voen);
html += field_row('Asan Login', data.asan_login);
html += field_row('Employee', data.employee);
html += '</tbody></table>';
html += '<div style="margin-top: 15px; padding: 15px; background-color: #f8f9fa; border-radius: 4px;">';
html += '<p style="margin-bottom: 10px;"><strong>Actions:</strong></p>';
html += '<button class="btn btn-primary btn-sm" onclick="frappe.set_route(\'Form\', \'Amas Employees\', \'' + data.name + '\');">';
html += '<i class="fa fa-external-link"></i> View Full AMAS Record</button>';
html += '</div>';
$('#amas_integration_data').html(html);
}
// Bootstrap 4.6 stacked-modal fix (twbs/bootstrap#4182): when any modal hides,
// BS4 unconditionally strips `modal-open` from <body>, which orphans the
// backdrop of any modal still open underneath (e.g. progress + nested confirm).
// Re-apply the class if at least one modal is still visible. Idempotent: the
// flag prevents duplicate listeners if this file is re-loaded.
if (!window._amas_stacked_modal_fix_installed) {
window._amas_stacked_modal_fix_installed = true;
$(document).on('hidden.bs.modal', '.modal', function () {
if ($('.modal.show').length) {
$('body').addClass('modal-open');
}
});
}
// List view — add "Load from ƏMAS" button
if (frappe.listview_settings['Employee']) {
const orig_onload = frappe.listview_settings['Employee'].onload;
frappe.listview_settings['Employee'].onload = function(listview) {
if (orig_onload) orig_onload(listview);
add_amas_load_button(listview);
reattach_amas_import_if_running(listview);
};
} else {
frappe.listview_settings['Employee'] = {
onload: function(listview) {
add_amas_load_button(listview);
reattach_amas_import_if_running(listview);
}
};
}
function add_amas_load_button(listview) {
listview.page.add_inner_button(__('Load from ƏMAS'), function() {
start_amas_employee_import(listview);
});
}
// On listview mount, check if a worker is currently running an import for any
// of the user's connected Asan Logins. If yes, re-register the realtime
// listeners so the user can see progress + Cancel on the bar even after a
// page reload.
function reattach_amas_import_if_running(listview) {
frappe.call({
method: 'invoice_az.amas_api.get_connected_asan_logins',
callback: function(r) {
if (!r.message || !r.message.success) return;
const asan_logins = r.message.asan_logins || [];
if (!asan_logins.length) return;
// Probe each login until we find one with an active job.
const tryNext = function(idx) {
if (idx >= asan_logins.length) return;
const name = asan_logins[idx].name;
frappe.call({
method: 'invoice_az.amas_api.get_amas_import_status',
args: { asan_login_name: name },
callback: function(r2) {
if (r2.message && r2.message.running) {
attach_amas_import_listeners(listview, name, 0);
} else {
tryNext(idx + 1);
}
}
});
};
tryNext(0);
}
});
}
// Step 1: Get default connected Asan Login → fetch employees from ƏMAS API
function start_amas_employee_import(listview) {
frappe.call({
method: 'invoice_az.amas_api.get_connected_asan_logins',
freeze: true,
freeze_message: __('Checking ƏMAS connection...'),
callback: function(r) {
if (!r.message || !r.message.success) {
frappe.msgprint({
title: __('Error'),
indicator: 'red',
message: r.message ? r.message.message : __('Failed to check ƏMAS connections')
});
return;
}
const asan_logins = r.message.asan_logins || [];
if (asan_logins.length === 0) {
// No connected ƏMAS - start connection process automatically
start_amas_connection_for_import(listview);
return;
}
// Use the first (default) connected login
fetch_employees_from_amas(listview, asan_logins[0].name, asan_logins[0].organization_name);
}
});
}
// Start ƏMAS connection process automatically for import
function start_amas_connection_for_import(listview) {
// Get first available Asan Login
frappe.call({
method: 'frappe.client.get_list',
args: {
doctype: 'Asan Login',
fields: ['name', 'phone', 'user_id'],
limit_page_length: 1
},
callback: function(r) {
if (!r.message || r.message.length === 0) {
frappe.msgprint({
title: __('No Asan Login Found'),
indicator: 'red',
message: __('Please create an Asan Login first.')
});
return;
}
const asan_login = r.message[0];
// Show confirmation dialog (same design as e-taxes)
const d = new frappe.ui.Dialog({
title: __('ƏMAS Connection Required'),
fields: [
{
fieldname: 'message',
fieldtype: 'HTML',
options: `
<div class="text-center">
<p>ƏMAS connection is required to import employees.</p>
<p>Would you like to connect now?</p>
</div>
`
}
],
primary_action_label: __('Connect'),
primary_action: function() {
d.hide();
start_mygovid_then_amas_for_employees(asan_login.name, function() {
// After successful connection, restart import
start_amas_employee_import(listview);
}, true);
},
secondary_action_label: __('Cancel')
});
d.show();
}
});
}
// Step 2: Fetch employees from ƏMAS API
function fetch_employees_from_amas(listview, asan_login_name, organization_name) {
frappe.show_alert({
message: __('Fetching employees from ƏMAS...'),
indicator: 'blue'
}, 3);
frappe.call({
method: 'invoice_az.amas_api.get_employees_report',
args: {
asan_login_name: asan_login_name,
offset: 0,
limit: 1000
},
freeze: true,
freeze_message: __('Loading employees from') + ' ' + organization_name + '...',
callback: function(r) {
if (!r.message || !r.message.success) {
// Check for CSRF error - need to reconnect
if (r.message && r.message.csrf_error) {
show_amas_reconnect_dialog(asan_login_name, function() {
// Retry after successful reconnection
fetch_employees_from_amas(listview, asan_login_name, organization_name);
});
return;
}
if (r.message && r.message.permission_error) {
show_amas_permission_error_dialog(
asan_login_name,
r.message.message,
function() {
fetch_employees_from_amas(listview, asan_login_name, organization_name);
}
);
return;
}
frappe.msgprint({
title: __('Error'),
indicator: 'red',
message: r.message ? r.message.message : __('Failed to load employees from ƏMAS')
});
return;
}
const employees = r.message.employees || [];
if (employees.length === 0) {
frappe.msgprint({
title: __('Information'),
indicator: 'blue',
message: __('No employees found in ƏMAS for this organization.')
});
return;
}
frappe.show_alert({
message: __('Processing ') + employees.length + __(' employees...'),
indicator: 'blue'
}, 3);
check_existing_and_show(listview, asan_login_name, organization_name, employees);
}
});
}
// Step 3: Check existing employees and show selection dialog
function check_existing_and_show(listview, asan_login_name, organization_name, employees) {
const fins = employees
.map(e => e.identification_number)
.filter(f => f);
frappe.call({
method: 'frappe.client.get_list',
args: {
doctype: 'Employee',
filters: { passport_number: ['in', fins] },
fields: ['name', 'passport_number', 'employee_name'],
limit_page_length: 0
},
callback: function(r) {
const existing_map = {};
if (r.message) {
r.message.forEach(function(emp) {
if (emp.passport_number) {
existing_map[emp.passport_number] = emp.name;
}
});
}
show_employee_selection(listview, asan_login_name, organization_name, employees, existing_map);
}
});
}
// Step 4: Show selection dialog (e-taxes style)
function show_employee_selection(listview, asan_login_name, organization_name, employees, existing_map) {
const new_count = employees.filter(e => !existing_map[e.identification_number]).length;
const existing_count = employees.length - new_count;
// Build table (e-taxes style)
// Таблица сотрудников — единственная прокручиваемая область. Высота привязана
// к вьюпорту (calc), чтобы модал целиком помещался в экран. Шапка «липкая».
let invoiceTable = '<div class="amas-employees-scroll" style="max-height: calc(100vh - 360px); min-height: 220px; overflow-y: auto; border: 1px solid var(--border-color); border-radius: 6px;">' +
'<table class="table table-bordered amas-employees-table" style="width: 100%; table-layout: fixed; margin-bottom: 0;">' +
'<thead style="position: sticky; top: 0; z-index: 1;"><tr>' +
'<th style="width: 4%; background: var(--control-bg, var(--bg-color));"><input type="checkbox" class="select-all-employees"></th>' +
'<th style="width: 20%; background: var(--control-bg, var(--bg-color));">' + __('Full Name') + '</th>' +
'<th style="width: 10%; background: var(--control-bg, var(--bg-color));">' + __('FIN') + '</th>' +
'<th style="width: 20%; background: var(--control-bg, var(--bg-color));">' + __('Position') + '</th>' +
'<th style="width: 8%; background: var(--control-bg, var(--bg-color));">' + __('Salary') + '</th>' +
'<th style="width: 12%; background: var(--control-bg, var(--bg-color));">' + __('Status') + '</th>' +
'<th style="width: 10%; background: var(--control-bg, var(--bg-color));">' + __('Begin Date') + '</th>' +
'<th style="width: 16%; background: var(--control-bg, var(--bg-color));">' + __('In System') + '</th>' +
'</tr></thead><tbody>';
employees.forEach(function(emp, idx) {
const fin = emp.identification_number || '';
const existing_id = existing_map[fin];
const status_cell = existing_id
? '<span class="indicator-pill green" style="font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; display: inline-block;">' + frappe.utils.escape_html(existing_id) + '</span>'
: '';
invoiceTable += '<tr>' +
'<td><input type="checkbox" class="select-employee" data-index="' + idx + '"></td>' +
'<td style="word-break: break-word;">' + frappe.utils.escape_html(emp.full_name || '') + '</td>' +
'<td>' + frappe.utils.escape_html(fin) + '</td>' +
'<td style="word-break: break-word;">' + frappe.utils.escape_html(emp.work_position_text || '') + '</td>' +
'<td style="text-align: right;">' + frappe.utils.escape_html(emp.monthly_salary || '') + '</td>' +
'<td>' + frappe.utils.escape_html(emp.contract_status || '') + '</td>' +
'<td>' + frappe.utils.escape_html(emp.begin_date || '') + '</td>' +
'<td>' + status_cell + '</td>' +
'</tr>';
});
invoiceTable += '</tbody></table></div>';
// Компактная шапка: счётчик сотрудников слева + организация справа в одну
// строку. Заменяет alert со счётчиком и убирает пустые Section Break-лейблы.
const headerHtml =
'<div style="display:flex; justify-content:space-between; align-items:center; gap:12px; flex-wrap:wrap; margin-bottom:8px;">' +
'<span class="indicator-pill blue" style="font-size:13px;">' +
__('Employees found: ') + '<strong>' + employees.length + '</strong>' +
' (' + __('New') + ': ' + new_count + ', ' + __('Already in system') + ': ' + existing_count + ')' +
'</span>' +
(organization_name
? '<span style="color: var(--text-muted);">' +
__('Organization: ') + '<strong>' + frappe.utils.escape_html(organization_name) + '</strong>' +
'</span>'
: '') +
'</div>';
const default_company = frappe.defaults.get_user_default('Company') || frappe.defaults.get_global_default('company');
const d = new frappe.ui.Dialog({
title: __('Select employees from ƏMAS'),
size: 'large',
fields: [
{
fieldname: 'header_html',
fieldtype: 'HTML',
options: headerHtml
},
{
fieldname: 'create_designation',
fieldtype: 'Check',
label: __('Create Designation if not found'),
default: 1
},
{
fieldname: 'employees_html',
fieldtype: 'HTML',
options: invoiceTable
}
],
primary_action_label: __('Load selected'),
primary_action: function(values) {
const selected_employees = [];
d.$wrapper.find('.select-employee:checked').each(function() {
const idx = $(this).data('index');
selected_employees.push(employees[idx]);
});
if (selected_employees.length === 0) {
frappe.msgprint({
title: __('Warning'),
indicator: 'orange',
message: __('No employees selected')
});
return;
}
d.hide();
create_employees(listview, asan_login_name, selected_employees, default_company, values.create_designation);
},
secondary_action_label: __('Cancel'),
secondary_action: function() { d.hide(); }
});
// Match e-taxes dialog sizing
d.$wrapper.find('.modal-dialog').css({
'max-width': '80%',
'width': '80%',
'margin': '30px auto'
});
d.$wrapper.find('.modal-body').css({
'padding': '15px'
});
d.show();
// Select all checkbox
d.$wrapper.find('.select-all-employees').on('change', function() {
const isChecked = $(this).prop('checked');
d.$wrapper.find('.select-employee').prop('checked', isChecked);
});
}
// Step 5: Create employees via backend - Socket.IO bulk import
// Wire realtime listeners for an in-flight ƏMAS import. Used both when the
// user starts a new import and when reattaching to an already-running one
// after a page reload.
function attach_amas_import_listeners(listview, asan_login_name, total_hint) {
const total = total_hint || 0;
// Reused single confirm dialog. frappe.confirm creates a fresh dialog on
// each call and never removes the old element from DOM, which causes
// Bootstrap's modal-backdrop stack to glitch on repeat use.
let cancel_confirm_dialog = null;
const ensure_cancel_button = function(dialog) {
if (!dialog || dialog._amas_cancel_attached) return;
dialog._amas_cancel_attached = true;
dialog.set_primary_action(__('Cancel Import'), function() {
if (!cancel_confirm_dialog) {
cancel_confirm_dialog = new frappe.ui.Dialog({
title: __('Confirm'),
primary_action_label: __('Yes, cancel'),
primary_action: function() {
cancel_confirm_dialog.hide();
frappe.call({
method: 'invoice_az.amas_api.cancel_amas_import',
args: { asan_login_name: asan_login_name },
callback: function() {
frappe.show_alert({
message: __('Cancellation requested. Finishing current employee...'),
indicator: 'orange'
}, 5);
}
});
},
secondary_action_label: __('No, keep importing'),
secondary_action: function() {
cancel_confirm_dialog.hide();
}
});
cancel_confirm_dialog.$body.append(
'<p>' + __('Cancel the running ƏMAS import? Already-saved employees will stay; the rest will be skipped.') + '</p>'
);
}
cancel_confirm_dialog.show();
// Progress dialog forces z-index 2000; lift the confirm above it.
cancel_confirm_dialog.$wrapper.css('z-index', 2100);
});
};
// Hand the reusable confirm dialog over to the import-complete handler so
// it can fully remove the leftover element from DOM when the import ends.
const cleanup_cancel_dialog = function() {
if (cancel_confirm_dialog) {
cancel_confirm_dialog.hide();
cancel_confirm_dialog.$wrapper.remove();
cancel_confirm_dialog = null;
}
};
// Защита от гонки realtime: 'complete' вызывает hide_progress() (обнуляет
// frappe.cur_progress). Опоздавший 'progress' иначе пересоздаёт модал, который
// уже никто не закрывает. Флаг делает обработчик прогресса no-op после finish.
let importFinished = false;
// True once we've observed the import actually running (via a realtime tick
// or a status poll). Guards the poll-based completion detector so it can't
// fire before the worker has even started (the running flag is set
// server-side a beat after import_bulk_employees is called).
let sawRunning = (total > 0);
// Highest "current" rendered so far. Realtime and polling both feed the bar;
// this keeps it monotonic so a stale poll can't rewind a fresher realtime
// tick (or vice-versa).
let lastCurrent = 0;
let knownTotal = total || 0;
let dialog = frappe.show_progress(
__('Importing from ƏMAS'), 0, total,
total
? __('Starting import of {0} employees...', [total])
: __('Importing employees, please wait...')
);
ensure_cancel_button(dialog);
// Single entry point for moving the bar, fed by both the realtime handler
// and the status poll. Never regresses; tolerates an unknown total (0).
const renderProgress = function(current, totalArg, phase, employee_name) {
if (importFinished) return;
if (totalArg && totalArg > 0) knownTotal = totalArg;
if (typeof current === 'number' && current > lastCurrent) lastCurrent = current;
const is_save = phase === 'save';
const verb = is_save ? __('Saving: ') : __('Fetching: ');
let detail;
if (employee_name) {
detail = verb + employee_name;
} else if (knownTotal > 0) {
detail = verb + __('{0} of {1}', [lastCurrent, knownTotal]);
} else {
// Running, but no count yet (first tick not in, or server hasn't
// populated progress). Keep it reassuring rather than alarming.
detail = __('Importing employees, please wait...');
}
// Guard against total=0 (NaN width); show_progress reuses the dialog.
const d = frappe.show_progress(
__('Importing from ƏMAS'), lastCurrent, knownTotal > 0 ? knownTotal : 1, detail
);
ensure_cancel_button(d);
};
frappe.realtime.off('amas_import_progress');
frappe.realtime.on('amas_import_progress', function(data) {
if (importFinished) return;
sawRunning = true;
renderProgress(data.current, data.total, data.phase, data.employee_name);
});
// Polling fallback. Realtime events are transient — anything published
// before this page subscribed (after the wizard redirects to /app, or a
// reload) is lost, leaving the bar frozen. Polling the server-persisted
// progress guarantees the bar moves, and lets us detect completion even
// when the 'amas_import_complete' event itself was missed.
const finishFromPoll = function() {
if (importFinished) return;
importFinished = true;
frappe.realtime.off('amas_import_progress');
frappe.realtime.off('amas_import_complete');
closeAmasProgress(); // also clears amasPollTimer
cleanup_cancel_dialog();
listview.refresh();
frappe.show_alert({ message: __('ƏMAS import finished.'), indicator: 'green' }, 5);
};
const pollStatus = function() {
if (importFinished) return;
frappe.call({
method: 'invoice_az.amas_api.get_amas_import_status',
args: { asan_login_name: asan_login_name },
callback: function(r) {
if (importFinished) return;
const m = r.message || {};
if (m.running) {
sawRunning = true;
renderProgress(m.current, m.total, m.phase, m.employee_name);
} else if (sawRunning) {
// We watched it run and now it's gone — the realtime
// 'complete' was missed. Wrap up gracefully.
finishFromPoll();
}
}
});
};
if (amasPollTimer) clearInterval(amasPollTimer);
amasPollTimer = setInterval(pollStatus, 2000);
// Reconnect path (no total hint): the caller already confirmed an import is
// running, so seed the bar from real numbers immediately instead of waiting
// a full poll interval on the bare "reconnecting" text.
if (!total) pollStatus();
frappe.realtime.off('amas_import_complete');
frappe.realtime.on('amas_import_complete', function(data) {
if (importFinished) return; // poll-based finish already wrapped up
importFinished = true;
frappe.realtime.off('amas_import_progress');
frappe.realtime.off('amas_import_complete');
closeAmasProgress();
cleanup_cancel_dialog();
if (data.cancelled) {
frappe.msgprint({
title: __('Import Cancelled'),
indicator: 'orange',
message: __(
'Cancelled. Created: {0}, updated: {1}.',
[data.created || 0, data.updated || 0]
)
});
listview.refresh();
return;
}
if (data.aborted) {
const reason = (data.errors && data.errors[0] && data.errors[0].error)
|| __('Import was aborted.');
frappe.msgprint({
title: __('Import Aborted'),
indicator: 'red',
message: reason
});
listview.refresh();
return;
}
const error_messages = (data.errors || []).map(function(err) {
return (err.employee || 'Unknown') + ': ' + (err.error || 'Unknown error');
});
const total_processed = data.created + data.updated + (data.errors || []).length;
show_import_summary(total_processed, data.created, data.updated, (data.errors || []).length, error_messages);
listview.refresh();
});
}
// Module-level handle for the status-poll interval (fallback that drives the
// progress bar when transient realtime events are missed). Only one ƏMAS import
// is ever tracked at a time, so a single timer is enough. closeAmasProgress()
// is the universal teardown path, so clearing it there covers every exit.
let amasPollTimer = null;
// Надёжное закрытие прогресс-бара: frappe.hide_progress() в этом окружении
// (jey_theme/Bootstrap) не всегда убирает модал — он зависает. Принудительно.
function closeAmasProgress() {
if (amasPollTimer) {
clearInterval(amasPollTimer);
amasPollTimer = null;
}
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');
}
}
function create_employees(listview, asan_login_name, selected_employees, company, create_designation) {
const total = selected_employees.length;
attach_amas_import_listeners(listview, asan_login_name, total);
frappe.call({
method: 'invoice_az.amas_api.import_bulk_employees',
args: {
asan_login_name: asan_login_name,
employees_data: JSON.stringify(selected_employees),
company: company,
create_designation: create_designation ? 1 : 0
},
callback: function(r) {
if (r.message && r.message.already_running) {
frappe.realtime.off('amas_import_progress');
frappe.realtime.off('amas_import_complete');
closeAmasProgress();
frappe.msgprint({
title: __('Import Already Running'),
indicator: 'orange',
message: r.message.message
});
return;
}
if (!r.message || !r.message.enqueued) {
frappe.realtime.off('amas_import_progress');
frappe.realtime.off('amas_import_complete');
closeAmasProgress();
frappe.msgprint({
title: __('Error'),
indicator: 'red',
message: __('Failed to start import job')
});
}
},
error: function() {
frappe.realtime.off('amas_import_progress');
frappe.realtime.off('amas_import_complete');
closeAmasProgress();
frappe.msgprint({
title: __('Error'),
indicator: 'red',
message: __('Network error starting import')
});
}
});
}
// Process employees one by one (like E-Taxes loadSelectedInvoices)
function process_employees_one_by_one(listview, asan_login_name, employees_queue, company, create_designation,
created_count, updated_count, error_count, errors, current_index) {
// Check if finished
if (employees_queue.length === 0) {
closeAmasProgress();
$(document).off('progress-cancel.loading_amas_employees');
const total = created_count + updated_count + error_count;
show_import_summary(total, created_count, updated_count, error_count, errors);
window.amas_employees_to_process = null;
listview.refresh();
return;
}
// Check if cancelled
if (window.amas_cancel_loading) {
employees_queue = [];
closeAmasProgress();
$(document).off('progress-cancel.loading_amas_employees');
const total = created_count + updated_count + error_count;
frappe.show_alert({
message: __('Import cancelled. Processed ') + total + __(' employees.'),
indicator: 'orange'
}, 5);
listview.refresh();
return;
}
// Take first employee from queue
const emp_data = employees_queue.shift();
const is_last = employees_queue.length === 0;
const total_to_process = window.amas_employees_to_process || (created_count + updated_count + error_count + employees_queue.length + 1);
current_index++;
// Show progress
frappe.show_progress(
__('Loading from ƏMAS'),
current_index - 1,
total_to_process,
__('Processing employee ') + current_index + __(' of ') + total_to_process,
null,
true
);
// Setup cancel handler
$(document).off('progress-cancel.loading_amas_employees');
$(document).on('progress-cancel.loading_amas_employees', function() {
window.amas_cancel_loading = true;
frappe.show_alert({
message: __('Cancelling import... Finishing current employee.'),
indicator: 'orange'
}, 3);
closeAmasProgress();
});
// Get employee name for progress message
const emp_name = emp_data.full_name || emp_data.identification_number || 'Employee';
// Update progress with employee name
frappe.show_progress(
__('Loading from ƏMAS'),
current_index - 1,
total_to_process,
__('Processing: ') + emp_name
);
// Create/update single employee
frappe.call({
method: 'invoice_az.amas_api.create_single_employee_from_amas',
args: {
asan_login_name: asan_login_name,
emp_data: emp_data,
company: company,
create_designation: create_designation ? 1 : 0
},
callback: function(r) {
if (r.message && r.message.success) {
// Success - increment counters
if (r.message.action === 'created') {
created_count++;
frappe.show_alert({
message: __('Created: ') + r.message.full_name,
indicator: 'green'
}, 2);
} else if (r.message.action === 'updated') {
updated_count++;
frappe.show_alert({
message: __('Updated: ') + r.message.full_name,
indicator: 'blue'
}, 2);
}
} else {
// Error
error_count++;
const error_msg = r.message ? r.message.message : 'Unknown error';
errors.push(error_msg);
frappe.show_alert({
message: __('Error: ') + (r.message ? r.message.full_name : emp_name),
indicator: 'red'
}, 3);
}
// Continue with next employee after short delay (like E-Taxes)
setTimeout(function() {
process_employees_one_by_one(
listview,
asan_login_name,
employees_queue,
company,
create_designation,
created_count,
updated_count,
error_count,
errors,
current_index
);
}, 50); // 50ms delay like E-Taxes PROGRESS_UPDATE_DELAY
},
error: function(xhr, status, error) {
// Network error
error_count++;
const error_msg = emp_name + ': Network error - ' + (error || 'Unknown error');
errors.push(error_msg);
frappe.show_alert({
message: __('Network error: ') + emp_name,
indicator: 'red'
}, 3);
// Continue with next employee
setTimeout(function() {
process_employees_one_by_one(
listview,
asan_login_name,
employees_queue,
company,
create_designation,
created_count,
updated_count,
error_count,
errors,
current_index
);
}, 50);
}
});
}
// Show final summary (like E-Taxes)
function show_import_summary(total, created_count, updated_count, error_count, errors) {
if (error_count === 0) {
let msg = __('All ') + total + __(' employees were processed successfully.');
if (created_count > 0) {
msg += '<br>' + __('Created: ') + '<strong>' + created_count + '</strong>';
}
if (updated_count > 0) {
msg += '<br>' + __('Updated: ') + '<strong>' + updated_count + '</strong>';
}
frappe.msgprint({
title: __('Import Completed Successfully'),
indicator: 'green',
message: msg
});
} else {
let msg = '<div style="margin-bottom: 15px;">';
if (created_count > 0) {
msg += __('Successfully created: ') + '<strong>' + created_count + '</strong><br>';
}
if (updated_count > 0) {
msg += __('Updated: ') + '<strong>' + updated_count + '</strong><br>';
}
msg += __('Failed: ') + '<strong>' + error_count + '</strong>';
msg += '</div>';
if (errors && errors.length > 0) {
msg += '<div style="max-height: 200px; overflow-y: auto;">';
msg += '<ul style="color: red; font-size: 12px; margin: 0; padding-left: 20px;">';
errors.slice(0, 10).forEach(function(err) {
msg += '<li>' + frappe.utils.escape_html(err) + '</li>';
});
if (errors.length > 10) {
msg += '<li><em>... and ' + (errors.length - 10) + ' more errors</em></li>';
}
msg += '</ul></div>';
}
frappe.msgprint({
title: created_count > 0 ? __('Import Completed with Errors') : __('Import Failed'),
indicator: created_count > 0 ? 'orange' : 'red',
message: msg
});
}
}
// Show a confirm dialog for ƏMAS permission errors with a Reconnect action.
// On confirm: runs full MyGovID + ƏMAS reauth and shows the organization
// selection dialog so the user can pick a different certificate.
function show_amas_permission_error_dialog(asan_login_name, message, retry_callback) {
const body = (message || __("The selected ƏMAS certificate doesn't have permission for this operation."))
+ '<br><br>'
+ __('Would you like to reconnect to ƏMAS and choose a different organization?');
frappe.confirm(
body,
function() {
start_mygovid_then_amas_for_employees(asan_login_name, retry_callback, true);
}
);
}
// Show reconnection dialog for ƏMAS when token expires (CSRF error)
function show_amas_reconnect_dialog(asan_login_name, retry_callback) {
const d = new frappe.ui.Dialog({
title: __('ƏMAS Authentication Required'),
fields: [
{
fieldname: 'message',
fieldtype: 'HTML',
options: `
<div class="text-center" style="padding: 20px;">
<p style="font-size: 16px; margin-bottom: 15px;">
<i class="fa fa-exclamation-triangle" style="color: #f39c12; font-size: 48px;"></i>
</p>
<p style="font-size: 14px;">Your ƏMAS session has expired or authentication is required.</p>
<p style="font-size: 14px; margin-bottom: 0;">Would you like to reconnect to ƏMAS?</p>
</div>
`
}
],
primary_action_label: __('Reconnect'),
primary_action: function() {
d.hide();
// Try to reconnect to ƏMAS
frappe.call({
method: 'invoice_az.amas_api.connect_amas',
args: {
'asan_login_name': asan_login_name
},
freeze: true,
freeze_message: __('Reconnecting to ƏMAS...'),
callback: function(r) {
if (r.message && r.message.success) {
// Check if we need to select organization
const accounts = r.message.accounts || [];
if (accounts.length > 0) {
// Show organization selection dialog
show_amas_accounts_dialog(asan_login_name, accounts, retry_callback);
} else {
// No accounts to select, just proceed
frappe.show_alert({
message: __('ƏMAS reconnected successfully!'),
indicator: 'green'
}, 3);
// Retry the original operation
if (typeof retry_callback === 'function') {
setTimeout(retry_callback, 500);
}
}
} else if (r.message && r.message.need_mygovid_login) {
// MyGovID token expired - start full authentication flow with organization selection
start_mygovid_then_amas_for_employees(asan_login_name, retry_callback, true);
} else {
frappe.msgprint({
title: __('Reconnection Failed'),
indicator: 'red',
message: r.message ? r.message.message : __('Failed to reconnect to ƏMAS.')
});
}
}
});
},
secondary_action_label: __('Cancel')
});
d.show();
}
// Start MyGovID authentication followed by ƏMAS connection (for employees)
// show_org_selection: true = show organization selection dialog, false = auto-select first org
function start_mygovid_then_amas_for_employees(asan_login_name, retry_callback, show_org_selection) {
// Default to false (auto-select) for backward compatibility
if (show_org_selection === undefined) {
show_org_selection = false;
}
frappe.show_alert({
message: __('Starting MyGovID authentication...'),
indicator: 'blue'
}, 3);
// Create status dialog
const status_dialog = new frappe.ui.Dialog({
title: __('MyGovID Authentication for ƏMAS'),
fields: [
{
fieldname: 'status_html',
fieldtype: 'HTML',
options: `
<div class="text-center">
<div class="mygovid-spinner" style="display: inline-block; width: 64px; height: 64px;">
<div style="width: 64px; height: 64px; border: 6px solid #4e73df; border-radius: 50%; border-color: #4e73df transparent #4e73df transparent; animation: mygovid-spin 1.2s linear infinite;"></div>
</div>
<style>
@keyframes mygovid-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
<div class="mt-3" id="mygovid_status">
<h4>Waiting for confirmation on your phone</h4>
<p>Please check your phone and confirm the authentication request in ASAN Sign app.</p>
</div>
<div id="mygovid_code" style="display:none; margin-top: 15px;">
<div style="background: #f8f9fa; padding: 10px; display: inline-block; border-radius: 5px; font-size: 24px; font-weight: bold; letter-spacing: 2px; color: #333; border: 1px solid #ddd;">
<span id="mygovid_code_text">----</span>
</div>
<p style="margin-top: 5px; font-size: 12px; color: #666;">Verification Code</p>
</div>
</div>
`
}
],
primary_action_label: __('Cancel'),
primary_action: function() {
window.mygovidStop = true;
status_dialog.hide();
}
});
status_dialog.show();
window.mygovidStop = false;
// Start MyGovID login
frappe.call({
method: 'invoice_az.invoice_az.doctype.asan_login.asan_login.start_mygovid_login',
args: {
'asan_login_name': asan_login_name
},
timeout: 130000,
callback: function(r) {
if (window.mygovidStop) return;
if (r.message && r.message.success) {
// MyGovID login successful, now connect to ƏMAS
$('#mygovid_status').html(`
<h4 style="color: blue;">Connecting to ƏMAS...</h4>
<p>MyGovID authentication successful. Now connecting to ƏMAS...</p>
`);
$('#mygovid_code').hide();
// Connect to ƏMAS
frappe.call({
method: 'invoice_az.amas_api.connect_amas',
args: {
'asan_login_name': asan_login_name
},
callback: function(r2) {
if (r2.message && r2.message.success) {
const accounts = r2.message.accounts || [];
if (accounts.length > 0) {
// Check if we should show selection dialog or auto-select
if (show_org_selection) {
// Show organization selection dialog (for re-authentication)
status_dialog.hide();
show_amas_accounts_dialog(asan_login_name, accounts, retry_callback);
} else {
// Auto-select first organization (for initial connection)
const first_account = accounts[0];
const oid = first_account.accountOid || first_account.oid || '';
const name = first_account.accountName || first_account.name || 'Unknown';
const number = first_account.accountNumber || first_account.voen || first_account.number || '';
const role = first_account.roleName || first_account.role || 'chairman';
$('#mygovid_status').html(`
<h4 style="color: blue;">Selecting organization...</h4>
<p>ƏMAS connected. Selecting ${frappe.utils.escape_html(name)}...</p>
`);
// Auto-select first account
frappe.call({
method: 'invoice_az.amas_api.change_amas_account',
args: {
'asan_login_name': asan_login_name,
'account_oid': oid,
'account_name': name,
'account_number': number,
'role_name': role
},
callback: function(r3) {
if (r3.message && r3.message.success) {
$('#mygovid_status').html(`
<h4 style="color: green;">ƏMAS Connected!</h4>
<p>Successfully connected to ${frappe.utils.escape_html(name)}.</p>
`);
$('.mygovid-spinner').html(`
<div style="width: 64px; height: 64px; display: flex; align-items: center; justify-content: center;">
<i class="fa fa-check" style="font-size: 48px; color: #5cb85c;"></i>
</div>
`);
setTimeout(function() {
status_dialog.hide();
if (typeof retry_callback === 'function') {
retry_callback();
}
}, 2000);
} else {
$('#mygovid_status').html(`
<h4 style="color: red;">Organization Selection Failed</h4>
<p>${r3.message ? r3.message.message : 'Failed to select organization'}</p>
`);
$('.mygovid-spinner').css('display', 'none');
status_dialog.set_primary_action(__('Close'), function() {
status_dialog.hide();
});
}
}
});
}
} else {
// No accounts - just complete
$('#mygovid_status').html(`
<h4 style="color: green;">ƏMAS Connected!</h4>
<p>Successfully connected to ƏMAS.</p>
`);
$('.mygovid-spinner').html(`
<div style="width: 64px; height: 64px; display: flex; align-items: center; justify-content: center;">
<i class="fa fa-check" style="font-size: 48px; color: #5cb85c;"></i>
</div>
`);
status_dialog.set_primary_action(__('Continue'), function() {
status_dialog.hide();
if (typeof retry_callback === 'function') {
setTimeout(retry_callback, 500);
}
});
setTimeout(function() {
status_dialog.hide();
if (typeof retry_callback === 'function') {
retry_callback();
}
}, 2000);
}
} else {
$('#mygovid_status').html(`
<h4 style="color: red;">ƏMAS Connection Failed</h4>
<p>${r2.message ? r2.message.message : 'Unknown error'}</p>
`);
$('.mygovid-spinner').css('display', 'none');
status_dialog.set_primary_action(__('Close'), function() {
status_dialog.hide();
});
}
}
});
} else {
// MyGovID login failed
$('#mygovid_status').html(`
<h4 style="color: red;">Authentication Failed</h4>
<p>${r.message ? r.message.message : 'Unknown error'}</p>
`);
$('.mygovid-spinner').css('display', 'none');
$('#mygovid_code').hide();
status_dialog.set_primary_action(__('Close'), function() {
status_dialog.hide();
});
}
},
error: function() {
if (window.mygovidStop) return;
$('#mygovid_status').html(`
<h4 style="color: red;">Authentication Timeout</h4>
<p>The request timed out. Please try again.</p>
`);
$('.mygovid-spinner').css('display', 'none');
$('#mygovid_code').hide();
status_dialog.set_primary_action(__('Close'), function() {
status_dialog.hide();
});
}
});
// Get verification code
setTimeout(function() {
frappe.call({
method: 'invoice_az.invoice_az.doctype.asan_login.asan_login.get_mygovid_verification_code',
args: { 'asan_login_name': asan_login_name },
callback: function(r) {
if (r.message && r.message.success) {
$('#mygovid_code_text').text(r.message.code);
$('#mygovid_code').show();
}
}
});
}, 500);
}
// Show ƏMAS accounts selection dialog
function show_amas_accounts_dialog(asan_login_name, accounts, retry_callback) {
if (!accounts || accounts.length === 0) {
frappe.msgprint(__('No organizations found in ƏMAS'));
return;
}
// Build accounts table
let table_html = '<div style="max-height: 400px; overflow-y: auto;">';
table_html += '<table class="table table-bordered table-hover">';
table_html += '<thead><tr><th style="width: 50%;">Organization Name</th><th style="width: 25%;">VOEN</th><th style="width: 15%;">Role</th><th style="width: 10%;">Action</th></tr></thead>';
table_html += '<tbody>';
accounts.forEach(function(account, index) {
const name = account.accountName || account.name || 'Unknown';
const voen = account.accountNumber || account.voen || account.number || '';
const role = account.roleName || account.role || '';
const oid = account.accountOid || account.oid || '';
table_html += `<tr>
<td>${frappe.utils.escape_html(name)}</td>
<td>${frappe.utils.escape_html(voen)}</td>
<td>${frappe.utils.escape_html(role)}</td>
<td>
<button class="btn btn-xs btn-primary select-amas-account"
data-oid="${frappe.utils.escape_html(oid)}"
data-name="${frappe.utils.escape_html(name)}"
data-number="${frappe.utils.escape_html(voen)}"
data-role="${frappe.utils.escape_html(role)}">
Select
</button>
</td>
</tr>`;
});
table_html += '</tbody></table></div>';
const d = new frappe.ui.Dialog({
title: __('Select ƏMAS Organization'),
fields: [
{
fieldname: 'info',
fieldtype: 'HTML',
options: '<div class="alert alert-info" style="margin-bottom: 15px;"><strong>Note:</strong> Please select the organization you want to work with.</div>'
},
{
fieldname: 'accounts_html',
fieldtype: 'HTML',
options: table_html
}
],
primary_action_label: __('Cancel'),
primary_action: function() {
d.hide();
}
});
// Make dialog wider
d.$wrapper.find('.modal-dialog').css({
'max-width': '70%',
'width': '70%'
});
d.show();
// Add click handler for account selection
d.$wrapper.find('.select-amas-account').on('click', function() {
const oid = $(this).data('oid');
const name = $(this).data('name');
const number = $(this).data('number');
const role = $(this).data('role');
// Disable all buttons
d.$wrapper.find('.select-amas-account').prop('disabled', true);
$(this).html('<i class="fa fa-spinner fa-spin"></i> Selecting...');
// Call backend to select account
frappe.call({
method: 'invoice_az.amas_api.change_amas_account',
args: {
'asan_login_name': asan_login_name,
'account_oid': oid,
'account_name': name,
'account_number': number,
'role_name': role || 'chairman'
},
callback: function(r) {
if (r.message && r.message.success) {
d.hide();
frappe.show_alert({
message: __('Organization selected: ') + name,
indicator: 'green'
}, 3);
// Retry the original operation after short delay
if (typeof retry_callback === 'function') {
setTimeout(retry_callback, 500);
}
} else {
// Re-enable buttons on error
d.$wrapper.find('.select-amas-account').prop('disabled', false);
d.$wrapper.find('.select-amas-account').each(function() {
if ($(this).find('.fa-spinner').length) {
$(this).html('Select');
}
});
if (r.message && r.message.permission_error) {
d.hide();
show_amas_permission_error_dialog(
asan_login_name,
r.message.message,
retry_callback
);
return;
}
frappe.msgprint({
title: __('Error'),
indicator: 'red',
message: r.message ? r.message.message : __('Failed to select organization')
});
}
}
});
});
}