Skip to main content

Lead Source Performance — Deluge Build Spec

Developer hand-off for the ACH Lead Performance Dashboard. One workflow function computes everything and returns the full HTML string; the Creator page renders it in an HTML snippet. Template: /mockups/ach-lead-dashboard-nojs.html.

:::note Field link names Field/report link names below are taken from the schema docs and this project's verification sessions — verify each against the live app before coding (e.g. the Lead_Source display field, the exact Hire_Status/Status link names on Claims_Management_Form). :::


1. Page wiring​

PieceValue
PageLead_Source_Performance
URL parametersmonth (MM-yyyy, default current month) · source (Lead Source name, default Accident Claims Helpline)
Data functionget_lead_performance_data(p_month, p_source) → returns map — all counts, rows and drill-down links; no HTML. The single source of truth
HTMLLives in the Creator page's HTML snippet — the classic report markup pasted in as the template, with Deluge placeholders where the numbers go
Snippet logicInvokes the data function once, then substitutes map values via <%= %> expressions and for each loops for the daily rows — see §3
Filter behaviourThe function renders the two <details> dropdowns; every option is a link back to #Page:Lead_Source_Performance?month=…&source=… → selecting = full page reload with new parameters

2. The data function​

One function computes everything and returns a map — separated from presentation so the classic report page, the dashboard page, the monthly email, and the widget (v2, via a Custom API) all consume identical numbers.

/* =====================================================================
* get_lead_performance_data(p_month string, p_source string) : map
* The single source of truth — counts, rows, drill-down links. No HTML.
* Criteria: see "Criteria Definitions" in ach-lead-performance-dashboard
* ===================================================================== */

/* ---------- 1. Parameters & date window ---------- */
monthStr = p_month;
if(monthStr == null || monthStr == "")
{
monthStr = zoho.currentdate.toString("MM-yyyy");
}
sourceName = p_source;
if(sourceName == null || sourceName == "")
{
sourceName = "Accident Claims Helpline";
}
monthStart = ("01-" + monthStr).toDate();
monthEnd = monthStart.addMonth(1).subDay(1);
today = zoho.currentdate;

/* ---------- 2. Base population — CONFIRMED criteria ---------- */
// Lead Source = <source>, Date_Created in month. Adjust the lookup's
// display-field name (Lead_Source1.<display field>) to the live schema.
leadRecs = Claim_Form[Lead_Source1.Lead_Source == sourceName
&& Date_Created >= monthStart && Date_Created <= monthEnd]
sort by Date_Created;

/* ---------- 3. Claim-type grouping ---------- */
// TODO before go-live: confirm Free Rental + Towing/Storage placements
repairOnlyTypes = {"Repair Only","Repair Only IITR/Crash Collab","Towing + Repair"};
repairRentalTypes = {"Repair & Rental","Towing + Repair + Rental","Towing + Repair + Free Rental"};
rentalOnlyTypes = {"Rental Only","Rental Only IITR/Crash Collab","Free Rental","Free Rental Only","Total Loss Rental Only","Towing/Storage + Rental","Towing/Storage + Free Rental","Total Loss + Towing/Storage + Rental"};

/* ---------- 4. Classify every lead, aggregate ---------- */
// per-outcome totals
cntWent = 0; cntBooked = 0; cntPending = 0; cntCancelled = 0;
unmappedTypes = 0;

// dayMap: "dd/MM" -> Map{leads, went, booked, pending, cancelled, ro, rr, rent}
dayMap = Map();
dayKeys = List(); // preserves order (records sorted by Date_Created)

// typeAgg: group -> Map{total, went, booked, pending, cancelled}
typeAgg = Map();
for each g in {"Repair Only","Repair & Rental","Rental Only"}
{
typeAgg.put(g, {"total":0,"went":0,"booked":0,"pending":0,"cancelled":0});
}

bookedRows = List(); // rows for "Upcoming bookings"
pendingRows = List(); // rows for "Pending — needs a decision"
nextBooking = null; // earliest future Booking_Date
oldestWait = 0; // max days waiting among pending

