Files
dre/frontend/dashboard.html
T
root 7a62b0b340 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).
2026-08-26 02:26:33 -04:00

541 lines
20 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard - Debt Recovery Experts Client Portal</title>
<link rel="stylesheet" href="css/dre.css">
</head>
<body>
<header class="dre-header">
<div class="dre-header-inner">
<a class="dre-brand" href="dashboard.html">
<span class="dre-mark">DRE</span>
<span>
Debt Recovery Experts
<span class="dre-brand-sub">Client Portal</span>
</span>
</a>
<div class="dre-header-actions">
<span class="dre-user" id="header-user"></span>
<a href="#" id="logout-link">Log Out</a>
</div>
</div>
</header>
<main class="dre-main">
<div id="dash-alert" class="dre-alert dre-alert-error" role="alert"></div>
<!-- ============================== LOADING STATE ============================== -->
<div id="dash-loading" class="dre-state">
<div class="dre-spinner dre-spinner-dark" style="width:32px;height:32px;border-width:3px;margin:0 auto 1rem;"></div>
<p>Loading your account...</p>
</div>
<!-- ============================== LIST VIEW ============================== -->
<div id="view-list" class="dre-hidden">
<div class="dre-flex-between" style="margin-bottom:1.25rem;">
<h1 style="margin:0;">Your Claims</h1>
</div>
<div class="dre-stats-row">
<div class="dre-stat">
<div class="dre-stat-label">Total Claims</div>
<div class="dre-stat-value" id="stat-total">0</div>
</div>
<div class="dre-stat">
<div class="dre-stat-label">Open</div>
<div class="dre-stat-value" id="stat-open">0</div>
</div>
<div class="dre-stat">
<div class="dre-stat-label">Recovered / Resolved</div>
<div class="dre-stat-value" id="stat-resolved">0</div>
</div>
</div>
<div id="claim-list" class="dre-claim-list"></div>
<div id="claim-empty" class="dre-card dre-empty dre-hidden">
<h3>No Claims Yet</h3>
<p>You have not submitted any claims. Once you submit a claim, it will appear here with
live status updates.</p>
<a href="https://my.debtrecoveryexperts.com/start" class="dre-btn dre-btn-primary">Submit a Claim</a>
</div>
</div>
<!-- ============================== DETAIL VIEW ============================== -->
<div id="view-detail" class="dre-hidden">
<a href="#" id="back-to-list" class="dre-back-link">&larr; Back to all claims</a>
<div class="dre-card">
<div class="dre-flex-between">
<div>
<h2 id="detail-claim-number" style="margin-bottom:0.2rem;">--</h2>
<span id="detail-badge" class="dre-badge">--</span>
</div>
<div style="text-align:right;">
<div class="dre-stat-label">Amount</div>
<div class="dre-stat-value" id="detail-amount">--</div>
</div>
</div>
<div class="dre-progress" id="detail-progress"></div>
<div class="dre-progress-labels">
<span>Soft Touch</span>
<span>Formal Demand</span>
<span>Escalation</span>
<span>Legal Action</span>
</div>
<dl class="dre-detail-grid">
<div>
<dt>Debtor</dt>
<dd id="detail-debtor-name">--</dd>
</div>
<div>
<dt>Debtor Type</dt>
<dd id="detail-debtor-type">--</dd>
</div>
<div>
<dt>Description</dt>
<dd id="detail-description">--</dd>
</div>
<div>
<dt>Your Reference</dt>
<dd id="detail-reference">--</dd>
</div>
<div>
<dt>Invoice Date</dt>
<dd id="detail-invoice-date">--</dd>
</div>
<div>
<dt>Date Assigned</dt>
<dd id="detail-created">--</dd>
</div>
</dl>
</div>
<div class="dre-card">
<h3>Documents</h3>
<ul id="doc-list" class="dre-doc-list"></ul>
<p id="doc-empty" class="dre-small dre-hidden">No documents uploaded yet.</p>
<div id="upload-alert" class="dre-alert dre-alert-error" role="alert"></div>
<label for="doc-file-input" class="dre-dropzone" id="dropzone">
<strong>Click to choose a file</strong> or drag one here<br>
<span class="dre-small">PDF, JPG, PNG, DOC, DOCX up to 20 MB</span>
<input type="file" id="doc-file-input" accept=".pdf,.jpg,.jpeg,.png,.doc,.docx">
</label>
<div id="upload-progress" class="dre-small dre-hidden dre-mt">Uploading...</div>
</div>
<div class="dre-card">
<h3>Case Notes</h3>
<ul id="note-list" class="dre-note-list"></ul>
<p id="note-empty" class="dre-small dre-hidden">No notes yet.</p>
</div>
<div class="dre-card">
<h3>Message the Team</h3>
<div id="message-alert" class="dre-alert dre-alert-error" role="alert"></div>
<div id="message-sent" class="dre-alert dre-alert-success" role="status"></div>
<form id="message-form">
<div class="dre-field">
<label for="message-subject">Subject <span class="dre-required">*</span></label>
<select id="message-subject" required>
<option value="Question about my claim">Question about my claim</option>
<option value="New information about the debtor">New information about the debtor</option>
<option value="Payment received / want to stop recovery">Payment received / want to stop recovery</option>
<option value="Update my contact info">Update my contact info</option>
<option value="Complaint or concern">Complaint or concern</option>
<option value="Other">Other</option>
</select>
</div>
<div class="dre-field">
<label for="message-content">Message <span class="dre-required">*</span></label>
<textarea id="message-content" maxlength="10000" required placeholder="Type your message to the recovery team..."></textarea>
</div>
<button type="submit" id="message-submit" class="dre-btn dre-btn-primary">Send Message</button>
</form>
</div>
</div>
</main>
<footer class="dre-footer">
Debt Recovery Experts Client Portal. All account activity is logged for compliance purposes.
</footer>
<script src="js/dre-api.js"></script>
<script>
(function () {
"use strict";
var token = DRE.requireSessionOrRedirect("login.html");
if (!token) return; // redirect already triggered
var loadingEl = document.getElementById("dash-loading");
var listView = document.getElementById("view-list");
var detailView = document.getElementById("view-detail");
var dashAlert = document.getElementById("dash-alert");
var claimsCache = [];
function handleAuthFailure(result) {
if (result.status === 401) {
DRE.clearSessionToken();
window.location.href = "login.html";
return true;
}
return false;
}
function logout() {
DRE.apiFetch("/api/auth/logout", { method: "POST", auth: true }).then(function () {
DRE.clearSessionToken();
window.location.href = "login.html";
});
}
document.getElementById("logout-link").addEventListener("click", function (e) {
e.preventDefault();
logout();
});
// ---------------------------------------------------------- init: /me
DRE.apiFetch("/api/auth/me", { auth: true }).then(function (result) {
if (!result.ok) {
if (handleAuthFailure(result)) return;
loadingEl.classList.add("dre-hidden");
DRE.showAlert(dashAlert, DRE.getErrorMessage(result, "Could not load your account."));
return;
}
var client = result.data.client || {};
document.getElementById("header-user").textContent =
(client.contact_name || "") + (client.company_name ? " - " + client.company_name : "");
loadClaims();
});
// ---------------------------------------------------------- claim list
function loadClaims() {
DRE.apiFetch("/api/claims", { auth: true }).then(function (result) {
loadingEl.classList.add("dre-hidden");
if (!result.ok) {
if (handleAuthFailure(result)) return;
DRE.showAlert(dashAlert, DRE.getErrorMessage(result, "Could not load your claims."));
listView.classList.remove("dre-hidden");
return;
}
claimsCache = result.data.claims || [];
renderClaimList(claimsCache);
listView.classList.remove("dre-hidden");
routeFromHash();
});
}
function renderClaimList(claims) {
var listEl = document.getElementById("claim-list");
var emptyEl = document.getElementById("claim-empty");
listEl.innerHTML = "";
var openCount = 0, resolvedCount = 0;
var openStatuses = { NEW: 1, UNDER_REVIEW: 1, ACTIVE: 1, NEGOTIATION: 1, LEGAL: 1 };
var resolvedStatuses = { SETTLED: 1, CLOSED: 1, WRITE_OFF: 1 };
claims.forEach(function (c) {
if (openStatuses[c.status]) openCount++;
if (resolvedStatuses[c.status]) resolvedCount++;
var row = document.createElement("div");
row.className = "dre-claim-row";
row.setAttribute("role", "button");
row.setAttribute("tabindex", "0");
var main = document.createElement("div");
main.className = "dre-claim-main";
var num = document.createElement("span");
num.className = "dre-claim-number";
num.textContent = c.claim_number;
var sub = document.createElement("span");
sub.className = "dre-claim-sub";
sub.textContent = "Debtor: " + (c.debtor_name || "--");
var badge = document.createElement("span");
badge.className = DRE.badgeClass(c.status);
badge.textContent = c.status_label || c.status;
badge.style.marginTop = "0.3rem";
badge.style.width = "fit-content";
main.appendChild(num);
main.appendChild(sub);
main.appendChild(badge);
var right = document.createElement("div");
var amt = document.createElement("div");
amt.className = "dre-claim-amount";
amt.textContent = c.amount_display || DRE.formatCentsUSD(c.amount_cents);
var meta = document.createElement("div");
meta.className = "dre-claim-meta";
meta.textContent = "Submitted " + DRE.formatDate(c.created_at);
right.appendChild(amt);
right.appendChild(meta);
row.appendChild(main);
row.appendChild(right);
row.addEventListener("click", function () {
window.location.hash = "claim/" + encodeURIComponent(c.claim_number);
});
row.addEventListener("keypress", function (e) {
if (e.key === "Enter") window.location.hash = "claim/" + encodeURIComponent(c.claim_number);
});
listEl.appendChild(row);
});
document.getElementById("stat-total").textContent = claims.length;
document.getElementById("stat-open").textContent = openCount;
document.getElementById("stat-resolved").textContent = resolvedCount;
emptyEl.classList.toggle("dre-hidden", claims.length !== 0);
listEl.classList.toggle("dre-hidden", claims.length === 0);
}
// ---------------------------------------------------------- claim detail
var currentClaimNumber = null;
function showListView() {
detailView.classList.add("dre-hidden");
listView.classList.remove("dre-hidden");
}
function showDetailView(claimNumber) {
currentClaimNumber = claimNumber;
listView.classList.add("dre-hidden");
detailView.classList.remove("dre-hidden");
loadClaimDetail(claimNumber);
}
document.getElementById("back-to-list").addEventListener("click", function (e) {
e.preventDefault();
window.location.hash = "";
});
var TIER_LABELS = {
TIER_1: "Soft Touch", TIER_2: "Formal Demand", TIER_2_5: "Lien Threat",
TIER_3: "Escalation", TIER_4: "Legal Action",
};
function loadClaimDetail(claimNumber) {
DRE.hideAlert(dashAlert);
DRE.apiFetch("/api/claims/" + encodeURIComponent(claimNumber), { auth: true }).then(function (result) {
if (!result.ok) {
if (handleAuthFailure(result)) return;
if (result.status === 404) {
DRE.showAlert(dashAlert, "That claim was not found on your account.");
window.location.hash = "";
return;
}
DRE.showAlert(dashAlert, DRE.getErrorMessage(result, "Could not load claim detail."));
return;
}
renderClaimDetail(result.data);
});
}
function renderClaimDetail(c) {
document.getElementById("detail-claim-number").textContent = c.claim_number;
var badge = document.getElementById("detail-badge");
badge.className = DRE.badgeClass(c.status);
badge.textContent = c.status_label || c.status;
document.getElementById("detail-amount").textContent = c.amount_display || DRE.formatCentsUSD(c.amount_cents);
document.getElementById("detail-debtor-name").textContent = (c.debtor && c.debtor.name) || "--";
document.getElementById("detail-debtor-type").textContent = (c.debtor && c.debtor.business_type) || "--";
document.getElementById("detail-description").textContent = c.description || "Not provided";
document.getElementById("detail-reference").textContent = c.client_reference || "Not provided";
document.getElementById("detail-invoice-date").textContent = c.invoice_date ? DRE.formatDate(c.invoice_date) : "Not provided";
document.getElementById("detail-created").textContent = c.date_assigned ? DRE.formatDate(c.date_assigned) : "Not yet assigned";
// progress bar
var progressEl = document.getElementById("detail-progress");
progressEl.innerHTML = "";
var step = c.tier_step || 1;
for (var i = 1; i <= 4; i++) {
var seg = document.createElement("div");
seg.className = "dre-progress-step" + (i <= step ? " is-done" : "");
progressEl.appendChild(seg);
}
// documents
var docList = document.getElementById("doc-list");
var docEmpty = document.getElementById("doc-empty");
docList.innerHTML = "";
var docs = c.documents || [];
docs.forEach(function (d) {
var li = document.createElement("li");
li.className = "dre-doc-item";
var left = document.createElement("div");
var name = document.createElement("div");
name.className = "dre-doc-name";
name.textContent = d.original_name;
var meta = document.createElement("div");
meta.className = "dre-doc-meta";
meta.textContent = DRE.formatBytes(d.size_bytes) + " - Uploaded " + DRE.formatDateTime(d.created_at);
left.appendChild(name);
left.appendChild(meta);
li.appendChild(left);
docList.appendChild(li);
});
docEmpty.classList.toggle("dre-hidden", docs.length !== 0);
// notes
var noteList = document.getElementById("note-list");
var noteEmpty = document.getElementById("note-empty");
noteList.innerHTML = "";
var notes = c.notes || [];
notes.forEach(function (n) {
var li = document.createElement("li");
li.className = "dre-note-item";
var head = document.createElement("div");
head.className = "dre-note-head";
var author = document.createElement("span");
author.className = "dre-note-author";
author.textContent = n.author_name + (n.subject ? " - " + n.subject : "");
var when = document.createElement("span");
when.textContent = DRE.formatDateTime(n.created_at);
head.appendChild(author);
head.appendChild(when);
var body = document.createElement("div");
body.className = "dre-note-body";
// Backend already HTML-escapes content; we still use textContent so
// it renders literally either way (defense in depth, no innerHTML).
body.textContent = n.content;
li.appendChild(head);
li.appendChild(body);
noteList.appendChild(li);
});
noteEmpty.classList.toggle("dre-hidden", notes.length !== 0);
}
// ---------------------------------------------------------- document upload
var fileInput = document.getElementById("doc-file-input");
var dropzone = document.getElementById("dropzone");
var uploadAlert = document.getElementById("upload-alert");
var uploadProgress = document.getElementById("upload-progress");
fileInput.addEventListener("change", function () {
if (fileInput.files && fileInput.files[0]) {
uploadFile(fileInput.files[0]);
}
});
["dragover", "dragenter"].forEach(function (evt) {
dropzone.addEventListener(evt, function (e) {
e.preventDefault();
dropzone.classList.add("is-dragover");
});
});
["dragleave", "drop"].forEach(function (evt) {
dropzone.addEventListener(evt, function (e) {
e.preventDefault();
dropzone.classList.remove("is-dragover");
});
});
dropzone.addEventListener("drop", function (e) {
e.preventDefault();
var files = e.dataTransfer && e.dataTransfer.files;
if (files && files[0]) uploadFile(files[0]);
});
function uploadFile(file) {
DRE.hideAlert(uploadAlert);
if (file.size > 20 * 1024 * 1024) {
DRE.showAlert(uploadAlert, "File exceeds the 20 MB limit.");
fileInput.value = "";
return;
}
var allowedExt = [".pdf", ".jpg", ".jpeg", ".png", ".doc", ".docx"];
var lower = file.name.toLowerCase();
var ok = allowedExt.some(function (ext) { return lower.endsWith(ext); });
if (!ok) {
DRE.showAlert(uploadAlert, "File type not allowed. Use PDF, JPG, PNG, DOC, or DOCX.");
fileInput.value = "";
return;
}
var formData = new FormData();
formData.append("file", file);
uploadProgress.classList.remove("dre-hidden");
DRE.apiFetch("/api/claims/" + encodeURIComponent(currentClaimNumber) + "/documents", {
method: "POST",
body: formData,
auth: true,
}).then(function (result) {
uploadProgress.classList.add("dre-hidden");
fileInput.value = "";
if (!result.ok) {
if (handleAuthFailure(result)) return;
DRE.showAlert(uploadAlert, DRE.getErrorMessage(result, "Upload failed. Please try again."));
return;
}
loadClaimDetail(currentClaimNumber);
});
}
// ---------------------------------------------------------- message the team
var messageForm = document.getElementById("message-form");
var messageAlert = document.getElementById("message-alert");
var messageSent = document.getElementById("message-sent");
var messageSubmit = document.getElementById("message-submit");
messageForm.addEventListener("submit", function (e) {
e.preventDefault();
DRE.hideAlert(messageAlert);
DRE.hideAlert(messageSent);
var subject = document.getElementById("message-subject").value;
var content = document.getElementById("message-content").value.trim();
if (!content) {
DRE.showAlert(messageAlert, "Please enter a message before sending.");
return;
}
DRE.setLoading(messageSubmit, true, "Sending...");
DRE.apiFetch("/api/claims/" + encodeURIComponent(currentClaimNumber) + "/messages", {
method: "POST",
body: { subject: subject, content: content },
auth: true,
}).then(function (result) {
DRE.setLoading(messageSubmit, false, null, "Send Message");
if (!result.ok) {
if (handleAuthFailure(result)) return;
DRE.showAlert(messageAlert, DRE.getErrorMessage(result, "Could not send your message. Please try again."));
return;
}
document.getElementById("message-content").value = "";
DRE.showAlert(messageSent, "Message sent. Our team will follow up if needed.");
loadClaimDetail(currentClaimNumber);
});
});
// ---------------------------------------------------------- hash routing
function routeFromHash() {
var hash = window.location.hash.replace(/^#/, "");
if (hash.indexOf("claim/") === 0) {
var claimNumber = decodeURIComponent(hash.slice("claim/".length));
showDetailView(claimNumber);
} else {
showListView();
}
}
window.addEventListener("hashchange", routeFromHash);
})();
</script>
</body>
</html>