feat: creats frontend for the project
This commit is contained in:
13
frontend/src/api/accounts.ts
Normal file
13
frontend/src/api/accounts.ts
Normal 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);
|
||||
}
|
||||
38
frontend/src/api/analytics.ts
Normal file
38
frontend/src/api/analytics.ts
Normal 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
14
frontend/src/api/auth.ts
Normal 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');
|
||||
}
|
||||
13
frontend/src/api/categories.ts
Normal file
13
frontend/src/api/categories.ts
Normal 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}` : ''}`);
|
||||
}
|
||||
62
frontend/src/api/client.ts
Normal file
62
frontend/src/api/client.ts
Normal 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) }),
|
||||
};
|
||||
8
frontend/src/api/import.ts
Normal file
8
frontend/src/api/import.ts
Normal 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
40
frontend/src/api/rules.ts
Normal 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`);
|
||||
}
|
||||
27
frontend/src/api/transactions.ts
Normal file
27
frontend/src/api/transactions.ts
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user