1568 lines
68 KiB
JavaScript
1568 lines
68 KiB
JavaScript
// 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')
|
||
});
|
||
}
|
||
}
|
||
});
|
||
});
|
||
}
|