Drop legacy pages, contexts, and dashboard widgets

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>
This commit is contained in:
Carlos Escalante
2026-04-29 22:01:26 -06:00
parent 9fe17c0607
commit c4768e6912
12 changed files with 155 additions and 976 deletions

470
frontend/src/lib/api.ts Normal file
View File

@@ -0,0 +1,470 @@
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,
});

View File

@@ -0,0 +1,51 @@
import api from './api';
function urlBase64ToUint8Array(base64String: string): Uint8Array {
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
export async function subscribeToPush(): Promise<void> {
if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
return;
}
const permission = await Notification.requestPermission();
if (permission !== 'granted') {
return;
}
try {
const { data } = await api.get<{ publicKey: string }>('/notifications/vapid-public-key');
const registration = await navigator.serviceWorker.ready;
const existing = await registration.pushManager.getSubscription();
if (existing) {
await sendSubscriptionToServer(existing);
return;
}
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(data.publicKey),
});
await sendSubscriptionToServer(subscription);
} catch (err) {
console.warn('Push subscription failed:', err);
}
}
async function sendSubscriptionToServer(subscription: PushSubscription): Promise<void> {
const json = subscription.toJSON();
await api.post('/notifications/subscribe', {
endpoint: json.endpoint,
keys: json.keys,
});
}