"use client";
import { useEffect, useState } from "react";
import {
BarChart,
Bar,
XAxis,
YAxis,
Tooltip,
ResponsiveContainer,
Legend,
} from "recharts";
import { api } from "@/lib/api";
import { buildKpis } from "@/lib/overview";
import { KPIStrip } from "@/components/prism/KPIStrip";
import { TickerHeader } from "@/components/prism/TickerHeader";
import type {
FilingsResponse,
FilingItem,
CadencePoint,
FormMixRow,
TickerOverview,
} from "@/types/api";
type FilingsState = "loading" | "ready" | "error" | "empty";
type FormFilter = "all" | "10-K" | "10-Q" | "8-K";
type DateRange = "6m" | "1y" | "all";
const PAGE_SIZE = 25;
type Props = {
ticker: string;
overview: TickerOverview;
isSaved: boolean;
onToggleWatchlist: () => void;
};
// ── Skeleton ──────────────────────────────────────────────────────────────────
function SkeletonBlock({ height, className = "" }: { height: number; className?: string }) {
return (
);
}
function FilingsSkeleton() {
return (
<>
{[0, 1, 2, 3, 4].map((i) => (
))}
{[0, 1, 2, 3, 4].map((i) => (
))}
>
);
}
// ── KPI strip ─────────────────────────────────────────────────────────────────
function FilingKPIs({ kpis }: { kpis: FilingsResponse["kpis"] }) {
const items = [
{ label: "TOTAL FILINGS", value: String(kpis.total) },
{ label: "10-K ANNUAL", value: String(kpis.count_10k) },
{ label: "10-Q QUARTERLY", value: String(kpis.count_10q) },
{ label: "8-K EVENTS", value: String(kpis.count_8k) },
{ label: "DISTINCT FORMS", value: String(kpis.distinct_forms) },
];
return (
{items.map((item) => (
{item.label}
{item.value}
))}
);
}
// ── Cadence chart (Recharts stacked bars) ─────────────────────────────────────
const FORM_COLORS: Record = {
"10-K": "var(--info, #4A78B5)",
"10-Q": "#5B9BD5",
"8-K": "var(--caution, #C49545)",
other: "var(--fg-3, #8A9AAF)",
};
function CadenceChart({ data }: { data: CadencePoint[] }) {
if (!data.length) {
return (
No filing history available
);
}
const chartData = data.map((d) => ({
month: d.month.slice(5), // "MM"
"10-K": d.count_10k,
"10-Q": d.count_10q,
"8-K": d.count_8k,
Other: d.count_other,
}));
return (
);
}
// ── Form mix table ─────────────────────────────────────────────────────────────
function FormMixTable({ rows }: { rows: FormMixRow[] }) {
if (!rows.length) return null;
return (
Form
Count
Mix
{rows.map((row) => (
{row.form}
{row.count}
{row.pct != null ? `${row.pct.toFixed(1)}%` : "—"}
))}
);
}
// ── Filing badge ───────────────────────────────────────────────────────────────
function FormBadge({ form }: { form: string }) {
let bg = "var(--ink-2, #181d26)";
let color = "var(--fg-3)";
let border = "1px solid var(--line-1, #232934)";
if (form === "10-K" || form === "10-Q") {
bg = "rgba(74, 120, 181, 0.15)";
color = "var(--info, #4A78B5)";
border = "1px solid rgba(74,120,181,0.3)";
} else if (form === "8-K") {
bg = "rgba(196, 149, 69, 0.15)";
color = "var(--caution, #C49545)";
border = "1px solid rgba(196,149,69,0.3)";
}
return (
{form}
);
}
// ── Filing table row ───────────────────────────────────────────────────────────
function FilingRow({ filing }: { filing: FilingItem }) {
return (
{filing.date}
{filing.title}
{filing.url ? (
↗
) : (
—
)}
);
}
// ── Filing table with filters + pagination ─────────────────────────────────────
// Cutoffs computed once at module load (not during render) to satisfy react-hooks/purity.
const _now = Date.now();
const CUTOFF_6M = new Date(_now - 180 * 86400 * 1000).toISOString().slice(0, 10);
const CUTOFF_1Y = new Date(_now - 365 * 86400 * 1000).toISOString().slice(0, 10);
const DATE_CUTOFF: Record = {
"6m": CUTOFF_6M,
"1y": CUTOFF_1Y,
"all": null,
};
function FilingTable({ filings }: { filings: FilingItem[] }) {
const [formFilter, setFormFilter] = useState("all");
const [dateRange, setDateRange] = useState("all");
const [page, setPage] = useState(0);
const FORM_FILTERS: { key: FormFilter; label: string }[] = [
{ key: "all", label: "ALL" },
{ key: "10-K", label: "10-K" },
{ key: "10-Q", label: "10-Q" },
{ key: "8-K", label: "8-K" },
];
const DATE_FILTERS: { key: DateRange; label: string }[] = [
{ key: "6m", label: "6M" },
{ key: "1y", label: "1Y" },
{ key: "all", label: "ALL" },
];
const filtered = filings.filter((f) => {
if (formFilter !== "all" && f.form !== formFilter) return false;
const cut = DATE_CUTOFF[dateRange];
if (cut && f.date < cut) return false;
return true;
});
const totalPages = Math.ceil(filtered.length / PAGE_SIZE);
const pageItems = filtered.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE);
// Reset to page 0 when filters change
const setFormFilterAndReset = (f: FormFilter) => { setFormFilter(f); setPage(0); };
const setDateRangeAndReset = (r: DateRange) => { setDateRange(r); setPage(0); };
return (
{/* Controls row */}
{FORM_FILTERS.map(({ key, label }) => (
))}
{DATE_FILTERS.map(({ key, label }) => (
))}
{/* Table header */}
{["DATE", "FORM", "TITLE", ""].map((h) => (
{h}
))}
{/* Rows */}
{pageItems.length === 0 ? (
No filings match selected filters
) : (
pageItems.map((f, i) =>
)
)}
{/* Pagination */}
{totalPages > 1 && (
{page * PAGE_SIZE + 1}–{Math.min((page + 1) * PAGE_SIZE, filtered.length)} of {filtered.length}
)}
);
}
// ── Page ──────────────────────────────────────────────────────────────────────
export function FilingsPage({ ticker, overview, isSaved, onToggleWatchlist }: Props) {
const [data, setData] = useState(null);
const [state, setState] = useState("loading");
const kpis = buildKpis(overview);
useEffect(() => {
let cancelled = false;
setState("loading");
setData(null);
api
.filings(ticker)
.then((res) => {
if (cancelled) return;
if (!res.filings.length) {
setState("empty");
} else {
setData(res);
setState("ready");
}
})
.catch(() => {
if (!cancelled) setState("error");
});
return () => {
cancelled = true;
};
}, [ticker]);
return (
<>
{state === "loading" &&
}
{state === "error" && (
Failed to load filings data
)}
{state === "empty" && (
No SEC filings found for this ticker
)}
{state === "ready" && data && (
<>
{/* Two-column: cadence chart + form mix */}
{/* Readout bar */}
{data.readout}
{/* Filing table */}
>
)}
>
);
}