Files
ns_erpnext_app/ns_app/public/js/customer_quick_entry.js

321 lines
13 KiB
JavaScript

frappe.provide("ns_app.customer");
console.log("NS App: customer_quick_entry.js loaded");
// ─── 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.
(function () {
"use strict";
function install_override() {
const Base = frappe.ui.form.CustomerQuickEntryForm;
if (!Base) return false;
if (Base.__ns_patched) return true;
frappe.ui.form.CustomerQuickEntryForm = class extends Base {
// 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");
// Capture the typed customer name before anything mutates focus
const customer_name = this._get_typed_name();
console.log("NS App: prefill name =", customer_name);
// 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
});
// 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 };
}
}
_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 "";
}
};
frappe.ui.form.CustomerQuickEntryForm.__ns_patched = true;
console.log("NS App: CustomerQuickEntryForm override installed ✓");
return true;
}
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: open_quick_entry called", opts);
const d = new frappe.ui.Dialog({
title: "New Customer",
size: "large",
fields: [
// ── Customer ──────────────────────────────────────────────────
{ 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: "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: "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"
}
],
primary_action_label: "Create Customer",
primary_action(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,
callback(r) {
if (!r.message) {
console.error("NS App: create_customer_full returned empty");
d.enable_primary_action();
return;
}
const customer_name = r.message;
console.log("NS App: customer created →", customer_name);
d.hide();
frappe.show_alert({
message: `Customer "${customer_name}" created`,
indicator: "green"
});
// ── 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();
}
});
}
});
d.show();
// ── Accessibility ────────────────────────────────────────────────────────
setTimeout(() => {
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);
// ── ZIP autofill ─────────────────────────────────────────────────────────
d.fields_dict.pincode.df.onchange = () => {
const zip = d.get_value("pincode");
if (!zip || zip.length < 5) return;
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(() => {});
};
// ── 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;
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();
});
};