Backend Aug 22-25: AI analysis, welcome-packet templating, LetterStream, DocuSeal, staff RBAC + tier gate
- 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).
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
/* ==========================================================================
|
||||
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);
|
||||
Reference in New Issue
Block a user