wip: receiving dashboard, customer records, archive old 2-level paths, various fixes

This commit is contained in:
vagrant
2026-05-28 03:30:45 +00:00
parent d4ed4b1d89
commit 6fe6d61779
141 changed files with 4247 additions and 1175 deletions
@@ -0,0 +1 @@
{"creation":"2026-05-20 15:00:00.000000","docstatus":0,"doctype":"Page","idx":0,"module":"westech_r2","name":"customer-intake","page_name":"customer-intake","roles":[],"script":null,"standard":"Yes","style":null,"system_page":0,"title":"Customer Intake"}
@@ -0,0 +1,96 @@
<div class="container" style="padding:20px;">
<div class="row">
<div class="col-md-12">
<h3>Customer Intake</h3>
</div>
</div>
<div class="row">
<div class="col-md-6">
<label>Search Customer</label>
<input type="text" id="intake-search" class="form-control" placeholder="Type company name, number, or phone...">
<div id="search-results" class="list-group" style="margin-top:8px; max-height:250px; overflow-y:auto;"></div>
</div>
<div class="col-md-6">
<div id="no-match" style="display:none;">
<button id="btn-add-new" class="btn btn-primary">Add New Customer</button>
</div>
</div>
</div>
<hr>
<div class="row">
<div class="col-md-4">
<label>Customer Name</label>
<input type="text" id="cust-name" class="form-control">
</div>
<div class="col-md-4">
<label>Customer Number</label>
<input type="text" id="cust-number" class="form-control">
</div>
<div class="col-md-4">
<label>Phone</label>
<input type="text" id="cust-phone" class="form-control">
</div>
</div>
<div class="row" style="margin-top:10px;">
<div class="col-md-6">
<label>Address Line 1</label>
<input type="text" id="cust-address" class="form-control">
</div>
<div class="col-md-2">
<label>City</label>
<input type="text" id="cust-city" class="form-control">
</div>
<div class="col-md-2">
<label>State</label>
<input type="text" id="cust-state" class="form-control">
</div>
<div class="col-md-2">
<label>Zip</label>
<input type="text" id="cust-zip" class="form-control">
</div>
</div>
<div class="row" style="margin-top:10px;">
<div class="col-md-4">
<label>Contact Persons</label>
<input type="text" id="cust-contacts" class="form-control">
</div>
<div class="col-md-4">
<label>Email</label>
<input type="text" id="cust-email" class="form-control">
</div>
<div class="col-md-4">
<label>Hours of Operation</label>
<input type="text" id="cust-hours" class="form-control">
</div>
</div>
<div class="row" style="margin-top:10px;">
<div class="col-md-4">
<label>Data Status</label>
<select id="pallet-data-status" class="form-control">
<option value="D0">D0 - Unknown</option>
<option value="D1">D1 - Contains Data</option>
</select>
</div>
<div class="col-md-4">
<label>Status</label>
<select id="pallet-status" class="form-control">
<option value="Received">Received</option>
<option value="Sorting">Sorting</option>
<option value="Processing">Processing</option>
<option value="Complete">Complete</option>
<option value="Shipped">Shipped</option>
</select>
</div>
<div class="col-md-4">
<label>Inbound Weight (lbs)</label>
<input type="text" id="pallet-weight" class="form-control">
</div>
</div>
<div class="row" style="margin-top:15px;">
<div class="col-md-12">
<button id="btn-save-cust" class="btn btn-success">Save Customer</button>
<button id="btn-create-pallet" class="btn btn-warning" style="margin-left:10px;">Create Pallet</button>
<span id="cust-status" style="margin-left:15px;"></span>
</div>
</div>
</div>
@@ -0,0 +1,125 @@
frappe.pages["customer-intake"].on_page_load = function(wrapper) {
var page = frappe.ui.make_app_page({parent: wrapper, title: __("Customer Intake"), single_column: true});
var content = frappe.render_template("customer-intake", {});
$(page.body).append(content);
var currentCustomer = null;
var searchTimer = null;
function clearForm() {
currentCustomer = null;
$("#cust-name, #cust-number, #cust-phone, #cust-address, #cust-city, #cust-state, #cust-zip, #cust-contacts, #cust-email, #cust-hours").val("");
$("#search-results").empty();
$("#no-match").hide();
$("#cust-status").text("");
}
function fillForm(c) {
currentCustomer = c;
$("#cust-name").val(c.customer_name || "");
$("#cust-number").val(c.customer_number || "");
$("#cust-phone").val(c.phone || c.address_phone || "");
$("#cust-address").val(c.address_line1 || "");
$("#cust-city").val(c.city || "");
$("#cust-state").val(c.state || "");
$("#cust-zip").val(c.pincode || "");
$("#cust-contacts").val(c.contact_persons || "");
$("#cust-email").val(c.email_id || "");
$("#cust-hours").val(c.hours_of_operation || "");
$("#cust-status").text("Selected: " + (c.customer_name || c.name));
$("#no-match").hide();
}
function doSearch() {
var q = $("#intake-search").val().trim();
if (q.length < 2) { $("#search-results").empty(); return; }
frappe.call({
method: "westech_r2.page.customer-intake.customer-intake.search_customers",
args: {q: q},
callback: function(r) {
var list = $("#search-results").empty();
if (r.message && r.message.length) {
r.message.forEach(function(c) {
var item = $('<div class="list-group-item" style="cursor:pointer;">')
.html('<b>' + frappe.utils.escape_html(c.customer_name || c.name) + '</b> ' +
(c.address_line1 ? '<br>' + frappe.utils.escape_html(c.address_line1) : '') +
(c.city ? ', ' + c.city : '') +
(c.phone ? ' <br><small>' + c.phone + '</small>' : ''));
item.on("click", function() { fillForm(c); });
list.append(item);
});
$("#no-match").hide();
} else {
$("#no-match").show();
}
}
});
}
$("#intake-search").on("input", function() {
clearTimeout(searchTimer);
searchTimer = setTimeout(doSearch, 300);
});
$("#btn-add-new").click(function() {
clearForm();
$("#cust-name").val($("#intake-search").val());
$("#cust-status").text("Adding new customer...");
});
$("#btn-save-cust").click(function() {
var data = {
customer_name: $("#cust-name").val(),
customer_number: $("#cust-number").val(),
phone: $("#cust-phone").val(),
address_line1: $("#cust-address").val(),
city: $("#cust-city").val(),
state: $("#cust-state").val(),
pincode: $("#cust-zip").val(),
contact_persons: $("#cust-contacts").val(),
email_id: $("#cust-email").val(),
hours_of_operation: $("#cust-hours").val()
};
if (currentCustomer && currentCustomer.name) {
data.name = currentCustomer.name;
}
frappe.call({
method: "westech_r2.page.customer-intake.customer-intake.create_customer_from_intake",
args: {data: JSON.stringify(data)},
callback: function(r) {
if (r.message && r.message.status === "ok") {
currentCustomer = {name: r.message.customer, customer_name: data.customer_name};
$("#cust-status").text("Saved: " + r.message.customer);
frappe.show_alert("Customer saved", 3);
} else {
frappe.show_alert("Error saving customer", 5);
}
}
});
});
$("#btn-create-pallet").click(function() {
if (!currentCustomer || !currentCustomer.name) {
frappe.msgprint("Please select or save a customer first.");
return;
}
frappe.call({
method: "westech_r2.page.customer-intake.customer-intake.create_pallet",
args: {
data: JSON.stringify({
customer: currentCustomer.name,
customer_number: $("#cust-number").val(),
data_status: $("#pallet-data-status").val(),
status: $("#pallet-status").val(),
inbound_weight: $("#pallet-weight").val()
})
},
callback: function(r) {
if (r.message && r.message.status === "ok") {
frappe.msgprint("Pallet created: " + r.message.pallet);
$("#cust-status").text("Pallet: " + r.message.pallet);
}
}
});
});
};
@@ -0,0 +1,84 @@
import frappe
from frappe import _
@frappe.whitelist()
def search_customers(q=""):
if not q or len(q) < 2:
return []
q = q.strip().lower()
customers = frappe.db.sql("""
SELECT c.name, c.customer_name, c.customer_number, c.mobile_no,
a.address_line1, a.city, a.state, a.pincode
FROM tabCustomer c
LEFT JOIN `tabDynamic Link` dl ON dl.link_doctype = 'Customer' AND dl.link_name = c.name AND dl.parenttype = 'Address'
LEFT JOIN tabAddress a ON a.name = dl.parent
WHERE LOWER(c.customer_name) LIKE %s OR LOWER(c.customer_number) LIKE %s OR LOWER(c.mobile_no) LIKE %s
ORDER BY c.customer_name
LIMIT 20
""", ("%" + q + "%", "%" + q + "%", "%" + q + "%"), as_dict=True)
return customers
@frappe.whitelist()
def get_customer(name):
if not name:
return {}
cust = frappe.get_doc("Customer", name)
result = cust.as_dict()
addr = frappe.db.sql("""
SELECT a.address_line1, a.city, a.state, a.pincode, a.phone
FROM tabAddress a
JOIN `tabDynamic Link` dl ON dl.parent = a.name AND dl.link_doctype = 'Customer' AND dl.link_name = %s
LIMIT 1
""", (name,), as_dict=True)
if addr:
result.update({
"address_line1": addr[0].address_line1,
"city": addr[0].city,
"state": addr[0].state,
"pincode": addr[0].pincode,
"address_phone": addr[0].phone
})
return result
@frappe.whitelist()
def create_customer_from_intake(data):
data = frappe.parse_json(data)
if not data.get("customer_name"):
frappe.throw(_("Customer name required"))
customer = frappe.new_doc("Customer")
customer.customer_name = data.get("customer_name")
customer.customer_group = data.get("customer_group", "IT Recycling")
customer.customer_type = "Company"
customer.customer_number = data.get("customer_number")
customer.mobile_no = data.get("phone")
customer.email_id = data.get("email_id")
customer.legacy_notes = data.get("legacy_notes")
customer.hours_of_operation = data.get("hours_of_operation")
customer.contact_persons = data.get("contact_persons")
customer.save()
if data.get("address_line1") or data.get("city"):
addr = frappe.new_doc("Address")
addr.address_title = customer.customer_name
addr.address_type = "Billing"
addr.address_line1 = data.get("address_line1", "Unknown")
addr.city = data.get("city", "Unknown")
addr.state = data.get("state", "")
addr.pincode = data.get("pincode", "")
addr.country = "United States"
addr.append("links", {"link_doctype": "Customer", "link_name": customer.name})
addr.save()
return {"status": "ok", "customer": customer.name}
@frappe.whitelist()
def create_pallet(data):
data = frappe.parse_json(data)
if not data.get("customer"):
frappe.throw(_("Customer required"))
pallet = frappe.new_doc("Pallet")
pallet.customer = data.get("customer")
pallet.customer_number = data.get("customer_number")
pallet.data_status = data.get("data_status", "D0")
pallet.status = data.get("status", "Received")
pallet.inbound_weight = data.get("inbound_weight", "")
pallet.save()
return {"status": "ok", "pallet": pallet.name}
@@ -0,0 +1 @@
# Customer Records page
@@ -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,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,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"
}
@@ -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
@@ -0,0 +1,5 @@
.badge-fresh { background-color: #28a745; }
.badge-aging { background-color: #ffc107; color: #212529; }
.badge-expired { background-color: #dc3545; }
.badge-needs { background-color: #fd7e14; }
.badge-error { background-color: #6c757d; }
@@ -0,0 +1,8 @@
<style>
.badge.badge-fresh { background-color: #28a745; }
.badge.badge-aging { background-color: #ffc107; color: #212529; }
.badge.badge-expired { background-color: #dc3545; }
.badge.badge-needs { background-color: #fd7e14; }
.badge.badge-error { background-color: #6c757d; }
</style>
<div id="ebay-pricing-page"></div>
@@ -0,0 +1,323 @@
frappe.pages['ebay-pricing'].on_page_load = function(wrapper) {
var page = frappe.ui.make_app_page({
parent: wrapper,
title: __('eBay Pricing'),
single_column: true
});
let $container = $(`
<div style="padding: 1rem;">
<div class="row">
<div class="col-md-8">
<div class="form-group">
<label>Search Model</label>
<div class="input-group">
<input type="text" class="form-control" id="ebay-search-input"
placeholder="Dell Latitude 5410..." autocomplete="off">
<span class="input-group-btn">
<button class="btn btn-primary" id="ebay-search-btn">
<i class="fa fa-search"></i> Search
</button>
</span>
</div>
</div>
</div>
<div class="col-md-4 text-right">
<div class="form-group">
<label>Batch Size</label>
<select class="form-control" id="ebay-batch-size" style="display:inline-block; width:auto;">
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
<option value="all">All</option>
</select>
<button class="btn btn-warning" id="ebay-batch-btn">
<i class="fa fa-play"></i> Price Batch
</button>
</div>
</div>
</div>
<hr>
<div class="row">
<div class="col-md-6">
<h4>Apply Pricing to Inventory</h4>
<div class="form-group">
<select class="form-control" id="ebay-apply-item" style="width: 100%;">
<option value="">Select Item to apply pricing...</option>
</select>
</div>
<button class="btn btn-success" id="ebay-apply-btn">
<i class="fa fa-check"></i> Apply Pricing
</button>
<button class="btn btn-info" id="ebay-apply-all-btn" style="margin-left: 0.5rem;">
<i class="fa fa-check-double"></i> Apply All
</button>
</div>
<div class="col-md-6 text-right">
<div class="well" style="display: inline-block; text-align: left;">
<h5>Pricing Status</h5>
<div id="pricing-stats">
<p class="text-muted">Click Apply All to see stats</p>
</div>
</div>
</div>
</div>
<hr>
<div id="ebay-results-area">
<div class="text-muted text-center" style="padding: 4rem;">
<i class="fa fa-search" style="font-size: 3rem; opacity: 0.3;"></i>
<p>Search for a model or run batch pricing</p>
</div>
</div>
</div>`).appendTo(page.main);
// Load item dropdown
load_item_dropdown();
// Event handlers
$container.find('#ebay-search-btn').on('click', function() {
let query = $container.find('#ebay-search-input').val().trim();
if (!query) {
frappe.msgprint(__('Enter a model to search'));
return;
}
search_ebay(query);
});
$container.find('#ebay-search-input').on('keypress', function(e) {
if (e.which === 13) {
$container.find('#ebay-search-btn').trigger('click');
}
});
$container.find('#ebay-batch-btn').on('click', function() {
let size = $container.find('#ebay-batch-size').val();
run_batch(size);
});
$container.find('#ebay-apply-btn').on('click', function() {
let item = $container.find('#ebay-apply-item').val();
if (!item) {
frappe.msgprint(__('Select an Item first'));
return;
}
apply_pricing(item);
});
$container.find('#ebay-apply-all-btn').on('click', function() {
apply_pricing_all();
});
function load_item_dropdown() {
frappe.call({
method: 'frappe.client.get_list',
args: {
doctype: 'Item',
filters: {
'disabled': 0,
'item_group': ['in', ['Laptop', 'Desktop', 'Tablet', 'Phone', 'Workstation']]
},
fields: ['name', 'item_name'],
limit: 1000
},
callback: function(r) {
if (r.message) {
let $select = $container.find('#ebay-apply-item');
r.message.forEach(item => {
$select.append(`<option value="${item.name}">${item.item_name || item.name}</option>`);
});
}
}
});
}
function search_ebay(query) {
frappe.call({
method: 'westech_r2.api.ebay_pricing.search_model',
args: { query: query },
freeze: true,
freeze_message: __('Searching eBay sold listings...'),
callback: function(r) {
if (r.message && r.message.results) {
render_results(r.message);
} else {
let msg = (r.message && r.message.message) || __('No results found');
frappe.msgprint(msg);
}
}
});
}
function run_batch(size) {
frappe.call({
method: 'westech_r2.api.ebay_pricing.run_batch',
args: { batch_size: size },
freeze: true,
freeze_message: __('Running batch pricing...'),
callback: function(r) {
if (r.message) {
frappe.msgprint(__('Batch complete: {0} priced, {1} failed, {2} skipped',
[r.message.priced, r.message.failed, r.message.skipped]));
load_recent_pricing();
}
}
});
}
function apply_pricing(item_code) {
frappe.call({
method: 'westech_r2.api.ebay_pricing.batch_apply_pricing',
args: { item_code: item_code },
freeze: true,
freeze_message: __('Applying pricing to Serial Nos...'),
callback: function(r) {
if (r.message) {
render_pricing_stats(r.message);
frappe.msgprint(__('Pricing applied: {0} priced, {1} commodity, {2} needs grading',
[r.message.priced, r.message.commodity, r.message.needs_grading]));
}
}
});
}
function apply_pricing_all() {
frappe.call({
method: 'westech_r2.api.ebay_pricing.batch_apply_pricing',
args: { batch_size: 1000 },
freeze: true,
freeze_message: __('Applying pricing to all Serial Nos...'),
callback: function(r) {
if (r.message) {
render_pricing_stats(r.message);
frappe.msgprint(__('Batch pricing applied: {0} priced, {1} commodity, {2} needs grading, {3} errors',
[r.message.priced, r.message.commodity, r.message.needs_grading, r.message.errors]));
}
}
});
}
function render_pricing_stats(stats) {
let html = `
<table class="table table-condensed" style="margin-bottom: 0;">
<tr><td>Priced</td><td><span class="badge badge-success">${stats.priced || 0}</span></td></tr>
<tr><td>Commodity</td><td><span class="badge badge-warning">${stats.commodity || 0}</span></td></tr>
<tr><td>Needs Grading</td><td><span class="badge badge-info">${stats.needs_grading || 0}</span></td></tr>
<tr><td>Needs Price Point</td><td><span class="badge badge-primary">${stats.needs_price_point || 0}</span></td></tr>
<tr><td>Errors</td><td><span class="badge badge-danger">${stats.errors || 0}</span></td></tr>
</table>
`;
$container.find('#pricing-stats').html(html);
}
function render_results(data) {
let $area = $container.find('#ebay-results-area').empty();
if (!data.results || !data.results.length) {
$area.html(`<div class="text-muted text-center" style="padding: 2rem;">No results</div>`);
return;
}
let html = `<table class="table table-bordered">
<thead><tr>
<th>Title</th>
<th>Price</th>
<th>Condition</th>
<th>Sold</th>
<th>Shipping</th>
</tr></thead>
<tbody>`;
data.results.forEach(item => {
html += `<tr>
<td>${frappe.utils.escape_html(item.title || '')}</td>
<td>$${(item.price || 0).toFixed(2)}</td>
<td>${frappe.utils.escape_html(item.condition || '')}</td>
<td>${item.sold || ''}</td>
<td>${item.shipping || ''}</td>
</tr>`;
});
html += `</tbody></table>`;
if (data.pricing) {
html += `<div class="well">
<h4>Pricing Summary</h4>
<div class="row">
<div class="col-md-3"><strong>Low:</strong> $${data.pricing.price_low}</div>
<div class="col-md-3"><strong>High:</strong> $${data.pricing.price_high}</div>
<div class="col-md-3"><strong>Average:</strong> $${data.pricing.price_average}</div>
<div class="col-md-3"><strong>Median:</strong> $${data.pricing.price_auction}</div>
</div>
<div class="row" style="margin-top: 1rem;">
<div class="col-md-6"><strong>Source:</strong> ${data.pricing.source}</div>
<div class="col-md-6"><strong>Samples:</strong> ${data.pricing.sample_count}</div>
</div>
</div>`;
}
$area.html(html);
}
function load_recent_pricing() {
frappe.call({
method: 'westech_r2.api.ebay_pricing.get_recent_pricing',
args: { limit: 50 },
callback: function(r) {
if (r.message) {
render_pricing_grid(r.message);
}
}
});
}
function render_pricing_grid(items) {
let $area = $container.find('#ebay-results-area');
if (!items || !items.length) {
$area.html(`<div class="text-muted text-center" style="padding: 2rem;">No pricing data yet</div>`);
return;
}
let html = `<h4>Recent Pricing Results</h4>
<table class="table table-bordered table-hover">
<thead><tr>
<th>Manufacturer</th>
<th>Model</th>
<th>Status</th>
<th>Age</th>
<th>Low</th>
<th>High</th>
<th>Avg</th>
<th>Samples</th>
<th>Source</th>
<th>Last Priced</th>
</tr></thead>
<tbody>`;
items.forEach(row => {
let status_class = 'badge-needs';
if (row.pricing_status === 'Priced') status_class = 'badge-fresh';
else if (row.pricing_status === 'Manual Override') status_class = 'badge-fresh';
else if (row.pricing_status === 'Expired') status_class = 'badge-expired';
else if (row.pricing_status === 'Error') status_class = 'badge-error';
let age = row.days_since_pricing || 0;
let age_badge = age < 90 ? 'badge-fresh' : (age < 120 ? 'badge-aging' : 'badge-expired');
html += `<tr>
<td>${frappe.utils.escape_html(row.manufacturer || '')}</td>
<td>${frappe.utils.escape_html(row.model || '')}</td>
<td><span class="badge ${status_class}">${row.pricing_status}</span></td>
<td><span class="badge ${age_badge}">${age} days</span></td>
<td>$${row.price_low || ''}</td>
<td>$${row.price_high || ''}</td>
<td>$${row.price_average || ''}</td>
<td>${row.sample_count || ''}</td>
<td>${row.source || ''}</td>
<td>${frappe.datetime.str_to_user(row.scraped_at) || ''}</td>
</tr>`;
});
html += `</tbody></table>`;
$area.html(html);
}
load_recent_pricing();
};
@@ -0,0 +1,26 @@
{
"creation": "2026-05-17 05:30:00.000000",
"docstatus": 0,
"doctype": "Page",
"icon": "fa fa-tags",
"modified": "2026-05-17 05:30:00.000000",
"modified_by": "Administrator",
"module": "Westech R2",
"name": "ebay-pricing",
"owner": "Administrator",
"page_name": "ebay-pricing",
"roles": [
{
"role": "System Manager"
},
{
"role": "Stock User"
},
{
"role": "Sales User"
}
],
"standard": "Yes",
"system_page": 0,
"title": "eBay Pricing"
}
@@ -0,0 +1 @@
# eBay Pricing desk page
@@ -0,0 +1,5 @@
.badge-fresh { background-color: #28a745; }
.badge-aging { background-color: #ffc107; color: #212529; }
.badge-expired { background-color: #dc3545; }
.badge-needs { background-color: #fd7e14; }
.badge-error { background-color: #6c757d; }
@@ -0,0 +1,8 @@
<style>
.badge.badge-fresh { background-color: #28a745; }
.badge.badge-aging { background-color: #ffc107; color: #212529; }
.badge.badge-expired { background-color: #dc3545; }
.badge.badge-needs { background-color: #fd7e14; }
.badge.badge-error { background-color: #6c757d; }
</style>
<div id="ebay-pricing-page"></div>
@@ -0,0 +1 @@
ebay-pricing.js
@@ -0,0 +1 @@
ebay-pricing.json
@@ -0,0 +1 @@
# eBay Pricing desk page
@@ -0,0 +1 @@
@@ -0,0 +1,4 @@
frappe.pages["eim-portal"].on_page_load = function(wrapper) {
wrapper.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:60vh;font-family:sans-serif;"><div style="text-align:center;"><i class="fa fa-spinner fa-spin" style="font-size:24px;color:#2d7d46;"></i><p style="margin-top:12px;color:#555;">Redirecting to EIM Device Portal...</p></div></div>';
setTimeout(function() { window.location.href = "https://eim.diagalon.com"; }, 500);
};
@@ -0,0 +1,13 @@
{
"creation": "2026-05-09 14:00:00",
"docstatus": 0,
"doctype": "Page",
"idx": 0,
"modified": "2026-05-09 14:00:00",
"modified_by": "Administrator",
"module": "Westech R2",
"name": "eim-portal",
"owner": "Administrator",
"standard": "Yes",
"title": "EIM Device Portal"
}
@@ -0,0 +1,5 @@
import frappe
def get_context(context):
frappe.local.flags.redirect_location = "https://eim.diagalon.com"
raise frappe.Redirect
@@ -0,0 +1,7 @@
frappe.pages['eim-portal'].on_page_load = function(wrapper) {
var page = frappe.ui.make_app_page({
parent: wrapper,
title: 'EIM Device Portal',
single_column: true
});
}
@@ -0,0 +1,23 @@
{
"content": null,
"creation": "2026-05-09 14:00:00",
"docstatus": 0,
"doctype": "Page",
"idx": 0,
"modified": "2026-05-09 15:09:48.653878",
"modified_by": "Administrator",
"module": "Westech R2",
"name": "eim-portal",
"owner": "Administrator",
"page_name": "eim-portal",
"roles": [
{
"role": "All"
}
],
"script": null,
"standard": "Yes",
"style": null,
"system_page": 0,
"title": "EIM Device Portal"
}
@@ -0,0 +1,11 @@
.intake-station .card { border: 1px solid #e0e0e0; border-radius: 8px; overflow: hidden; }
.intake-station .card-header { padding: 15px; }
.intake-station .card-body { padding: 20px; }
.intake-station .form-group { margin-bottom: 15px; }
.intake-station .form-control { border-radius: 4px; padding: 8px 12px; font-size: 16px; }
.intake-station .form-control:focus { border-color: #6f42c1; box-shadow: 0 0 0 0.2rem rgba(111,66,193,0.25); }
.intake-station label { font-weight: 600; margin-bottom: 4px; }
.intake-station h5 { margin-bottom: 15px; padding-bottom: 8px; border-bottom: 2px solid #e0e0e0; }
.intake-station .table th { background: #f8f9fa; }
.intake-station .btn-primary { background: linear-gradient(135deg, #6f42c1, #28a745) !important; border: none !important; }
.intake-station .label { font-size: 0.85em; }
@@ -0,0 +1,580 @@
frappe.pages['intake'].on_page_load = function(wrapper) {
try {
var page = frappe.ui.make_app_page({
parent: wrapper,
title: 'Customer Management',
single_column: true
});
page.set_primary_action('New Customer', function() {
show_intake_form();
}, 'add');
page.add_inner_button('Refresh', function() {
load_customer_list();
});
$(wrapper).find('.layout-main-section').html(`
<div class="intake-station" style="padding: 20px;">
<div id="intake-form-container">
<div class="card" style="margin-bottom: 20px;">
<div class="card-header" style="background: linear-gradient(135deg, #6f42c1, #28a745); color: white; padding: 15px;">
<h4 style="margin:0; color: white;">Customer Management</h4>
</div>
<div class="card-body" style="padding: 20px;">
<form id="intake-form">
<div class="row">
<div class="col-md-4">
<h5 style="color:#6f42c1;">Customer</h5>
<div class="form-group">
<label>Customer <span class="text-danger">*</span></label>
<div id="customer-number-control"></div>
</div>
<div class="form-group">
<label>Company Name</label>
<input type="text" id="company_name" class="form-control" readonly style="background:#f8f9fa;">
</div>
<div class="form-group">
<label>Driver</label>
<div id="driver-control"></div>
</div>
<div class="form-group">
<label>Contact Name</label>
<input type="text" id="contact_name" class="form-control">
</div>
<div class="form-group">
<label>Contact #</label>
<input type="tel" id="contact_number" class="form-control">
</div>
<div class="form-group">
<label>Contact Email</label>
<input type="email" id="contact_email" class="form-control">
</div>
<div class="form-group">
<label>Address</label>
<input type="text" id="address_line" class="form-control">
</div>
</div>
<div class="col-md-4">
<h5 style="color:#6f42c1;">Dates & Source</h5>
<div class="form-group">
<label>Received Date <span class="text-danger">*</span></label>
<input type="date" id="received_date" class="form-control" required>
</div>
<div class="form-group">
<label>Weekday</label>
<input type="text" id="weekday" class="form-control" readonly style="background:#f8f9fa;">
</div>
<div class="form-group">
<label>Pickup / Drop-off</label>
<select id="pickup" class="form-control">
<option value="">—</option>
<option value="Pickup">Pickup</option>
<option value="Drop-off">Drop-off</option>
</select>
</div>
<div class="form-group">
<label>Hours of Operation</label>
<input type="text" id="hours_of_operation" class="form-control" readonly style="background:#f8f9fa;" placeholder="Auto-filled from Customer">
</div>
<div class="form-group">
<label>Data Status</label>
<select id="data_status" class="form-control">
<option value="">—</option>
<option value="D0">D0 (Unknown)</option>
<option value="D1">D1 (Contains Data)</option>
</select>
</div>
<div class="form-group">
<label>RED / R2</label>
<select id="red_r2" class="form-control">
<option value="">—</option>
<option value="RED">RED</option>
<option value="R2">R2</option>
<option value="Both">Both</option>
<option value="Clear">Clear</option>
</select>
</div>
<div class="form-group">
<label>Notes</label>
<textarea id="notes" class="form-control" rows="3"></textarea>
</div>
<div class="form-group">
<label>Legacy Notes</label>
<textarea id="legacy_notes" class="form-control" rows="2" readonly style="background:#fafafa;" title="Auto-filled from Customer record"></textarea>
</div>
</div>
<div class="col-md-4" id="load-info-section" style="display:none;">
<h5 style="color:#6f42c1;">Items & Weight</h5>
<div class="form-group">
<label>Barcode</label>
<input type="text" id="barcode" class="form-control" placeholder="Scan barcode...">
</div>
<div class="form-group">
<label>Total Items</label>
<input type="number" id="total_items" class="form-control" value="0" min="0">
</div>
<div class="form-group">
<label>Number of Labels</label>
<input type="number" id="num_labels" class="form-control" value="1" min="1" max="20">
</div>
<hr>
<div class="form-group">
<label>Weight <span class="text-danger">*</span></label>
<input type="text" id="weights" class="form-control" placeholder="e.g. 340 lbs" required>
</div>
<div class="form-group">
<label>Invoice / Check Request</label>
<input type="text" id="invoice_check_request" class="form-control">
</div>
<div class="form-group">
<label>Amount</label>
<input type="number" id="amount" class="form-control" step="0.01" value="0">
</div>
<div class="form-group">
<label>Paid / Received</label>
<select id="paid_received" class="form-control">
<option value="">—</option>
<option value="Paid">Paid</option>
<option value="Received">Received</option>
<option value="Pending">Pending</option>
</select>
</div>
</div>
</div>
<div class="row" style="margin-top: 20px;">
<div class="col-md-12">
<button type="submit" class="btn btn-primary btn-lg" style="background: linear-gradient(135deg, #6f42c1, #28a745); border: none;">
Save Contact Info
</button>
<button type="button" class="btn btn-default btn-lg" id="btn-print-labels" disabled>
Print Labels
</button>
<button type="button" class="btn btn-default btn-lg" id="btn-generate-cor" disabled>
CoR/AoR
</button>
<button type="button" class="btn btn-default btn-lg" id="btn-cancel">
Cancel
</button>
<span id="save-status" class="ml-3" style="font-size: 1.2em;"></span>
</div>
</div>
</form>
</div>
</div>
</div>
<div id="recent-pallets" style="display:none;">
<div class="card">
<div class="card-header" style="background: #f8f9fa; display: flex; justify-content: space-between; align-items: center;">
<h5 style="margin:0;">Customers</h5>
<input type="text" id="customer-search" class="form-control" placeholder="Search..." style="width: 300px;">
</div>
<div class="card-body" style="padding: 0;">
<table class="table table-striped table-hover" id="customer-table">
<thead>
<tr>
<th>Company</th>
<th>Contact</th>
<th>Phone</th>
<th>Address</th>
<th></th>
</tr>
</thead>
<tbody id="customer-tbody">
<tr><td colspan="5" class="text-center">Loading...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
`);
setup_link_controls();
set_today_date();
// Trigger weekday calculation after date field is set
setTimeout(function() {
var dateVal = $('#received_date').val();
if (dateVal) {
var d = new Date(dateVal + 'T12:00:00');
var days = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
$('#weekday').val(days[d.getDay()] || '');
}
}, 200);
// Show/hide load info based on pickup dropdown
.on('change', function() {
var val = .val();
if (val) {
.show();
} else {
.hide();
}
});
// Initial state - hide if blank
.trigger('change');
load_customer_list();
$('#received_date').on('change', function() {
var d = new Date($(this).val() + 'T12:00:00');
var days = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
$('#weekday').val(days[d.getDay()] || '');
});
$('#intake-form').on('submit', function(e) {
e.preventDefault();
save_customer();
});
$('#btn-cancel').on('click', function() {
show_customer_list();
});
$('#btn-print-labels').on('click', function() {
frappe.msgprint('Label printing coming soon.');
});
$('#btn-generate-cor').on('click', function() {
generate_cor_report();
});
$('#customer-search').on('input', function() {
var q = $(this).val().toLowerCase();
$('#customer-tbody tr').each(function() {
var text = $(this).text().toLowerCase();
$(this).toggle(text.indexOf(q) > -1);
});
});
} catch(e) {
console.error('[INTAKE] FATAL:', e.message, e.stack);
$(wrapper).find('.layout-main-section').html(
'<div style="padding:20px;"><h3>Error Loading Page</h3><pre>' + e.message + '</pre></div>'
);
}
};
// ── Link Controls ──────────────────────────────────────────────
var customer_number_control = null;
var driver_control = null;
var selected_customer_name = null;
function setup_link_controls() {
customer_number_control = frappe.ui.form.make_control({
parent: $('#customer-number-control'),
df: {
fieldtype: 'Link',
fieldname: 'customer_number',
options: 'Customer',
label: 'Customer',
reqd: 1,
placeholder: 'Search customer...',
onchange: function() {
var val = customer_number_control.get_value();
if (val) {
selected_customer_name = val;
fetch_customer_details(val);
} else {
selected_customer_name = null;
clear_customer_fields();
}
}
},
only_input: true,
});
customer_number_control.refresh();
$('#customer-number-control .control-input').css('margin', '0');
$('#customer-number-control .help-box').remove();
driver_control = frappe.ui.form.make_control({
parent: $('#driver-control'),
df: {
fieldtype: 'Link',
fieldname: 'driver',
options: 'Employee',
label: 'Driver',
placeholder: 'Search driver...',
onchange: function() {}
},
only_input: true,
});
driver_control.refresh();
$('#driver-control .control-input').css('margin', '0');
$('#driver-control .help-box').remove();
}
function fetch_customer_details(customer_name) {
frappe.call({
method: 'frappe.client.get',
args: {doctype: 'Customer', name: customer_name},
callback: function(r) {
if (r.message) {
var c = r.message;
$('#company_name').val(c.customer_name || '');
$('#contact_name').val(c.contact_persons || '');
$('#hours_of_operation').val(c.hours_of_operation || '');
$('#legacy_notes').val(c.legacy_notes || '');
// Parse phone and email from contact_persons if not in dedicated fields
var phone = c.mobile_no || '';
var email = c.email_id || '';
var addressLine = '';
// Extract phone from contact_persons text blob
if (!phone && c.contact_persons) {
var phoneMatch = c.contact_persons.match(/\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}/);
if (phoneMatch) phone = phoneMatch[0];
}
// Extract email from contact_persons text blob
if (!email && c.contact_persons) {
var emailMatch = c.contact_persons.match(/[\w.+-]+@[\w.-]+\.\w+/);
if (emailMatch) email = emailMatch[0];
}
$('#contact_number').val(phone);
$('#contact_email').val(email);
// Enable CoR button once we have a customer with data
$('#btn-generate-cor').prop('disabled', false);
// Get address — always fetch from Address record for full street+city+state+zip
if (c.customer_primary_address) {
frappe.call({
method: 'frappe.client.get',
args: {doctype: 'Address', name: c.customer_primary_address},
callback: function(r3) {
if (r3.message) {
var addr = r3.message;
var line = (addr.address_line1 || '') + (addr.address_line2 ? ', ' + addr.address_line2 : '');
var full = line + (addr.city ? ', ' + addr.city : '') + (addr.state ? ', ' + addr.state : '') + (addr.pincode ? ' ' + addr.pincode : '');
$('#address_line').val(full);
}
}
});
} else if (c.primary_address) {
// Fallback: parse text blob (less reliable)
var lines = c.primary_address.split('\n');
addressLine = lines.join(', ');
$('#address_line').val(addressLine);
}
// If still missing phone/email, try linked Contact
if ((!phone || !email) && c.customer_primary_contact) {
frappe.call({
method: 'frappe.client.get',
args: {doctype: 'Contact', name: c.customer_primary_contact},
callback: function(r2) {
if (r2.message) {
var ct = r2.message;
if (!phone) $('#contact_number').val(ct.phone || ct.mobile_no || '');
if (!email) $('#contact_email').val(ct.email_id || '');
}
}
});
}
}
}
});
}
function clear_customer_fields() {
$('#company_name').val('');
$('#contact_name').val('');
$('#contact_number').val('');
$('#contact_email').val('');
$('#address_line').val('');
$('#hours_of_operation').val('');
}
function clear_form() {
if (customer_number_control) customer_number_control.set_value('');
if (driver_control) driver_control.set_value('');
$('#company_name').val('');
$('#received_date').val('');
$('#weekday').val('');
$('#pickup').val('');
$('#data_status').val('');
$('#red_r2').val('');
$('#barcode').val('');
$('#total_items').val(0);
$('#num_labels').val(1);
$('#contact_name').val('');
$('#contact_number').val('');
$('#contact_email').val('');
$('#address_line').val('');
$('#hours_of_operation').val('');
$('#legacy_notes').val('');
$('#weights').val('');
$('#invoice_check_request').val('');
$('#amount').val(0);
$('#paid_received').val('');
$('#notes').val('');
$('#btn-print-labels').prop('disabled', true);
$('#btn-generate-cor').prop('disabled', true);
$('#save-status').html('');
}
function set_today_date() {
var today = new Date().toISOString().split('T')[0];
$('#received_date').val(today);
$('#received_date').trigger('change');
}
function show_intake_form() {
$('#intake-form-container').show();
$('#recent-pallets').hide();
}
function show_customer_list() {
$('#intake-form-container').hide();
$('#recent-pallets').show();
clear_form();
load_customer_list();
}
function load_customer_list() {
frappe.call({
method: 'frappe.client.get_list',
args: {
doctype: 'Customer',
fields: ['name', 'customer_name', 'customer_number', 'mobile_no', 'email_id', 'contact_persons', 'primary_address', 'customer_primary_address'],
limit_page_length: 50,
order_by: 'customer_name asc'
},
callback: function(r) {
var tbody = $('#customer-tbody');
tbody.empty();
if (!r.message || r.message.length === 0) {
tbody.append('<tr><td colspan="5" class="text-center">No customers found.</td></tr>');
return;
}
r.message.forEach(function(c) {
var contactName = '';
if (c.contact_persons) {
var parts = c.contact_persons.split('|');
if (parts.length > 0) contactName = parts[0].trim();
}
tbody.append(
'<tr style="cursor:pointer;" onclick="select_customer_from_list(\'' + c.name.replace(/'/g, "\\'") + '\')">' +
'<td><strong>' + (c.customer_name || c.name) + '</strong>' + (c.customer_number ? ' <span class="text-muted">(#' + c.customer_number + ')</span>' : '') + '</td>' +
'<td>' + contactName + '</td>' +
'<td>' + (c.mobile_no || '') + '</td>' +
'<td>' + (c.primary_address ? c.primary_address.replace(/\n/g, ', ') : '') + '</td>' +
'<td><button class="btn btn-xs btn-default"><i class="fa fa-arrow-right"></i></button></td>' +
'</tr>'
);
});
}
});
}
function select_customer_from_list(customer_name) {
if (customer_number_control) customer_number_control.set_value(customer_name);
fetch_customer_details(customer_name);
}
function edit_pallet(name) {
frappe.call({
method: 'frappe.client.get',
args: {doctype: 'Pallet', name: name},
callback: function(r) {
if (r.message) {
show_intake_form();
var d = r.message;
$('#intake-form-container').data('pallet-name', name);
$('#received_date').val(d.received_date || '');
if (customer_number_control) customer_number_control.set_value(d.customer_number || '');
if (driver_control) driver_control.set_value(d.driver || '');
$('#company_name').val(d.company_name || '');
$('#pickup').val(d.pickup || '');
$('#data_status').val(d.data_status || '');
$('#red_r2').val(d.red_r2 || '');
$('#barcode').val(d.barcode || '');
$('#total_items').val(d.total_items || 0);
$('#num_labels').val(d.num_labels || 1);
$('#contact_name').val(d.contact_name || '');
$('#contact_number').val(d.contact_number || '');
$('#contact_email').val(d.contact_email || '');
$('#address_line').val(d.address_line || '');
$('#hours_of_operation').val(d.hours_of_operation || '');
$('#legacy_notes').val(d.legacy_notes || '');
$('#weights').val(d.weights || '');
$('#invoice_check_request').val(d.invoice_check_request || '');
$('#amount').val(d.amount || 0);
$('#paid_received').val(d.paid_received || '');
$('#notes').val(d.notes || '');
$('#received_date').trigger('change');
$('#btn-print-labels').prop('disabled', false);
$('#btn-generate-cor').prop('disabled', false);
}
}
});
}
function save_customer() {
var customer_name = customer_number_control ? customer_number_control.get_value() : null;
if (!customer_name) {
frappe.msgprint("Please select a customer first.");
return;
}
frappe.call({
method: "frappe.client.get",
args: { doctype: "Customer", name: customer_name },
callback: function(r) {
if (r.message) {
var doc = r.message;
doc.contact_persons = .val();
doc.mobile_no = .val();
doc.email_id = .val();
doc.hours_of_operation = .val();
doc.legacy_notes = .val();
frappe.call({
method: "frappe.client.save",
args: { doc: doc },
callback: function(r2) {
if (r2.message) {
frappe.msgprint("Customer updated!");
.html("<span style=\"color:green;\">Saved!</span>");
}
}
});
}
}
});
}
function generate_cor_report() {
var companyName = $('#company_name').val();
if (!companyName) {
frappe.msgprint('Please select a customer first.');
return;
}
var args = {
company_name: companyName,
weights: $('#weights').val() || '',
received_date: $('#received_date').val() || '',
red_r2: $('#red_r2').val() || '',
contact_name: $('#contact_name').val() || '',
contact_number: $('#contact_number').val() || '',
address_line: $('#address_line').val() || '',
pallet_name: $('#intake-form-container').data('pallet-name') || ''
};
window.open('/api/method/westech_r2.api.cor_generator.generate_cor?'
+ '&company_name=' + encodeURIComponent(args.company_name)
+ '&weights=' + encodeURIComponent(args.weights)
+ '&received_date=' + encodeURIComponent(args.received_date)
+ '&red_r2=' + encodeURIComponent(args.red_r2)
+ '&contact_name=' + encodeURIComponent(args.contact_name)
+ '&contact_number=' + encodeURIComponent(args.contact_number)
+ '&address_line=' + encodeURIComponent(args.address_line)
+ '&pallet_name=' + encodeURIComponent(args.pallet_name)
);
}
window.edit_pallet = edit_pallet;
@@ -0,0 +1,23 @@
{
"content": null,
"creation": "2026-05-09 12:05:32.403207",
"docstatus": 0,
"doctype": "Page",
"idx": 0,
"modified": "2026-05-23 01:31:28.579759",
"modified_by": "Administrator",
"module": "Westech R2",
"name": "intake",
"owner": "Administrator",
"page_name": "intake",
"roles": [
{
"role": "All"
}
],
"script": null,
"standard": "Yes",
"style": null,
"system_page": 0,
"title": "Customer Management"
}
@@ -0,0 +1,5 @@
from frappe import _
def get_context(context):
context.no_cache = 1
context.title = _("Intake Station")
@@ -0,0 +1 @@
/* CSS */
@@ -0,0 +1,53 @@
<div class="ld-container" style="padding: 20px;">
<style>
.ld-header { background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 20px; }
.ld-table { width: 100%; border-collapse: collapse; margin-top: 20px; font-size: 13px; }
.ld-table th { background: #37474f; color: white; padding: 8px; text-align: left; }
.ld-table td { padding: 6px 8px; border-bottom: 1px solid #e0e0e0; }
.ld-table .num { text-align: right; font-family: monospace; }
.ld-btn { background: #455a64; color: white; border: none; padding: 8px 20px; border-radius: 4px; cursor: pointer; margin-bottom: 20px; margin-right: 10px; }
.ld-badge { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 11px; margin-right: 4px; }
.bg-rcv { background: #e3f2fd; color: #1565c0; }
.bg-hdr { background: #fff3e0; color: #e65100; }
.bg-tst { background: #e8f5e9; color: #2e7d32; }
.bg-r2 { background: #f3e5f5; color: #7b1fa2; }
.bg-des { background: #ffebee; color: #c62828; }
</style>
<div class="ld-header">
<h2>Load: <span id="ld-name"></span></h2>
<div>In Date: <span id="ld-date"></span> | Customer: <span id="ld-cust"></span> | Devices: <span id="ld-dev"></span> | Weight: <span id="ld-wt"></span> lbs</div>
</div>
<div style="margin-bottom: 15px;">
<span class="ld-badge bg-rcv">Receiving</span>
<span class="ld-badge bg-hdr">HDR / Disassembly</span>
<span class="ld-badge bg-tst">Test</span>
<span class="ld-badge bg-r2">R2 Downstream</span>
<span class="ld-badge bg-des">Destruction</span>
</div>
<button class="ld-btn" onclick="window.print()">Print Data Tracking Worksheet</button>
<button class="ld-btn" style="background: #1976d2;" onclick="window.location.href='/app/load-list'">Back to Loads</button>
<table class="ld-table" id="ld-table">
<thead>
<tr>
<th rowspan="2">Material Type</th>
<th colspan="3" style="text-align:center; background:#1565c0;">Receiving</th>
<th colspan="3" style="text-align:center; background:#e65100;">HDR / Disassembly</th>
<th colspan="3" style="text-align:center; background:#2e7d32;">Test</th>
<th style="text-align:center; background:#7b1fa2;">R2</th>
<th colspan="4" style="text-align:center; background:#c62828;">Destruction</th>
</tr>
<tr>
<th>Count</th><th>Status</th><th>Send To</th>
<th>Recv'd</th><th>HDD Out</th><th>Send To</th>
<th>Sanitized</th><th>Status</th><th>Send To</th>
<th>Sent</th>
<th>Phys</th><th>HDD Need</th><th>HDD Phys</th><th>HDD Logic</th>
</tr>
</thead>
<tbody id="ld-body"></tbody>
</table>
</div>
@@ -0,0 +1,65 @@
frappe.pages["load-detail"].on_page_load = function(wrapper) {
var page = frappe.ui.make_app_page({
parent: wrapper,
title: "Load Detail",
single_column: true
});
var loadName = frappe.utils.get_url_arg("load");
if (!loadName) {
$(page.body).html("<div style='padding:40px;text-align:center;'><h2>No load specified</h2><p>Use ?load=MMDDYYYY-XXXX</p></div>");
return;
}
frappe.call({
method: "frappe.client.get",
args: { doctype: "Load", name: loadName },
callback: function(r) {
if (r.message) { showLoad(page, r.message); }
else { $(page.body).html("<div style='padding:40px;text-align:center;'><h2>Load not found</h2></div>"); }
}
});
function showLoad(page, load) {
var h = "<div style='padding:20px;'>";
h += "<div style='background:#f8f9fa;padding:15px;border-radius:8px;margin-bottom:20px;'>";
h += "<h2>Load: " + load.name + "</h2>";
h += "<div>In Date: " + (load.incoming_date || "N/A") + " | Customer: " + (load.customer_name || load.customer_number || "N/A") + "</div>";
h += "<div>Devices: " + (load.total_devices || 0) + " | Weight: " + (load.total_weight || 0) + " lbs</div>";
h += "</div>";
h += "<button class='btn btn-primary' onclick='window.print()' style='margin-bottom:15px;'>Print Data Tracking Worksheet</button> ";
h += "<a href='/app/load/" + encodeURIComponent(load.name) + "' class='btn btn-default' style='margin-bottom:15px;'>Open Form View</a> ";
h += "<a href='/app/load-update?load=" + encodeURIComponent(load.name) + "' class='btn btn-default' style='margin-bottom:15px;'>Edit Load</a>";
h += "<table style='width:100%;border-collapse:collapse;font-size:13px;margin-top:20px;'>";
h += "<thead><tr style='background:#37474f;color:white;'>";
h += "<th>Material Type</th><th>Count</th><th>Rcv Status</th><th>Send To</th>";
h += "<th>Recv'd</th><th>HDD Out</th><th>Dis Send To</th>";
h += "<th>Sanitized</th><th>Test Status</th><th>Test Send</th>";
h += "<th>R2 Sent</th><th>Phys</th><th>HDD Need</th><th>HDD Phys</th><th>HDD Logic</th>";
h += "</tr></thead><tbody>";
if (load.material_items && load.material_items.length > 0) {
load.material_items.forEach(function(item) {
h += "<tr style='border-bottom:1px solid #e0e0e0;'>";
h += "<td><strong>" + (item.material_type || "") + "</strong></td>";
h += "<td style='text-align:right;'>" + (item.total_count || 0) + "</td>";
h += "<td>" + (item.initial_data_status || "") + "</td>";
h += "<td>" + (item.send_to || "") + "</td>";
h += "<td style='text-align:right;'>" + (item.devices_received || 0) + "</td>";
h += "<td style='text-align:right;'>" + (item.hdd_removed || 0) + "</td>";
h += "<td>" + (item.disassembly_send_to || "") + "</td>";
h += "<td style='text-align:right;'>" + (item.units_sanitized_software || 0) + "</td>";
h += "<td>" + (item.test_data_status || "") + "</td>";
h += "<td>" + (item.test_send_to || "") + "</td>";
h += "<td style='text-align:right;'>" + (item.r2_units_sent || 0) + "</td>";
h += "<td style='text-align:right;'>" + (item.units_physical_destruction || 0) + "</td>";
h += "<td style='text-align:right;'>" + (item.hdd_needs_sanitize || 0) + "</td>";
h += "<td style='text-align:right;'>" + (item.hdd_physical_destruction || 0) + "</td>";
h += "<td style='text-align:right;'>" + (item.hdd_logical_sanitization || 0) + "</td>";
h += "</tr>";
});
}
h += "</tbody></table></div>";
$(page.body).html(h);
}
};
@@ -0,0 +1,18 @@
{
"doctype": "Page",
"name": "load-detail",
"page_name": "load-detail",
"title": "Load Detail",
"page_type": "Web Page",
"module": "Westech R2",
"standard": "Yes",
"system_page": 0,
"roles": [
{
"role": "All"
}
],
"content": "",
"script": null,
"style": null
}
@@ -0,0 +1,3 @@
import frappe
no_cache = 1
@@ -0,0 +1 @@
/* CSS */
@@ -0,0 +1,53 @@
<div class="ld-container" style="padding: 20px;">
<style>
.ld-header { background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 20px; }
.ld-table { width: 100%; border-collapse: collapse; margin-top: 20px; font-size: 13px; }
.ld-table th { background: #37474f; color: white; padding: 8px; text-align: left; }
.ld-table td { padding: 6px 8px; border-bottom: 1px solid #e0e0e0; }
.ld-table .num { text-align: right; font-family: monospace; }
.ld-btn { background: #455a64; color: white; border: none; padding: 8px 20px; border-radius: 4px; cursor: pointer; margin-bottom: 20px; margin-right: 10px; }
.ld-badge { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 11px; margin-right: 4px; }
.bg-rcv { background: #e3f2fd; color: #1565c0; }
.bg-hdr { background: #fff3e0; color: #e65100; }
.bg-tst { background: #e8f5e9; color: #2e7d32; }
.bg-r2 { background: #f3e5f5; color: #7b1fa2; }
.bg-des { background: #ffebee; color: #c62828; }
</style>
<div class="ld-header">
<h2>Load: <span id="ld-name"></span></h2>
<div>In Date: <span id="ld-date"></span> | Customer: <span id="ld-cust"></span> | Devices: <span id="ld-dev"></span> | Weight: <span id="ld-wt"></span> lbs</div>
</div>
<div style="margin-bottom: 15px;">
<span class="ld-badge bg-rcv">Receiving</span>
<span class="ld-badge bg-hdr">HDR / Disassembly</span>
<span class="ld-badge bg-tst">Test</span>
<span class="ld-badge bg-r2">R2 Downstream</span>
<span class="ld-badge bg-des">Destruction</span>
</div>
<button class="ld-btn" onclick="window.print()">Print Data Tracking Worksheet</button>
<button class="ld-btn" style="background: #1976d2;" onclick="window.location.href='/app/load-list'">Back to Loads</button>
<table class="ld-table" id="ld-table">
<thead>
<tr>
<th rowspan="2">Material Type</th>
<th colspan="3" style="text-align:center; background:#1565c0;">Receiving</th>
<th colspan="3" style="text-align:center; background:#e65100;">HDR / Disassembly</th>
<th colspan="3" style="text-align:center; background:#2e7d32;">Test</th>
<th style="text-align:center; background:#7b1fa2;">R2</th>
<th colspan="4" style="text-align:center; background:#c62828;">Destruction</th>
</tr>
<tr>
<th>Count</th><th>Status</th><th>Send To</th>
<th>Recv'd</th><th>HDD Out</th><th>Send To</th>
<th>Sanitized</th><th>Status</th><th>Send To</th>
<th>Sent</th>
<th>Phys</th><th>HDD Need</th><th>HDD Phys</th><th>HDD Logic</th>
</tr>
</thead>
<tbody id="ld-body"></tbody>
</table>
</div>
@@ -0,0 +1,65 @@
frappe.pages["load-detail"].on_page_load = function(wrapper) {
var page = frappe.ui.make_app_page({
parent: wrapper,
title: "Load Detail",
single_column: true
});
var loadName = frappe.utils.get_url_arg("load");
if (!loadName) {
$(page.body).html("<div style='padding:40px;text-align:center;'><h2>No load specified</h2><p>Use ?load=MMDDYYYY-XXXX</p></div>");
return;
}
frappe.call({
method: "frappe.client.get",
args: { doctype: "Load", name: loadName },
callback: function(r) {
if (r.message) { showLoad(page, r.message); }
else { $(page.body).html("<div style='padding:40px;text-align:center;'><h2>Load not found</h2></div>"); }
}
});
function showLoad(page, load) {
var h = "<div style='padding:20px;'>";
h += "<div style='background:#f8f9fa;padding:15px;border-radius:8px;margin-bottom:20px;'>";
h += "<h2>Load: " + load.name + "</h2>";
h += "<div>In Date: " + (load.incoming_date || "N/A") + " | Customer: " + (load.customer_name || load.customer_number || "N/A") + "</div>";
h += "<div>Devices: " + (load.total_devices || 0) + " | Weight: " + (load.total_weight || 0) + " lbs</div>";
h += "</div>";
h += "<button class='btn btn-primary' onclick='window.print()' style='margin-bottom:15px;'>Print Data Tracking Worksheet</button> ";
h += "<a href='/app/load/" + encodeURIComponent(load.name) + "' class='btn btn-default' style='margin-bottom:15px;'>Open Form View</a> ";
h += "<a href='/app/load-update?load=" + encodeURIComponent(load.name) + "' class='btn btn-default' style='margin-bottom:15px;'>Edit Load</a>";
h += "<table style='width:100%;border-collapse:collapse;font-size:13px;margin-top:20px;'>";
h += "<thead><tr style='background:#37474f;color:white;'>";
h += "<th>Material Type</th><th>Count</th><th>Rcv Status</th><th>Send To</th>";
h += "<th>Recv'd</th><th>HDD Out</th><th>Dis Send To</th>";
h += "<th>Sanitized</th><th>Test Status</th><th>Test Send</th>";
h += "<th>R2 Sent</th><th>Phys</th><th>HDD Need</th><th>HDD Phys</th><th>HDD Logic</th>";
h += "</tr></thead><tbody>";
if (load.material_items && load.material_items.length > 0) {
load.material_items.forEach(function(item) {
h += "<tr style='border-bottom:1px solid #e0e0e0;'>";
h += "<td><strong>" + (item.material_type || "") + "</strong></td>";
h += "<td style='text-align:right;'>" + (item.total_count || 0) + "</td>";
h += "<td>" + (item.initial_data_status || "") + "</td>";
h += "<td>" + (item.send_to || "") + "</td>";
h += "<td style='text-align:right;'>" + (item.devices_received || 0) + "</td>";
h += "<td style='text-align:right;'>" + (item.hdd_removed || 0) + "</td>";
h += "<td>" + (item.disassembly_send_to || "") + "</td>";
h += "<td style='text-align:right;'>" + (item.units_sanitized_software || 0) + "</td>";
h += "<td>" + (item.test_data_status || "") + "</td>";
h += "<td>" + (item.test_send_to || "") + "</td>";
h += "<td style='text-align:right;'>" + (item.r2_units_sent || 0) + "</td>";
h += "<td style='text-align:right;'>" + (item.units_physical_destruction || 0) + "</td>";
h += "<td style='text-align:right;'>" + (item.hdd_needs_sanitize || 0) + "</td>";
h += "<td style='text-align:right;'>" + (item.hdd_physical_destruction || 0) + "</td>";
h += "<td style='text-align:right;'>" + (item.hdd_logical_sanitization || 0) + "</td>";
h += "</tr>";
});
}
h += "</tbody></table></div>";
$(page.body).html(h);
}
};
@@ -0,0 +1,18 @@
{
"doctype": "Page",
"name": "load-detail",
"page_name": "load-detail",
"title": "Load Detail",
"page_type": "Web Page",
"module": "Westech R2",
"standard": "Yes",
"system_page": 0,
"roles": [
{
"role": "All"
}
],
"content": "",
"script": null,
"style": null
}
@@ -0,0 +1,3 @@
import frappe
no_cache = 1
@@ -0,0 +1 @@
/* Load Update page CSS */
@@ -0,0 +1,4 @@
<div style="padding:20px;">
<h2>Load Update</h2>
<p>Use the form below to update load material items.</p>
</div>
@@ -0,0 +1,109 @@
frappe.pages["load-update"].on_page_load = function(wrapper) {
var page = frappe.ui.make_app_page({
parent: wrapper,
title: "Load Update",
single_column: true
});
var loadName = frappe.utils.get_url_arg("load");
if (!loadName) {
$(page.body).html("<div style='padding:40px;text-align:center;'><h2>No load specified</h2><p>Use ?load=MMDDYYYY-XXXX</p></div>");
return;
}
function loadLoad() {
frappe.call({
method: "frappe.client.get",
args: { doctype: "Load", name: loadName },
callback: function(r) {
if (r.message) { renderForm(page, r.message); }
else { $(page.body).html("<div style='padding:40px;text-align:center;'><h2>Load not found</h2></div>"); }
}
});
}
function renderForm(page, load) {
var h = "<div style='padding:20px;'>";
h += "<h2>Update Load: " + load.name + "</h2>";
h += "<p>Date: " + (load.incoming_date || "N/A") + " | Customer: " + (load.customer_name || load.customer_number || "N/A") + "</p>";
h += "<div style='margin-bottom:15px;'>";
h += "<button id='lu-save' class='btn btn-primary'>Save Changes</button> ";
h += "<button id='lu-print' class='btn btn-default'>Print Worksheet</button> ";
h += "<a href='/app/load/" + encodeURIComponent(load.name) + "' class='btn btn-default'>Open Form View</a>";
h += "</div>";
h += "<table style='width:100%;border-collapse:collapse;font-size:13px;'>";
h += "<thead><tr style='background:#37474f;color:white;'>";
h += "<th>Material Type</th><th>Count</th><th>Recv Status</th><th>Send To</th>";
h += "<th>Recv'd</th><th>HDD Out</th><th>Dis Send To</th>";
h += "<th>Sanitized</th><th>Test Status</th><th>Test Send</th>";
h += "<th>R2 Sent</th><th>Phys</th><th>HDD Need</th><th>HDD Phys</th><th>HDD Logic</th>";
h += "</tr></thead><tbody>";
if (load.material_items && load.material_items.length > 0) {
load.material_items.forEach(function(item, idx) {
var prefix = "item_" + idx + "_";
h += "<tr style='border-bottom:1px solid #e0e0e0;'>";
h += "<td><strong>" + (item.material_type || "") + "</strong></td>";
h += "<td><input type='number' id='" + prefix + "total_count' value='" + (item.total_count || 0) + "' style='width:50px;'></td>";
h += "<td><input type='text' id='" + prefix + "initial_data_status' value='" + (item.initial_data_status || "") + "' style='width:60px;'></td>";
h += "<td><input type='text' id='" + prefix + "send_to' value='" + (item.send_to || "") + "' style='width:80px;'></td>";
h += "<td><input type='number' id='" + prefix + "devices_received' value='" + (item.devices_received || 0) + "' style='width:50px;'></td>";
h += "<td><input type='number' id='" + prefix + "hdd_removed' value='" + (item.hdd_removed || 0) + "' style='width:50px;'></td>";
h += "<td><input type='text' id='" + prefix + "disassembly_send_to' value='" + (item.disassembly_send_to || "") + "' style='width:80px;'></td>";
h += "<td><input type='number' id='" + prefix + "units_sanitized_software' value='" + (item.units_sanitized_software || 0) + "' style='width:50px;'></td>";
h += "<td><input type='text' id='" + prefix + "test_data_status' value='" + (item.test_data_status || "") + "' style='width:60px;'></td>";
h += "<td><input type='text' id='" + prefix + "test_send_to' value='" + (item.test_send_to || "") + "' style='width:80px;'></td>";
h += "<td><input type='number' id='" + prefix + "r2_units_sent' value='" + (item.r2_units_sent || 0) + "' style='width:50px;'></td>";
h += "<td><input type='number' id='" + prefix + "units_physical_destruction' value='" + (item.units_physical_destruction || 0) + "' style='width:50px;'></td>";
h += "<td><input type='number' id='" + prefix + "hdd_needs_sanitize' value='" + (item.hdd_needs_sanitize || 0) + "' style='width:50px;'></td>";
h += "<td><input type='number' id='" + prefix + "hdd_physical_destruction' value='" + (item.hdd_physical_destruction || 0) + "' style='width:50px;'></td>";
h += "<td><input type='number' id='" + prefix + "hdd_logical_sanitization' value='" + (item.hdd_logical_sanitization || 0) + "' style='width:50px;'></td>";
h += "</tr>";
});
}
h += "</tbody></table></div>";
$(page.body).html(h);
$("#lu-save").on("click", function() {
var items = [];
load.material_items.forEach(function(item, idx) {
var prefix = "#item_" + idx + "_";
items.push({
name: item.name,
total_count: parseInt($(prefix + "total_count").val()) || 0,
initial_data_status: $(prefix + "initial_data_status").val(),
send_to: $(prefix + "send_to").val(),
devices_received: parseInt($(prefix + "devices_received").val()) || 0,
hdd_removed: parseInt($(prefix + "hdd_removed").val()) || 0,
disassembly_send_to: $(prefix + "disassembly_send_to").val(),
units_sanitized_software: parseInt($(prefix + "units_sanitized_software").val()) || 0,
test_data_status: $(prefix + "test_data_status").val(),
test_send_to: $(prefix + "test_send_to").val(),
r2_units_sent: parseInt($(prefix + "r2_units_sent").val()) || 0,
units_physical_destruction: parseInt($(prefix + "units_physical_destruction").val()) || 0,
hdd_needs_sanitize: parseInt($(prefix + "hdd_needs_sanitize").val()) || 0,
hdd_physical_destruction: parseInt($(prefix + "hdd_physical_destruction").val()) || 0,
hdd_logical_sanitization: parseInt($(prefix + "hdd_logical_sanitization").val()) || 0
});
});
frappe.call({
method: "westech_r2.page.load-update.load-update.save_load_items",
args: { load_name: loadName, items: JSON.stringify(items) },
callback: function(r) {
if (r.message && r.message.status === "ok") {
frappe.show_alert({message: "Saved", indicator: "green"});
loadLoad();
} else {
frappe.show_alert({message: "Save failed", indicator: "red"});
}
}
});
});
$("#lu-print").on("click", function() {
window.print();
});
}
loadLoad();
};
@@ -0,0 +1,18 @@
{
"doctype": "Page",
"name": "load-update",
"page_name": "load-update",
"title": "Load Update",
"page_type": "Web Page",
"module": "Westech R2",
"standard": "Yes",
"system_page": 0,
"roles": [
{
"role": "All"
}
],
"content": "",
"script": null,
"style": null
}
@@ -0,0 +1,17 @@
import frappe
import json
@frappe.whitelist()
def save_load_items(load_name, items):
items = json.loads(items)
load_doc = frappe.get_doc("Load", load_name)
for item_data in items:
for row in load_doc.material_items:
if row.name == item_data["name"]:
for field, value in item_data.items():
if field != "name":
row.set(field, value)
break
load_doc.save(ignore_permissions=True)
frappe.db.commit()
return {"status": "ok", "message": "Saved " + str(len(items)) + " items"}
@@ -0,0 +1 @@
/* Load Update page CSS */
@@ -0,0 +1,4 @@
<div style="padding:20px;">
<h2>Load Update</h2>
<p>Use the form below to update load material items.</p>
</div>
@@ -0,0 +1,109 @@
frappe.pages["load-update"].on_page_load = function(wrapper) {
var page = frappe.ui.make_app_page({
parent: wrapper,
title: "Load Update",
single_column: true
});
var loadName = frappe.utils.get_url_arg("load");
if (!loadName) {
$(page.body).html("<div style='padding:40px;text-align:center;'><h2>No load specified</h2><p>Use ?load=MMDDYYYY-XXXX</p></div>");
return;
}
function loadLoad() {
frappe.call({
method: "frappe.client.get",
args: { doctype: "Load", name: loadName },
callback: function(r) {
if (r.message) { renderForm(page, r.message); }
else { $(page.body).html("<div style='padding:40px;text-align:center;'><h2>Load not found</h2></div>"); }
}
});
}
function renderForm(page, load) {
var h = "<div style='padding:20px;'>";
h += "<h2>Update Load: " + load.name + "</h2>";
h += "<p>Date: " + (load.incoming_date || "N/A") + " | Customer: " + (load.customer_name || load.customer_number || "N/A") + "</p>";
h += "<div style='margin-bottom:15px;'>";
h += "<button id='lu-save' class='btn btn-primary'>Save Changes</button> ";
h += "<button id='lu-print' class='btn btn-default'>Print Worksheet</button> ";
h += "<a href='/app/load/" + encodeURIComponent(load.name) + "' class='btn btn-default'>Open Form View</a>";
h += "</div>";
h += "<table style='width:100%;border-collapse:collapse;font-size:13px;'>";
h += "<thead><tr style='background:#37474f;color:white;'>";
h += "<th>Material Type</th><th>Count</th><th>Recv Status</th><th>Send To</th>";
h += "<th>Recv'd</th><th>HDD Out</th><th>Dis Send To</th>";
h += "<th>Sanitized</th><th>Test Status</th><th>Test Send</th>";
h += "<th>R2 Sent</th><th>Phys</th><th>HDD Need</th><th>HDD Phys</th><th>HDD Logic</th>";
h += "</tr></thead><tbody>";
if (load.material_items && load.material_items.length > 0) {
load.material_items.forEach(function(item, idx) {
var prefix = "item_" + idx + "_";
h += "<tr style='border-bottom:1px solid #e0e0e0;'>";
h += "<td><strong>" + (item.material_type || "") + "</strong></td>";
h += "<td><input type='number' id='" + prefix + "total_count' value='" + (item.total_count || 0) + "' style='width:50px;'></td>";
h += "<td><input type='text' id='" + prefix + "initial_data_status' value='" + (item.initial_data_status || "") + "' style='width:60px;'></td>";
h += "<td><input type='text' id='" + prefix + "send_to' value='" + (item.send_to || "") + "' style='width:80px;'></td>";
h += "<td><input type='number' id='" + prefix + "devices_received' value='" + (item.devices_received || 0) + "' style='width:50px;'></td>";
h += "<td><input type='number' id='" + prefix + "hdd_removed' value='" + (item.hdd_removed || 0) + "' style='width:50px;'></td>";
h += "<td><input type='text' id='" + prefix + "disassembly_send_to' value='" + (item.disassembly_send_to || "") + "' style='width:80px;'></td>";
h += "<td><input type='number' id='" + prefix + "units_sanitized_software' value='" + (item.units_sanitized_software || 0) + "' style='width:50px;'></td>";
h += "<td><input type='text' id='" + prefix + "test_data_status' value='" + (item.test_data_status || "") + "' style='width:60px;'></td>";
h += "<td><input type='text' id='" + prefix + "test_send_to' value='" + (item.test_send_to || "") + "' style='width:80px;'></td>";
h += "<td><input type='number' id='" + prefix + "r2_units_sent' value='" + (item.r2_units_sent || 0) + "' style='width:50px;'></td>";
h += "<td><input type='number' id='" + prefix + "units_physical_destruction' value='" + (item.units_physical_destruction || 0) + "' style='width:50px;'></td>";
h += "<td><input type='number' id='" + prefix + "hdd_needs_sanitize' value='" + (item.hdd_needs_sanitize || 0) + "' style='width:50px;'></td>";
h += "<td><input type='number' id='" + prefix + "hdd_physical_destruction' value='" + (item.hdd_physical_destruction || 0) + "' style='width:50px;'></td>";
h += "<td><input type='number' id='" + prefix + "hdd_logical_sanitization' value='" + (item.hdd_logical_sanitization || 0) + "' style='width:50px;'></td>";
h += "</tr>";
});
}
h += "</tbody></table></div>";
$(page.body).html(h);
$("#lu-save").on("click", function() {
var items = [];
load.material_items.forEach(function(item, idx) {
var prefix = "#item_" + idx + "_";
items.push({
name: item.name,
total_count: parseInt($(prefix + "total_count").val()) || 0,
initial_data_status: $(prefix + "initial_data_status").val(),
send_to: $(prefix + "send_to").val(),
devices_received: parseInt($(prefix + "devices_received").val()) || 0,
hdd_removed: parseInt($(prefix + "hdd_removed").val()) || 0,
disassembly_send_to: $(prefix + "disassembly_send_to").val(),
units_sanitized_software: parseInt($(prefix + "units_sanitized_software").val()) || 0,
test_data_status: $(prefix + "test_data_status").val(),
test_send_to: $(prefix + "test_send_to").val(),
r2_units_sent: parseInt($(prefix + "r2_units_sent").val()) || 0,
units_physical_destruction: parseInt($(prefix + "units_physical_destruction").val()) || 0,
hdd_needs_sanitize: parseInt($(prefix + "hdd_needs_sanitize").val()) || 0,
hdd_physical_destruction: parseInt($(prefix + "hdd_physical_destruction").val()) || 0,
hdd_logical_sanitization: parseInt($(prefix + "hdd_logical_sanitization").val()) || 0
});
});
frappe.call({
method: "westech_r2.page.load-update.load-update.save_load_items",
args: { load_name: loadName, items: JSON.stringify(items) },
callback: function(r) {
if (r.message && r.message.status === "ok") {
frappe.show_alert({message: "Saved", indicator: "green"});
loadLoad();
} else {
frappe.show_alert({message: "Save failed", indicator: "red"});
}
}
});
});
$("#lu-print").on("click", function() {
window.print();
});
}
loadLoad();
};
@@ -0,0 +1,18 @@
{
"doctype": "Page",
"name": "load-update",
"page_name": "load-update",
"title": "Load Update",
"page_type": "Web Page",
"module": "Westech R2",
"standard": "Yes",
"system_page": 0,
"roles": [
{
"role": "All"
}
],
"content": "",
"script": null,
"style": null
}
@@ -0,0 +1,17 @@
import frappe
import json
@frappe.whitelist()
def save_load_items(load_name, items):
items = json.loads(items)
load_doc = frappe.get_doc("Load", load_name)
for item_data in items:
for row in load_doc.material_items:
if row.name == item_data["name"]:
for field, value in item_data.items():
if field != "name":
row.set(field, value)
break
load_doc.save(ignore_permissions=True)
frappe.db.commit()
return {"status": "ok", "message": "Saved " + str(len(items)) + " items"}
@@ -0,0 +1 @@
# Pallet List page for Westech R2
@@ -0,0 +1,82 @@
<style>
.pallet-list-page { font-family: Helvetica Neue, Arial, sans-serif; }
.pallet-list-page h2 { color: #6f42c1; margin-bottom: 16px; font-size: 20px; }
.pallet-search-box { margin-bottom: 16px; display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.pallet-search-box input, .pallet-search-box select {
padding: 6px 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 13px;
}
.pallet-search-box button {
padding: 6px 14px; border: 1px solid #ddd; border-radius: 4px; cursor: pointer;
font-size: 13px; background: white;
}
.pallet-search-box button.btn-primary { background: #6f42c1; color: white; border-color: #6f42c1; }
.pallet-search-box button.btn-success { background: #28a745; color: white; border-color: #28a745; }
.pallet-table-container { padding: 0; overflow-x: auto; background: white; border-radius: 4px; }
.pallet-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.pallet-table th {
background: #6f42c1; color: white; padding: 8px 10px; text-align: left;
cursor: pointer; white-space: nowrap; font-weight: 500;
}
.pallet-table td { padding: 5px 10px; border-bottom: 1px solid #eee; }
.pallet-table tr:hover { background: #f5f5f5; }
.pallet-table tr.new-row { background: #FFF9E6 !important; }
.pallet-table a { color: #6f42c1; text-decoration: none; font-weight: 600; }
.status-received { color: #2196F3; font-weight: 600; }
.status-sorting { color: #FF9800; font-weight: 600; }
.status-processing { color: #9C27B0; font-weight: 600; }
.status-complete { color: #4CAF50; font-weight: 600; }
.status-shipped { color: #607D8B; font-weight: 600; }
.pallet-pagination { margin-top: 12px; display: flex; gap: 5px; align-items: center; }
.pallet-pagination button {
padding: 5px 12px; border: 1px solid #ddd; background: white; cursor: pointer;
border-radius: 4px; font-size: 13px; color: #6f42c1;
}
.pallet-pagination button.active { background: #6f42c1; color: white; border-color: #6f42c1; }
.pallet-pagination button:disabled { opacity: 0.5; cursor: not-allowed; }
.pallet-count { margin-left: auto; color: #666; font-size: 13px; }
</style>
<div class="pallet-list-page">
<h2>📦 Pallet List</h2>
<div class="pallet-search-box">
<input type="text" id="pallet-search" placeholder="Search pallet #..." style="max-width:200px;">
<select id="status-filter">
<option value="">All Statuses</option>
<option value="Received">Received</option>
<option value="Sorting">Sorting</option>
<option value="Processing">Processing</option>
<option value="Complete">Complete</option>
<option value="Shipped">Shipped</option>
</select>
<button class="btn btn-default" id="btn-clear">✕ Clear</button>
<button class="btn btn-success" id="btn-export">⬇ Export CSV</button>
<span class="pallet-count" id="pallet-count"></span>
</div>
<div class="pallet-table-container">
<table class="pallet-table" id="pallet-table">
<thead>
<tr>
<th data-sort="pallet_number">Pallet # ⇅</th>
<th data-sort="date_reserved">Date Reserved ⇅</th>
<th data-sort="received_date">Rec. Date ⇅</th>
<th data-sort="customer_number">Customer # ⇅</th>
<th data-sort="inbound_weight">Lbs ⇅</th>
<th data-sort="tester">Who ⇅</th>
<th data-sort="description">Items Tested ⇅</th>
<th data-sort="qty_to_sales">QTY Sale ⇅</th>
<th data-sort="weight_to_sales">Lbs Sale ⇅</th>
<th data-sort="finish_date">Finish Date ⇅</th>
<th>Status</th>
<th>Notes</th>
</tr>
</thead>
<tbody id="pallet-tbody">
<tr><td colspan="12" style="text-align:center;padding:40px;color:#666;">Loading...</td></tr>
</tbody>
</table>
</div>
<div class="pallet-pagination" id="pallet-pagination"></div>
</div>
@@ -0,0 +1,128 @@
frappe.pages["pallet-list"].on_page_load = function(wrapper) {
var page = frappe.ui.make_app_page({
parent: wrapper,
title: __("Pallet List"),
single_column: true
});
var content = frappe.render_template("pallet-list", {});
$(page.body).append(content);
var currentPage = 1;
var pageSize = 100;
var currentSort = "pallet_number";
var sortDir = "desc";
var searchTerm = "";
var statusFilter = "";
function loadPallets() {
frappe.call({
method: "westech_r2.page.pallet_list.pallet_list.get_pallets",
args: {
page: currentPage,
page_size: pageSize,
sort_field: currentSort,
sort_dir: sortDir,
status_filter: statusFilter,
search: searchTerm
},
callback: function(r) {
if (r.message) {
renderPallets(r.message.pallets, r.message.total);
}
}
});
}
function renderPallets(pallets, total) {
var tbody = $("#pallet-tbody");
tbody.empty();
if (!pallets || pallets.length === 0) {
tbody.append('<tr><td colspan="13" style="text-align:center;padding:40px;">No pallets found</td></tr>');
return;
}
pallets.forEach(function(p) {
var statusClass = "status-" + (p.status || "").toLowerCase().replace(/\s+/g, "-");
var link = "/app/pallet/" + encodeURIComponent(p.name);
var pn = (p.pallet_number || "").replace(/</g, "&lt;").replace(/>/g, "&gt;");
var row = '<tr>' +
'<td><a href="' + link + '" style="color:#3cc062;text-decoration:none;font-weight:600;">' + pn + '</a></td>' +
'<td>' + fmtDate(p.date_reserved) + '</td>' +
'<td>' + fmtDate(p.received_date) + '</td>' +
'<td>' + (p.customer_number || "") + '</td>' +
'<td>' + (p.company_name || "") + '</td>' +
'<td>' + (p.inbound_weight || "") + '</td>' +
'<td>' + (p.tester || "") + '</td>' +
'<td>' + (p.description || "") + '</td>' +
'<td>' + (p.qty_to_sales || "") + '</td>' +
'<td>' + (p.weight_to_sales || "") + '</td>' +
'<td>' + fmtDate(p.finish_date) + '</td>' +
'<td class="' + statusClass + '">' + (p.status || "") + '</td>' +
'<td>' + (p.notes || "").substring(0, 50) + '</td>' +
'</tr>';
tbody.append(row);
});
renderPagination(total);
}
function renderPagination(total) {
var totalPages = Math.ceil(total / pageSize);
var pagination = $("#pallet-pagination");
pagination.empty();
if (totalPages <= 1) return;
var prevBtn = $('<button>&laquo; Prev</button>').attr("disabled", currentPage === 1)
.css({padding: "5px 12px", border: "1px solid #ddd", background: "white", cursor: "pointer", marginRight: "5px"})
.on("click", function() {
if (currentPage > 1) { currentPage--; loadPallets(); }
});
pagination.append(prevBtn);
var startPage = Math.max(1, currentPage - 2);
var endPage = Math.min(totalPages, startPage + 4);
for (var i = startPage; i <= endPage; i++) {
var btn = $('<button>' + i + '</button>')
.css({padding: "5px 12px", border: "1px solid #ddd", background: i === currentPage ? "#3cc062" : "white",
color: i === currentPage ? "white" : "#333", cursor: "pointer", marginRight: "5px"})
.on("click", function(page) {
return function() { currentPage = page; loadPallets(); };
}(i));
pagination.append(btn);
}
var nextBtn = $('<button>Next &raquo;</button>').attr("disabled", currentPage === totalPages)
.css({padding: "5px 12px", border: "1px solid #ddd", background: "white", cursor: "pointer", marginRight: "5px"})
.on("click", function() {
if (currentPage < totalPages) { currentPage++; loadPallets(); }
});
pagination.append(nextBtn);
}
function fmtDate(v) {
if (!v) return "";
var s = String(v);
if (s.indexOf("T") > -1) s = s.split("T")[0];
if (s.indexOf(" ") > -1) s = s.split(" ")[0];
return s;
}
$("#pallet-search").on("input", function() {
searchTerm = $(this).val();
currentPage = 1;
loadPallets();
});
$("#status-filter").on("change", function() {
statusFilter = $(this).val();
currentPage = 1;
loadPallets();
});
loadPallets();
};
@@ -0,0 +1,19 @@
{
"content": null,
"creation": "2026-05-19 13:00:00.000000",
"docstatus": 0,
"doctype": "Page",
"idx": 0,
"modified": "2026-05-19 13:00:00.000000",
"modified_by": "Administrator",
"module": "Westech R2",
"name": "pallet-list",
"owner": "Administrator",
"page_name": "pallet-list",
"roles": [],
"script": null,
"standard": "Yes",
"style": null,
"system_page": 0,
"title": "Pallet List"
}
@@ -0,0 +1,64 @@
import frappe
@frappe.whitelist()
def get_pallets(page=1, page_size=100, sort_field="pallet_number", sort_dir="desc", status_filter="", search=""):
page = int(page)
page_size = int(page_size)
offset = (page - 1) * page_size
conditions = [
"pallet_number IS NOT NULL",
"pallet_number != ''",
"date_reserved IS NOT NULL",
"customer_number IS NOT NULL",
"customer_number != ''"
]
junk = ["", "0", "0000", "N/A", "TBD", "null", "999990", "999995"]
junk_list = "', '".join(junk)
conditions.append("pallet_number NOT IN ('" + junk_list + "')")
conditions.append("pallet_number NOT LIKE '999%'")
conditions.append("pallet_number REGEXP '^[0-9]'")
if status_filter:
conditions.append("status = '" + frappe.db.escape(status_filter) + "'")
if search:
conditions.append("pallet_number LIKE '%" + frappe.db.escape(search) + "%'")
where_clause = " AND ".join(conditions)
total = frappe.db.sql("SELECT COUNT(*) FROM tabPallet WHERE " + where_clause)[0][0]
pallets = frappe.db.sql("SELECT name, pallet_number, date_reserved, received_date, customer_number, company_name, inbound_weight, tester, description, qty_to_sales, weight_to_sales, finish_date, notes, status FROM tabPallet WHERE " + where_clause + " ORDER BY CAST(pallet_number AS UNSIGNED) " + sort_dir + " LIMIT " + str(page_size) + " OFFSET " + str(offset), as_dict=True)
empty_pallet = frappe.db.sql("SELECT name, pallet_number, date_reserved, received_date, customer_number, company_name, inbound_weight, tester, description, qty_to_sales, weight_to_sales, finish_date, notes, status FROM tabPallet WHERE date_reserved IS NULL AND (customer_number IS NULL OR customer_number = '') AND pallet_number NOT IN ('" + junk_list + "') AND pallet_number NOT LIKE '999%' AND pallet_number REGEXP '^[0-9]' ORDER BY CAST(pallet_number AS UNSIGNED) DESC LIMIT 1", as_dict=True)
result = []
if empty_pallet:
empty_pallet[0]["_is_new"] = True
result.append(empty_pallet[0])
result.extend(pallets)
return {"pallets": result, "total": total, "page": page, "page_size": page_size}
@frappe.whitelist()
def update_pallet(docname, field, value):
pallet = frappe.get_doc("Pallet", docname)
pallet.set(field, value)
pallet.save(ignore_permissions=True)
frappe.db.commit()
return {"status": "ok", "message": "Updated " + field}
@frappe.whitelist()
def create_pallet(data):
data = frappe.parse_json(data)
pallet = frappe.new_doc("Pallet")
pallet.pallet_number = data.get("pallet_number")
pallet.status = data.get("status", "Received")
pallet.date_reserved = data.get("date_reserved")
for field, value in data.items():
if field not in ["pallet_number", "status", "date_reserved"] and value:
pallet.set(field, value)
pallet.insert(ignore_permissions=True)
frappe.db.commit()
return {"status": "ok", "name": pallet.name}
@@ -0,0 +1 @@
@@ -0,0 +1,4 @@
frappe.pages["r2-tracking"].on_page_load = function(wrapper) {
wrapper.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:60vh;font-family:sans-serif;"><div style="text-align:center;"><i class="fa fa-spinner fa-spin" style="font-size:24px;color:#1a6b8a;"></i><p style="margin-top:12px;color:#555;">Redirecting to R2 Data Tracking...</p></div></div>';
setTimeout(function() { window.location.href = "https://eim.diagalon.com/report/data-tracking-form"; }, 500);
};
@@ -0,0 +1,13 @@
{
"creation": "2026-05-09 14:00:00",
"docstatus": 0,
"doctype": "Page",
"idx": 0,
"modified": "2026-05-09 14:00:00",
"modified_by": "Administrator",
"module": "Westech R2",
"name": "r2-tracking",
"owner": "Administrator",
"standard": "Yes",
"title": "R2 Data Tracking"
}
@@ -0,0 +1,5 @@
import frappe
def get_context(context):
frappe.local.flags.redirect_location = "https://eim.diagalon.com/report/data-tracking-form"
raise frappe.Redirect
@@ -0,0 +1,7 @@
frappe.pages['r2-tracking'].on_page_load = function(wrapper) {
var page = frappe.ui.make_app_page({
parent: wrapper,
title: 'R2 Data Tracking',
single_column: true
});
}
@@ -0,0 +1,23 @@
{
"content": null,
"creation": "2026-05-09 14:00:00",
"docstatus": 0,
"doctype": "Page",
"idx": 0,
"modified": "2026-05-09 15:09:48.707863",
"modified_by": "Administrator",
"module": "Westech R2",
"name": "r2-tracking",
"owner": "Administrator",
"page_name": "r2-tracking",
"roles": [
{
"role": "All"
}
],
"script": null,
"standard": "Yes",
"style": null,
"system_page": 0,
"title": "R2 Data Tracking"
}
@@ -0,0 +1,642 @@
frappe.pages['receiving'].on_page_load = function(wrapper) {
var page = frappe.ui.make_app_page({
parent: wrapper,
title: 'Receiving',
single_column: true
});
// Inline HTML
$(wrapper).find('.layout-main-section').html(`
<div class="receiving-station" style="padding: 20px;">
<div class="row">
<div class="col-md-12">
<h3 style="margin-top: 0; color: #2F5496;">🚛 Receiving</h3>
<p class="text-muted">Schedule pickups, manage routes, and check in loads.</p>
</div>
</div>
<ul class="nav nav-tabs" role="tablist" id="receiving-tabs">
<li role="presentation" class="active"><span class="stage-tab active" data-target="#stage-a" style="cursor:pointer">📋 Stage A — Schedule Pickup</span></li>
<li role="presentation"><span class="stage-tab" data-target="#stage-b" style="cursor:pointer">🗺️ Stage B — Route & Dispatch</span></li>
<li role="presentation"><span class="stage-tab" data-target="#stage-c" style="cursor:pointer">⚖️ Stage C — Load Check-in</span></li>
</ul>
<div class="tab-content" style="padding-top: 20px;">
<div role="tabpanel" class="tab-pane active" id="stage-a">
<div class="row">
<div class="col-md-3">
<div class="panel panel-primary">
<div class="panel-heading">📅 Pickup Calendar — Next 30 Days</div>
<div class="panel-body" id="pickup-calendar"><div class="text-muted text-center">Loading...</div></div>
</div>
</div>
<div class="col-md-9">
<div class="panel panel-default">
<div class="panel-heading">
<div class="row">
<div class="col-md-6"><strong>Scheduled Pickups</strong><span id="pickup-count-label" class="text-muted" style="margin-left: 8px;"></span></div>
<div class="col-md-6 text-right">
<button class="btn btn-primary btn-sm" id="btn-new-pickup">+ New Pickup</button>
<input type="date" id="pickup-date-filter" class="form-control input-sm" style="display:inline-block;width:auto;vertical-align:middle;margin-left:8px;">
<button class="btn btn-default btn-sm" id="btn-clear-date" style="margin-left:4px;">Clear</button>
</div>
</div>
</div>
<div class="panel-body" style="padding: 0; overflow-x: auto;">
<table class="table table-striped table-hover" id="pickup-table" style="font-size: 13px; margin-bottom: 0;">
<thead><tr><th>Date</th><th>Weekday</th><th>Type</th><th>Customer</th><th>Contact</th><th>Address</th><th>Est. Items</th><th>Data</th><th>RED/R2</th><th>Status</th><th>Notes</th><th>Truck</th><th>AoR</th><th>CoD</th></tr></thead>
<tbody id="pickup-tbody"><tr><td colspan="14" class="text-center text-muted">Loading...</td></tr></tbody>
</table>
</div>
</div>
</div>
</div>
<div id="new-pickup-form" style="display:none; margin-top: 16px;">
<div class="panel panel-primary">
<div class="panel-heading">+ New Scheduled Pickup</div>
<div class="panel-body">
<form id="pickup-form">
<div class="row">
<div class="col-md-4">
<h5 style="color:#6f42c1;">📅 Pickup Info</h5>
<div class="form-group"><label>Pickup Date <span class="text-danger">*</span></label><input type="date" id="sp-pickup_date" class="form-control" required></div>
<div class="form-group"><label>Type <span class="text-danger">*</span></label><select id="sp-pickup_type" class="form-control" required><option value="Pickup">Pickup</option><option value="Drop-off">Drop-off</option></select></div>
<div class="form-group">
<label>Customer <span class="text-danger">*</span></label>
<div class="input-group">
<div id="sp-customer-control" style="flex:1;"></div>
<span class="input-group-btn"><button class="btn btn-success" id="btn-new-customer" type="button" title="New Customer">+</button></span>
</div>
</div>
<div class="form-group"><label>Company Name</label><input type="text" id="sp-company_name" class="form-control"></div>
<div class="form-group"><label>Contact Name</label><input type="text" id="sp-contact_name" class="form-control"></div>
<div class="form-group"><label>Contact Phone</label><input type="text" id="sp-contact_phone" class="form-control"></div>
<div class="form-group"><label>Contact Email</label><input type="email" id="sp-contact_email" class="form-control"></div>
</div>
<div class="col-md-4">
<h5 style="color:#6f42c1;">📍 Address</h5>
<div class="form-group"><label>Street Address</label><input type="text" id="sp-address_line" class="form-control"></div>
<div class="form-group"><label>City</label><input type="text" id="sp-city" class="form-control"></div>
<div class="form-group"><label>State</label><input type="text" id="sp-state" class="form-control" value="AZ"></div>
<div class="form-group"><label>ZIP</label><input type="text" id="sp-zip_code" class="form-control"></div>
<div class="form-group"><label>Hours of Operation</label><input type="text" id="sp-hours_of_operation" class="form-control" placeholder="e.g. Mon-Fri 8am-5pm"></div>
</div>
<div class="col-md-4">
<h5 style="color:#6f42c1;">📦 Load Info</h5>
<div class="form-group"><label>Estimated Items</label><input type="number" id="sp-estimated_items" class="form-control"></div>
<div class="form-group"><label>Estimated Weight</label><input type="text" id="sp-estimated_weight" class="form-control"></div>
<div class="form-group"><label>Load Contents</label><input type="text" id="sp-load_contents" class="form-control" placeholder="Wire, Monitors, Laptops..."></div>
<div class="form-group"><label>Data Status</label><select id="sp-data_status" class="form-control"><option value="">—</option><option value="D0">D0</option><option value="D1">D1</option><option value="ND1">ND1</option><option value="ND2">ND2</option><option value="ND3">ND3</option><option value="ND4">ND4</option></select></div>
<div class="form-group"><label>RED / R2</label><select id="sp-red_r2" class="form-control"><option value="">—</option><option value="RED">RED</option><option value="NIST">NIST</option><option value="Red+NIST">Red+NIST</option><option value="R2">R2</option><option value="Both">Both</option><option value="Neither">Neither</option></select></div>
<div class="form-group"><div class="checkbox"><label><input type="checkbox" id="sp-needs_aor"> <strong>Needs AoR</strong></label></div><div class="checkbox"><label><input type="checkbox" id="sp-needs_cod"> <strong>Needs CoD</strong></label></div></div>
<div class="form-group"><label>Notes / Special Handling</label><textarea id="sp-notes" class="form-control" rows="3"></textarea></div>
</div>
</div>
<div class="row" style="margin-top: 16px;"><div class="col-md-12"><button type="submit" class="btn btn-primary btn-lg">Save Pickup</button><button type="button" class="btn btn-default btn-lg" id="btn-cancel-pickup">Cancel</button></div></div>
</form>
</div>
</div>
</div>
</div>
<div role="tabpanel" class="tab-pane" id="stage-b">
<div class="row" style="margin-bottom: 16px;"><div class="col-md-12"><div class="btn-group"><input type="date" id="route-date" class="form-control" style="display:inline-block;width:auto;"><button class="btn btn-primary" id="btn-load-routes">Load Pickups</button><button class="btn btn-primary" id="btn-auto-route">🧮 Auto-Route</button><button class="btn btn-success" id="btn-route-sheet">🖨️ Route Sheet</button><button class="btn btn-success" id="btn-green-sheet">📄 Green Sheet</button><button class="btn btn-success" id="btn-labels">🏷️ Labels</button></div></div></div>
<div class="row" id="route-columns">
<div class="col-md-4"><div class="panel panel-default truck-column" data-truck="Truck 1"><div class="panel-heading">🚛 Truck 1 <span id="truck1-count" class="text-muted"></span></div><div class="panel-body truck-stops" id="truck1-stops"></div></div></div>
<div class="col-md-4"><div class="panel panel-default truck-column" data-truck="Truck 2"><div class="panel-heading">🚛 Truck 2 <span id="truck2-count" class="text-muted"></span></div><div class="panel-body truck-stops" id="truck2-stops"></div></div></div>
<div class="col-md-4"><div class="panel panel-default truck-column" data-truck="Truck 3"><div class="panel-heading">🚛 Truck 3 <span id="truck3-count" class="text-muted"></span></div><div class="panel-body truck-stops" id="truck3-stops"></div></div></div>
</div>
<div class="row" style="margin-top: 16px;"><div class="col-md-12"><div class="panel panel-default truck-column" data-truck=""><div class="panel-heading">📋 Unassigned <span id="unassigned-count" class="text-muted"></span></div><div class="panel-body truck-stops" id="unassigned-stops"></div></div></div></div>
</div>
<div role="tabpanel" class="tab-pane" id="stage-c">
<div class="row" style="margin-bottom: 16px;"><div class="col-md-12"><button class="btn btn-primary" id="btn-new-checkin">+ Check In Load</button><button class="btn btn-success" id="btn-cor-report">📋 CoR Report</button></div></div>
<div class="panel panel-default">
<div class="panel-heading">Recent Check-ins</div>
<div class="panel-body" style="padding: 0; overflow-x: auto;">
<table class="table table-striped table-hover" id="checkin-table" style="font-size: 13px; margin-bottom: 0;">
<thead><tr><th>Date</th><th>Customer</th><th>Load #</th><th class="text-right">Pallets</th><th class="text-right">Weight</th><th>Contents</th><th>Data Status</th><th>RED/R2</th></tr></thead>
<tbody id="checkin-tbody"><tr><td colspan="8" class="text-center text-muted">Loading...</td></tr></tbody>
</table>
</div>
</div>
<div id="checkin-form" style="display:none; margin-top: 16px;">
<div class="panel panel-primary">
<div class="panel-heading">+ Load Check-in</div>
<div class="panel-body">
<form id="checkin-form-inner">
<div class="row">
<div class="col-md-4">
<h5 style="color:#6f42c1;">📅 Pickup Reference</h5>
<div class="form-group"><label>Scheduled Pickup <span class="text-danger">*</span></label><div id="ci-pickup-control"></div></div>
<div id="ci-pickup-details" style="display:none; background:#f8f9fa; border:1px solid #ddd; border-radius:4px; padding:10px; margin-bottom:10px;">
<div id="ci-customer-info" style="font-weight:700;"></div>
<div id="ci-address-info" style="font-size:12px; color:#666;"></div>
<div id="ci-contact-info" style="font-size:12px; color:#666;"></div>
<div id="ci-special-handling" style="display:none; margin-top:8px; padding:6px; background:#FFCDD2; border:1px solid #C62828; border-radius:4px; font-size:12px;"></div>
<div id="ci-pickup-notes" style="display:none; margin-top:8px; padding:6px; background:#F5F5F5; border:1px solid #999; border-radius:4px; font-size:12px;"></div>
</div>
</div>
<div class="col-md-4">
<h5 style="color:#6f42c1;">⚖️ Actual Load</h5>
<div class="form-group"><label>Received Date <span class="text-danger">*</span></label><input type="date" id="ci-received_date" class="form-control" required></div>
<div class="form-group"><label>Actual # of Pallets/Gaylords <span class="text-danger">*</span></label><input type="number" id="ci-actual_pallets" class="form-control" min="1" required></div>
<div class="form-group"><label>Total Weight (lbs)</label><input type="text" id="ci-total_weight" class="form-control"></div>
</div>
<div class="col-md-4">
<h5 style="color:#6f42c1;">📦 Contents & Classification</h5>
<div class="form-group"><label>Load Contents</label><textarea id="ci-load_contents" class="form-control" rows="2" placeholder="e.g. 80% wire 20% tablets"></textarea></div>
<div class="form-group"><label>Data Status</label><select id="ci-data_status" class="form-control"><option value="">—</option><option value="D0">D0</option><option value="D1">D1</option><option value="ND1">ND1</option><option value="ND2">ND2</option><option value="ND3">ND3</option><option value="ND4">ND4</option></select></div>
<div class="form-group"><label>RED / R2</label><select id="ci-red_r2" class="form-control"><option value="">—</option><option value="RED">RED</option><option value="NIST">NIST</option><option value="Red+NIST">Red+NIST</option><option value="R2">R2</option><option value="Both">Both</option><option value="Neither">Neither</option></select></div>
</div>
</div>
<div class="row" style="margin-top: 8px;"><div class="col-md-12"><button type="submit" class="btn btn-primary btn-lg">✓ Check In Load</button><button type="button" class="btn btn-default btn-lg" id="btn-cancel-checkin">Cancel</button></div></div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
.truck-stops { min-height: 60px; }
.stop-card { background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 6px; padding: 10px 12px; margin: 6px 0; cursor: grab; font-size: 13px; }
.stop-card:hover { border-color: #2F5496; }
.stop-card .stop-co { font-weight: 700; color: #2F5496; }
.stop-card .stop-addr { color: #666; font-size: 12px; margin-top: 2px; }
.stop-card .stop-meta { display: flex; gap: 8px; margin-top: 4px; font-size: 11px; }
.stop-card .stop-meta span { background: #D6E4F0; color: #2F5496; padding: 1px 6px; border-radius: 3px; }
.stop-card.dragging { opacity: 0.5; }
.cal-day { display: inline-block; width: 36px; height: 36px; line-height: 36px; text-align: center; margin: 1px; border-radius: 4px; font-size: 12px; cursor: pointer; }
.cal-day:hover { background: #D6E4F0; }
.cal-day.has-pickups { background: #2F5496; color: #fff; font-weight: 700; }
.stage-tab { display: inline-block; padding: 10px 15px; font-size: 14px; color: #555; } .stage-tab:hover { color: #2F5496; } .stage-tab.active { color: #2F5496; font-weight: 600; border-bottom: 2px solid #2F5496; }
.cal-day.today { border: 2px solid #C62828; }
</style>
`);
// Prevent Frappe router from intercepting tab clicks
// Tab switching — direct DOM to avoid Frappe router intercepting <a> clicks
$("#receiving-tabs").on("click", ".stage-tab", function() {
var target = $(this).data("target");
$("#receiving-tabs li, .stage-tab").removeClass("active");
$(this).addClass("active").closest("li").addClass("active");
$(".tab-pane").removeClass("active");
$(target).addClass("active");
});
// ── Stage A: Link Controls ──
var customer_control = null;
function setupCustomerLink() {
customer_control = frappe.ui.form.make_control({
parent: $("#sp-customer-control"),
df: {
fieldtype: "Link",
fieldname: "customer_number",
options: "Customer",
label: "Customer",
reqd: 1,
placeholder: "Search by name, number, or address...",
onchange: function() {
var val = customer_control.get_value();
if (val) fetchCustomerDetails(val);
else clearCustomerFields();
}
},
only_input: true,
});
customer_control.refresh();
$("#sp-customer-control .control-input").css("margin", "0");
$("#sp-customer-control .help-box").remove();
}
// New Customer button
$(document).on("click", "#btn-new-customer", function() {
frappe.call({
method: "frappe.client.insert",
args: { doc: { doctype: "Customer", customer_type: "Company", customer_group: "All Customer Groups", territory: "United States" } },
callback: function(r) {
if (r.message) {
frappe.set_route("Form", "Customer", r.message.name);
}
}
});
});
function fetchCustomerDetails(customer_name) {
frappe.call({
method: "frappe.client.get",
args: { doctype: "Customer", name: customer_name },
callback: function(r) {
if (!r.message) return;
var c = r.message;
$("#sp-company_name").val(c.customer_name || "");
$("#sp-contact_name").val(c.contact_name || "");
$("#sp-contact_phone").val(c.contact_phone || "");
$("#sp-contact_email").val(c.contact_email || "");
frappe.call({
method: "frappe.client.get_list",
args: {
doctype: "Address",
filters: [["Dynamic Link", "link_name", "=", customer_name]],
fields: ["address_line1", "city", "state", "pincode"],
limit_page_length: 1
},
callback: function(ra) {
if (ra.message && ra.message.length) {
var a = ra.message[0];
$("#sp-address_line").val(a.address_line1 || "");
$("#sp-city").val(a.city || "");
$("#sp-state").val(a.state || "AZ");
$("#sp-zip_code").val(a.pincode || "");
}
}
});
frappe.call({
method: "frappe.client.get_list",
args: {
doctype: "Contact",
filters: [["Dynamic Link", "link_name", "=", customer_name]],
fields: ["first_name", "last_name", "email_id", "phone", "mobile_no"],
limit_page_length: 1
},
callback: function(rc) {
if (rc.message && rc.message.length) {
var ct = rc.message[0];
if (!$("#sp-contact_name").val()) {
$("#sp-contact_name").val((ct.first_name || "") + " " + (ct.last_name || ""));
}
if (!$("#sp-contact_phone").val()) {
$("#sp-contact_phone").val(ct.phone || ct.mobile_no || "");
}
if (!$("#sp-contact_email").val()) {
$("#sp-contact_email").val(ct.email_id || "");
}
}
}
});
}
});
}
function clearCustomerFields() {
$("#sp-company_name, #sp-contact_name, #sp-contact_phone, #sp-contact_email, #sp-address_line, #sp-city, #sp-state, #sp-zip_code").val("");
$("#sp-state").val("AZ");
}
// ── Stage A: Load Pickups ──
function loadPickups() {
var dateFilter = $("#pickup-date-filter").val();
frappe.call({
method: "westech_r2.api.receiving_api.get_pickups",
args: { date: dateFilter || undefined },
callback: function(r) {
if (r.message) {
renderPickupTable(r.message.pickups || []);
renderCalendar(r.message.calendar || []);
}
}
});
}
function renderPickupTable(pickups) {
var tbody = $("#pickup-tbody");
$("#pickup-count-label").text("(" + pickups.length + ")");
if (!pickups.length) {
tbody.html('<tr><td colspan="14" class="text-center text-muted">No pickups found</td></tr>');
return;
}
tbody.html(pickups.map(function(p) {
var dt = p.pickup_date ? new Date(p.pickup_date + "T00:00:00") : null;
var dn = dt ? dayName(dt) : "";
return '<tr style="cursor:pointer" data-pickup="' + esc(p.name) + '">' +
'<td>' + esc(p.pickup_date || "") + '</td>' +
'<td>' + dn + '</td>' +
'<td>' + esc(p.pickup_type || "") + '</td>' +
'<td><strong>' + esc(p.company_name || p.customer_number || "") + '</strong></td>' +
'<td>' + esc(p.contact_name || "") + '</td>' +
'<td style="max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + esc((p.address_line || "") + (p.city ? ", " + p.city : "")) + '</td>' +
'<td class="text-right">' + (p.estimated_items || "—") + '</td>' +
'<td>' + esc(p.data_status || "—") + '</td>' +
'<td>' + esc(p.red_r2 || "—") + '</td>' +
'<td>' + esc(p.status || "") + '</td>' +
'<td style="max-width:100px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + esc(p.notes || "") + '</td>' +
'<td>' + esc(p.truck || "") + '</td>' +
'<td>' + (p.needs_aor ? "✓" : "") + '</td>' +
'<td>' + (p.needs_cod ? "✓" : "") + '</td></tr>';
}).join(""));
}
function renderCalendar(calendar) {
var el = $("#pickup-calendar");
if (!calendar.length) { el.html('<div class="text-muted text-center">No data</div>'); return; }
var todayStr = frappe.datetime.nowdate();
var html = '<div style="text-align:center;">';
calendar.forEach(function(d) {
var cls = "cal-day";
if (d.count > 0) cls += " has-pickups";
if (d.date === todayStr) cls += " today";
var label = d.date.substring(5);
html += '<div class="' + cls + '" data-date="' + d.date + '" title="' + d.count + ' pickup(s)">' + label + '</div>';
});
html += '</div>';
el.html(html);
}
$(document).on("click", ".cal-day.has-pickups", function() {
$("#pickup-date-filter").val($(this).data("date"));
loadPickups();
});
// ── Stage A: New Pickup ──
$("#btn-new-pickup").on("click", function() {
$("#new-pickup-form").show();
$("#sp-pickup_date").val(frappe.datetime.nowdate());
setupCustomerLink();
});
$("#btn-cancel-pickup").on("click", function() {
$("#new-pickup-form").hide();
});
$("#pickup-form").on("submit", function(e) {
e.preventDefault();
var customerVal = customer_control ? customer_control.get_value() : "";
if (!customerVal) { frappe.msgprint("Select a customer"); return; }
var doc = {
doctype: "Scheduled Pickup",
pickup_date: $("#sp-pickup_date").val(),
pickup_type: $("#sp-pickup_type").val(),
customer_number: customerVal,
company_name: $("#sp-company_name").val(),
contact_name: $("#sp-contact_name").val(),
contact_phone: $("#sp-contact_phone").val(),
contact_email: $("#sp-contact_email").val(),
address_line: $("#sp-address_line").val(),
city: $("#sp-city").val(),
state: $("#sp-state").val(),
zip_code: $("#sp-zip_code").val(),
estimated_items: parseInt($("#sp-estimated_items").val()) || 0,
estimated_weight: $("#sp-estimated_weight").val(),
load_contents: $("#sp-load_contents").val(),
data_status: $("#sp-data_status").val(),
red_r2: $("#sp-red_r2").val(),
needs_aor: $("#sp-needs_aor").is(":checked") ? 1 : 0,
needs_cod: $("#sp-needs_cod").is(":checked") ? 1 : 0,
notes: $("#sp-notes").val(),
status: "Scheduled"
};
frappe.call({
method: "frappe.client.insert",
args: { doc: doc },
callback: function(r) {
if (r.message) {
frappe.show_alert({ message: "Pickup scheduled", indicator: "green" });
$("#new-pickup-form").hide();
loadPickups();
}
}
});
});
$("#pickup-date-filter").on("change", loadPickups);
$("#btn-clear-date").on("click", function() {
$("#pickup-date-filter").val("");
loadPickups();
});
// ── Stage B: Routing ──
function loadRoutes() {
var date = $("#route-date").val() || frappe.datetime.nowdate();
$("#route-date").val(date);
frappe.call({
method: "westech_r2.api.receiving_api.get_pickups",
args: { date: date },
callback: function(r) {
if (r.message) renderRouteColumns(r.message.pickups || []);
}
});
}
function renderRouteColumns(pickups) {
var trucks = { "Truck 1": [], "Truck 2": [], "Truck 3": [], "Unassigned": [] };
pickups.forEach(function(p) {
var t = p.truck || "";
if (t && trucks[t]) trucks[t].push(p);
else trucks["Unassigned"].push(p);
});
["Truck 1", "Truck 2", "Truck 3"].forEach(function(t) {
var key = t.toLowerCase().replace(/ /g, "");
$("#" + key + "-count").text("(" + trucks[t].length + " stops)");
$("#" + key + "-stops").html(trucks[t].map(function(p, i) { return stopCard(p, i + 1); }).join(""));
});
$("#unassigned-count").text("(" + trucks["Unassigned"].length + ")");
$("#unassigned-stops").html(trucks["Unassigned"].map(function(p) { return stopCard(p, 0); }).join(""));
}
function stopCard(p, order) {
var h = '<div class="stop-card" data-pickup="' + esc(p.name) + '">';
if (order) h += '<div style="font-size:11px;color:#666;margin-bottom:2px">Stop #' + order + '</div>';
h += '<div class="stop-co">' + esc(p.company_name || p.customer_number || "Unknown") + '</div>';
h += '<div class="stop-addr">' + esc((p.address_line || "") + (p.city ? ", " + p.city : "")) + '</div>';
h += '<div class="stop-meta">';
if (p.estimated_items) h += '<span>' + p.estimated_items + ' items</span>';
if (p.data_status) h += '<span>' + esc(p.data_status) + '</span>';
if (p.red_r2) h += '<span>' + esc(p.red_r2) + '</span>';
if (p.needs_aor) h += '<span>AoR</span>';
if (p.needs_cod) h += '<span>CoD</span>';
h += '</div></div>';
return h;
}
$("#btn-load-routes").on("click", loadRoutes);
$("#btn-auto-route").on("click", function() {
var date = $("#route-date").val();
if (!date) { frappe.msgprint("Select a date first"); return; }
frappe.call({
method: "westech_r2.api.receiving_api.auto_route",
args: { date: date },
callback: function(r) {
if (r.message && r.message.success) {
frappe.show_alert({ message: "Routes optimized", indicator: "green" });
loadRoutes();
}
}
});
});
$("#btn-route-sheet").on("click", function() {
var date = $("#route-date").val() || frappe.datetime.nowdate();
window.open("/api/method/westech_r2.api.receiving_api.print_route_sheet?date=" + date, "_blank");
});
$("#btn-green-sheet").on("click", function() {
var date = $("#route-date").val() || frappe.datetime.nowdate();
window.open("/api/method/westech_r2.api.receiving_api.print_green_sheet?date=" + date, "_blank");
});
$("#btn-labels").on("click", function() {
var date = $("#route-date").val() || frappe.datetime.nowdate();
window.open("/api/method/westech_r2.api.receiving_api.print_labels?date=" + date, "_blank");
});
// ── Stage C: Check-in ──
var checkin_pickup_control = null;
var current_pickup_details = null;
function loadCheckins() {
frappe.call({
method: "westech_r2.api.receiving_api.get_checkins",
callback: function(r) {
if (r.message) renderCheckinTable(r.message.checkins || []);
}
});
}
function renderCheckinTable(checkins) {
var tbody = $("#checkin-tbody");
if (!checkins.length) {
tbody.html('<tr><td colspan="8" class="text-center text-muted">No check-ins yet</td></tr>');
return;
}
tbody.html(checkins.map(function(c) {
var palletInfo = (c.pallets || []).map(function(p) { return esc(p.pallet_number || p.name); }).join(", ");
return '<tr>' +
'<td>' + esc(c.incoming_date || "") + '</td>' +
'<td><strong>' + esc(c.customer_name || c.customer || "") + '</strong></td>' +
'<td>' + esc(c.name || "") + '</td>' +
'<td class="text-right">' + (c.pallet_count || 0) + '</td>' +
'<td class="text-right">' + (c.total_weight || "—") + '</td>' +
'<td style="max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + esc(c.data_status || "") + '</td>' +
'<td>' + esc(c.red_r2 || "—") + '</td>' +
'<td style="font-size:11px;color:#666;">' + palletInfo + '</td></tr>';
}).join(""));
}
$("#btn-new-checkin").on("click", function() {
$("#checkin-form").show();
$("#ci-received_date").val(frappe.datetime.nowdate());
$("#ci-pickup-details").hide();
current_pickup_details = null;
checkin_pickup_control = frappe.ui.form.make_control({
parent: $("#ci-pickup-control"),
df: {
fieldtype: "Link",
fieldname: "pickup_ref",
options: "Scheduled Pickup",
label: "Scheduled Pickup",
reqd: 1,
placeholder: "Search pickup...",
get_query: function() {
return {
filters: [
["Scheduled Pickup", "status", "in", ["Scheduled", "Routed", "In Progress"]]
]
};
},
onchange: function() {
var val = checkin_pickup_control ? checkin_pickup_control.get_value() : "";
if (val) loadPickupDetails(val);
else {
$("#ci-pickup-details").hide();
current_pickup_details = null;
}
}
},
only_input: true,
});
checkin_pickup_control.refresh();
$("#ci-pickup-control .control-input").css("margin", "0");
$("#ci-pickup-control .help-box").remove();
});
function loadPickupDetails(pickup_name) {
frappe.call({
method: "westech_r2.api.receiving_api.get_pickup_details",
args: { pickup_name: pickup_name },
callback: function(r) {
if (!r.message) return;
current_pickup_details = r.message;
var d = r.message;
$("#ci-pickup-details").show();
$("#ci-customer-info").text((d.company_name || d.customer_number || "Unknown") + (d.red_r2 ? " — " + d.red_r2 : ""));
$("#ci-address-info").text((d.address_line || "") + (d.city ? ", " + d.city : "") + (d.state ? ", " + d.state : "") + (d.zip_code ? " " + d.zip_code : ""));
$("#ci-contact-info").text((d.contact_name || "") + (d.contact_phone ? " • " + d.contact_phone : "") + (d.contact_email ? " • " + d.contact_email : ""));
// Special handling for RED/NIST
if (d.red_r2 && d.red_r2 !== "Neither" && d.red_r2 !== "") {
$("#ci-special-handling").show().html("<strong>⚠ " + esc(d.red_r2) + "</strong> — Special handling required" + (d.needs_aor ? " • AoR ✓" : "") + (d.needs_cod ? " • CoD ✓" : ""));
} else {
$("#ci-special-handling").hide();
}
// Notes
if (d.notes) {
$("#ci-pickup-notes").show().html("<strong>Notes:</strong> " + esc(d.notes));
} else {
$("#ci-pickup-notes").hide();
}
// Pre-fill check-in fields from pickup
$("#ci-actual_pallets").val(d.estimated_items || 1);
$("#ci-data_status").val(d.data_status || "");
$("#ci-red_r2").val(d.red_r2 || "");
}
});
}
$("#btn-cancel-checkin").on("click", function() {
$("#checkin-form").hide();
});
$("#checkin-form-inner").on("submit", function(e) {
e.preventDefault();
var pickupName = checkin_pickup_control ? checkin_pickup_control.get_value() : "";
if (!pickupName) { frappe.msgprint("Select a pickup"); return; }
var actualPallets = parseInt($("#ci-actual_pallets").val()) || 0;
if (actualPallets < 1) { frappe.msgprint("Enter at least 1 pallet"); return; }
frappe.confirm(
"Check in " + actualPallets + " pallet(s) for this load?",
function() {
frappe.call({
method: "westech_r2.api.receiving_api.checkin_load",
args: {
pickup_name: pickupName,
received_date: $("#ci-received_date").val(),
actual_pallets: actualPallets,
total_weight: $("#ci-total_weight").val(),
load_contents: $("#ci-load_contents").val(),
data_status: $("#ci-data_status").val(),
red_r2: $("#ci-red_r2").val()
},
callback: function(r) {
if (r.message && r.message.success) {
frappe.show_alert({
message: "Load checked in! Created " + r.message.pallets_created + " pallet(s) in Load " + r.message.load,
indicator: "green"
});
$("#checkin-form").hide();
loadCheckins();
loadPickups();
}
}
});
}
);
});
$("#btn-cor-report").on("click", function() {
window.open("/api/method/westech_r2.api.receiving_api.cor_report", "_blank");
});
// ── Helpers ──
function esc(s) { return s ? String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;") : ""; }
function dayName(d) { return ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][d.getDay()]; }
// ── Init ──
loadPickups();
loadCheckins();
};
@@ -0,0 +1,566 @@
frappe.pages['receiving'].on_page_load = function(wrapper) {
var page = frappe.ui.make_app_page({
parent: wrapper,
title: 'Receiving',
single_column: true
});
// Inline HTML — same pattern as intake.js
$(wrapper).find('.layout-main-section').html(`
<div class="receiving-station" style="padding: 20px;">
<div class="row">
<div class="col-md-12">
<h3 style="margin-top: 0; color: #2F5496;">🚛 Receiving</h3>
<p class="text-muted">Schedule pickups, manage routes, and check in loads.</p>
</div>
</div>
<ul class="nav nav-tabs" role="tablist" id="receiving-tabs">
<li role="presentation" class="active"><a href="#stage-a" role="tab" data-toggle="tab">📋 Stage A — Schedule Pickup</a></li>
<li role="presentation"><a href="#stage-b" role="tab" data-toggle="tab">🗺️ Stage B — Route & Dispatch</a></li>
<li role="presentation"><a href="#stage-c" role="tab" data-toggle="tab">⚖️ Stage C — Load Check-in</a></li>
</ul>
<div class="tab-content" style="padding-top: 20px;">
<div role="tabpanel" class="tab-pane active" id="stage-a">
<div class="row">
<div class="col-md-3">
<div class="panel panel-primary">
<div class="panel-heading">📅 Pickup Calendar — Next 30 Days</div>
<div class="panel-body" id="pickup-calendar"><div class="text-muted text-center">Loading...</div></div>
</div>
</div>
<div class="col-md-9">
<div class="panel panel-default">
<div class="panel-heading">
<div class="row">
<div class="col-md-6"><strong>Scheduled Pickups</strong><span id="pickup-count-label" class="text-muted" style="margin-left: 8px;"></span></div>
<div class="col-md-6 text-right">
<button class="btn btn-primary btn-sm" id="btn-new-pickup">+ New Pickup</button>
<input type="date" id="pickup-date-filter" class="form-control input-sm" style="display:inline-block;width:auto;vertical-align:middle;margin-left:8px;">
<button class="btn btn-default btn-sm" id="btn-clear-date" style="margin-left:4px;">Clear</button>
</div>
</div>
</div>
<div class="panel-body" style="padding: 0; overflow-x: auto;">
<table class="table table-striped table-hover" id="pickup-table" style="font-size: 13px; margin-bottom: 0;">
<thead><tr><th>Date</th><th>Weekday</th><th>Type</th><th>Customer</th><th>Contact</th><th>Address</th><th>Est. Items</th><th>Data</th><th>RED/R2</th><th>Status</th><th>Notes</th><th>Truck</th><th>AoR</th><th>CoD</th></tr></thead>
<tbody id="pickup-tbody"><tr><td colspan="14" class="text-center text-muted">Loading...</td></tr></tbody>
</table>
</div>
</div>
</div>
</div>
<div id="new-pickup-form" style="display:none; margin-top: 16px;">
<div class="panel panel-primary">
<div class="panel-heading">+ New Scheduled Pickup</div>
<div class="panel-body">
<form id="pickup-form">
<div class="row">
<div class="col-md-4">
<h5 style="color:#6f42c1;">📅 Pickup Info</h5>
<div class="form-group"><label>Pickup Date <span class="text-danger">*</span></label><input type="date" id="sp-pickup_date" class="form-control" required></div>
<div class="form-group"><label>Type <span class="text-danger">*</span></label><select id="sp-pickup_type" class="form-control" required><option value="Pickup">Pickup</option><option value="Drop-off">Drop-off</option></select></div>
<div class="form-group"><label>Customer <span class="text-danger">*</span></label><div id="sp-customer-control"></div></div>
<div class="form-group"><label>Company Name</label><input type="text" id="sp-company_name" class="form-control" readonly style="background:#f8f9fa;"></div>
<div class="form-group"><label>Contact Name</label><input type="text" id="sp-contact_name" class="form-control"></div>
<div class="form-group"><label>Contact Phone</label><input type="text" id="sp-contact_phone" class="form-control"></div>
<div class="form-group"><label>Contact Email</label><input type="email" id="sp-contact_email" class="form-control"></div>
</div>
<div class="col-md-4">
<h5 style="color:#6f42c1;">📍 Address</h5>
<div class="form-group"><label>Street Address</label><input type="text" id="sp-address_line" class="form-control"></div>
<div class="form-group"><label>City</label><input type="text" id="sp-city" class="form-control"></div>
<div class="form-group"><label>State</label><input type="text" id="sp-state" class="form-control" value="AZ"></div>
<div class="form-group"><label>ZIP</label><input type="text" id="sp-zip_code" class="form-control"></div>
<div class="form-group"><label>Hours of Operation</label><input type="text" id="sp-hours_of_operation" class="form-control" placeholder="e.g. Mon-Fri 8am-5pm"></div>
</div>
<div class="col-md-4">
<h5 style="color:#6f42c1;">📦 Load Info</h5>
<div class="form-group"><label>Estimated Items</label><input type="number" id="sp-estimated_items" class="form-control"></div>
<div class="form-group"><label>Estimated Weight</label><input type="text" id="sp-estimated_weight" class="form-control"></div>
<div class="form-group"><label>Load Contents</label><input type="text" id="sp-load_contents" class="form-control" placeholder="Wire, Monitors, Laptops..."></div>
<div class="form-group"><label>Data Status</label><select id="sp-data_status" class="form-control"><option value="">—</option><option value="D0">D0</option><option value="D1">D1</option><option value="ND1">ND1</option><option value="ND2">ND2</option><option value="ND3">ND3</option><option value="ND4">ND4</option></select></div>
<div class="form-group"><label>RED / R2</label><select id="sp-red_r2" class="form-control"><option value="">—</option><option value="RED">RED</option><option value="R2">R2</option><option value="Both">Both</option><option value="Neither">Neither</option></select></div>
<div class="form-group"><div class="checkbox"><label><input type="checkbox" id="sp-needs_aor"> <strong>Needs AoR</strong></label></div><div class="checkbox"><label><input type="checkbox" id="sp-needs_cod"> <strong>Needs CoD</strong></label></div></div>
<div class="form-group"><label>Notes</label><textarea id="sp-notes" class="form-control" rows="2"></textarea></div>
<div class="form-group"><label>Legacy Notes</label><textarea id="sp-legacy_notes" class="form-control" rows="2" style="background:#fafafa;" readonly></textarea></div>
</div>
</div>
<div class="row" style="margin-top: 16px;"><div class="col-md-12"><button type="submit" class="btn btn-primary btn-lg">Save Pickup</button><button type="button" class="btn btn-default btn-lg" id="btn-cancel-pickup">Cancel</button></div></div>
</form>
</div>
</div>
</div>
</div>
<div role="tabpanel" class="tab-pane" id="stage-b">
<div class="row" style="margin-bottom: 16px;"><div class="col-md-12"><div class="btn-group"><input type="date" id="route-date" class="form-control" style="display:inline-block;width:auto;"><button class="btn btn-primary" id="btn-load-routes">Load Pickups</button><button class="btn btn-primary" id="btn-auto-route">🧮 Auto-Route</button><button class="btn btn-success" id="btn-route-sheet">🖨️ Route Sheet</button><button class="btn btn-success" id="btn-green-sheet">📄 Green Sheet</button><button class="btn btn-success" id="btn-labels">🏷️ Labels</button></div></div></div>
<div class="row" id="route-columns">
<div class="col-md-4"><div class="panel panel-default truck-column" data-truck="Truck 1"><div class="panel-heading">🚛 Truck 1 <span id="truck1-count" class="text-muted"></span></div><div class="panel-body truck-stops" id="truck1-stops"></div></div></div>
<div class="col-md-4"><div class="panel panel-default truck-column" data-truck="Truck 2"><div class="panel-heading">🚛 Truck 2 <span id="truck2-count" class="text-muted"></span></div><div class="panel-body truck-stops" id="truck2-stops"></div></div></div>
<div class="col-md-4"><div class="panel panel-default truck-column" data-truck="Truck 3"><div class="panel-heading">🚛 Truck 3 <span id="truck3-count" class="text-muted"></span></div><div class="panel-body truck-stops" id="truck3-stops"></div></div></div>
</div>
<div class="row" style="margin-top: 16px;"><div class="col-md-12"><div class="panel panel-default truck-column" data-truck=""><div class="panel-heading">📋 Unassigned <span id="unassigned-count" class="text-muted"></span></div><div class="panel-body truck-stops" id="unassigned-stops"></div></div></div></div>
</div>
<div role="tabpanel" class="tab-pane" id="stage-c">
<div class="row" style="margin-bottom: 16px;"><div class="col-md-12"><button class="btn btn-primary" id="btn-new-checkin">+ Check In Load</button><button class="btn btn-success" id="btn-cor-report">📋 CoR Report</button></div></div>
<div class="panel panel-default">
<div class="panel-heading">Recent Check-ins</div>
<div class="panel-body" style="padding: 0; overflow-x: auto;">
<table class="table table-striped table-hover" id="checkin-table" style="font-size: 13px; margin-bottom: 0;">
<thead><tr><th>Date</th><th>Customer</th><th>Type</th><th class="text-right">Actual Pallets</th><th class="text-right">Actual Weight</th><th>Load Contents</th><th>Data Status</th><th>RED/R2</th><th>Status</th></tr></thead>
<tbody id="checkin-tbody"><tr><td colspan="9" class="text-center text-muted">Loading...</td></tr></tbody>
</table>
</div>
</div>
<div id="checkin-form" style="display:none; margin-top: 16px;">
<div class="panel panel-primary">
<div class="panel-heading">+ Load Check-in</div>
<div class="panel-body">
<form id="checkin-form-inner">
<div class="row">
<div class="col-md-4"><div class="form-group"><label>Scheduled Pickup <span class="text-danger">*</span></label><div id="ci-pickup-control"></div></div></div>
<div class="col-md-4"><div class="form-group"><label>Received Date <span class="text-danger">*</span></label><input type="date" id="ci-received_date" class="form-control" required></div></div>
<div class="col-md-4"><div class="form-group"><label>Actual # of Pallets/Gaylords</label><input type="number" id="ci-actual_pallets" class="form-control"></div></div>
</div>
<div class="row">
<div class="col-md-4"><div class="form-group"><label>Actual Weight (lbs)</label><input type="text" id="ci-actual_weight" class="form-control"></div></div>
<div class="col-md-8"><div class="form-group"><label>Load Contents</label><textarea id="ci-load_contents" class="form-control" rows="2" placeholder="Wire, Monitors, Laptops, MRI machine..."></textarea></div></div>
</div>
<div class="row" style="margin-top: 8px;"><div class="col-md-12"><button type="submit" class="btn btn-primary btn-lg">Check In</button><button type="button" class="btn btn-default btn-lg" id="btn-cancel-checkin">Cancel</button></div></div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
.truck-stops { min-height: 60px; }
.stop-card { background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 6px; padding: 10px 12px; margin: 6px 0; cursor: grab; font-size: 13px; }
.stop-card:hover { border-color: #2F5496; }
.stop-card .stop-co { font-weight: 700; color: #2F5496; }
.stop-card .stop-addr { color: #666; font-size: 12px; margin-top: 2px; }
.stop-card .stop-meta { display: flex; gap: 8px; margin-top: 4px; font-size: 11px; }
.stop-card .stop-meta span { background: #D6E4F0; color: #2F5496; padding: 1px 6px; border-radius: 3px; }
.stop-card.dragging { opacity: 0.5; }
.truck-column { min-height: 200px; }
#pickup-calendar { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; font-size: 12px; }
.cal-day { text-align: center; padding: 6px 4px; border-radius: 6px; }
.cal-day.has-pickups { background: #D6E4F0; cursor: pointer; }
.cal-day.today { background: #2F5496; color: #fff; }
.cal-day .day-num { font-weight: 600; }
.cal-day .day-count { font-size: 10px; font-weight: 700; }
</style>
`);
// ── Stage Tabs ──
$("#receiving-tabs a").on("click", function(e) {
e.preventDefault();
$(this).tab("show");
var stage = $(this).attr("href").replace("#stage-", "");
if (stage === "a") loadPickups();
if (stage === "b") loadRoutes();
if (stage === "c") loadCheckins();
});
// ── Stage A: Link Controls ──
var customer_control = null;
function setupCustomerLink() {
customer_control = frappe.ui.form.make_control({
parent: $("#sp-customer-control"),
df: {
fieldtype: "Link",
fieldname: "customer_number",
options: "Customer",
label: "Customer",
reqd: 1,
placeholder: "Search customer...",
onchange: function() {
var val = customer_control.get_value();
if (val) fetchCustomerDetails(val);
else clearCustomerFields();
}
},
only_input: true,
});
customer_control.refresh();
$("#sp-customer-control .control-input").css("margin", "0");
$("#sp-customer-control .help-box").remove();
}
function fetchCustomerDetails(customer_name) {
frappe.call({
method: "frappe.client.get",
args: { doctype: "Customer", name: customer_name },
callback: function(r) {
if (!r.message) return;
var c = r.message;
$("#sp-company_name").val(c.customer_name || "");
$("#sp-contact_name").val(c.contact_name || "");
$("#sp-contact_phone").val(c.contact_phone || "");
$("#sp-contact_email").val(c.contact_email || "");
$("#sp-legacy_notes").val(c.legacy_notes || "");
$("#sp-hours_of_operation").val(c.hours_of_operation || "");
frappe.call({
method: "frappe.client.get_list",
args: {
doctype: "Address",
filters: [["Dynamic Link", "link_name", "=", customer_name]],
fields: ["address_line1", "city", "state", "pincode"],
limit_page_length: 1
},
callback: function(ra) {
if (ra.message && ra.message.length) {
var a = ra.message[0];
$("#sp-address_line").val(a.address_line1 || "");
$("#sp-city").val(a.city || "");
$("#sp-state").val(a.state || "AZ");
$("#sp-zip_code").val(a.pincode || "");
}
}
});
frappe.call({
method: "frappe.client.get_list",
args: {
doctype: "Contact",
filters: [["Dynamic Link", "link_name", "=", customer_name]],
fields: ["first_name", "last_name", "email_id", "phone", "mobile_no"],
limit_page_length: 1
},
callback: function(rc) {
if (rc.message && rc.message.length) {
var ct = rc.message[0];
if (!$("#sp-contact_name").val()) {
$("#sp-contact_name").val((ct.first_name || "") + " " + (ct.last_name || ""));
}
if (!$("#sp-contact_phone").val()) {
$("#sp-contact_phone").val(ct.phone || ct.mobile_no || "");
}
if (!$("#sp-contact_email").val()) {
$("#sp-contact_email").val(ct.email_id || "");
}
}
}
});
}
});
}
function clearCustomerFields() {
$("#sp-company_name, #sp-contact_name, #sp-contact_phone, #sp-contact_email, #sp-address_line, #sp-city, #sp-state, #sp-zip_code, #sp-legacy_notes, #sp-hours_of_operation").val("");
$("#sp-state").val("AZ");
}
// ── Stage A: Load Pickups ──
function loadPickups() {
var dateFilter = $("#pickup-date-filter").val();
frappe.call({
method: "westech_r2.api.receiving_api.get_pickups",
args: { date: dateFilter },
callback: function(r) {
if (r.message) {
renderPickupTable(r.message.pickups || []);
renderCalendar(r.message.calendar || []);
// weekly chart removed
$("#pickup-count-label").text((r.message.pickups || []).length + " pickups");
}
}
});
}
function renderPickupTable(pickups) {
var tbody = $("#pickup-tbody");
if (!pickups.length) {
tbody.html('<tr><td colspan="14" class="text-center text-muted">No scheduled pickups</td></tr>');
return;
}
var statusColors = { "Scheduled": "#2196F3", "Routed": "#009688", "In Progress": "#FF9800", "Complete": "#4CAF50", "Cancelled": "#F44336" };
var h = "";
pickups.forEach(function(p) {
var st = p.status || "Scheduled";
var sc = statusColors[st] || "#999";
var weekday = p.pickup_date ? dayName(new Date(p.pickup_date + "T12:00:00")) : "";
var typeBadge = p.pickup_type === "Drop-off"
? '<span class="badge" style="background:#E3F2FD;color:#1565C0">Drop-off</span>'
: '<span class="badge" style="background:#FFF3E0;color:#E65100">Pickup</span>';
h += '<tr style="cursor:pointer" onclick=\"window.open(\'/app/scheduled-pickup/\' + encodeURIComponent(p.name) + \'\', \'_blank\')\">';
h += '<td>' + esc(p.pickup_date || "") + '</td>';
h += '<td class="text-muted">' + weekday + '</td>';
h += '<td>' + typeBadge + '</td>';
h += '<td><strong>' + esc(p.company_name || p.customer_number || "") + '</strong></td>';
h += '<td style="font-size:12px">' + esc((p.contact_name || "") + (p.contact_phone ? " • " + p.contact_phone : "")) + '</td>';
h += '<td style="font-size:12px">' + esc((p.address_line || "") + (p.city ? ", " + p.city : "")) + '</td>';
h += '<td class="text-right">' + (p.estimated_items || "—") + '</td>';
h += '<td>' + esc(p.data_status || "—") + '</td>';
h += '<td>' + esc(p.red_r2 || "—") + '</td>';
h += '<td><span class="badge" style="background:' + sc + '22;color:' + sc + '">' + esc(st) + '</span></td>';
h += '<td style="max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + esc(p.notes || "") + '</td>';
h += '<td>' + esc(p.truck || "—") + '</td>';
h += '<td class="text-center">' + (p.needs_aor ? "✓" : "") + '</td>';
h += '<td class="text-center">' + (p.needs_cod ? "✓" : "") + '</td>';
h += '</tr>';
});
tbody.html(h);
}
function renderCalendar(days) {
var el = $("#pickup-calendar");
if (!days || !days.length) { el.html('<div class="text-muted text-center">No upcoming pickups</div>'); return; }
var today = frappe.datetime.nowdate();
var h = '<div style="display:grid;grid-template-columns:repeat(7,1fr);gap:4px;font-size:12px">';
["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"].forEach(function(d) {
h += '<div style="text-align:center;font-weight:600;color:#666;padding:4px">' + d + '</div>';
});
var first = new Date(days[0].date + "T12:00:00");
for (var i = 0; i < first.getDay(); i++) h += '<div></div>';
days.forEach(function(d) {
var isToday = d.date === today;
var hasCount = d.count > 0;
var bg = isToday ? "cal-day today" : (hasCount ? "cal-day has-pickups" : "cal-day");
var onclick = hasCount ? "onclick=$('#pickup-date-filter').val('" + d.date + "');loadPickups();" : "";
h += '<div class="' + bg + '" ' + onclick + '>';
h += '<div style="font-weight:' + (isToday ? "700" : "400") + '">' + d.date.split("-")[2] + '</div>';
if (hasCount) h += '<div class="day-count">' + d.count + '</div>';
h += '</div>';
});
h += '</div>';
el.html(h);
}
// ── Stage A: New Pickup ──
$("#btn-new-pickup").on("click", function() {
$("#new-pickup-form").show();
$("#sp-pickup_date").val(frappe.datetime.nowdate());
setupCustomerLink();
});
$("#btn-cancel-pickup").on("click", function() {
$("#new-pickup-form").hide();
if (customer_control) customer_control.set_value("");
});
$("#pickup-form").on("submit", function(e) {
e.preventDefault();
var doc = {
doctype: "Scheduled Pickup",
pickup_date: $("#sp-pickup_date").val(),
pickup_type: $("#sp-pickup_type").val(),
customer_number: customer_control ? customer_control.get_value() : "",
company_name: $("#sp-company_name").val(),
contact_name: $("#sp-contact_name").val(),
contact_phone: $("#sp-contact_phone").val(),
contact_email: $("#sp-contact_email").val(),
address_line: $("#sp-address_line").val(),
city: $("#sp-city").val(),
state: $("#sp-state").val(),
zip_code: $("#sp-zip_code").val(),
estimated_items: parseInt($("#sp-estimated_items").val()) || 0,
estimated_weight: $("#sp-estimated_weight").val(),
load_contents: $("#sp-load_contents").val(),
data_status: $("#sp-data_status").val(),
red_r2: $("#sp-red_r2").val(),
needs_aor: $("#sp-needs_aor").is(":checked") ? 1 : 0,
needs_cod: $("#sp-needs_cod").is(":checked") ? 1 : 0,
notes: $("#sp-notes").val(),
legacy_notes: $("#sp-legacy_notes").val(),
status: "Scheduled"
};
frappe.call({
method: "frappe.client.insert",
args: { doc: doc },
callback: function(r) {
if (r.message) {
frappe.show_alert({ message: "Pickup scheduled", indicator: "green" });
$("#new-pickup-form").hide();
loadPickups();
}
}
});
});
$("#pickup-date-filter").on("change", loadPickups);
$("#btn-clear-date").on("click", function() {
$("#pickup-date-filter").val("");
loadPickups();
});
// ── Stage B: Routing ──
function loadRoutes() {
var date = $("#route-date").val() || frappe.datetime.nowdate();
$("#route-date").val(date);
frappe.call({
method: "westech_r2.api.receiving_api.get_pickups",
args: { date: date },
callback: function(r) {
if (r.message) renderRouteColumns(r.message.pickups || []);
}
});
}
function renderRouteColumns(pickups) {
var trucks = { "Truck 1": [], "Truck 2": [], "Truck 3": [], "Unassigned": [] };
pickups.forEach(function(p) {
var t = p.truck || "";
if (t && trucks[t]) trucks[t].push(p);
else trucks["Unassigned"].push(p);
});
["Truck 1", "Truck 2", "Truck 3"].forEach(function(t) {
var key = t.toLowerCase().replace(/ /g, "");
$("#" + key + "-count").text("(" + trucks[t].length + " stops)");
$("#" + key + "-stops").html(trucks[t].map(function(p, i) { return stopCard(p, i + 1); }).join(""));
});
$("#unassigned-count").text("(" + trucks["Unassigned"].length + " stops)");
$("#unassigned-stops").html(trucks["Unassigned"].map(function(p) { return stopCard(p, 0); }).join(""));
}
function stopCard(p, order) {
var h = '<div class="stop-card" data-pickup="' + esc(p.name) + '">';
if (order) h += '<div style="font-size:11px;color:#666;margin-bottom:2px">Stop #' + order + '</div>';
h += '<div class="stop-co">' + esc(p.company_name || p.customer_number || "Unknown") + '</div>';
h += '<div class="stop-addr">' + esc((p.address_line || "") + (p.city ? ", " + p.city : "")) + '</div>';
h += '<div class="stop-meta">';
if (p.estimated_items) h += '<span>' + p.estimated_items + ' items</span>';
if (p.data_status) h += '<span>' + esc(p.data_status) + '</span>';
if (p.red_r2) h += '<span>' + esc(p.red_r2) + '</span>';
if (p.needs_aor) h += '<span>AoR</span>';
if (p.needs_cod) h += '<span>CoD</span>';
h += '</div></div>';
return h;
}
$("#btn-load-routes").on("click", loadRoutes);
$("#btn-auto-route").on("click", function() {
var date = $("#route-date").val();
if (!date) { frappe.msgprint("Select a date first"); return; }
frappe.call({
method: "westech_r2.api.receiving_api.auto_route",
args: { date: date },
callback: function(r) {
if (r.message && r.message.success) {
frappe.show_alert({ message: "Routes optimized", indicator: "green" });
loadRoutes();
}
}
});
});
$("#btn-route-sheet").on("click", function() {
var date = $("#route-date").val() || frappe.datetime.nowdate();
window.open("/api/method/westech_r2.api.receiving_api.print_route_sheet?date=" + date, "_blank");
});
$("#btn-green-sheet").on("click", function() {
var date = $("#route-date").val() || frappe.datetime.nowdate();
window.open("/api/method/westech_r2.api.receiving_api.print_green_sheet?date=" + date, "_blank");
});
$("#btn-labels").on("click", function() {
var date = $("#route-date").val() || frappe.datetime.nowdate();
window.open("/api/method/westech_r2.api.receiving_api.print_labels?date=" + date, "_blank");
});
// ── Stage C: Check-in ──
var checkin_pickup_control = null;
function loadCheckins() {
frappe.call({
method: "westech_r2.api.receiving_api.get_checkins",
callback: function(r) {
if (r.message) renderCheckinTable(r.message.checkins || []);
}
});
}
function renderCheckinTable(checkins) {
var tbody = $("#checkin-tbody");
if (!checkins.length) {
tbody.html('<tr><td colspan="9" class="text-center text-muted">No check-ins yet</td></tr>');
return;
}
tbody.html(checkins.map(function(c) {
return '<tr><td>' + esc(c.pickup_date || "") + '</td>' +
'<td><strong>' + esc(c.company_name || "") + '</strong></td>' +
'<td>' + esc(c.pickup_type || "") + '</td>' +
'<td class="text-right">' + (c.estimated_items || "—") + '</td>' +
'<td class="text-right">' + (c.estimated_weight || "—") + '</td>' +
'<td style="max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + esc(c.load_contents || "") + '</td>' +
'<td>' + esc(c.data_status || "—") + '</td>' +
'<td>' + esc(c.red_r2 || "—") + '</td>' +
'<td>' + esc(c.status || "") + '</td></tr>';
}).join(""));
}
$("#btn-new-checkin").on("click", function() {
$("#checkin-form").show();
$("#ci-received_date").val(frappe.datetime.nowdate());
checkin_pickup_control = frappe.ui.form.make_control({
parent: $("#ci-pickup-control"),
df: {
fieldtype: "Link",
fieldname: "pickup_ref",
options: "Scheduled Pickup",
label: "Scheduled Pickup",
reqd: 1,
placeholder: "Search pickup...",
get_query: function() {
return {
filters: [
["Scheduled Pickup", "status", "in", ["Scheduled", "Routed", "In Progress"]]
]
};
}
},
only_input: true,
});
checkin_pickup_control.refresh();
$("#ci-pickup-control .control-input").css("margin", "0");
$("#ci-pickup-control .help-box").remove();
});
$("#btn-cancel-checkin").on("click", function() {
$("#checkin-form").hide();
});
$("#checkin-form-inner").on("submit", function(e) {
e.preventDefault();
var pickupName = checkin_pickup_control ? checkin_pickup_control.get_value() : "";
if (!pickupName) { frappe.msgprint("Select a pickup"); return; }
var update = {};
update.status = "Complete";
if ($("#ci-actual_pallets").val()) update.estimated_items = parseInt($("#ci-actual_pallets").val());
if ($("#ci-actual_weight").val()) update.estimated_weight = $("#ci-actual_weight").val();
if ($("#ci-load_contents").val()) update.load_contents = $("#ci-load_contents").val();
frappe.call({
method: "frappe.client.set_value",
args: {
doctype: "Scheduled Pickup",
name: pickupName,
fieldname: update
},
callback: function(r) {
if (r.message) {
frappe.show_alert({ message: "Load checked in", indicator: "green" });
$("#checkin-form").hide();
loadCheckins();
}
}
});
});
$("#btn-cor-report").on("click", function() {
window.open("/api/method/westech_r2.api.receiving_api.cor_report", "_blank");
});
// ── Helpers ──
function esc(s) { return s ? String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;") : ""; }
function dayName(d) { return ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][d.getDay()]; }
// ── Init ──
loadPickups();
};
@@ -0,0 +1,637 @@
frappe.pages['receiving'].on_page_load = function(wrapper) {
var page = frappe.ui.make_app_page({
parent: wrapper,
title: 'Receiving',
single_column: true
});
// Inline HTML
$(wrapper).find('.layout-main-section').html(`
<div class="receiving-station" style="padding: 20px;">
<div class="row">
<div class="col-md-12">
<h3 style="margin-top: 0; color: #2F5496;">🚛 Receiving</h3>
<p class="text-muted">Schedule pickups, manage routes, and check in loads.</p>
</div>
</div>
<ul class="nav nav-tabs" role="tablist" id="receiving-tabs">
<li role="presentation" class="active"><button class="btn btn-link" data-target="#stage-a" style="padding:10px 15px;border:none;font-size:14px;cursor:pointer">📋 Stage A — Schedule Pickup</button></li>
<li role="presentation"><a data-target="#stage-b" role="tab" data-toggle="tab" style="cursor:pointer">🗺️ Stage B — Route & Dispatch</a></li>
<li role="presentation"><a data-target="#stage-c" role="tab" data-toggle="tab" style="cursor:pointer">⚖️ Stage C — Load Check-in</a></li>
</ul>
<div class="tab-content" style="padding-top: 20px;">
<div role="tabpanel" class="tab-pane active" id="stage-a">
<div class="row">
<div class="col-md-3">
<div class="panel panel-primary">
<div class="panel-heading">📅 Pickup Calendar — Next 30 Days</div>
<div class="panel-body" id="pickup-calendar"><div class="text-muted text-center">Loading...</div></div>
</div>
</div>
<div class="col-md-9">
<div class="panel panel-default">
<div class="panel-heading">
<div class="row">
<div class="col-md-6"><strong>Scheduled Pickups</strong><span id="pickup-count-label" class="text-muted" style="margin-left: 8px;"></span></div>
<div class="col-md-6 text-right">
<button class="btn btn-primary btn-sm" id="btn-new-pickup">+ New Pickup</button>
<input type="date" id="pickup-date-filter" class="form-control input-sm" style="display:inline-block;width:auto;vertical-align:middle;margin-left:8px;">
<button class="btn btn-default btn-sm" id="btn-clear-date" style="margin-left:4px;">Clear</button>
</div>
</div>
</div>
<div class="panel-body" style="padding: 0; overflow-x: auto;">
<table class="table table-striped table-hover" id="pickup-table" style="font-size: 13px; margin-bottom: 0;">
<thead><tr><th>Date</th><th>Weekday</th><th>Type</th><th>Customer</th><th>Contact</th><th>Address</th><th>Est. Items</th><th>Data</th><th>RED/R2</th><th>Status</th><th>Notes</th><th>Truck</th><th>AoR</th><th>CoD</th></tr></thead>
<tbody id="pickup-tbody"><tr><td colspan="14" class="text-center text-muted">Loading...</td></tr></tbody>
</table>
</div>
</div>
</div>
</div>
<div id="new-pickup-form" style="display:none; margin-top: 16px;">
<div class="panel panel-primary">
<div class="panel-heading">+ New Scheduled Pickup</div>
<div class="panel-body">
<form id="pickup-form">
<div class="row">
<div class="col-md-4">
<h5 style="color:#6f42c1;">📅 Pickup Info</h5>
<div class="form-group"><label>Pickup Date <span class="text-danger">*</span></label><input type="date" id="sp-pickup_date" class="form-control" required></div>
<div class="form-group"><label>Type <span class="text-danger">*</span></label><select id="sp-pickup_type" class="form-control" required><option value="Pickup">Pickup</option><option value="Drop-off">Drop-off</option></select></div>
<div class="form-group">
<label>Customer <span class="text-danger">*</span></label>
<div class="input-group">
<div id="sp-customer-control" style="flex:1;"></div>
<span class="input-group-btn"><button class="btn btn-success" id="btn-new-customer" type="button" title="New Customer">+</button></span>
</div>
</div>
<div class="form-group"><label>Company Name</label><input type="text" id="sp-company_name" class="form-control"></div>
<div class="form-group"><label>Contact Name</label><input type="text" id="sp-contact_name" class="form-control"></div>
<div class="form-group"><label>Contact Phone</label><input type="text" id="sp-contact_phone" class="form-control"></div>
<div class="form-group"><label>Contact Email</label><input type="email" id="sp-contact_email" class="form-control"></div>
</div>
<div class="col-md-4">
<h5 style="color:#6f42c1;">📍 Address</h5>
<div class="form-group"><label>Street Address</label><input type="text" id="sp-address_line" class="form-control"></div>
<div class="form-group"><label>City</label><input type="text" id="sp-city" class="form-control"></div>
<div class="form-group"><label>State</label><input type="text" id="sp-state" class="form-control" value="AZ"></div>
<div class="form-group"><label>ZIP</label><input type="text" id="sp-zip_code" class="form-control"></div>
<div class="form-group"><label>Hours of Operation</label><input type="text" id="sp-hours_of_operation" class="form-control" placeholder="e.g. Mon-Fri 8am-5pm"></div>
</div>
<div class="col-md-4">
<h5 style="color:#6f42c1;">📦 Load Info</h5>
<div class="form-group"><label>Estimated Items</label><input type="number" id="sp-estimated_items" class="form-control"></div>
<div class="form-group"><label>Estimated Weight</label><input type="text" id="sp-estimated_weight" class="form-control"></div>
<div class="form-group"><label>Load Contents</label><input type="text" id="sp-load_contents" class="form-control" placeholder="Wire, Monitors, Laptops..."></div>
<div class="form-group"><label>Data Status</label><select id="sp-data_status" class="form-control"><option value="">—</option><option value="D0">D0</option><option value="D1">D1</option><option value="ND1">ND1</option><option value="ND2">ND2</option><option value="ND3">ND3</option><option value="ND4">ND4</option></select></div>
<div class="form-group"><label>RED / R2</label><select id="sp-red_r2" class="form-control"><option value="">—</option><option value="RED">RED</option><option value="NIST">NIST</option><option value="Red+NIST">Red+NIST</option><option value="R2">R2</option><option value="Both">Both</option><option value="Neither">Neither</option></select></div>
<div class="form-group"><div class="checkbox"><label><input type="checkbox" id="sp-needs_aor"> <strong>Needs AoR</strong></label></div><div class="checkbox"><label><input type="checkbox" id="sp-needs_cod"> <strong>Needs CoD</strong></label></div></div>
<div class="form-group"><label>Notes / Special Handling</label><textarea id="sp-notes" class="form-control" rows="3"></textarea></div>
</div>
</div>
<div class="row" style="margin-top: 16px;"><div class="col-md-12"><button type="submit" class="btn btn-primary btn-lg">Save Pickup</button><button type="button" class="btn btn-default btn-lg" id="btn-cancel-pickup">Cancel</button></div></div>
</form>
</div>
</div>
</div>
</div>
<div role="tabpanel" class="tab-pane" id="stage-b">
<div class="row" style="margin-bottom: 16px;"><div class="col-md-12"><div class="btn-group"><input type="date" id="route-date" class="form-control" style="display:inline-block;width:auto;"><button class="btn btn-primary" id="btn-load-routes">Load Pickups</button><button class="btn btn-primary" id="btn-auto-route">🧮 Auto-Route</button><button class="btn btn-success" id="btn-route-sheet">🖨️ Route Sheet</button><button class="btn btn-success" id="btn-green-sheet">📄 Green Sheet</button><button class="btn btn-success" id="btn-labels">🏷️ Labels</button></div></div></div>
<div class="row" id="route-columns">
<div class="col-md-4"><div class="panel panel-default truck-column" data-truck="Truck 1"><div class="panel-heading">🚛 Truck 1 <span id="truck1-count" class="text-muted"></span></div><div class="panel-body truck-stops" id="truck1-stops"></div></div></div>
<div class="col-md-4"><div class="panel panel-default truck-column" data-truck="Truck 2"><div class="panel-heading">🚛 Truck 2 <span id="truck2-count" class="text-muted"></span></div><div class="panel-body truck-stops" id="truck2-stops"></div></div></div>
<div class="col-md-4"><div class="panel panel-default truck-column" data-truck="Truck 3"><div class="panel-heading">🚛 Truck 3 <span id="truck3-count" class="text-muted"></span></div><div class="panel-body truck-stops" id="truck3-stops"></div></div></div>
</div>
<div class="row" style="margin-top: 16px;"><div class="col-md-12"><div class="panel panel-default truck-column" data-truck=""><div class="panel-heading">📋 Unassigned <span id="unassigned-count" class="text-muted"></span></div><div class="panel-body truck-stops" id="unassigned-stops"></div></div></div></div>
</div>
<div role="tabpanel" class="tab-pane" id="stage-c">
<div class="row" style="margin-bottom: 16px;"><div class="col-md-12"><button class="btn btn-primary" id="btn-new-checkin">+ Check In Load</button><button class="btn btn-success" id="btn-cor-report">📋 CoR Report</button></div></div>
<div class="panel panel-default">
<div class="panel-heading">Recent Check-ins</div>
<div class="panel-body" style="padding: 0; overflow-x: auto;">
<table class="table table-striped table-hover" id="checkin-table" style="font-size: 13px; margin-bottom: 0;">
<thead><tr><th>Date</th><th>Customer</th><th>Load #</th><th class="text-right">Pallets</th><th class="text-right">Weight</th><th>Contents</th><th>Data Status</th><th>RED/R2</th></tr></thead>
<tbody id="checkin-tbody"><tr><td colspan="8" class="text-center text-muted">Loading...</td></tr></tbody>
</table>
</div>
</div>
<div id="checkin-form" style="display:none; margin-top: 16px;">
<div class="panel panel-primary">
<div class="panel-heading">+ Load Check-in</div>
<div class="panel-body">
<form id="checkin-form-inner">
<div class="row">
<div class="col-md-4">
<h5 style="color:#6f42c1;">📅 Pickup Reference</h5>
<div class="form-group"><label>Scheduled Pickup <span class="text-danger">*</span></label><div id="ci-pickup-control"></div></div>
<div id="ci-pickup-details" style="display:none; background:#f8f9fa; border:1px solid #ddd; border-radius:4px; padding:10px; margin-bottom:10px;">
<div id="ci-customer-info" style="font-weight:700;"></div>
<div id="ci-address-info" style="font-size:12px; color:#666;"></div>
<div id="ci-contact-info" style="font-size:12px; color:#666;"></div>
<div id="ci-special-handling" style="display:none; margin-top:8px; padding:6px; background:#FFCDD2; border:1px solid #C62828; border-radius:4px; font-size:12px;"></div>
<div id="ci-pickup-notes" style="display:none; margin-top:8px; padding:6px; background:#F5F5F5; border:1px solid #999; border-radius:4px; font-size:12px;"></div>
</div>
</div>
<div class="col-md-4">
<h5 style="color:#6f42c1;">⚖️ Actual Load</h5>
<div class="form-group"><label>Received Date <span class="text-danger">*</span></label><input type="date" id="ci-received_date" class="form-control" required></div>
<div class="form-group"><label>Actual # of Pallets/Gaylords <span class="text-danger">*</span></label><input type="number" id="ci-actual_pallets" class="form-control" min="1" required></div>
<div class="form-group"><label>Total Weight (lbs)</label><input type="text" id="ci-total_weight" class="form-control"></div>
</div>
<div class="col-md-4">
<h5 style="color:#6f42c1;">📦 Contents & Classification</h5>
<div class="form-group"><label>Load Contents</label><textarea id="ci-load_contents" class="form-control" rows="2" placeholder="e.g. 80% wire 20% tablets"></textarea></div>
<div class="form-group"><label>Data Status</label><select id="ci-data_status" class="form-control"><option value="">—</option><option value="D0">D0</option><option value="D1">D1</option><option value="ND1">ND1</option><option value="ND2">ND2</option><option value="ND3">ND3</option><option value="ND4">ND4</option></select></div>
<div class="form-group"><label>RED / R2</label><select id="ci-red_r2" class="form-control"><option value="">—</option><option value="RED">RED</option><option value="NIST">NIST</option><option value="Red+NIST">Red+NIST</option><option value="R2">R2</option><option value="Both">Both</option><option value="Neither">Neither</option></select></div>
</div>
</div>
<div class="row" style="margin-top: 8px;"><div class="col-md-12"><button type="submit" class="btn btn-primary btn-lg">✓ Check In Load</button><button type="button" class="btn btn-default btn-lg" id="btn-cancel-checkin">Cancel</button></div></div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
.truck-stops { min-height: 60px; }
.stop-card { background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 6px; padding: 10px 12px; margin: 6px 0; cursor: grab; font-size: 13px; }
.stop-card:hover { border-color: #2F5496; }
.stop-card .stop-co { font-weight: 700; color: #2F5496; }
.stop-card .stop-addr { color: #666; font-size: 12px; margin-top: 2px; }
.stop-card .stop-meta { display: flex; gap: 8px; margin-top: 4px; font-size: 11px; }
.stop-card .stop-meta span { background: #D6E4F0; color: #2F5496; padding: 1px 6px; border-radius: 3px; }
.stop-card.dragging { opacity: 0.5; }
.cal-day { display: inline-block; width: 36px; height: 36px; line-height: 36px; text-align: center; margin: 1px; border-radius: 4px; font-size: 12px; cursor: pointer; }
.cal-day:hover { background: #D6E4F0; }
.cal-day.has-pickups { background: #2F5496; color: #fff; font-weight: 700; }
.cal-day.today { border: 2px solid #C62828; }
</style>
`);
// Prevent Frappe router from intercepting tab clicks
$("#receiving-tabs").on("click", "a[data-toggle=tab]", function(e) {
e.preventDefault();
$(this).tab("show");
});
// ── Stage A: Link Controls ──
var customer_control = null;
function setupCustomerLink() {
customer_control = frappe.ui.form.make_control({
parent: $("#sp-customer-control"),
df: {
fieldtype: "Link",
fieldname: "customer_number",
options: "Customer",
label: "Customer",
reqd: 1,
placeholder: "Search by name, number, or address...",
onchange: function() {
var val = customer_control.get_value();
if (val) fetchCustomerDetails(val);
else clearCustomerFields();
}
},
only_input: true,
});
customer_control.refresh();
$("#sp-customer-control .control-input").css("margin", "0");
$("#sp-customer-control .help-box").remove();
}
// New Customer button
$(document).on("click", "#btn-new-customer", function() {
frappe.call({
method: "frappe.client.insert",
args: { doc: { doctype: "Customer", customer_type: "Company", customer_group: "All Customer Groups", territory: "United States" } },
callback: function(r) {
if (r.message) {
frappe.set_route("Form", "Customer", r.message.name);
}
}
});
});
function fetchCustomerDetails(customer_name) {
frappe.call({
method: "frappe.client.get",
args: { doctype: "Customer", name: customer_name },
callback: function(r) {
if (!r.message) return;
var c = r.message;
$("#sp-company_name").val(c.customer_name || "");
$("#sp-contact_name").val(c.contact_name || "");
$("#sp-contact_phone").val(c.contact_phone || "");
$("#sp-contact_email").val(c.contact_email || "");
frappe.call({
method: "frappe.client.get_list",
args: {
doctype: "Address",
filters: [["Dynamic Link", "link_name", "=", customer_name]],
fields: ["address_line1", "city", "state", "pincode"],
limit_page_length: 1
},
callback: function(ra) {
if (ra.message && ra.message.length) {
var a = ra.message[0];
$("#sp-address_line").val(a.address_line1 || "");
$("#sp-city").val(a.city || "");
$("#sp-state").val(a.state || "AZ");
$("#sp-zip_code").val(a.pincode || "");
}
}
});
frappe.call({
method: "frappe.client.get_list",
args: {
doctype: "Contact",
filters: [["Dynamic Link", "link_name", "=", customer_name]],
fields: ["first_name", "last_name", "email_id", "phone", "mobile_no"],
limit_page_length: 1
},
callback: function(rc) {
if (rc.message && rc.message.length) {
var ct = rc.message[0];
if (!$("#sp-contact_name").val()) {
$("#sp-contact_name").val((ct.first_name || "") + " " + (ct.last_name || ""));
}
if (!$("#sp-contact_phone").val()) {
$("#sp-contact_phone").val(ct.phone || ct.mobile_no || "");
}
if (!$("#sp-contact_email").val()) {
$("#sp-contact_email").val(ct.email_id || "");
}
}
}
});
}
});
}
function clearCustomerFields() {
$("#sp-company_name, #sp-contact_name, #sp-contact_phone, #sp-contact_email, #sp-address_line, #sp-city, #sp-state, #sp-zip_code").val("");
$("#sp-state").val("AZ");
}
// ── Stage A: Load Pickups ──
function loadPickups() {
var dateFilter = $("#pickup-date-filter").val();
frappe.call({
method: "westech_r2.api.receiving_api.get_pickups",
args: { date: dateFilter || undefined },
callback: function(r) {
if (r.message) {
renderPickupTable(r.message.pickups || []);
renderCalendar(r.message.calendar || []);
}
}
});
}
function renderPickupTable(pickups) {
var tbody = $("#pickup-tbody");
$("#pickup-count-label").text("(" + pickups.length + ")");
if (!pickups.length) {
tbody.html('<tr><td colspan="14" class="text-center text-muted">No pickups found</td></tr>');
return;
}
tbody.html(pickups.map(function(p) {
var dt = p.pickup_date ? new Date(p.pickup_date + "T00:00:00") : null;
var dn = dt ? dayName(dt) : "";
return '<tr style="cursor:pointer" data-pickup="' + esc(p.name) + '">' +
'<td>' + esc(p.pickup_date || "") + '</td>' +
'<td>' + dn + '</td>' +
'<td>' + esc(p.pickup_type || "") + '</td>' +
'<td><strong>' + esc(p.company_name || p.customer_number || "") + '</strong></td>' +
'<td>' + esc(p.contact_name || "") + '</td>' +
'<td style="max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + esc((p.address_line || "") + (p.city ? ", " + p.city : "")) + '</td>' +
'<td class="text-right">' + (p.estimated_items || "—") + '</td>' +
'<td>' + esc(p.data_status || "—") + '</td>' +
'<td>' + esc(p.red_r2 || "—") + '</td>' +
'<td>' + esc(p.status || "") + '</td>' +
'<td style="max-width:100px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + esc(p.notes || "") + '</td>' +
'<td>' + esc(p.truck || "") + '</td>' +
'<td>' + (p.needs_aor ? "✓" : "") + '</td>' +
'<td>' + (p.needs_cod ? "✓" : "") + '</td></tr>';
}).join(""));
}
function renderCalendar(calendar) {
var el = $("#pickup-calendar");
if (!calendar.length) { el.html('<div class="text-muted text-center">No data</div>'); return; }
var todayStr = frappe.datetime.nowdate();
var html = '<div style="text-align:center;">';
calendar.forEach(function(d) {
var cls = "cal-day";
if (d.count > 0) cls += " has-pickups";
if (d.date === todayStr) cls += " today";
var label = d.date.substring(5);
html += '<div class="' + cls + '" data-date="' + d.date + '" title="' + d.count + ' pickup(s)">' + label + '</div>';
});
html += '</div>';
el.html(html);
}
$(document).on("click", ".cal-day.has-pickups", function() {
$("#pickup-date-filter").val($(this).data("date"));
loadPickups();
});
// ── Stage A: New Pickup ──
$("#btn-new-pickup").on("click", function() {
$("#new-pickup-form").show();
$("#sp-pickup_date").val(frappe.datetime.nowdate());
setupCustomerLink();
});
$("#btn-cancel-pickup").on("click", function() {
$("#new-pickup-form").hide();
});
$("#pickup-form").on("submit", function(e) {
e.preventDefault();
var customerVal = customer_control ? customer_control.get_value() : "";
if (!customerVal) { frappe.msgprint("Select a customer"); return; }
var doc = {
doctype: "Scheduled Pickup",
pickup_date: $("#sp-pickup_date").val(),
pickup_type: $("#sp-pickup_type").val(),
customer_number: customerVal,
company_name: $("#sp-company_name").val(),
contact_name: $("#sp-contact_name").val(),
contact_phone: $("#sp-contact_phone").val(),
contact_email: $("#sp-contact_email").val(),
address_line: $("#sp-address_line").val(),
city: $("#sp-city").val(),
state: $("#sp-state").val(),
zip_code: $("#sp-zip_code").val(),
estimated_items: parseInt($("#sp-estimated_items").val()) || 0,
estimated_weight: $("#sp-estimated_weight").val(),
load_contents: $("#sp-load_contents").val(),
data_status: $("#sp-data_status").val(),
red_r2: $("#sp-red_r2").val(),
needs_aor: $("#sp-needs_aor").is(":checked") ? 1 : 0,
needs_cod: $("#sp-needs_cod").is(":checked") ? 1 : 0,
notes: $("#sp-notes").val(),
status: "Scheduled"
};
frappe.call({
method: "frappe.client.insert",
args: { doc: doc },
callback: function(r) {
if (r.message) {
frappe.show_alert({ message: "Pickup scheduled", indicator: "green" });
$("#new-pickup-form").hide();
loadPickups();
}
}
});
});
$("#pickup-date-filter").on("change", loadPickups);
$("#btn-clear-date").on("click", function() {
$("#pickup-date-filter").val("");
loadPickups();
});
// ── Stage B: Routing ──
function loadRoutes() {
var date = $("#route-date").val() || frappe.datetime.nowdate();
$("#route-date").val(date);
frappe.call({
method: "westech_r2.api.receiving_api.get_pickups",
args: { date: date },
callback: function(r) {
if (r.message) renderRouteColumns(r.message.pickups || []);
}
});
}
function renderRouteColumns(pickups) {
var trucks = { "Truck 1": [], "Truck 2": [], "Truck 3": [], "Unassigned": [] };
pickups.forEach(function(p) {
var t = p.truck || "";
if (t && trucks[t]) trucks[t].push(p);
else trucks["Unassigned"].push(p);
});
["Truck 1", "Truck 2", "Truck 3"].forEach(function(t) {
var key = t.toLowerCase().replace(/ /g, "");
$("#" + key + "-count").text("(" + trucks[t].length + " stops)");
$("#" + key + "-stops").html(trucks[t].map(function(p, i) { return stopCard(p, i + 1); }).join(""));
});
$("#unassigned-count").text("(" + trucks["Unassigned"].length + ")");
$("#unassigned-stops").html(trucks["Unassigned"].map(function(p) { return stopCard(p, 0); }).join(""));
}
function stopCard(p, order) {
var h = '<div class="stop-card" data-pickup="' + esc(p.name) + '">';
if (order) h += '<div style="font-size:11px;color:#666;margin-bottom:2px">Stop #' + order + '</div>';
h += '<div class="stop-co">' + esc(p.company_name || p.customer_number || "Unknown") + '</div>';
h += '<div class="stop-addr">' + esc((p.address_line || "") + (p.city ? ", " + p.city : "")) + '</div>';
h += '<div class="stop-meta">';
if (p.estimated_items) h += '<span>' + p.estimated_items + ' items</span>';
if (p.data_status) h += '<span>' + esc(p.data_status) + '</span>';
if (p.red_r2) h += '<span>' + esc(p.red_r2) + '</span>';
if (p.needs_aor) h += '<span>AoR</span>';
if (p.needs_cod) h += '<span>CoD</span>';
h += '</div></div>';
return h;
}
$("#btn-load-routes").on("click", loadRoutes);
$("#btn-auto-route").on("click", function() {
var date = $("#route-date").val();
if (!date) { frappe.msgprint("Select a date first"); return; }
frappe.call({
method: "westech_r2.api.receiving_api.auto_route",
args: { date: date },
callback: function(r) {
if (r.message && r.message.success) {
frappe.show_alert({ message: "Routes optimized", indicator: "green" });
loadRoutes();
}
}
});
});
$("#btn-route-sheet").on("click", function() {
var date = $("#route-date").val() || frappe.datetime.nowdate();
window.open("/api/method/westech_r2.api.receiving_api.print_route_sheet?date=" + date, "_blank");
});
$("#btn-green-sheet").on("click", function() {
var date = $("#route-date").val() || frappe.datetime.nowdate();
window.open("/api/method/westech_r2.api.receiving_api.print_green_sheet?date=" + date, "_blank");
});
$("#btn-labels").on("click", function() {
var date = $("#route-date").val() || frappe.datetime.nowdate();
window.open("/api/method/westech_r2.api.receiving_api.print_labels?date=" + date, "_blank");
});
// ── Stage C: Check-in ──
var checkin_pickup_control = null;
var current_pickup_details = null;
function loadCheckins() {
frappe.call({
method: "westech_r2.api.receiving_api.get_checkins",
callback: function(r) {
if (r.message) renderCheckinTable(r.message.checkins || []);
}
});
}
function renderCheckinTable(checkins) {
var tbody = $("#checkin-tbody");
if (!checkins.length) {
tbody.html('<tr><td colspan="8" class="text-center text-muted">No check-ins yet</td></tr>');
return;
}
tbody.html(checkins.map(function(c) {
var palletInfo = (c.pallets || []).map(function(p) { return esc(p.pallet_number || p.name); }).join(", ");
return '<tr>' +
'<td>' + esc(c.incoming_date || "") + '</td>' +
'<td><strong>' + esc(c.customer_name || c.customer || "") + '</strong></td>' +
'<td>' + esc(c.name || "") + '</td>' +
'<td class="text-right">' + (c.pallet_count || 0) + '</td>' +
'<td class="text-right">' + (c.total_weight || "—") + '</td>' +
'<td style="max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + esc(c.data_status || "") + '</td>' +
'<td>' + esc(c.red_r2 || "—") + '</td>' +
'<td style="font-size:11px;color:#666;">' + palletInfo + '</td></tr>';
}).join(""));
}
$("#btn-new-checkin").on("click", function() {
$("#checkin-form").show();
$("#ci-received_date").val(frappe.datetime.nowdate());
$("#ci-pickup-details").hide();
current_pickup_details = null;
checkin_pickup_control = frappe.ui.form.make_control({
parent: $("#ci-pickup-control"),
df: {
fieldtype: "Link",
fieldname: "pickup_ref",
options: "Scheduled Pickup",
label: "Scheduled Pickup",
reqd: 1,
placeholder: "Search pickup...",
get_query: function() {
return {
filters: [
["Scheduled Pickup", "status", "in", ["Scheduled", "Routed", "In Progress"]]
]
};
},
onchange: function() {
var val = checkin_pickup_control ? checkin_pickup_control.get_value() : "";
if (val) loadPickupDetails(val);
else {
$("#ci-pickup-details").hide();
current_pickup_details = null;
}
}
},
only_input: true,
});
checkin_pickup_control.refresh();
$("#ci-pickup-control .control-input").css("margin", "0");
$("#ci-pickup-control .help-box").remove();
});
function loadPickupDetails(pickup_name) {
frappe.call({
method: "westech_r2.api.receiving_api.get_pickup_details",
args: { pickup_name: pickup_name },
callback: function(r) {
if (!r.message) return;
current_pickup_details = r.message;
var d = r.message;
$("#ci-pickup-details").show();
$("#ci-customer-info").text((d.company_name || d.customer_number || "Unknown") + (d.red_r2 ? " — " + d.red_r2 : ""));
$("#ci-address-info").text((d.address_line || "") + (d.city ? ", " + d.city : "") + (d.state ? ", " + d.state : "") + (d.zip_code ? " " + d.zip_code : ""));
$("#ci-contact-info").text((d.contact_name || "") + (d.contact_phone ? " • " + d.contact_phone : "") + (d.contact_email ? " • " + d.contact_email : ""));
// Special handling for RED/NIST
if (d.red_r2 && d.red_r2 !== "Neither" && d.red_r2 !== "") {
$("#ci-special-handling").show().html("<strong>⚠ " + esc(d.red_r2) + "</strong> — Special handling required" + (d.needs_aor ? " • AoR ✓" : "") + (d.needs_cod ? " • CoD ✓" : ""));
} else {
$("#ci-special-handling").hide();
}
// Notes
if (d.notes) {
$("#ci-pickup-notes").show().html("<strong>Notes:</strong> " + esc(d.notes));
} else {
$("#ci-pickup-notes").hide();
}
// Pre-fill check-in fields from pickup
$("#ci-actual_pallets").val(d.estimated_items || 1);
$("#ci-data_status").val(d.data_status || "");
$("#ci-red_r2").val(d.red_r2 || "");
}
});
}
$("#btn-cancel-checkin").on("click", function() {
$("#checkin-form").hide();
});
$("#checkin-form-inner").on("submit", function(e) {
e.preventDefault();
var pickupName = checkin_pickup_control ? checkin_pickup_control.get_value() : "";
if (!pickupName) { frappe.msgprint("Select a pickup"); return; }
var actualPallets = parseInt($("#ci-actual_pallets").val()) || 0;
if (actualPallets < 1) { frappe.msgprint("Enter at least 1 pallet"); return; }
frappe.confirm(
"Check in " + actualPallets + " pallet(s) for this load?",
function() {
frappe.call({
method: "westech_r2.api.receiving_api.checkin_load",
args: {
pickup_name: pickupName,
received_date: $("#ci-received_date").val(),
actual_pallets: actualPallets,
total_weight: $("#ci-total_weight").val(),
load_contents: $("#ci-load_contents").val(),
data_status: $("#ci-data_status").val(),
red_r2: $("#ci-red_r2").val()
},
callback: function(r) {
if (r.message && r.message.success) {
frappe.show_alert({
message: "Load checked in! Created " + r.message.pallets_created + " pallet(s) in Load " + r.message.load,
indicator: "green"
});
$("#checkin-form").hide();
loadCheckins();
loadPickups();
}
}
});
}
);
});
$("#btn-cor-report").on("click", function() {
window.open("/api/method/westech_r2.api.receiving_api.cor_report", "_blank");
});
// ── Helpers ──
function esc(s) { return s ? String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;") : ""; }
function dayName(d) { return ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][d.getDay()]; }
// ── Init ──
loadPickups();
loadCheckins();
};
@@ -0,0 +1,24 @@
{
"content": null,
"creation": "2026-05-20 21:24:08.575605",
"docstatus": 0,
"doctype": "Page",
"icon": "stock",
"idx": 0,
"modified": "2026-05-20 21:27:35.049633",
"modified_by": "Administrator",
"module": "Westech R2",
"name": "receiving",
"owner": "Administrator",
"page_name": "receiving",
"roles": [
{
"role": "All"
}
],
"script": null,
"standard": "No",
"style": null,
"system_page": 0,
"title": "Receiving"
}
@@ -0,0 +1,5 @@
from frappe import _
def get_context(context):
context.no_cache = 1
context.title = _("Receiving")
@@ -0,0 +1,4 @@
<div style="padding:20px;">
<h2>Route Planner</h2>
<p>Optimize pickup routes for scheduled pickups.</p>
</div>
@@ -0,0 +1,38 @@
frappe.pages["route-planner"].on_page_load = function(wrapper) {
var page = frappe.ui.make_app_page({
parent: wrapper,
title: "Route Planner",
single_column: true
});
frappe.call({
method: "westech_r2.api.optimize_routes.get_scheduled_pickups",
callback: function(r) {
if (r.message) {
renderRoutePlanner(page, r.message);
}
}
});
function renderRoutePlanner(page, data) {
var h = "<div style='padding:20px;'>";
h += "<h2>Route Planner</h2>";
h += "<p>Schedule and optimize pickup routes.</p>";
h += "<div id='route-map' style='width:100%;height:400px;background:#f0f0f0;margin:20px 0;'></div>";
h += "<button class='btn btn-primary' id='optimize-btn'>Optimize Routes</button>";
h += "<div id='routes-container' style='margin-top:20px;'></div>";
h += "</div>";
$(page.body).html(h);
$("#optimize-btn").on("click", function() {
frappe.call({
method: "westech_r2.api.optimize_routes.optimize_routes",
callback: function(r) {
if (r.message) {
frappe.show_alert({message: "Routes optimized", indicator: "green"});
}
}
});
});
}
};
@@ -0,0 +1,14 @@
{
"doctype": "Page",
"name": "route-planner",
"page_name": "route-planner",
"title": "Route Planner",
"module": "Westech R2",
"standard": "Yes",
"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,3 @@
import frappe
no_cache = 1
@@ -0,0 +1,4 @@
<div style="padding:20px;">
<h2>Route Planner</h2>
<p>Optimize pickup routes for scheduled pickups.</p>
</div>
@@ -0,0 +1,38 @@
frappe.pages["route-planner"].on_page_load = function(wrapper) {
var page = frappe.ui.make_app_page({
parent: wrapper,
title: "Route Planner",
single_column: true
});
frappe.call({
method: "westech_r2.api.optimize_routes.get_scheduled_pickups",
callback: function(r) {
if (r.message) {
renderRoutePlanner(page, r.message);
}
}
});
function renderRoutePlanner(page, data) {
var h = "<div style='padding:20px;'>";
h += "<h2>Route Planner</h2>";
h += "<p>Schedule and optimize pickup routes.</p>";
h += "<div id='route-map' style='width:100%;height:400px;background:#f0f0f0;margin:20px 0;'></div>";
h += "<button class='btn btn-primary' id='optimize-btn'>Optimize Routes</button>";
h += "<div id='routes-container' style='margin-top:20px;'></div>";
h += "</div>";
$(page.body).html(h);
$("#optimize-btn").on("click", function() {
frappe.call({
method: "westech_r2.api.optimize_routes.optimize_routes",
callback: function(r) {
if (r.message) {
frappe.show_alert({message: "Routes optimized", indicator: "green"});
}
}
});
});
}
};
@@ -0,0 +1,9 @@
{
"doctype": "Page",
"name": "route-planner",
"page_name": "route-planner",
"title": "Route Planner",
"module": "Westech R2",
"standard": "Yes",
"roles": [{"role": "All"}]
}
@@ -0,0 +1,3 @@
import frappe
no_cache = 1
@@ -0,0 +1 @@
@@ -0,0 +1,4 @@
frappe.pages["wes-ai"].on_page_load = function(wrapper) {
wrapper.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:60vh;font-family:sans-serif;"><div style="text-align:center;"><i class="fa fa-spinner fa-spin" style="font-size:24px;color:#5b3a8c;"></i><p style="margin-top:12px;color:#555;">Redirecting to Wes AI Assistant...</p></div></div>';
setTimeout(function() { window.location.href = "https://wes.advante.ch"; }, 500);
};
@@ -0,0 +1,13 @@
{
"creation": "2026-05-09 14:00:00",
"docstatus": 0,
"doctype": "Page",
"idx": 0,
"modified": "2026-05-09 14:00:00",
"modified_by": "Administrator",
"module": "Westech R2",
"name": "wes-ai",
"owner": "Administrator",
"standard": "Yes",
"title": "Wes AI Assistant"
}
@@ -0,0 +1,5 @@
import frappe
def get_context(context):
frappe.local.flags.redirect_location = "https://wes.advante.ch"
raise frappe.Redirect
@@ -0,0 +1,30 @@
frappe.pages["wes-ai"].on_page_load = function(wrapper) {
var page = frappe.ui.make_app_page({
parent: wrapper,
title: "Wes AI Assistant",
single_column: true
});
// Create iframe that embeds Wes AI
var $container = $(wrapper).find(".layout-main-section");
$container.css({
"position": "relative",
"overflow": "hidden"
});
// Determine Wes URL based on environment
var wes_url = "https://wes.advante.ch";
// On production VM, Wes runs locally
if (window.location.hostname === "erpnext.local" || window.location.hostname === "localhost") {
wes_url = "http://localhost:8082";
}
var $iframe = $('<iframe>', {
src: wes_url,
style: "width: 100%; height: calc(100vh - 120px); border: none; border-radius: 4px;",
id: "wes-ai-iframe"
});
$container.empty().append($iframe);
};