ƏMAS step: surface failure reason on intro, clearer 'cached' state

Symptom: after the second phone tap the wizard appeared to drop the
user back to the ƏMAS intro screen with no indication of what
happened — they couldn't tell if employees had been cached, the
import had run, or anything had failed.

Root cause was a UX bug in the JS layer: every error path in the
ƏMAS sub-flow (connect_amas, change_amas_account, get_employees_
report, cache_selected_employees) did

    this.$wrap.find(".jey-status").text(error_message);
    this.amas_state = "intro";
    this.show_current_step();

but show_current_step() unconditionally calls .jey-status.empty()
at the top of the render, so the message was wiped before the user
could read it. They saw a clean intro screen and assumed nothing
ran or that it silently succeeded.

Fix:

- New persistent fields data.amas_last_error / data.asan_last_error
  that survive show_current_step re-renders. Every ƏMAS error path
  now writes to data.amas_last_error before flipping state.
- intro renderer reads data.amas_last_error and shows a red
  "Previous attempt failed: <message>" banner above the usual
  copy when set. amas_start_auth clears it on retry.
- Added .error callbacks to the four frappe.call sites that didn't
  have one — network errors used to drop the user to intro with no
  message at all.

Also rewrote the success "done" screen so it can't be misread:
heading is now "Cached and ready", body explicitly says "Cached on
the server. Actual import into Employee / ƏMAS Employees runs as a
background job right after you click Finish — you can keep working
in /app while it runs." Same data, just unmistakable.
This commit is contained in:
Ali 2026-05-01 16:22:20 +00:00
parent 29f277a7bf
commit 8c178524d9
2 changed files with 43 additions and 13 deletions

View File

