Auto-import all fetched ƏMAS employees; apply default language on boot

- Remove the manual employee selection UI. After loading, the wizard caches
  every record returned by emasValidEmploymentContractOnline and lands on
  the done sub-state. import_bulk_employees updates existing records on
  FIN match, so running on a populated site is safe.
- On constructor, if the booted session lang differs from the wizard's
  default (az), trigger load_messages so the UI renders in that language
  without the user having to re-pick it from the dropdown.
This commit is contained in:
Ali 2026-04-24 11:38:19 +00:00
parent 3f62fea6e6
commit e166da49cc
2 changed files with 47 additions and 93 deletions

View File

@ -1 +1 @@
__version__ = "0.0.7" __version__ = "0.0.8"

View File

@ -10,7 +10,7 @@ frappe.provide("jey_wizard");
// Bump this string in every commit that changes wizard code. Displayed in the badge so // Bump this string in every commit that changes wizard code. Displayed in the badge so
// we can tell at a glance which version is actually running on a given machine. Kept in // we can tell at a glance which version is actually running on a given machine. Kept in
// sync with __version__ in jey_wizard/__init__.py. // sync with __version__ in jey_wizard/__init__.py.
const JEY_WIZARD_VERSION = "0.0.7"; const JEY_WIZARD_VERSION = "0.0.8";
// Wipe Frappe + ERPNext default slides so their `before_load`/`after_load` listeners // Wipe Frappe + ERPNext default slides so their `before_load`/`after_load` listeners
// don't try to mutate a wizard that isn't slide-based anymore. // don't try to mutate a wizard that isn't slide-based anymore.
@ -72,6 +72,15 @@ frappe.setup.SetupWizard = class JeySetupWizard {
this.current_step = 0; this.current_step = 0;
this.steps = ["language", "asan", "employees", "company", "confirm"]; this.steps = ["language", "asan", "employees", "company", "confirm"];
this.render(); this.render();
// Session arrives in whatever language Frappe booted in (usually English for
// the first-run Administrator). Pull the default language's translations
// immediately so every subsequent render runs in that language without the
// user having to re-pick it.
const bootLang = (frappe.boot.lang || "en").split("-")[0];
if (bootLang !== this.data.language) {
this.change_language(this.data.language, /* force */ true);
}
} }
// Frappe's on_page_show calls frappe.wizard.show_slide — no-op for us. // Frappe's on_page_show calls frappe.wizard.show_slide — no-op for us.
@ -334,71 +343,20 @@ frappe.setup.SetupWizard = class JeySetupWizard {
<div style="color:#666">${__("Fetching from ƏMAS...")}</div> <div style="color:#666">${__("Fetching from ƏMAS...")}</div>
</div> </div>
`); `);
} else if (this.amas_state === "select") {
const emps = this.amas_employees || [];
const existing = this.amas_existing_map || {};
const newCount = emps.filter((e) => !existing[e.identification_number]).length;
const existingCount = emps.length - newCount;
const rows = emps.map((e, i) => {
const fin = e.identification_number || "";
const existingId = existing[fin];
const statusCell = existingId
? `<span class="indicator-pill green" style="font-size:11px">${frappe.utils.escape_html(existingId)}</span>`
: "";
return `
<tr>
<td style="padding:6px"><input type="checkbox" class="jey-emp-pick" data-idx="${i}" ${existingId ? "" : "checked"}></td>
<td style="padding:6px">${frappe.utils.escape_html(e.full_name || "")}</td>
<td style="padding:6px;font-family:monospace">${frappe.utils.escape_html(fin)}</td>
<td style="padding:6px">${frappe.utils.escape_html(e.work_position_text || "")}</td>
<td style="padding:6px;text-align:right">${frappe.utils.escape_html(String(e.monthly_salary || ""))}</td>
<td style="padding:6px">${frappe.utils.escape_html(e.contract_status || "")}</td>
<td style="padding:6px">${statusCell}</td>
</tr>
`;
}).join("");
$body.html(`
<h3 style="margin-bottom:12px">${__("Select employees from ƏMAS")}</h3>
<div style="margin-bottom:10px;padding:10px;background:#e3f2fd;border-radius:4px">
${__("Organization:")} <b>${frappe.utils.escape_html(this.data.amas_organization_name || "")}</b><br>
${__("Employees found:")} <b>${emps.length}</b>
(${__("new")}: <b>${newCount}</b>, ${__("already in system")}: <b>${existingCount}</b>)
</div>
<label style="display:block;margin-bottom:10px">
<input type="checkbox" class="jey-emp-create-desig" ${this.data.amas_create_designation ? "checked" : ""}>
${__("Create Designation if not found")}
</label>
<div style="max-height:400px;overflow-y:auto;border:1px solid #eee;border-radius:4px">
<table style="width:100%;border-collapse:collapse;font-size:13px">
<thead style="position:sticky;top:0;background:#fafafa">
<tr>
<th style="padding:8px;text-align:left"><input type="checkbox" class="jey-emp-all"></th>
<th style="padding:8px;text-align:left">${__("Full Name")}</th>
<th style="padding:8px;text-align:left">${__("FIN")}</th>
<th style="padding:8px;text-align:left">${__("Position")}</th>
<th style="padding:8px;text-align:right">${__("Salary")}</th>
<th style="padding:8px;text-align:left">${__("Status")}</th>
<th style="padding:8px;text-align:left">${__("In System")}</th>
</tr>
</thead>
<tbody>${rows}</tbody>
</table>
</div>
`);
$body.find(".jey-emp-all").on("change", (e) => {
const checked = $(e.currentTarget).prop("checked");
$body.find(".jey-emp-pick").prop("checked", checked);
});
} else if (this.amas_state === "done") { } else if (this.amas_state === "done") {
const n = (this.data.amas_selected_employees || []).length; const n = (this.data.amas_selected_employees || []).length;
const existingCount = Object.keys(this.amas_existing_map || {}).length;
const newCount = n - existingCount;
$body.html(` $body.html(`
<h3 style="margin-bottom:16px;color:#080"> ${__("Employees selected")}</h3> <h3 style="margin-bottom:16px;color:#080"> ${__("Employees ready to import")}</h3>
<p>${__("Selected {0} employee(s). They will be imported into ƏMAS Employees and the standard Employee doctype after the wizard finishes.", [n])}</p> <div style="padding:12px;background:#e8f5e9;border-radius:4px;margin-bottom:12px">
${__("Organization:")} <b>${frappe.utils.escape_html(this.data.amas_organization_name || "")}</b><br>
${__("Employees found:")} <b>${n}</b>
(${__("new")}: <b>${newCount < 0 ? n : newCount}</b>, ${__("already in system")}: <b>${existingCount}</b>)
</div>
<p style="color:#666">${__("All employees will be imported into ƏMAS Employees and the standard Employee doctype after the wizard finishes.")}</p>
<div style="margin-top:16px"> <div style="margin-top:16px">
<button class="btn btn-default jey-amas-back-intro">${__("Change selection")}</button> <button class="btn btn-default jey-amas-back-intro">${__("Reload from ƏMAS")}</button>
</div> </div>
`); `);
$body.find(".jey-amas-back-intro").on("click", () => { $body.find(".jey-amas-back-intro").on("click", () => {
@ -479,13 +437,6 @@ frappe.setup.SetupWizard = class JeySetupWizard {
// surfaces a standard Next. // surfaces a standard Next.
showStdNext = this.asan_state === "done"; showStdNext = this.asan_state === "done";
} else if (step === "employees") { } else if (step === "employees") {
if (this.amas_state === "select") {
// Commit selection instead of plain Next.
$(`<button class="btn btn-primary jey-emp-commit">${__("Next")}</button>`)
.appendTo($nav)
.on("click", () => this.amas_commit_selection());
return;
}
// On intro the user uses explicit Skip / Load buttons, not Next. // On intro the user uses explicit Skip / Load buttons, not Next.
// Only the "done" sub-state surfaces a standard Next to move on. // Only the "done" sub-state surfaces a standard Next to move on.
showStdNext = this.amas_state === "done"; showStdNext = this.amas_state === "done";
@ -565,8 +516,9 @@ frappe.setup.SetupWizard = class JeySetupWizard {
// ---------- language ---------- // ---------- language ----------
change_language(code) { change_language(code, force = false) {
if (!code || code === this.data.language) return; if (!code) return;
if (!force && code === this.data.language) return;
const langName = LANG_NAME[code] || "English"; const langName = LANG_NAME[code] || "English";
frappe._messages = {}; frappe._messages = {};
frappe.call({ frappe.call({
@ -943,21 +895,22 @@ frappe.setup.SetupWizard = class JeySetupWizard {
this.show_current_step(); this.show_current_step();
return; return;
} }
this.amas_employees = m.employees || []; const employees = m.employees || [];
if (this.amas_employees.length === 0) { if (employees.length === 0) {
this.$wrap.find(".jey-status").text(__("No employees found in ƏMAS for this organization.")); this.$wrap.find(".jey-status").text(__("No employees found in ƏMAS for this organization."));
this.amas_state = "intro"; this.amas_state = "intro";
this.show_current_step(); this.show_current_step();
return; return;
} }
// Check which FINs are already in the Employee doctype. this.amas_employees = employees;
const fins = this.amas_employees // Populate existing_map purely for the summary UI; the actual import
// updates existing records rather than skipping them, so no filtering.
const fins = employees
.map((e) => e.identification_number) .map((e) => e.identification_number)
.filter(Boolean); .filter(Boolean);
if (fins.length === 0) { if (fins.length === 0) {
this.amas_existing_map = {}; this.amas_existing_map = {};
this.amas_state = "select"; this.amas_commit_selection();
this.show_current_step();
return; return;
} }
frappe.call({ frappe.call({
@ -974,43 +927,44 @@ frappe.setup.SetupWizard = class JeySetupWizard {
if (emp.passport_number) map[emp.passport_number] = emp.name; if (emp.passport_number) map[emp.passport_number] = emp.name;
}); });
this.amas_existing_map = map; this.amas_existing_map = map;
this.amas_state = "select"; this.amas_commit_selection();
this.show_current_step(); },
error: () => {
this.amas_existing_map = {};
this.amas_commit_selection();
}, },
}); });
}, },
}); });
} }
// Caches the full fetched list (user requested: import every active contract the
// report returned, no manual selection). Runs right after amas_load_employees.
amas_commit_selection() { amas_commit_selection() {
const $body = this.$wrap.find(".jey-body"); const all = this.amas_employees || [];
const selected = []; if (all.length === 0) {
$body.find(".jey-emp-pick:checked").each((i, el) => { this.amas_state = "intro";
const idx = parseInt($(el).attr("data-idx"), 10); this.show_current_step();
if (!isNaN(idx)) selected.push(this.amas_employees[idx]);
});
if (selected.length === 0) {
this.$wrap.find(".jey-status").text(__("Select at least one employee, or go back and skip this step."));
return; return;
} }
const createDesignation = $body.find(".jey-emp-create-desig").is(":checked") ? 1 : 0; const createDesignation = 1;
this.data.amas_create_designation = createDesignation; this.data.amas_create_designation = createDesignation;
frappe.call({ frappe.call({
method: "jey_wizard.amas.cache_selected_employees", method: "jey_wizard.amas.cache_selected_employees",
args: { args: {
employees_json: JSON.stringify(selected), employees_json: JSON.stringify(all),
create_designation: createDesignation, create_designation: createDesignation,
}, },
freeze: true,
freeze_message: __("Saving selection..."),
callback: (r) => { callback: (r) => {
const m = r.message || {}; const m = r.message || {};
if (!m.ok) { if (!m.ok) {
this.$wrap.find(".jey-status").text(__("Failed to cache selection.")); this.$wrap.find(".jey-status").text(__("Failed to cache selection."));
this.amas_state = "intro";
this.show_current_step();
return; return;
} }
this.data.amas_selected_employees = selected; this.data.amas_selected_employees = all;
this.data.amas_skipped = false; this.data.amas_skipped = false;
this.amas_state = "done"; this.amas_state = "done";
this.show_current_step(); this.show_current_step();