diff --git a/ns_app/public/js/customer_quick_entry.js b/ns_app/public/js/customer_quick_entry.js index 857beb8..eadfaf8 100644 --- a/ns_app/public/js/customer_quick_entry.js +++ b/ns_app/public/js/customer_quick_entry.js @@ -1,317 +1,271 @@ frappe.provide("ns_app.customer"); -console.log("NS APP CUSTOMER JS LOADED"); +console.log("NS App: customer_quick_entry.js loaded"); -$(document).ready(() => { +// ─── Install override ──────────────────────────────────────────────────────── +// Poll until frappe.ui.form.make_quick_entry exists (ERPNext bundle settled), +// then wrap make_quick_entry so we re-assert our class at every Customer call. - setTimeout(() => { +(function () { + "use strict"; - const TargetClass = - frappe.ui.form.CustomerQuickEntryForm; + function install_override() { + const Base = frappe.ui.form.CustomerQuickEntryForm; + if (!Base) return false; + if (Base.__ns_patched) return true; - if (!TargetClass) { + frappe.ui.form.CustomerQuickEntryForm = class extends Base { - console.error( - "NS App: CustomerQuickEntryForm not found" - ); + // render_dialog is called by QuickEntryForm.setup() after the + // constructor runs. At this point this.after_insert is already + // set by the base constructor. We show our custom dialog instead + // of ERPNext's, but we PRESERVE this.after_insert so the link + // field callback chain stays intact. + render_dialog() { + console.log("NS App: render_dialog intercepted"); - return; - } + // Capture the typed customer name before anything mutates focus + const customer_name = this._get_typed_name(); - // Prevent duplicate patching - if (TargetClass.__ns_patched) { + console.log("NS App: prefill name =", customer_name); - console.log( - "NS App: already patched" - ); + // Open our custom dialog, passing: + // - the prefilled name + // - this.after_insert as the callback so ERPNext's link + // field gets notified when the customer is created + ns_app.customer.open_quick_entry({ + customer_name: customer_name, + after_insert: this.after_insert // ← this is the key + }); - return; - } - - console.log( - "NS App: patching CustomerQuickEntryForm" - ); - - frappe.ui.form.CustomerQuickEntryForm = - class extends TargetClass { - - render_dialog() { - - console.log( - "NS App: render_dialog intercepted" - ); - - let customer_name = ""; - - // Route option first - if (frappe.route_options?.name) { - - customer_name = - frappe.route_options.name; - } - - // Focused field fallback - if (!customer_name) { - - const active = - document.activeElement; - - if ( - active && - active.value - ) { - - customer_name = - active.value; - } - } - - // cur_frm fallback - if ( - !customer_name && - typeof cur_frm !== - "undefined" && - cur_frm - ) { - - customer_name = - cur_frm.doc.customer || - cur_frm.doc.party_name || - ""; - } - - console.log( - "NS App: Captured customer name:", - customer_name - ); - - // DO NOT call super.render_dialog() - // This restores the fully custom dialog - - ns_app.customer.open_quick_entry({ - customer_name: - customer_name, - - callback: - this.after_insert - }); + // We intentionally do NOT call super.render_dialog(). + // ERPNext's dialog is replaced entirely by ours. + // But we must call this.dialog = something so that + // QuickEntryForm.setup() doesn't crash on teardown. + // A minimal placeholder dialog satisfies that contract. + if (!this.dialog) { + this.dialog = { hide: () => {}, get_field: () => null }; } - }; + } - TargetClass.__ns_patched = true; + _get_typed_name() { + // 1. The link field control that triggered quick entry + if (frappe.ui.form.cur_field) { + const v = frappe.ui.form.cur_field.get_value?.(); + if (v) return v; + } + // 2. Whatever input had focus when the dialog opened + const el = document.activeElement; + if (el?.value) return el.value; + // 3. Current form doc + if (typeof cur_frm !== "undefined" && cur_frm?.doc) { + return cur_frm.doc.customer || cur_frm.doc.party_name || ""; + } + return ""; + } + }; - console.log( - "NS App: CustomerQuickEntryForm patched successfully" - ); + frappe.ui.form.CustomerQuickEntryForm.__ns_patched = true; + console.log("NS App: CustomerQuickEntryForm override installed ✓"); + return true; + } - }, 1000); + function patch_make_quick_entry() { + const orig = frappe.ui.form.make_quick_entry; + if (!orig || orig.__ns_patched) return; -}); + frappe.ui.form.make_quick_entry = function (doctype, after_insert, init_callback, doc, force) { + if (doctype === "Customer") { + install_override(); // re-assert in case anything clobbered it + } + return orig.apply(this, arguments); + }; + + frappe.ui.form.make_quick_entry.__ns_patched = true; + console.log("NS App: make_quick_entry patched ✓"); + } + + let attempts = 0; + const poller = setInterval(() => { + if (++attempts > 100) { + clearInterval(poller); + console.error("NS App: gave up waiting for frappe.ui.form.make_quick_entry"); + return; + } + if (frappe.ui.form?.make_quick_entry) { + clearInterval(poller); + install_override(); + patch_make_quick_entry(); + } + }, 100); + +})(); + + +// ─── Custom dialog ─────────────────────────────────────────────────────────── +// opts: +// customer_name {string} prefill value +// after_insert {function} ERPNext's link field callback — MUST be called +// with the new customer name on success ns_app.customer.open_quick_entry = function (opts = {}) { - console.log( - "NS App: Custom Customer Quick Entry OPENED" - ); + console.log("NS App: open_quick_entry called", opts); const d = new frappe.ui.Dialog({ - title: "New Customer", - - size: "large", - + size: "large", fields: [ - // ───────── CUSTOMER ───────── + // ── Customer ────────────────────────────────────────────────── + { fieldtype: "Section Break", label: "Customer Information" }, { - fieldtype: "Section Break", - label: "Customer Information" + fieldname: "customer_name", + label: "Customer Name", + fieldtype: "Data", + reqd: 1, + default: opts.customer_name || "", + description: "Enter the customer or company name" + }, + { + fieldname: "customer_type", + label: "Customer Type", + fieldtype: "Select", + options: "Company\nIndividual", + default: "Company", + reqd: 1, + description: "Select whether this customer is a company or individual" + }, + { + fieldname: "customer_group", + label: "Customer Group", + fieldtype: "Link", + options: "Customer Group", + default: "Commercial", + reqd: 1, + description: "Select the customer group" + }, + { + fieldname: "custom_send_via", + label: "Preferred Delivery Method", + fieldtype: "Select", + options: "mail\nemail\nfax", + description: "Choose how documents should be sent to the customer" }, + // ── Contact ─────────────────────────────────────────────────── + { fieldtype: "Section Break", label: "Primary Contact" }, + { - fieldname: "customer_name", - label: "Customer Name", - fieldtype: "Data", - reqd: 1, - default: - opts.customer_name || "", - description: - "Enter the customer or company name" + fieldname: "email_id", + label: "Email Address", + fieldtype: "Data", + options: "Email", + description: "Enter the customer's email address" + }, + { + fieldname: "mobile_no", + label: "Mobile Phone Number", + fieldtype: "Data", + reqd: 1, + description: "Enter the customer's mobile phone number" }, + // ── Address ─────────────────────────────────────────────────── + { fieldtype: "Section Break", label: "Address Information" }, + { - fieldname: "customer_type", - label: "Customer Type", - fieldtype: "Select", - options: - "Company\nIndividual", - default: "Company", - reqd: 1, - description: - "Select whether this customer is a company or individual" + fieldname: "address_line1", + label: "Address Line 1", + fieldtype: "Data", + reqd: 1, + description: "Enter the street address" }, - { - fieldname: "customer_group", - label: "Customer Group", - fieldtype: "Link", - options: "Customer Group", - default: "Commercial", - reqd: 1, - description: - "Select the customer group" + fieldname: "address_line2", + label: "Address Line 2", + fieldtype: "Data", + description: "Enter apartment, suite, or secondary address information" }, - { - fieldname: "custom_send_via", - label: - "Preferred Delivery Method", - fieldtype: "Select", - options: - "mail\nemail\nfax", - description: - "Choose how documents should be sent to the customer" + fieldname: "pincode", + label: "ZIP Code", + fieldtype: "Data", + reqd: 1, + description: "Enter the ZIP or postal code" }, - - // ───────── CONTACT ───────── - { - fieldtype: "Section Break", - label: "Primary Contact" + fieldname: "city", + label: "City", + fieldtype: "Data", + description: "Enter the city" }, - { - fieldname: "email_id", - label: "Email Address", - fieldtype: "Data", - options: "Email", - description: - "Enter the customer's email address" + fieldname: "state", + label: "State", + fieldtype: "Data", + description: "Enter the state" }, - { - fieldname: "mobile_no", - label: - "Mobile Phone Number", - fieldtype: "Data", - reqd: 1, - description: - "Enter the customer's mobile phone number" - }, - - // ───────── ADDRESS ───────── - - { - fieldtype: "Section Break", - label: - "Address Information" - }, - - { - fieldname: "address_line1", - label: - "Address Line 1", - fieldtype: "Data", - reqd: 1, - description: - "Enter the street address" - }, - - { - fieldname: "address_line2", - label: - "Address Line 2", - fieldtype: "Data", - description: - "Enter apartment, suite, or secondary address information" - }, - - { - fieldname: "pincode", - label: "ZIP Code", - fieldtype: "Data", - reqd: 1, - description: - "Enter the ZIP or postal code" - }, - - { - fieldname: "city", - label: "City", - fieldtype: "Data", - description: - "Enter the city" - }, - - { - fieldname: "state", - label: "State", - fieldtype: "Data", - description: - "Enter the state" - }, - - { - fieldname: "country", - label: "Country", - fieldtype: "Link", - options: "Country", - default: - "United States", - description: - "Select the country" + fieldname: "country", + label: "Country", + fieldtype: "Link", + options: "Country", + default: "United States", + description: "Select the country" } ], - primary_action_label: - "Create Customer", + primary_action_label: "Create Customer", primary_action(values) { - - console.log( - "NS App: Create Customer clicked", - values - ); - + console.log("NS App: submitting customer creation", values); d.disable_primary_action(); frappe.call({ - - method: - "ns_app.api.customer.create_customer_full", - - args: values, + method: "ns_app.api.customer.create_customer_full", + args: values, callback(r) { + if (!r.message) { + console.error("NS App: create_customer_full returned empty"); + d.enable_primary_action(); + return; + } - console.log( - "NS App: Customer created", - r.message - ); + const customer_name = r.message; + console.log("NS App: customer created →", customer_name); d.hide(); frappe.show_alert({ - - message: - "Customer created via NS App", - + message: `Customer "${customer_name}" created`, indicator: "green" }); - if ( - opts.callback - ) { - - opts.callback( - r.message - ); + // ── Hand control back to ERPNext ───────────────────── + // after_insert is ERPNext's link field callback. + // Calling it with the new customer name does everything: + // - populates the Customer field on the originating form + // - triggers the field's onchange/fetch logic + // - does NOT require any routing from our side + // This is the ONLY correct way to resume the originating + // document flow without racing the backend transaction. + if (typeof opts.after_insert === "function") { + console.log("NS App: calling after_insert with", customer_name); + // ERPNext's callback expects an object with a .name property, + // not a plain string — { name: "cu-00741" } + opts.after_insert({ name: customer_name }); + } else { + // Fallback: no callback was passed (e.g. dialog opened + // standalone). Just reload to a new Customer form. + console.warn("NS App: no after_insert callback — navigating to customer"); + frappe.set_route("Form", "Customer", customer_name); } }, always() { - d.enable_primary_action(); } }); @@ -320,166 +274,48 @@ ns_app.customer.open_quick_entry = function (opts = {}) { d.show(); - // Accessibility labels - + // ── Accessibility ──────────────────────────────────────────────────────── setTimeout(() => { - - d.fields.forEach(field => { - - const control = - d.get_field( - field.fieldname - ); - - if ( - !control || - !control.$input - ) { - return; - } - - control.$input.attr( - "aria-label", - field.label || - field.fieldname - ); - - control.$input.attr( - "title", - field.label || - field.fieldname - ); - - if (field.label) { - - control.$input.attr( - "placeholder", - field.label - ); + d.fields.forEach(f => { + const ctrl = d.get_field(f.fieldname); + if (!ctrl?.$input) return; + ctrl.$input + .attr("aria-label", f.label || f.fieldname) + .attr("title", f.label || f.fieldname); + if (f.label && !ctrl.$input.attr("placeholder")) { + ctrl.$input.attr("placeholder", f.label); } }); + }, 100); - console.log( - "NS App: accessibility applied" - ); + // ── ZIP autofill ───────────────────────────────────────────────────────── + d.fields_dict.pincode.df.onchange = () => { + const zip = d.get_value("pincode"); + if (!zip || zip.length < 5) return; - }, 300); + fetch(`https://api.zippopotam.us/us/${zip}`) + .then(r => r.ok ? r.json() : null) + .then(data => { + if (!data?.places?.length) return; + const p = data.places[0]; + d.set_value("city", p["place name"]); + d.set_value("state", p["state"]); + d.set_value("country", data.country); + console.log("NS App: ZIP autofill →", p["place name"], p["state"]); + }) + .catch(() => {}); + }; - // ZIP autofill + // ── Enter → next field ─────────────────────────────────────────────────── + d.$wrapper.on("keydown", "input, select, textarea", function (e) { + if (e.key !== "Enter") return; + if (document.activeElement?.classList.contains("btn-primary")) return; - d.fields_dict.pincode.df.onchange = - () => { - - const zip = - d.get_value( - "pincode" - ); - - if ( - !zip || - zip.length < 5 - ) { - return; - } - - console.log( - "NS App: ZIP lookup", - zip - ); - - fetch( - `https://api.zippopotam.us/us/${zip}` - ) - .then(r => - r.ok - ? r.json() - : null - ) - .then(data => { - - if ( - !data || - !data.places?.length - ) { - return; - } - - const p = - data.places[0]; - - d.set_value( - "city", - p["place name"] - ); - - d.set_value( - "state", - p["state"] - ); - - d.set_value( - "country", - data.country - ); - - console.log( - "NS App: ZIP autofill success" - ); - }) - .catch(() => {}); - }; - - // Enter navigation - - d.$wrapper.on( - "keydown", - "input, select, textarea", - function (e) { - - if ( - e.key === "Enter" - ) { - - const active = - document.activeElement; - - // Allow submit only - // on primary button - - if ( - active && - active.classList.contains( - "btn-primary" - ) - ) { - return; - } - - e.preventDefault(); - - const fields = - d.$wrapper - .find( - "input, select, textarea" - ) - .filter( - ":visible:not([disabled])" - ); - - const index = - fields.index(this); - - if ( - index > -1 && - index + 1 < - fields.length - ) { - - fields - .eq(index + 1) - .focus(); - } - } - } - ); + e.preventDefault(); + const fields = d.$wrapper + .find("input, select, textarea") + .filter(":visible:not([disabled])"); + const i = fields.index(this); + if (i > -1 && i + 1 < fields.length) fields.eq(i + 1).focus(); + }); }; \ No newline at end of file