mirror of
https://github.com/Zproger/Aurum.git
synced 2026-10-16 20:46:39 +07:00
Initial public release
Aurum — a self-hosted personal finance operating system: cash flow, net worth tracking, budgets, goals, recurring payments, reports, proactive alerts, and a compound-interest returns calculator, all bilingual (RU/EN) and mobile-first. Ships as a three-container Docker Compose stack (Postgres + FastAPI + nginx-served React frontend). See README.md for setup and CONTRIBUTING.md for development.
This commit is contained in:
18
frontend/src/api/accounts.ts
Normal file
18
frontend/src/api/accounts.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { api } from "@/api/client";
|
||||
import type { AccountInput, AccountWithBalance } from "@/types";
|
||||
|
||||
export function fetchAccounts(includeArchived = false) {
|
||||
return api.get<AccountWithBalance[]>(`/accounts?include_archived=${includeArchived}`);
|
||||
}
|
||||
|
||||
export function createAccount(input: AccountInput) {
|
||||
return api.post<AccountWithBalance>("/accounts", input);
|
||||
}
|
||||
|
||||
export function updateAccount(id: number, input: Partial<AccountInput> & { is_archived?: boolean }) {
|
||||
return api.patch<AccountWithBalance>(`/accounts/${id}`, input);
|
||||
}
|
||||
|
||||
export function deleteAccount(id: number) {
|
||||
return api.delete<void>(`/accounts/${id}`);
|
||||
}
|
||||
6
frontend/src/api/advice.ts
Normal file
6
frontend/src/api/advice.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { api } from "@/api/client";
|
||||
import type { AdviceResponse } from "@/types";
|
||||
|
||||
export function fetchAdvice() {
|
||||
return api.get<AdviceResponse>("/advice");
|
||||
}
|
||||
22
frontend/src/api/assets.ts
Normal file
22
frontend/src/api/assets.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { api } from "@/api/client";
|
||||
import type { Asset, AssetInput, AssetUpdateInput, AssetValuationInput } from "@/types";
|
||||
|
||||
export function fetchAssets() {
|
||||
return api.get<Asset[]>("/assets");
|
||||
}
|
||||
|
||||
export function createAsset(input: AssetInput) {
|
||||
return api.post<Asset>("/assets", input);
|
||||
}
|
||||
|
||||
export function updateAsset(id: number, input: AssetUpdateInput) {
|
||||
return api.patch<Asset>(`/assets/${id}`, input);
|
||||
}
|
||||
|
||||
export function addAssetValuation(id: number, input: AssetValuationInput) {
|
||||
return api.post<Asset>(`/assets/${id}/valuations`, input);
|
||||
}
|
||||
|
||||
export function deleteAsset(id: number) {
|
||||
return api.delete<void>(`/assets/${id}`);
|
||||
}
|
||||
31
frontend/src/api/backup.ts
Normal file
31
frontend/src/api/backup.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { api } from "@/api/client";
|
||||
import { t } from "@/lib/i18n";
|
||||
|
||||
export async function exportBackup(): Promise<void> {
|
||||
const response = await fetch("/api/backup/export");
|
||||
if (!response.ok) {
|
||||
throw new Error(await response.text());
|
||||
}
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const date = new Date().toISOString().slice(0, 10);
|
||||
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = `aurum-backup-${date}.json`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
export async function importBackup(file: File): Promise<void> {
|
||||
const text = await file.text();
|
||||
let payload: unknown;
|
||||
try {
|
||||
payload = JSON.parse(text);
|
||||
} catch {
|
||||
throw new Error(t("backup.invalidFile"));
|
||||
}
|
||||
await api.post<{ status: string }>("/backup/import", payload);
|
||||
}
|
||||
22
frontend/src/api/budgets.ts
Normal file
22
frontend/src/api/budgets.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { api } from "@/api/client";
|
||||
import type { Budget, BudgetInput, BudgetStatusResponse } from "@/types";
|
||||
|
||||
export function fetchBudgets() {
|
||||
return api.get<Budget[]>("/budgets");
|
||||
}
|
||||
|
||||
export function fetchBudgetStatus(year: number, month: number) {
|
||||
return api.get<BudgetStatusResponse>(`/budgets/status?year=${year}&month=${month}`);
|
||||
}
|
||||
|
||||
export function createBudget(input: BudgetInput) {
|
||||
return api.post<Budget>("/budgets", input);
|
||||
}
|
||||
|
||||
export function updateBudget(id: number, monthlyLimit: string) {
|
||||
return api.patch<Budget>(`/budgets/${id}`, { monthly_limit: monthlyLimit });
|
||||
}
|
||||
|
||||
export function deleteBudget(id: number) {
|
||||
return api.delete<void>(`/budgets/${id}`);
|
||||
}
|
||||
10
frontend/src/api/cashFlow.ts
Normal file
10
frontend/src/api/cashFlow.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { api } from "@/api/client";
|
||||
import type { CashFlowResponse } from "@/types";
|
||||
|
||||
export function fetchCashFlow(startDate?: string, endDate?: string) {
|
||||
const params = new URLSearchParams();
|
||||
if (startDate) params.set("start_date", startDate);
|
||||
if (endDate) params.set("end_date", endDate);
|
||||
const query = params.toString();
|
||||
return api.get<CashFlowResponse>(`/cash-flow${query ? `?${query}` : ""}`);
|
||||
}
|
||||
6
frontend/src/api/categories.ts
Normal file
6
frontend/src/api/categories.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { api } from "@/api/client";
|
||||
import type { Category } from "@/types";
|
||||
|
||||
export function fetchCategories() {
|
||||
return api.get<Category[]>("/categories");
|
||||
}
|
||||
43
frontend/src/api/client.ts
Normal file
43
frontend/src/api/client.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
const API_BASE = "/api";
|
||||
|
||||
export class ApiError extends Error {
|
||||
status: number;
|
||||
constructor(status: number, message: string) {
|
||||
super(message);
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
|
||||
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
const response = await fetch(`${API_BASE}${path}`, {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
...init,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const body = await response.text();
|
||||
let message = body || response.statusText;
|
||||
try {
|
||||
const parsed = JSON.parse(body) as { detail?: unknown };
|
||||
if (typeof parsed.detail === "string") message = parsed.detail;
|
||||
} catch {
|
||||
// body wasn't JSON — fall back to the raw text set above
|
||||
}
|
||||
throw new ApiError(response.status, message);
|
||||
}
|
||||
|
||||
if (response.status === 204) {
|
||||
return undefined as T;
|
||||
}
|
||||
|
||||
return (await response.json()) as T;
|
||||
}
|
||||
|
||||
export const api = {
|
||||
get: <T>(path: string) => request<T>(path),
|
||||
post: <T>(path: string, body: unknown) =>
|
||||
request<T>(path, { method: "POST", body: JSON.stringify(body) }),
|
||||
patch: <T>(path: string, body: unknown) =>
|
||||
request<T>(path, { method: "PATCH", body: JSON.stringify(body) }),
|
||||
delete: <T>(path: string) => request<T>(path, { method: "DELETE" }),
|
||||
};
|
||||
6
frontend/src/api/dashboard.ts
Normal file
6
frontend/src/api/dashboard.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { api } from "@/api/client";
|
||||
import type { DashboardSummary } from "@/types";
|
||||
|
||||
export function fetchDashboardSummary(year: number, month: number) {
|
||||
return api.get<DashboardSummary>(`/dashboard/summary?year=${year}&month=${month}`);
|
||||
}
|
||||
22
frontend/src/api/goals.ts
Normal file
22
frontend/src/api/goals.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { api } from "@/api/client";
|
||||
import type { Goal, GoalContributionInput, GoalInput } from "@/types";
|
||||
|
||||
export function fetchGoals() {
|
||||
return api.get<Goal[]>("/goals");
|
||||
}
|
||||
|
||||
export function createGoal(input: GoalInput) {
|
||||
return api.post<Goal>("/goals", input);
|
||||
}
|
||||
|
||||
export function updateGoal(id: number, input: Partial<GoalInput>) {
|
||||
return api.patch<Goal>(`/goals/${id}`, input);
|
||||
}
|
||||
|
||||
export function deleteGoal(id: number) {
|
||||
return api.delete<void>(`/goals/${id}`);
|
||||
}
|
||||
|
||||
export function addGoalContribution(id: number, input: GoalContributionInput) {
|
||||
return api.post<Goal>(`/goals/${id}/contributions`, input);
|
||||
}
|
||||
6
frontend/src/api/insights.ts
Normal file
6
frontend/src/api/insights.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { api } from "@/api/client";
|
||||
import type { FinancialAlert } from "@/types";
|
||||
|
||||
export function fetchFinancialAlerts() {
|
||||
return api.get<{ alerts: FinancialAlert[] }>("/insights/alerts");
|
||||
}
|
||||
6
frontend/src/api/netWorth.ts
Normal file
6
frontend/src/api/netWorth.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { api } from "@/api/client";
|
||||
import type { NetWorthRange, NetWorthSummary } from "@/types";
|
||||
|
||||
export function fetchNetWorthSummary(range: NetWorthRange) {
|
||||
return api.get<NetWorthSummary>(`/net-worth/summary?range=${range}`);
|
||||
}
|
||||
22
frontend/src/api/recurring.ts
Normal file
22
frontend/src/api/recurring.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { api } from "@/api/client";
|
||||
import type { RecurringTransaction, RecurringTransactionInput } from "@/types";
|
||||
|
||||
export function fetchRecurring() {
|
||||
return api.get<RecurringTransaction[]>("/recurring");
|
||||
}
|
||||
|
||||
export function createRecurring(input: RecurringTransactionInput) {
|
||||
return api.post<RecurringTransaction>("/recurring", input);
|
||||
}
|
||||
|
||||
export function updateRecurring(id: number, input: Partial<RecurringTransactionInput>) {
|
||||
return api.patch<RecurringTransaction>(`/recurring/${id}`, input);
|
||||
}
|
||||
|
||||
export function deleteRecurring(id: number) {
|
||||
return api.delete<void>(`/recurring/${id}`);
|
||||
}
|
||||
|
||||
export function postRecurring(id: number) {
|
||||
return api.post<RecurringTransaction>(`/recurring/${id}/post`, {});
|
||||
}
|
||||
16
frontend/src/api/reports.ts
Normal file
16
frontend/src/api/reports.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { api } from "@/api/client";
|
||||
import type { CategoryKind, CategoryRankingReport, CategorySpendingReport } from "@/types";
|
||||
|
||||
export function fetchCategorySpendingReport(categoryId: number, startDate?: string, endDate?: string) {
|
||||
const params = new URLSearchParams({ category_id: String(categoryId) });
|
||||
if (startDate) params.set("start_date", startDate);
|
||||
if (endDate) params.set("end_date", endDate);
|
||||
return api.get<CategorySpendingReport>(`/reports/category-spending?${params.toString()}`);
|
||||
}
|
||||
|
||||
export function fetchCategoryRanking(kind: CategoryKind, startDate?: string, endDate?: string) {
|
||||
const params = new URLSearchParams({ kind });
|
||||
if (startDate) params.set("start_date", startDate);
|
||||
if (endDate) params.set("end_date", endDate);
|
||||
return api.get<CategoryRankingReport>(`/reports/category-ranking?${params.toString()}`);
|
||||
}
|
||||
12
frontend/src/api/settings.ts
Normal file
12
frontend/src/api/settings.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { api } from "@/api/client";
|
||||
import type { AppSettings } from "@/types";
|
||||
|
||||
export function fetchSettings() {
|
||||
return api.get<AppSettings>("/settings");
|
||||
}
|
||||
|
||||
// Partial — the backend applies whatever fields are present (PATCH
|
||||
// semantics), so callers only send the field(s) they're changing.
|
||||
export function updateSettings(input: Partial<AppSettings>) {
|
||||
return api.patch<AppSettings>("/settings", input);
|
||||
}
|
||||
37
frontend/src/api/transactions.ts
Normal file
37
frontend/src/api/transactions.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { api } from "@/api/client";
|
||||
import type { Transaction, TransactionInput, TransactionPage } from "@/types";
|
||||
|
||||
export type TransactionSort = "date_desc" | "amount_desc" | "amount_asc";
|
||||
|
||||
export interface TransactionFilters {
|
||||
year?: number;
|
||||
month?: number;
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
account_id?: number;
|
||||
category_id?: number;
|
||||
type?: string;
|
||||
sort?: TransactionSort;
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
}
|
||||
|
||||
export function fetchTransactions(filters: TransactionFilters = {}) {
|
||||
const params = new URLSearchParams();
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null) params.set(key, String(value));
|
||||
});
|
||||
return api.get<TransactionPage>(`/transactions?${params.toString()}`);
|
||||
}
|
||||
|
||||
export function createTransaction(input: TransactionInput) {
|
||||
return api.post<Transaction>("/transactions", input);
|
||||
}
|
||||
|
||||
export function updateTransaction(id: number, input: Partial<TransactionInput>) {
|
||||
return api.patch<Transaction>(`/transactions/${id}`, input);
|
||||
}
|
||||
|
||||
export function deleteTransaction(id: number) {
|
||||
return api.delete<void>(`/transactions/${id}`);
|
||||
}
|
||||
Reference in New Issue
Block a user