feat: založení základní stránky příjem/výdej + import a statistiky

This commit is contained in:
Ondřej Anděl
2026-09-07 14:58:49 +02:00
commit a5711f3e1e
98 changed files with 15505 additions and 0 deletions
+75
View File
@@ -0,0 +1,75 @@
import { Request } from 'express';
/** Regulární výraz pro datum ve formátu YYYY-MM-DD. */
const ISO_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
/** Chyba způsobená neplatným vstupem uživatele — mapuje se na HTTP 400. */
export class BadRequestError extends Error { }
/** Chyba hledaného, ale neexistujícího záznamu — mapuje se na HTTP 404. */
export class NotFoundError extends Error { }
/** Vrátí datum v ISO formátu (YYYY-MM-DD) v lokální časové zóně. */
export function formatDate(date: Date): string {
const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0');
const year = String(date.getFullYear());
return `${year}-${month}-${day}`;
}
/** Vrátí dnešní datum s vynulovaným časem. */
export function getToday(): Date {
const today = new Date();
today.setHours(0, 0, 0, 0);
return today;
}
/**
* Ověří, že řetězec je datum ve formátu YYYY-MM-DD a odpovídá reálnému dni.
* Zachytí i přetečení typu 2024-02-31, které by `new Date` tiše posunul na březen.
*/
export function isValidIsoDate(value: unknown): value is string {
if (typeof value !== 'string' || !ISO_DATE_PATTERN.test(value)) {
return false;
}
const [year, month, day] = value.split('-').map(Number);
const date = new Date(year, month - 1, day);
return date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day;
}
/**
* Vrátí datum z query parametru, nebo vyhodí {@link BadRequestError}.
*
* @param value hodnota query parametru
* @param name název parametru pro chybovou hlášku
*/
export function requireIsoDate(value: unknown, name: string): string {
if (!isValidIsoDate(value)) {
throw new BadRequestError(`Parametr '${name}' není platné datum ve formátu YYYY-MM-DD`);
}
return value;
}
/** Vrátí JWT token z hlavičky Authorization, pokud tam je. */
export function parseToken(req: Request): string | undefined {
return req.headers?.authorization?.split(' ')[1];
}
/**
* Vrátí všechna data v rozsahu (včetně obou krajů) jako ISO řetězce.
*
* @param from první den rozsahu (YYYY-MM-DD)
* @param to poslední den rozsahu (YYYY-MM-DD)
*/
export function listDatesInRange(from: string, to: string): string[] {
const dates: string[] = [];
const [fromYear, fromMonth, fromDay] = from.split('-').map(Number);
const [toYear, toMonth, toDay] = to.split('-').map(Number);
const cursor = new Date(fromYear, fromMonth - 1, fromDay);
const end = new Date(toYear, toMonth - 1, toDay);
while (cursor <= end) {
dates.push(formatDate(cursor));
cursor.setDate(cursor.getDate() + 1);
}
return dates;
}