feat: Add Customer Records page with Lead integration
This commit is contained in:
@@ -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"><</button>
|
||||
<button class="nav-btn" id="btn-next">></button>
|
||||
<button class="nav-btn" id="btn-first"><<</button>
|
||||
<button class="nav-btn" id="btn-last">>></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",
|
||||
"module": "Westech R2",
|
||||
"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"
|
||||
}
|
||||
Binary file not shown.
@@ -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"
|
||||
}
|
||||
Reference in New Issue
Block a user