for each rec in leadRecs
{
/* --- outcome: precedence order, first match wins --- */
cmStatus = "";
hireStatus = "";
if(rec.Claims_Management_Form != null)
{
cmStatus = ifnull(rec.Claims_Management_Form.Status, "");
hireStatus = ifnull(rec.Claims_Management_Form.Hire_Status, "");
}
outcome = "";
if(cmStatus == "Cancel" || cmStatus == "Reject" || rec.Is_Claim_Accepted == "Declined")
{
outcome = "cancelled"; cntCancelled = cntCancelled + 1;
}
else if(rec.Job_Taken_On1 != null)
{
outcome = "went"; cntWent = cntWent + 1;
}
else if(hireStatus == "Booked")
{
outcome = "booked"; cntBooked = cntBooked + 1;
bookedRows.add({"file":rec.File_Number, "type":rec.Claim_Type1,
"received":rec.Date_Created, "scheduled":rec.Booking_Date,
"id":rec.ID});
if(rec.Booking_Date != null && (nextBooking == null || rec.Booking_Date < nextBooking))
{
nextBooking = rec.Booking_Date;
}
}
else
{
outcome = "pending"; cntPending = cntPending + 1;
waitDays = daysBetween(rec.Date_Created, today);
if(waitDays > oldestWait) { oldestWait = waitDays; }
pendingRows.add({"file":rec.File_Number, "reason":rec.Is_Claim_Accepted,
"received":rec.Date_Created, "wait":waitDays, "id":rec.ID});
}

/* --- claim-type group --- */
grp = "";
if(repairOnlyTypes.contains(rec.Claim_Type1)) { grp = "Repair Only"; }
else if(repairRentalTypes.contains(rec.Claim_Type1)) { grp = "Repair & Rental"; }
else if(rentalOnlyTypes.contains(rec.Claim_Type1)) { grp = "Rental Only"; }
else { unmappedTypes = unmappedTypes + 1; } // never silently dropped

if(grp != "")
{
gAgg = typeAgg.get(grp);
gAgg.put("total", gAgg.get("total") + 1);
gAgg.put(outcome, gAgg.get(outcome) + 1);
typeAgg.put(grp, gAgg);
}

/* --- daily bucket --- */
dKey = rec.Date_Created.toString("dd/MM");
if(!dayMap.containKey(dKey))
{
dayKeys.add(dKey);
dayMap.put(dKey, {"leads":0,"went":0,"booked":0,"pending":0,"cancelled":0,"ro":0,"rr":0,"rent":0});
}
d = dayMap.get(dKey);
d.put("leads", d.get("leads") + 1);
d.put(outcome, d.get(outcome) + 1);
if(grp == "Repair Only") { d.put("ro", d.get("ro") + 1); }
if(grp == "Repair & Rental") { d.put("rr", d.get("rr") + 1); }
if(grp == "Rental Only") { d.put("rent",d.get("rent") + 1); }
dayMap.put(dKey, d);
}

totalLeads = leadRecs.count();
convPct = 0;
if(totalLeads > 0) { convPct = (cntWent * 100 / totalLeads).round(0); }

/* ---------- 5. Component matrix (derived, no extra tracking) ---------- */
// Repairs <o> = RepairOnly.<o> + R&R.<o> · Rentals <o> = RentalOnly.<o> + R&R.<o>
ro = typeAgg.get("Repair Only"); rr = typeAgg.get("Repair & Rental"); ren = typeAgg.get("Rental Only");
repWent = ro.get("went") + rr.get("went"); renWent = ren.get("went") + rr.get("went");
repBook = ro.get("booked") + rr.get("booked"); renBook = ren.get("booked") + rr.get("booked");
repPend = ro.get("pending") + rr.get("pending"); renPend = ren.get("pending") + rr.get("pending");
repCanc = ro.get("cancelled") + rr.get("cancelled"); renCanc = ren.get("cancelled") + rr.get("cancelled");

/* ---------- 6. Assemble the return map (NO HTML here) ---------- */
// per-day conversion + flatten dayMap into an ordered list for the template
days = List();
for each k in dayKeys
{
d = dayMap.get(k);
d.put("date", k);
dc = 0;
if(d.get("leads") > 0) { dc = (d.get("went") * 100 / d.get("leads")).round(0); }
d.put("conversion", dc);
days.add(d);
}

