Skip to content

Commit 10cf39e

Browse files
committed
Add XBRL frontend and backend scaffolding
1 parent 842e098 commit 10cf39e

29 files changed

+13498
-0
lines changed

frontend/package-lock.json

Lines changed: 5430 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import { apiClient } from '../client';
2+
3+
// XBRL Types
4+
export interface XbrlSummary {
5+
documentUri: string;
6+
format: 'XBRL' | 'INLINE_XBRL' | 'UNKNOWN';
7+
parseTime: string;
8+
entityIdentifier: string;
9+
totalFacts: number;
10+
totalContexts: number;
11+
totalUnits: number;
12+
factsByType: Record<string, number>;
13+
factsByNamespace: Record<string, number>;
14+
parseTimeMs: number;
15+
successRate: number;
16+
nestedFactsExtracted: number;
17+
warnings: number;
18+
errors: number;
19+
}
20+
21+
export interface SecFilingMetadata {
22+
entityName: string;
23+
cik: string;
24+
tradingSymbol: string;
25+
securityExchange: string;
26+
formType: string;
27+
isAmendment: boolean;
28+
documentPeriodEndDate: string;
29+
fiscalYear: number;
30+
fiscalPeriod: string;
31+
fiscalYearEndDate: string;
32+
sharesOutstanding: number;
33+
filingCategory: string;
34+
deiData: Record<string, string>;
35+
}
36+
37+
export interface FinancialStatement {
38+
type: 'BALANCE_SHEET' | 'INCOME_STATEMENT' | 'CASH_FLOW' | 'STOCKHOLDERS_EQUITY';
39+
title: string;
40+
lineItems: LineItem[];
41+
periods: ReportingPeriod[];
42+
}
43+
44+
export interface LineItem {
45+
concept: string;
46+
label: string;
47+
indentLevel: number;
48+
isTotal: boolean;
49+
isSubtotal: boolean;
50+
unit: string;
51+
isMonetary: boolean;
52+
valuesByPeriod: Record<string, number>;
53+
}
54+
55+
export interface ReportingPeriod {
56+
contextId: string;
57+
startDate: string;
58+
endDate: string;
59+
isInstant: boolean;
60+
durationDays: number;
61+
label: string;
62+
}
63+
64+
export interface FinancialStatements {
65+
entityName: string;
66+
cik: string;
67+
fiscalYearEnd: string;
68+
periods: ReportingPeriod[];
69+
balanceSheet: FinancialStatement;
70+
incomeStatement: FinancialStatement;
71+
cashFlowStatement: FinancialStatement;
72+
equityStatement: FinancialStatement;
73+
}
74+
75+
export interface KeyFinancials {
76+
[concept: string]: number;
77+
}
78+
79+
export interface StandardizedData {
80+
standardizedValues: Record<string, number>;
81+
unmappedConcepts: number;
82+
}
83+
84+
export interface CalculationValidation {
85+
totalChecks: number;
86+
validCalculations: number;
87+
errors: number;
88+
isValid: boolean;
89+
}
90+
91+
export interface ComprehensiveAnalysis {
92+
summary: XbrlSummary;
93+
secMetadata: SecFilingMetadata;
94+
keyFinancials: KeyFinancials;
95+
standardizedValues: Record<string, number>;
96+
unmappedConcepts: number;
97+
calculationValidation: CalculationValidation;
98+
}
99+
100+
export interface XbrlFact {
101+
concept: string;
102+
namespace: string;
103+
fullNamespace: string;
104+
value: number | string;
105+
rawValue: string;
106+
factType: string;
107+
isNil: boolean;
108+
contextRef: string;
109+
contextDescription: string;
110+
periodEnd: string;
111+
unitRef: string;
112+
unitDisplay: string;
113+
decimals: number;
114+
scale: number;
115+
}
116+
117+
// XBRL API
118+
export const xbrlApi = {
119+
// Parse XBRL from URL
120+
parseFromUrl: (url: string) =>
121+
apiClient.get<XbrlSummary>(`/xbrl/parse?url=${encodeURIComponent(url)}`),
122+
123+
// Parse XBRL package from URL
124+
parsePackageFromUrl: (url: string) =>
125+
apiClient.get<{
126+
packageUri: string;
127+
totalFiles: number;
128+
instanceFiles: string[];
129+
totalFacts: number;
130+
instances: number;
131+
errors: Record<string, string>;
132+
}>(`/xbrl/parse-package?url=${encodeURIComponent(url)}`),
133+
134+
// Get key financial metrics from URL
135+
getFinancialsFromUrl: (url: string) =>
136+
apiClient.get<KeyFinancials>(`/xbrl/financials?url=${encodeURIComponent(url)}`),
137+
138+
// Validate calculations from URL
139+
validateFromUrl: (url: string) =>
140+
apiClient.get<CalculationValidation>(`/xbrl/validate?url=${encodeURIComponent(url)}`),
141+
142+
// Get cache statistics
143+
getCacheStats: () =>
144+
apiClient.get<Record<string, number>>('/xbrl/cache/stats'),
145+
146+
// Clear caches
147+
clearCache: () =>
148+
apiClient.post<void>('/xbrl/cache/clear'),
149+
150+
// NEW: Parse and get comprehensive analysis
151+
getComprehensiveAnalysis: (url: string) =>
152+
apiClient.get<ComprehensiveAnalysis>(`/xbrl/analysis?url=${encodeURIComponent(url)}`),
153+
154+
// NEW: Get reconstructed financial statements
155+
getStatements: (url: string) =>
156+
apiClient.get<FinancialStatements>(`/xbrl/statements?url=${encodeURIComponent(url)}`),
157+
158+
// NEW: Get SEC metadata
159+
getSecMetadata: (url: string) =>
160+
apiClient.get<SecFilingMetadata>(`/xbrl/sec-metadata?url=${encodeURIComponent(url)}`),
161+
162+
// NEW: Export all facts
163+
exportFacts: (url: string) =>
164+
apiClient.get<XbrlFact[]>(`/xbrl/facts?url=${encodeURIComponent(url)}`),
165+
166+
// NEW: Search facts
167+
searchFacts: (url: string, query: string) =>
168+
apiClient.get<XbrlFact[]>(`/xbrl/facts/search?url=${encodeURIComponent(url)}&query=${encodeURIComponent(query)}`),
169+
};

