const BASE_URL = import.meta.env.VITE_API_URL || '/api/v1'; class ApiError extends Error { response: { status: number; data: unknown }; constructor(status: number, data: unknown) { super(`Request failed with status ${status}`); this.response = { status, data }; } } interface RequestConfig { params?: Record; } async function request(method: string, url: string, body?: unknown, config?: RequestConfig): Promise<{ data: T }> { let fullUrl = `${BASE_URL}${url}`; if (config?.params) { const search = new URLSearchParams(); for (const [k, v] of Object.entries(config.params)) { if (v !== undefined) search.set(k, String(v)); } const qs = search.toString(); if (qs) fullUrl += `?${qs}`; } const headers: Record = {}; const token = localStorage.getItem('token'); if (token) headers['Authorization'] = `Bearer ${token}`; let fetchBody: BodyInit | undefined; if (body instanceof FormData || body instanceof URLSearchParams) { fetchBody = body; } else if (body !== undefined) { headers['Content-Type'] = 'application/json'; fetchBody = JSON.stringify(body); } const res = await fetch(fullUrl, { method, headers, body: fetchBody }); if (res.status === 401) { localStorage.removeItem('token'); window.location.href = '/login'; throw new ApiError(401, null); } if (!res.ok) { let data: unknown = null; try { data = await res.json(); } catch {} throw new ApiError(res.status, data); } if (res.status === 204) return { data: null as T }; const data = await res.json(); return { data }; } const api = { get(url: string, config?: RequestConfig) { return request('GET', url, undefined, config); }, post(url: string, body?: unknown, config?: RequestConfig) { return request('POST', url, body, config); }, patch(url: string, body?: unknown, config?: RequestConfig) { return request('PATCH', url, body, config); }, put(url: string, body?: unknown, config?: RequestConfig) { return request('PUT', url, body, config); }, delete(url: string, config?: RequestConfig) { return request('DELETE', url, undefined, config); }, }; export default api; export async function login(username: string, password: string) { const form = new URLSearchParams(); form.append('username', username); form.append('password', password); const { data } = await api.post('/auth/login', form); localStorage.setItem('token', data.access_token); return data; } export interface Account { id: number; bank: string; currency: string; label: string; balance: number; account_type: string; next_payment: number | null; updated_at: string; } export interface Category { id: number; name: string; icon: string; auto_match_patterns: string | null; } export interface ImportResult { imported: number; duplicates: number; errors: string[]; } // --- User Settings --- export interface SectionSettings { label: string; color: string; cardColor: string; visible: boolean; order: number; expanded: boolean; } export interface DashboardSettings { sections: Record; } export interface UserSettingsData { dashboard: DashboardSettings; } export interface UserSettingsResponse { key: string; data: UserSettingsData; updated_at: string; } export const getSettings = () => api.get('/settings/'); export const updateSettings = (data: UserSettingsData) => api.patch('/settings/', { data }); export interface Transaction { id: number; amount: number; currency: string; merchant: string; city: string | null; date: string; card_type: string | null; card_last4: string | null; authorization_code: string | null; reference: string | null; transaction_type: string; source: string; bank: string; notes: string | null; category_id: number | null; category: Category | null; deferred_to_next_cycle: boolean; created_at: string; } // --- Budget / Recurring Items --- export type RecurringItemType = 'INCOME' | 'EXPENSE'; export type RecurringFrequency = 'WEEKLY' | 'MONTHLY' | 'QUARTERLY' | 'BIANNUAL' | 'YEARLY'; export interface RecurringItem { id: number; name: string; amount: number; currency: string; item_type: RecurringItemType; frequency: RecurringFrequency; day_of_month: number | null; month_of_year: number | null; override_amounts: Record | null; category_id: number | null; is_active: boolean; notes: string | null; created_at: string; category: Category | null; } export interface RecurringItemCreate { name: string; amount: number; currency?: string; item_type: RecurringItemType; frequency?: RecurringFrequency; day_of_month?: number | null; month_of_year?: number | null; override_amounts?: Record | null; category_id?: number | null; is_active?: boolean; notes?: string | null; } export interface RecurringItemUpdate { name?: string; amount?: number; currency?: string; item_type?: RecurringItemType; frequency?: RecurringFrequency; day_of_month?: number | null; month_of_year?: number | null; override_amounts?: Record | null; category_id?: number | null; is_active?: boolean; notes?: string | null; } export interface RecurringItemDetail { id: number; name: string; amount: number; projected_amount: number | null; used_actual: boolean; item_type: string; frequency: string; category_name: string | null; category_id: number | null; } export interface ActualsBySource { source: string; total_compra: number; total_devolucion: number; net: number; count: number; } export interface MonthlyProjection { month: number; year: number; projected_income: number; projected_fixed_expenses: number; actual_credit_card: number; actual_cash: number; actual_transfers: number; uncovered_actual: number; gran_total_egresos: number; net_balance: number; carryover_balance: number; cumulative_balance: number; balance_overridden: boolean; } export interface YearlyProjection { year: number; months: MonthlyProjection[]; annual_income: number; annual_expenses: number; annual_net: number; } export interface MonthlyDetail { year: number; month: number; income_items: RecurringItemDetail[]; expense_items: RecurringItemDetail[]; actuals_by_source: ActualsBySource[]; total_projected_income: number; total_projected_expenses: number; uncovered_actual: number; gran_total_egresos: number; net_balance: number; cc_by_category: { category_name: string; amount: number }[]; } // --- Savings Accrual --- export interface SavingsAccrual { id: number; year: number; month: number; memp_amount: number; mpat_amount: number; trigger_transaction_id: number | null; applied_at: string; notes: string | null; } export interface SavingsAccrualCreate { year: number; month: number; memp_amount?: number; mpat_amount?: number; trigger_transaction_id?: number | null; notes?: string | null; } export interface SavingsAccrualUpdate { memp_amount?: number; mpat_amount?: number; notes?: string | null; } export const getSavingsAccruals = () => api.get('/savings-accrual/'); export const createSavingsAccrual = (data: SavingsAccrualCreate) => api.post('/savings-accrual/', data); export const updateSavingsAccrual = (id: number, data: SavingsAccrualUpdate) => api.patch(`/savings-accrual/${id}`, data); export const deleteSavingsAccrual = (id: number) => api.delete(`/savings-accrual/${id}`); // Budget API functions export const getRecurringItems = (params?: { item_type?: string; is_active?: boolean }) => api.get('/budget/recurring', { params }); export const createRecurringItem = (data: RecurringItemCreate) => api.post('/budget/recurring', data); export const updateRecurringItem = (id: number, data: RecurringItemUpdate) => api.patch(`/budget/recurring/${id}`, data); export const deleteRecurringItem = (id: number) => api.delete(`/budget/recurring/${id}`); export const getYearlyProjection = (year: number) => api.get(`/budget/projection/${year}`); export const getMonthlyDetail = (year: number, month: number) => api.get(`/budget/month/${year}/${month}`); export const upsertBalanceOverride = (year: number, month: number, override_balance: number) => api.put(`/budget/balance-override/${year}/${month}`, { override_balance }); export const deleteBalanceOverride = (year: number, month: number) => api.delete(`/budget/balance-override/${year}/${month}`); // --- Salarios --- export interface SalariosSummary { count: number; total_amount: number; latest_date: string | null; } export const getSalarios = (params?: { limit?: number; offset?: number }) => api.get('/salarios/', { params }); export const getSalariosSummary = () => api.get('/salarios/summary'); // --- Pensions --- export interface PensionSnapshot { id: number; fund: string; contract_number: string; period_start: string; period_end: string; saldo_anterior: number; aportes: number; rendimientos: number; retiros: number; traslados: number; comision: number; correccion: number; bonificacion: number; saldo_final: number; source_filename: string; created_at: string; } export interface PensionUploadResult { imported: number; updated: number; duplicates: number; errors: string[]; snapshots: PensionSnapshot[]; } export interface PensionManualEntry { fund: string; period_start: string; period_end: string; saldo_anterior: number; aportes: number; rendimientos: number; retiros: number; traslados: number; comision: number; correccion: number; bonificacion: number; saldo_final: number; } export const uploadPensionPDFs = (files: File[]) => { const form = new FormData(); files.forEach((f) => form.append('files', f)); return api.post('/pensions/upload', form); }; export const getPensionSnapshots = () => api.get('/pensions/snapshots'); export const getPensionFundSummary = () => api.get('/pensions/fund-summary'); export const submitPensionManualEntries = (entries: PensionManualEntry[]) => api.post('/pensions/manual', { entries }); // --- Municipal Receipts --- export interface MunicipalCharge { detail: string; amount: number; } export interface WaterMeterReading { id: number; meter_id: string; period: string; reading_previous: number; reading_current: number; consumption_m3: number; agua_potable: number; serv_ambientales: number; alcant_sanitario: number; iva: number; is_historical: boolean; receipt_id: number | null; created_at: string; } export interface MunicipalReceipt { id: number; receipt_date: string; due_date: string; period: string; account: string; finca: string; holder_name: string; holder_cedula: string; holder_address: string; subtotal: number; interests: number; iva: number; total: number; raw_charges: MunicipalCharge[]; source_filename: string; created_at: string; } export interface MunicipalReceiptDetail extends MunicipalReceipt { water_readings: WaterMeterReading[]; } export interface MunicipalReceiptUploadResult { imported: number; updated: number; errors: string[]; receipt: MunicipalReceipt | null; } export const uploadMunicipalReceipt = (file: File) => { const form = new FormData(); form.append('file', file); return api.post('/municipal-receipts/upload', form); }; export const getMunicipalReceipts = () => api.get('/municipal-receipts/'); export const getMunicipalReceiptDetail = (id: number) => api.get(`/municipal-receipts/${id}`); export const getWaterConsumption = (months?: number) => api.get('/municipal-receipts/water-consumption', { params: months ? { months } : undefined, });