const DEFAULT_INSTITUTION_TYPE = "bank";
const QUICK_REQUEST_ENABLED = false;
const REGISTRATION_SESSION_KEY = "registrationWorkflowStateV1";
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,
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,
},
quickRequest: {
open: false,
applied: false,
activeRow: 0,
rows: [],
},
logo: {
logo_name: "",
logo_data_url: "",
logo_source_url: "",
logo_search_url: "",
autoExtract: true,
colors: {
primaryBg: "#0b3d66",
cardBg: "#1f4f7a",
textColor: "#f8fafc",
accentColor: "#d4af37",
},
},
};
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"),
submitHelper: document.getElementById("submitHelper"),
formStatus: document.getElementById("formStatus"),
disclaimerAck: document.getElementById("disclaimerAck"),
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"),
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 pendingLogoSearchTimer = null;
let logoSearchRequestId = 0;
const DEFAULT_SUBMIT_BUTTON_TEXT = "Submit Request";
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 LOGO_SEARCH_STABILIZATION_MS = 220;
const REQUIRED_USER_FIELD_IDS = ["firstName", "lastName", "email", "phone", "organization"];
const SEARCH_DISPATCH_DEDUPE_WINDOW_MS = 350;
let lastSubjectSearchDispatch = { value: "", ts: 0 };
const lastPeerSearchDispatchByIndex = new Map();
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 } = {}) {
const targetWindow = logoSearchPopupWindow;
if (!targetWindow || targetWindow.closed) {
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 = null;
}
}
function isLogoDropZoneInteractive() {
return Boolean(state.subject && els.logoControls && !els.logoControls.hidden && els.logoPreview);
}
function waitForLogoDropZoneReady(callback) {
const requestId = logoSearchRequestId;
setupLogoDropZone();
if (state.subject) {
els.logoControls.hidden = false;
setBrandingWindowOpen(true);
syncBrandingWindowState();
}
requestAnimationFrame(() => {
if (requestId !== logoSearchRequestId) return;
requestAnimationFrame(() => {
if (requestId !== logoSearchRequestId) return;
// Force layout after the branding panel expands so drag targets are mounted
// before the external browser can take focus.
els.logoPreview?.getBoundingClientRect();
pendingLogoSearchTimer = window.setTimeout(() => {
if (requestId !== logoSearchRequestId) return;
callback();
}, LOGO_SEARCH_STABILIZATION_MS);
});
});
}
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('"', """);
}
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]);
return "1";
}
function persistSessionState() {
const snapshot = {
state: {
institutionType: state.institutionType,
subject: state.subject,
additional: state.additional,
logo: state.logo,
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 {
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);
if (parsed.state.logo) state.logo = parsed.state.logo;
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 targetPath = REGISTRATION_STEP_PATHS[nextStep] || REGISTRATION_STEP_PATHS[1];
if (window.location.pathname !== targetPath) {
window.location.href = targetPath;
}
}
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 institutionIdLabel(institution) {
const charter = institution?.cert_or_charter || "";
const rssd = institution?.rssd_id || "";
if (charter && rssd && charter !== rssd) {
return `NCUA Charter: ${charter} | RSSD: ${rssd}`;
}
if (charter) return `NCUA Charter: ${charter}`;
if (rssd) return `RSSD: ${rssd}`;
return "ID not available";
}
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"} |
${institutionIdLabel(institution)} |
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");
}
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.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 response.json();
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 = `${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);
const debouncedPeerSearch = debounce((peerIndex) => {
const value = (els.peerSearches[peerIndex]?.value || "").trim();
if (!value || value.length < 2) return;
const now = Date.now();
const last = lastPeerSearchDispatchByIndex.get(peerIndex);
if (
last
&& value === last.value
&& (now - last.ts) < SEARCH_DISPATCH_DEDUPE_WINDOW_MS
) {
return;
}
lastPeerSearchDispatchByIndex.set(peerIndex, { value, ts: now });
runPeerSearch(peerIndex);
}, 150);
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"} | ${institutionIdLabel(institution)}
`)
.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
));
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;
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()),
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.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.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.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
? "Submitting Request..."
: state.ui.submitSucceeded
? SUCCESS_SUBMIT_BUTTON_TEXT
: DEFAULT_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 = "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 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)} | ${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}`;
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))}
${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("
");
}
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();
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();
syncOpenStateGlow();
applyPendingFocus();
requestAnimationFrame(syncWorkflowScrollHint);
persistSessionState();
syncSelectionSummary();
}
function buildQuarterOptions() {
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);
els.quarterSelects.forEach((select, index) => {
select.innerHTML = options
.map((option) => ``)
.join("");
select.selectedIndex = Math.min(index, options.length - 1);
select.disabled = true;
});
}
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() {
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 isCustom = selectedReportFormatMode() === "custom";
els.quarterSelects.forEach((select) => {
select.disabled = !isCustom;
});
els.periodHint.textContent = isCustom
? "Custom mode: choose exactly 5 reporting quarters."
: "Standard mode automatically uses the last 5 completed quarters.";
}
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.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 {
institution_type: institutionType,
subject_institution: effectiveSubjectInstitution(),
additional_institutions: effectiveAdditionalInstitutions(),
reporting_period: selectedPeriod(),
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.validation = {
institutionTypeSelected: false,
subjectSelected: false,
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);
}
}
async function submitForm(event) {
event.preventDefault();
hideSubmissionSuccessModal();
const error = validateForm();
const submitPayload = payload();
if (!submitPayload.requester_email || !submitPayload.requester_email.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;
}
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.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;
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 setupLogoUpload() {
els.logoSearchButton.addEventListener("click", () => {
if (!state.subject) return;
const query = (els.logoSearchQuery.value || "").trim() || logoSearchInstitutionQuery();
logoSearchRequestId += 1;
state.logo.logo_search_url = `https://www.google.com/search?tbm=isch&q=${encodeURIComponent(query)}`;
setStatus("Preparing the logo drop zone before opening image search.", "");
render();
if (pendingLogoSearchTimer) {
window.clearTimeout(pendingLogoSearchTimer);
pendingLogoSearchTimer = null;
}
waitForLogoDropZoneReady(() => {
pendingLogoSearchTimer = null;
openLogoSearchWindow();
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;
closeLogoSearchWindow();
els.logoPreview.classList.add("has-logo");
els.logoPreview.innerHTML = `
${name}
`;
if (state.logo.autoExtract) {
extractLogoColor(src);
}
render();
}
function handleLogoFile(file) {
if (!file || !file.type.startsWith("image/")) {
setStatus("Drop or upload an image file for the logo.", "error");
return;
}
const reader = new FileReader();
reader.onload = () => setLogoPreview(reader.result, file.name);
reader.readAsDataURL(file);
}
function handleLogoUrl(url) {
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;
}
setLogoPreview(cleanUrl, "Dragged logo image", cleanUrl);
}
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();
closeLogoSearchWindow({ aggressive: true });
const file = event.dataTransfer.files[0];
if (file) {
handleLogoFile(file);
return;
}
const uri = event.dataTransfer.getData("text/uri-list");
const text = event.dataTransfer.getData("text/plain");
handleLogoUrl(uri || text);
});
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 = "";
debouncedPeerSearch(peerIndex);
});
input.addEventListener("keyup", (event) => {
if (event.key === "Enter") return;
const peerIndex = Number(input.dataset.peerIndex);
els.peerResults[peerIndex].innerHTML = "";
debouncedPeerSearch(peerIndex);
});
input.addEventListener("change", () => {
const peerIndex = Number(input.dataset.peerIndex);
const value = els.peerSearches[peerIndex]?.value || "";
els.peerResults[peerIndex].innerHTML = "";
if (!value.trim() || value.trim().length < 2) return;
runPeerSearch(peerIndex);
});
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", () => {
applyReportFormatMode();
render();
});
input.addEventListener("focus", () => syncGlowState(els.reportFormatChoice, true));
input.addEventListener("blur", syncOpenStateGlow);
});
els.quarterSelects.forEach((select) => {
select.addEventListener("change", 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();
});
});
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.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.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();
}
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();
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");
}
syncWorkflowScrollHint();