Template
## Summary Closes #11. - Mirror Monica: compose can use the postcard mailing list or 1–2 USPS carrier routes, with estimated cost shown. - Place selected routes through PCM list-count orders instead of importing occupant addresses.
251 lines
8.2 KiB
JavaScript
251 lines
8.2 KiB
JavaScript
(function () {
|
||
function money(n) {
|
||
if (n === null || n === undefined || n === "") return "—";
|
||
var v = Number(n);
|
||
if (Number.isNaN(v)) return "—";
|
||
return "$" + v.toFixed(2);
|
||
}
|
||
|
||
function initPostcardRoutes(opts) {
|
||
var form = document.getElementById("campaign-compose");
|
||
if (!form) return;
|
||
var audience = document.getElementById("id_audience");
|
||
var picker = document.getElementById("postcard-route-picker");
|
||
var listCost = document.getElementById("postcard-list-cost");
|
||
var hidden = document.getElementById("id_route_targeting");
|
||
var findBtn = document.getElementById("route-find-btn");
|
||
var status = document.getElementById("route-status");
|
||
var tableWrap = document.getElementById("route-table-wrap");
|
||
var tbody = document.getElementById("route-table-body");
|
||
var selectedBox = document.getElementById("route-selected-cost");
|
||
var quoteUrl = opts.quoteUrl;
|
||
var csrfToken = opts.csrfToken;
|
||
var maxRoutes = 2;
|
||
var routes = [];
|
||
|
||
function isPostcardAudience() {
|
||
var v = (audience && audience.value) || "";
|
||
return v === "postcard_opt_in" || v === "carrier_route";
|
||
}
|
||
|
||
function isRouteAudience() {
|
||
return ((audience && audience.value) || "") === "carrier_route";
|
||
}
|
||
|
||
function templateId() {
|
||
return (document.getElementById("id_template_id") || {}).value || "";
|
||
}
|
||
|
||
function setStatus(text, isError) {
|
||
if (!status) return;
|
||
status.textContent = text || "";
|
||
status.classList.toggle("error", !!isError);
|
||
}
|
||
|
||
function selectedCodes() {
|
||
return Array.prototype.map.call(
|
||
form.querySelectorAll('input[name="route_code"]:checked'),
|
||
function (el) { return el.value; }
|
||
);
|
||
}
|
||
|
||
function postQuote(body) {
|
||
return fetch(quoteUrl, {
|
||
method: "POST",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
"X-CSRFToken": csrfToken,
|
||
Accept: "application/json"
|
||
},
|
||
credentials: "same-origin",
|
||
body: JSON.stringify(body)
|
||
}).then(function (res) {
|
||
return res.json().then(function (json) {
|
||
if (!res.ok) throw new Error(json.detail || "Quote failed");
|
||
return json;
|
||
});
|
||
});
|
||
}
|
||
|
||
function renderListCost(data) {
|
||
if (!listCost) return;
|
||
if (!isPostcardAudience() || isRouteAudience()) {
|
||
listCost.hidden = true;
|
||
return;
|
||
}
|
||
listCost.hidden = false;
|
||
var src = data.price_source === "fallback" ? " (env fallback rate)" : "";
|
||
if (data.per_piece == null) {
|
||
listCost.innerHTML =
|
||
"<strong>Estimated cost:</strong> " + data.count +
|
||
" household" + (data.count === 1 ? "" : "s") +
|
||
" · per-piece price unavailable. Set PCM_POSTCARD_PRICE_STANDARD / FIRSTCLASS.";
|
||
return;
|
||
}
|
||
listCost.innerHTML =
|
||
"<strong>Estimated cost:</strong> " + data.count +
|
||
" × " + money(data.per_piece) + " = " + money(data.total) +
|
||
" (" + data.mail_class + src + ")";
|
||
}
|
||
|
||
function refreshListCost() {
|
||
if (!isPostcardAudience() || isRouteAudience()) {
|
||
if (listCost) listCost.hidden = true;
|
||
return;
|
||
}
|
||
postQuote({ action: "mailing_list", template_id: templateId() })
|
||
.then(renderListCost)
|
||
.catch(function (err) {
|
||
if (listCost) {
|
||
listCost.hidden = false;
|
||
listCost.textContent = err.message || "Could not load mailing-list cost.";
|
||
}
|
||
});
|
||
}
|
||
|
||
function writeHidden(payload) {
|
||
if (hidden) hidden.value = payload ? JSON.stringify(payload) : "";
|
||
}
|
||
|
||
function renderSelected(data) {
|
||
if (!selectedBox) return;
|
||
if (!data) {
|
||
selectedBox.hidden = true;
|
||
writeHidden("");
|
||
return;
|
||
}
|
||
selectedBox.hidden = false;
|
||
var src = data.price_source === "fallback" ? " using env fallback rate" : "";
|
||
var missing = data.per_piece == null
|
||
? " Per-piece price unavailable — set PCM_POSTCARD_PRICE_* before sending."
|
||
: "";
|
||
selectedBox.innerHTML =
|
||
"<strong>Selected:</strong> " + (data.record_count || 0) +
|
||
" household" + ((data.record_count || 0) === 1 ? "" : "s") +
|
||
" on " + (data.selected_codes || []).join(", ") +
|
||
" · " + (data.mail_class || "") +
|
||
" · est. " + money(data.total) + src + "." + missing;
|
||
writeHidden({
|
||
selected_codes: data.selected_codes,
|
||
list_count_id: data.list_count_id,
|
||
record_count: data.record_count,
|
||
mail_class: data.mail_class,
|
||
estimated: data.estimated,
|
||
route_rows: data.route_rows || data.routes
|
||
});
|
||
}
|
||
|
||
function recount() {
|
||
var codes = selectedCodes();
|
||
if (!codes.length) {
|
||
renderSelected(null);
|
||
setStatus("Select one or two routes.");
|
||
return;
|
||
}
|
||
setStatus("Updating quote…");
|
||
postQuote({
|
||
action: "select",
|
||
template_id: templateId(),
|
||
routes: codes
|
||
}).then(function (data) {
|
||
setStatus("");
|
||
renderSelected(data);
|
||
}).catch(function (err) {
|
||
renderSelected(null);
|
||
setStatus(err.message || "Could not quote selected routes.", true);
|
||
});
|
||
}
|
||
|
||
function renderRoutes(list) {
|
||
routes = list || [];
|
||
if (!tbody || !tableWrap) return;
|
||
if (!routes.length) {
|
||
tableWrap.hidden = true;
|
||
tbody.innerHTML = "";
|
||
return;
|
||
}
|
||
tableWrap.hidden = false;
|
||
tbody.innerHTML = routes.map(function (row) {
|
||
return "<tr>" +
|
||
"<td><label class=\"check-row\" style=\"margin:0\">" +
|
||
"<input type=\"checkbox\" name=\"route_code\" value=\"" +
|
||
String(row.code).replace(/"/g, """) + "\"> " +
|
||
String(row.code) + "</label></td>" +
|
||
"<td>" + String(row.text || "") + "</td>" +
|
||
"<td>" + String(row.total) + "</td>" +
|
||
"<td>" + money(row.cost) + "</td></tr>";
|
||
}).join("");
|
||
tbody.querySelectorAll('input[name="route_code"]').forEach(function (box) {
|
||
box.addEventListener("change", function () {
|
||
var checked = selectedCodes();
|
||
if (checked.length > maxRoutes) {
|
||
box.checked = false;
|
||
setStatus("Pick at most two routes.", true);
|
||
return;
|
||
}
|
||
recount();
|
||
});
|
||
});
|
||
}
|
||
|
||
function findRoutes() {
|
||
var zip = (document.getElementById("route-zip") || {}).value || "";
|
||
var address = (document.getElementById("route-address") || {}).value || "";
|
||
var city = (document.getElementById("route-city") || {}).value || "";
|
||
var state = (document.getElementById("route-state") || {}).value || "";
|
||
var radius = (document.getElementById("route-radius") || {}).value || "";
|
||
setStatus("Looking up routes…");
|
||
tableWrap.hidden = true;
|
||
renderSelected(null);
|
||
postQuote({
|
||
action: "search",
|
||
template_id: templateId(),
|
||
zip: zip,
|
||
address: address,
|
||
city: city,
|
||
state: state,
|
||
radius: radius
|
||
}).then(function (data) {
|
||
renderRoutes(data.routes || []);
|
||
if (!(data.routes || []).length) {
|
||
setStatus("No carrier routes for that search.", true);
|
||
} else {
|
||
setStatus("Select one or two routes. Cost uses " + (data.mail_class || "Standard") + ".");
|
||
}
|
||
}).catch(function (err) {
|
||
renderRoutes([]);
|
||
setStatus(err.message || "Route lookup failed.", true);
|
||
});
|
||
}
|
||
|
||
function syncVisibility() {
|
||
if (picker) picker.hidden = !isRouteAudience();
|
||
if (!isRouteAudience()) {
|
||
writeHidden("");
|
||
if (selectedBox) selectedBox.hidden = true;
|
||
}
|
||
refreshListCost();
|
||
}
|
||
|
||
if (findBtn) findBtn.addEventListener("click", function (e) {
|
||
e.preventDefault();
|
||
findRoutes();
|
||
});
|
||
var tmpl = document.getElementById("id_template_id");
|
||
if (tmpl) tmpl.addEventListener("change", function () {
|
||
syncVisibility();
|
||
if (isRouteAudience() && selectedCodes().length) recount();
|
||
});
|
||
if (audience) audience.addEventListener("change", syncVisibility);
|
||
syncVisibility();
|
||
|
||
window.MonicaPostcardRoutes = {
|
||
isPostcardAudience: isPostcardAudience,
|
||
isRouteAudience: isRouteAudience,
|
||
syncVisibility: syncVisibility
|
||
};
|
||
}
|
||
|
||
window.initPostcardRoutes = initPostcardRoutes;
|
||
})();
|