// drill-down links generated once here so every consumer shares them — see §4
linkBase = "#Report:All_Claims?..."; // base population filter
linkWent = "#Report:ACH_Went_Ahead?...";
linkBooked = "#Report:ACH_Booked?...";
linkPending = "#Report:ACH_Pending?...";
linkCancelled = "#Report:ACH_Cancelled?...";

summary = Map();
summary.put("total_leads", {"count": totalLeads, "link": linkBase});
summary.put("went_ahead", {"count": cntWent, "link": linkWent, "conversion": convPct});
summary.put("booked", {"count": cntBooked, "link": linkBooked, "next_booking": nextBooking});
summary.put("pending", {"count": cntPending, "link": linkPending,"oldest_wait": oldestWait});
summary.put("cancelled", {"count": cntCancelled, "link": linkCancelled});

result = Map();
result.put("meta", {"month": monthStr, "source": sourceName, "as_of": today.toString("dd/MM/yyyy")});
result.put("summary", summary);
result.put("days", days); // list of per-day maps
result.put("types", typeAgg); // group -> {total, went, booked, pending, cancelled}
result.put("components", {
"repairs": {"went": repWent, "booked": repBook, "pending": repPend, "cancelled": repCanc},
"rentals": {"went": renWent, "booked": renBook, "pending": renPend, "cancelled": renCanc}
});
result.put("booked_rows", bookedRows);
result.put("pending_rows", pendingRows);
result.put("unmapped_types", unmappedTypes);
return result;

Declare the function's return type as map — nested maps/lists pass to the page snippet as-is. Because the map carries data only (no markup), the same function later powers the monthly email schedule and the widget v2 (published as a Custom API, where Deluge auto-serialises the map to JSON).

3. Page template — invoke once, substitute variables​

The page's HTML snippet holds the classic report markup as a template: paste the <style> block and tables, invoke the data function once at the top, and replace every hardcoded number with a placeholder. Static single-value cells use <%= %>; repeating rows (daily table, claim-type rows, booked/pending lists) use for each loops.

<%{
data = thisapp.get_lead_performance_data(input.month, input.source);
s = data.get("summary");
%>

<!-- summary header: values row -->
<table class="summary">
<tr>
<td class="navycell sv"><%=s.get("total_leads").get("count")%></td>
<td class="c-went sv"><%=s.get("went_ahead").get("count")%></td>
<td class="c-hold sv"><%=s.get("pending").get("count")%></td>
<td class="c-booked sv"><%=s.get("booked").get("count")%></td>
<td class="c-canc sv"><%=s.get("cancelled").get("count")%></td>
</tr>
<!-- labels row: static, except "(<%=s.get("went_ahead").get("conversion")%>%)" -->
</table>

<!-- daily breakdown: one loop replaces all hardcoded rows -->
<tbody>
<% for each day in data.get("days") { %>
<tr>
<td class="datecol"><%=day.get("date")%></td>
<td><%=day.get("leads")%></td>
<td><%=day.get("went")%></td>
<td><%=day.get("pending")%></td>
<td><%=day.get("booked")%></td>
<td><%=day.get("cancelled")%></td>
<td><%=day.get("ro")%></td>
<td><%=day.get("rr")%></td>
<td><%=day.get("rent")%></td>
<td><%=day.get("conversion")%>%</td>
</tr>
<% } %>
</tbody>

<!-- drill-down: wrap any figure in its link from the map -->
<a href="<%=s.get("cancelled").get("link")%>"><%=s.get("cancelled").get("count")%></a>

<%
}%>
note

The exact Deluge-in-HTML delimiters (<%{ }%> / <%= %> / <% %>) vary slightly by Creator page type — copy the working pattern from an existing page (e.g. the Management Dashboard) rather than trusting this doc's rendering of them.

Template token map​

Every dynamic token in the report and the map value that fills it:

