mirror of
https://github.com/escalante29/WealthySmart.git
synced 2026-07-17 14:08:46 +02:00
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:
470
frontend/src/lib/api.ts
Normal file
470
frontend/src/lib/api.ts
Normal 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,
|
||||
});
|
||||
51
frontend/src/lib/push-notifications.ts
Normal file
51
frontend/src/lib/push-notifications.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user