152 lines
3.2 KiB
TypeScript
152 lines
3.2 KiB
TypeScript
const API_BASE = import.meta.env.VITE_API_BASE_URL || ''
|
|
|
|
export interface ApiResponse<T> {
|
|
success?: boolean
|
|
data?: T
|
|
error?: string
|
|
}
|
|
|
|
export interface UserInfo {
|
|
id: string
|
|
username: string
|
|
email?: string
|
|
role: 'system_admin' | 'admin' | 'user'
|
|
status: string
|
|
created_at: string
|
|
}
|
|
|
|
export interface Role {
|
|
id: number
|
|
name: 'system_admin' | 'admin' | 'user'
|
|
description?: string
|
|
permissions: string[]
|
|
created_at: string
|
|
}
|
|
|
|
export interface AuthResponse {
|
|
token: string
|
|
user: UserInfo
|
|
}
|
|
|
|
export interface StockRow {
|
|
symbol: string
|
|
name: string
|
|
close: number
|
|
change_pct: number
|
|
amount?: number
|
|
}
|
|
|
|
export interface MarketOverview {
|
|
latest_trade_date: string
|
|
counts: {
|
|
up: number
|
|
down: number
|
|
flat: number
|
|
total: number
|
|
}
|
|
avg_change_pct: number
|
|
indices: StockRow[]
|
|
top_gainers: StockRow[]
|
|
top_losers: StockRow[]
|
|
turnover_leaders: StockRow[]
|
|
}
|
|
|
|
export interface SyncJob {
|
|
id: string
|
|
job_date: string
|
|
status: 'running' | 'success' | 'failed'
|
|
records_count: number
|
|
started_at: string
|
|
finished_at?: string
|
|
error_message?: string
|
|
trigger_by: string
|
|
}
|
|
|
|
export function getToken(): string | null {
|
|
try {
|
|
return localStorage.getItem('access_token')
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
export function setToken(token: string) {
|
|
localStorage.setItem('access_token', token)
|
|
}
|
|
|
|
export function removeToken() {
|
|
localStorage.removeItem('access_token')
|
|
}
|
|
|
|
async function request<T>(
|
|
path: string,
|
|
options: RequestInit = {},
|
|
): Promise<T> {
|
|
const url = `${API_BASE}${path}`
|
|
const token = getToken()
|
|
const headers: Record<string, string> = {
|
|
'Content-Type': 'application/json',
|
|
...(options.headers as Record<string, string>),
|
|
}
|
|
if (token) {
|
|
headers['Authorization'] = `Bearer ${token}`
|
|
}
|
|
|
|
const res = await fetch(url, {
|
|
...options,
|
|
headers,
|
|
})
|
|
|
|
let data: any
|
|
try {
|
|
data = await res.json()
|
|
} catch {
|
|
data = {}
|
|
}
|
|
|
|
if (!res.ok) {
|
|
const msg = data.error || data.message || `HTTP ${res.status}`
|
|
throw new Error(msg)
|
|
}
|
|
|
|
return data as T
|
|
}
|
|
|
|
export const api = {
|
|
login: (body: { username: string; password: string }) =>
|
|
request<AuthResponse>('/api/auth/login', {
|
|
method: 'POST',
|
|
body: JSON.stringify(body),
|
|
}),
|
|
|
|
me: () => request<UserInfo>('/api/auth/me'),
|
|
|
|
logout: () => request<{ message: string }>('/api/auth/logout', { method: 'POST' }),
|
|
|
|
listUsers: () => request<UserInfo[]>('/api/admin/users'),
|
|
|
|
createUser: (body: { username: string; email?: string; password: string; role?: string }) =>
|
|
request<UserInfo>('/api/admin/users', {
|
|
method: 'POST',
|
|
body: JSON.stringify(body),
|
|
}),
|
|
|
|
updateUser: (id: string, body: Partial<{ email: string; role: string; status: string }>) =>
|
|
request<UserInfo>(`/api/admin/users/${id}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(body),
|
|
}),
|
|
|
|
deleteUser: (id: string) =>
|
|
request<{ message: string }>(`/api/admin/users/${id}`, { method: 'DELETE' }),
|
|
|
|
listRoles: () => request<Role[]>('/api/admin/roles'),
|
|
|
|
stockOverview: () => request<MarketOverview>('/api/stocks/overview'),
|
|
|
|
stockSyncStatus: () => request<SyncJob | null>('/api/stocks/sync/status'),
|
|
|
|
triggerStockSync: () =>
|
|
request<{ job: SyncJob }>('/api/admin/stocks/sync', { method: 'POST' }),
|
|
}
|