485 lines
12 KiB
JavaScript
485 lines
12 KiB
JavaScript
frappe.provide("ns_app.customer");
|
|
|
|
console.log("NS APP CUSTOMER JS LOADED");
|
|
|
|
$(document).ready(() => {
|
|
|
|
setTimeout(() => {
|
|
|
|
const TargetClass =
|
|
frappe.ui.form.CustomerQuickEntryForm;
|
|
|
|
if (!TargetClass) {
|
|
|
|
console.error(
|
|
"NS App: CustomerQuickEntryForm not found"
|
|
);
|
|
|
|
return;
|
|
}
|
|
|
|
// Prevent duplicate patching
|
|
if (TargetClass.__ns_patched) {
|
|
|
|
console.log(
|
|
"NS App: already patched"
|
|
);
|
|
|
|
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
|
|
});
|
|
}
|
|
};
|
|
|
|
TargetClass.__ns_patched = true;
|
|
|
|
console.log(
|
|
"NS App: CustomerQuickEntryForm patched successfully"
|
|
);
|
|
|
|
}, 1000);
|
|
|
|
});
|
|
|
|
ns_app.customer.open_quick_entry = function (opts = {}) {
|
|
|
|
console.log(
|
|
"NS App: Custom Customer Quick Entry OPENED"
|
|
);
|
|
|
|
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: Create Customer clicked",
|
|
values
|
|
);
|
|
|
|
d.disable_primary_action();
|
|
|
|
frappe.call({
|
|
|
|
method:
|
|
"ns_app.api.customer.create_customer_full",
|
|
|
|
args: values,
|
|
|
|
callback(r) {
|
|
|
|
console.log(
|
|
"NS App: Customer created",
|
|
r.message
|
|
);
|
|
|
|
d.hide();
|
|
|
|
frappe.show_alert({
|
|
|
|
message:
|
|
"Customer created via NS App",
|
|
|
|
indicator: "green"
|
|
});
|
|
|
|
if (
|
|
opts.callback
|
|
) {
|
|
|
|
opts.callback(
|
|
r.message
|
|
);
|
|
}
|
|
},
|
|
|
|
always() {
|
|
|
|
d.enable_primary_action();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
d.show();
|
|
|
|
// Accessibility labels
|
|
|
|
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
|
|
);
|
|
}
|
|
});
|
|
|
|
console.log(
|
|
"NS App: accessibility applied"
|
|
);
|
|
|
|
}, 300);
|
|
|
|
// ZIP autofill
|
|
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
);
|
|
}; |