Fix filings ticker scope consistency

This commit is contained in:
2026-03-14 19:16:04 -04:00
parent ac3b036c93
commit 61b072d31f
3 changed files with 309 additions and 54 deletions

View File

@@ -1,12 +1,12 @@
'use client';
import { useQueryClient } from '@tanstack/react-query';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import Link from 'next/link';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { Suspense } from 'react';
import { format } from 'date-fns';
import { Bot, Download, ExternalLink, NotebookPen, Search, TimerReset } from 'lucide-react';
import { useSearchParams } from 'next/navigation';
import { useRouter, useSearchParams } from 'next/navigation';
import { AppShell } from '@/components/shell/app-shell';
import { Panel } from '@/components/ui/panel';
import { Button } from '@/components/ui/button';
@@ -28,6 +28,7 @@ const FINANCIAL_VALUE_SCALE_OPTIONS: Array<{ value: NumberScaleUnit; label: stri
{ value: 'millions', label: 'Millions (M)' },
{ value: 'billions', label: 'Billions (B)' }
];
const FILINGS_QUERY_LIMIT = 120;
export default function FilingsPage() {
return (
@@ -66,6 +67,15 @@ function parseTagsInput(input: string) {
return [...unique];
}
function normalizeTickerParam(value: string | null) {
if (typeof value !== 'string') {
return null;
}
const normalized = value.trim().toUpperCase();
return normalized.length > 0 ? normalized : null;
}
function asScaledFinancialSnapshot(
value: number | null | undefined,
scale: NumberScaleUnit
@@ -123,54 +133,39 @@ function FilingExternalLink({ href, label }: FilingExternalLinkProps) {
function FilingsPageContent() {
const { isPending, isAuthenticated } = useAuthGuard();
const searchParams = useSearchParams();
const router = useRouter();
const queryClient = useQueryClient();
const { prefetchReport } = useLinkPrefetch();
const activeTickerFilter = useMemo(() => normalizeTickerParam(searchParams.get('ticker')), [searchParams]);
const activeFilingsQueryKey = useMemo(
() => queryKeys.filings(activeTickerFilter, FILINGS_QUERY_LIMIT),
[activeTickerFilter]
);
const filingsQuery = useQuery({
...filingsQueryOptions({ ticker: activeTickerFilter ?? undefined, limit: FILINGS_QUERY_LIMIT }),
enabled: !isPending && isAuthenticated
});
const [filings, setFilings] = useState<Filing[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [syncTickerInput, setSyncTickerInput] = useState('');
const [syncTickerInput, setSyncTickerInput] = useState(() => activeTickerFilter ?? '');
const [syncCategoryInput, setSyncCategoryInput] = useState('');
const [syncTagsInput, setSyncTagsInput] = useState('');
const [filterTickerInput, setFilterTickerInput] = useState('');
const [searchTicker, setSearchTicker] = useState('');
const [filterTickerInput, setFilterTickerInput] = useState(() => activeTickerFilter ?? '');
const [financialValueScale, setFinancialValueScale] = useState<NumberScaleUnit>('millions');
const [actionNotice, setActionNotice] = useState<string | null>(null);
const [actionError, setActionError] = useState<string | null>(null);
useEffect(() => {
const ticker = searchParams.get('ticker');
if (ticker) {
const normalized = ticker.toUpperCase();
setSyncTickerInput(normalized);
setFilterTickerInput(normalized);
setSearchTicker(normalized);
}
}, [searchParams]);
setSyncTickerInput(activeTickerFilter ?? '');
setFilterTickerInput(activeTickerFilter ?? '');
}, [activeTickerFilter]);
const loadFilings = useCallback(async (ticker?: string) => {
const options = filingsQueryOptions({ ticker, limit: 120 });
if (!queryClient.getQueryData(options.queryKey)) {
setLoading(true);
}
setError(null);
try {
const response = await queryClient.fetchQuery(options);
setFilings(response.filings);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unable to fetch filings');
} finally {
setLoading(false);
}
}, [queryClient]);
useEffect(() => {
if (!isPending && isAuthenticated) {
void loadFilings(searchTicker || undefined);
}
}, [isPending, isAuthenticated, searchTicker, loadFilings]);
const filings = filingsQuery.data?.filings ?? [];
const loading = filingsQuery.isPending;
const filingsError = filingsQuery.error instanceof Error
? filingsQuery.error.message
: filingsQuery.error
? 'Unable to fetch filings'
: null;
const triggerSync = async () => {
if (!syncTickerInput.trim()) {
@@ -178,6 +173,7 @@ function FilingsPageContent() {
}
try {
setActionError(null);
await queueFilingSync({
ticker: syncTickerInput.trim().toUpperCase(),
limit: 20,
@@ -185,25 +181,26 @@ function FilingsPageContent() {
tags: parseTagsInput(syncTagsInput)
});
void queryClient.invalidateQueries({ queryKey: queryKeys.recentTasks(20) });
void queryClient.invalidateQueries({ queryKey: ['filings'] });
await loadFilings(searchTicker || undefined);
void queryClient.invalidateQueries({ queryKey: activeFilingsQueryKey });
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to queue filing sync');
setActionError(err instanceof Error ? err.message : 'Failed to queue filing sync');
}
};
const triggerAnalysis = async (accessionNumber: string) => {
try {
setActionError(null);
await queueFilingAnalysis(accessionNumber);
void queryClient.invalidateQueries({ queryKey: queryKeys.recentTasks(20) });
void queryClient.invalidateQueries({ queryKey: ['report'] });
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to queue filing analysis');
setActionError(err instanceof Error ? err.message : 'Failed to queue filing analysis');
}
};
const saveToLibrary = async (filing: Filing) => {
try {
setActionError(null);
await createResearchArtifact({
ticker: filing.ticker,
kind: 'filing',
@@ -234,10 +231,23 @@ function FilingsPageContent() {
void queryClient.invalidateQueries({ queryKey: queryKeys.watchlist() });
setActionNotice(`Saved ${filing.accession_number} to the ${filing.ticker} research library.`);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to save filing to library');
setActionError(err instanceof Error ? err.message : 'Failed to save filing to library');
}
};
const replaceTickerFilter = (ticker: string | null) => {
const nextParams = new URLSearchParams(searchParams.toString());
if (ticker) {
nextParams.set('ticker', ticker);
} else {
nextParams.delete('ticker');
}
const nextQuery = nextParams.toString();
router.replace(nextQuery ? `/filings?${nextQuery}` : '/filings', { scroll: false });
};
const groupedByTicker = useMemo(() => {
const counts = new Map<string, number>();
@@ -260,11 +270,11 @@ function FilingsPageContent() {
<AppShell
title="Filings"
subtitle="Sync SEC submissions, keep 10-K/10-Q financial snapshots, and analyze qualitative signals from other forms."
activeTicker={searchTicker || null}
activeTicker={activeTickerFilter}
actions={(
<>
<Link
href={`/search${searchTicker ? `?ticker=${encodeURIComponent(searchTicker)}` : ''}`}
href={`/search${activeTickerFilter ? `?ticker=${encodeURIComponent(activeTickerFilter)}` : ''}`}
className="inline-flex items-center justify-center gap-2 rounded-lg border border-[color:var(--line-weak)] px-3 py-2 text-sm text-[color:var(--accent)] transition hover:border-[color:var(--line-strong)] hover:text-[color:var(--accent-strong)]"
>
<Search className="size-4" />
@@ -274,8 +284,7 @@ function FilingsPageContent() {
variant="secondary"
className="w-full sm:w-auto"
onClick={() => {
void queryClient.invalidateQueries({ queryKey: queryKeys.filings(searchTicker || null, 120) });
void loadFilings(searchTicker || undefined);
void queryClient.invalidateQueries({ queryKey: activeFilingsQueryKey });
}}
>
<TimerReset className="size-4" />
@@ -323,7 +332,9 @@ function FilingsPageContent() {
className="flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center"
onSubmit={(event) => {
event.preventDefault();
setSearchTicker(filterTickerInput.trim().toUpperCase());
const nextTicker = normalizeTickerParam(filterTickerInput);
setFilterTickerInput(nextTicker ?? '');
replaceTickerFilter(nextTicker);
}}
>
<Input
@@ -342,7 +353,7 @@ function FilingsPageContent() {
className="w-full sm:w-auto"
onClick={() => {
setFilterTickerInput('');
setSearchTicker('');
replaceTickerFilter(null);
}}
>
Clear
@@ -353,7 +364,7 @@ function FilingsPageContent() {
<Panel
title="Filing Ledger"
subtitle={`${filings.length} records loaded${searchTicker ? ` for ${searchTicker}` : ''}. Values shown in ${selectedFinancialScaleLabel}.`}
subtitle={`${filings.length} records loaded${activeTickerFilter ? ` for ${activeTickerFilter}` : ''}. Values shown in ${selectedFinancialScaleLabel}.`}
variant="surface"
actions={(
<div className="flex w-full flex-wrap justify-start gap-2 sm:justify-end">
@@ -371,7 +382,8 @@ function FilingsPageContent() {
</div>
)}
>
{error ? <p className="text-sm text-[#ffb5b5]">{error}</p> : null}
{actionError ? <p className="text-sm text-[#ffb5b5]">{actionError}</p> : null}
{filingsError ? <p className="text-sm text-[#ffb5b5]">{filingsError}</p> : null}
{actionNotice ? <p className="mt-2 text-sm text-[color:var(--accent)]">{actionNotice}</p> : null}
{loading ? (
<p className="text-sm text-[color:var(--terminal-muted)]">Fetching filings...</p>