feat: creats frontend for the project

This commit is contained in:
vakabunga
2026-03-02 00:33:09 +03:00
parent 4d67636633
commit cd56e2bf9d
37 changed files with 3762 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
import type { Account, UpdateAccountRequest } from '@family-budget/shared';
import { api } from './client';
export async function getAccounts(): Promise<Account[]> {
return api.get<Account[]>('/api/accounts');
}
export async function updateAccount(
id: number,
data: UpdateAccountRequest,
): Promise<Account> {
return api.put<Account>(`/api/accounts/${id}`, data);
}

View File

@@ -0,0 +1,38 @@
import type {
AnalyticsSummaryParams,
AnalyticsSummaryResponse,
ByCategoryParams,
ByCategoryItem,
TimeseriesParams,
TimeseriesItem,
} from '@family-budget/shared';
import { api } from './client';
function toQuery(params: object): string {
const sp = new URLSearchParams();
for (const [key, value] of Object.entries(params) as [string, unknown][]) {
if (value != null && value !== '') {
sp.set(key, String(value));
}
}
const qs = sp.toString();
return qs ? `?${qs}` : '';
}
export async function getSummary(
params: AnalyticsSummaryParams,
): Promise<AnalyticsSummaryResponse> {
return api.get(`/api/analytics/summary${toQuery(params)}`);
}
export async function getByCategory(
params: ByCategoryParams,
): Promise<ByCategoryItem[]> {
return api.get(`/api/analytics/by-category${toQuery(params)}`);
}
export async function getTimeseries(
params: TimeseriesParams,
): Promise<TimeseriesItem[]> {
return api.get(`/api/analytics/timeseries${toQuery(params)}`);
}

14
frontend/src/api/auth.ts Normal file
View File

@@ -0,0 +1,14 @@
import type { LoginRequest, MeResponse } from '@family-budget/shared';
import { api } from './client';
export async function login(data: LoginRequest): Promise<void> {
await api.post('/api/auth/login', data);
}
export async function logout(): Promise<void> {
await api.post('/api/auth/logout');
}
export async function getMe(): Promise<MeResponse> {
return api.get<MeResponse>('/api/auth/me');
}

View File

@@ -0,0 +1,13 @@
import type { Category, GetCategoriesParams } from '@family-budget/shared';
import { api } from './client';
export async function getCategories(
params?: GetCategoriesParams,
): Promise<Category[]> {
const sp = new URLSearchParams();
if (params?.isActive != null) {
sp.set('isActive', String(params.isActive));
}
const qs = sp.toString();
return api.get(`/api/categories${qs ? `?${qs}` : ''}`);
}

View File

@@ -0,0 +1,62 @@
import type { ApiError } from '@family-budget/shared';
let onUnauthorized: (() => void) | null = null;
export function setOnUnauthorized(cb: () => void) {
onUnauthorized = cb;
}
export class ApiException extends Error {
constructor(
public status: number,
public body: ApiError,
) {
super(body.message);
}
}
async function request<T>(url: string, options: RequestInit = {}): Promise<T> {
const res = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
...(options.headers as Record<string, string>),
},
credentials: 'include',
});
if (res.status === 401) {
onUnauthorized?.();
throw new ApiException(401, { error: 'UNAUTHORIZED', message: 'Сессия истекла' });
}
if (!res.ok) {
let body: ApiError;
try {
body = await res.json();
} catch {
body = { error: 'UNKNOWN', message: res.statusText };
}
throw new ApiException(res.status, body);
}
if (res.status === 204) return undefined as T;
return res.json();
}
export const api = {
get: <T>(url: string) => request<T>(url),
post: <T>(url: string, body?: unknown) =>
request<T>(url, {
method: 'POST',
body: body != null ? JSON.stringify(body) : undefined,
}),
put: <T>(url: string, body: unknown) =>
request<T>(url, { method: 'PUT', body: JSON.stringify(body) }),
patch: <T>(url: string, body: unknown) =>
request<T>(url, { method: 'PATCH', body: JSON.stringify(body) }),
};

View File

@@ -0,0 +1,8 @@
import type { ImportStatementResponse } from '@family-budget/shared';
import { api } from './client';
export async function importStatement(
file: unknown,
): Promise<ImportStatementResponse> {
return api.post<ImportStatementResponse>('/api/import/statement', file);
}

40
frontend/src/api/rules.ts Normal file
View File

@@ -0,0 +1,40 @@
import type {
CategoryRule,
GetCategoryRulesParams,
CreateCategoryRuleRequest,
UpdateCategoryRuleRequest,
ApplyRuleResponse,
} from '@family-budget/shared';
import { api } from './client';
export async function getCategoryRules(
params?: GetCategoryRulesParams,
): Promise<CategoryRule[]> {
const sp = new URLSearchParams();
if (params) {
for (const [key, value] of Object.entries(params)) {
if (value != null && value !== '') {
sp.set(key, String(value));
}
}
}
const qs = sp.toString();
return api.get(`/api/category-rules${qs ? `?${qs}` : ''}`);
}
export async function createCategoryRule(
data: CreateCategoryRuleRequest,
): Promise<CategoryRule> {
return api.post<CategoryRule>('/api/category-rules', data);
}
export async function updateCategoryRule(
id: number,
data: UpdateCategoryRuleRequest,
): Promise<CategoryRule> {
return api.patch<CategoryRule>(`/api/category-rules/${id}`, data);
}
export async function applyRule(id: number): Promise<ApplyRuleResponse> {
return api.post<ApplyRuleResponse>(`/api/category-rules/${id}/apply`);
}

View File

@@ -0,0 +1,27 @@
import type {
Transaction,
GetTransactionsParams,
PaginatedResponse,
UpdateTransactionRequest,
} from '@family-budget/shared';
import { api } from './client';
export async function getTransactions(
params: GetTransactionsParams,
): Promise<PaginatedResponse<Transaction>> {
const sp = new URLSearchParams();
for (const [key, value] of Object.entries(params)) {
if (value != null && value !== '') {
sp.set(key, String(value));
}
}
const qs = sp.toString();
return api.get(`/api/transactions${qs ? `?${qs}` : ''}`);
}
export async function updateTransaction(
id: number,
data: UpdateTransactionRequest,
): Promise<Transaction> {
return api.put<Transaction>(`/api/transactions/${id}`, data);
}