const BASE_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 = {}; 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, credentials: "same-origin", }); if (res.status === 401) { await fetch("/api/auth/logout", { method: "POST" }).catch(() => {}); if (typeof window !== "undefined") window.location.replace("/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 res = await fetch("/api/auth/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username, password }), credentials: "same-origin", }); if (!res.ok) { let data: unknown = null; try { data = await res.json(); } catch {} throw new ApiError(res.status, data); } return res.json(); } export async function logout() { await fetch("/api/auth/logout", { method: "POST", credentials: "same-origin" }); } // ─── Types ────────────────────────────────────────────────────────────────── 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[]; } 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 --- 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, });