1
2
Web application designed to assist Ukrainian developers in
3
calculating and managing their taxes.
4
5
It provides a simple interface to input income data, either
6
manually or by uploading bank statements from monobank or
7
privatbank.
8
9
Built with modern web technologies like React and Vite, it aims
10
to make tax management straightforward for IT professionals in
11
Ukraine.
12
13
14
16
17
const incomes = lines.map((line) => {
18
// "03.01.2023 17:08:27" => 03.01.2023
19
const date = toDate(line.split(",")[0].replaceAll('"', "").split(" ")[0]);
20
21
// cut values around currency to avoid messing with characters in 'name'
22
// 3900.41,1000.0,EUR,39.0041
23
const matches = line.match(/[\d.]+,[\d.]+,[A-Z]{3},[\d.]+/g);
24
25
const [uah, amount, currency, rate] = (matches![0] || "").split(",");
26
const tax = taxFor(Number(uah));
27
return {
28
date,
29
uah: Number(uah),
30
amount: Number(amount),
31
currency,
32
rate: Number(rate),
33
tax,
34
};
35
});
36
return incomes;
37
};
38
39
40
41
42
43
const incomes = lines.map((line) => {
// "03.01.2023 17:08:27" => 03.01.2023
const date = toDate(line.split(",")[0].replaceAll('"', "").split(" ")[0]);
// cut values around currency to avoid messing with characters in 'name'
// 3900.41,1000.0,EUR,39.0041
const matches = line.match(/[\d.]+,[\d.]+,[A-Z]{3},[\d.]+/g);
const [uah, amount, currency, rate] = (matches![0] || "").split(",");
const tax = taxFor(Number(uah));
return {
date,
uah: Number(uah),
amount: Number(amount),
currency,
rate: Number(rate),
tax,
};
});
return incomes;
};
// [ { r030: 978, txt: "Євро", rate: 32.9039, cc: "EUR", exchangedate: "23.02.2022" } ];
export const getRate = (currency: string, date: Date): Promise<Rate> =>
fetch(
`https://bank.gov.ua/NBUStatService/v1/statdirectory/exchange?valcode=${currency}&date=${format(
date,
"yyyyMMdd",
)}&json`,
)
.then((it) => it.json())
.then((rates) => rates[0]);
const PERCENT_TAX = 5;
export const taxFor = (sum: number) => Number((PERCENT_TAX * sum) / 100);
export const sumOf = (incomes: Income[], mapper: (it: Income) => number) =>
Number(incomes.map(mapper).reduce((acc, it) => acc + it, 0));
export const prettyPrint = (num: number): string => {
const rounded = Number(num.toFixed(2));
const formattedNum = rounded.toLocaleString("en").replace(/,/g, " ");
const decimalPart = formattedNum.split(".")[1];
return decimalPart && decimalPart.length === 1
? formattedNum + "0"
: formattedNum;
};
// '10.04.2023' => Date
export const toDate = (date: string) => {
const [day, month, year] = date.split(".").map(Number);
return new Date(year, month - 1, day);
};
export const validate = (args: { date: Date; amount: number }) =>
z
.object({
date: z.date().refine((value) => value.getTime() < Date.now()),
amount: z.number().positive(),
})
.parseAsync(args)
.catch(
(error) =>
error.issues.map((it: { path: string[] }) => it.path[0]) as Promise<
string[]
>,
)
.then(() => []);
const Button = ({
klass,
children,
...rest
}: {
klass: string;
children: ReactNode;
} & ButtonHTMLAttributes<HTMLButtonElement>) => (
<button {...rest} className={`${klass} p-1 text-lg rounded-md`}>
{children}
</button>
);
const Input = ({
isError,
...rest
}: {
isError: boolean;
} & React.InputHTMLAttributes<HTMLInputElement>) => {
const color = isError ? "#FF3419" : "#888888";
return (
<input
{...rest}
className={`p-1 border-solid border-2 rounded-md border-[${color}]`}
/>
);
};