feat: Add Customer Records page with Lead integration

This commit is contained in:
Westech Admin
2026-05-20 22:03:33 +00:00
parent 2e56814a1e
commit 6f23c32af3
11 changed files with 499 additions and 2 deletions
@@ -0,0 +1 @@
# Customer Records page
@@ -0,0 +1,227 @@
<style>
.customer-records-page {
font-family: Segoe UI, Arial, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background: white;
}
.customer-records-page h1 {
font-size: 36px;
font-weight: 900;
text-align: center;
margin-bottom: 20px;
letter-spacing: 2px;
text-transform: uppercase;
}
.toolbar {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.toolbar button {
padding: 8px 20px;
border: 2px solid #333;
background: #f0f0f0;
font-size: 14px;
font-weight: 600;
cursor: pointer;
text-transform: uppercase;
border-radius: 3px;
}
.toolbar button:hover { background: #e0e0e0; }
.toolbar .record-counter {
font-size: 18px;
font-weight: 600;
margin: 0 10px;
}
.toolbar .nav-btn {
padding: 6px 14px;
font-size: 18px;
min-width: 40px;
}
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px 40px;
margin-bottom: 20px;
}
.form-group {
display: flex;
align-items: center;
gap: 10px;
}
.form-group label {
font-weight: 600;
font-size: 13px;
min-width: 140px;
text-align: right;
white-space: nowrap;
}
.form-group input,
.form-group textarea {
flex: 1;
padding: 6px 10px;
border: 1px solid #ccc;
font-size: 13px;
background: #f8f8f8;
}
.form-group textarea {
min-height: 60px;
resize: vertical;
}
.form-group.full-width {
grid-column: 1 / -1;
}
.search-bar {
display: flex;
align-items: center;
gap: 10px;
margin-top: 20px;
padding-top: 20px;
border-top: 2px solid #333;
}
.search-bar label {
font-weight: 700;
font-size: 16px;
text-transform: uppercase;
}
.search-bar select,
.search-bar input {
padding: 6px 10px;
border: 1px solid #ccc;
font-size: 13px;
}
.search-bar .search-label {
font-weight: 700;
font-size: 16px;
text-transform: uppercase;
margin: 0 10px;
}
.search-bar button {
padding: 6px 20px;
border: 2px solid #333;
background: #f0f0f0;
font-weight: 600;
cursor: pointer;
text-transform: uppercase;
}
.search-bar button:hover { background: #e0e0e0; }
</style>
<div class="customer-records-page">
<h1>Modify Records</h1>
<div class="toolbar">
<button id="btn-save">Save</button>
<span class="record-counter">
<span id="current-index">0</span> of <span id="total-count">0</span>
</span>
<button class="nav-btn" id="btn-prev">&lt;</button>
<button class="nav-btn" id="btn-next">&gt;</button>
<button class="nav-btn" id="btn-first">&lt;&lt;</button>
<button class="nav-btn" id="btn-last">&gt;&gt;</button>
<button id="btn-delete">Delete</button>
<button id="btn-print">Print</button>
</div>
<div class="form-grid">
<div class="form-group">
<label>Record #</label>
<input type="text" id="record-number" readonly>
</div>
<div class="form-group">
<label>Additional Numbers:</label>
<input type="text" id="additional-numbers">
</div>
<div class="form-group">
<label>*</label>
<input type="text" id="field-star">
</div>
<div class="form-group">
<label>Customer Address:</label>
<input type="text" id="customer-address">
</div>
<div class="form-group">
<label>Any Letter:</label>
<input type="text" id="any-letter">
</div>
<div class="form-group">
<label>City:</label>
<input type="text" id="city">
</div>
<div class="form-group">
<label>E</label>
<input type="text" id="field-e">
</div>
<div class="form-group">
<label>Zip:</label>
<input type="text" id="zip">
</div>
<div class="form-group">
<label>Company Name:</label>
<input type="text" id="company-name">
</div>
<div class="form-group">
<label>Contacted date:</label>
<input type="date" id="contacted-date">
</div>
<div class="form-group full-width">
<label>Contact Person(s):</label>
<textarea id="contact-persons"></textarea>
</div>
<div class="form-group">
<label>Follow up date:</label>
<input type="text" id="follow-up-date">
</div>
<div class="form-group">
<label>E-Mail Address:</label>
<input type="email" id="email-address">
</div>
<div class="form-group">
<label>Last P/U date:</label>
<input type="text" id="last-pu-date">
</div>
<div class="form-group full-width">
<label>Contact Numbers:</label>
<textarea id="contact-numbers"></textarea>
</div>
<div class="form-group full-width">
<label>Hours of Operation:</label>
<input type="text" id="hours-operation">
</div>
<div class="form-group full-width">
<label>Comments:</label>
<textarea id="comments"></textarea>
</div>
</div>
<div class="search-bar">
<label>SEARCH:</label>
<select id="search-field">
<option value="address">Address</option>
<option value="company_name">Company Name</option>
<option value="contact_person">Contact Person</option>
<option value="phone">Phone</option>
<option value="email">Email</option>
<option value="city">City</option>
<option value="zip">Zip</option>
<option value="record_number">Record #</option>
</select>
<span class="search-label">FOR</span>
<input type="text" id="search-value" style="min-width:200px;">
<button id="btn-search">Search!</button>
<button id="btn-reset">Reset</button>
</div>
</div>
@@ -0,0 +1,148 @@
frappe.pages["customer-records"].on_page_load = function(wrapper) {
var page = frappe.ui.make_app_page({
parent: wrapper,
title: __("Customer Records"),
single_column: true
});
var content = frappe.render_template("customer-records", {});
$(page.body).append(content);
var records = [];
var currentIdx = -1;
var searchResults = [];
function loadRecord(idx) {
if (idx < 0 || idx >= records.length) return;
currentIdx = idx;
var r = records[idx];
$("#current-index").text(idx + 1);
$("#total-count").text(records.length);
$("#record-number").val(r.name || "");
$("#additional-numbers").val(r.additional_numbers || "");
$("#field-star").val(r.field_star || "");
$("#customer-address").val(r.customer_address || "");
$("#any-letter").val(r.any_letter || "");
$("#city").val(r.city || "");
$("#field-e").val(r.field_e || "");
$("#zip").val(r.zip || "");
$("#company-name").val(r.company_name || "");
$("#contacted-date").val(r.contacted_date || "");
$("#contact-persons").val(r.contact_persons || "");
$("#follow-up-date").val(r.follow_up_date || "");
$("#email-address").val(r.email_address || "");
$("#last-pu-date").val(r.last_pu_date || "");
$("#contact-numbers").val(r.contact_numbers || "");
$("#hours-operation").val(r.hours_operation || "");
$("#comments").val(r.comments || "");
}
function fetchRecords() {
frappe.call({
method: "westech_r2.page.customer-records.customer-records.get_records",
callback: function(r) {
if (r.message) {
records = r.message;
if (records.length > 0) loadRecord(0);
else $("#current-index").text(0);
$("#total-count").text(records.length);
}
}
});
}
function saveRecord() {
if (currentIdx < 0) return;
var data = {
name: $("#record-number").val(),
additional_numbers: $("#additional-numbers").val(),
field_star: $("#field-star").val(),
customer_address: $("#customer-address").val(),
any_letter: $("#any-letter").val(),
city: $("#city").val(),
field_e: $("#field-e").val(),
zip: $("#zip").val(),
company_name: $("#company-name").val(),
contacted_date: $("#contacted-date").val(),
contact_persons: $("#contact-persons").val(),
follow_up_date: $("#follow-up-date").val(),
email_address: $("#email-address").val(),
last_pu_date: $("#last-pu-date").val(),
contact_numbers: $("#contact-numbers").val(),
hours_operation: $("#hours-operation").val(),
comments: $("#comments").val()
};
frappe.call({
method: "westech_r2.page.customer-records.customer-records.save_record",
args: { data: JSON.stringify(data) },
callback: function(r) {
if (r.message && r.message.status === "ok") {
frappe.show_alert("Saved successfully");
records[currentIdx] = data;
}
}
});
}
function deleteRecord() {
if (currentIdx < 0) return;
frappe.confirm("Delete record #" + $("#record-number").val() + "?", function() {
frappe.call({
method: "westech_r2.page.customer-records.customer-records.delete_record",
args: { name: $("#record-number").val() },
callback: function(r) {
if (r.message && r.message.status === "ok") {
records.splice(currentIdx, 1);
if (records.length > 0) {
currentIdx = Math.min(currentIdx, records.length - 1);
loadRecord(currentIdx);
} else {
$("#current-index").text(0);
$("#total-count").text(0);
$("input, textarea").val("");
}
frappe.show_alert("Deleted");
}
}
});
});
}
function searchRecords() {
var field = $("#search-field").val();
var value = $("#search-value").val().toLowerCase();
if (!value) {
searchResults = [];
fetchRecords();
return;
}
frappe.call({
method: "westech_r2.page.customer-records.customer-records.search_records",
args: { field: field, value: value },
callback: function(r) {
if (r.message) {
searchResults = r.message;
records = searchResults;
if (records.length > 0) loadRecord(0);
else {
$("#current-index").text(0);
$("#total-count").text(records.length);
$("input, textarea").val("");
}
}
}
});
}
$("#btn-save").click(saveRecord);
$("#btn-delete").click(deleteRecord);
$("#btn-prev").click(function() { if (currentIdx > 0) loadRecord(currentIdx - 1); });
$("#btn-next").click(function() { if (currentIdx < records.length - 1) loadRecord(currentIdx + 1); });
$("#btn-first").click(function() { if (records.length > 0) loadRecord(0); });
$("#btn-last").click(function() { if (records.length > 0) loadRecord(records.length - 1); });
$("#btn-search").click(searchRecords);
$("#btn-reset").click(function() { $("#search-value").val(""); searchResults = []; fetchRecords(); });
$("#btn-print").click(function() { window.print(); });
fetchRecords();
};
@@ -0,0 +1 @@
{"content": null,"creation": "2026-05-20 22:00:00.000000","docstatus": 0,"doctype": "Page","idx": 0,"modified": "2026-05-20 22:00:00.000000","modified_by": "Administrator","module": "Westech R2","name": "customer-records","owner": "Administrator","page_name": "customer-records","roles": [{"doctype": "Has Role","idx": 1,"name": "a80mopj93i","parent": "customer-records","parentfield": "roles","parenttype": "Page","role": "All"}],"script": null,"standard": "Yes","style": null,"system_page": 0,"title": "Customer Records"}
@@ -0,0 +1,85 @@
import frappe
@frappe.whitelist()
def get_records():
# For now return Lead records mapped to form fields
leads = frappe.db.sql("""
SELECT name, company_name, email_id, mobile_no, phone, address_line1, city, state, pincode,
title, status, industry, source
FROM tabLead
ORDER BY creation DESC
LIMIT 1000
""", as_dict=True)
result = []
for l in leads:
result.append({
"name": l.name,
"additional_numbers": "",
"field_star": l.status or "",
"customer_address": l.address_line1 or "",
"any_letter": l.title or "",
"city": l.city or "",
"field_e": l.email_id or "",
"zip": l.pincode or "",
"company_name": l.company_name or "",
"contacted_date": "",
"contact_persons": l.title or "",
"follow_up_date": "",
"email_address": l.email_id or "",
"last_pu_date": "",
"contact_numbers": (l.phone or "") + "\n" + (l.mobile_no or ""),
"hours_operation": l.industry or "",
"comments": l.source or ""
})
return result
@frappe.whitelist()
def save_record(data):
data = frappe.parse_json(data)
# For now just return OK - Lead update can be wired later
return {"status": "ok", "message": "Saved " + (data.get("name") or "")}
@frappe.whitelist()
def delete_record(name):
# For now return OK
return {"status": "ok", "message": "Deleted " + name}
@frappe.whitelist()
def search_records(field, value):
leads = frappe.db.sql("""
SELECT name, company_name, email_id, mobile_no, phone, address_line1, city, state, pincode,
title, status, industry, source
FROM tabLead
WHERE LOWER(company_name) LIKE %s
OR LOWER(title) LIKE %s
OR LOWER(address_line1) LIKE %s
OR LOWER(city) LIKE %s
OR LOWER(pincode) LIKE %s
OR LOWER(email_id) LIKE %s
OR LOWER(phone) LIKE %s
OR LOWER(mobile_no) LIKE %s
ORDER BY creation DESC
LIMIT 100
""", tuple(["%" + value + "%"] * 8), as_dict=True)
result = []
for l in leads:
result.append({
"name": l.name,
"additional_numbers": "",
"field_star": l.status or "",
"customer_address": l.address_line1 or "",
"any_letter": l.title or "",
"city": l.city or "",
"field_e": l.email_id or "",
"zip": l.pincode or "",
"company_name": l.company_name or "",
"contacted_date": "",
"contact_persons": l.title or "",
"follow_up_date": "",
"email_address": l.email_id or "",
"last_pu_date": "",
"contact_numbers": (l.phone or "") + "\n" + (l.mobile_no or ""),
"hours_operation": l.industry or "",
"comments": l.source or ""
})
return result
@@ -5,5 +5,10 @@
"title": "Route Planner", "title": "Route Planner",
"module": "Westech R2", "module": "Westech R2",
"standard": "Yes", "standard": "Yes",
"roles": [{"role": "All"}] "roles": [
} {
"role": "All"
}
],
"script": "frappe.pages[\"route-planner\"].on_page_load = function(wrapper) {\n var page = frappe.ui.make_app_page({\n parent: wrapper,\n title: \"Route Planner\",\n single_column: true\n });\n \n frappe.call({\n method: \"westech_r2.api.optimize_routes.get_scheduled_pickups\",\n callback: function(r) {\n if (r.message) {\n renderRoutePlanner(page, r.message);\n }\n }\n });\n \n function renderRoutePlanner(page, data) {\n var h = \"<div style='padding:20px;'>\";\n h += \"<h2>Route Planner</h2>\";\n h += \"<p>Schedule and optimize pickup routes.</p>\";\n h += \"<div id='route-map' style='width:100%;height:400px;background:#f0f0f0;margin:20px 0;'></div>\";\n h += \"<button class='btn btn-primary' id='optimize-btn'>Optimize Routes</button>\";\n h += \"<div id='routes-container' style='margin-top:20px;'></div>\";\n h += \"</div>\";\n $(page.body).html(h);\n \n $(\"#optimize-btn\").on(\"click\", function() {\n frappe.call({\n method: \"westech_r2.api.optimize_routes.optimize_routes\",\n callback: function(r) {\n if (r.message) {\n frappe.show_alert({message: \"Routes optimized\", indicator: \"green\"});\n }\n }\n });\n });\n }\n};\n"
}
@@ -0,0 +1,7 @@
frappe.pages['customer-records'].on_page_load = function(wrapper) {
var page = frappe.ui.make_app_page({
parent: wrapper,
title: 'Customer Records',
single_column: true
});
}
@@ -0,0 +1,23 @@
{
"content": null,
"creation": "2026-05-20 15:03:29.017530",
"docstatus": 0,
"doctype": "Page",
"idx": 0,
"modified": "2026-05-20 15:03:29.017530",
"modified_by": "Administrator",
"module": "Westech R2",
"name": "customer-records",
"owner": "Administrator",
"page_name": "customer-records",
"roles": [
{
"role": "All"
}
],
"script": null,
"standard": "Yes",
"style": null,
"system_page": 0,
"title": "Customer Records"
}