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

+53 -17
View File
@@ -1,7 +1,9 @@
import { randomUUID } from 'crypto';
import { DayRecord, MealEntry, MealType } from "../../types/gen/types.gen";
import getStorage from "./storage";
import { BadRequestError, NotFoundError, isValidIsoDate, listDatesInRange } from "./utils";
import {
BadRequestError, NotFoundError, applyMultiplier, isValidIsoDate, listDatesInRange, normalizeMultiplier,
} from "./utils";
import { rememberCalories } from "./calories";
const storage = getStorage();
@@ -47,7 +49,7 @@ export function buildDayRecord(date: string, entries: MealEntry[]): DayRecord {
date,
entries: sorted,
totalPrice: sorted.reduce((sum, entry) => sum + (entry.price ?? 0), 0),
totalCalories: sorted.reduce((sum, entry) => sum + (entry.calories ?? 0), 0),
totalCalories: sorted.reduce((sum, entry) => sum + (applyMultiplier(entry.calories, entry.multiplier) ?? 0), 0),
};
}
@@ -122,6 +124,7 @@ export type NormalizedMealInput = {
weight?: number;
pricePer100g?: number;
caloriesPer100g?: number;
multiplier?: number;
};
/**
@@ -168,6 +171,7 @@ export function normalizeInput(input: unknown): NormalizedMealInput {
weight: normalizeNumber(value.weight, 'weight'),
pricePer100g: normalizeNumber(value.pricePer100g, 'pricePer100g'),
caloriesPer100g: normalizeNumber(value.caloriesPer100g, 'caloriesPer100g'),
multiplier: normalizeMultiplier(value.multiplier),
});
}
@@ -200,8 +204,10 @@ export async function addMeal(login: string, input: unknown): Promise<DayRecord>
*
* @param login přihlašovací jméno uživatele
* @param entries záznamy k uložení
* @param replaceImportSource pokud je vyplněný, odeberou se z dotčených dnů nejdřív
* dřívější záznamy z tohoto importu; ručně zadaná jídla zůstanou
*/
export async function addEntries(login: string, entries: MealEntry[]): Promise<string[]> {
export async function addEntries(login: string, entries: MealEntry[], replaceImportSource?: string): Promise<string[]> {
const byDate = new Map<string, MealEntry[]>();
for (const entry of entries) {
const forDate = byDate.get(entry.date) ?? [];
@@ -211,7 +217,10 @@ export async function addEntries(login: string, entries: MealEntry[]): Promise<s
for (const [date, forDate] of byDate) {
await storage.updateData<MealEntry[]>(
getDayKey(login, date),
current => [...(current ?? []), ...forDate]
current => [
...(current ?? []).filter(entry => !replaceImportSource || entry.importSource !== replaceImportSource),
...forDate,
]
);
}
return Array.from(byDate.keys()).sort();
@@ -258,6 +267,7 @@ export async function updateMeal(login: string, id: unknown, input: unknown): Pr
weight: normalized.weight,
pricePer100g: normalized.pricePer100g,
caloriesPer100g: normalized.caloriesPer100g,
multiplier: normalized.multiplier,
updatedAt: new Date().toISOString(),
};
const entries = await storage.updateData<MealEntry[]>(
@@ -295,6 +305,40 @@ export async function deleteMeal(login: string, id: unknown, date: unknown): Pro
return buildDayRecord(date, entries);
}
/**
* Nastaví násobek porce jednoho jídla. Ostatní údaje záznamu zůstanou beze změny,
* takže jde násobek měnit přímo z přehledu dne bez otevření formuláře.
*
* @param login přihlašovací jméno uživatele
* @param id identifikátor záznamu
* @param date datum záznamu (YYYY-MM-DD)
* @param multiplier nový násobek; 1 násobek zruší
*/
export async function setMealMultiplier(login: string, id: unknown, date: unknown, multiplier: unknown): Promise<string> {
if (typeof id !== 'string' || !id.length) {
throw new BadRequestError("Nebyl předán identifikátor záznamu");
}
if (!isValidIsoDate(date)) {
throw new BadRequestError("Nebylo předáno platné datum ve formátu YYYY-MM-DD");
}
if (multiplier == null) {
throw new BadRequestError("Nebyl předán násobek");
}
const normalized = normalizeMultiplier(multiplier);
let found = false;
await storage.updateData<MealEntry[]>(getDayKey(login, date), current => (current ?? []).map(entry => {
if (entry.id !== id) return entry;
found = true;
const updated: MealEntry = { ...entry, multiplier: normalized, updatedAt: new Date().toISOString() };
if (normalized == null) delete updated.multiplier;
return updated;
}));
if (!found) {
throw new NotFoundError("Záznam o jídle nebyl nalezen");
}
return date;
}
/**
* Najde datum, pod kterým je záznam uložený.
* Nejdřív zkusí očekávaný den, teprve pak prohledá ostatní klíče uživatele —
@@ -317,21 +361,13 @@ async function findEntryDate(login: string, id: string, expectedDate: string): P
}
/**
* Vrátí klíče importovaných záznamů uživatele v předaných dnech.
* Slouží k rozpoznání duplicit při opakovaném importu stejného měsíce.
* Vrátí všechna jídla uživatele v předaných dnech, seřazená dle dne a chodu.
* Import podle nich rozpoznává duplicity a v náhledu je ukazuje uživateli.
*
* @param login přihlašovací jméno uživatele
* @param dates dny, ve kterých se má hledat
*/
export async function getExistingImportKeys(login: string, dates: string[]): Promise<Set<string>> {
const keys = new Set<string>();
for (const date of dates) {
const entries = await storage.getData<MealEntry[]>(getDayKey(login, date));
for (const entry of entries ?? []) {
if (entry.importKey) {
keys.add(entry.importKey);
}
}
}
return keys;
export async function findMealsForDays(login: string, dates: string[]): Promise<MealEntry[]> {
const days = await Promise.all([...dates].sort().map(date => getDay(login, date)));
return days.flatMap(day => day.entries);
}