feat: založení základní stránky příjem/výdej + import a statistiky
This commit is contained in:
commit
a5711f3e1e
98 files changed
+15505
No files matched your search
@@ -0,0 +1,195 @@
|
||||
const TOKEN_KEY = "token";
|
||||
|
||||
/** Uloží token do local storage prohlížeče. */
|
||||
export const storeToken = (token: string) => {
|
||||
localStorage.setItem(TOKEN_KEY, token);
|
||||
}
|
||||
|
||||
/** Vrátí token z local storage, pokud tam je. */
|
||||
export const getToken = (): string | undefined => {
|
||||
return localStorage.getItem(TOKEN_KEY) ?? undefined;
|
||||
}
|
||||
|
||||
/** Odstraní token z local storage, pokud tam je. */
|
||||
export const deleteToken = () => {
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
}
|
||||
|
||||
/** 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í měsíc data ve formátu YYYY-MM (hodnota pro <input type="month">). */
|
||||
export function formatMonth(date: Date): string {
|
||||
return formatDate(date).substring(0, 7);
|
||||
}
|
||||
|
||||
/** Převede datum ve formátu YYYY-MM-DD na DD.MM.YYYY */
|
||||
export function formatDateString(date: string): string {
|
||||
const [year, month, day] = date.split('-');
|
||||
return `${day}.${month}.${year}`;
|
||||
}
|
||||
|
||||
/** Převede řetězec YYYY-MM-DD na Date v lokální časové zóně. */
|
||||
export function parseIsoDate(date: string): Date {
|
||||
const [year, month, day] = date.split('-').map(Number);
|
||||
return new Date(year, month - 1, day);
|
||||
}
|
||||
|
||||
/** Posune datum ve formátu YYYY-MM-DD o zadaný počet dní. */
|
||||
export function shiftDate(date: string, days: number): string {
|
||||
const shifted = parseIsoDate(date);
|
||||
shifted.setDate(shifted.getDate() + days);
|
||||
return formatDate(shifted);
|
||||
}
|
||||
|
||||
/** Vrátí první den měsíce (YYYY-MM) ve formátu YYYY-MM-DD. */
|
||||
export function getMonthStart(month: string): string {
|
||||
return `${month}-01`;
|
||||
}
|
||||
|
||||
/** Vrátí poslední den měsíce (YYYY-MM) ve formátu YYYY-MM-DD. */
|
||||
export function getMonthEnd(month: string): string {
|
||||
const [year, monthNumber] = month.split('-').map(Number);
|
||||
// Nultý den následujícího měsíce = poslední den tohoto měsíce
|
||||
return formatDate(new Date(year, monthNumber, 0));
|
||||
}
|
||||
|
||||
const DAY_OF_WEEK_FORMAT = new Intl.DateTimeFormat('cs-CZ', { weekday: 'long' });
|
||||
|
||||
const HUMAN_DATE_FORMAT = new Intl.DateTimeFormat('cs-CZ', { day: 'numeric', month: 'long', year: 'numeric' });
|
||||
|
||||
const MONTH_FORMAT = new Intl.DateTimeFormat('cs-CZ', { month: 'long', year: 'numeric' });
|
||||
|
||||
/** Vrátí název dne v týdnu pro datum ve formátu YYYY-MM-DD. */
|
||||
export function getDayOfWeek(date: string): string {
|
||||
return DAY_OF_WEEK_FORMAT.format(parseIsoDate(date));
|
||||
}
|
||||
|
||||
/** Vrátí čitelný zápis data, např. "3. března 2025". */
|
||||
export function getHumanDate(date: string): string {
|
||||
return HUMAN_DATE_FORMAT.format(parseIsoDate(date));
|
||||
}
|
||||
|
||||
/** Vrátí čitelný zápis měsíce (YYYY-MM), např. "březen 2025". */
|
||||
export function getHumanMonth(month: string): string {
|
||||
return MONTH_FORMAT.format(parseIsoDate(`${month}-01`));
|
||||
}
|
||||
|
||||
const PRICE_FORMAT = new Intl.NumberFormat('cs-CZ', {
|
||||
style: 'currency',
|
||||
currency: 'CZK',
|
||||
maximumFractionDigits: 2,
|
||||
});
|
||||
|
||||
/**
|
||||
* Naformátuje cenu z haléřů na koruny, např. 15900 → "159,00 Kč".
|
||||
* Nevyplněnou cenu zobrazí jako pomlčku.
|
||||
*/
|
||||
export function formatPrice(halere?: number): string {
|
||||
if (halere == null) return '—';
|
||||
return PRICE_FORMAT.format(halere / 100);
|
||||
}
|
||||
|
||||
/** Naformátuje cenu z haléřů bez desetinných míst — pro souhrny a osy grafů. */
|
||||
export function formatPriceShort(halere: number): string {
|
||||
return `${Math.round(halere / 100).toLocaleString('cs-CZ')} Kč`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Převede uživatelem zadanou cenu v korunách na haléře.
|
||||
* Přijímá desetinnou čárku i tečku. Prázdný vstup vrací undefined.
|
||||
*/
|
||||
export function parsePriceInput(value: string): number | undefined {
|
||||
const trimmed = value.trim().replace(',', '.');
|
||||
if (!trimmed.length) return undefined;
|
||||
const parsed = Number(trimmed);
|
||||
return Number.isFinite(parsed) && parsed >= 0 ? Math.round(parsed * 100) : undefined;
|
||||
}
|
||||
|
||||
/** Převede haléře na text pro editační pole (koruny s desetinnou čárkou). */
|
||||
export function formatPriceInput(halere?: number): string {
|
||||
if (halere == null) return '';
|
||||
return (halere / 100).toFixed(2).replace('.', ',');
|
||||
}
|
||||
|
||||
/** Naformátuje počet kalorií, např. 850 → "850 kcal". */
|
||||
export function formatCalories(kcal?: number): string {
|
||||
if (!kcal) return '—';
|
||||
return `${kcal.toLocaleString('cs-CZ')} kcal`;
|
||||
}
|
||||
|
||||
/** Načte soubor jako Base64 řetězec (bez data: prefixu). */
|
||||
export function readFileAsBase64(file: File): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const result = String(reader.result);
|
||||
// FileReader vrací "data:<mime>;base64,<data>", server chce jen data
|
||||
resolve(result.substring(result.indexOf(',') + 1));
|
||||
};
|
||||
reader.onerror = () => reject(new Error('Soubor se nepodařilo načíst'));
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Dopočte gramáž a kalorie z jednotkových hodnot.
|
||||
*
|
||||
* Musí dávat stejné výsledky jako `deriveAmounts` na serveru — klient jen ukazuje
|
||||
* živý náhled toho, co server uloží.
|
||||
*
|
||||
* @param price cena v haléřích
|
||||
* @param pricePer100g cena za 100 g v haléřích
|
||||
* @param weight ručně zadaná gramáž v gramech, má přednost před dopočtem
|
||||
* @param caloriesPer100g energie na 100 g v kcal
|
||||
*/
|
||||
export function deriveAmounts(
|
||||
price?: number,
|
||||
pricePer100g?: number,
|
||||
weight?: number,
|
||||
caloriesPer100g?: number,
|
||||
): { weight?: number, calories?: number } {
|
||||
const resolvedWeight = weight ?? (
|
||||
price != null && pricePer100g ? Math.round(price / pricePer100g * 100) : undefined
|
||||
);
|
||||
const calories = resolvedWeight != null && caloriesPer100g
|
||||
? Math.round(resolvedWeight / 100 * caloriesPer100g)
|
||||
: undefined;
|
||||
return { weight: resolvedWeight, calories };
|
||||
}
|
||||
|
||||
/** Naformátuje gramáž, např. 400 → "400 g". */
|
||||
export function formatWeight(grams?: number): string {
|
||||
if (grams == null) return '—';
|
||||
return `${grams.toLocaleString('cs-CZ')} g`;
|
||||
}
|
||||
|
||||
/** Převede celé číslo z pole na number, prázdné pole na undefined. */
|
||||
export function parseIntInput(value: string): number | undefined {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed.length) return undefined;
|
||||
const parsed = Number(trimmed);
|
||||
return Number.isFinite(parsed) && parsed >= 0 ? Math.round(parsed) : undefined;
|
||||
}
|
||||
|
||||
/** Naformátuje množství aktivity i s jednotkou, např. "10 000 kroků". */
|
||||
export function formatQuantity(quantity: number, unitShort: string): string {
|
||||
return `${quantity.toLocaleString('cs-CZ')} ${unitShort}`;
|
||||
}
|
||||
|
||||
/** Dopočte spálené kalorie z množství a sazby na 100 jednotek. */
|
||||
export function deriveActivityCalories(quantity?: number, caloriesPer100Units?: number): number | undefined {
|
||||
if (quantity == null || !caloriesPer100Units) return undefined;
|
||||
return Math.round(quantity / 100 * caloriesPer100Units);
|
||||
}
|
||||
|
||||
/** Naformátuje bilanci se znaménkem, např. "−320 kcal" nebo "+150 kcal". */
|
||||
export function formatBalance(kcal: number): string {
|
||||
const sign = kcal < 0 ? '−' : '+';
|
||||
return `${sign}${Math.abs(kcal).toLocaleString('cs-CZ')} kcal`;
|
||||
}
|
||||
Reference in new issue
Block a user