const DEFAULT_INSTITUTION_TYPE = "bank";
const QUICK_REQUEST_ENABLED = false;
const REGISTRATION_SESSION_KEY = "registrationWorkflowStateV1";
const INITIAL_QUERY = new URLSearchParams(window.location.search);
const WORKFLOW_GUID = INITIAL_QUERY.get("workflow_guid") || "";
const PRICING_PLAN_QUERY = String(INITIAL_QUERY.get("plan") || "").trim().toLowerCase();
const EXPLICIT_PRICING_PLAN = INITIAL_QUERY.get("pricing_reviewed") === "1"
? (["pro", "professional"].includes(PRICING_PLAN_QUERY) ? "pro" : "free")
: "";
const CHECKOUT_RETURN_STATUS = INITIAL_QUERY.get("upgrade") === "1"
? (INITIAL_QUERY.get("checkout") || "")
: "";
const CHECKOUT_SESSION_ID = CHECKOUT_RETURN_STATUS === "success"
? (INITIAL_QUERY.get("session_id") || "")
: "";
const REGISTRATION_STEPS = ["1", "2", "3", "3b", "4"];
const REGISTRATION_STEP_PATHS = {
1: "/registration/step1",
2: "/registration/step2",
3: "/registration/step3",
"3b": "/registration/step3b",
4: "/registration/step4",
};
const helperContent = {
step1: {
text: "Select Bank or Credit Union to begin.",
anchorId: "anchor-step1",
target: () => els.typeToggle,
},
step2: {
text: "Type the institution name, add optional city/state hints, then choose the official match.",
anchorId: "anchor-step2",
target: () => els.subjectSearch,
},
step2b: {
text: [
"Optional: Add up to three additional institutions for a full comparative credit analysis, including Reg F documentation and supporting credit materials. Click Next when finished.",
"0 of 3 additional institutions selected.",
].join("\n\n"),
anchorId: "anchor-step2b",
target: () => els.peerSearches[0],
},
step3: {
text: "Select the reporting periods below.",
anchorId: "anchor-step3",
target: () => els.quarterSelects.find((select) => !select.disabled) || els.quarterSelects[0],
},
};
const state = {
institutionType: DEFAULT_INSTITUTION_TYPE,
subject: null,
additional: [null, null, null],
validation: {
institutionTypeSelected: false,
subjectSelected: false,
bankPeriodReady: true,
peersSelected: 0,
reportFormatSelected: true,
reportingPeriodValid: false,
userFieldsComplete: false,
disclaimerAccepted: false,
},
ui: {
activeAccordion: "institutionTypePanel",
routeStep: "1",
reviewAutoCollapsed: false,
reviewPrimed: false,
submitting: false,
submitSucceeded: false,
wasSubmitReady: false,
pendingFocus: "",
isBrandingWindowOpen: false,
submissionSuccessOpen: false,
institutionTypeChosen: false,
forceRegeneration: false,
previousOfferKey: "",
},
quickRequest: {
open: false,
applied: false,
activeRow: 0,
rows: [],
},
reportingPeriods: {
status: "ready",
requestId: 0,
error: "",
},
bankPeriodConfirmation: {
status: "idle",
confirmed: false,
rssd_id: "",
latest_period: "",
period_end: "",
official_ubpr_peer_period_label: "",
official_ubpr_peer_period_end: "",
official_ubpr_peer_source: "",
bulk_comparison_period_label: "",
bulk_comparison_period_end: "",
bulk_comparison_source: "",
bulk_comparison_data_lagging: false,
verified_at: "",
source: "",
error: "",
},
cuPeriodConfirmation: {
status: "idle",
confirmed: false,
charter_number: "",
latest_period: "",
period_end: "",
verified_at: "",
source: "",
error: "",
},
logo: {
logo_name: "",
logo_data_url: "",
logo_source_url: "",
logo_search_url: "",
autoExtract: true,
colors: {
primaryBg: "#0b3d66",
cardBg: "#1f4f7a",
textColor: "#f8fafc",
accentColor: "#d4af37",
},
},
account: {
email: "",
verified: false,
verifying: false,
paid: false,
selectedPlan: WORKFLOW_GUID || EXPLICIT_PRICING_PLAN === "pro" || CHECKOUT_SESSION_ID ? "pro" : "free",
message: WORKFLOW_GUID
? "Loading paid workflow access..."
: EXPLICIT_PRICING_PLAN === "pro"
? "Professional selected. Complete Step 4 to continue to secure payment."
: "Free request. You can choose current-data access before submission.",
error: "",
requestId: 0,
},
periodAvailability: {
loading: false,
loaded: false,
error: "",
options: [],
requestId: 0,
stale: false,
verifiedAt: "",
},
workflow: {
guid: WORKFLOW_GUID,
loaded: false,
lockedFields: [],
},
checkout: {
returnStatus: CHECKOUT_RETURN_STATUS,
sessionId: CHECKOUT_SESSION_ID,
requestedPlan: WORKFLOW_GUID || EXPLICIT_PRICING_PLAN === "pro" || CHECKOUT_SESSION_ID ? "pro" : "free",
modalOpen: false,
latestPeriodLabel: "",
},
};
const els = {
form: document.getElementById("registrationForm"),
pageTitle: document.getElementById("pageTitle"),
pageSubtext: document.getElementById("pageSubtext"),
workflowScroll: document.querySelector(".workflow-scroll"),
leftRailStack: document.querySelector(".left-rail-stack"),
workflowScrollHint: document.getElementById("workflowScrollHint"),
helperPanel: document.getElementById("helperPanel"),
helperText: document.getElementById("helperText"),
helperArrow: document.getElementById("helperArrow"),
typeButtons: Array.from(document.querySelectorAll(".type-option")),
typeToggle: document.querySelector(".type-toggle"),
typeWarning: document.getElementById("typeWarning"),
clearSelectionsButton: document.getElementById("clearSelectionsButton"),
subjectSearch: document.getElementById("subjectSearch"),
subjectStateHint: document.getElementById("subjectStateHint"),
subjectCityHint: document.getElementById("subjectCityHint"),
subjectSearchButton: document.getElementById("subjectSearchButton"),
subjectNextIndicator: document.getElementById("subjectNextIndicator"),
subjectResultsHint: document.getElementById("subjectResultsHint"),
subjectResults: document.getElementById("subjectResults"),
subjectSelection: document.getElementById("subjectSelection"),
logoPanel: document.getElementById("logoPanel"),
logoLocked: document.getElementById("logoLocked"),
brandingStageShell: document.getElementById("brandingStageShell"),
brandingStage: document.getElementById("brandingStage"),
logoControls: document.getElementById("logoControls"),
logoSearchQuery: document.getElementById("logoSearchQuery"),
logoSearchButton: document.getElementById("logoSearchButton"),
logoUpload: document.getElementById("logoUpload"),
logoPreview: document.getElementById("logoPreview"),
logoSelectionStatus: document.getElementById("logoSelectionStatus"),
autoColorToggle: document.getElementById("autoColorToggle"),
resetBrandingButton: document.getElementById("resetBrandingButton"),
presetCards: Array.from(document.querySelectorAll(".preset-card")),
brandPreviewInstitution: document.getElementById("brandPreviewInstitution"),
brandPreviewTitle: document.getElementById("brandPreviewTitle"),
brandPreviewSubtitle: document.getElementById("brandPreviewSubtitle"),
brandPreview: document.getElementById("brandPreview"),
brandPreviewLogo: document.getElementById("brandPreviewLogo"),
brandingCloseContinueButton: document.getElementById("brandingCloseContinueButton"),
peerSlots: Array.from(document.querySelectorAll(".peer-slot")),
peerSearches: Array.from(document.querySelectorAll(".peer-search")),
peerStateHints: Array.from(document.querySelectorAll(".peer-state-hint")),
peerCityHints: Array.from(document.querySelectorAll(".peer-city-hint")),
peerSearchButtons: Array.from(document.querySelectorAll(".peer-search-button")),
peerResults: Array.from(document.querySelectorAll(".peer-results")),
peerSelections: Array.from(document.querySelectorAll(".peer-selection")),
additionalHint: document.getElementById("additionalHint"),
additionalCounter: document.getElementById("additionalCounter"),
reportPanel: document.getElementById("reportPanel"),
reportFormatChoice: document.getElementById("reportFormatChoice"),
reportFormatModes: Array.from(document.querySelectorAll('input[name="reportFormatMode"]')),
quarterSelects: Array.from(document.querySelectorAll(".quarter-select")),
periodHint: document.getElementById("periodHint"),
periodError: document.getElementById("periodError"),
submitButton: document.getElementById("submitButton"),
cancelRequestButton: document.getElementById("cancelRequestButton"),
submitHelper: document.getElementById("submitHelper"),
accountVerificationStatus: document.getElementById("accountVerificationStatus"),
registrationInfoTitle: document.getElementById("registrationInfoTitle"),
registrationInfoDescription: document.getElementById("registrationInfoDescription"),
professionalSignupNotice: document.getElementById("professionalSignupNotice"),
freeUpgrade: {
modal: document.getElementById("freeUpgradeModal"),
latestPeriod: document.getElementById("freeUpgradeLatestPeriod"),
status: document.getElementById("freeUpgradeStatus"),
upgradeButton: document.getElementById("freeUpgradePaidButton"),
freeButton: document.getElementById("freeUpgradeContinueButton"),
},
formStatus: document.getElementById("formStatus"),
disclaimerAck: document.getElementById("disclaimerAck"),
bankPeriodDisclosure: document.getElementById("bankPeriodDisclosure"),
bankPeriodDisclosurePeriod: document.getElementById("bankPeriodDisclosurePeriod"),
bankPeriodDisclosureInstitution: document.getElementById("bankPeriodDisclosureInstitution"),
bankPeriodDisclosureStatus: document.getElementById("bankPeriodDisclosureStatus"),
bankPeriodDisclosureLanguage: document.getElementById("bankPeriodDisclosureLanguage"),
bankPeriodRetryButton: document.getElementById("bankPeriodRetryButton"),
colorInputs: {
primaryBg: document.getElementById("primaryBg"),
cardBg: document.getElementById("cardBg"),
textColor: document.getElementById("textColor"),
accentColor: document.getElementById("accentColor"),
},
colorHexInputs: {
primaryBg: document.getElementById("primaryBgHex"),
cardBg: document.getElementById("cardBgHex"),
textColor: document.getElementById("textColorHex"),
accentColor: document.getElementById("accentColorHex"),
},
accordionPanels: Array.from(document.querySelectorAll(".accordion-panel")),
accordionTriggers: Array.from(document.querySelectorAll(".accordion-trigger")),
stageTabs: Array.from(document.querySelectorAll(".stage-tab")),
stepSections: Array.from(document.querySelectorAll("[data-step-section]")),
stepBackButton: document.getElementById("stepBackButton"),
stepNextButton: document.getElementById("stepNextButton"),
progressFill: document.getElementById("progressFill"),
completionPercent: document.getElementById("completionPercent"),
statuses: {
type: document.getElementById("typeStatus"),
subject: document.getElementById("subjectStatus"),
additional: document.getElementById("additionalStatus"),
period: document.getElementById("periodStatus"),
logo: document.getElementById("logoStatus"),
review: document.getElementById("reviewStatus"),
},
quickRequest: {
modal: document.getElementById("quickRequestModal"),
summaryCard: document.getElementById("quickRequestSummaryCard"),
summaryTitle: document.getElementById("quickRequestSummaryTitle"),
summaryMeta: document.getElementById("quickRequestSummaryMeta"),
summaryList: document.getElementById("quickRequestSummaryList"),
openButton: document.getElementById("quickRequestOpenButton"),
editButton: document.getElementById("quickRequestEditButton"),
modalStatus: document.getElementById("quickRequestModalStatus"),
gridBody: document.getElementById("quickRequestGridBody"),
addRowButton: document.getElementById("quickRequestAddRowButton"),
cancelButton: document.getElementById("quickRequestCancelButton"),
applyButton: document.getElementById("quickRequestApplyButton"),
typeInputs: Array.from(document.querySelectorAll('input[name="quickRequestType"]')),
idLabels: Array.from(document.querySelectorAll(".quick-id-label")),
},
readiness: {
type: document.getElementById("readyType"),
subject: document.getElementById("readySubject"),
additional: document.getElementById("readyAdditional"),
period: document.getElementById("readyPeriod"),
user: document.getElementById("readyUser"),
disclaimer: document.getElementById("readyDisclaimer"),
},
submissionSuccess: {
modal: document.getElementById("submissionSuccessModal"),
closeIcon: document.getElementById("submissionSuccessCloseIcon"),
closeButton: document.getElementById("submissionSuccessCloseButton"),
},
notesModal: {
modal: document.getElementById("notesModal"),
toggleButton: document.getElementById("notesToggleButton"),
closeButton: document.getElementById("notesModalCloseButton"),
inlinePreview: document.getElementById("notesInlinePreview"),
},
submitSummary: {
subject: document.getElementById("submitSummarySubject"),
additional: document.getElementById("submitSummaryAdditional"),
period: document.getElementById("submitSummaryPeriod"),
freePeriodAdjustment: document.getElementById("freePeriodAdjustmentNotice"),
freePeriodAdjustmentMessage: document.getElementById("freePeriodAdjustmentMessage"),
logo: document.getElementById("submitSummaryLogo"),
colors: document.getElementById("submitSummaryColors"),
theme: document.getElementById("submitSummaryTheme"),
notes: document.getElementById("submitSummaryNotes"),
},
submitReadiness: {
type: document.getElementById("submitReadyType"),
subject: document.getElementById("submitReadySubject"),
additional: document.getElementById("submitReadyAdditional"),
period: document.getElementById("submitReadyPeriod"),
user: document.getElementById("submitReadyUser"),
disclaimer: document.getElementById("submitReadyDisclaimer"),
},
selectionSummary: {
institutionType: document.getElementById("summaryInstitutionType"),
subjectInstitution: document.getElementById("summarySubjectInstitution"),
additionalInstitutions: document.getElementById("summaryAdditionalInstitutions"),
reportFormat: document.getElementById("summaryReportFormat"),
logo: document.getElementById("summaryLogo"),
theme: document.getElementById("summaryTheme"),
colors: document.getElementById("summaryColors"),
notes: document.getElementById("summaryNotes"),
logoPreview: document.getElementById("summaryLogoPreview"),
},
};
let brandingCloseTimer = null;
let logoSearchPopupWindow = null;
let logoDropZoneReady = false;
let logoSearchInteractionReady = false;
let logoSearchOpening = false;
let logoSearchReadinessPromise = null;
const FREE_SUBMIT_BUTTON_TEXT = "Submit Request";
const PROFESSIONAL_SUBMIT_BUTTON_TEXT = "Submit Request and Make Payment";
const SUCCESS_SUBMIT_BUTTON_TEXT = "Request Sent";
const MAX_INLINE_LOGO_DATA_URL_LENGTH = 180000;
const SLOW_SUBMIT_NOTICE_MS = 20000;
const SUBMIT_CONFIRMATION_TIMEOUT_MS = 90000;
const REQUIRED_USER_FIELD_IDS = ["firstName", "lastName", "email", "phone", "organization"];
const SEARCH_DISPATCH_DEDUPE_WINDOW_MS = 350;
let lastSubjectSearchDispatch = { value: "", ts: 0 };
let searchRequestCounter = 0;
let activeSubjectSearchRequestId = 0;
const activePeerSearchRequestByIndex = new Map();
let subjectResultsLocked = false;
let helperActiveAnchorId = "";
let helperActiveTargetElement = null;
let helperObservedTargetElement = null;
let helperPositionFrame = 0;
let helperMutationObserver = null;
let helperResizeObserver = null;
function closeLogoSearchWindow({ aggressive = false, targetWindow = logoSearchPopupWindow } = {}) {
if (!targetWindow || targetWindow.closed) {
if (!targetWindow || logoSearchPopupWindow === targetWindow) {
logoSearchPopupWindow = null;
}
return;
}
try {
targetWindow.close();
} catch (error) {
// Ignore browser restrictions; we'll optionally retry in aggressive mode.
}
if (aggressive && targetWindow && !targetWindow.closed) {
// Some browsers can delay close on cross-window drag flows; retry once.
setTimeout(() => {
try {
targetWindow.close();
} catch (error) {
// Ignore - nothing else required.
}
if (logoSearchPopupWindow === targetWindow && targetWindow.closed) {
logoSearchPopupWindow = null;
}
}, 0);
}
if (targetWindow.closed && logoSearchPopupWindow === targetWindow) {
logoSearchPopupWindow = null;
}
}
function isLogoDropZoneInteractive() {
return Boolean(state.subject && els.logoControls && !els.logoControls.hidden && els.logoPreview);
}
function initializeLogoSearchReadiness() {
if (logoSearchReadinessPromise) return logoSearchReadinessPromise;
setupLogoDropZone();
logoSearchReadinessPromise = new Promise((resolve) => {
const afterDomLoaded = () => {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
// Force one layout read after listener binding and DOM mount. The
// Search button remains disabled until this barrier completes.
els.logoPreview?.getBoundingClientRect();
logoSearchInteractionReady = Boolean(logoDropZoneReady && els.logoPreview);
render();
resolve(logoSearchInteractionReady);
});
});
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", afterDomLoaded, { once: true });
} else {
afterDomLoaded();
}
});
return logoSearchReadinessPromise;
}
function debounce(fn, delay = 350) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
function escapeHtml(value) {
return String(value || "")
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """);
}
async function parseJsonResponse(response, fallbackMessage) {
const responseText = await response.text();
try {
return JSON.parse(responseText);
} catch (_error) {
throw new Error(
response.ok
? fallbackMessage
: "Institution search is temporarily unavailable. Please try again."
);
}
}
function hasSelections() {
return Boolean(state.subject) || selectedAdditional().length > 0;
}
function selectedAdditional() {
return state.additional.filter(Boolean);
}
function formatTypeLabel(type) {
return type === "credit_union" ? "Credit Union" : "Bank";
}
function clampStep(step) {
const normalized = String(step || "").toLowerCase();
const match = REGISTRATION_STEPS.find((candidate) => candidate === normalized);
return match || "1";
}
function detectRouteStep() {
const match = String(window.location.pathname || "").match(/^\/registration\/step(1|2|3b|3|4)\/?$/i);
if (match) return clampStep(match[1]);
const queryStep = new URLSearchParams(window.location.search || "").get("step");
if (queryStep) return clampStep(queryStep);
return "1";
}
function persistSessionState() {
const snapshot = {
state: {
institutionType: state.institutionType,
subject: state.subject,
additional: state.additional,
bankPeriodConfirmation: state.bankPeriodConfirmation,
cuPeriodConfirmation: state.cuPeriodConfirmation,
logo: state.logo,
account: {
selectedPlan: state.account.selectedPlan,
},
checkout: {
requestedPlan: state.checkout.requestedPlan,
},
ui: {
activeAccordion: state.ui.activeAccordion,
routeStep: state.ui.routeStep,
institutionTypeChosen: state.ui.institutionTypeChosen,
},
},
form: {
firstName: document.getElementById("firstName")?.value || "",
lastName: document.getElementById("lastName")?.value || "",
title: document.getElementById("title")?.value || "",
email: document.getElementById("email")?.value || "",
phone: document.getElementById("phone")?.value || "",
organization: document.getElementById("organization")?.value || "",
notes: document.getElementById("notes")?.value || "",
disclaimerAck: Boolean(els.disclaimerAck?.checked),
reportFormatMode: selectedReportFormatMode(),
quarters: els.quarterSelects.map((select) => select.value),
subjectStateHint: els.subjectStateHint?.value || "",
subjectCityHint: els.subjectCityHint?.value || "",
peerSearches: els.peerSearches.map((input) => input.value),
peerStateHints: els.peerStateHints.map((input) => input.value),
peerCityHints: els.peerCityHints.map((input) => input.value),
},
};
sessionStorage.setItem(REGISTRATION_SESSION_KEY, JSON.stringify(snapshot));
}
function restoreSessionState() {
try {
if (EXPLICIT_PRICING_PLAN) {
sessionStorage.removeItem(REGISTRATION_SESSION_KEY);
return;
}
const raw = sessionStorage.getItem(REGISTRATION_SESSION_KEY);
if (!raw) return;
const parsed = JSON.parse(raw);
if (parsed?.state) {
state.institutionType = parsed.state.institutionType || DEFAULT_INSTITUTION_TYPE;
state.subject = parsed.state.subject || null;
state.additional = Array.isArray(parsed.state.additional) ? parsed.state.additional.slice(0, 3) : [null, null, null];
while (state.additional.length < 3) state.additional.push(null);
state.bankPeriodConfirmation = {
...emptyBankPeriodConfirmation(),
...(parsed.state.bankPeriodConfirmation || {}),
};
state.cuPeriodConfirmation = {
...emptyCuPeriodConfirmation(),
...(parsed.state.cuPeriodConfirmation || {}),
};
if (!bankPeriodConfirmationMatchesSubject()) {
state.bankPeriodConfirmation = emptyBankPeriodConfirmation();
}
if (!cuPeriodConfirmationMatchesSelection()) {
state.cuPeriodConfirmation = emptyCuPeriodConfirmation();
}
if (parsed.state.logo) state.logo = parsed.state.logo;
if (parsed.state.checkout?.requestedPlan === "pro") {
state.checkout.requestedPlan = "pro";
}
if (
parsed.state.account?.selectedPlan === "pro"
|| state.checkout.requestedPlan === "pro"
|| state.checkout.sessionId
) {
state.account.selectedPlan = "pro";
}
if (parsed.state.ui?.activeAccordion) state.ui.activeAccordion = parsed.state.ui.activeAccordion;
state.ui.institutionTypeChosen = Boolean(parsed.state.ui?.institutionTypeChosen || parsed.state.subject);
}
const form = parsed?.form || {};
["firstName", "lastName", "title", "email", "phone", "organization", "notes"].forEach((id) => {
const input = document.getElementById(id);
if (input && typeof form[id] === "string") input.value = form[id];
});
if (els.disclaimerAck) els.disclaimerAck.checked = Boolean(form.disclaimerAck);
if (typeof form.subjectStateHint === "string" && els.subjectStateHint) els.subjectStateHint.value = form.subjectStateHint;
if (typeof form.subjectCityHint === "string" && els.subjectCityHint) els.subjectCityHint.value = form.subjectCityHint;
if (Array.isArray(form.peerSearches)) form.peerSearches.forEach((value, index) => { if (els.peerSearches[index]) els.peerSearches[index].value = value || ""; });
if (Array.isArray(form.peerStateHints)) form.peerStateHints.forEach((value, index) => { if (els.peerStateHints[index]) els.peerStateHints[index].value = value || ""; });
if (Array.isArray(form.peerCityHints)) form.peerCityHints.forEach((value, index) => { if (els.peerCityHints[index]) els.peerCityHints[index].value = value || ""; });
if (Array.isArray(form.quarters)) form.quarters.forEach((value, index) => { if (els.quarterSelects[index] && value) els.quarterSelects[index].value = value; });
if (form.reportFormatMode) {
els.reportFormatModes.forEach((modeInput) => {
modeInput.checked = modeInput.value === form.reportFormatMode;
});
}
} catch (error) {
// Ignore invalid session payloads and continue with defaults.
}
}
function goToRouteStep(step) {
const nextStep = clampStep(step);
persistSessionState();
const skinnySession = String(window.LOR_SKINNY?.session || "").trim();
let targetUrl = skinnySession
? `/workflow/${encodeURIComponent(skinnySession)}/order?step=${encodeURIComponent(nextStep)}`
: (REGISTRATION_STEP_PATHS[nextStep] || REGISTRATION_STEP_PATHS[1]);
if (!skinnySession && state.workflow.guid) {
targetUrl += `?workflow_guid=${encodeURIComponent(state.workflow.guid)}`;
}
const currentUrl = `${window.location.pathname}${window.location.search}`;
if (currentUrl !== targetUrl) {
window.location.href = targetUrl;
}
}
function stepIndex(stepId) {
const index = REGISTRATION_STEPS.indexOf(clampStep(stepId));
return index >= 0 ? index : 0;
}
function quickRequestType() {
const selected = els.quickRequest.typeInputs.find((input) => input.checked);
return selected ? selected.value : DEFAULT_INSTITUTION_TYPE;
}
function createQuickRequestRow(role = "peer", index = 0) {
return {
role,
institutionLabel: role === "subject" ? "Subject" : `Peer ${index}`,
name: "",
state: "",
city: "",
};
}
function ensureQuickRequestRows() {
if (!state.quickRequest.rows.length || state.quickRequest.rows[0]?.role !== "subject") {
state.quickRequest.rows = [createQuickRequestRow("subject"), ...state.quickRequest.rows.filter((row) => row.role !== "subject")];
}
const peerRows = state.quickRequest.rows.filter((row) => row.role === "peer").slice(0, 3);
peerRows.forEach((row, index) => {
row.institutionLabel = `Peer ${index + 1}`;
});
state.quickRequest.rows = [state.quickRequest.rows[0], ...peerRows];
state.quickRequest.activeRow = Math.max(0, Math.min(state.quickRequest.activeRow || 0, state.quickRequest.rows.length - 1));
}
function setQuickRequestStatus(message = "", kind = "") {
if (!els.quickRequest.modalStatus) return;
els.quickRequest.modalStatus.textContent = message;
els.quickRequest.modalStatus.className = `quick-request-modal-status ${kind}`.trim();
}
function syncQuickRequestTypeUI(type) {
const activeType = type || quickRequestType();
els.quickRequest.typeInputs.forEach((input) => {
input.checked = input.value === activeType;
});
renderQuickRequestModal();
renderQuickRequestSummary();
}
function institutionId(institution) {
return institution.cert_or_charter || institution.rssd_id || institution.name;
}
function normalizeMatchText(value) {
return String(value || "")
.toLowerCase()
.replaceAll("&", " and ")
.replace(/[^a-z0-9]+/g, " ")
.replace(/\s+/g, " ")
.trim();
}
function parseLocationParts(location) {
const raw = String(location || "").trim();
if (!raw) return { city: "", state: "" };
const parts = raw.split(",").map((part) => part.trim()).filter(Boolean);
if (parts.length >= 2) {
return { city: parts.slice(0, -1).join(", "), state: parts[parts.length - 1] };
}
return { city: raw, state: "" };
}
function buildOfficialSearchQueries(name, city = "", regionState = "") {
const cleanName = String(name || "").trim();
const cleanCity = String(city || "").trim();
const cleanState = String(regionState || "").trim();
if (!cleanName) return [];
const queries = [];
const add = (value) => {
const normalized = String(value || "").trim().replace(/\s+/g, " ");
if (normalized && !queries.includes(normalized)) {
queries.push(normalized);
}
};
add(cleanName);
if (state.institutionType === "bank" && !/\bbank\b/i.test(cleanName)) {
add(`${cleanName} Bank`);
}
if (cleanState) add(`${cleanName} ${cleanState}`);
if (cleanCity) add(`${cleanName} ${cleanCity}`);
if (cleanCity && cleanState) add(`${cleanName} ${cleanCity} ${cleanState}`);
return queries;
}
function scoreOfficialMatch(item, queryName, queryCity = "", queryState = "") {
const normalizedName = normalizeMatchText(queryName);
const normalizedCity = normalizeMatchText(queryCity);
const normalizedState = normalizeMatchText(queryState);
const itemName = normalizeMatchText(item.name);
const locationParts = parseLocationParts(item.location);
const itemCity = normalizeMatchText(item.city || locationParts.city);
const itemState = normalizeMatchText(item.state || locationParts.state);
let score = 0;
if (itemName === normalizedName) score += 120;
else if (itemName.includes(normalizedName)) score += 80;
else if (normalizedName.includes(itemName)) score += 55;
if (normalizedState) {
if (itemState === normalizedState) score += 45;
else if (itemState.includes(normalizedState)) score += 20;
else score -= 25;
}
if (normalizedCity) {
if (itemCity === normalizedCity) score += 35;
else if (itemCity.includes(normalizedCity)) score += 15;
}
return score;
}
function prioritizeOfficialResults(results, queryName, queryCity = "", queryState = "") {
const deduped = [];
const seen = new Set();
results.forEach((item) => {
const key = `${(item.rssd_id || "").trim()}|${(item.cert_or_charter || "").trim()}|${normalizeMatchText(item.name)}`;
if (seen.has(key)) return;
seen.add(key);
deduped.push(item);
});
return deduped.sort((a, b) => scoreOfficialMatch(b, queryName, queryCity, queryState) - scoreOfficialMatch(a, queryName, queryCity, queryState));
}
function setActiveAccordion(panelId) {
if (panelId === "reviewPanel") {
state.ui.reviewAutoCollapsed = false;
state.ui.reviewPrimed = true;
}
els.accordionPanels.forEach((panel) => {
const isOpen = Boolean(panelId) && panel.id === panelId;
panel.classList.toggle("open", isOpen);
const trigger = panel.querySelector(".accordion-trigger");
if (trigger) trigger.setAttribute("aria-expanded", String(isOpen));
});
state.ui.activeAccordion = panelId || "";
syncOpenStateGlow();
syncSubmitButtonState(state.validation);
}
function syncBrandingWindowState() {
const unlocked = Boolean(state.subject);
const isOpen = Boolean(state.ui.isBrandingWindowOpen && unlocked);
els.logoControls.hidden = !unlocked;
els.brandingStageShell.classList.toggle("is-unlocked", unlocked);
els.brandingStageShell.classList.toggle("expanded-active", isOpen);
els.logoControls.classList.toggle("expanded-active", isOpen);
els.logoControls.setAttribute("aria-hidden", String(!isOpen));
}
function setBrandingWindowOpen(isOpen) {
const shouldOpen = Boolean(isOpen && state.subject);
state.ui.isBrandingWindowOpen = shouldOpen;
if (shouldOpen) {
els.logoControls.hidden = false;
}
syncBrandingWindowState();
}
function alignBrandingPanelInView() {
if (!els.logoPanel) return;
const rect = els.logoPanel.getBoundingClientRect();
const targetTop = 10;
if (rect.top > targetTop && rect.bottom <= window.innerHeight - 8) return;
window.scrollBy({
top: rect.top - targetTop,
behavior: "smooth",
});
}
function closeBrandingAndContinue() {
setBrandingWindowOpen(false);
openAccordion("");
requestAnimationFrame(() => {
if (!els.submitButton.disabled) {
els.submitButton.focus();
}
});
}
function openAccordion(panelId) {
if (brandingCloseTimer) {
clearTimeout(brandingCloseTimer);
brandingCloseTimer = null;
}
const leavingBranding = state.ui.activeAccordion === "logoPanel" && panelId !== "logoPanel" && state.ui.isBrandingWindowOpen;
if (leavingBranding) {
setBrandingWindowOpen(false);
brandingCloseTimer = setTimeout(() => {
setActiveAccordion(panelId);
brandingCloseTimer = null;
}, 160);
return;
}
setActiveAccordion(panelId);
if (panelId === "logoPanel" && state.subject) {
els.logoControls.hidden = false;
requestAnimationFrame(() => {
if (state.ui.activeAccordion === "logoPanel" && state.subject) {
setBrandingWindowOpen(true);
alignBrandingPanelInView();
}
});
return;
}
setBrandingWindowOpen(false);
}
function queueFocus(target) {
state.ui.pendingFocus = target || "";
}
function applyPendingFocus() {
if (!state.ui.pendingFocus) return;
const pendingTarget = state.ui.pendingFocus;
state.ui.pendingFocus = "";
let element = null;
if (pendingTarget === "subjectSearch") {
element = els.subjectSearch;
} else if (pendingTarget === "reportFormatChoice") {
element = els.reportFormatModes.find((input) => !input.disabled) || els.reportFormatChoice;
} else if (pendingTarget.startsWith("peerSearch:")) {
const peerIndex = Number(pendingTarget.split(":")[1]);
element = els.peerSearches[peerIndex] || null;
}
if (!element || element.disabled) return;
requestAnimationFrame(() => {
const slot = element.closest(".peer-slot");
if (slot) {
slot.scrollIntoView({ behavior: "smooth", block: "center", inline: "nearest" });
} else {
element.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "nearest" });
}
element.focus();
});
}
function syncWorkflowScrollHint() {
const container = els.workflowScroll;
const hint = els.workflowScrollHint;
if (!container || !hint) return;
const overflow = (container.scrollHeight - container.clientHeight) > 8;
const nearBottom = (container.scrollTop + container.clientHeight) >= (container.scrollHeight - 8);
const shouldShow = overflow && !nearBottom;
hint.hidden = !shouldShow;
hint.setAttribute("aria-hidden", String(!shouldShow));
}
function applyStep3bViewportScale() {
if (!document.body) return;
if (!document.body.classList.contains("step-3b")) {
document.body.style.setProperty("--step3b-scale", "1");
document.body.classList.remove("step3b-tight", "step3b-ultra-tight");
return;
}
const viewportHeight = Math.max(window.innerHeight || 0, document.documentElement?.clientHeight || 0);
const viewportWidth = Math.max(window.innerWidth || 0, document.documentElement?.clientWidth || 0);
const verticalFitBuffer = 30;
const horizontalFitBuffer = 8;
const fitHeight = Math.max(320, viewportHeight - verticalFitBuffer);
const fitWidth = Math.max(320, viewportWidth - horizontalFitBuffer);
const shell = document.querySelector(".monitor-shell");
const stepNav = document.querySelector(".step-navigation-panel");
const designHeight = 860;
const designWidth = 1500;
const heightScale = fitHeight / designHeight;
const widthScale = fitWidth / designWidth;
const nextScale = Math.min(1, heightScale, widthScale);
let boundedScale = Math.max(0.2, nextScale);
document.body.style.setProperty("--step3b-scale", boundedScale.toFixed(3));
if (shell) {
const maxBottom = viewportHeight - 14;
const maxRight = viewportWidth - 10;
const shellRect = shell.getBoundingClientRect();
const navRect = stepNav ? stepNav.getBoundingClientRect() : shellRect;
const bottomEdge = Math.max(shellRect.bottom, navRect.bottom);
const rightEdge = Math.max(shellRect.right, navRect.right);
const heightRatio = bottomEdge > maxBottom ? maxBottom / bottomEdge : 1;
const widthRatio = rightEdge > maxRight ? maxRight / rightEdge : 1;
const correction = Math.min(heightRatio, widthRatio, 1);
if (correction < 0.999) {
boundedScale = Math.max(0.2, boundedScale * correction);
document.body.style.setProperty("--step3b-scale", boundedScale.toFixed(3));
}
}
// Final guard rail: keep Step 3b a touch under full size to avoid sub-pixel clipping.
boundedScale = Math.max(0.2, boundedScale * 0.985);
document.body.style.setProperty("--step3b-scale", boundedScale.toFixed(3));
document.body.classList.toggle("step3b-tight", boundedScale < 0.92);
document.body.classList.toggle("step3b-ultra-tight", boundedScale < 0.82);
}
function renderInstitutionDetails(institution) {
return `
${institution.name}
${institution.location || "Location unavailable"} |
Cert/Charter/RSSD: ${institution.cert_or_charter || institution.rssd_id || "Not available"} |
Assets: ${institution.assets || "Not available"}
`;
}
function setInstitutionType(type, options = {}) {
const { advance = true, userInitiated = false } = options;
if (state.institutionType && state.institutionType !== type && hasSelections()) {
els.typeWarning.hidden = false;
return;
}
state.institutionType = type;
if (userInitiated) {
state.ui.institutionTypeChosen = true;
}
els.typeWarning.hidden = true;
els.typeButtons.forEach((button) => {
const active = button.dataset.type === type;
button.classList.toggle("active", active);
button.setAttribute("aria-pressed", String(active));
});
els.subjectSearch.disabled = false;
if (els.subjectStateHint) els.subjectStateHint.disabled = false;
if (els.subjectCityHint) els.subjectCityHint.disabled = false;
els.subjectSearchButton.disabled = false;
els.subjectSearch.placeholder = `Type ${formatTypeLabel(type)} name and press Search or Enter`;
syncQuickRequestTypeUI(type);
if (advance) {
openAccordion("subjectPanel");
} else {
openAccordion("institutionTypePanel");
}
buildQuarterOptions();
applyReportFormatMode();
render();
}
function syncInstitutionTypeUI() {
const activeType = effectiveInstitutionType();
els.typeButtons.forEach((button) => {
const active = button.dataset.type === activeType;
button.classList.toggle("active", active);
button.setAttribute("aria-pressed", String(active));
});
const hasType = Boolean(activeType);
els.subjectSearch.disabled = !hasType;
els.subjectSearchButton.disabled = !hasType;
els.subjectSearch.placeholder = hasType
? `Type ${formatTypeLabel(activeType)} name and press Search or Enter`
: "Select Bank or Credit Union first";
if (els.subjectStateHint) els.subjectStateHint.disabled = !hasType;
if (els.subjectCityHint) els.subjectCityHint.disabled = !hasType;
}
function clearSelections() {
state.subject = null;
state.additional = [null, null, null];
state.bankPeriodConfirmation = emptyBankPeriodConfirmation();
state.cuPeriodConfirmation = emptyCuPeriodConfirmation();
state.ui.institutionTypeChosen = false;
els.subjectSearch.value = "";
if (els.subjectStateHint) els.subjectStateHint.value = "";
if (els.subjectCityHint) els.subjectCityHint.value = "";
els.peerSearches.forEach((input) => {
input.value = "";
});
els.peerStateHints.forEach((input) => {
input.value = "";
});
els.peerCityHints.forEach((input) => {
input.value = "";
});
els.subjectResults.innerHTML = "";
els.peerResults.forEach((target) => {
target.innerHTML = "";
});
els.typeWarning.hidden = true;
render();
}
function updateQuickRequestRow(index, field, value) {
ensureQuickRequestRows();
if (!state.quickRequest.rows[index]) return;
state.quickRequest.rows[index][field] = value;
}
function getQuickRequestValidationClass(value) {
const trimmed = String(value || "").trim();
if (!trimmed) return "quick-required";
return "quick-valid";
}
function rowDisplayLabel(row) {
return row.role === "subject" ? "Subject Institution" : row.institutionLabel;
}
function renderQuickRequestModal() {
if (!els.quickRequest.gridBody) return;
ensureQuickRequestRows();
els.quickRequest.gridBody.innerHTML = state.quickRequest.rows.map((row, index) => {
const nameClass = getQuickRequestValidationClass(row.name);
const stateClass = getQuickRequestValidationClass(row.state);
const cityClass = getQuickRequestValidationClass(row.city);
const canDelete = row.role === "peer";
const isExpanded = state.quickRequest.activeRow === index;
const summary = [row.name || "Institution name pending", row.city && row.state ? `${row.city}, ${row.state}` : "City/state pending"].join(" | ");
return `
`;
}).join("");
const peerCount = state.quickRequest.rows.filter((row) => row.role === "peer").length;
els.quickRequest.addRowButton.disabled = peerCount >= 3;
els.quickRequest.addRowButton.hidden = peerCount >= 3;
}
function quickRequestSummaryData() {
ensureQuickRequestRows();
const type = quickRequestType();
return {
applied: state.quickRequest.applied,
institution_type: type,
reporting_period_note: "Quick Request defaults to the last 5 available quarters. Use Status & Controls for different report periods.",
subject_institution: {
name: state.quickRequest.rows[0]?.name.trim() || "",
city: state.quickRequest.rows[0]?.city.trim() || "",
state: state.quickRequest.rows[0]?.state.trim() || "",
city_state: [state.quickRequest.rows[0]?.city.trim() || "", state.quickRequest.rows[0]?.state.trim() || ""].filter(Boolean).join(", "),
},
additional_institutions: state.quickRequest.rows
.filter((row) => row.role === "peer")
.map((row) => ({
name: row.name.trim(),
city: row.city.trim(),
state: row.state.trim(),
city_state: [row.city.trim(), row.state.trim()].filter(Boolean).join(", "),
})),
};
}
function renderQuickRequestSummary() {
if (!els.quickRequest.summaryTitle) return;
const summary = quickRequestSummaryData();
const peerCount = summary.additional_institutions.filter((row) => row.name || row.city_state).length;
const subjectReady = Boolean(summary.subject_institution.name);
if (!state.quickRequest.applied || !subjectReady) {
els.quickRequest.summaryTitle.textContent = "No Quick Request institutions added yet.";
els.quickRequest.summaryMeta.textContent = "Add the subject institution plus up to 3 peers in a fixed-size modal. The server will resolve RSSIDs or CU identifiers after you submit the request.";
els.quickRequest.summaryList.hidden = true;
els.quickRequest.summaryList.innerHTML = "";
els.quickRequest.editButton.hidden = true;
return;
}
els.quickRequest.summaryTitle.textContent = `Quick Request: ${summary.subject_institution.name || "Subject added"} + ${peerCount} peer${peerCount === 1 ? "" : "s"} added`;
els.quickRequest.summaryMeta.textContent = `${formatTypeLabel(summary.institution_type)} | Last 5 available quarters | Use Status & Controls if you need different report periods.`;
els.quickRequest.summaryList.hidden = false;
els.quickRequest.editButton.hidden = false;
const items = [
{
label: "Subject",
name: summary.subject_institution.name,
meta: `${summary.subject_institution.city_state || "City/State pending"} | Identifier resolved after submit`,
},
...summary.additional_institutions
.filter((row) => row.name || row.city_state)
.map((row, index) => ({
label: `Peer ${index + 1}`,
name: row.name || `Peer ${index + 1}`,
meta: `${row.city_state || "City/State pending"} | Identifier resolved after submit`,
})),
];
els.quickRequest.summaryList.innerHTML = items.map((item) => `
${item.label}: ${escapeHtml(item.name)}
${escapeHtml(item.meta)}
`).join("");
}
function openQuickRequestModal() {
ensureQuickRequestRows();
state.quickRequest.open = true;
state.quickRequest.activeRow = 0;
els.quickRequest.modal.hidden = false;
els.quickRequest.modal.setAttribute("aria-hidden", "false");
document.body.classList.add("quick-request-modal-open");
renderQuickRequestModal();
setQuickRequestStatus("");
}
function closeQuickRequestModal() {
state.quickRequest.open = false;
els.quickRequest.modal.hidden = true;
els.quickRequest.modal.setAttribute("aria-hidden", "true");
document.body.classList.remove("quick-request-modal-open");
setQuickRequestStatus("");
}
function addQuickRequestPeerRow() {
ensureQuickRequestRows();
const peerRows = state.quickRequest.rows.filter((row) => row.role === "peer");
if (peerRows.length >= 3) return;
state.quickRequest.rows.push(createQuickRequestRow("peer", peerRows.length + 1));
state.quickRequest.activeRow = state.quickRequest.rows.length - 1;
renderQuickRequestModal();
}
function removeQuickRequestPeerRow(index) {
ensureQuickRequestRows();
if (state.quickRequest.rows[index]?.role !== "peer") return;
state.quickRequest.rows.splice(index, 1);
ensureQuickRequestRows();
state.quickRequest.activeRow = Math.max(0, Math.min(state.quickRequest.activeRow, state.quickRequest.rows.length - 1));
renderQuickRequestModal();
}
function validateQuickRequestRows() {
ensureQuickRequestRows();
let allValid = true;
let firstInvalidMessage = "";
let firstInvalidIndex = -1;
state.quickRequest.rows.forEach((row, index) => {
const nameOk = Boolean(row.name.trim());
const stateOk = Boolean(row.state.trim());
const cityOk = Boolean(row.city.trim());
const touched = nameOk || stateOk || cityOk;
const rowRequired = row.role === "subject" || touched;
if (rowRequired && !(nameOk && stateOk && cityOk)) {
allValid = false;
if (firstInvalidIndex < 0) firstInvalidIndex = index;
if (!firstInvalidMessage) {
firstInvalidMessage = `${rowDisplayLabel(row)} needs a name, state, and city before you can apply Quick Request.`;
}
}
});
if (firstInvalidIndex >= 0) {
state.quickRequest.activeRow = Math.min(firstInvalidIndex, state.quickRequest.rows.length - 1);
}
renderQuickRequestModal();
return { valid: allValid, message: firstInvalidMessage || "Quick Request is ready." };
}
function applyQuickRequestModal() {
const validation = validateQuickRequestRows();
if (!validation.valid) {
setQuickRequestStatus(validation.message, "error");
return;
}
state.quickRequest.applied = true;
renderQuickRequestSummary();
closeQuickRequestModal();
}
function quickRequestAppliedWithRows() {
const summary = quickRequestSummaryData();
return Boolean(
summary.applied
&& summary.subject_institution.name
);
}
function buildInstitutionFromQuickRequest(row, institutionType) {
const city = String(row.city || "").trim();
const regionState = String(row.state || "").trim();
return {
institution_type: institutionType,
name: String(row.name || "").trim(),
city,
state: regionState,
cert_or_charter: "",
rssd_id: "",
assets: "",
location: [city, regionState].filter(Boolean).join(", "),
raw: {
entry_source: "quick_request",
entered_name: String(row.name || "").trim(),
entered_city: city,
entered_state: regionState,
},
};
}
function effectiveInstitutionType() {
return state.institutionType || (QUICK_REQUEST_ENABLED ? quickRequestType() : "") || DEFAULT_INSTITUTION_TYPE;
}
function logoSearchTypeQuery() {
const subjectTypeHint = String(
state.subject?.institution_type ||
state.subject?.institutionType ||
state.subject?.type ||
""
).toLowerCase();
const subjectNameHint = String(state.subject?.name || "").toLowerCase();
const selectedType = els.typeButtons.find((button) => button.getAttribute("aria-pressed") === "true")?.dataset?.type || "";
const normalizedType = String(selectedType || effectiveInstitutionType() || "").toLowerCase();
const isCreditUnion =
subjectTypeHint.includes("credit") ||
subjectTypeHint.includes("cu") ||
subjectNameHint.includes("credit union") ||
/\bfcu\b/i.test(subjectNameHint) ||
normalizedType === "credit_union";
return isCreditUnion ? "credit union logo" : "bank logo";
}
function logoSearchInstitutionQuery() {
if (!state.subject) return "";
const locationParts = parseLocationParts(state.subject.location);
const city = state.subject.city || locationParts.city || state.subject.raw?.entered_city || "";
const regionState = state.subject.state || locationParts.state || state.subject.raw?.entered_state || "";
return [
state.subject.name,
city,
regionState,
logoSearchTypeQuery(),
].map((part) => String(part || "").trim()).filter(Boolean).join(" ");
}
function activateOriginalMode() {
if (!QUICK_REQUEST_ENABLED) return;
if (!state.quickRequest.applied) return;
state.quickRequest.applied = false;
setQuickRequestStatus("");
renderQuickRequestSummary();
}
function effectiveSubjectInstitution() {
if (state.subject) return state.subject;
if (!QUICK_REQUEST_ENABLED) return null;
if (!quickRequestAppliedWithRows()) return null;
return buildInstitutionFromQuickRequest(quickRequestSummaryData().subject_institution, effectiveInstitutionType());
}
function effectiveAdditionalInstitutions() {
const selected = selectedAdditional();
if (selected.length) return selected;
if (!QUICK_REQUEST_ENABLED) return [];
if (!quickRequestAppliedWithRows()) return [];
return quickRequestSummaryData()
.additional_institutions
.filter((row) => row.name && row.city && row.state)
.map((row) => buildInstitutionFromQuickRequest(row, effectiveInstitutionType()));
}
async function searchInstitutions(query, target, peerIndex = null) {
const nameQuery = query.trim();
const hintState = peerIndex === null ? els.subjectStateHint?.value.trim() || "" : els.peerStateHints[peerIndex]?.value.trim() || "";
const hintCity = peerIndex === null ? els.subjectCityHint?.value.trim() || "" : els.peerCityHints[peerIndex]?.value.trim() || "";
const institutionType = effectiveInstitutionType();
if (!institutionType || nameQuery.length < 2) {
target.innerHTML = "";
return;
}
const requestId = ++searchRequestCounter;
if (peerIndex === null) {
activeSubjectSearchRequestId = requestId;
} else {
activePeerSearchRequestByIndex.set(peerIndex, requestId);
}
const isRequestStillActive = () => (
peerIndex === null
? activeSubjectSearchRequestId === requestId
: activePeerSearchRequestByIndex.get(peerIndex) === requestId
);
target.innerHTML = `
Searching official institution names...
`;
try {
const aggregatedResults = [];
const queries = institutionType === "credit_union"
? [nameQuery]
: buildOfficialSearchQueries(nameQuery, hintCity, hintState);
for (const candidate of queries) {
const url = new URL("/api/institutions/search", window.location.origin);
url.searchParams.set("type", institutionType);
url.searchParams.set("q", candidate);
const response = await fetch(url.toString(), { cache: "no-store" });
if (!isRequestStillActive()) return;
const data = await parseJsonResponse(response, "Institution search returned an invalid response. Please try again.");
if (!isRequestStillActive()) return;
if (!response.ok || !data.success) {
throw new Error(data.error || "Institution search failed.");
}
aggregatedResults.push(...(data.results || []));
}
if (!isRequestStillActive()) return;
renderResults(prioritizeOfficialResults(aggregatedResults, nameQuery, hintCity, hintState), target, peerIndex);
} catch (error) {
if (!isRequestStillActive()) return;
target.innerHTML = `${escapeHtml(error.message)}
`;
}
}
function runSubjectSearch() {
if (
subjectResultsLocked
&& state.subject
&& normalizeMatchText(els.subjectSearch.value || "") === normalizeMatchText(state.subject.name || "")
) {
els.subjectResults.innerHTML = "";
els.subjectResults.style.display = "none";
els.subjectResults.classList.remove("results-attention");
syncSubjectResultsGuidance();
return Promise.resolve();
}
activateOriginalMode();
openAccordion("subjectPanel");
return searchInstitutions(els.subjectSearch.value, els.subjectResults);
}
function runPeerSearch(peerIndex) {
activateOriginalMode();
openAccordion("additionalPanel");
return searchInstitutions(els.peerSearches[peerIndex]?.value || "", els.peerResults[peerIndex], peerIndex);
}
const debouncedSubjectSearch = debounce(() => {
const value = (els.subjectSearch.value || "").trim();
if (!value || value.length < 2) return;
const now = Date.now();
if (
value === lastSubjectSearchDispatch.value
&& (now - lastSubjectSearchDispatch.ts) < SEARCH_DISPATCH_DEDUPE_WINDOW_MS
) {
return;
}
lastSubjectSearchDispatch = { value, ts: now };
runSubjectSearch();
}, 150);
function emptyBankPeriodConfirmation() {
return {
status: "idle",
confirmed: false,
rssd_id: "",
latest_period: "",
period_end: "",
official_ubpr_peer_period_label: "",
official_ubpr_peer_period_end: "",
official_ubpr_peer_source: "",
bulk_comparison_period_label: "",
bulk_comparison_period_end: "",
bulk_comparison_source: "",
bulk_comparison_data_lagging: false,
verified_at: "",
source: "",
error: "",
};
}
function emptyCuPeriodConfirmation() {
return {
status: "idle",
confirmed: false,
charter_number: "",
latest_period: "",
period_end: "",
verified_at: "",
source: "",
error: "",
};
}
function requiresBankPeriodDisclosure() {
return (
state.account.selectedPlan === "pro"
&& effectiveInstitutionType() === "bank"
&& Boolean(state.subject)
);
}
function requiresCuPeriodDisclosure() {
return (
state.account.selectedPlan === "pro"
&& effectiveInstitutionType() === "credit_union"
&& Boolean(state.subject)
);
}
function bankPeriodConfirmationMatchesSubject() {
if (!requiresBankPeriodDisclosure()) return true;
const rssd = String(state.subject?.rssd_id || "").trim();
return Boolean(
rssd
&& state.bankPeriodConfirmation.status === "ready"
&& state.bankPeriodConfirmation.confirmed
&& state.bankPeriodConfirmation.rssd_id === rssd
&& state.bankPeriodConfirmation.latest_period
&& state.bankPeriodConfirmation.source === "validated_regulatory_data"
);
}
function cuPeriodConfirmationMatchesSelection() {
if (!requiresCuPeriodDisclosure()) return true;
const selected = selectedPeriod().quarters[0] || {};
const charter = String(state.subject?.cert_or_charter || "").trim();
return Boolean(
selected.label
&& selected.value
&& state.cuPeriodConfirmation.status === "ready"
&& state.cuPeriodConfirmation.confirmed
&& state.cuPeriodConfirmation.charter_number === charter
&& state.cuPeriodConfirmation.latest_period === selected.label
&& state.cuPeriodConfirmation.period_end === selected.value
&& state.cuPeriodConfirmation.source === "ncua_bulk_archives"
);
}
function syncBankPeriodDisclosure() {
if (!els.bankPeriodDisclosure) return;
const isCuDisclosure = requiresCuPeriodDisclosure();
const visible = state.ui.routeStep === "4" && (
requiresBankPeriodDisclosure() || isCuDisclosure
);
els.bankPeriodDisclosure.hidden = !visible;
if (!visible) return;
const confirmation = isCuDisclosure
? state.cuPeriodConfirmation
: state.bankPeriodConfirmation;
els.bankPeriodDisclosure.classList.toggle(
"is-loading",
!["ready", "error"].includes(confirmation.status),
);
const institutionIdentifier = isCuDisclosure
? state.subject?.cert_or_charter
? ` · Charter ${state.subject.cert_or_charter}`
: ""
: state.subject?.rssd_id
? ` · RSSD ${state.subject.rssd_id}`
: "";
els.bankPeriodDisclosureInstitution.textContent = state.subject?.name
? `${state.subject.name}${institutionIdentifier}`
: "";
els.bankPeriodDisclosurePeriod.textContent = confirmation.status === "ready"
? confirmation.latest_period
: confirmation.status === "error"
? "Unavailable"
: "Checking…";
const officialUbprPeerPeriodLabel = confirmation.official_ubpr_peer_period_label
|| confirmation.latest_period;
const officialUbprPeerPeriodEnd = confirmation.official_ubpr_peer_period_end
|| confirmation.period_end;
const bulkComparisonPeriodLabel = confirmation.bulk_comparison_period_label
|| confirmation.latest_period;
const bulkComparisonPeriodEnd = confirmation.bulk_comparison_period_end
|| confirmation.period_end;
els.bankPeriodDisclosureStatus.textContent = confirmation.status === "ready"
? isCuDisclosure
? `Verified against the official NCUA quarterly bulk-file release${confirmation.period_end ? ` · Period ended ${confirmation.period_end}` : ""}.`
: `Selected bank and official UBPR peer group (PG/PCT): ${officialUbprPeerPeriodLabel}${officialUbprPeerPeriodEnd ? ` · Period ended ${officialUbprPeerPeriodEnd}` : ""}. Separate bulk/API competitor and custom comparisons: ${bulkComparisonPeriodLabel}${bulkComparisonPeriodEnd ? ` · Period ended ${bulkComparisonPeriodEnd}` : ""}.`
: confirmation.status === "error"
? `${confirmation.error} No payment has been processed.`
: isCuDisclosure
? "Checking the selected credit union’s reporting period against official NCUA quarterly bulk-file availability."
: "Checking the selected bank's live FFIEC UBPR period and separate bulk/API comparison availability.";
els.bankPeriodDisclosureStatus.classList.toggle("is-error", confirmation.status === "error");
els.bankPeriodDisclosureLanguage.hidden = confirmation.status !== "ready";
if (confirmation.status === "ready") {
els.bankPeriodDisclosureLanguage.innerHTML = isCuDisclosure
? "This Professional package will use the reporting period shown above for the selected credit union. That period is backed by the latest verified NCUA quarterly bulk-file release available to the system. By selecting Submit Request and Make Payment, you acknowledge that the report will be prepared using this reporting period."
: `This Professional package uses ${escapeHtml(officialUbprPeerPeriodLabel)} live FFIEC UBPR data for the selected bank and its official UBPR peer-group averages and percentiles (PG/PCT). ${confirmation.bulk_comparison_data_lagging ? `Separate competitor or custom comparisons use ${escapeHtml(bulkComparisonPeriodLabel)} from the latest validated bulk/API sources and therefore lag the subject UBPR.` : "Separate competitor or custom comparisons use the same reporting period from validated bulk/API sources."} By selecting Submit Request and Make Payment, you acknowledge these distinct reporting periods and data sources.`;
}
els.bankPeriodRetryButton.hidden = confirmation.status !== "error";
}
async function verifyFinalBankPeriod({ force = false, allowFree = false } = {}) {
if (
state.ui.routeStep !== "4"
|| !state.subject
|| (!allowFree && !requiresBankPeriodDisclosure())
) {
return;
}
const subject = state.subject;
const rssd = String(subject?.rssd_id || "").trim();
if (!force && ["loading", "ready"].includes(state.bankPeriodConfirmation.status)) return;
state.bankPeriodConfirmation = {
...emptyBankPeriodConfirmation(),
status: "loading",
rssd_id: rssd,
};
render();
if (!/^\d{1,12}$/.test(rssd)) {
state.bankPeriodConfirmation.status = "error";
state.bankPeriodConfirmation.error = "The selected bank does not include a valid RSSD ID.";
render();
return;
}
try {
const periodUrl = new URL("/api/registration/bank-reporting-period", window.location.origin);
periodUrl.searchParams.set("rssd_id", rssd);
const response = await fetch(periodUrl, {
cache: "no-store",
headers: { Accept: "application/json" },
});
const data = await response.json();
if (
!response.ok
|| !data.success
|| data.verified !== true
|| !data.period?.label
|| !data.period?.value
) {
throw new Error(data.detail || "The available bank reporting period could not be verified.");
}
if (state.subject !== subject || String(state.subject?.rssd_id || "") !== rssd) return;
state.bankPeriodConfirmation = {
status: "ready",
confirmed: true,
rssd_id: rssd,
latest_period: data.period.label,
period_end: data.period.value,
official_ubpr_peer_period_label: data.official_ubpr_peer_period_label || data.period.label,
official_ubpr_peer_period_end: data.official_ubpr_peer_period_end || data.period.value,
official_ubpr_peer_source: data.official_ubpr_peer_source || "ffiec_live_ubpr",
bulk_comparison_period_label: data.bulk_comparison_period_label || data.period.label,
bulk_comparison_period_end: data.bulk_comparison_period_end || data.period.value,
bulk_comparison_source: data.bulk_comparison_source || "validated_bulk_api",
bulk_comparison_data_lagging: Boolean(
data.bulk_comparison_data_lagging ?? data.peer_data_lagging
),
verified_at: data.checked_at || new Date().toISOString(),
source: data.source || "validated_regulatory_data",
error: "",
};
} catch (error) {
if (state.subject !== subject) return;
state.bankPeriodConfirmation = {
...emptyBankPeriodConfirmation(),
status: "error",
rssd_id: rssd,
error: error.message || "The available bank reporting period could not be verified.",
};
}
render();
}
async function verifyFinalCuPeriod({ force = false } = {}) {
if (
state.ui.routeStep !== "4"
|| !requiresCuPeriodDisclosure()
|| state.reportingPeriods.status !== "ready"
) {
return;
}
if (!force && ["loading", "ready"].includes(state.cuPeriodConfirmation.status)) return;
const subject = state.subject;
const selected = selectedPeriod().quarters[0] || {};
const charter = String(subject?.cert_or_charter || "").trim();
state.cuPeriodConfirmation = {
...emptyCuPeriodConfirmation(),
status: "loading",
charter_number: charter,
};
render();
if (!selected.label || !selected.value) {
state.cuPeriodConfirmation.status = "error";
state.cuPeriodConfirmation.error = "A valid credit-union reporting period has not been selected.";
render();
return;
}
try {
const response = await fetch("/api/reporting-periods/ncua", { cache: "no-store" });
const data = await response.json().catch(() => ({}));
if (!response.ok || data?.verified !== true || !Array.isArray(data?.options)) {
throw new Error(data?.detail || "NCUA reporting-period availability could not be verified.");
}
const verifiedOption = data.options.find((option) => (
option?.label === selected.label && option?.value === selected.value
));
if (!verifiedOption) {
throw new Error(`${selected.label} is not available in the verified NCUA quarterly bulk files.`);
}
if (state.subject !== subject) return;
const currentSelected = selectedPeriod().quarters[0] || {};
if (
currentSelected.label !== selected.label
|| currentSelected.value !== selected.value
) {
state.cuPeriodConfirmation = emptyCuPeriodConfirmation();
render();
return;
}
state.cuPeriodConfirmation = {
status: "ready",
confirmed: true,
charter_number: charter,
latest_period: verifiedOption.label,
period_end: verifiedOption.value,
verified_at: new Date().toISOString(),
source: "ncua_bulk_archives",
error: "",
};
} catch (error) {
if (state.subject !== subject) return;
state.cuPeriodConfirmation = {
...emptyCuPeriodConfirmation(),
status: "error",
charter_number: charter,
error: error.message || "NCUA reporting-period availability could not be verified.",
};
}
render();
}
function ensureFinalPeriodCheck() {
if (
state.ui.routeStep === "4"
&& requiresBankPeriodDisclosure()
&& state.bankPeriodConfirmation.status === "idle"
) {
queueMicrotask(() => verifyFinalBankPeriod());
}
if (
state.ui.routeStep === "4"
&& requiresCuPeriodDisclosure()
&& state.reportingPeriods.status === "ready"
&& state.cuPeriodConfirmation.status === "idle"
) {
queueMicrotask(() => verifyFinalCuPeriod());
}
}
function cancelRegistrationRequest() {
sessionStorage.removeItem(REGISTRATION_SESSION_KEY);
window.location.assign("/pricing");
}
function renderResults(results, target, peerIndex = null) {
if (!results.length) {
target.innerHTML = `No matching institutions found.
`;
if (target === els.subjectResults) {
els.subjectResults.classList.remove("results-attention");
syncSubjectResultsGuidance();
syncHelperPanel();
}
return;
}
const isSubject = target === els.subjectResults;
target.innerHTML = results
.map((institution, index) => `
${institution.name}
${institution.location || "Location unavailable"} | ${institution.cert_or_charter || institution.rssd_id || "No ID"}
`)
.join("");
if (isSubject) {
target.style.display = "";
const firstItem = target.querySelector(".result-item");
firstItem?.classList.add("prefocus");
applySubjectResultsAttentionCue();
syncSubjectResultsGuidance();
syncHelperPanel();
requestAnimationFrame(scrollSubjectResultsIntoViewIfNeeded);
}
target.querySelectorAll(".result-item").forEach((item) => {
const selectInstitution = () => {
const institution = results[Number(item.dataset.resultIndex)];
if (isSubject) {
state.subject = institution;
state.additional = state.additional.map((item) => (
item && institutionId(item) === institutionId(institution) ? null : item
));
state.bankPeriodConfirmation = emptyBankPeriodConfirmation();
state.cuPeriodConfirmation = emptyCuPeriodConfirmation();
activeSubjectSearchRequestId = 0;
subjectResultsLocked = true;
els.subjectSearch.value = institution.name;
els.subjectResults.innerHTML = "";
els.subjectResults.style.display = "none";
els.subjectResults.classList.remove("results-attention");
syncSubjectResultsGuidance();
openAccordion("additionalPanel");
queueFocus("peerSearch:0");
} else {
if (Number.isInteger(peerIndex)) {
activePeerSearchRequestByIndex.set(peerIndex, 0);
}
addAdditionalInstitution(institution, peerIndex);
}
render();
};
item.addEventListener("mousedown", (event) => {
// Commit selection before input blur handlers can clear/re-render results.
// Mouse selection is handled here to avoid duplicate mousedown+click dispatch.
event.preventDefault();
selectInstitution();
});
item.addEventListener("keydown", (event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
selectInstitution();
}
});
});
}
function addAdditionalInstitution(institution, peerIndex = null) {
if (!state.subject) return;
if (institutionId(state.subject) === institutionId(institution)) return;
if (selectedAdditional().some((item) => institutionId(item) === institutionId(institution))) return;
const slotIndex = Number.isInteger(peerIndex)
? peerIndex
: state.additional.findIndex((item) => !item);
if (slotIndex < 0 || slotIndex > 2) return;
state.additional[slotIndex] = institution;
els.peerSearches[slotIndex].value = "";
els.peerResults[slotIndex].innerHTML = "";
if (selectedAdditional().length >= 3) {
openAccordion("reportPanel");
queueFocus("reportFormatChoice");
} else {
openAccordion("additionalPanel");
const nextOpenIndex = state.additional.findIndex((item) => !item);
if (nextOpenIndex >= 0) {
queueFocus(`peerSearch:${nextOpenIndex}`);
}
}
}
function renderAdditional() {
const count = selectedAdditional().length;
els.peerSlots.forEach((slot, index) => {
const institution = state.additional[index];
const previousReady = index === 0 || Boolean(state.additional[index - 1]);
const unlocked = Boolean(state.subject) && previousReady;
const isActive = unlocked && !institution;
slot.classList.toggle("locked", !unlocked);
slot.classList.toggle("selected", Boolean(institution));
slot.classList.toggle("active", isActive);
const input = els.peerSearches[index];
const stateHint = els.peerStateHints[index];
const cityHint = els.peerCityHints[index];
const button = els.peerSearchButtons[index];
const selection = els.peerSelections[index];
input.disabled = !unlocked || Boolean(institution);
stateHint.disabled = !unlocked || Boolean(institution);
cityHint.disabled = !unlocked || Boolean(institution);
button.disabled = input.disabled;
input.placeholder = !state.subject
? "Select a subject institution first"
: !previousReady
? `Select additional institution ${index} first`
: institution
? "Institution selected"
: `Type ${formatTypeLabel(state.institutionType)} name and press Search or Enter`;
if (institution) {
selection.classList.remove("empty");
selection.innerHTML = `
${renderInstitutionDetails(institution)}
`;
} else {
selection.classList.add("empty");
selection.innerHTML = unlocked ? "No institution selected." : "Locked until prior selection is complete.";
}
});
els.peerSelections.forEach((selection) => {
selection.querySelectorAll(".remove-button").forEach((button) => {
button.addEventListener("click", () => {
const index = Number(button.dataset.removeIndex);
state.additional[index] = null;
for (let next = index + 1; next < state.additional.length; next += 1) {
state.additional[next] = null;
els.peerSearches[next].value = "";
els.peerStateHints[next].value = "";
els.peerCityHints[next].value = "";
els.peerResults[next].innerHTML = "";
}
render();
});
});
});
if (els.additionalCounter) {
els.additionalCounter.textContent = count >= 3
? "3 of 3 additional institutions selected."
: `${count} of 3 additional institutions selected.`;
}
updateAdditionalInstitutionHelper(count);
els.reportPanel.classList.toggle("is-prominent", count >= 3);
}
function renderLogo() {
const unlocked = Boolean(state.subject);
els.logoPanel.classList.toggle("is-muted", !unlocked);
els.logoLocked.hidden = unlocked;
els.logoSearchQuery.value = unlocked ? logoSearchInstitutionQuery() : "";
els.logoSearchButton.disabled = !unlocked || !logoSearchInteractionReady || logoSearchOpening;
els.autoColorToggle.disabled = !unlocked;
els.resetBrandingButton.disabled = !unlocked;
els.logoSelectionStatus.textContent = state.logo.logo_name ? "Logo selected" : "Awaiting logo";
els.autoColorToggle.classList.toggle("is-active", state.logo.autoExtract);
els.autoColorToggle.setAttribute("aria-pressed", String(state.logo.autoExtract));
els.brandPreviewInstitution.textContent = unlocked ? state.subject.name : "Your Institution";
els.brandPreviewTitle.textContent = unlocked ? `${state.subject.name} Analysis` : "Institution Analysis";
els.brandPreviewSubtitle.textContent = unlocked
? `${formatTypeLabel(state.institutionType)} performance and intelligence dashboard`
: "Interest Rate Risk & Performance Dashboard";
if (!unlocked) {
setBrandingWindowOpen(false);
} else if (state.ui.activeAccordion === "logoPanel" && !brandingCloseTimer) {
setBrandingWindowOpen(true);
} else {
syncBrandingWindowState();
}
updateBrandPreview();
}
function updateBrandPreview() {
const colors = state.logo.colors;
els.brandPreview.style.background = colors.primaryBg;
els.brandPreview.style.color = colors.textColor;
const previewMetrics = els.brandPreview.querySelectorAll(".preview-metric-card");
previewMetrics.forEach((card) => {
card.style.background = colors.cardBg;
card.style.borderColor = mixHex(colors.accentColor, colors.cardBg, 0.52);
card.style.color = colors.textColor;
});
els.brandPreviewTitle.style.color = colors.textColor;
els.brandPreviewSubtitle.style.color = mixHex(colors.textColor, colors.primaryBg, 0.34);
els.brandPreviewLogo.innerHTML = state.logo.logo_data_url
? `
`
: "HR";
els.brandPreviewLogo.style.background = colors.cardBg;
els.brandPreviewLogo.style.color = colors.accentColor;
els.brandPreview.querySelectorAll(".preview-metric-card strong").forEach((node) => {
node.style.color = colors.textColor;
});
els.brandPreview.querySelectorAll(".preview-metric-card small").forEach((node, index) => {
node.style.color = index % 2 === 0 ? "#34d399" : "#f87171";
});
const footer = els.brandPreview.querySelector(".brand-preview-footer");
if (footer) footer.style.color = mixHex(colors.textColor, colors.primaryBg, 0.46);
}
function syncGlowState(element, isActive) {
if (!element) return;
element.classList.toggle("glow-active", Boolean(isActive));
}
function firstEnabledPeerSearch() {
return els.peerSearches.find((input) => !input.disabled) || null;
}
function syncOpenStateGlow() {
const typeOpen = state.ui.activeAccordion === "institutionTypePanel";
const subjectOpen = state.ui.activeAccordion === "subjectPanel";
const additionalOpen = state.ui.activeAccordion === "additionalPanel";
const reportOpen = state.ui.activeAccordion === "reportPanel";
const activePeerInput = firstEnabledPeerSearch();
const customMode = selectedReportFormatMode() === "custom";
const activeQuarterSelect = customMode
? els.quarterSelects.find((select) => !select.disabled) || null
: null;
syncGlowState(els.typeToggle, typeOpen);
syncGlowState(els.subjectSearch, subjectOpen && !els.subjectSearch.disabled);
els.peerSearches.forEach((input) => {
syncGlowState(input, additionalOpen && input === activePeerInput);
});
syncGlowState(els.reportFormatChoice, reportOpen);
els.quarterSelects.forEach((select) => {
syncGlowState(select, reportOpen && customMode && select === activeQuarterSelect);
});
}
function subjectResultsListHasOptions() {
return Boolean(els.subjectResults?.querySelector(".result-item"));
}
function isSubjectResultsInteractiveOpen() {
if (!els.subjectResults || els.subjectResults.style.display === "none") return false;
return subjectResultsListHasOptions();
}
function scrollSubjectResultsIntoViewIfNeeded() {
if (!els.subjectResults || !isSubjectResultsInteractiveOpen()) return;
const rect = els.subjectResults.getBoundingClientRect();
const viewportHeight = Math.max(window.innerHeight || 0, document.documentElement?.clientHeight || 0);
if (rect.bottom > (viewportHeight - 8) || rect.top < 0) {
els.subjectResults.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "nearest" });
}
}
function applySubjectResultsAttentionCue() {
if (!els.subjectResults) return;
els.subjectResults.classList.remove("results-attention");
void els.subjectResults.offsetWidth;
els.subjectResults.classList.add("results-attention");
els.subjectResults.addEventListener("animationend", () => {
els.subjectResults?.classList.remove("results-attention");
}, { once: true });
}
function syncSubjectResultsGuidance() {
const hint = els.subjectResultsHint;
const indicator = els.subjectNextIndicator;
if (!hint || !indicator) return;
const resultsOpen = isSubjectResultsInteractiveOpen();
document.body.classList.toggle("subject-results-open", resultsOpen);
indicator.classList.toggle("visible", resultsOpen);
if (resultsOpen) {
hint.hidden = false;
hint.classList.remove("is-selected");
hint.textContent = "Select your institution from the list below to continue.";
return;
}
if (state.subject?.name) {
hint.hidden = false;
hint.classList.add("is-selected");
hint.textContent = `Selected: ${state.subject.name}`;
return;
}
hint.hidden = true;
hint.classList.remove("is-selected");
hint.textContent = "";
}
function stepNextBlockedBySubjectResults(validation) {
return clampStep(state.ui.routeStep) === "1"
&& isSubjectResultsInteractiveOpen()
&& !validation.subjectSelected;
}
function helperStepForCurrentState() {
const routeStep = clampStep(state.ui.routeStep);
if (routeStep === "4") {
return null;
}
if (routeStep === "1") {
if (!state.ui.institutionTypeChosen) {
return "step1";
}
return "step2";
}
if (routeStep === "2") {
return "step2b";
}
if (routeStep === "3") {
return "step3";
}
return null;
}
function helperAdditionalSelectedCount() {
return selectedAdditional().length;
}
function helperAdditionalAnchorForCount(countSelected) {
if (countSelected <= 0) return "anchor-step2b";
if (countSelected === 1) return "anchor-addl2";
return "anchor-addl3";
}
function updateAdditionalInstitutionHelper(countSelected) {
if (helperStepForCurrentState() !== "step2b") return;
const content = helperContent.step2b;
if (els.helperText && !els.helperPanel?.classList.contains("collapsed") && content) {
els.helperText.textContent = content.text.replace("0 of 3 additional institutions selected.", `${countSelected} of 3 additional institutions selected.`);
}
moveHelperTo(helperAdditionalAnchorForCount(countSelected));
const targetIndex = Math.min(countSelected, 2);
const targetInput = els.peerSearches[targetIndex] || els.peerSearches[2];
if (targetInput) {
positionArrow(targetInput);
}
}
function updateGuidancePosition(targetEl, guidanceEl, arrowEl) {
if (!targetEl || !guidanceEl || !arrowEl || guidanceEl.hidden) return;
const targetRect = targetEl.getBoundingClientRect();
const container = els.leftRailStack || guidanceEl.offsetParent;
if (!container || targetRect.width <= 0 || targetRect.height <= 0 || !targetEl.getClientRects().length) {
arrowEl.style.opacity = "0";
return;
}
const containerRect = container.getBoundingClientRect();
const guidanceHeight = guidanceEl.offsetHeight;
if (guidanceHeight <= 0) {
arrowEl.style.opacity = "0";
return;
}
const targetMidY = targetRect.top + (targetRect.height / 2);
const desiredViewportTop = targetMidY - (guidanceHeight / 2);
const relativeTop = desiredViewportTop - containerRect.top + container.scrollTop;
const maxTop = Math.max(0, container.scrollHeight - guidanceHeight);
guidanceEl.style.top = `${Math.round(Math.max(0, Math.min(relativeTop, maxTop)))}px`;
guidanceEl.style.zIndex = "9999";
const arrowSize = 32;
const preferredLeft = targetRect.left - arrowSize - 6;
const arrowLeft = Math.max(4, Math.min(preferredLeft, window.innerWidth - arrowSize - 4));
const arrowTop = Math.max(4, targetMidY - (arrowSize / 2));
arrowEl.style.transform = `translate3d(${Math.round(arrowLeft)}px, ${Math.round(arrowTop)}px, 0)`;
arrowEl.style.zIndex = "10000";
arrowEl.style.opacity = "1";
}
function scheduleGuidancePosition() {
if (helperPositionFrame) return;
helperPositionFrame = requestAnimationFrame(() => {
helperPositionFrame = 0;
updateGuidancePosition(helperActiveTargetElement, els.helperPanel, els.helperArrow);
});
}
function observeGuidanceTarget(targetEl) {
if (helperObservedTargetElement === targetEl) return;
if (helperResizeObserver && helperObservedTargetElement) {
helperResizeObserver.unobserve(helperObservedTargetElement);
}
helperObservedTargetElement = targetEl || null;
if (helperResizeObserver && helperObservedTargetElement) {
helperResizeObserver.observe(helperObservedTargetElement);
}
}
function setGuidanceTarget(targetEl) {
helperActiveTargetElement = targetEl || null;
observeGuidanceTarget(helperActiveTargetElement);
if (!helperActiveTargetElement && els.helperArrow) {
els.helperArrow.style.opacity = "0";
}
scheduleGuidancePosition();
}
function initializeGuidancePositioning() {
helperResizeObserver = new ResizeObserver(scheduleGuidancePosition);
if (els.helperPanel) helperResizeObserver.observe(els.helperPanel);
if (els.leftRailStack) helperResizeObserver.observe(els.leftRailStack);
helperMutationObserver = new MutationObserver(scheduleGuidancePosition);
helperMutationObserver.observe(document.body, {
subtree: true,
childList: true,
attributes: true,
attributeFilter: ["class", "hidden", "aria-hidden", "aria-expanded", "disabled"],
});
window.addEventListener("resize", scheduleGuidancePosition, { passive: true });
window.addEventListener("scroll", scheduleGuidancePosition, { passive: true, capture: true });
document.addEventListener("focusin", (event) => {
if (event.target === helperActiveTargetElement || helperActiveTargetElement?.contains(event.target)) {
scheduleGuidancePosition();
}
});
}
function attachHelperTo(anchorId, targetEl) {
if (anchorId) helperActiveAnchorId = anchorId;
if (targetEl) setGuidanceTarget(targetEl);
else scheduleGuidancePosition();
}
// Compatibility shims so existing call sites keep working unchanged.
function moveHelperTo(anchorId) {
attachHelperTo(anchorId, helperActiveTargetElement);
}
function positionArrow(targetElement) {
attachHelperTo(helperActiveAnchorId, targetElement);
}
function userInteracted() {
scheduleGuidancePosition();
}
function syncHelperPanel() {
if (!els.helperPanel || !els.helperText || !els.helperArrow) return;
const activeHelperStep = helperStepForCurrentState();
const content = activeHelperStep ? helperContent[activeHelperStep] : null;
const shouldReserveColumn = ["1", "2", "3"].includes(clampStep(state.ui.routeStep));
const isStep4 = clampStep(state.ui.routeStep) === "4";
document.body.classList.toggle("helper-rail-active", shouldReserveColumn);
els.helperPanel.hidden = !shouldReserveColumn;
els.helperPanel.classList.toggle("is-visible", Boolean(content));
els.helperPanel.classList.toggle("is-hidden", !content);
els.helperPanel.dataset.helperStep = activeHelperStep || "";
if (isStep4) {
setGuidanceTarget(null);
return;
}
if (!content) {
setGuidanceTarget(null);
return;
}
if (!els.helperPanel.classList.contains("collapsed")) {
const additionalCount = helperAdditionalSelectedCount();
const helperText = activeHelperStep === "step2b"
? content.text.replace("0 of 3 additional institutions selected.", `${additionalCount} of 3 additional institutions selected.`)
: content.text;
els.helperText.textContent = helperText;
}
const anchorId = activeHelperStep === "step2b"
? helperAdditionalAnchorForCount(helperAdditionalSelectedCount())
: content.anchorId;
moveHelperTo(anchorId);
const target = typeof content.target === "function" ? content.target() : null;
if (target) {
positionArrow(target);
} else {
setGuidanceTarget(null);
}
}
function hasRequiredUserFields() {
const requiredValues = getRequiredUserFieldValues();
const emailValue = requiredValues.email;
return Boolean(
requiredValues.firstName
&& requiredValues.lastName
&& emailValue.includes("@")
&& requiredValues.phone
&& requiredValues.organization
);
}
function getRequiredUserFieldValues() {
const requesterEmail = document.getElementById("email").value.trim();
return {
firstName: document.getElementById("firstName").value.trim(),
lastName: document.getElementById("lastName").value.trim(),
email: requesterEmail,
phone: document.getElementById("phone").value.trim(),
organization: document.getElementById("organization").value.trim(),
};
}
function clearMissingFieldHighlights() {
document.querySelectorAll(".input-missing").forEach((node) => {
node.classList.remove("input-missing");
});
}
function highlightMissingField(fieldId) {
const field = document.getElementById(fieldId);
field?.classList.add("input-missing");
}
function hasDisclaimerAcknowledged() {
return Boolean(els.disclaimerAck?.checked);
}
function computeValidationState() {
return {
institutionTypeSelected: Boolean(effectiveInstitutionType()),
subjectSelected: Boolean(effectiveSubjectInstitution()),
bankPeriodReady: (
bankPeriodConfirmationMatchesSubject()
&& cuPeriodConfirmationMatchesSelection()
),
peersSelected: effectiveAdditionalInstitutions().length,
reportFormatSelected: Boolean(selectedReportFormatMode()),
reportingPeriodValid: isValidPeriod(),
userFieldsComplete: hasRequiredUserFields(),
disclaimerAccepted: hasDisclaimerAcknowledged(),
};
}
function validationChanged(nextValidation) {
return (
state.validation.institutionTypeSelected !== nextValidation.institutionTypeSelected
|| state.validation.subjectSelected !== nextValidation.subjectSelected
|| state.validation.bankPeriodReady !== nextValidation.bankPeriodReady
|| state.validation.peersSelected !== nextValidation.peersSelected
|| state.validation.reportFormatSelected !== nextValidation.reportFormatSelected
|| state.validation.reportingPeriodValid !== nextValidation.reportingPeriodValid
|| state.validation.userFieldsComplete !== nextValidation.userFieldsComplete
|| state.validation.disclaimerAccepted !== nextValidation.disclaimerAccepted
);
}
function allRequirementsMet(validation = state.validation) {
return Boolean(
validation.institutionTypeSelected
&& validation.subjectSelected
&& validation.bankPeriodReady
&& validation.reportFormatSelected
&& validation.reportingPeriodValid
&& validation.userFieldsComplete
&& validation.disclaimerAccepted
);
}
function missingRequirementMessages(validation = state.validation) {
const items = [];
if (!validation.institutionTypeSelected) items.push("institution type");
if (!validation.subjectSelected) items.push("subject institution");
if (!validation.bankPeriodReady) {
items.push(
requiresCuPeriodDisclosure()
? "verified NCUA reporting-period confirmation"
: "validated bank reporting-period availability",
);
}
if (!validation.reportFormatSelected) items.push("report format");
if (!validation.reportingPeriodValid) items.push("report date selection");
if (!validation.userFieldsComplete) items.push("first name, last name, email, phone, and organization");
if (!validation.disclaimerAccepted) items.push("disclosure acknowledgement");
return items;
}
function syncReviewAccordion(validation, didValidationChange) {
const allValid = allRequirementsMet(validation);
if (allValid) {
state.ui.reviewAutoCollapsed = true;
if (state.ui.activeAccordion === "reviewPanel") {
openAccordion("");
} else {
syncOpenStateGlow();
}
return;
}
state.ui.reviewAutoCollapsed = false;
if (state.ui.reviewPrimed && (didValidationChange || state.ui.activeAccordion === "reviewPanel")) {
openAccordion("reviewPanel");
}
}
function syncSubmitButtonState(validation) {
const allValid = allRequirementsMet(validation);
if (allValid) {
state.ui.reviewAutoCollapsed = true;
}
const submitReady = allValid;
if (submitReady && !state.ui.wasSubmitReady && !state.ui.submitting) {
els.submitButton.classList.remove("ready-flash");
void els.submitButton.offsetWidth;
els.submitButton.classList.add("ready-flash");
}
state.ui.wasSubmitReady = submitReady;
els.submitButton.disabled = state.ui.submitting || !submitReady;
els.submitButton.classList.toggle("is-submitting", state.ui.submitting);
els.submitButton.textContent = state.ui.submitting
? state.account.selectedPlan === "pro"
? "Submitting Request and Starting Payment..."
: "Submitting Request..."
: state.ui.submitSucceeded
? SUCCESS_SUBMIT_BUTTON_TEXT
: state.account.selectedPlan === "pro"
? PROFESSIONAL_SUBMIT_BUTTON_TEXT
: FREE_SUBMIT_BUTTON_TEXT;
syncGlowState(els.submitButton, false);
const missingItems = missingRequirementMessages(validation);
if (state.ui.submitting) {
els.submitHelper.textContent = "";
els.submitHelper.className = "submit-helper";
els.submitButton.title = "";
} else if (submitReady) {
els.submitHelper.textContent = state.account.selectedPlan === "pro"
? "All required items are complete. Review the verified period, then submit and continue to payment."
: "All required items are complete. Submit Request is ready.";
els.submitHelper.className = "submit-helper is-ready";
els.submitButton.title = "";
} else {
els.submitHelper.textContent = `Submit unlocks after: ${missingItems.join(", ")}.`;
els.submitHelper.className = "submit-helper is-blocked";
els.submitButton.title = `Missing: ${missingItems.join(", ")}`;
}
}
function stepNextEnabled(validation) {
if (stepNextBlockedBySubjectResults(validation)) return false;
const step = clampStep(state.ui.routeStep);
if (step === "1") return Boolean(validation.subjectSelected);
if (step === "2") return true;
if (step === "3") return Boolean(validation.reportFormatSelected);
if (step === "3b") return true;
return false;
}
function syncStepVisibilityAndNavigation(validation) {
const currentStep = clampStep(state.ui.routeStep);
els.stepSections.forEach((section) => {
const sectionStep = clampStep(section.dataset.stepSection);
section.hidden = sectionStep !== currentStep;
});
els.stageTabs.forEach((tab) => {
const tabStep = clampStep(tab.dataset.routeStep);
tab.classList.toggle("active", tabStep === currentStep);
tab.classList.toggle("complete", tabStep < currentStep);
});
if (els.stepBackButton) {
els.stepBackButton.hidden = currentStep === "1";
els.stepBackButton.disabled = state.ui.submitting;
}
if (els.stepNextButton) {
const isLast = currentStep === "4";
const blockedBySubjectResults = stepNextBlockedBySubjectResults(validation);
els.stepNextButton.hidden = isLast;
els.stepNextButton.disabled = state.ui.submitting || !stepNextEnabled(validation);
els.stepNextButton.title = blockedBySubjectResults
? "Select an institution to continue."
: "";
}
}
function updateDashboardState() {
const nextValidation = computeValidationState();
const didValidationChange = validationChanged(nextValidation);
state.validation = nextValidation;
const checks = {
type: nextValidation.institutionTypeSelected,
subject: nextValidation.subjectSelected,
additional: true,
period: nextValidation.reportFormatSelected && nextValidation.reportingPeriodValid,
user: nextValidation.userFieldsComplete,
disclaimer: nextValidation.disclaimerAccepted,
};
const percent = Math.round(((stepIndex(state.ui.routeStep) + 1) / REGISTRATION_STEPS.length) * 100);
els.progressFill.style.width = `${percent}%`;
els.completionPercent.textContent = `${percent}%`;
els.statuses.type.textContent = checks.type ? formatTypeLabel(state.institutionType) : "Required";
document.getElementById("institutionTypePanel").classList.toggle("needs-attention", !checks.type);
els.statuses.subject.textContent = checks.subject ? "Selected" : checks.type ? "Ready" : "Locked";
els.statuses.additional.textContent = `${effectiveAdditionalInstitutions().length} selected`;
els.statuses.period.textContent = checks.period ? "Ready" : "Invalid";
els.statuses.logo.textContent = state.logo.logo_name ? "Logo added" : checks.subject ? "Optional" : "Locked";
els.statuses.review.textContent = percent === 100 ? "Ready" : "Pending";
if (QUICK_REQUEST_ENABLED) {
syncQuickRequestTypeUI(state.institutionType || quickRequestType());
}
[els.readiness, els.submitReadiness].forEach((targetGroup) => {
Object.entries(checks).forEach(([key, ready]) => {
const target = targetGroup[key];
if (!target) return;
target.classList.remove("valid-green", "invalid-red");
target.classList.add(ready ? "valid-green" : "invalid-red");
});
});
syncReviewAccordion(nextValidation, didValidationChange);
syncSubmitButtonState(nextValidation);
syncStepVisibilityAndNavigation(nextValidation);
syncHelperPanel();
}
function syncSelectionSummary() {
if (!els.selectionSummary?.institutionType) return;
const additionalList = effectiveAdditionalInstitutions();
const period = selectedPeriod();
const notesValue = document.getElementById("notes")?.value?.trim() || "";
const themeValue = state.logo.autoExtract ? "Standard" : "Manual / Preset";
const subject = effectiveSubjectInstitution();
const institutionIdLabel = (institution) => institution?.cert_or_charter || institution?.rssd_id || "Not available";
const locationLabel = (institution) => institution?.location || [institution?.city, institution?.state].filter(Boolean).join(", ") || "Location unavailable";
const logoSource = !state.logo.logo_name
? "No logo selected."
: state.logo.logo_source_url
? "Search for Logo"
: "Uploaded";
const reportQuarterLines = period.mode === "custom"
? els.quarterSelects.map((select) => select.options[select.selectedIndex]?.text || select.value).filter(Boolean)
: ["Last 5 quarters (default)"];
const additionalLines = additionalList.length
? additionalList.map((institution, index) => {
return `${index + 1}. ${institution.name || "Unknown"} | ${locationLabel(institution)} | FDIC/NCUA ID: ${institutionIdLabel(institution)}`;
}).join("
")
: "No additional institutions selected.";
const notesPreview = notesValue.length > 80 ? `${notesValue.slice(0, 80)}...` : (notesValue || "None");
const escapedNotesPreview = escapeHtml(notesPreview);
const escapedFullNotes = escapeHtml(notesValue || "None");
const colorSummaryHtml = `
${state.logo.colors.primaryBg.toUpperCase()}
${state.logo.colors.cardBg.toUpperCase()}
${state.logo.colors.accentColor.toUpperCase()}
`;
els.selectionSummary.institutionType.textContent = `Institution Type: ${formatTypeLabel(effectiveInstitutionType())}`;
els.selectionSummary.subjectInstitution.textContent = `Subject Institution: ${effectiveSubjectInstitution()?.name || "Not selected"}`;
els.selectionSummary.additionalInstitutions.textContent = `Additional Institutions: ${additionalList.length}`;
els.selectionSummary.reportFormat.textContent = `Report Format: ${period.mode === "custom" ? period.label : "Standard (last 5 completed quarters)"}`;
els.selectionSummary.logo.textContent = `Logo: ${state.logo.logo_name || "Not selected"}`;
els.selectionSummary.theme.textContent = `Theme: ${themeValue}`;
els.selectionSummary.colors.innerHTML = `Colors: ${colorSummaryHtml}`;
if (els.selectionSummary.notes) {
els.selectionSummary.notes.textContent = `Notes: ${notesValue || "None"}`;
}
if (els.selectionSummary.logoPreview) {
if (state.logo.logo_data_url) {
els.selectionSummary.logoPreview.innerHTML = `
`;
} else {
els.selectionSummary.logoPreview.textContent = "No logo selected.";
}
}
if (els.notesModal.inlinePreview) {
els.notesModal.inlinePreview.textContent = notesValue || "No notes added.";
}
if (els.submitSummary.subject) {
els.submitSummary.subject.innerHTML = subject
? `${escapeHtml(subject.name || "Unknown")}
${escapeHtml(locationLabel(subject))}
FDIC/NCUA ID: ${escapeHtml(institutionIdLabel(subject))}
${escapeHtml(formatTypeLabel(effectiveInstitutionType()))}`
: "No subject institution selected.";
}
if (els.submitSummary.additional) {
els.submitSummary.additional.innerHTML = additionalLines;
}
if (els.submitSummary.period) {
els.submitSummary.period.innerHTML = reportQuarterLines.map((line) => escapeHtml(line)).join("
");
}
syncFreePeriodAdjustmentNotice();
if (els.submitSummary.logo) {
els.submitSummary.logo.innerHTML = state.logo.logo_data_url
? `
Source: ${escapeHtml(logoSource)}
${escapeHtml(state.logo.logo_name || "")}`
: "No logo selected.";
}
if (els.submitSummary.colors) {
els.submitSummary.colors.innerHTML = colorSummaryHtml;
}
if (els.submitSummary.theme) {
els.submitSummary.theme.textContent = themeValue;
}
if (els.submitSummary.notes) {
els.submitSummary.notes.innerHTML = `${escapedNotesPreview} View Notes`;
const viewNotesLink = document.getElementById("submitSummaryViewNotes");
viewNotesLink?.addEventListener("click", () => {
if (!els.notesModal.modal) return;
els.notesModal.modal.hidden = false;
els.notesModal.modal.setAttribute("aria-hidden", "false");
document.body.classList.add("quick-request-modal-open");
document.getElementById("notes")?.focus();
}, { once: true });
}
}
function render() {
syncInstitutionTypeUI();
syncStep4SignupCopy();
els.subjectSelection.classList.toggle("empty", !state.subject);
els.subjectSelection.innerHTML = state.subject
? renderInstitutionDetails(state.subject)
: "No subject institution selected.";
const subjectQueryMatchesSelection = Boolean(
state.subject
&& normalizeMatchText(els.subjectSearch?.value || "") === normalizeMatchText(state.subject.name || "")
);
const shouldShowSubjectResults = !state.subject || !subjectQueryMatchesSelection;
if (!shouldShowSubjectResults) {
els.subjectResults.innerHTML = "";
}
els.subjectResults.style.display = shouldShowSubjectResults && !subjectResultsLocked ? "" : "none";
if (els.subjectResults.style.display === "none") {
els.subjectResults.classList.remove("results-attention");
}
syncSubjectResultsGuidance();
renderQuickRequestSummary();
renderQuickRequestModal();
renderLogo();
renderAdditional();
updateDashboardState();
syncBankPeriodDisclosure();
ensureFinalPeriodCheck();
syncOpenStateGlow();
applyPendingFocus();
requestAnimationFrame(syncWorkflowScrollHint);
persistSessionState();
syncSelectionSummary();
}
function buildCalendarQuarterOptions() {
const now = new Date();
const quarterEnds = [
{ quarter: "Q1", month: 2, day: 31 },
{ quarter: "Q2", month: 5, day: 30 },
{ quarter: "Q3", month: 8, day: 30 },
{ quarter: "Q4", month: 11, day: 31 },
];
const options = [];
for (let year = now.getFullYear(); year >= now.getFullYear() - 6; year -= 1) {
quarterEnds.forEach(({ quarter, month, day }) => {
const date = new Date(year, month, day);
if (date <= now) {
const value = `${String(month + 1).padStart(2, "0")}/${String(day).padStart(2, "0")}/${year}`;
options.push({ label: `${quarter} ${year}`, value, date });
}
});
}
options.sort((a, b) => b.date - a.date);
// Step 3 is a reporting-period preference selector, so expose every
// completed calendar quarter. Step 4 performs authoritative period and
// source checks before a paid request can proceed; free requests retain
// their prior-year adjustment behavior.
return options;
}
function reportingPeriodOffset(options) {
const now = new Date();
const previousYearEndIndex = options.findIndex(
(option) => option.label === `Q4 ${now.getFullYear() - 1}`,
);
if (state.account.selectedPlan === "pro") return 0;
if (previousYearEndIndex < 0 && state.institutionType === "credit_union") {
throw new Error("The verified NCUA list does not include the required prior-year period.");
}
return Math.max(previousYearEndIndex, 0);
}
function availableQuarterOptions() {
if (state.institutionType === "credit_union") {
return state.periodAvailability.loaded ? state.periodAvailability.options : [];
}
return buildCalendarQuarterOptions();
}
function freeReportingPeriodOptions(options = availableQuarterOptions()) {
const now = new Date();
const previousYearEndIndex = options.findIndex(
(option) => option.label === `Q4 ${now.getFullYear() - 1}`,
);
if (previousYearEndIndex < 0) return [];
return options.slice(previousYearEndIndex, previousYearEndIndex + els.quarterSelects.length);
}
function freePeriodAdjustmentDetails() {
if (state.account.selectedPlan === "pro" || selectedReportFormatMode() !== "custom") {
return null;
}
const selectedQuarters = selectedPeriod().quarters.filter((quarter) => quarter.value);
const freeQuarters = freeReportingPeriodOptions();
if (selectedQuarters.length !== els.quarterSelects.length || freeQuarters.length !== els.quarterSelects.length) {
return null;
}
const includedValues = new Set(freeQuarters.map((quarter) => quarter.value));
const excludedQuarters = selectedQuarters.filter((quarter) => !includedValues.has(quarter.value));
if (!excludedQuarters.length) return null;
return {
excludedLabels: excludedQuarters.map((quarter) => quarter.label),
adjustedLabels: freeQuarters.map((quarter) => quarter.label),
};
}
function syncFreePeriodAdjustmentNotice() {
const notice = els.submitSummary.freePeriodAdjustment;
const message = els.submitSummary.freePeriodAdjustmentMessage;
if (!notice || !message) return;
const details = state.ui.routeStep === "4" ? freePeriodAdjustmentDetails() : null;
notice.hidden = !details;
if (!details) {
message.textContent = "";
return;
}
message.textContent = `Your selection includes ${details.excludedLabels.join(", ")}, which ${details.excludedLabels.length === 1 ? "is" : "are"} not included in the free report. If you continue free, the report dates will be adjusted to ${details.adjustedLabels.join(", ")}. Upgrade to keep your selected dates.`;
}
function populateQuarterOptions(options, preservedValues = []) {
const periodOffset = reportingPeriodOffset(options);
const availableValues = new Set(options.map((option) => option.value));
els.quarterSelects.forEach((select, index) => {
const preserved = preservedValues[index];
select.innerHTML = options
.map((option) => ``)
.join("");
if (preserved && availableValues.has(preserved)) {
select.value = preserved;
} else {
select.selectedIndex = Math.min(index + periodOffset, options.length - 1);
}
});
if (requiresCuPeriodDisclosure()) {
state.cuPeriodConfirmation = emptyCuPeriodConfirmation();
}
}
function shouldPreserveQuarterSelections() {
return selectedReportFormatMode() === "custom";
}
function resetStandardQuarterSelections() {
if (selectedReportFormatMode() !== "standard") return;
if (state.institutionType === "credit_union") {
if (state.periodAvailability.loaded && state.periodAvailability.options.length >= 5) {
populateQuarterOptions(state.periodAvailability.options);
}
return;
}
populateQuarterOptions(buildCalendarQuarterOptions());
}
async function loadCreditUnionQuarterOptions({ force = false } = {}) {
const availability = state.periodAvailability;
if (availability.loading && !force) return;
const preservedValues = availability.loaded && shouldPreserveQuarterSelections()
? els.quarterSelects.map((select) => select.value)
: [];
const requestId = availability.requestId + 1;
availability.requestId = requestId;
availability.loading = true;
availability.loaded = false;
availability.error = "";
availability.options = [];
els.quarterSelects.forEach((select) => {
select.disabled = true;
});
els.periodHint.textContent = "Checking official NCUA Call Report availability...";
updateDashboardState();
try {
const response = await fetch("/api/registration/credit-union-reporting-periods", {
cache: "no-store",
headers: { Accept: "application/json" },
});
const contentType = response.headers.get("content-type") || "";
if (!contentType.includes("application/json")) {
throw new Error("The NCUA availability service returned an invalid response.");
}
const data = await response.json();
if (!response.ok || !data.success) {
throw new Error(data.message || data.detail || "NCUA period availability could not be verified.");
}
const options = Array.isArray(data.periods)
? data.periods.filter((period) => period?.identifier && period?.label && period?.value)
: [];
if (options.length < 5) {
throw new Error("Fewer than five verified NCUA reporting periods are available.");
}
if (requestId !== availability.requestId || state.institutionType !== "credit_union") return;
availability.options = options;
availability.loaded = true;
availability.stale = Boolean(data.stale);
availability.verifiedAt = data?.daily_cache?.refreshed_at || data?.verified_at || "";
populateQuarterOptions(options, preservedValues);
applyReportFormatMode();
render();
} catch (error) {
if (requestId !== availability.requestId || state.institutionType !== "credit_union") return;
availability.error = error.message || "NCUA period availability could not be verified.";
availability.loaded = false;
availability.options = [];
availability.verifiedAt = "";
els.quarterSelects.forEach((select) => {
select.innerHTML = "";
select.disabled = true;
});
applyReportFormatMode();
render();
} finally {
if (requestId === availability.requestId) {
availability.loading = false;
applyReportFormatMode();
updateDashboardState();
}
}
}
function buildQuarterOptions() {
if (state.institutionType === "credit_union") {
void loadCreditUnionQuarterOptions();
return;
}
state.periodAvailability.requestId += 1;
state.periodAvailability.loading = false;
state.periodAvailability.loaded = false;
state.periodAvailability.error = "";
state.periodAvailability.options = [];
state.periodAvailability.stale = false;
state.periodAvailability.verifiedAt = "";
const preservedValues = shouldPreserveQuarterSelections()
? els.quarterSelects.map((select) => select.value)
: [];
populateQuarterOptions(buildCalendarQuarterOptions(), preservedValues);
}
function applyWorkflowUser(user, lockedFields) {
const fieldMap = {
first_name: "firstName",
last_name: "lastName",
title: "title",
email: "email",
phone: "phone",
organization: "organization",
};
const locked = new Set(Array.isArray(lockedFields) ? lockedFields : []);
Object.entries(fieldMap).forEach(([key, elementId]) => {
const input = document.getElementById(elementId);
if (!input) return;
if (typeof user?.[key] === "string" && user[key]) {
input.value = user[key];
}
const shouldLock = locked.has(key);
input.readOnly = shouldLock;
input.classList.toggle("workflow-prefill-locked", shouldLock);
if (shouldLock) {
input.setAttribute("aria-readonly", "true");
input.title = "Loaded and locked from your paid signup.";
}
});
}
async function loadPaidWorkflow() {
if (!state.workflow.guid) return false;
try {
const response = await fetch(`/api/registration/workflow/${encodeURIComponent(state.workflow.guid)}`, {
cache: "no-store",
});
const data = await response.json();
if (!response.ok || !data.success) {
throw new Error(data.detail || data.message || "The paid workflow link is invalid or inactive.");
}
state.workflow.guid = data.workflow_guid;
state.workflow.loaded = true;
state.workflow.lockedFields = data.locked_fields || [];
state.account.selectedPlan = "pro";
state.account.paid = true;
state.account.verified = true;
state.account.email = data.user?.email || "";
state.account.message = data.message;
applyWorkflowUser(data.user, data.locked_fields);
buildQuarterOptions();
applyReportFormatMode();
render();
if (clampStep(state.ui.routeStep) === "4") {
await verifyStep4Account({ force: true });
}
return true;
} catch (error) {
state.workflow.loaded = false;
state.account.error = error.message || "The paid workflow link could not be loaded.";
state.account.message = state.account.error;
renderAccountVerification();
return false;
}
}
function applyVerifiedReportingPeriod(reportingPeriod) {
const quarters = Array.isArray(reportingPeriod?.quarters) ? reportingPeriod.quarters : [];
if (quarters.length !== els.quarterSelects.length) return;
const latestAvailable = Date.parse(quarters[0]?.value || "");
els.quarterSelects.forEach((select, index) => {
if (Number.isFinite(latestAvailable)) {
Array.from(select.options).forEach((candidate) => {
const candidateDate = Date.parse(candidate.value);
if (Number.isFinite(candidateDate) && candidateDate > latestAvailable) {
candidate.remove();
}
});
}
const quarter = quarters[index];
if (!quarter?.value) return;
let option = Array.from(select.options).find((candidate) => candidate.value === quarter.value);
if (!option) {
if (state.institutionType === "credit_union") return;
option = new Option(quarter.label || quarter.value, quarter.value);
select.add(option);
}
select.value = quarter.value;
});
applyReportFormatMode();
}
function renderAccountVerification() {
if (!els.accountVerificationStatus) return;
const account = state.account;
els.accountVerificationStatus.textContent = account.message;
els.accountVerificationStatus.className = "account-verification-status";
if (account.verifying) {
els.accountVerificationStatus.classList.add("is-verifying");
} else if (account.error) {
els.accountVerificationStatus.classList.add("is-error");
} else if (account.verified) {
els.accountVerificationStatus.classList.add(account.paid ? "is-paid" : "is-free");
}
}
function syncStep4SignupCopy() {
const professionalSelected = state.account.selectedPlan === "pro";
if (els.registrationInfoTitle) {
els.registrationInfoTitle.textContent = professionalSelected
? "Professional Signup Information"
: "Registration Information";
}
if (els.registrationInfoDescription) {
els.registrationInfoDescription.textContent = professionalSelected
? "Enter the contact and billing details used for secure Stripe checkout."
: "Contact details for follow-up.";
}
if (els.professionalSignupNotice) {
els.professionalSignupNotice.hidden = !professionalSelected;
}
if (state.ui.routeStep === "4" && els.pageSubtext) {
els.pageSubtext.textContent = professionalSelected
? "Confirm your signup information and selections, then continue to secure payment."
: "Confirm your information and submit your reporting package request.";
}
}
async function verifyStep4Account({ force = false } = {}) {
const email = document.getElementById("email")?.value.trim().toLowerCase() || "";
if (!email || !email.includes("@")) {
state.account.email = email;
state.account.verified = false;
state.account.verifying = false;
state.account.error = "";
state.account.message = state.checkout.requestedPlan === "pro"
? "Enter your billing email to continue with Professional checkout."
: "Enter your email to continue with the free request.";
renderAccountVerification();
return false;
}
if (!state.workflow.guid) {
const professionalRequested = state.checkout.requestedPlan === "pro";
state.account.email = email;
state.account.verified = true;
state.account.verifying = false;
state.account.paid = Boolean(state.checkout.sessionId);
state.account.selectedPlan = state.checkout.sessionId || professionalRequested ? "pro" : "free";
state.account.error = "";
state.account.message = state.checkout.sessionId
? "Stripe payment will be verified when this request is submitted."
: professionalRequested
? "Professional selected. Payment will open after you review and submit Step 4."
: "Free request. You can upgrade to the latest verified data before submission.";
renderAccountVerification();
return true;
}
if (state.workflow.loaded) {
state.account.email = email;
state.account.verified = true;
state.account.verifying = false;
state.account.paid = true;
state.account.selectedPlan = "pro";
state.account.error = "";
renderAccountVerification();
return true;
}
if (!force && state.account.verified && state.account.email === email) {
return true;
}
const requestId = state.account.requestId + 1;
state.account.requestId = requestId;
state.account.email = email;
state.account.verifying = true;
state.account.verified = false;
state.account.error = "";
state.account.message = "Verifying paid account and reporting-period access...";
renderAccountVerification();
try {
const institutionName = state.subject?.name || "";
const response = await fetch(
`/api/registration/account-status?email=${encodeURIComponent(email)}&institution_name=${encodeURIComponent(institutionName)}&institution_type=${encodeURIComponent(state.institutionType || "bank")}&rssd_id=${encodeURIComponent(String(state.subject?.rssd_id || ""))}`,
{ cache: "no-store" },
);
const data = await response.json();
if (requestId !== state.account.requestId) return false;
if (!response.ok || !data.success) {
throw new Error(data.detail || data.message || "Account verification is temporarily unavailable.");
}
state.account.verified = true;
state.account.paid = Boolean(data.paid);
state.account.selectedPlan = data.selected_plan === "pro" ? "pro" : "free";
state.account.message = data.message;
applyVerifiedReportingPeriod(data.reporting_period);
render();
if (data.previous_report && state.subject && !state.ui.forceRegeneration) {
const offerKey = `${data.previous_report.previous_job_id}:${email}`;
if (state.ui.previousOfferKey !== offerKey) {
state.ui.previousOfferKey = offerKey;
showPreviousReportOffer(data.previous_report, payload());
}
}
return true;
} catch (error) {
if (requestId !== state.account.requestId) return false;
state.account.verified = false;
state.account.error = error.message || "Account verification is temporarily unavailable.";
state.account.message = state.account.error;
renderAccountVerification();
return false;
} finally {
if (requestId === state.account.requestId) {
state.account.verifying = false;
renderAccountVerification();
}
}
}
function selectedPeriod() {
const mode = selectedReportFormatMode();
const quarters = els.quarterSelects.map((select) => ({
label: select.options[select.selectedIndex]?.text || "",
value: select.value,
}));
const first = quarters[0] || { label: "", value: "" };
const last = quarters[quarters.length - 1] || { label: "", value: "" };
return {
mode,
label: `${quarters.map((quarter) => quarter.label).filter(Boolean).join(", ")}`,
start_period: last.value,
end_period: first.value,
quarters,
};
}
function isValidPeriod() {
if (
state.institutionType === "credit_union"
&& (!state.periodAvailability.loaded || state.periodAvailability.loading || state.periodAvailability.error)
) {
return false;
}
const period = selectedPeriod();
if (period.quarters.length !== 5) return false;
return period.quarters.every((quarter) => quarter.value);
}
function selectedReportFormatMode() {
return els.reportFormatModes.find((input) => input.checked)?.value || "standard";
}
function applyReportFormatMode() {
const paidAccount = state.account.selectedPlan === "pro";
const customInput = els.reportFormatModes.find((input) => input.value === "custom");
if (customInput) customInput.disabled = false;
const isCustom = selectedReportFormatMode() === "custom";
const creditUnionUnavailable = state.institutionType === "credit_union"
&& (!state.periodAvailability.loaded || state.periodAvailability.loading || state.periodAvailability.error);
els.quarterSelects.forEach((select) => {
select.disabled = creditUnionUnavailable || !isCustom;
});
if (state.institutionType === "credit_union" && state.periodAvailability.loading) {
els.periodHint.textContent = "Checking official NCUA Call Report availability...";
} else if (state.institutionType === "credit_union" && state.periodAvailability.error) {
els.periodHint.textContent = `Credit-union period selection is disabled: ${state.periodAvailability.error}`;
} else if (state.institutionType === "credit_union" && state.periodAvailability.loaded) {
const newest = state.periodAvailability.options[0]?.label || "the newest verified quarter";
const cacheNote = state.periodAvailability.stale ? " (using the last verified cache)" : "";
els.periodHint.textContent = isCustom
? paidAccount
? `Custom mode: choose exactly 5 NCUA-verified quarters. Newest available: ${newest}${cacheNote}.`
: `Choose exactly 5 NCUA-verified quarters. Quarters outside the free report period will be adjusted before submission. Newest available: ${newest}${cacheNote}.`
: paidAccount
? `Paid account: standard mode uses the latest 5 NCUA-verified quarters through ${newest}${cacheNote}.`
: `Free report: fixed five-quarter verified period ending at the previous calendar year-end${cacheNote}.`;
} else {
els.periodHint.textContent = isCustom
? paidAccount
? "Custom mode: choose exactly 5 reporting quarters."
: "Choose exactly 5 reporting quarters. Quarters outside the free report period will be adjusted before submission."
: paidAccount
? "Paid account: standard mode uses the latest 5 completed quarters."
: "Free report: fixed five-quarter reporting period ending at the previous calendar year-end.";
}
}
function setStatus(message, kind = "") {
els.formStatus.textContent = message;
els.formStatus.className = `form-status ${kind}`.trim();
}
function hideSubmissionSuccessModal() {
if (!els.submissionSuccess.modal) return;
els.submissionSuccess.modal.hidden = true;
els.submissionSuccess.modal.setAttribute("aria-hidden", "true");
document.body.classList.remove("submission-success-modal-open");
state.ui.submissionSuccessOpen = false;
}
function openSubmissionSuccessModal() {
if (!els.submissionSuccess.modal) return;
els.submissionSuccess.modal.hidden = false;
els.submissionSuccess.modal.setAttribute("aria-hidden", "false");
document.body.classList.add("submission-success-modal-open");
state.ui.submissionSuccessOpen = true;
requestAnimationFrame(() => {
els.submissionSuccess.closeButton?.focus();
});
}
function closeAndExitAfterSubmission() {
sessionStorage.removeItem(REGISTRATION_SESSION_KEY);
hideSubmissionSuccessModal();
try {
if (window.parent && window.parent !== window) {
window.parent.postMessage({ type: "registration-close" }, "*");
}
} catch (error) {
// Ignore cross-window errors and continue with local navigation fallback.
}
try {
window.close();
} catch (error) {
// Some browsers disallow closing non-script-opened tabs.
}
// Fallback: if the tab cannot be closed, always return to landing.
setTimeout(() => {
if (!window.closed) {
window.location.assign("/");
}
}, 80);
}
function evaluateSubmissionReadiness(validation = computeValidationState()) {
const requiredValues = getRequiredUserFieldValues();
if (!validation.institutionTypeSelected) {
return { ready: false, error: "Select Bank or Credit Union.", focusFieldId: "subjectSearch" };
}
if (!validation.subjectSelected) {
return { ready: false, error: "Select a subject institution.", focusFieldId: "subjectSearch" };
}
if (!validation.bankPeriodReady) {
return {
ready: false,
error: requiresCuPeriodDisclosure()
? "The selected credit union’s reporting period must be verified against the official NCUA quarterly bulk files before payment."
: "The selected bank's latest live FFIEC UBPR period must be verified before submission.",
focusFieldId: "bankPeriodRetryButton",
};
}
if (!validation.reportFormatSelected) {
return { ready: false, error: "Select a report format.", focusFieldId: "reportFormatChoice" };
}
if (!validation.reportingPeriodValid) {
return { ready: false, error: "Select a valid reporting period.", focusFieldId: "quarter1" };
}
if (!requiredValues.firstName) {
return { ready: false, error: "First name is required.", focusFieldId: "firstName" };
}
if (!requiredValues.lastName) {
return { ready: false, error: "Last name is required.", focusFieldId: "lastName" };
}
if (!requiredValues.email.includes("@")) {
return { ready: false, error: "A valid email is required.", focusFieldId: "email" };
}
if (!requiredValues.phone) {
return { ready: false, error: "Phone is required.", focusFieldId: "phone" };
}
if (!requiredValues.organization) {
return { ready: false, error: "Organization is required.", focusFieldId: "organization" };
}
if (!validation.disclaimerAccepted) {
return {
ready: false,
error: "Please review and acknowledge the disclosures before submitting.",
focusFieldId: "disclaimerAck",
};
}
return { ready: true, error: "", focusFieldId: "" };
}
function validateForm() {
state.validation = computeValidationState();
clearMissingFieldHighlights();
const readiness = evaluateSubmissionReadiness(state.validation);
if (readiness.focusFieldId === "disclaimerAck") {
els.disclaimerAck?.classList.add("input-missing");
} else if (readiness.focusFieldId) {
highlightMissingField(readiness.focusFieldId);
}
return readiness.error;
}
function payload() {
const quickRequest = QUICK_REQUEST_ENABLED
? quickRequestSummaryData()
: { applied: false, disabled: true, message: "Quick Request is currently inactive." };
const institutionType = effectiveInstitutionType();
const logoPayload = {
...state.logo,
logo_data_url: state.logo.logo_data_url && state.logo.logo_data_url.length <= MAX_INLINE_LOGO_DATA_URL_LENGTH
? state.logo.logo_data_url
: "",
logo_omitted_reason: state.logo.logo_data_url && state.logo.logo_data_url.length > MAX_INLINE_LOGO_DATA_URL_LENGTH
? "inline_logo_too_large"
: "",
};
const requesterEmail = document.getElementById("email").value.trim();
return {
selected_plan: state.account.selectedPlan,
workflow_guid: state.workflow.guid,
checkout_session_id: state.checkout.sessionId,
force_regeneration: state.ui.forceRegeneration,
institution_type: institutionType,
subject_institution: effectiveSubjectInstitution(),
additional_institutions: effectiveAdditionalInstitutions(),
reporting_period: selectedPeriod(),
bank_period_confirmation: state.bankPeriodConfirmation,
quick_request: quickRequest,
logo_colors: logoPayload,
disclaimer_acknowledged: hasDisclaimerAcknowledged(),
// ⭐ ADD THIS FIELD ⭐
requester_email: document.getElementById("email").value.trim(),
user: {
first_name: document.getElementById("firstName").value.trim(),
last_name: document.getElementById("lastName").value.trim(),
title: document.getElementById("title").value.trim(),
email: document.getElementById("email").value.trim(),
phone: document.getElementById("phone").value.trim(),
organization: document.getElementById("organization").value.trim(),
notes: document.getElementById("notes").value.trim(),
},
timestamp: new Date().toISOString(),
};
}
function resetRegistrationFlow(statusMessage = "", statusKind = "") {
sessionStorage.removeItem(REGISTRATION_SESSION_KEY);
closeLogoSearchWindow();
els.form.reset();
state.institutionType = DEFAULT_INSTITUTION_TYPE;
state.subject = null;
state.additional = [null, null, null];
state.bankPeriodConfirmation = emptyBankPeriodConfirmation();
state.cuPeriodConfirmation = emptyCuPeriodConfirmation();
state.validation = {
institutionTypeSelected: false,
subjectSelected: false,
bankPeriodReady: true,
peersSelected: 0,
reportFormatSelected: true,
reportingPeriodValid: false,
userFieldsComplete: false,
disclaimerAccepted: false,
};
state.ui.activeAccordion = "institutionTypePanel";
state.ui.reviewAutoCollapsed = false;
state.ui.reviewPrimed = false;
state.ui.submitting = false;
state.ui.submitSucceeded = statusKind === "success";
state.ui.wasSubmitReady = false;
state.ui.pendingFocus = "";
state.ui.isBrandingWindowOpen = false;
state.ui.submissionSuccessOpen = false;
state.quickRequest = {
open: false,
applied: false,
activeRow: 0,
rows: [],
};
state.logo = {
logo_name: "",
logo_data_url: "",
logo_source_url: "",
logo_search_url: "",
autoExtract: true,
colors: {
primaryBg: "#0b3d66",
cardBg: "#1f4f7a",
textColor: "#f8fafc",
accentColor: "#d4af37",
},
};
els.typeButtons.forEach((button) => {
const active = button.dataset.type === DEFAULT_INSTITUTION_TYPE;
button.classList.toggle("active", active);
button.setAttribute("aria-pressed", String(active));
});
els.typeWarning.hidden = true;
els.subjectSearch.value = "";
els.subjectSearch.disabled = false;
els.subjectSearch.placeholder = `Type ${formatTypeLabel(DEFAULT_INSTITUTION_TYPE)} name and press Search or Enter`;
if (els.subjectStateHint) {
els.subjectStateHint.value = "";
els.subjectStateHint.disabled = false;
}
if (els.subjectCityHint) {
els.subjectCityHint.value = "";
els.subjectCityHint.disabled = false;
}
els.subjectSearchButton.disabled = false;
els.subjectResults.innerHTML = "";
els.peerSearches.forEach((input) => {
input.value = "";
});
els.peerStateHints.forEach((input) => {
input.value = "";
input.disabled = true;
});
els.peerCityHints.forEach((input) => {
input.value = "";
input.disabled = true;
});
els.peerResults.forEach((target) => {
target.innerHTML = "";
});
buildQuarterOptions();
applyReportFormatMode();
syncColorInputs();
if (els.logoPreview) {
els.logoPreview.classList.remove("has-logo", "is-dragover");
els.logoPreview.innerHTML = "Drag Selected Logo Here";
}
if (els.disclaimerAck) {
els.disclaimerAck.checked = false;
}
hideSubmissionSuccessModal();
closeQuickRequestModal();
setInstitutionType(DEFAULT_INSTITUTION_TYPE, { advance: false });
render();
if (statusMessage) {
setStatus(statusMessage, statusKind);
}
}
function closeFreeUpgradeModal() {
if (!els.freeUpgrade.modal) return;
els.freeUpgrade.modal.hidden = true;
els.freeUpgrade.modal.setAttribute("aria-hidden", "true");
document.body.classList.remove("submission-success-modal-open");
state.checkout.modalOpen = false;
}
function formatUpgradePeriod(label, value) {
const match = String(value || "").match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
if (!match) return label || value || "the latest verified reporting period";
const date = new Date(Number(match[3]), Number(match[1]) - 1, Number(match[2]));
const dateLabel = date.toLocaleDateString("en-US", {
month: "long",
day: "numeric",
year: "numeric",
});
return label ? `${dateLabel} (${label})` : dateLabel;
}
function bankQuarterOptionsThrough(periodEnd) {
const match = String(periodEnd || "").match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
if (!match) return [];
let year = Number(match[3]);
let quarter = Math.ceil(Number(match[1]) / 3);
const options = [];
const quarterEnd = {
1: ["03", "31"],
2: ["06", "30"],
3: ["09", "30"],
4: ["12", "31"],
};
for (let index = 0; index < 12; index += 1) {
const [month, day] = quarterEnd[quarter];
options.push({
label: `Q${quarter} ${year}`,
value: `${month}/${day}/${year}`,
});
quarter -= 1;
if (quarter === 0) {
quarter = 4;
year -= 1;
}
}
return options;
}
async function resolveLatestUpgradePeriod({ applyPaidSelection = false } = {}) {
if (effectiveInstitutionType() === "credit_union") {
await loadCreditUnionQuarterOptions({ force: true });
const latest = state.periodAvailability.options[0];
if (!state.periodAvailability.loaded || !latest?.label || !latest?.value) {
throw new Error(
state.periodAvailability.error || "The latest NCUA reporting period could not be verified."
);
}
if (applyPaidSelection) {
populateQuarterOptions(state.periodAvailability.options);
applyReportFormatMode();
}
state.cuPeriodConfirmation = {
status: "ready",
confirmed: true,
charter_number: String(state.subject?.cert_or_charter || "").trim(),
latest_period: latest.label,
period_end: latest.value,
verified_at: state.periodAvailability.verifiedAt || new Date().toISOString(),
source: "ncua_bulk_archives",
error: "",
};
return { label: latest.label, value: latest.value };
}
await verifyFinalBankPeriod({ force: true, allowFree: true });
if (
state.bankPeriodConfirmation.status !== "ready"
|| !state.bankPeriodConfirmation.latest_period
|| !state.bankPeriodConfirmation.period_end
) {
throw new Error(
state.bankPeriodConfirmation.error || "The latest bank reporting period could not be verified."
);
}
if (applyPaidSelection) {
const options = bankQuarterOptionsThrough(state.bankPeriodConfirmation.period_end);
if (options.length < 5) {
throw new Error("Five bank reporting periods could not be prepared.");
}
populateQuarterOptions(options);
applyReportFormatMode();
}
return {
label: state.bankPeriodConfirmation.latest_period,
value: state.bankPeriodConfirmation.period_end,
};
}
async function openFreeUpgradeModal() {
if (!els.freeUpgrade.modal) return;
state.checkout.modalOpen = true;
els.freeUpgrade.modal.hidden = false;
els.freeUpgrade.modal.setAttribute("aria-hidden", "false");
document.body.classList.add("submission-success-modal-open");
els.freeUpgrade.latestPeriod.textContent = "Checking the latest verified data...";
els.freeUpgrade.status.textContent = "";
els.freeUpgrade.status.className = "form-status";
els.freeUpgrade.upgradeButton.disabled = true;
els.freeUpgrade.freeButton.disabled = false;
try {
const latest = await resolveLatestUpgradePeriod();
state.checkout.latestPeriodLabel = formatUpgradePeriod(latest.label, latest.value);
els.freeUpgrade.latestPeriod.textContent = state.checkout.latestPeriodLabel;
els.freeUpgrade.upgradeButton.disabled = false;
} catch (error) {
els.freeUpgrade.latestPeriod.textContent = "Latest-data availability could not be verified.";
els.freeUpgrade.status.textContent = error.message || "Please continue with the free report.";
els.freeUpgrade.status.className = "form-status error";
}
els.freeUpgrade.upgradeButton?.focus();
}
async function startProfessionalCheckout() {
const fromUpgradeModal = state.checkout.modalOpen;
state.checkout.requestedPlan = "pro";
state.account.selectedPlan = "pro";
if (fromUpgradeModal) {
els.freeUpgrade.upgradeButton.disabled = true;
els.freeUpgrade.freeButton.disabled = true;
els.freeUpgrade.status.textContent = "Opening secure Stripe checkout...";
els.freeUpgrade.status.className = "form-status";
} else {
state.ui.submitting = true;
syncSubmitButtonState(state.validation);
setStatus("Opening secure Stripe checkout...", "");
}
try {
await resolveLatestUpgradePeriod();
persistSessionState();
const checkoutPayload = payload();
const response = await fetch("/api/stripe/checkout-session", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
plan: "professional",
email: checkoutPayload.user.email,
name: `${checkoutPayload.user.first_name} ${checkoutPayload.user.last_name}`.trim(),
institution_name: checkoutPayload.subject_institution.name,
phone: checkoutPayload.user.phone,
role: checkoutPayload.user.title,
success_path: "/registration/step4?upgrade=1&checkout=success&session_id={CHECKOUT_SESSION_ID}",
cancel_path: "/registration/step4?upgrade=1&checkout=cancelled",
}),
});
const data = await response.json().catch(() => ({}));
if (!response.ok || !data.success || !data.url) {
throw new Error(data.detail || data.message || "Stripe checkout could not be started.");
}
window.location.assign(data.url);
} catch (error) {
const message = error.message || "Stripe checkout could not be started.";
if (fromUpgradeModal) {
els.freeUpgrade.status.textContent = message;
els.freeUpgrade.status.className = "form-status error";
els.freeUpgrade.upgradeButton.disabled = false;
els.freeUpgrade.freeButton.disabled = false;
} else {
state.ui.submitting = false;
setStatus(message, "error");
render();
}
}
}
function continueAsFreeRequest() {
state.checkout.returnStatus = "free";
state.checkout.sessionId = "";
state.checkout.requestedPlan = "free";
state.account.selectedPlan = "free";
state.account.paid = false;
closeFreeUpgradeModal();
resetStandardQuarterSelections();
els.form.requestSubmit();
}
async function handleDirectCheckoutReturn() {
if (state.ui.routeStep !== "4" || state.workflow.guid) return;
if (state.checkout.returnStatus === "cancelled") {
if (state.checkout.requestedPlan === "pro") {
state.checkout.returnStatus = "";
state.account.selectedPlan = "pro";
state.account.paid = false;
state.account.message = "Payment was cancelled. Review Step 4 and submit again when you are ready.";
render();
setStatus("Stripe checkout was cancelled. No payment was processed.", "error");
return;
}
state.account.selectedPlan = "free";
state.account.paid = false;
state.account.message = "Payment was not completed. Continuing with the free prior-year report.";
resetStandardQuarterSelections();
render();
queueMicrotask(() => els.form.requestSubmit());
return;
}
if (state.checkout.returnStatus !== "success" || !state.checkout.sessionId) return;
state.checkout.requestedPlan = "pro";
state.account.selectedPlan = "pro";
state.account.paid = true;
state.account.verified = true;
state.account.message = "Stripe payment will be verified before the latest-data report is queued.";
try {
await resolveLatestUpgradePeriod({ applyPaidSelection: true });
render();
queueMicrotask(() => els.form.requestSubmit());
} catch (error) {
state.account.error = error.message || "The latest reporting period could not be verified.";
state.account.message = state.account.error;
render();
setStatus(state.account.error, "error");
}
}
async function submitForm(event) {
event.preventDefault();
hideSubmissionSuccessModal();
const error = validateForm();
const accountEmail = document.getElementById("email")?.value.trim() || "";
if (!accountEmail || !accountEmail.includes("@")) {
state.ui.submitSucceeded = false;
setStatus("Please enter a valid email address for the reporting package.", "error");
render();
return;
}
els.periodError.hidden = isValidPeriod();
if (error) {
state.ui.submitSucceeded = false;
setStatus(error, "error");
render();
return;
}
if (
!state.workflow.guid
&& !state.checkout.sessionId
&& !["free", "cancelled"].includes(state.checkout.returnStatus)
) {
if (state.checkout.requestedPlan === "pro") {
await startProfessionalCheckout();
return;
}
await openFreeUpgradeModal();
return;
}
await verifyStep4Account({ force: true });
const submitPayload = payload();
const isCreditUnionSubmission = effectiveInstitutionType() === "credit_union";
state.ui.submitting = true;
state.ui.submitSucceeded = false;
syncGlowState(els.submitButton, false);
syncSubmitButtonState(state.validation);
setStatus(
isCreditUnionSubmission
? "Credit Union request received. Starting the CU workflow; this can take several minutes."
: ""
);
// CU runs are long-lived cross-service requests. Keep the request open so Cloud Run keeps
// processing, but keep the UI visibly alive while the server works.
const slowNoticeId = setTimeout(() => {
if (state.ui.submitting) {
if (isCreditUnionSubmission) {
setStatus(
"CU workflow is still running. Please keep this page open; the secure-link email will be sent when packaging finishes.",
""
);
} else {
state.ui.submitting = false;
state.ui.submitSucceeded = true;
setStatus(
"Your request has been received and is being processed. You will receive an email when your report is ready.",
"success"
);
render();
openSubmissionSuccessModal();
}
}
}, isCreditUnionSubmission ? 3000 : SLOW_SUBMIT_NOTICE_MS);
const submitController = new AbortController();
const submitTimeoutId = isCreditUnionSubmission
? null
: setTimeout(() => {
submitController.abort("submit-confirmation-timeout");
}, SUBMIT_CONFIRMATION_TIMEOUT_MS);
try {
const response = await fetch("/api/registration/submit", {
method: "POST",
headers: { "Content-Type": "application/json" },
signal: submitController.signal,
body: JSON.stringify(submitPayload),
});
const data = await response.json();
if (response.status === 409 && ["previous_report", "previous_free_report"].includes(data.code)) {
state.ui.submitting = false;
render();
showPreviousReportOffer(data, submitPayload);
return;
}
if (!response.ok || !data.success) {
throw new Error(data.detail || data.message || "Submission failed.");
}
const providerMessage = data.email?.provider ? ` Provider: ${data.email.provider}.` : "";
const successMessage = `${data.message} ${data.email?.message || ""}${providerMessage}`.trim();
state.ui.submitting = false;
state.ui.submitSucceeded = true;
state.ui.forceRegeneration = false;
setStatus(successMessage, data.email?.sent ? "success" : "");
render();
openSubmissionSuccessModal();
return;
} catch (submitError) {
const submitTimedOut = submitError?.name === "AbortError";
state.ui.submitSucceeded = false;
if (submitTimedOut) {
setStatus(
"Submission confirmation timed out. The request may already be queued and processing may have started; please check queue status before re-submitting.",
""
);
} else {
setStatus(submitError.message || "Submit request failed.", "error");
}
} finally {
clearTimeout(slowNoticeId);
if (submitTimeoutId) {
clearTimeout(submitTimeoutId);
}
if (state.ui.submitting) {
state.ui.submitting = false;
render();
}
}
}
function showPreviousReportOffer(data, submitPayload) {
document.getElementById("previousFreeReportModal")?.remove();
const modal = document.createElement("div");
modal.id = "previousFreeReportModal";
modal.className = "submission-success-modal";
modal.setAttribute("role", "dialog");
modal.setAttribute("aria-modal", "true");
modal.innerHTML = `
Report Already Generated
${escapeHtml(data.message || "This institution's report has already been generated for this email address.")}
${data.regenerate ? '' : ""}
`;
document.body.appendChild(modal);
document.body.classList.add("submission-success-modal-open");
modal.querySelector('[data-action="regenerate"]')?.addEventListener("click", () => {
state.ui.forceRegeneration = true;
modal.remove();
document.body.classList.remove("submission-success-modal-open");
els.form.requestSubmit();
});
modal.querySelector('[data-action="close"]').addEventListener("click", () => {
modal.remove();
document.body.classList.remove("submission-success-modal-open");
});
modal.querySelector('[data-action="resend"]').addEventListener("click", async (event) => {
const button = event.currentTarget;
const status = modal.querySelector("[data-status]");
button.disabled = true;
status.textContent = "Creating and emailing a fresh secure link…";
try {
const response = await fetch(data.resend_url || "/api/registration/previous-report/resend", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
job_id: data.previous_job_id,
institution_name: submitPayload.subject_institution.name,
requester_email: submitPayload.requester_email,
}),
});
const result = await response.json();
if (!response.ok || !result.success) {
throw new Error(result.detail || result.message || "Resend failed.");
}
status.textContent = result.message;
status.className = "form-status success";
} catch (error) {
status.textContent = error.message || "Unable to resend the secure link.";
status.className = "form-status error";
button.disabled = false;
}
});
}
function setupLogoUpload() {
els.logoSearchButton.addEventListener("click", () => {
if (!state.subject) return;
if (!logoSearchInteractionReady) {
setStatus("Preparing logo search… drag will be ready shortly.", "");
return;
}
const query = (els.logoSearchQuery.value || "").trim() || logoSearchInstitutionQuery();
state.logo.logo_search_url = `https://www.google.com/search?tbm=isch&q=${encodeURIComponent(query)}`;
logoSearchOpening = true;
setStatus("Preparing logo search… drag will be ready shortly.", "");
render();
// Readiness was established before the button became interactive, so the
// popup can be opened synchronously inside the trusted click event. This
// avoids popup blocking and first-open event-loop stalls.
openLogoSearchWindow();
logoSearchOpening = false;
render();
});
els.logoUpload.addEventListener("change", () => {
const file = els.logoUpload.files[0];
if (!file) return;
closeLogoSearchWindow();
handleLogoFile(file);
});
setupLogoDropZone();
}
function openLogoSearchWindow() {
if (!state.logo.logo_search_url) return;
const availWidth = Math.max(window.outerWidth, window.screen?.availWidth || 0);
const availHeight = Math.max(window.outerHeight, window.screen?.availHeight || 0);
const width = Math.min(900, Math.max(560, Math.round(availWidth * 0.46)));
const height = Math.min(900, Math.max(560, Math.round(availHeight * 0.78)));
const left = Math.max(0, window.screenX + window.outerWidth - width - 16);
const preferredTop = Math.max(0, window.screenY + 24);
const maxTop = Math.max(0, window.screenY + availHeight - height - 12);
const top = Math.min(preferredTop, maxTop);
const windowFeatures = `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes`;
// Reuse the existing search window when possible to keep drag/drop behavior stable.
if (logoSearchPopupWindow && !logoSearchPopupWindow.closed) {
try {
logoSearchPopupWindow.location.href = state.logo.logo_search_url;
logoSearchPopupWindow.focus();
} catch (error) {
closeLogoSearchWindow();
}
}
if (!logoSearchPopupWindow || logoSearchPopupWindow.closed) {
// Open a regular named browser window (not popup mode) for more consistent cross-window dragging.
logoSearchPopupWindow = window.open("about:blank", "registrationLogoSearch", windowFeatures);
if (logoSearchPopupWindow && !logoSearchPopupWindow.closed) {
try {
logoSearchPopupWindow.location.href = state.logo.logo_search_url;
logoSearchPopupWindow.focus();
} catch (error) {
closeLogoSearchWindow();
}
}
}
if (!logoSearchPopupWindow || logoSearchPopupWindow.closed) {
logoSearchPopupWindow = window.open(state.logo.logo_search_url, "_blank");
}
}
function setLogoPreview(src, name, sourceUrl = "") {
state.logo.logo_name = name;
state.logo.logo_data_url = src;
state.logo.logo_source_url = sourceUrl;
els.logoPreview.classList.add("has-logo");
els.logoPreview.innerHTML = `
${name}
`;
if (state.logo.autoExtract) {
extractLogoColor(src);
}
render();
}
function handleLogoFile(file, onSelected = null) {
const fileName = String(file?.name || "");
const looksLikeImage = Boolean(
file && (
String(file.type || "").startsWith("image/")
|| /\.(png|jpe?g|gif|webp|svg|bmp|ico)$/i.test(fileName)
)
);
if (!looksLikeImage) {
setStatus("Drop or upload an image file for the logo.", "error");
return;
}
const reader = new FileReader();
reader.onload = () => {
setLogoPreview(reader.result, file.name);
onSelected?.();
};
reader.readAsDataURL(file);
}
function handleLogoUrl(url, onSelected = null) {
const cleanUrl = (url || "").trim();
if (!/^https?:\/\//i.test(cleanUrl) && !cleanUrl.startsWith("data:image/")) {
setStatus("Drag an image from the search window or upload a saved logo file.", "error");
return false;
}
setLogoPreview(cleanUrl, "Dragged logo image", cleanUrl);
onSelected?.();
return true;
}
function decodeDraggedHtml(value) {
const textarea = document.createElement("textarea");
textarea.innerHTML = String(value || "");
return textarea.value;
}
function unwrapDraggedImageUrl(value) {
const candidate = decodeDraggedHtml(value).trim().replace(/^['"]|['"]$/g, "");
if (!candidate) return "";
if (candidate.startsWith("data:image/")) return candidate;
try {
const parsed = new URL(candidate, window.location.href);
const googleRedirect = /(^|\.)google\./i.test(parsed.hostname)
|| /(^|\.)googleusercontent\.com$/i.test(parsed.hostname);
if (googleRedirect) {
for (const key of ["imgurl", "mediaurl", "image_url"]) {
const nested = parsed.searchParams.get(key);
if (nested && /^https?:\/\//i.test(nested)) return nested;
}
}
return /^https?:$/i.test(parsed.protocol) ? parsed.href : "";
} catch (error) {
return "";
}
}
function extractDroppedLogoUrl(dataTransfer) {
const transferText = (format) => {
try {
return String(dataTransfer?.getData(format) || "");
} catch (_error) {
return "";
}
};
const uriList = transferText("text/uri-list")
.split(/\r?\n/)
.find((line) => line.trim() && !line.trim().startsWith("#"));
const plainText = transferText("text/plain");
const mozUrl = transferText("text/x-moz-url").split(/\r?\n/)[0];
const mozUrlData = transferText("text/x-moz-url-data");
const downloadUrl = transferText("DownloadURL").match(/^(?:[^:]+):(?:[^:]*):(https?:\/\/.*)$/i)?.[1] || "";
const htmlText = transferText("text/html");
const htmlLinkCandidates = [];
const htmlImageCandidates = [];
if (htmlText) {
const documentFragment = new DOMParser().parseFromString(htmlText, "text/html");
documentFragment.querySelectorAll("img, source").forEach((node) => {
[
"data-src",
"data-original",
"data-lazy-src",
"data-iurl",
"data-image-url",
"src",
"srcset",
].forEach((attribute) => {
const value = node.getAttribute(attribute);
if (!value) return;
if (attribute === "srcset") {
String(value).split(",").forEach((candidate) => {
const source = candidate.trim().split(/\s+/)[0];
if (source) htmlImageCandidates.push(source);
});
return;
}
htmlImageCandidates.push(String(value).trim());
});
const backgroundMatch = String(node.getAttribute("style") || "")
.match(/background-image\s*:\s*url\((['"]?)(.*?)\1\)/i);
if (backgroundMatch?.[2]) htmlImageCandidates.push(backgroundMatch[2]);
});
documentFragment.querySelectorAll("a[href]").forEach((node) => {
htmlLinkCandidates.push(node.getAttribute("href"));
});
}
// Browser image searches commonly put the source page in text/uri-list and
// an original image URL in a Google redirect. Prefer redirect URLs before
// thumbnail sources, then fall back to ordinary image element sources.
for (const candidate of [
...htmlLinkCandidates,
downloadUrl,
mozUrlData,
mozUrl,
uriList,
...htmlImageCandidates,
plainText,
]) {
const resolved = unwrapDraggedImageUrl(candidate);
if (resolved) return resolved;
}
return "";
}
function extractDroppedLogoFile(dataTransfer) {
const files = Array.from(dataTransfer?.files || []);
const isUsableImageFile = (file) => {
const fileName = String(file?.name || "");
const imageType = String(file?.type || "").startsWith("image/");
const imageExtension = /\.(png|jpe?g|gif|webp|svg|bmp|ico)$/i.test(fileName);
return Boolean(file && Number(file.size || 0) > 0 && (imageType || imageExtension));
};
const imageFile = files.find(isUsableImageFile);
if (imageFile) return imageFile;
const itemFiles = Array.from(dataTransfer?.items || [])
.filter((item) => item.kind === "file")
.map((item) => {
try {
return item.getAsFile();
} catch (_error) {
return null;
}
})
.filter(Boolean);
return itemFiles.find(isUsableImageFile) || null;
}
function completeLogoDrop(sourceWindow) {
setStatus("Logo selected — closing search window.", "success");
closeLogoSearchWindow({ aggressive: true, targetWindow: sourceWindow });
// A script-opened window should close in all supported browsers. If a
// browser policy refuses, return focus and clearly tell the user what
// happened; never affect a newer sequential-search window.
window.setTimeout(() => {
if (!sourceWindow || sourceWindow.closed) return;
try {
sourceWindow.blur();
window.focus();
} catch (error) {
// Cross-origin focus restrictions are harmless here.
}
setStatus("Logo selected — closing search window. If it remains open, close it manually.", "");
}, 100);
}
function setupLogoDropZone() {
if (logoDropZoneReady || !els.logoPreview) return;
logoDropZoneReady = true;
const keepLogoDropEnabled = (event) => {
if (!isLogoDropZoneInteractive()) return;
event.preventDefault();
};
window.addEventListener("dragenter", keepLogoDropEnabled);
window.addEventListener("dragover", keepLogoDropEnabled);
["dragenter", "dragover"].forEach((eventName) => {
els.logoPreview.addEventListener(eventName, (event) => {
event.preventDefault();
event.stopPropagation();
event.dataTransfer.dropEffect = "copy";
els.logoPreview.classList.add("is-dragover");
});
});
["dragleave", "drop"].forEach((eventName) => {
els.logoPreview.addEventListener(eventName, (event) => {
event.preventDefault();
event.stopPropagation();
els.logoPreview.classList.remove("is-dragover");
});
});
els.logoPreview.addEventListener("drop", (event) => {
event.preventDefault();
const sourceWindow = logoSearchPopupWindow;
const imageUrl = extractDroppedLogoUrl(event.dataTransfer);
if (imageUrl) {
handleLogoUrl(imageUrl, () => completeLogoDrop(sourceWindow));
return;
}
const file = extractDroppedLogoFile(event.dataTransfer);
if (file) {
handleLogoFile(file, () => completeLogoDrop(sourceWindow));
return;
}
setStatus(
"The browser did not provide an image for that drop. Try dragging the image itself, copying and pasting it, or use Browse.",
"error"
);
});
window.addEventListener("paste", (event) => {
if (!isLogoDropZoneInteractive()) return;
const imageItem = Array.from(event.clipboardData?.items || []).find((item) => item.type.startsWith("image/"));
if (imageItem) {
event.preventDefault();
closeLogoSearchWindow();
handleLogoFile(imageItem.getAsFile());
}
});
}
function extractLogoColor(src) {
const image = new Image();
image.crossOrigin = "anonymous";
image.onload = () => {
try {
const canvas = document.createElement("canvas");
const size = 48;
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext("2d", { willReadFrequently: true });
ctx.drawImage(image, 0, 0, size, size);
const palette = extractPalette(ctx.getImageData(0, 0, size, size).data);
if (!palette.length) return;
applyExtractedColors(palette);
syncColorInputs();
updateBrandPreview();
setStatus("Brand colors extracted from logo. You can adjust them before submitting.", "success");
} catch (error) {
// Remote logos can block canvas reads through CORS; keep the preview and let users adjust colors manually.
}
};
image.onerror = () => {
setStatus("Logo preview loaded from a remote source, but color extraction was blocked. You can set colors manually.", "");
};
image.src = src;
}
function extractPalette(data) {
const buckets = new Map();
for (let i = 0; i < data.length; i += 4) {
if (data[i + 3] < 80) continue;
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const luminance = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
const saturation = max === 0 ? 0 : (max - min) / max;
if (luminance > 0.94 || luminance < 0.05 || saturation < 0.06) continue;
const key = [r, g, b].map((value) => Math.round(value / 24) * 24).join(",");
const bucket = buckets.get(key) || { r: 0, g: 0, b: 0, count: 0, saturation: 0, luminance: 0 };
bucket.r += r;
bucket.g += g;
bucket.b += b;
bucket.count += 1;
bucket.saturation += saturation;
bucket.luminance += luminance;
buckets.set(key, bucket);
}
return Array.from(buckets.values())
.map((bucket) => ({
r: Math.round(bucket.r / bucket.count),
g: Math.round(bucket.g / bucket.count),
b: Math.round(bucket.b / bucket.count),
count: bucket.count,
saturation: bucket.saturation / bucket.count,
luminance: bucket.luminance / bucket.count,
}))
.sort((a, b) => (b.count * (1 + b.saturation)) - (a.count * (1 + a.saturation)));
}
function rgbToHex({ r, g, b }) {
return `#${[r, g, b].map((value) => value.toString(16).padStart(2, "0")).join("")}`;
}
function hexToRgb(hex) {
const normalized = String(hex || "").trim().replace("#", "");
if (!/^[0-9a-fA-F]{6}$/.test(normalized)) return null;
return {
r: Number.parseInt(normalized.slice(0, 2), 16),
g: Number.parseInt(normalized.slice(2, 4), 16),
b: Number.parseInt(normalized.slice(4, 6), 16),
};
}
function mixHex(colorA, colorB, ratio = 0.5) {
const rgbA = hexToRgb(colorA);
const rgbB = hexToRgb(colorB);
if (!rgbA || !rgbB) return colorA;
return rgbToHex({
r: Math.round((rgbA.r * (1 - ratio)) + (rgbB.r * ratio)),
g: Math.round((rgbA.g * (1 - ratio)) + (rgbB.g * ratio)),
b: Math.round((rgbA.b * (1 - ratio)) + (rgbB.b * ratio)),
});
}
function normalizeHex(value, fallback) {
const normalized = String(value || "").trim().toUpperCase();
if (/^#[0-9A-F]{6}$/.test(normalized)) return normalized;
if (/^[0-9A-F]{6}$/.test(normalized)) return `#${normalized}`;
return String(fallback || "#000000").toUpperCase();
}
function mixColor(color, target, amount) {
const mix = (value, targetValue) => Math.round(value + (targetValue - value) * amount);
return {
r: mix(color.r, target.r),
g: mix(color.g, target.g),
b: mix(color.b, target.b),
};
}
function applyExtractedColors(palette) {
const primary = palette.find((color) => color.luminance < 0.45) || palette[0];
const accent = palette.find((color) => color.saturation > 0.28 && color.luminance >= 0.22) || palette[1] || primary;
const card = mixColor(primary, { r: 255, g: 255, b: 255 }, primary.luminance < 0.35 ? 0.16 : 0.08);
const textColor = primary.luminance > 0.58 ? "#0b1220" : "#f8fafc";
state.logo.colors.primaryBg = rgbToHex(primary);
state.logo.colors.cardBg = rgbToHex(card);
state.logo.colors.textColor = textColor;
state.logo.colors.accentColor = rgbToHex(accent);
}
function syncColorInputs() {
Object.entries(els.colorInputs).forEach(([key, input]) => {
input.value = state.logo.colors[key];
});
Object.entries(els.colorHexInputs).forEach(([key, input]) => {
input.value = state.logo.colors[key].toUpperCase();
});
}
function applyPreset(presetName) {
const presets = {
"corporate-blue": { primaryBg: "#0F1B2D", cardBg: "#1E2D40", textColor: "#E2E8F0", accentColor: "#3B82F6" },
"forest-green": { primaryBg: "#0D2414", cardBg: "#1C3B2A", textColor: "#E8F7EF", accentColor: "#42C781" },
burgundy: { primaryBg: "#2B0E17", cardBg: "#4A1826", textColor: "#F8EAF0", accentColor: "#D94F7A" },
midnight: { primaryBg: "#111426", cardBg: "#1E2448", textColor: "#E7EDFF", accentColor: "#6D7DFF" },
"clean-light": { primaryBg: "#E8EDF3", cardBg: "#C8D5E4", textColor: "#122033", accentColor: "#4B89D8" },
};
const palette = presets[presetName];
if (!palette) return;
state.logo.autoExtract = false;
Object.assign(state.logo.colors, palette);
syncColorInputs();
render();
}
function resetBrandingPalette() {
Object.assign(state.logo.colors, {
primaryBg: "#0b3d66",
cardBg: "#1f4f7a",
textColor: "#f8fafc",
accentColor: "#d4af37",
});
syncColorInputs();
render();
}
function bindHelperInteractionEvents() {
const interactTargets = [
els.subjectSearch,
els.subjectSearchButton,
...els.peerSearches,
...els.peerSearchButtons,
...els.reportFormatModes,
...els.quarterSelects,
...els.typeButtons,
].filter(Boolean);
interactTargets.forEach((target) => {
target.addEventListener("input", userInteracted);
target.addEventListener("focus", userInteracted);
target.addEventListener("click", userInteracted);
target.addEventListener("change", userInteracted);
});
}
function bindEvents() {
bindHelperInteractionEvents();
els.typeButtons.forEach((button) => {
button.addEventListener("click", () => setInstitutionType(button.dataset.type));
});
if (QUICK_REQUEST_ENABLED && els.quickRequest.modal && els.quickRequest.openButton && els.quickRequest.gridBody) {
els.quickRequest.typeInputs.forEach((input) => {
input.addEventListener("change", () => {
syncQuickRequestTypeUI(input.value);
if (input.checked && (!state.institutionType || !hasSelections())) {
setInstitutionType(input.value);
}
});
});
els.quickRequest.openButton.addEventListener("click", openQuickRequestModal);
els.quickRequest.editButton?.addEventListener("click", openQuickRequestModal);
els.quickRequest.cancelButton?.addEventListener("click", closeQuickRequestModal);
els.quickRequest.applyButton?.addEventListener("click", applyQuickRequestModal);
els.quickRequest.addRowButton?.addEventListener("click", addQuickRequestPeerRow);
els.quickRequest.modal.addEventListener("click", (event) => {
if (event.target.dataset.quickClose === "true") {
closeQuickRequestModal();
}
});
els.quickRequest.gridBody.addEventListener("input", (event) => {
const target = event.target;
if (!(target instanceof HTMLInputElement)) return;
const rowIndex = Number(target.dataset.quickRowIndex);
const field = target.dataset.quickField;
if (!Number.isInteger(rowIndex) || !field) return;
updateQuickRequestRow(rowIndex, field, target.value);
target.classList.remove("quick-required", "quick-valid", "quick-invalid");
target.classList.add(getQuickRequestValidationClass(target.value));
}, true);
els.quickRequest.gridBody.addEventListener("click", (event) => {
const target = event.target;
if (!(target instanceof HTMLElement)) return;
if (
target instanceof HTMLInputElement
|| target instanceof HTMLTextAreaElement
|| target instanceof HTMLSelectElement
|| target.closest("label")
) {
return;
}
const accordionButton = target.closest("[data-quick-accordion]");
const deleteButton = target.closest("[data-quick-delete]");
const accordionIndex = accordionButton?.getAttribute("data-quick-accordion");
const deleteIndex = deleteButton?.getAttribute("data-quick-delete");
if (accordionIndex !== null) {
state.quickRequest.activeRow = Number(accordionIndex);
renderQuickRequestModal();
return;
}
if (deleteIndex !== null) {
removeQuickRequestPeerRow(Number(deleteIndex));
}
});
}
els.clearSelectionsButton.addEventListener("click", clearSelections);
els.subjectSearch.addEventListener("input", () => {
if (
!state.subject
|| normalizeMatchText(els.subjectSearch.value || "") !== normalizeMatchText(state.subject.name || "")
) {
subjectResultsLocked = false;
}
els.subjectResults.innerHTML = "";
els.subjectResults.style.display = "";
els.subjectResults.classList.remove("results-attention");
syncSubjectResultsGuidance();
debouncedSubjectSearch();
});
els.subjectSearch.addEventListener("keyup", (event) => {
if (event.key === "Enter") return;
if (
!state.subject
|| normalizeMatchText(els.subjectSearch.value || "") !== normalizeMatchText(state.subject.name || "")
) {
subjectResultsLocked = false;
}
els.subjectResults.innerHTML = "";
els.subjectResults.style.display = "";
els.subjectResults.classList.remove("results-attention");
syncSubjectResultsGuidance();
debouncedSubjectSearch();
});
els.subjectSearch.addEventListener("change", () => {
if (
!state.subject
|| normalizeMatchText(els.subjectSearch.value || "") !== normalizeMatchText(state.subject.name || "")
) {
subjectResultsLocked = false;
}
els.subjectResults.innerHTML = "";
els.subjectResults.style.display = "";
els.subjectResults.classList.remove("results-attention");
syncSubjectResultsGuidance();
if (!els.subjectSearch.value.trim() || els.subjectSearch.value.trim().length < 2) return;
runSubjectSearch();
});
els.subjectSearchButton.addEventListener("click", runSubjectSearch);
els.subjectSearch.addEventListener("keydown", (event) => {
if (event.key === "Enter") {
event.preventDefault();
runSubjectSearch();
}
});
els.subjectSearch.addEventListener("focus", activateOriginalMode);
els.subjectSearchButton.addEventListener("click", activateOriginalMode);
els.subjectSearch.addEventListener("focus", () => syncGlowState(els.subjectSearch, true));
els.subjectSearch.addEventListener("blur", syncOpenStateGlow);
[els.subjectStateHint, els.subjectCityHint].forEach((input) => {
input?.addEventListener("input", () => {
els.subjectResults.innerHTML = "";
});
});
els.peerSearches.forEach((input) => {
input.addEventListener("input", () => {
const peerIndex = Number(input.dataset.peerIndex);
els.peerResults[peerIndex].innerHTML = "";
});
input.addEventListener("focus", activateOriginalMode);
input.addEventListener("keydown", (event) => {
if (event.key === "Enter") {
event.preventDefault();
const index = Number(input.dataset.peerIndex);
runPeerSearch(index);
}
});
input.addEventListener("focus", () => {
els.peerSearches.forEach((peerInput) => syncGlowState(peerInput, peerInput === input));
});
input.addEventListener("blur", syncOpenStateGlow);
});
els.peerStateHints.forEach((input) => {
input.addEventListener("input", () => {
const index = Number(input.dataset.peerIndex);
els.peerResults[index].innerHTML = "";
});
});
els.peerCityHints.forEach((input) => {
input.addEventListener("input", () => {
const index = Number(input.dataset.peerIndex);
els.peerResults[index].innerHTML = "";
});
});
els.peerSearchButtons.forEach((button) => {
button.addEventListener("click", () => {
const index = Number(button.dataset.peerIndex);
runPeerSearch(index);
});
});
Object.entries(els.colorInputs).forEach(([key, input]) => {
input.addEventListener("input", () => {
state.logo.autoExtract = false;
state.logo.colors[key] = input.value;
els.colorHexInputs[key].value = input.value.toUpperCase();
updateBrandPreview();
renderLogo();
});
});
Object.entries(els.colorHexInputs).forEach(([key, input]) => {
input.addEventListener("input", () => {
state.logo.autoExtract = false;
const normalized = normalizeHex(input.value, state.logo.colors[key]);
state.logo.colors[key] = normalized;
els.colorInputs[key].value = normalized;
input.value = normalized.toUpperCase();
updateBrandPreview();
renderLogo();
});
});
els.autoColorToggle.addEventListener("click", () => {
state.logo.autoExtract = !state.logo.autoExtract;
if (state.logo.autoExtract && state.logo.logo_data_url) {
extractLogoColor(state.logo.logo_data_url);
} else {
render();
}
});
els.resetBrandingButton.addEventListener("click", () => {
state.logo.autoExtract = true;
resetBrandingPalette();
if (state.logo.logo_data_url) {
extractLogoColor(state.logo.logo_data_url);
}
});
els.presetCards.forEach((button) => {
button.addEventListener("click", () => applyPreset(button.dataset.preset));
});
els.reportFormatModes.forEach((input) => {
input.addEventListener("change", () => {
if (input.checked && input.value === "standard") {
resetStandardQuarterSelections();
}
state.cuPeriodConfirmation = emptyCuPeriodConfirmation();
applyReportFormatMode();
render();
});
input.addEventListener("focus", () => syncGlowState(els.reportFormatChoice, true));
input.addEventListener("blur", syncOpenStateGlow);
});
els.quarterSelects.forEach((select) => {
select.addEventListener("change", () => {
state.cuPeriodConfirmation = emptyCuPeriodConfirmation();
render();
});
select.addEventListener("focus", () => syncGlowState(select, true));
select.addEventListener("blur", syncOpenStateGlow);
});
document.querySelectorAll("#firstName, #lastName, #email, #title, #phone, #organization, #notes").forEach((input) => {
input.addEventListener("input", () => {
input.classList.remove("input-missing");
render();
});
});
let accountVerificationTimer = null;
document.getElementById("email")?.addEventListener("input", () => {
if (clampStep(state.ui.routeStep) !== "4") return;
clearTimeout(accountVerificationTimer);
state.account.verified = false;
state.account.error = "";
state.account.message = state.checkout.requestedPlan === "pro"
? "Enter your billing email for Professional checkout."
: "Enter your email for report delivery.";
renderAccountVerification();
accountVerificationTimer = setTimeout(() => {
verifyStep4Account();
}, 450);
});
els.notesModal.toggleButton?.addEventListener("click", () => {
if (!els.notesModal.modal) return;
els.notesModal.modal.hidden = false;
els.notesModal.modal.setAttribute("aria-hidden", "false");
document.body.classList.add("quick-request-modal-open");
document.getElementById("notes")?.focus();
});
const closeNotesModal = () => {
if (!els.notesModal.modal) return;
els.notesModal.modal.hidden = true;
els.notesModal.modal.setAttribute("aria-hidden", "true");
document.body.classList.remove("quick-request-modal-open");
render();
};
els.notesModal.closeButton?.addEventListener("click", closeNotesModal);
els.notesModal.modal?.addEventListener("click", (event) => {
if (event.target?.dataset?.notesClose === "true") {
closeNotesModal();
}
});
els.disclaimerAck?.addEventListener("input", () => {
els.disclaimerAck.classList.remove("input-missing");
render();
});
els.accordionTriggers.forEach((trigger) => {
trigger.addEventListener("click", () => openAccordion(trigger.closest(".accordion-panel").id));
});
els.stageTabs.forEach((tab) => {
tab.addEventListener("click", () => {
const step = clampStep(tab.dataset.routeStep);
if (stepIndex(step) <= stepIndex(state.ui.routeStep)) {
goToRouteStep(step);
}
});
});
els.stepBackButton?.addEventListener("click", () => {
const index = Math.max(0, stepIndex(state.ui.routeStep) - 1);
goToRouteStep(REGISTRATION_STEPS[index]);
});
els.stepNextButton?.addEventListener("click", () => {
if (!stepNextEnabled(state.validation)) return;
const index = Math.min(REGISTRATION_STEPS.length - 1, stepIndex(state.ui.routeStep) + 1);
goToRouteStep(REGISTRATION_STEPS[index]);
});
els.submitButton.addEventListener("animationend", (event) => {
if (event.animationName === "submit-ready-flash") {
els.submitButton.classList.remove("ready-flash");
}
});
els.form.addEventListener("submit", submitForm);
els.freeUpgrade.upgradeButton?.addEventListener("click", startProfessionalCheckout);
els.freeUpgrade.freeButton?.addEventListener("click", continueAsFreeRequest);
els.workflowScroll?.addEventListener("scroll", syncWorkflowScrollHint, { passive: true });
window.addEventListener("resize", () => {
syncWorkflowScrollHint();
applyStep3bViewportScale();
syncHelperPanel();
});
els.workflowScrollHint?.addEventListener("click", () => {
if (!els.workflowScroll) return;
const jump = Math.max(180, Math.round(els.workflowScroll.clientHeight * 0.52));
els.workflowScroll.scrollBy({ top: jump, behavior: "smooth" });
});
els.brandingCloseContinueButton?.addEventListener("click", closeBrandingAndContinue);
els.bankPeriodRetryButton?.addEventListener("click", () => {
if (requiresCuPeriodDisclosure()) {
verifyFinalCuPeriod({ force: true });
} else {
verifyFinalBankPeriod({ force: true });
}
});
els.cancelRequestButton?.addEventListener("click", cancelRegistrationRequest);
els.submissionSuccess.closeButton?.addEventListener("click", closeAndExitAfterSubmission);
els.submissionSuccess.closeIcon?.addEventListener("click", closeAndExitAfterSubmission);
els.submissionSuccess.modal?.addEventListener("click", (event) => {
if (event.target?.classList?.contains("submission-success-modal-backdrop")) {
closeAndExitAfterSubmission();
}
});
document.addEventListener("keydown", (event) => {
if (event.key === "Escape" && state.ui.submissionSuccessOpen) {
closeAndExitAfterSubmission();
}
});
setupLogoUpload();
initializeLogoSearchReadiness();
}
window.registrationSetInstitutionType = (type) => setInstitutionType(type, { advance: true, userInitiated: true });
window.registrationSearchSubject = () => {
return runSubjectSearch();
};
window.registrationSearchPeer = (index) => {
const peerIndex = Number(index);
return runPeerSearch(peerIndex);
};
buildQuarterOptions();
applyReportFormatMode();
syncColorInputs();
state.ui.routeStep = detectRouteStep();
document.body.classList.remove("step-1", "step-2", "step-3", "step-3b", "step-4");
document.body.classList.add(`step-${state.ui.routeStep}`);
applyStep3bViewportScale();
if (state.ui.routeStep === "4") {
if (els.pageTitle) els.pageTitle.textContent = "Step 4 - Review & Submit";
if (els.pageSubtext) els.pageSubtext.textContent = "Confirm your information and submit your reporting package request.";
} else {
if (els.pageTitle) els.pageTitle.textContent = "Request Reporting Package";
if (els.pageSubtext) els.pageSubtext.textContent = "";
}
restoreSessionState();
// A stored confirmation is useful for continuity, but it must never unlock
// payment on a newly opened final-review page. Re-verify the live period and
// keep Submit disabled until the disclosure has been refreshed successfully.
if (state.ui.routeStep === "4" && requiresBankPeriodDisclosure()) {
state.bankPeriodConfirmation = emptyBankPeriodConfirmation();
}
if (state.ui.routeStep === "4" && requiresCuPeriodDisclosure()) {
state.cuPeriodConfirmation = emptyCuPeriodConfirmation();
}
applyReportFormatMode();
ensureQuickRequestRows();
if (QUICK_REQUEST_ENABLED) {
syncQuickRequestTypeUI();
}
bindEvents();
initializeGuidancePositioning();
setInstitutionType(state.institutionType || DEFAULT_INSTITUTION_TYPE, { advance: false });
if (state.ui.routeStep === "1") {
openAccordion("institutionTypePanel");
} else if (state.ui.routeStep === "2") {
openAccordion("additionalPanel");
} else if (state.ui.routeStep === "3") {
openAccordion("reportPanel");
} else if (state.ui.routeStep === "3b") {
openAccordion("logoPanel");
} else {
openAccordion("reviewPanel");
if (!state.workflow.guid) {
verifyStep4Account();
}
}
loadPaidWorkflow().finally(() => {
void handleDirectCheckoutReturn();
});
syncWorkflowScrollHint();