1195 lines
44 KiB
JavaScript
1195 lines
44 KiB
JavaScript
// Loaded via setup_wizard_requires hook. At the time this runs:
|
||
// - frappe/setup_wizard.js has already defined frappe.setup.SetupWizard and the default
|
||
// slides_settings + the `frappe.pages["setup-wizard"].on_page_load` handler.
|
||
// - The handler called frappe.require([...this file...], cb). After we finish loading,
|
||
// cb runs and does `new frappe.setup.SetupWizard(wizard_settings)`.
|
||
// So overriding the class here replaces the default UI entirely for this pageload.
|
||
|
||
frappe.provide("jey_wizard");
|
||
|
||
// 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
|
||
// sync with __version__ in jey_wizard/__init__.py.
|
||
const JEY_WIZARD_VERSION = "0.1.5";
|
||
|
||
// 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.
|
||
frappe.setup.slides = [];
|
||
frappe.setup.slides_settings = [];
|
||
frappe.setup.events = {};
|
||
|
||
// Asan polling: 20 attempts × 5s = ~100s window for the user to confirm on phone.
|
||
const ASAN_POLL_INTERVAL_MS = 5000;
|
||
const ASAN_POLL_MAX_ATTEMPTS = 20;
|
||
|
||
// MyGovID's /asanSignLogin is a server-held long-poll (up to 120s). We let the call
|
||
// run for 130s before giving up client-side.
|
||
const MYGOVID_TIMEOUT_MS = 130000;
|
||
|
||
// Language code -> full name. load_messages and Frappe's setup_complete both accept
|
||
// the full name form; the language step uses codes internally so the field is compact.
|
||
const LANG_NAME = {
|
||
en: "English",
|
||
az: "Azərbaycan dili",
|
||
ru: "Русский",
|
||
};
|
||
|
||
frappe.setup.SetupWizard = class JeySetupWizard {
|
||
constructor(settings = {}) {
|
||
this.parent = settings.parent;
|
||
this.values = {};
|
||
const year = new Date().getFullYear();
|
||
this.data = {
|
||
language: "az",
|
||
asan_prefix: "",
|
||
asan_local_number: "",
|
||
asan_phone: "",
|
||
asan_user_id: "",
|
||
asan_login_name: "",
|
||
asan_voen: "",
|
||
company_name: "",
|
||
company_abbr: "",
|
||
company_abbr_user_edited: false,
|
||
fy_start_date: `${year}-01-01`,
|
||
fy_end_date: `${year}-12-31`,
|
||
valuation_method: "FIFO",
|
||
// AMAS
|
||
amas_skipped: false,
|
||
amas_selected_employees: [],
|
||
amas_create_designation: 1,
|
||
amas_organization_name: "",
|
||
};
|
||
|
||
// Sub-state machines per step. `input` / `intro` is the always-safe fallback
|
||
// that Back can return to without worrying about dangling network state.
|
||
this.asan_state = "input";
|
||
this.asan_certificates = [];
|
||
this.asan_bearer_token = "";
|
||
this.asan_poll_attempts = 0;
|
||
this.asan_poll_timer = null;
|
||
|
||
this.amas_state = "intro";
|
||
this.amas_accounts = [];
|
||
this.amas_employees = [];
|
||
this.amas_existing_map = {};
|
||
this.amas_verification_code = "";
|
||
|
||
this.current_step = 0;
|
||
this.steps = ["language", "asan", "employees", "company", "confirm"];
|
||
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.
|
||
show_slide() {}
|
||
|
||
render() {
|
||
const $parent = $(this.parent).empty();
|
||
this.$wrap = $(`
|
||
<div class="jey-wizard" style="max-width:720px;margin:80px auto;padding:32px;font-family:system-ui,sans-serif;position:relative">
|
||
<div class="jey-version" style="position:absolute;top:8px;right:8px;background:#222;color:#fff;padding:4px 10px;border-radius:4px;font-size:12px;font-weight:600">v${JEY_WIZARD_VERSION}</div>
|
||
<h1 style="margin-bottom:8px">Jey Setup</h1>
|
||
<div class="jey-progress" style="color:#888;margin-bottom:24px"></div>
|
||
<div class="jey-body"></div>
|
||
<div class="jey-nav" style="margin-top:24px;display:flex;gap:8px;justify-content:flex-end"></div>
|
||
<div class="jey-status" style="margin-top:16px;color:#c00"></div>
|
||
<style>@keyframes jey-spin { to { transform: rotate(360deg) } }</style>
|
||
</div>
|
||
`).appendTo($parent);
|
||
this.show_current_step();
|
||
}
|
||
|
||
show_current_step() {
|
||
this.stop_polling();
|
||
const step = this.steps[this.current_step];
|
||
const $body = this.$wrap.find(".jey-body").empty();
|
||
const $nav = this.$wrap.find(".jey-nav").empty();
|
||
this.$wrap.find(".jey-progress").text(
|
||
__("Step {0} of {1}", [this.current_step + 1, this.steps.length])
|
||
);
|
||
this.$wrap.find(".jey-status").empty();
|
||
|
||
if (step === "language") this.render_language($body);
|
||
else if (step === "asan") this.render_asan($body);
|
||
else if (step === "employees") this.render_employees($body);
|
||
else if (step === "company") this.render_company($body);
|
||
else if (step === "confirm") this.render_confirm($body);
|
||
|
||
this.render_nav($nav, step);
|
||
}
|
||
|
||
// ---------- step renderers ----------
|
||
|
||
render_language($body) {
|
||
$body.html(`
|
||
<h3 style="margin-bottom:16px">${__("Language")}</h3>
|
||
<label style="display:block;margin-bottom:8px">${__("Wizard & site language")}</label>
|
||
<select class="jey-language form-control" style="max-width:280px">
|
||
<option value="az">Azərbaycan</option>
|
||
<option value="ru">Русский</option>
|
||
<option value="en">English</option>
|
||
</select>
|
||
`);
|
||
$body.find(".jey-language").val(this.data.language);
|
||
// Apply language immediately on change so subsequent screens pick it up.
|
||
$body.find(".jey-language").on("change", (e) => {
|
||
const code = $(e.currentTarget).val();
|
||
this.change_language(code);
|
||
});
|
||
}
|
||
|
||
render_asan($body) {
|
||
if (this.asan_state === "input") {
|
||
// +994 is prefixed silently on submit — we don't render it in the UI
|
||
// because it's the only possibility for Azerbaijani mobile numbers and
|
||
// a static label clutters the form. The visible parts are the carrier
|
||
// prefix (dropdown) and the 7-digit local number.
|
||
const prefixOptions = ["50", "51", "10", "55", "60", "70", "77", "99"]
|
||
.map((p) => `<option value="${p}">0${p}</option>`)
|
||
.join("");
|
||
$body.html(`
|
||
<h3 style="margin-bottom:16px">${__("Asan Imza authentication")}</h3>
|
||
<div style="margin-bottom:12px">
|
||
<label style="display:block;margin-bottom:4px">${__("Phone number")}</label>
|
||
<div style="display:flex;align-items:center;gap:8px;max-width:320px">
|
||
<select class="jey-asan-prefix form-control" style="width:90px;border:1px solid #bcc2ca;border-radius:4px;padding:6px 8px;background:#fff">
|
||
<option value="">${__("Prefix")}</option>
|
||
${prefixOptions}
|
||
</select>
|
||
<input type="text" inputmode="numeric" maxlength="7" class="jey-asan-phone form-control" style="flex:1" placeholder="${__("Enter number")}" />
|
||
</div>
|
||
</div>
|
||
<div style="margin-bottom:12px">
|
||
<label style="display:block;margin-bottom:4px">${__("User ID")}</label>
|
||
<input type="text" class="jey-asan-userid form-control" style="max-width:280px" placeholder="${__("ASAN ID")}" />
|
||
</div>
|
||
<button class="btn btn-primary jey-asan-auth" style="margin-top:8px">${__("Send request to Asan Imza")}</button>
|
||
`);
|
||
$body.find(".jey-asan-prefix").val(this.data.asan_prefix || "");
|
||
$body.find(".jey-asan-phone").val(this.data.asan_local_number || "");
|
||
$body.find(".jey-asan-userid").val(this.data.asan_user_id);
|
||
$body.find(".jey-asan-phone").on("input", (e) => {
|
||
// Strip anything that isn't a digit as the user types.
|
||
const cleaned = String($(e.currentTarget).val() || "").replace(/\D/g, "").slice(0, 7);
|
||
$(e.currentTarget).val(cleaned);
|
||
});
|
||
$body.find(".jey-asan-auth").on("click", () => this.start_asan_auth());
|
||
} else if (this.asan_state === "polling") {
|
||
const code = this.asan_verification_code || "";
|
||
$body.html(`
|
||
<h3 style="margin-bottom:16px">${__("Confirm on your phone")}</h3>
|
||
<p>${__("Open the Asan Imza app on your phone and confirm the request.")}</p>
|
||
${code
|
||
? `<div style="margin:16px 0;padding:14px;background:#f7f7f7;border-radius:6px;text-align:center">
|
||
<div style="color:#666;font-size:12px;margin-bottom:6px">${__("Verification code")}</div>
|
||
<div style="font-size:28px;letter-spacing:4px;font-weight:600">${frappe.utils.escape_html(String(code))}</div>
|
||
</div>`
|
||
: ""}
|
||
<div style="display:flex;align-items:center;gap:12px;margin-top:16px">
|
||
<div class="jey-spinner" style="width:18px;height:18px;border:3px solid #ddd;border-top-color:#444;border-radius:50%;animation:jey-spin 0.8s linear infinite"></div>
|
||
<div class="jey-asan-poll-status" style="color:#666">${__("Waiting for confirmation...")}</div>
|
||
</div>
|
||
`);
|
||
} else if (this.asan_state === "certs") {
|
||
const rows = (this.asan_certificates || []).map((c, i) => {
|
||
const legal = c.legalInfo || {};
|
||
const name = legal.name || "";
|
||
const voen = legal.voen || legal.tin || "";
|
||
const liquidated = c.liquidated ? ` <i style='color:#c00'>(${__("liquidated")})</i>` : "";
|
||
return `
|
||
<tr>
|
||
<td style="padding:8px;border-bottom:1px solid #eee">${frappe.utils.escape_html(name)}${liquidated}</td>
|
||
<td style="padding:8px;border-bottom:1px solid #eee;font-family:monospace">${frappe.utils.escape_html(voen)}</td>
|
||
<td style="padding:8px;border-bottom:1px solid #eee;text-align:right">
|
||
<button class="btn btn-sm btn-primary jey-cert-pick" data-idx="${i}">${__("Select")}</button>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
}).join("");
|
||
|
||
$body.html(`
|
||
<h3 style="margin-bottom:16px">${__("Select your company")}</h3>
|
||
${rows
|
||
? `<table style="width:100%;border-collapse:collapse">
|
||
<thead><tr>
|
||
<th style="text-align:left;padding:8px;border-bottom:2px solid #ccc">${__("Company")}</th>
|
||
<th style="text-align:left;padding:8px;border-bottom:2px solid #ccc">${__("VÖEN")}</th>
|
||
<th style="padding:8px;border-bottom:2px solid #ccc"></th>
|
||
</tr></thead>
|
||
<tbody>${rows}</tbody>
|
||
</table>`
|
||
: `<p style="color:#c00">${__("No legal-entity certificates available on this Asan ID. Use a phone/ID linked to a company.")}</p>`}
|
||
`);
|
||
$body.find(".jey-cert-pick").on("click", (e) => {
|
||
const idx = parseInt($(e.currentTarget).attr("data-idx"), 10);
|
||
this.confirm_and_pick_certificate(idx);
|
||
});
|
||
} else if (this.asan_state === "fetching") {
|
||
$body.html(`
|
||
<h3 style="margin-bottom:16px">${__("Loading data from tax portal...")}</h3>
|
||
<p style="color:#666">${__("Pulling objects, cash registers, POS terminals, bank accounts, obligation pacts and presented certificates. This usually takes 10-30 seconds.")}</p>
|
||
<div style="display:flex;align-items:center;gap:12px;margin-top:20px">
|
||
<div class="jey-spinner" style="width:18px;height:18px;border:3px solid #ddd;border-top-color:#444;border-radius:50%;animation:jey-spin 0.8s linear infinite"></div>
|
||
<div style="color:#666">${__("Fetching...")}</div>
|
||
</div>
|
||
`);
|
||
} else if (this.asan_state === "done") {
|
||
const summary = this.asan_fetch_summary || {};
|
||
const errors = this.asan_fetch_errors || {};
|
||
const labels = {
|
||
objects_json: __("Objects"),
|
||
cash_registers_json: __("Cash registers"),
|
||
pos_terminals_json: __("POS terminals"),
|
||
bank_accounts_json: __("Bank accounts"),
|
||
obligation_pacts_json: __("Obligation pacts"),
|
||
presented_certs_json: __("Presented certificates"),
|
||
};
|
||
const rows = Object.keys(labels).map((k) => {
|
||
const label = labels[k];
|
||
if (errors[k]) {
|
||
return `<li><b>${label}:</b> <span style="color:#c00">${__("failed")}</span> <span style="color:#888;font-size:12px">(${frappe.utils.escape_html(errors[k])})</span></li>`;
|
||
}
|
||
const n = summary[k];
|
||
if (n === undefined) {
|
||
return `<li><b>${label}:</b> <span style="color:#888">—</span></li>`;
|
||
}
|
||
return `<li><b>${label}:</b> ${n}</li>`;
|
||
}).join("");
|
||
|
||
$body.html(`
|
||
<h3 style="margin-bottom:16px;color:#080">✓ ${__("Authenticated & data loaded")}</h3>
|
||
<p>${__("Pulled from the tax portal:")}</p>
|
||
<ul style="line-height:1.8">
|
||
<li><b>${__("Company")}:</b> ${frappe.utils.escape_html(this.data.company_name)}</li>
|
||
<li><b>${__("VÖEN")}:</b> ${frappe.utils.escape_html(this.data.asan_voen)}</li>
|
||
</ul>
|
||
<div style="margin-top:16px;padding:12px;background:#f7f7f7;border-radius:6px">
|
||
<div style="font-weight:600;margin-bottom:6px">${__("Cached datasets")}</div>
|
||
<ul style="line-height:1.6;margin:0;padding-left:20px">${rows}</ul>
|
||
</div>
|
||
<div style="margin-top:16px;text-align:right">
|
||
<button class="btn btn-default btn-sm jey-change-company">${__("Wrong company? Change")}</button>
|
||
</div>
|
||
`);
|
||
$body.find(".jey-change-company").on("click", () => this.reset_company_selection());
|
||
}
|
||
}
|
||
|
||
render_employees($body) {
|
||
if (this.amas_state === "intro") {
|
||
const alreadyLoaded = (this.data.amas_selected_employees || []).length;
|
||
$body.html(`
|
||
<h3 style="margin-bottom:16px">${__("Load employees from ƏMAS")}</h3>
|
||
<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">
|
||
<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.")}
|
||
</div>
|
||
<p style="color:#888;margin-top:12px">${__("This step is optional. Use Next to skip if your company has no employees or is not registered with ƏMAS.")}</p>
|
||
${alreadyLoaded
|
||
? `<div style="margin-top:12px;padding:10px;background:#e8f5e9;border-radius:4px">
|
||
${__("Already loaded:")} <b>${alreadyLoaded}</b> ${__("employee(s)")}.
|
||
</div>`
|
||
: ""}
|
||
<div style="margin-top:20px">
|
||
<button class="btn btn-primary jey-amas-load">${__("Load employees")}</button>
|
||
</div>
|
||
`);
|
||
$body.find(".jey-amas-load").on("click", () => this.amas_start_auth());
|
||
} else if (this.amas_state === "auth") {
|
||
const code = this.amas_verification_code || "";
|
||
$body.html(`
|
||
<h3 style="margin-bottom:16px">${__("Confirm ƏMAS request on your phone")}</h3>
|
||
<p>${__("A second confirmation request has been sent to your phone. Open ASAN Sign and approve it.")}</p>
|
||
${code
|
||
? `<div style="margin:16px 0;padding:14px;background:#f7f7f7;border-radius:6px;text-align:center">
|
||
<div style="color:#666;font-size:12px;margin-bottom:6px">${__("Verification code")}</div>
|
||
<div style="font-size:28px;letter-spacing:4px;font-weight:600">${frappe.utils.escape_html(String(code))}</div>
|
||
</div>`
|
||
: `<div style="color:#888;font-size:12px;margin-top:8px">${__("Retrieving verification code...")}</div>`}
|
||
<div style="display:flex;align-items:center;gap:12px;margin-top:16px">
|
||
<div class="jey-spinner" style="width:18px;height:18px;border:3px solid #ddd;border-top-color:#444;border-radius:50%;animation:jey-spin 0.8s linear infinite"></div>
|
||
<div style="color:#666">${__("Waiting for confirmation...")}</div>
|
||
</div>
|
||
`);
|
||
} else if (this.amas_state === "connecting") {
|
||
$body.html(`
|
||
<h3 style="margin-bottom:16px">${__("Connecting to ƏMAS...")}</h3>
|
||
<div style="display:flex;align-items:center;gap:12px;margin-top:16px">
|
||
<div class="jey-spinner" style="width:18px;height:18px;border:3px solid #ddd;border-top-color:#444;border-radius:50%;animation:jey-spin 0.8s linear infinite"></div>
|
||
<div style="color:#666">${__("Establishing session...")}</div>
|
||
</div>
|
||
`);
|
||
} else if (this.amas_state === "accounts") {
|
||
const rows = (this.amas_accounts || []).map((a, i) => {
|
||
const name = a.accountName || a.name || "";
|
||
const voen = a.accountNumber || a.voen || a.number || "";
|
||
const role = a.roleName || a.role || "";
|
||
return `
|
||
<tr>
|
||
<td style="padding:8px;border-bottom:1px solid #eee">${frappe.utils.escape_html(name)}</td>
|
||
<td style="padding:8px;border-bottom:1px solid #eee;font-family:monospace">${frappe.utils.escape_html(voen)}</td>
|
||
<td style="padding:8px;border-bottom:1px solid #eee">${frappe.utils.escape_html(role)}</td>
|
||
<td style="padding:8px;border-bottom:1px solid #eee;text-align:right">
|
||
<button class="btn btn-sm btn-primary jey-amas-acc-pick" data-idx="${i}">${__("Select")}</button>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
}).join("");
|
||
|
||
$body.html(`
|
||
<h3 style="margin-bottom:16px">${__("Select organization")}</h3>
|
||
<p style="color:#888">${__("We could not auto-match by VÖEN. Pick the organization you want to import employees from.")}</p>
|
||
<table style="width:100%;border-collapse:collapse;margin-top:12px">
|
||
<thead><tr>
|
||
<th style="text-align:left;padding:8px;border-bottom:2px solid #ccc">${__("Organization")}</th>
|
||
<th style="text-align:left;padding:8px;border-bottom:2px solid #ccc">${__("VÖEN")}</th>
|
||
<th style="text-align:left;padding:8px;border-bottom:2px solid #ccc">${__("Role")}</th>
|
||
<th style="padding:8px;border-bottom:2px solid #ccc"></th>
|
||
</tr></thead>
|
||
<tbody>${rows}</tbody>
|
||
</table>
|
||
`);
|
||
$body.find(".jey-amas-acc-pick").on("click", (e) => {
|
||
const idx = parseInt($(e.currentTarget).attr("data-idx"), 10);
|
||
this.amas_pick_account(idx);
|
||
});
|
||
} else if (this.amas_state === "loading") {
|
||
$body.html(`
|
||
<h3 style="margin-bottom:16px">${__("Loading employees...")}</h3>
|
||
<div style="display:flex;align-items:center;gap:12px;margin-top:16px">
|
||
<div class="jey-spinner" style="width:18px;height:18px;border:3px solid #ddd;border-top-color:#444;border-radius:50%;animation:jey-spin 0.8s linear infinite"></div>
|
||
<div style="color:#666">${__("Fetching from ƏMAS...")}</div>
|
||
</div>
|
||
`);
|
||
} else if (this.amas_state === "done") {
|
||
const n = (this.data.amas_selected_employees || []).length;
|
||
$body.html(`
|
||
<h3 style="margin-bottom:16px;color:#080">✓ ${__("Employees ready to import")}</h3>
|
||
<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>
|
||
</div>
|
||
<p style="color:#666">${__("All employees will be imported into ƏMAS Employees and the standard Employee doctype after the wizard finishes.")}</p>
|
||
`);
|
||
// No extra buttons here — Back/Next in nav cover everything.
|
||
}
|
||
}
|
||
|
||
render_company($body) {
|
||
$body.html(`
|
||
<h3 style="margin-bottom:16px">${__("Company")}</h3>
|
||
<div style="margin-bottom:12px">
|
||
<label style="display:block;margin-bottom:4px">${__("Company name")}</label>
|
||
<input type="text" class="jey-company-name form-control" style="max-width:380px" />
|
||
${this.data.asan_voen
|
||
? `<div style="color:#888;font-size:12px;margin-top:6px">${__("VÖEN {0} (from Asan)", [frappe.utils.escape_html(this.data.asan_voen)])}</div>`
|
||
: ""}
|
||
</div>
|
||
<div style="margin-bottom:12px">
|
||
<label style="display:block;margin-bottom:4px">${__("Company abbreviation")}</label>
|
||
<input type="text" class="jey-company-abbr form-control" style="max-width:140px;text-transform:uppercase" maxlength="10" />
|
||
<div style="color:#888;font-size:12px;margin-top:6px">${__("Short code suffixed to account names. Auto-suggested from the company name — you can edit it.")}</div>
|
||
</div>
|
||
<h4 style="margin-top:20px;margin-bottom:10px">${__("Fiscal Year")}</h4>
|
||
<div style="display:flex;gap:16px;max-width:420px">
|
||
<div style="flex:1">
|
||
<label style="display:block;margin-bottom:4px">${__("Start date")}</label>
|
||
<input type="date" class="jey-fy-start form-control" />
|
||
</div>
|
||
<div style="flex:1">
|
||
<label style="display:block;margin-bottom:4px">${__("End date")}</label>
|
||
<input type="date" class="jey-fy-end form-control" />
|
||
</div>
|
||
</div>
|
||
<div style="color:#888;font-size:12px;margin-top:6px">${__("Default: current calendar year (Jan 1 – Dec 31).")}</div>
|
||
<h4 style="margin-top:20px;margin-bottom:10px">${__("Stock Valuation")}</h4>
|
||
<div style="max-width:280px">
|
||
<label style="display:block;margin-bottom:4px">${__("Valuation method")}</label>
|
||
<select class="jey-valuation-method form-control">
|
||
<option value="FIFO">FIFO</option>
|
||
<option value="Moving Average">${__("Moving Average")}</option>
|
||
</select>
|
||
<div style="color:#888;font-size:12px;margin-top:6px">${__("Cannot be changed after the first stock entry.")}</div>
|
||
</div>
|
||
`);
|
||
$body.find(".jey-company-name").val(this.data.company_name);
|
||
$body.find(".jey-fy-start").val(this.data.fy_start_date);
|
||
$body.find(".jey-fy-end").val(this.data.fy_end_date);
|
||
$body.find(".jey-valuation-method").val(this.data.valuation_method || "FIFO");
|
||
|
||
// If user has never touched the abbr field, keep it in sync with the name.
|
||
// Once they edit it manually we stop auto-suggesting so their choice sticks.
|
||
const $nameEl = $body.find(".jey-company-name");
|
||
const $abbrEl = $body.find(".jey-company-abbr");
|
||
const fillAbbrDefault = () => {
|
||
if (!this.data.company_abbr_user_edited) {
|
||
const suggested = this.derive_abbr($nameEl.val() || "");
|
||
this.data.company_abbr = suggested;
|
||
$abbrEl.val(suggested);
|
||
}
|
||
};
|
||
$abbrEl.val(this.data.company_abbr || this.derive_abbr(this.data.company_name || ""));
|
||
$nameEl.on("input", fillAbbrDefault);
|
||
$abbrEl.on("input", (e) => {
|
||
this.data.company_abbr_user_edited = true;
|
||
this.data.company_abbr = ($(e.currentTarget).val() || "").toUpperCase().slice(0, 10);
|
||
$(e.currentTarget).val(this.data.company_abbr);
|
||
});
|
||
}
|
||
|
||
derive_abbr(rawName) {
|
||
// Normalise Azerbaijani company-form boilerplate before tokenising so the
|
||
// auto-suggest doesn't spell out each word of "Məhdud Məsuliyyətli Cəmiyyəti"
|
||
// as separate initials. Also strip every quote character — the stock
|
||
// ERPNext deriver chokes on them.
|
||
if (!rawName) return "";
|
||
let name = String(rawName).replace(/["“”„‟«»‘’‚‛'`´]/g, " ");
|
||
name = name.replace(/[ƏE]/gi, (ch) => (ch === ch.toUpperCase() ? "E" : "e"));
|
||
const rules = [
|
||
[/\bMEHDUD\s+MESULIYYETLI\s+CEMIYYETI\b/gi, "MMC"],
|
||
[/\bA[CÇ]IQ\s+SEHMDAR\s+CEMIYYETI\b/gi, "ASC"],
|
||
[/\bQAPALI\s+SEHMDAR\s+CEMIYYETI\b/gi, "QSC"],
|
||
[/\bSEHMDAR\s+CEMIYYETI\b/gi, "SC"],
|
||
[/\bKOOPERATIV\b/gi, "K"],
|
||
];
|
||
for (const [re, rep] of rules) name = name.replace(re, rep);
|
||
const parts = name.split(/\s+/).filter(Boolean);
|
||
if (parts.length === 0) return "";
|
||
if (parts.length === 1) return parts[0].slice(0, 3).toUpperCase();
|
||
return parts.slice(0, 4).map((p) => p[0]).join("").toUpperCase().slice(0, 10);
|
||
}
|
||
|
||
render_confirm($body) {
|
||
const empCount = (this.data.amas_selected_employees || []).length;
|
||
const empRow = this.data.amas_skipped
|
||
? __("Skipped")
|
||
: (empCount > 0 ? __("{0} selected", [empCount]) : __("None"));
|
||
$body.html(`
|
||
<h3>${__("Confirm")}</h3>
|
||
<ul style="line-height:1.8">
|
||
<li><b>${__("Language")}:</b> ${LANG_NAME[this.data.language] || this.data.language}</li>
|
||
<li><b>${__("Company")}:</b> ${frappe.utils.escape_html(this.data.company_name)} <span style="color:#888">(${frappe.utils.escape_html(this.data.company_abbr || "")})</span></li>
|
||
${this.data.asan_voen
|
||
? `<li><b>${__("VÖEN")}:</b> ${frappe.utils.escape_html(this.data.asan_voen)}</li>`
|
||
: ""}
|
||
<li><b>${__("Fiscal Year")}:</b> ${frappe.utils.escape_html(this.data.fy_start_date)} — ${frappe.utils.escape_html(this.data.fy_end_date)}</li>
|
||
<li><b>${__("Valuation method")}:</b> ${this.data.valuation_method === "Moving Average" ? __("Moving Average") : "FIFO"}</li>
|
||
<li><b>${__("ƏMAS employees")}:</b> ${empRow}</li>
|
||
</ul>
|
||
`);
|
||
}
|
||
|
||
render_nav($nav, step) {
|
||
// Back is available on every step except the very first (language). Within a
|
||
// multi-sub-state step, Back first rewinds the sub-state to its intro; only
|
||
// Back from intro jumps to the previous wizard step.
|
||
if (this.current_step > 0) {
|
||
$(`<button class="btn btn-default jey-back">${__("Back")}</button>`)
|
||
.appendTo($nav)
|
||
.on("click", () => this.prev());
|
||
}
|
||
|
||
// Standard Next: shown when the step has no internal flow-control, or when a
|
||
// multi-sub-state step is in its "terminal" sub-state.
|
||
let showStdNext = false;
|
||
const lastStep = this.current_step === this.steps.length - 1;
|
||
if (!lastStep) {
|
||
if (step === "asan") {
|
||
// Asan drives its own transitions. Only the terminal "done" sub-state
|
||
// surfaces a standard Next.
|
||
showStdNext = this.asan_state === "done";
|
||
} else if (step === "employees") {
|
||
// Next is visible on intro (acts as "skip this step") and on done
|
||
// (move on with the loaded selection). During auth/loading/etc it
|
||
// stays hidden so the flow can't be short-circuited mid-request.
|
||
showStdNext = this.amas_state === "intro" || this.amas_state === "done";
|
||
} else {
|
||
showStdNext = true;
|
||
}
|
||
}
|
||
|
||
if (showStdNext) {
|
||
$(`<button class="btn btn-primary jey-next">${__("Next")}</button>`)
|
||
.appendTo($nav)
|
||
.on("click", () => this.next());
|
||
}
|
||
if (lastStep) {
|
||
$(`<button class="btn btn-primary jey-finish">${__("Finish")}</button>`)
|
||
.appendTo($nav)
|
||
.on("click", () => this.finalize());
|
||
}
|
||
}
|
||
|
||
// ---------- step navigation ----------
|
||
|
||
capture_current() {
|
||
const step = this.steps[this.current_step];
|
||
if (step === "language") {
|
||
this.data.language = this.$wrap.find(".jey-language").val() || "az";
|
||
} else if (step === "company") {
|
||
const name = (this.$wrap.find(".jey-company-name").val() || "").trim();
|
||
if (!name) {
|
||
this.$wrap.find(".jey-status").text(__("Company name is required"));
|
||
return false;
|
||
}
|
||
this.data.company_name = name;
|
||
const abbrRaw = (this.$wrap.find(".jey-company-abbr").val() || "").toUpperCase().replace(/\s+/g, "");
|
||
const abbr = abbrRaw || this.derive_abbr(name);
|
||
if (!abbr) {
|
||
this.$wrap.find(".jey-status").text(__("Company abbreviation is required"));
|
||
return false;
|
||
}
|
||
if (!/^[A-Z0-9]{1,10}$/.test(abbr)) {
|
||
this.$wrap.find(".jey-status").text(__("Abbreviation must be 1–10 letters/digits (A–Z, 0–9)."));
|
||
return false;
|
||
}
|
||
this.data.company_abbr = abbr;
|
||
const fyStart = (this.$wrap.find(".jey-fy-start").val() || "").trim();
|
||
const fyEnd = (this.$wrap.find(".jey-fy-end").val() || "").trim();
|
||
if (!fyStart || !fyEnd) {
|
||
this.$wrap.find(".jey-status").text(__("Fiscal year start and end dates are required"));
|
||
return false;
|
||
}
|
||
if (fyEnd < fyStart) {
|
||
this.$wrap.find(".jey-status").text(__("Fiscal year end date must be after the start date"));
|
||
return false;
|
||
}
|
||
this.data.fy_start_date = fyStart;
|
||
this.data.fy_end_date = fyEnd;
|
||
const vm = (this.$wrap.find(".jey-valuation-method").val() || "FIFO").trim();
|
||
this.data.valuation_method = vm === "Moving Average" ? "Moving Average" : "FIFO";
|
||
}
|
||
return true;
|
||
}
|
||
|
||
next() {
|
||
if (!this.capture_current()) return;
|
||
const step = this.steps[this.current_step];
|
||
// Skip-by-Next on employees/intro with no prior selection: clear any stale
|
||
// cache so materialize_after_setup doesn't re-import a previous run's data.
|
||
if (
|
||
step === "employees" &&
|
||
this.amas_state === "intro" &&
|
||
(this.data.amas_selected_employees || []).length === 0
|
||
) {
|
||
this.data.amas_skipped = true;
|
||
frappe.call({
|
||
method: "jey_wizard.amas.clear_amas_cache",
|
||
callback: () => {
|
||
this.current_step++;
|
||
this.show_current_step();
|
||
},
|
||
});
|
||
return;
|
||
}
|
||
this.current_step++;
|
||
this.show_current_step();
|
||
}
|
||
|
||
prev() {
|
||
const step = this.steps[this.current_step];
|
||
// If we're inside a sub-state of a multi-state step, Back returns to its intro
|
||
// rather than jumping to the previous wizard step. Lets the user re-try.
|
||
if (step === "asan" && this.asan_state !== "input" && this.asan_state !== "done") {
|
||
this.asan_state = "input";
|
||
this.show_current_step();
|
||
return;
|
||
}
|
||
if (step === "employees" && this.amas_state !== "intro" && this.amas_state !== "done") {
|
||
this.amas_state = "intro";
|
||
this.show_current_step();
|
||
return;
|
||
}
|
||
|
||
// Crossing a step boundary: leave sub-state as-is so the user sees whatever
|
||
// they completed before (e.g. "done" screen). They can still rewind with a
|
||
// second Back.
|
||
this.current_step--;
|
||
this.show_current_step();
|
||
}
|
||
|
||
// ---------- language ----------
|
||
|
||
change_language(code, force = false) {
|
||
if (!code) return;
|
||
if (!force && code === this.data.language) return;
|
||
const langName = LANG_NAME[code] || "English";
|
||
frappe._messages = {};
|
||
frappe.call({
|
||
method: "frappe.desk.page.setup_wizard.setup_wizard.load_messages",
|
||
args: { language: langName },
|
||
freeze: true,
|
||
freeze_message: __("Loading translations..."),
|
||
callback: () => {
|
||
this.data.language = code;
|
||
this.show_current_step();
|
||
},
|
||
});
|
||
}
|
||
|
||
// ---------- asan flow ----------
|
||
|
||
start_asan_auth() {
|
||
const prefix = (this.$wrap.find(".jey-asan-prefix").val() || "").trim();
|
||
const local = (this.$wrap.find(".jey-asan-phone").val() || "").replace(/\D/g, "");
|
||
const userId = (this.$wrap.find(".jey-asan-userid").val() || "").trim();
|
||
if (!prefix) {
|
||
this.$wrap.find(".jey-status").text(__("Select a phone prefix"));
|
||
return;
|
||
}
|
||
if (local.length !== 7) {
|
||
this.$wrap.find(".jey-status").text(__("Phone number must be 7 digits"));
|
||
return;
|
||
}
|
||
if (!userId) {
|
||
this.$wrap.find(".jey-status").text(__("User ID is required"));
|
||
return;
|
||
}
|
||
this.data.asan_prefix = prefix;
|
||
this.data.asan_local_number = local;
|
||
this.data.asan_phone = `+994${prefix}${local}`;
|
||
this.data.asan_user_id = userId;
|
||
const phone = this.data.asan_phone;
|
||
this.$wrap.find(".jey-status").empty();
|
||
|
||
frappe.call({
|
||
method: "jey_wizard.api.ensure_asan_login",
|
||
args: { phone, user_id: userId },
|
||
freeze: true,
|
||
freeze_message: __("Preparing..."),
|
||
callback: (r) => {
|
||
const m = r.message || {};
|
||
if (!m.name) {
|
||
this.$wrap.find(".jey-status").text(__("Failed to create Asan Login record"));
|
||
return;
|
||
}
|
||
this.data.asan_login_name = m.name;
|
||
this.trigger_asan_authentication();
|
||
},
|
||
});
|
||
}
|
||
|
||
trigger_asan_authentication() {
|
||
frappe.call({
|
||
method: "invoice_az.auth.handle_authentication",
|
||
args: { asan_login_name: this.data.asan_login_name },
|
||
freeze: true,
|
||
freeze_message: __("Sending request to Asan Imza..."),
|
||
callback: (r) => {
|
||
const m = r.message || {};
|
||
if (!m.success) {
|
||
this.$wrap.find(".jey-status").text(m.message || __("Failed to start authentication"));
|
||
return;
|
||
}
|
||
this.asan_bearer_token = m.bearer_token || "";
|
||
this.asan_verification_code = m.verification_code || "";
|
||
this.asan_state = "polling";
|
||
this.asan_poll_attempts = 0;
|
||
this.show_current_step();
|
||
this.poll_asan();
|
||
},
|
||
});
|
||
}
|
||
|
||
poll_asan() {
|
||
if (this.asan_poll_attempts >= ASAN_POLL_MAX_ATTEMPTS) {
|
||
this.$wrap.find(".jey-asan-poll-status").html(
|
||
`<span style="color:#c00">${__("Timeout — confirmation not received within ~100 seconds.")}</span>`
|
||
);
|
||
this.$wrap.find(".jey-spinner").remove();
|
||
this.$wrap.find(".jey-nav")
|
||
.empty()
|
||
.append(
|
||
$(`<button class="btn btn-default jey-back">${__("Back")}</button>`)
|
||
.on("click", () => this.prev())
|
||
)
|
||
.append(
|
||
$(`<button class="btn btn-primary jey-asan-restart">${__("Try again")}</button>`)
|
||
.on("click", () => {
|
||
this.asan_state = "input";
|
||
this.show_current_step();
|
||
})
|
||
);
|
||
return;
|
||
}
|
||
this.asan_poll_attempts += 1;
|
||
|
||
frappe.call({
|
||
method: "invoice_az.auth.poll_auth_status",
|
||
args: {
|
||
asan_login_name: this.data.asan_login_name,
|
||
bearer_token: this.asan_bearer_token,
|
||
},
|
||
callback: (r) => {
|
||
if (this.asan_state !== "polling") return;
|
||
const m = r.message || {};
|
||
if (m.authenticated) {
|
||
this.fetch_certificates();
|
||
} else {
|
||
this.asan_poll_timer = setTimeout(
|
||
() => this.poll_asan(),
|
||
ASAN_POLL_INTERVAL_MS
|
||
);
|
||
}
|
||
},
|
||
});
|
||
}
|
||
|
||
stop_polling() {
|
||
if (this.asan_poll_timer) {
|
||
clearTimeout(this.asan_poll_timer);
|
||
this.asan_poll_timer = null;
|
||
}
|
||
}
|
||
|
||
fetch_certificates() {
|
||
this.stop_polling();
|
||
this.$wrap.find(".jey-asan-poll-status").text(__("Loading certificates..."));
|
||
frappe.call({
|
||
method: "invoice_az.auth.get_auth_certificates",
|
||
args: { asan_login_name: this.data.asan_login_name },
|
||
callback: (r) => {
|
||
const m = r.message || {};
|
||
if (!m.success) {
|
||
this.$wrap.find(".jey-status").text(m.message || __("Failed to load certificates"));
|
||
return;
|
||
}
|
||
const all = m.certificates || [];
|
||
this.asan_certificates = all.filter((c) => c.taxpayerType === "legal");
|
||
this.asan_state = "certs";
|
||
this.show_current_step();
|
||
},
|
||
});
|
||
}
|
||
|
||
confirm_and_pick_certificate(idx) {
|
||
const cert = this.asan_certificates[idx];
|
||
if (!cert) return;
|
||
const legal = cert.legalInfo || {};
|
||
const certName = `${legal.name || ""} (${legal.voen || legal.tin || ""})`;
|
||
frappe.confirm(
|
||
__("Confirm selection: {0}?", [frappe.utils.escape_html(certName)]),
|
||
() => this.pick_certificate(idx),
|
||
() => {}
|
||
);
|
||
}
|
||
|
||
pick_certificate(idx) {
|
||
const cert = this.asan_certificates[idx];
|
||
if (!cert) return;
|
||
const legal = cert.legalInfo || {};
|
||
const certName = `${legal.name || ""} (${legal.voen || legal.tin || ""})`;
|
||
|
||
frappe.call({
|
||
method: "invoice_az.auth.select_certificate",
|
||
args: {
|
||
asan_login_name: this.data.asan_login_name,
|
||
certificate_data: cert,
|
||
certificate_name: certName,
|
||
},
|
||
freeze: true,
|
||
freeze_message: __("Selecting certificate..."),
|
||
callback: (r) => {
|
||
const m = r.message || {};
|
||
if (!m.success) {
|
||
this.$wrap.find(".jey-status").text(m.message || __("Failed to select certificate"));
|
||
return;
|
||
}
|
||
this.run_select_taxpayer();
|
||
},
|
||
});
|
||
}
|
||
|
||
run_select_taxpayer() {
|
||
frappe.call({
|
||
method: "invoice_az.auth.select_taxpayer",
|
||
args: { asan_login_name: this.data.asan_login_name },
|
||
freeze: true,
|
||
freeze_message: __("Choosing taxpayer & getting main token..."),
|
||
callback: (r) => {
|
||
const m = r.message || {};
|
||
if (!m.success) {
|
||
this.$wrap.find(".jey-status").text(m.message || __("Failed to choose taxpayer"));
|
||
return;
|
||
}
|
||
this.fetch_company_from_cert();
|
||
},
|
||
});
|
||
}
|
||
|
||
fetch_company_from_cert() {
|
||
frappe.call({
|
||
method: "jey_wizard.api.get_company_from_cert",
|
||
args: { asan_login_name: this.data.asan_login_name },
|
||
callback: (r) => {
|
||
const m = r.message || {};
|
||
this.data.company_name = m.company_name || this.data.company_name;
|
||
this.data.asan_voen = m.voen || "";
|
||
this.asan_state = "fetching";
|
||
this.show_current_step();
|
||
this.fetch_etaxes_data();
|
||
},
|
||
});
|
||
}
|
||
|
||
reset_company_selection() {
|
||
// Reached from asan/done when the user realizes they picked the wrong cert.
|
||
// The e-taxes bearer_token is single-use per chooseTaxpayer call, so after
|
||
// the first select_taxpayer it can't be reused — the server returns 401.
|
||
// The only way to pick a different cert is to run a fresh auth round
|
||
// (another SMS + phone tap). We warn about that explicitly.
|
||
frappe.confirm(
|
||
__("This will clear loaded data and ƏMAS selection, and send a new confirmation request to your phone. Continue?"),
|
||
() => {
|
||
frappe.call({
|
||
method: "jey_wizard.api.reset_company_selection",
|
||
args: { asan_login_name: this.data.asan_login_name },
|
||
freeze: true,
|
||
freeze_message: __("Clearing..."),
|
||
callback: (r) => {
|
||
const m = r.message || {};
|
||
if (!m.ok) {
|
||
this.$wrap.find(".jey-status").text(__("Failed to clear data."));
|
||
return;
|
||
}
|
||
this.data.company_name = "";
|
||
this.data.asan_voen = "";
|
||
this.data.amas_selected_employees = [];
|
||
this.data.amas_skipped = false;
|
||
this.data.amas_organization_name = "";
|
||
this.amas_employees = [];
|
||
this.amas_existing_map = {};
|
||
this.amas_state = "intro";
|
||
this.asan_fetch_summary = {};
|
||
this.asan_fetch_errors = {};
|
||
this.asan_certificates = [];
|
||
this.asan_bearer_token = "";
|
||
this.asan_verification_code = "";
|
||
// Kick off a fresh auth round. Phone and user_id are still on
|
||
// the Asan Login record, so handle_authentication works without
|
||
// re-prompting the user.
|
||
this.trigger_asan_authentication();
|
||
},
|
||
});
|
||
},
|
||
() => {}
|
||
);
|
||
}
|
||
|
||
fetch_etaxes_data() {
|
||
frappe.call({
|
||
method: "jey_wizard.etaxes.fetch_all_etaxes",
|
||
callback: (r) => {
|
||
const m = r.message || {};
|
||
this.asan_fetch_summary = m.summary || {};
|
||
this.asan_fetch_errors = m.errors || {};
|
||
this.asan_state = "done";
|
||
this.show_current_step();
|
||
},
|
||
error: () => {
|
||
this.asan_fetch_summary = {};
|
||
this.asan_fetch_errors = { all: __("fetch call failed — see server logs") };
|
||
this.asan_state = "done";
|
||
this.show_current_step();
|
||
},
|
||
});
|
||
}
|
||
|
||
// ---------- AMAS flow ----------
|
||
|
||
amas_start_auth() {
|
||
if (!this.data.asan_login_name) {
|
||
this.$wrap.find(".jey-status").text(__("Finish Asan Imza authentication first."));
|
||
return;
|
||
}
|
||
this.data.amas_skipped = false;
|
||
this.amas_verification_code = "";
|
||
this.amas_state = "auth";
|
||
this.show_current_step();
|
||
|
||
// Long-polling: server holds the call until user confirms on phone (~120s).
|
||
frappe.call({
|
||
method: "invoice_az.invoice_az.doctype.asan_login.asan_login.start_mygovid_login",
|
||
args: { asan_login_name: this.data.asan_login_name },
|
||
timeout: MYGOVID_TIMEOUT_MS,
|
||
callback: (r) => {
|
||
if (this.amas_state !== "auth") return;
|
||
const m = r.message || {};
|
||
if (!m.success) {
|
||
this.$wrap.find(".jey-status").text(m.message || __("MyGovID authentication failed."));
|
||
this.amas_state = "intro";
|
||
this.show_current_step();
|
||
return;
|
||
}
|
||
this.amas_connect();
|
||
},
|
||
error: () => {
|
||
if (this.amas_state !== "auth") return;
|
||
this.$wrap.find(".jey-status").text(__("MyGovID request timed out."));
|
||
this.amas_state = "intro";
|
||
this.show_current_step();
|
||
},
|
||
});
|
||
|
||
// Fetch verification code shortly after the server started the long-poll.
|
||
setTimeout(() => {
|
||
if (this.amas_state !== "auth") return;
|
||
frappe.call({
|
||
method: "invoice_az.invoice_az.doctype.asan_login.asan_login.get_mygovid_verification_code",
|
||
args: { asan_login_name: this.data.asan_login_name },
|
||
callback: (r) => {
|
||
if (this.amas_state !== "auth") return;
|
||
const m = r.message || {};
|
||
if (m.success && m.code) {
|
||
this.amas_verification_code = m.code;
|
||
this.show_current_step();
|
||
}
|
||
},
|
||
});
|
||
}, 800);
|
||
}
|
||
|
||
amas_connect() {
|
||
this.amas_state = "connecting";
|
||
this.show_current_step();
|
||
|
||
frappe.call({
|
||
method: "invoice_az.amas_api.connect_amas",
|
||
args: { asan_login_name: this.data.asan_login_name },
|
||
callback: (r) => {
|
||
const m = r.message || {};
|
||
if (!m.success) {
|
||
this.$wrap.find(".jey-status").text(m.message || __("Failed to connect to ƏMAS."));
|
||
this.amas_state = "intro";
|
||
this.show_current_step();
|
||
return;
|
||
}
|
||
this.amas_accounts = m.accounts || [];
|
||
// Auto-pick by VOEN match with the e-taxes cert; fall back to manual list.
|
||
const targetVoen = String(this.data.asan_voen || "").trim();
|
||
let matchIdx = -1;
|
||
if (targetVoen) {
|
||
matchIdx = this.amas_accounts.findIndex((a) => {
|
||
const voen = String(a.accountNumber || a.voen || a.number || "").trim();
|
||
return voen && voen === targetVoen;
|
||
});
|
||
}
|
||
if (matchIdx >= 0) {
|
||
this.amas_pick_account(matchIdx);
|
||
} else if (this.amas_accounts.length === 1) {
|
||
this.amas_pick_account(0);
|
||
} else if (this.amas_accounts.length === 0) {
|
||
this.$wrap.find(".jey-status").text(__("No organizations found in ƏMAS for this account."));
|
||
this.amas_state = "intro";
|
||
this.show_current_step();
|
||
} else {
|
||
this.amas_state = "accounts";
|
||
this.show_current_step();
|
||
}
|
||
},
|
||
});
|
||
}
|
||
|
||
amas_pick_account(idx) {
|
||
const acc = this.amas_accounts[idx];
|
||
if (!acc) return;
|
||
const oid = acc.accountOid || acc.oid || "";
|
||
const name = acc.accountName || acc.name || "";
|
||
const number = acc.accountNumber || acc.voen || acc.number || "";
|
||
const role = acc.roleName || acc.role || "chairman";
|
||
|
||
this.amas_state = "connecting";
|
||
this.show_current_step();
|
||
|
||
frappe.call({
|
||
method: "invoice_az.amas_api.change_amas_account",
|
||
args: {
|
||
asan_login_name: this.data.asan_login_name,
|
||
account_oid: oid,
|
||
account_name: name,
|
||
account_number: number,
|
||
role_name: role,
|
||
},
|
||
callback: (r) => {
|
||
const m = r.message || {};
|
||
if (!m.success) {
|
||
this.$wrap.find(".jey-status").text(m.message || __("Failed to select organization."));
|
||
this.amas_state = "intro";
|
||
this.show_current_step();
|
||
return;
|
||
}
|
||
this.data.amas_organization_name = name;
|
||
this.amas_load_employees();
|
||
},
|
||
});
|
||
}
|
||
|
||
amas_load_employees() {
|
||
this.amas_state = "loading";
|
||
this.show_current_step();
|
||
|
||
frappe.call({
|
||
method: "invoice_az.amas_api.get_employees_report",
|
||
args: {
|
||
asan_login_name: this.data.asan_login_name,
|
||
offset: 0,
|
||
limit: 1000,
|
||
},
|
||
callback: (r) => {
|
||
const m = r.message || {};
|
||
if (!m.success) {
|
||
this.$wrap.find(".jey-status").text(m.message || __("Failed to fetch employees."));
|
||
this.amas_state = "intro";
|
||
this.show_current_step();
|
||
return;
|
||
}
|
||
const employees = m.employees || [];
|
||
if (employees.length === 0) {
|
||
this.$wrap.find(".jey-status").text(__("No employees found in ƏMAS for this organization."));
|
||
this.amas_state = "intro";
|
||
this.show_current_step();
|
||
return;
|
||
}
|
||
this.amas_employees = employees;
|
||
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() {
|
||
const all = this.amas_employees || [];
|
||
if (all.length === 0) {
|
||
this.amas_state = "intro";
|
||
this.show_current_step();
|
||
return;
|
||
}
|
||
const createDesignation = 1;
|
||
this.data.amas_create_designation = createDesignation;
|
||
|
||
frappe.call({
|
||
method: "jey_wizard.amas.cache_selected_employees",
|
||
args: {
|
||
employees_json: JSON.stringify(all),
|
||
create_designation: createDesignation,
|
||
},
|
||
callback: (r) => {
|
||
const m = r.message || {};
|
||
if (!m.ok) {
|
||
this.$wrap.find(".jey-status").text(__("Failed to cache selection."));
|
||
this.amas_state = "intro";
|
||
this.show_current_step();
|
||
return;
|
||
}
|
||
this.data.amas_selected_employees = all;
|
||
this.data.amas_skipped = false;
|
||
this.amas_state = "done";
|
||
this.show_current_step();
|
||
},
|
||
});
|
||
}
|
||
|
||
// ---------- finalization ----------
|
||
|
||
build_setup_args() {
|
||
const company = (this.data.company_name || "").trim() || "Test Company AZ";
|
||
// Prefer the user's (possibly edited) abbr captured in capture_current;
|
||
// fall back to the deriver if something bypassed validation.
|
||
const abbr = (
|
||
(this.data.company_abbr || "").toUpperCase().replace(/\s+/g, "") ||
|
||
this.derive_abbr(company) ||
|
||
"CO"
|
||
).slice(0, 10);
|
||
const admin = frappe.boot.user || {};
|
||
|
||
return {
|
||
language: LANG_NAME[this.data.language] || "English",
|
||
country: "Azerbaijan",
|
||
timezone: "Asia/Baku",
|
||
currency: "AZN",
|
||
enable_telemetry: 0,
|
||
full_name: admin.full_name || "Administrator",
|
||
email: admin.email || "admin@example.com",
|
||
password: "",
|
||
company_name: company,
|
||
company_abbr: abbr,
|
||
chart_of_accounts: "Azerbaijan Republic",
|
||
fy_start_date: this.data.fy_start_date,
|
||
fy_end_date: this.data.fy_end_date,
|
||
valuation_method: this.data.valuation_method || "FIFO",
|
||
setup_demo: 0,
|
||
};
|
||
}
|
||
|
||
finalize() {
|
||
this.$wrap.find(".jey-body").html(
|
||
`<p>${__("Setting up your system. This can take a minute...")}</p>`
|
||
);
|
||
this.$wrap.find(".jey-nav").empty();
|
||
this.$wrap.find(".jey-status").empty();
|
||
|
||
const values = this.build_setup_args();
|
||
|
||
frappe.call({
|
||
method: "frappe.desk.page.setup_wizard.setup_wizard.setup_complete",
|
||
args: { args: values },
|
||
freeze: true,
|
||
callback: (r) => {
|
||
const m = r.message || {};
|
||
if (m.status === "ok") {
|
||
this.$wrap.find(".jey-body").html(
|
||
`<p>${__("Setup complete! Redirecting...")}</p>`
|
||
);
|
||
setTimeout(() => {
|
||
window.location.href = "/app";
|
||
}, 1500);
|
||
} else if (m.status === "registered") {
|
||
this.show_error(
|
||
__("Setup is running in background mode, not supported by skeleton"),
|
||
JSON.stringify(m, null, 2)
|
||
);
|
||
} else if (m.fail !== undefined) {
|
||
this.show_error(m.fail, JSON.stringify(m, null, 2));
|
||
} else {
|
||
this.show_error(
|
||
__("Setup returned unexpected response"),
|
||
JSON.stringify(m, null, 2)
|
||
);
|
||
}
|
||
},
|
||
error: () => {
|
||
const resp = frappe.last_response || {};
|
||
this.show_error(
|
||
resp.setup_wizard_failure_message || __("Setup failed"),
|
||
resp.exc || resp.exception || __("(check server logs)")
|
||
);
|
||
},
|
||
});
|
||
}
|
||
|
||
show_error(summary, detail) {
|
||
this.$wrap.find(".jey-body").html(`
|
||
<h3 style="color:#c00;margin-bottom:8px">${frappe.utils.escape_html(summary)}</h3>
|
||
<pre style="max-height:360px;overflow:auto;background:#f7f7f7;padding:12px;border-radius:4px;font-size:12px;white-space:pre-wrap">${
|
||
detail ? frappe.utils.escape_html(String(detail)) : __("(no details)")
|
||
}</pre>
|
||
`);
|
||
this.$wrap.find(".jey-status").empty();
|
||
this.$wrap.find(".jey-nav")
|
||
.empty()
|
||
.append(
|
||
$(`<button class="btn btn-default jey-retry">${__("Retry")}</button>`)
|
||
.on("click", () => window.location.reload())
|
||
);
|
||
}
|
||
};
|