@ -1 +1 @@
__version__ = "0.1.21" __version__ = "0.1.22"

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.1.21"; const JEY_WIZARD_VERSION = "0.1.22";
// 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.
@ -71,6 +71,12 @@ frappe.setup.SetupWizard = class JeySetupWizard {
amas_selected_employees: [], amas_selected_employees: [],
amas_create_designation: 1, amas_create_designation: 1,
amas_organization_name: "", amas_organization_name: "",
// Survives show_current_step() re-renders so the user sees what
// failed when an error path resets amas_state to "intro" — the
// .jey-status helper is cleared by every render and would erase
// the message before they could read it.
amas_last_error: "",
asan_last_error: "",
}; };
// Sub-state machines per step. `input` / `intro` is the always-safe fallback // Sub-state machines per step. `input` / `intro` is the always-safe fallback
@ -314,8 +320,14 @@ frappe.setup.SetupWizard = class JeySetupWizard {
render_employees($body) { render_employees($body) {
if (this.amas_state === "intro") { if (this.amas_state === "intro") {
const alreadyLoaded = (this.data.amas_selected_employees || []).length; const alreadyLoaded = (this.data.amas_selected_employees || []).length;
const lastError = this.data.amas_last_error || "";
$body.html(` $body.html(`
<h3 style="margin-bottom:16px">${__("Load employees from ƏMAS")}</h3> <h3 style="margin-bottom:16px">${__("Load employees from ƏMAS")}</h3>
${lastError
? `<div style="margin-bottom:12px;padding:12px;background:#fee2e2;border-left:3px solid #dc2626;border-radius:4px;color:#991b1b">
<b>${__("Previous attempt failed:")}</b> ${frappe.utils.escape_html(lastError)}
</div>`
: ""}
<p>${__("ƏMAS (e-social.gov.az) stores employment contracts. If your company has employees registered there, we can import them now.")}</p> <p>${__("ƏMAS (e-social.gov.az) stores employment contracts. If your company has employees registered there, we can import them now.")}</p>
<div style="margin-top:12px;padding:12px;background:#fff8e1;border-left:3px solid #e0a800;border-radius:4px"> <div style="margin-top:12px;padding:12px;background:#fff8e1;border-left:3px solid #e0a800;border-radius:4px">
<b>${__("Heads up:")}</b> ${__("You will receive a second confirmation request on your phone from ASAN Sign / MyGovID. This is separate from the one you just approved for the tax portal.")} <b>${__("Heads up:")}</b> ${__("You will receive a second confirmation request on your phone from ASAN Sign / MyGovID. This is separate from the one you just approved for the tax portal.")}
@ -400,12 +412,13 @@ frappe.setup.SetupWizard = class JeySetupWizard {
} 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;
$body.html(` $body.html(`
<h3 style="margin-bottom:16px;color:#080"> ${__("Employees ready to import")}</h3> <h3 style="margin-bottom:16px;color:#080"> ${__("Cached and ready")}</h3>
<div style="padding:12px;background:#e8f5e9;border-radius:4px;margin-bottom:12px"> <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> ${__("Organization:")} <b>${frappe.utils.escape_html(this.data.amas_organization_name || "")}</b><br>
${__("Employees found:")} <b>${n}</b> ${__("Employees cached:")} <b>${n}</b>
</div> </div>
<p style="color:#666">${__("All employees will be imported into ƏMAS Employees and the standard Employee doctype after the wizard finishes.")}</p> <p style="color:#666">${__("Cached on the server. Actual import into Employee / ƏMAS Employees runs as a background job right after you click Finish — you can keep working in /app while it runs.")}</p>
<p style="color:#888;font-size:12px;margin-top:8px">${__("Click Next to continue, or Back to re-select.")}</p>
`); `);
// No extra buttons here — Back/Next in nav cover everything. // No extra buttons here — Back/Next in nav cover everything.
} }
@ -1157,6 +1170,7 @@ frappe.setup.SetupWizard = class JeySetupWizard {
return; return;
} }
this.data.amas_skipped = false; this.data.amas_skipped = false;
this.data.amas_last_error = "";
this.amas_verification_code = ""; this.amas_verification_code = "";
this.amas_state = "auth"; this.amas_state = "auth";
this.show_current_step(); this.show_current_step();
@ -1170,7 +1184,7 @@ frappe.setup.SetupWizard = class JeySetupWizard {
if (this.amas_state !== "auth") return; if (this.amas_state !== "auth") return;
const m = r.message || {}; const m = r.message || {};
if (!m.success) { if (!m.success) {
this.$wrap.find(".jey-status").text(m.message || __("MyGovID authentication failed.")); this.data.amas_last_error = m.message || __("MyGovID authentication failed.");
this.amas_state = "intro"; this.amas_state = "intro";
this.show_current_step(); this.show_current_step();
return; return;
@ -1179,7 +1193,7 @@ frappe.setup.SetupWizard = class JeySetupWizard {
}, },
error: () => { error: () => {
if (this.amas_state !== "auth") return; if (this.amas_state !== "auth") return;
this.$wrap.find(".jey-status").text(__("MyGovID request timed out.")); this.data.amas_last_error = __("MyGovID request timed out.");
this.amas_state = "intro"; this.amas_state = "intro";
this.show_current_step(); this.show_current_step();
}, },
@ -1213,7 +1227,7 @@ frappe.setup.SetupWizard = class JeySetupWizard {
callback: (r) => { callback: (r) => {
const m = r.message || {}; const m = r.message || {};
if (!m.success) { if (!m.success) {
this.$wrap.find(".jey-status").text(m.message || __("Failed to connect to ƏMAS.")); this.data.amas_last_error = m.message || __("Failed to connect to ƏMAS.");
this.amas_state = "intro"; this.amas_state = "intro";
this.show_current_step(); this.show_current_step();
return; return;
@ -1233,7 +1247,7 @@ frappe.setup.SetupWizard = class JeySetupWizard {
} else if (this.amas_accounts.length === 1) { } else if (this.amas_accounts.length === 1) {
this.amas_pick_account(0); this.amas_pick_account(0);
} else if (this.amas_accounts.length === 0) { } else if (this.amas_accounts.length === 0) {
this.$wrap.find(".jey-status").text(__("No organizations found in ƏMAS for this account.")); this.data.amas_last_error = __("No organizations found in ƏMAS for this account.");
this.amas_state = "intro"; this.amas_state = "intro";
this.show_current_step(); this.show_current_step();
} else { } else {
@ -1241,6 +1255,11 @@ frappe.setup.SetupWizard = class JeySetupWizard {
this.show_current_step(); this.show_current_step();
} }
}, },
error: () => {
this.data.amas_last_error = __("Network error while connecting to ƏMAS.");
this.amas_state = "intro";
this.show_current_step();
},
}); });
} }
@ -1267,7 +1286,7 @@ frappe.setup.SetupWizard = class JeySetupWizard {
callback: (r) => { callback: (r) => {
const m = r.message || {}; const m = r.message || {};
if (!m.success) { if (!m.success) {
this.$wrap.find(".jey-status").text(m.message || __("Failed to select organization.")); this.data.amas_last_error = m.message || __("Failed to select organization.");
this.amas_state = "intro"; this.amas_state = "intro";
this.show_current_step(); this.show_current_step();
return; return;
@ -1275,6 +1294,11 @@ frappe.setup.SetupWizard = class JeySetupWizard {
this.data.amas_organization_name = name; this.data.amas_organization_name = name;
this.amas_load_employees(); this.amas_load_employees();
}, },
error: () => {
this.data.amas_last_error = __("Network error while selecting organization.");
this.amas_state = "intro";
this.show_current_step();
},
}); });
} }
@ -1292,14 +1316,14 @@ frappe.setup.SetupWizard = class JeySetupWizard {
callback: (r) => { callback: (r) => {
const m = r.message || {}; const m = r.message || {};
if (!m.success) { if (!m.success) {
this.$wrap.find(".jey-status").text(m.message || __("Failed to fetch employees.")); this.data.amas_last_error = m.message || __("Failed to fetch employees.");
this.amas_state = "intro"; this.amas_state = "intro";
this.show_current_step(); this.show_current_step();
return; return;
} }
const employees = m.employees || []; const employees = m.employees || [];
if (employees.length === 0) { if (employees.length === 0) {
this.$wrap.find(".jey-status").text(__("No employees found in ƏMAS for this organization.")); this.data.amas_last_error = __("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;
@ -1315,6 +1339,7 @@ frappe.setup.SetupWizard = class JeySetupWizard {
amas_commit_selection() { amas_commit_selection() {
const all = this.amas_employees || []; const all = this.amas_employees || [];
if (all.length === 0) { if (all.length === 0) {
this.data.amas_last_error = __("Got empty employee list from ƏMAS.");
this.amas_state = "intro"; this.amas_state = "intro";
this.show_current_step(); this.show_current_step();
return; return;
@ -1331,7 +1356,7 @@ frappe.setup.SetupWizard = class JeySetupWizard {
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.data.amas_last_error = __("Failed to cache employee selection on the server.");
this.amas_state = "intro"; this.amas_state = "intro";
this.show_current_step(); this.show_current_step();
return; return;
@ -1341,6 +1366,11 @@ frappe.setup.SetupWizard = class JeySetupWizard {
this.amas_state = "done"; this.amas_state = "done";
this.show_current_step(); this.show_current_step();
}, },
error: () => {
this.data.amas_last_error = __("Network error while caching the employee selection.");
this.amas_state = "intro";
this.show_current_step();
},
}); });
} }