mirror of
https://github.com/escalante29/WealthySmart.git
synced 2026-07-17 12:48:47 +02:00
Removes Dashboard / Transactions / Transfers pages, the section configuration UI, the legacy useSettings hook, and the standalone PrivacyContext/ThemeContext modules. Privacy/theme contexts now live under src/contexts/ and the API helper / push-notifications module move under src/lib/. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
471 lines
12 KiB
TypeScript
471 lines
12 KiB
TypeScript
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<string, string | number | boolean | undefined>;
|
|
}
|
|
|
|
async function request<T>(
|
|
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<string, string> = {};
|
|
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<T = unknown>(url: string, config?: RequestConfig) {
|
|
return request<T>("GET", url, undefined, config);
|
|
},
|
|
post<T = unknown>(url: string, body?: unknown, config?: RequestConfig) {
|
|
return request<T>("POST", url, body, config);
|
|
},
|
|
patch<T = unknown>(url: string, body?: unknown, config?: RequestConfig) {
|
|
return request<T>("PATCH", url, body, config);
|
|
},
|
|
put<T = unknown>(url: string, body?: unknown, config?: RequestConfig) {
|
|
return request<T>("PUT", url, body, config);
|
|
},
|
|
delete<T = unknown>(url: string, config?: RequestConfig) {
|
|
return request<T>("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<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;
|
|
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<SavingsAccrual[]>("/savings-accrual/");
|
|
export const createSavingsAccrual = (data: SavingsAccrualCreate) =>
|
|
api.post<SavingsAccrual>("/savings-accrual/", data);
|
|
export const updateSavingsAccrual = (id: number, data: SavingsAccrualUpdate) =>
|
|
api.patch<SavingsAccrual>(`/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<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,
|
|
});
|