- analysis.py: deterministic claim scorer + /analyze /approve /letter /advance-tier endpoints (auto-runs on intake) - packet.py + packet_fields.json: welcome-packet templating engine (6 onboarding docs, field catalog) - letterstream.py + letters.py: certified-mail send pipeline + letter lifecycle (webhook verified) - docuseal.py: DocuSeal signing integration - staff.py/models.py/schema.sql/auth.py: approval actor from staff key, tier gate (APPROVED+ACTIVE+onboarding docs), onboarding_docs table - frontend/: dependency-free static portal (intake, magic-link login/verify, dashboard) - landing-mockups/: 4 design-stance mockups + favicons - legal/: aup/privacy/sms-terms/terms HTML - docs/: letter-queue scope, letterstream API contract, 6 welcome-packet templates - review-dre-landing-2026-08-21.md: 3-variant landing feedback sprint - compliance/DRE_Compliance_Manual.md: updated Source synced from deployed /opt/dre-portal/app/ (was 4 days ahead of git).
209 lines
5.8 KiB
JavaScript
209 lines
5.8 KiB
JavaScript
/* ==========================================================================
|
|
DRE Customer Portal — shared JS helpers
|
|
Dependency-free. Relative /api/* calls only (same-origin; Caddy proxies).
|
|
========================================================================== */
|
|
|
|
(function (global) {
|
|
"use strict";
|
|
|
|
var SESSION_KEY = "dre_session_token";
|
|
|
|
/**
|
|
* Perform a JSON fetch against the API.
|
|
* @param {string} path - relative API path, e.g. "/api/claims"
|
|
* @param {object} opts - { method, body, auth, headers }
|
|
* Returns a Promise resolving to { ok, status, data } where data is the
|
|
* parsed JSON body (success payload or {error:{code,message}}).
|
|
*/
|
|
function apiFetch(path, opts) {
|
|
opts = opts || {};
|
|
var headers = Object.assign({}, opts.headers || {});
|
|
var fetchOpts = { method: opts.method || "GET", headers: headers };
|
|
|
|
if (opts.body !== undefined && !(opts.body instanceof FormData)) {
|
|
headers["Content-Type"] = "application/json";
|
|
fetchOpts.body = JSON.stringify(opts.body);
|
|
} else if (opts.body instanceof FormData) {
|
|
fetchOpts.body = opts.body; // browser sets multipart boundary
|
|
}
|
|
|
|
if (opts.auth) {
|
|
var token = getSessionToken();
|
|
if (token) {
|
|
headers["Authorization"] = "Bearer " + token;
|
|
}
|
|
}
|
|
|
|
return fetch(path, fetchOpts)
|
|
.then(function (res) {
|
|
return res
|
|
.json()
|
|
.catch(function () {
|
|
return {};
|
|
})
|
|
.then(function (data) {
|
|
return { ok: res.ok, status: res.status, data: data };
|
|
});
|
|
})
|
|
.catch(function (err) {
|
|
return {
|
|
ok: false,
|
|
status: 0,
|
|
data: {
|
|
error: {
|
|
code: "network_error",
|
|
message: "Could not reach the server. Check your connection and try again.",
|
|
},
|
|
},
|
|
};
|
|
});
|
|
}
|
|
|
|
function getErrorMessage(result, fallback) {
|
|
if (result && result.data && result.data.error && result.data.error.message) {
|
|
return result.data.error.message;
|
|
}
|
|
return fallback || "Something went wrong. Please try again.";
|
|
}
|
|
|
|
function getErrorCode(result) {
|
|
if (result && result.data && result.data.error && result.data.error.code) {
|
|
return result.data.error.code;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function getSessionToken() {
|
|
try {
|
|
return localStorage.getItem(SESSION_KEY);
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function setSessionToken(token) {
|
|
try {
|
|
localStorage.setItem(SESSION_KEY, token);
|
|
} catch (e) {
|
|
/* localStorage unavailable; session will not persist across reload */
|
|
}
|
|
}
|
|
|
|
function clearSessionToken() {
|
|
try {
|
|
localStorage.removeItem(SESSION_KEY);
|
|
} catch (e) {
|
|
/* ignore */
|
|
}
|
|
}
|
|
|
|
function requireSessionOrRedirect(loginUrl) {
|
|
var token = getSessionToken();
|
|
if (!token) {
|
|
window.location.href = loginUrl || "login.html";
|
|
return null;
|
|
}
|
|
return token;
|
|
}
|
|
|
|
function formatCentsUSD(cents) {
|
|
var n = typeof cents === "number" ? cents : parseInt(cents, 10) || 0;
|
|
return "$" + (n / 100).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
}
|
|
|
|
function formatDate(iso) {
|
|
if (!iso) return "--";
|
|
try {
|
|
var d = new Date(iso);
|
|
if (isNaN(d.getTime())) return iso;
|
|
return d.toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" });
|
|
} catch (e) {
|
|
return iso;
|
|
}
|
|
}
|
|
|
|
function formatDateTime(iso) {
|
|
if (!iso) return "--";
|
|
try {
|
|
var d = new Date(iso);
|
|
if (isNaN(d.getTime())) return iso;
|
|
return d.toLocaleString("en-US", {
|
|
year: "numeric",
|
|
month: "short",
|
|
day: "numeric",
|
|
hour: "numeric",
|
|
minute: "2-digit",
|
|
});
|
|
} catch (e) {
|
|
return iso;
|
|
}
|
|
}
|
|
|
|
function formatBytes(n) {
|
|
if (!n && n !== 0) return "";
|
|
if (n < 1024) return n + " B";
|
|
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + " KB";
|
|
return (n / (1024 * 1024)).toFixed(1) + " MB";
|
|
}
|
|
|
|
function showAlert(el, message, isVisible) {
|
|
if (!el) return;
|
|
el.textContent = message || "";
|
|
if (isVisible === false) {
|
|
el.classList.remove("is-visible");
|
|
} else {
|
|
el.classList.add("is-visible");
|
|
}
|
|
}
|
|
|
|
function hideAlert(el) {
|
|
if (!el) return;
|
|
el.classList.remove("is-visible");
|
|
el.textContent = "";
|
|
}
|
|
|
|
function setLoading(button, loading, loadingText, normalText) {
|
|
if (!button) return;
|
|
if (loading) {
|
|
button.disabled = true;
|
|
button.dataset.originalText = button.dataset.originalText || button.innerHTML;
|
|
button.innerHTML = '<span class="dre-spinner"></span> ' + (loadingText || "Please wait...");
|
|
} else {
|
|
button.disabled = false;
|
|
button.innerHTML = normalText || button.dataset.originalText || button.innerHTML;
|
|
}
|
|
}
|
|
|
|
function badgeClass(status) {
|
|
return "dre-badge dre-badge-" + (status || "").toLowerCase();
|
|
}
|
|
|
|
// Escape helper for any spot where we must build markup with dynamic text.
|
|
// Prefer textContent everywhere; this exists only as a defensive fallback.
|
|
function escapeHtml(str) {
|
|
var div = document.createElement("div");
|
|
div.textContent = str === undefined || str === null ? "" : String(str);
|
|
return div.innerHTML;
|
|
}
|
|
|
|
global.DRE = {
|
|
SESSION_KEY: SESSION_KEY,
|
|
apiFetch: apiFetch,
|
|
getErrorMessage: getErrorMessage,
|
|
getErrorCode: getErrorCode,
|
|
getSessionToken: getSessionToken,
|
|
setSessionToken: setSessionToken,
|
|
clearSessionToken: clearSessionToken,
|
|
requireSessionOrRedirect: requireSessionOrRedirect,
|
|
formatCentsUSD: formatCentsUSD,
|
|
formatDate: formatDate,
|
|
formatDateTime: formatDateTime,
|
|
formatBytes: formatBytes,
|
|
showAlert: showAlert,
|
|
hideAlert: hideAlert,
|
|
setLoading: setLoading,
|
|
badgeClass: badgeClass,
|
|
escapeHtml: escapeHtml,
|
|
};
|
|
})(window);
|