"use client"; import { useEffect, useMemo, useState } from "react"; import { AppShell } from "@/components/app-shell"; import { apiFetch } from "@/lib/api"; type CreditScoreEntry = { id: string; score: number; bureau: string; source: string; model: string; scoreDate: string; factors?: Record; change?: number | null; }; type CreditScoreSummary = { latest: CreditScoreEntry | null; previous: CreditScoreEntry | null; change: number | null; averageScore: number | null; entryCount: number; latestByBureau: CreditScoreEntry[]; trend: CreditScoreEntry[]; }; const bureaus = [ { value: "experian", label: "Experian" }, { value: "equifax", label: "Equifax" }, { value: "transunion", label: "TransUnion" }, { value: "unknown", label: "Unknown" }, ]; const today = new Date().toISOString().slice(0, 10); export default function CreditScorePage() { const [summary, setSummary] = useState(null); const [entries, setEntries] = useState([]); const [filter, setFilter] = useState("all"); const [status, setStatus] = useState(""); const [form, setForm] = useState({ score: "", bureau: "experian", source: "manual", model: "fico_8", scoreDate: today, positiveFactors: "", negativeFactors: "", }); const filteredEntries = useMemo(() => { if (filter === "all") return entries; return entries.filter((entry) => entry.bureau === filter); }, [entries, filter]); const scoreBand = (score?: number | null) => { if (!score) return "No score"; if (score >= 800) return "Exceptional"; if (score >= 740) return "Very good"; if (score >= 670) return "Good"; if (score >= 580) return "Fair"; return "Needs work"; }; const load = async () => { const query = filter === "all" ? "" : `?bureau=${filter}`; const [summaryRes, entriesRes] = await Promise.all([ apiFetch("/api/credit-score/summary"), apiFetch(`/api/credit-score/entries${query}`), ]); if (!summaryRes.error) setSummary(summaryRes.data ?? null); if (!entriesRes.error) setEntries(entriesRes.data ?? []); }; useEffect(() => { load().catch(() => setStatus("Unable to load credit score history.")); }, [filter]); const addEntry = async () => { const score = Number(form.score); if (!score || score < 300 || score > 850) { setStatus("Score must be between 300 and 850."); return; } const factors = { positive: form.positiveFactors.split(",").map((item) => item.trim()).filter(Boolean), negative: form.negativeFactors.split(",").map((item) => item.trim()).filter(Boolean), }; const res = await apiFetch("/api/credit-score/entries", { method: "POST", body: JSON.stringify({ score, bureau: form.bureau, source: form.source, model: form.model, scoreDate: form.scoreDate, factors, }), }); if (res.error) { setStatus(res.error.message ?? "Unable to add score entry."); return; } const change = res.data?.change; setStatus(change === null || change === undefined ? "Credit score entry added." : `Credit score entry added. Change: ${change > 0 ? "+" : ""}${change}.`); setForm((prev) => ({ ...prev, score: "", positiveFactors: "", negativeFactors: "" })); await load(); }; const pullScore = async () => { const bureau = ["experian", "equifax", "transunion"].includes(form.bureau) ? form.bureau : "experian"; const res = await apiFetch("/api/credit-score/pull", { method: "POST", body: JSON.stringify({ bureau, consent: { acceptedAt: new Date().toISOString(), purpose: "credit_score_monitoring", }, }), }); if (res.error) { setStatus(res.error.message ?? "Unable to pull credit score."); return; } setStatus(`${bureau} score pulled: ${res.data.score}.`); await load(); }; const inputCls = "mt-2 w-full rounded-xl border border-border bg-background/50 px-4 py-2 text-sm text-foreground focus:border-primary focus:ring-primary focus:outline-none"; const labelCls = "text-xs text-muted-foreground font-semibold uppercase tracking-wider"; return (

Latest score

{summary?.latest?.score ?? "--"}

{scoreBand(summary?.latest?.score)}

Last change

{summary?.change === null || summary?.change === undefined ? "--" : `${summary.change > 0 ? "+" : ""}${summary.change}`}

Compared with previous same-bureau entry

Average score

{summary?.averageScore ?? "--"}

Across saved entries

Entries

{summary?.entryCount ?? 0}

Manual or imported history

Add Score Entry

setForm((prev) => ({ ...prev, score: event.target.value }))} className={inputCls} />
setForm((prev) => ({ ...prev, scoreDate: event.target.value }))} className={inputCls} />
setForm((prev) => ({ ...prev, positiveFactors: event.target.value }))} className={inputCls} placeholder="low utilization, on-time payments" />
setForm((prev) => ({ ...prev, negativeFactors: event.target.value }))} className={inputCls} placeholder="hard inquiry, high balance" />
{status &&

{status}

}

Bureau Snapshot

{(summary?.latestByBureau ?? []).map((entry) => (

{entry.bureau}

{entry.score}

{new Date(entry.scoreDate).toLocaleDateString()}

))} {(summary?.latestByBureau?.length ?? 0) === 0 &&

No bureau scores yet.

}

Score History

{filteredEntries.length === 0 &&

No score entries for this view.

} {filteredEntries.map((entry) => { const factors = entry.factors ?? {}; const positive = Array.isArray(factors.positive) ? factors.positive : []; const negative = Array.isArray(factors.negative) ? factors.negative : []; return (

{entry.score}

{entry.bureau} {entry.model}

{new Date(entry.scoreDate).toLocaleDateString()} · {entry.source}

{(positive.length > 0 || negative.length > 0) && (

{positive.length > 0 ? `Positive: ${positive.join(", ")}` : ""} {positive.length > 0 && negative.length > 0 ? " · " : ""} {negative.length > 0 ? `Negative: ${negative.join(", ")}` : ""}

)}

{scoreBand(entry.score)}

); })}
); }