Files
WealthySmart/frontend/src/api.ts
Carlos Escalante 739a32efd4
All checks were successful
Deploy to VPS / deploy (push) Successful in 58s
Add municipal receipt module and convert navbar to sidebar
- New module: Municipalidad de Belén receipt extraction via pdftotext+regex
  - Backend: MunicipalReceipt + WaterMeterReading models, upload/list/detail/water-consumption endpoints
  - Auto-creates budget Transaction on upload (duplicate-safe via reference)
  - Frontend: ServiciosMunicipales page with summary cards, water consumption bar chart, receipt history, PDF upload
- Convert top navbar to left sidebar with section headers (General, Finanzas, Servicios)
  - Desktop: fixed 220px sidebar, mobile: sheet overlay
  - Grouped nav: Dashboard | Presupuesto, Salarios, Pensiones, Analytics | Municipalidad

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 16:11:51 -06:00

377 lines
9.3 KiB
TypeScript

import axios from 'axios';
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL || '/api/v1',
});
api.interceptors.request.use((config) => {
const token = localStorage.getItem('token');
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});
api.interceptors.response.use(
(res) => res,
(err) => {
if (err.response?.status === 401) {
localStorage.removeItem('token');
window.location.href = '/login';
}
return Promise.reject(err);
},
);
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<string, SectionSettings>;
}
export interface UserSettingsData {
dashboard: DashboardSettings;
}
export interface UserSettingsResponse {
key: string;
data: UserSettingsData;
updated_at: string;
}
export const getSettings = () => api.get<UserSettingsResponse>('/settings/');
export const updateSettings = (data: UserSettingsData) => api.patch<UserSettingsResponse>('/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;
created_at: string;
}
// --- Budget / Recurring Items ---
export type RecurringItemType = 'INCOME' | 'EXPENSE' | 'SAVINGS';
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<string, number> | 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<string, number> | 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<string, number> | 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;
projected_savings: 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_savings: number;
annual_net: number;
}
export interface MonthlyDetail {
year: number;
month: number;
income_items: RecurringItemDetail[];
expense_items: RecurringItemDetail[];
savings_items: RecurringItemDetail[];
actuals_by_source: ActualsBySource[];
total_projected_income: number;
total_projected_expenses: number;
total_projected_savings: number;
uncovered_actual: number;
gran_total_egresos: number;
net_balance: number;
}
// Budget API functions
export const getRecurringItems = (params?: { item_type?: string; is_active?: boolean }) =>
api.get<RecurringItem[]>('/budget/recurring', { params });
export const createRecurringItem = (data: RecurringItemCreate) =>
api.post<RecurringItem>('/budget/recurring', data);
export const updateRecurringItem = (id: number, data: RecurringItemUpdate) =>
api.patch<RecurringItem>(`/budget/recurring/${id}`, data);
export const deleteRecurringItem = (id: number) =>
api.delete(`/budget/recurring/${id}`);
export const getYearlyProjection = (year: number) =>
api.get<YearlyProjection>(`/budget/projection/${year}`);
export const getMonthlyDetail = (year: number, month: number) =>
api.get<MonthlyDetail>(`/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<Transaction[]>('/salarios/', { params });
export const getSalariosSummary = () =>
api.get<SalariosSummary>('/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<PensionUploadResult>('/pensions/upload', form);
};
export const getPensionSnapshots = () =>
api.get<PensionSnapshot[]>('/pensions/snapshots');
export const getPensionFundSummary = () =>
api.get<PensionSnapshot[]>('/pensions/fund-summary');
export const submitPensionManualEntries = (entries: PensionManualEntry[]) =>
api.post<PensionUploadResult>('/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<MunicipalReceiptUploadResult>('/municipal-receipts/upload', form);
};
export const getMunicipalReceipts = () =>
api.get<MunicipalReceipt[]>('/municipal-receipts/');
export const getMunicipalReceiptDetail = (id: number) =>
api.get<MunicipalReceiptDetail>(`/municipal-receipts/${id}`);
export const getWaterConsumption = (months?: number) =>
api.get<WaterMeterReading[]>('/municipal-receipts/water-consumption', {
params: months ? { months } : undefined,
});