145 lines
3.7 KiB
TypeScript
145 lines
3.7 KiB
TypeScript
import type {
|
|
Filing,
|
|
Holding,
|
|
PortfolioInsight,
|
|
PortfolioSummary,
|
|
Task,
|
|
User,
|
|
WatchlistItem
|
|
} from './types';
|
|
import { resolveApiBaseURL } from './runtime-url';
|
|
|
|
const API_BASE = resolveApiBaseURL(process.env.NEXT_PUBLIC_API_URL);
|
|
|
|
export class ApiError extends Error {
|
|
status: number;
|
|
|
|
constructor(message: string, status: number) {
|
|
super(message);
|
|
this.name = 'ApiError';
|
|
this.status = status;
|
|
}
|
|
}
|
|
|
|
async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
|
const headers = new Headers(init?.headers);
|
|
if (!headers.has('Content-Type')) {
|
|
headers.set('Content-Type', 'application/json');
|
|
}
|
|
|
|
const response = await fetch(`${API_BASE}${path}`, {
|
|
...init,
|
|
credentials: 'include',
|
|
headers,
|
|
cache: 'no-store'
|
|
});
|
|
|
|
const body = await response.json().catch(() => ({}));
|
|
|
|
if (!response.ok) {
|
|
const message = typeof body?.error === 'string' ? body.error : `Request failed (${response.status})`;
|
|
throw new ApiError(message, response.status);
|
|
}
|
|
|
|
return body as T;
|
|
}
|
|
|
|
export async function getMe() {
|
|
return await apiFetch<{ user: User }>('/api/me');
|
|
}
|
|
|
|
export async function listWatchlist() {
|
|
return await apiFetch<{ items: WatchlistItem[] }>('/api/watchlist');
|
|
}
|
|
|
|
export async function upsertWatchlistItem(input: { ticker: string; companyName: string; sector?: string }) {
|
|
return await apiFetch<{ item: WatchlistItem }>('/api/watchlist', {
|
|
method: 'POST',
|
|
body: JSON.stringify(input)
|
|
});
|
|
}
|
|
|
|
export async function deleteWatchlistItem(id: number) {
|
|
return await apiFetch<{ success: boolean }>(`/api/watchlist/${id}`, {
|
|
method: 'DELETE'
|
|
});
|
|
}
|
|
|
|
export async function listHoldings() {
|
|
return await apiFetch<{ holdings: Holding[] }>('/api/portfolio/holdings');
|
|
}
|
|
|
|
export async function getPortfolioSummary() {
|
|
return await apiFetch<{ summary: PortfolioSummary }>('/api/portfolio/summary');
|
|
}
|
|
|
|
export async function upsertHolding(input: {
|
|
ticker: string;
|
|
shares: number;
|
|
avgCost: number;
|
|
currentPrice?: number;
|
|
}) {
|
|
return await apiFetch<{ holding: Holding }>('/api/portfolio/holdings', {
|
|
method: 'POST',
|
|
body: JSON.stringify(input)
|
|
});
|
|
}
|
|
|
|
export async function deleteHolding(id: number) {
|
|
return await apiFetch<{ success: boolean }>(`/api/portfolio/holdings/${id}`, {
|
|
method: 'DELETE'
|
|
});
|
|
}
|
|
|
|
export async function queuePriceRefresh() {
|
|
return await apiFetch<{ task: Task }>('/api/portfolio/refresh-prices', {
|
|
method: 'POST'
|
|
});
|
|
}
|
|
|
|
export async function queuePortfolioInsights() {
|
|
return await apiFetch<{ task: Task }>('/api/portfolio/insights/generate', {
|
|
method: 'POST'
|
|
});
|
|
}
|
|
|
|
export async function getLatestPortfolioInsight() {
|
|
return await apiFetch<{ insight: PortfolioInsight | null }>('/api/portfolio/insights/latest');
|
|
}
|
|
|
|
export async function listFilings(query?: { ticker?: string; limit?: number }) {
|
|
const params = new URLSearchParams();
|
|
|
|
if (query?.ticker) {
|
|
params.set('ticker', query.ticker);
|
|
}
|
|
|
|
if (query?.limit) {
|
|
params.set('limit', String(query.limit));
|
|
}
|
|
|
|
const suffix = params.size > 0 ? `?${params.toString()}` : '';
|
|
return await apiFetch<{ filings: Filing[] }>(`/api/filings${suffix}`);
|
|
}
|
|
|
|
export async function queueFilingSync(input: { ticker: string; limit?: number }) {
|
|
return await apiFetch<{ task: Task }>('/api/filings/sync', {
|
|
method: 'POST',
|
|
body: JSON.stringify(input)
|
|
});
|
|
}
|
|
|
|
export async function queueFilingAnalysis(accessionNumber: string) {
|
|
return await apiFetch<{ task: Task }>(`/api/filings/${accessionNumber}/analyze`, {
|
|
method: 'POST'
|
|
});
|
|
}
|
|
|
|
export async function getTask(taskId: string) {
|
|
return await apiFetch<{ task: Task }>(`/api/tasks/${taskId}`);
|
|
}
|
|
|
|
export async function listRecentTasks(limit = 20) {
|
|
return await apiFetch<{ tasks: Task[] }>(`/api/tasks?limit=${limit}`);
|
|
}
|