feat: import jen některých dnů

feat: násobení jídel/cviků
feat: refaktor napojování šablon
This commit is contained in:
Ondřej Anděl committed 2026-09-30 19:10:54 +02:00
1 parent c3990d80f8
commit 8dac67bdd1
37 files changed
+2314 -481

No files matched your search

+102
View File
@@ -1,3 +1,5 @@
import { MealTemplate } from "../../types";
const TOKEN_KEY = "token";
/** Uloží token do local storage prohlížeče. */
@@ -253,3 +255,103 @@ export const formatQuantityWithSets = (quantity: number, unitShort: string, sets
/** Vrátí celkové množství aktivity — počet sérií krát množství v jedné. */
export const getTotalQuantity = (quantity?: number, sets?: number): number | undefined =>
quantity == null ? undefined : quantity * Math.max(sets ?? 1, 1);
/**
* Vrátí kalorie záznamu po započtení násobku porce.
* Musí dávat stejné výsledky jako `applyMultiplier` na serveru.
*/
export const applyMultiplier = (calories?: number, multiplier?: number): number | undefined =>
calories == null ? undefined : Math.round(calories * (multiplier ?? 1));
/** Naformátuje násobek, např. 1.5 → "×1,5". */
export const formatMultiplier = (multiplier?: number): string =>
`×${(multiplier ?? 1).toLocaleString('cs-CZ', { maximumFractionDigits: 2 })}`;
/** Převede násobek zadaný uživatelem (i s desetinnou čárkou) na číslo; nekladný vstup odmítne. */
export const parseMultiplierInput = (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) / 100 : undefined;
};
/** Převede násobek na text pro editační pole; výchozí 1 nechá prázdné. */
export const formatMultiplierInput = (multiplier?: number): string =>
multiplier == null || multiplier === 1 ? '' : String(multiplier).replace('.', ',');
/** Naformátuje dodatečnou zátěž z gramů, např. 12500 → "+12,5 kg". */
export const formatExtraWeight = (grams?: number): string =>
grams ? `+${(grams / 1000).toLocaleString('cs-CZ', { maximumFractionDigits: 2 })} kg` : '';
/** Převede gramy na kilogramy pro editační pole bez zbytečných desetinných míst. */
export const formatKgInput = (grams?: number): string =>
grams ? String(grams / 1000).replace('.', ',') : '';
/** Surovina receptu tak, jak ji zná výpočet — hodnoty jsou už čísla. */
type RecipeIngredientValues = {
weight?: number,
caloriesPer100g?: number,
calories?: number,
price?: number,
};
/** Energie suroviny — z gramáže a energie na 100 g, jinak ručně zadaná. */
export const getIngredientCalories = (ingredient: RecipeIngredientValues): number | undefined =>
ingredient.weight != null && ingredient.caloriesPer100g
? Math.round(ingredient.weight / 100 * ingredient.caloriesPer100g)
: ingredient.calories;
/** Sečte vyplněné hodnoty; když není vyplněná žádná, vrátí undefined. */
const sumDefined = (valueList: (number | undefined)[]): number | undefined => {
const definedList = valueList.filter(value => value != null);
return definedList.length ? definedList.reduce((sum, value) => sum + value, 0) : undefined;
};
/**
* Dopočte ze surovin hodnoty hotového jídla a jedné porce.
* Musí dávat stejné výsledky jako `deriveRecipe` na serveru.
*
* @param ingredients suroviny receptu
* @param servings počet porcí, nevyplněný znamená jednu
* @param cookedWeight hmotnost hotového jídla v gramech, bez ní součet surovin
*/
export const deriveRecipe = (
ingredients: RecipeIngredientValues[],
servings?: number,
cookedWeight?: number,
) => {
const portions = Math.max(servings ?? 1, 1);
const totalCalories = sumDefined(ingredients.map(getIngredientCalories));
const totalWeight = cookedWeight || sumDefined(ingredients.map(ingredient => ingredient.weight)) || undefined;
const totalPrice = sumDefined(ingredients.map(ingredient => ingredient.price));
return {
totalCalories,
totalWeight,
totalPrice,
weight: totalWeight != null ? Math.round(totalWeight / portions) : undefined,
calories: totalCalories != null ? Math.round(totalCalories / portions) : undefined,
caloriesPer100g: totalCalories != null && totalWeight
? Math.round(totalCalories / totalWeight * 100)
: undefined,
price: totalPrice != null ? Math.round(totalPrice / portions) : undefined,
};
};
/** Znormalizuje text pro hledání — bez diakritiky a malými písmeny. */
export const normalizeSearch = (value: string): string =>
value.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase().trim();
/**
* Popíše šablonu jídla jednou řádkou — zdroj, gramáž porce, kalorie a cena,
* co je vyplněné. U receptu jsou to hodnoty jedné porce, které dopočítal server.
*/
export const describeMealTemplate = (template: MealTemplate): string => {
const derived = deriveAmounts(template.price, template.pricePer100g, template.weight, template.caloriesPer100g);
const calories = derived.calories ?? template.calories;
return [
template.source,
derived.weight != null ? formatWeight(derived.weight) : undefined,
calories != null ? formatCalories(calories) : undefined,
template.price != null ? formatPrice(template.price) : undefined,
].filter(item => !!item).join(' · ');
};