Mockup elementDeluge source
Filter dropdown — month optionsLoop last 12 months from today; each <a href="#Page:Lead_Source_Performance?month=MM-yyyy&source=…">; class='cur' when == monthStr
Filter dropdown — lead source optionsLead_Source[…] master records; same link pattern with source=
Tile: Total leads value / subtotalLeads · monthStart–min(today, monthEnd) label
Tile: sparkline points(dashboard/widget view only) computed in the template from data.get("days") leads values
Tile: Went ahead value / subcntWent · convPct + "% conversion"
Tile: Booked value / subcntBooked · "next: " + nextBooking.toString("dd/MM") (omit sub when null)
Tile: Pending value / subcntPending · "oldest waiting " + oldestWait + " days"
Tile: Cancelled value / subcntCancelled · (cntCancelled×100/totalLeads).round(0) + "% of leads"
Daily table rows (classic report)for each day in data.get("days") — see §3 template
Daily chart columns/gridlines(dashboard view only) computed in the template: maxY = busiest day rounded up to a multiple of 3; segment height = count/maxY × 180px minus 2px per gap
Claim-type rowsLoop typeAgg in fixed order; bar width = total/maxGroupTotal×100%; segments flex-grow = count; inline label only when count ≥ 3; row end total · conv%
"Unmapped types this month"unmappedTypes — if > 0, render an extra warning row so nothing is silently dropped
Components matrixrepWent/renWent/repBook/renBook/repPend/renPend/repCanc/renCanc
Upcoming bookings rowsbookedRows sorted by scheduled asc; bold + "· today" when scheduled == today
Pending rowspendingRows sorted by wait desc; class='age-hot' when wait > 10
Full data table rowsLoop dayKeys → all 8 counters + per-day conversion; footer = the month totals
Footer criteria lineStatic text (update if criteria ever change)

Escaping: any value that can contain user text (lead source names, claim types, Is_Claim_Accepted values) must be HTML-escaped before concatenation (at minimum replace &, <, >, "). File numbers and counts are numeric and safe.

Every tile (and later, chart rows/segments if desired) opens the claims report pre-filtered. Two implementation options — decide once, use everywhere:

OptionHowTrade-off
A. URL criteria params (pattern already used by the Management Dashboard, e.g. #Report:Active_Recoveries_CC?Recovery_Firm2.ID_op=30)Link to e.g. #Report:All_Claims?Lead_Source1.Lead_Source=…&Job_Taken_On1_op=30Simple; but OR-criteria (Cancel or Reject or Declined) and date-ranges are hard to express — confirm the op-codes actually cover each bucket
B. Saved report views per bucket (recommended)Create filtered views of the claims report — ACH_Went_Ahead, ACH_Booked, ACH_Pending, ACH_Cancelled — each with the bucket's full criteria built into the report filter; the link passes only lead source + month paramsOR-logic and cross-form criteria live in the report definition where they're expressible; links stay trivial

Per-element targets (whatever the option, the filtered set must match the tile's number exactly):

ElementFilter
Total leadsBase population (source + month)
Went aheadBase + Job_Taken_On1 not empty + not Cancelled
BookedBase + CM Hire_Status = Booked + not Cancelled/Went
PendingBase + the residual (via saved view mirroring the precedence rules)
CancelledBase + CM Status in (Cancel, Reject) OR Is_Claim_Accepted = Declined
Claim-type rowThe outcome filter + that group's Claim_Type1 values
File-number linksDirect record view: #Report:All_Claims?ID=<rec.ID> (or the record's detail URL)

5. Acceptance checklist​

  • July 2026 + Accident Claims Helpline returns exactly the 55 reference file numbers (list in the dashboard doc)
  • Tiles sum: Went + Booked + Pending + Cancelled = Total leads (every month, every source)
  • Tile numbers = daily-table footer = claim-type row sums (single dataset — must agree by construction)
  • Each drill-down opens a report whose record count equals the number on the element clicked
  • unmappedTypes = 0 for July 2026 ACH; if a new claim type appears later, the warning row shows instead of dropping records
  • Page defaults sensibly with no URL parameters (current month, ACH)
  • A month with zero leads renders an empty state, not a broken chart (guard the maxY / division logic)