342 lines
8.0 KiB
TypeScript
342 lines
8.0 KiB
TypeScript
export type Quote = {
|
|
id: number;
|
|
timeStamp: string;
|
|
buy: string;
|
|
sell: string;
|
|
type: "BELO" | "BLUE" | "BNA";
|
|
difference: string;
|
|
percentage: string;
|
|
previousDayDifference: string;
|
|
previousDayPercentage: string;
|
|
};
|
|
|
|
export type FetchResult = {
|
|
type: string;
|
|
status: string;
|
|
error?: string;
|
|
};
|
|
|
|
export type HistoryQuote = {
|
|
id: number;
|
|
timeStamp: string;
|
|
buy: string;
|
|
sell: string;
|
|
type: "BELO" | "BLUE" | "BNA";
|
|
};
|
|
|
|
export type DailyMinMax = {
|
|
date: string;
|
|
minBuy: number;
|
|
maxBuy: number;
|
|
minSell: number;
|
|
maxSell: number;
|
|
};
|
|
|
|
export type DailyQuote = {
|
|
date: string;
|
|
buy: number;
|
|
sell: number;
|
|
};
|
|
|
|
export type HistoricalMinMax = {
|
|
minBuy: number;
|
|
maxBuy: number;
|
|
minBuyDate: string | null;
|
|
maxBuyDate: string | null;
|
|
};
|
|
|
|
async function fetcher<T>(url: string): Promise<T> {
|
|
const res = await fetch(url, { credentials: "include" });
|
|
if (!res.ok) {
|
|
throw new Error(`API error: ${res.status} ${res.statusText}`);
|
|
}
|
|
return res.json();
|
|
}
|
|
|
|
async function mutator<T>(
|
|
url: string,
|
|
method: string,
|
|
body?: unknown,
|
|
): Promise<T> {
|
|
const res = await fetch(url, {
|
|
method,
|
|
headers: { "Content-Type": "application/json" },
|
|
body: body ? JSON.stringify(body) : undefined,
|
|
credentials: "include",
|
|
});
|
|
if (!res.ok) {
|
|
throw new Error(`API error: ${res.status} ${res.statusText}`);
|
|
}
|
|
return res.json();
|
|
}
|
|
|
|
export function getQuotes(): Promise<Quote[]> {
|
|
return fetcher<Quote[]>("/api/quotes");
|
|
}
|
|
|
|
export function fetchQuotes(): Promise<FetchResult[]> {
|
|
return fetcher<FetchResult[]>("/api/quotes/fetch");
|
|
}
|
|
|
|
export function getQuoteHistory(
|
|
type: string,
|
|
startDate: string,
|
|
endDate: string,
|
|
): Promise<HistoryQuote[]> {
|
|
const params = new URLSearchParams({ startDate, endDate });
|
|
return fetcher<HistoryQuote[]>(`/api/quotes/${type}/history?${params}`);
|
|
}
|
|
|
|
export function getDailyMinMax(
|
|
type: string,
|
|
startDate: string,
|
|
endDate: string,
|
|
): Promise<DailyMinMax[]> {
|
|
const params = new URLSearchParams({ startDate, endDate });
|
|
return fetcher<DailyMinMax[]>(`/api/quotes/${type}/min-max?${params}`);
|
|
}
|
|
|
|
export function getDailyQuotes(
|
|
type: string,
|
|
startDate: string,
|
|
endDate: string,
|
|
): Promise<DailyQuote[]> {
|
|
const params = new URLSearchParams({ startDate, endDate });
|
|
return fetcher<DailyQuote[]>(`/api/quotes/${type}/daily?${params}`);
|
|
}
|
|
|
|
export function getHistoricalMinMax(type: string): Promise<HistoricalMinMax> {
|
|
return fetcher<HistoricalMinMax>(`/api/quotes/${type}/historical/min-max`);
|
|
}
|
|
|
|
// Expenses
|
|
|
|
export type PeriodicExpense = {
|
|
id: number;
|
|
description: string;
|
|
defaultDueDay: number;
|
|
defaultAmount: string;
|
|
periods: number[];
|
|
isDeleted: boolean;
|
|
createdAt: string;
|
|
currentMonthApplicable?: boolean;
|
|
};
|
|
|
|
export type Expense = {
|
|
id: number;
|
|
description: string;
|
|
periodicExpenseId: number | null;
|
|
year: number;
|
|
month: number;
|
|
amount: string;
|
|
amountPayed: string | null;
|
|
beloPrice: string | null;
|
|
usdcEquivalent: string | null;
|
|
status: "PENDING" | "PAYED";
|
|
dueDate: string;
|
|
paymentDate: string | null;
|
|
periodicExpense?: PeriodicExpense | null;
|
|
};
|
|
|
|
export type CreatePeriodicExpenseInput = {
|
|
description: string;
|
|
defaultDueDay: number;
|
|
defaultAmount: number;
|
|
periods: number[];
|
|
};
|
|
|
|
export type CreateNonPeriodicExpenseInput = {
|
|
description: string;
|
|
amount: number;
|
|
dueDate: string;
|
|
};
|
|
|
|
export type PayExpenseInput = {
|
|
amountPayed: number;
|
|
paymentDate?: string;
|
|
};
|
|
|
|
export type UpdateExpenseInput = {
|
|
amount: number;
|
|
dueDate: string;
|
|
status?: "PENDING";
|
|
amountPayed?: number;
|
|
paymentDate?: string;
|
|
};
|
|
|
|
export function getPeriodicExpenses(): Promise<PeriodicExpense[]> {
|
|
return fetcher<PeriodicExpense[]>("/api/periodic-expenses");
|
|
}
|
|
|
|
export function createPeriodicExpense(
|
|
data: CreatePeriodicExpenseInput,
|
|
): Promise<PeriodicExpense> {
|
|
return mutator<PeriodicExpense>("/api/periodic-expenses", "POST", data);
|
|
}
|
|
|
|
export function updatePeriodicExpense(
|
|
id: number,
|
|
data: CreatePeriodicExpenseInput,
|
|
): Promise<PeriodicExpense> {
|
|
return mutator<PeriodicExpense>(`/api/periodic-expenses/${id}`, "PUT", data);
|
|
}
|
|
|
|
export function deletePeriodicExpense(id: number): Promise<void> {
|
|
return mutator<void>(`/api/periodic-expenses/${id}`, "DELETE");
|
|
}
|
|
|
|
export function generateMonthlyExpense(id: number): Promise<Expense> {
|
|
return mutator<Expense>(
|
|
`/api/periodic-expenses/${id}/generate-month`,
|
|
"POST",
|
|
);
|
|
}
|
|
|
|
export type MonthlyExpensesTotal = { total: number };
|
|
|
|
export function getMonthlyPayedTotal(
|
|
year?: number,
|
|
month?: number,
|
|
): Promise<MonthlyExpensesTotal> {
|
|
const params = new URLSearchParams();
|
|
if (year) params.set("year", String(year));
|
|
if (month) params.set("month", String(month));
|
|
return fetcher<MonthlyExpensesTotal>(`/api/expenses/monthly-total?${params}`);
|
|
}
|
|
|
|
export type PaginatedResponse<T> = {
|
|
data: T[];
|
|
total: number;
|
|
page: number;
|
|
pageSize: number;
|
|
};
|
|
|
|
export type MonthlyTotals = {
|
|
payed: number;
|
|
pending: number;
|
|
};
|
|
|
|
export function getExpenses(
|
|
status?: string,
|
|
page?: number,
|
|
pageSize?: number,
|
|
periodicExpenseId?: number,
|
|
search?: string,
|
|
): Promise<PaginatedResponse<Expense>> {
|
|
const params = new URLSearchParams();
|
|
if (status) params.set("status", status);
|
|
if (page) params.set("page", String(page));
|
|
if (pageSize) params.set("pageSize", String(pageSize));
|
|
if (periodicExpenseId)
|
|
params.set("periodicExpenseId", String(periodicExpenseId));
|
|
if (search) params.set("search", search);
|
|
const qs = params.toString();
|
|
return fetcher<PaginatedResponse<Expense>>(
|
|
`/api/expenses${qs ? `?${qs}` : ""}`,
|
|
);
|
|
}
|
|
|
|
export type PendingUpcomingExpense = Expense & {
|
|
dueType: "overdue" | "upcoming";
|
|
};
|
|
|
|
export function getPendingUpcomingExpenses(): Promise<
|
|
PendingUpcomingExpense[]
|
|
> {
|
|
return fetcher<PendingUpcomingExpense[]>("/api/expenses/pending-upcoming");
|
|
}
|
|
|
|
export function getTotalPending(): Promise<MonthlyExpensesTotal> {
|
|
return fetcher<MonthlyExpensesTotal>("/api/expenses/pending-total");
|
|
}
|
|
|
|
export function getMonthlyTotals(
|
|
year?: number,
|
|
month?: number,
|
|
): Promise<MonthlyTotals> {
|
|
const params = new URLSearchParams();
|
|
if (year) params.set("year", String(year));
|
|
if (month) params.set("month", String(month));
|
|
return fetcher<MonthlyTotals>(`/api/expenses/totals?${params}`);
|
|
}
|
|
|
|
export function createNonPeriodicExpense(
|
|
data: CreateNonPeriodicExpenseInput,
|
|
): Promise<Expense> {
|
|
return mutator<Expense>("/api/expenses", "POST", data);
|
|
}
|
|
|
|
export function payExpense(
|
|
id: number,
|
|
data: PayExpenseInput,
|
|
): Promise<Expense> {
|
|
return mutator<Expense>(`/api/expenses/${id}/pay`, "PUT", data);
|
|
}
|
|
|
|
export function updateExpense(
|
|
id: number,
|
|
data: UpdateExpenseInput,
|
|
): Promise<Expense> {
|
|
return mutator<Expense>(`/api/expenses/${id}`, "PUT", data);
|
|
}
|
|
|
|
export type ImportResult = {
|
|
imported: number;
|
|
skipped: number;
|
|
skippedNoPeriods: number;
|
|
skippedDuplicate: number;
|
|
errors?: string[];
|
|
};
|
|
|
|
export type ImportExpensesResult = {
|
|
imported: number;
|
|
skipped: number;
|
|
errors?: string[];
|
|
};
|
|
|
|
export function importExpenses(file: File): Promise<ImportExpensesResult> {
|
|
const formData = new FormData();
|
|
formData.append("file", file);
|
|
return fetch("/api/expenses/import", {
|
|
method: "POST",
|
|
body: formData,
|
|
credentials: "include",
|
|
}).then((r) => {
|
|
if (!r.ok) throw new Error(`API error: ${r.status} ${r.statusText}`);
|
|
return r.json();
|
|
});
|
|
}
|
|
|
|
export function importPeriodicExpenses(file: File): Promise<ImportResult> {
|
|
const formData = new FormData();
|
|
formData.append("file", file);
|
|
return fetch("/api/periodic-expenses/import", {
|
|
method: "POST",
|
|
body: formData,
|
|
credentials: "include",
|
|
}).then((r) => {
|
|
if (!r.ok) throw new Error(`API error: ${r.status} ${r.statusText}`);
|
|
return r.json();
|
|
});
|
|
}
|
|
|
|
// Telegram
|
|
|
|
export type TelegramStatus = {
|
|
botUsername: string | null;
|
|
connected: boolean;
|
|
};
|
|
|
|
export type TelegramCode = {
|
|
code: string;
|
|
botUsername: string | null;
|
|
};
|
|
|
|
export function getTelegramStatus(): Promise<TelegramStatus> {
|
|
return fetcher<TelegramStatus>("/api/telegram/status");
|
|
}
|
|
|
|
export function generateTelegramCode(): Promise<TelegramCode> {
|
|
return mutator<TelegramCode>("/api/telegram/code", "POST");
|
|
}
|