frontend/src/app/api/index.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,16 @@ export { filingsApi } from './endpoints/filings';
99
export { downloadsApi } from './endpoints/downloads';
1010
export { settingsApi } from './endpoints/settings';
1111
export { exportApi } from './endpoints/export';
12+
export { xbrlApi } from './endpoints/xbrl';
13+
export type {
14+
XbrlSummary,
15+
SecFilingMetadata,
16+
FinancialStatement,
17+
FinancialStatements,
18+
LineItem,
19+
ReportingPeriod,
20+
KeyFinancials,
21+
ComprehensiveAnalysis,
22+
XbrlFact,
23+
CalculationValidation,
24+
} from './endpoints/xbrl';
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import React from 'react';
2+
import { FinancialStatement, LineItem } from '../../api';
3+
import { ChevronDown, ChevronRight } from 'lucide-react';
4+
5+
interface FinancialStatementViewProps {
6+
statement: FinancialStatement;
7+
className?: string;
8+
}
9+
10+
export function FinancialStatementView({ statement, className = '' }: FinancialStatementViewProps) {
11+
const [expandedSections, setExpandedSections] = React.useState<Set<string>>(new Set());
12+
13+
// Get unique period columns
14+
const periods = React.useMemo(() => {
15+
const periodSet = new Set<string>();
16+
statement.lineItems.forEach(item => {
17+
Object.keys(item.valuesByPeriod || {}).forEach(p => periodSet.add(p));
18+
});
19+
return Array.from(periodSet).sort().reverse(); // Most recent first
20+
}, [statement]);
21+
22+
const formatValue = (value: number | undefined, item: LineItem) => {
23+
if (value === undefined || value === null) return '-';
24+
25+
// Format as currency if monetary
26+
if (item.isMonetary) {
27+
const absValue = Math.abs(value);
28+
const formatted = absValue >= 1_000_000_000
29+
? `${(absValue / 1_000_000_000).toFixed(1)}B`
30+
: absValue >= 1_000_000
31+
? `${(absValue / 1_000_000).toFixed(1)}M`
32+
: absValue >= 1_000
33+
? `${(absValue / 1_000).toFixed(1)}K`
34+
: absValue.toFixed(0);
35+
return value < 0 ? `(${formatted})` : formatted;
36+
}
37+
38+
// Format as plain number
39+
return value.toLocaleString();
40+
};
41+
42+
const formatPeriodLabel = (period: string) => {
43+
try {
44+
const date = new Date(period);
45+
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
46+
} catch {
47+
return period;
48+
}
49+
};
50+
51+
const toggleSection = (concept: string) => {
52+
setExpandedSections(prev => {
53+
const next = new Set(prev);
54+
if (next.has(concept)) {
55+
next.delete(concept);
56+
} else {
57+
next.add(concept);
58+
}
59+
return next;
60+
});
61+
};
62+
63+
return (
64+
<div className={`bg-white rounded-lg shadow-sm overflow-hidden ${className}`}>
65+
<div className="px-6 py-4 border-b border-gray-200 bg-gray-50">
66+
<h3 className="text-lg font-semibold text-gray-900">{statement.title}</h3>
67+
</div>
68+
69+
<div className="overflow-x-auto">
70+
<table className="min-w-full divide-y divide-gray-200">
71+
<thead className="bg-gray-50">
72+
<tr>
73+
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
74+
Item
75+
</th>
76+
{periods.slice(0, 4).map(period => (
77+
<th key={period} className="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider whitespace-nowrap">
78+
{formatPeriodLabel(period)}
79+
</th>
80+
))}
81+
</tr>
82+
</thead>
83+
<tbody className="bg-white divide-y divide-gray-100">
84+
{statement.lineItems.map((item, index) => (
85+
<tr
86+
key={`${item.concept}-${index}`}
87+
className={`
88+
${item.isTotal ? 'bg-gray-100 font-semibold' : ''}
89+
${item.isSubtotal ? 'bg-gray-50 font-medium' : ''}
90+
hover:bg-blue-50 transition-colors
91+
`}
92+
>
93+
<td className="px-6 py-2 whitespace-nowrap">
94+
<div
95+
className="flex items-center gap-1"
96+
style={{ paddingLeft: `${item.indentLevel * 16}px` }}
97+
>
98+
{(item.isTotal || item.isSubtotal) && (
99+
<button
100+
onClick={() => toggleSection(item.concept)}
101+
className="p-0.5 hover:bg-gray-200 rounded"
102+
>
103+
{expandedSections.has(item.concept) ? (
104+
<ChevronDown className="w-3 h-3" />
105+
) : (
106+
<ChevronRight className="w-3 h-3" />
107+
)}
108+
</button>
109+
)}
110+
<span className={`text-sm ${item.isTotal ? 'text-gray-900' : 'text-gray-700'}`}>
111+
{item.label}
112+
</span>
113+
</div>
114+
</td>
115+
{periods.slice(0, 4).map(period => (
116+
<td
117+
key={period}
118+
className={`px-4 py-2 text-right text-sm whitespace-nowrap font-mono
119+
${item.isTotal ? 'border-t-2 border-gray-300' : ''}
120+
${item.isSubtotal ? 'border-t border-gray-200' : ''}
121+
`}
122+
>
123+
{formatValue(item.valuesByPeriod?.[period], item)}
124+
</td>
125+
))}
126+
</tr>
127+
))}
128+
</tbody>
129+
</table>
130+
</div>
131+
132+
{statement.lineItems.length === 0 && (
133+
<div className="px-6 py-8 text-center text-gray-500">
134+
No data available for this statement
135+
</div>
136+
)}
137+
</div>
138+
);
139+
}

0 commit comments

Comments
 (0)