chore: commit chybějících souborů
This commit is contained in:
1 parent
f1ce3ff297
commit
9e72f431a1
7 files changed
+890
No files matched your search
@@ -0,0 +1,209 @@
|
||||
process.env.CALORIE_PROVIDER = 'none';
|
||||
|
||||
import { addActivity, deleteActivity, getActivityDay, setActivityMultiplier, updateActivity } from "../activities";
|
||||
import { applyWorkoutTemplate, deleteWorkoutTemplate, saveWorkoutTemplate } from "../workouts";
|
||||
import { getActivityStats } from "../statsService";
|
||||
import { getWorkoutPlans } from "../workoutPlans";
|
||||
import { resetMemoryStorage } from "../storage/memory";
|
||||
import { applyMealTemplate, saveMealTemplate } from "../mealTemplates";
|
||||
import { addMeal, getDay } from "../meals";
|
||||
import { ActivityUnit, MealType, PlanDeviationKind, WorkoutItemKind } from "../../../types/gen/types.gen";
|
||||
|
||||
const LOGIN = 'tester';
|
||||
const DATE = '2025-03-03';
|
||||
const NEXT_DATE = '2025-03-04';
|
||||
|
||||
/** Cvik jako položka šablony. */
|
||||
const cvik = (name: string, quantity: number, rate: number) => ({
|
||||
kind: WorkoutItemKind.CVIK,
|
||||
name,
|
||||
unit: ActivityUnit.OPAKOVANI,
|
||||
quantity,
|
||||
caloriesPer100Units: rate,
|
||||
});
|
||||
|
||||
/** Založí šablonu Kliky 100 × 0,5 kcal + Dřepy 100 × 0,3 kcal, tedy 80 kcal. */
|
||||
async function createWorkout(): Promise<string> {
|
||||
const [template] = await saveWorkoutTemplate(LOGIN, undefined, 'Workout', [
|
||||
cvik('Kliky', 100, 50),
|
||||
cvik('Dřepy', 100, 30),
|
||||
]);
|
||||
return template.id;
|
||||
}
|
||||
|
||||
const stats = () => getActivityStats(LOGIN, '2025-03-01', '2025-03-31');
|
||||
|
||||
beforeEach(() => resetMemoryStorage());
|
||||
|
||||
describe('statistiky výdeje', () => {
|
||||
|
||||
it('odcvičená šablona se shoduje s plánem', async () => {
|
||||
const id = await createWorkout();
|
||||
await applyWorkoutTemplate(LOGIN, id, DATE);
|
||||
|
||||
const result = await stats();
|
||||
expect(result.templateCalories).toBe(80);
|
||||
expect(result.plannedCalories).toBe(80);
|
||||
expect(result.extraCalories).toBe(0);
|
||||
expect(result.byTemplate).toEqual([expect.objectContaining({
|
||||
name: 'Workout', dayCount: 1, plannedCalories: 80, actualCalories: 80,
|
||||
plannedItemCount: 2, doneItemCount: 2,
|
||||
})]);
|
||||
});
|
||||
|
||||
it('smazaný cvik ze šablony se projeví jako odchylka', async () => {
|
||||
const id = await createWorkout();
|
||||
await applyWorkoutTemplate(LOGIN, id, DATE);
|
||||
const drepy = (await getActivityDay(LOGIN, DATE)).entries.find(entry => entry.name === 'Dřepy')!;
|
||||
await deleteActivity(LOGIN, drepy.id, DATE);
|
||||
|
||||
const result = await stats();
|
||||
expect(result.templateCalories).toBe(50);
|
||||
expect(result.plannedCalories).toBe(80);
|
||||
expect(result.byTemplate[0]).toEqual(expect.objectContaining({ plannedItemCount: 2, doneItemCount: 1 }));
|
||||
});
|
||||
|
||||
it('zkrácený cvik ze šablony se projeví jako odchylka', async () => {
|
||||
const id = await createWorkout();
|
||||
await applyWorkoutTemplate(LOGIN, id, DATE);
|
||||
const kliky = (await getActivityDay(LOGIN, DATE)).entries.find(entry => entry.name === 'Kliky')!;
|
||||
await setActivityMultiplier(LOGIN, kliky.id, DATE, 0.5);
|
||||
|
||||
const result = await stats();
|
||||
expect(result.templateCalories).toBe(55);
|
||||
expect(result.plannedCalories).toBe(80);
|
||||
});
|
||||
|
||||
it('smazání všech cviků šablony vrátí její použití, nepočítá se jako odchylka', async () => {
|
||||
const id = await createWorkout();
|
||||
await applyWorkoutTemplate(LOGIN, id, DATE);
|
||||
for (const entry of (await getActivityDay(LOGIN, DATE)).entries) {
|
||||
await deleteActivity(LOGIN, entry.id, DATE);
|
||||
}
|
||||
|
||||
expect(await getWorkoutPlans(LOGIN, DATE)).toEqual([]);
|
||||
const result = await stats();
|
||||
expect(result.plannedCalories).toBe(0);
|
||||
expect(result.byTemplate).toEqual([]);
|
||||
});
|
||||
|
||||
it('plán se drží podoby šablony v době použití, ne její pozdější úpravy', async () => {
|
||||
const id = await createWorkout();
|
||||
await applyWorkoutTemplate(LOGIN, id, DATE);
|
||||
await saveWorkoutTemplate(LOGIN, id, 'Workout v2', [cvik('Kliky', 200, 50)]);
|
||||
|
||||
const result = await stats();
|
||||
expect(result.plannedCalories).toBe(80);
|
||||
expect(result.byTemplate[0].name).toBe('Workout');
|
||||
});
|
||||
|
||||
it('aktivity mimo šablonu se sčítají zvlášť podle názvu', async () => {
|
||||
const id = await createWorkout();
|
||||
await applyWorkoutTemplate(LOGIN, id, DATE);
|
||||
await addActivity(LOGIN, { date: DATE, name: 'Chůze', unit: ActivityUnit.KROKY, quantity: 5000, caloriesPer100Units: 4 });
|
||||
await addActivity(LOGIN, { date: NEXT_DATE, name: 'Chůze', unit: ActivityUnit.KROKY, quantity: 5000, caloriesPer100Units: 4 });
|
||||
|
||||
const result = await stats();
|
||||
expect(result.totalCalories).toBe(480);
|
||||
expect(result.extraCalories).toBe(400);
|
||||
expect(result.extras).toEqual([{ key: 'Chůze', calories: 400, count: 2 }]);
|
||||
expect(result.dayCount).toBe(2);
|
||||
expect(result.byDay).toEqual([
|
||||
{ period: DATE, templateCalories: 80, plannedCalories: 80, extraCalories: 200 },
|
||||
{ period: NEXT_DATE, templateCalories: 0, plannedCalories: 0, extraCalories: 200 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('cvik přesunutý do jiného dne chybí v původním a v novém se bere bez odchylky', async () => {
|
||||
const id = await createWorkout();
|
||||
await applyWorkoutTemplate(LOGIN, id, DATE);
|
||||
const kliky = (await getActivityDay(LOGIN, DATE)).entries.find(entry => entry.name === 'Kliky')!;
|
||||
await updateActivity(LOGIN, kliky.id, { ...kliky, date: NEXT_DATE });
|
||||
|
||||
const result = await stats();
|
||||
// Původní den: plán 80, zůstaly dřepy za 30; nový den: kliky 50 bez plánu
|
||||
expect(result.templateCalories).toBe(80);
|
||||
expect(result.plannedCalories).toBe(130);
|
||||
});
|
||||
|
||||
it('šablona smazaná po použití se ve statistikách drží názvu z plánu', async () => {
|
||||
const id = await createWorkout();
|
||||
await applyWorkoutTemplate(LOGIN, id, DATE);
|
||||
await deleteWorkoutTemplate(LOGIN, id);
|
||||
|
||||
const result = await stats();
|
||||
expect(result.byTemplate[0].name).toBe('Workout');
|
||||
});
|
||||
|
||||
it('odmítne obrácený rozsah', async () => {
|
||||
await expect(getActivityStats(LOGIN, '2025-03-31', '2025-03-01')).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('denní detail výdeje', () => {
|
||||
|
||||
it('ukáže, jaká šablona se použila a který cvik z ní chybí', async () => {
|
||||
const id = await createWorkout();
|
||||
await applyWorkoutTemplate(LOGIN, id, DATE);
|
||||
const drepy = (await getActivityDay(LOGIN, DATE)).entries.find(entry => entry.name === 'Dřepy')!;
|
||||
await deleteActivity(LOGIN, drepy.id, DATE);
|
||||
|
||||
const [day] = (await stats()).days;
|
||||
expect(day.date).toBe(DATE);
|
||||
expect(day.templates).toEqual([expect.objectContaining({
|
||||
name: 'Workout', plannedCalories: 80, actualCalories: 50, hasPlan: true,
|
||||
deviations: [{
|
||||
kind: PlanDeviationKind.ODEBRANO,
|
||||
planned: { name: 'Dřepy', unit: ActivityUnit.OPAKOVANI, quantity: 100, calories: 30 },
|
||||
}],
|
||||
})]);
|
||||
});
|
||||
|
||||
it('upravený cvik ukáže plán i skutečnost', async () => {
|
||||
const id = await createWorkout();
|
||||
await applyWorkoutTemplate(LOGIN, id, DATE);
|
||||
const kliky = (await getActivityDay(LOGIN, DATE)).entries.find(entry => entry.name === 'Kliky')!;
|
||||
await updateActivity(LOGIN, kliky.id, { ...kliky, quantity: 40 });
|
||||
|
||||
const [usage] = (await stats()).days[0].templates;
|
||||
expect(usage.deviations).toEqual([{
|
||||
kind: PlanDeviationKind.UPRAVENO,
|
||||
planned: expect.objectContaining({ name: 'Kliky', quantity: 100, calories: 50 }),
|
||||
actual: expect.objectContaining({ name: 'Kliky', quantity: 40, calories: 20 }),
|
||||
}]);
|
||||
});
|
||||
|
||||
it('odcvičená šablona nemá žádnou odchylku', async () => {
|
||||
const id = await createWorkout();
|
||||
await applyWorkoutTemplate(LOGIN, id, DATE);
|
||||
expect((await stats()).days[0].templates[0].deviations).toEqual([]);
|
||||
});
|
||||
|
||||
it('aktivity mimo šablonu vrátí jednotlivě i se součtem', async () => {
|
||||
await addActivity(LOGIN, { date: DATE, name: 'Chůze', unit: ActivityUnit.KROKY, quantity: 5000, caloriesPer100Units: 4 });
|
||||
await addActivity(LOGIN, { date: DATE, name: 'Běh', unit: ActivityUnit.MINUTY, quantity: 20, caloriesPer100Units: 1000 });
|
||||
|
||||
const [day] = (await stats()).days;
|
||||
expect(day.templates).toEqual([]);
|
||||
expect(day.extraCalories).toBe(400);
|
||||
expect(day.extras.map(item => item.name)).toEqual(['Chůze', 'Běh']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('vazba jídla na šablonu', () => {
|
||||
|
||||
it('jídlo ze šablony si pamatuje, ze které vzniklo', async () => {
|
||||
const [template] = await saveMealTemplate(LOGIN, { name: 'Štrůdl', mealType: MealType.PULNOCNI_SVACINA });
|
||||
await applyMealTemplate(LOGIN, template.id, DATE);
|
||||
|
||||
const [meal] = (await getDay(LOGIN, DATE)).entries;
|
||||
expect(meal.templateId).toBe(template.id);
|
||||
expect(meal.mealType).toBe(MealType.PULNOCNI_SVACINA);
|
||||
});
|
||||
|
||||
it('ručně zadané jídlo vazbu nemá a klient ji podstrčit nemůže', async () => {
|
||||
await addMeal(LOGIN, { date: DATE, mealType: MealType.OBED, name: 'Guláš', templateId: 'podvrh' });
|
||||
const [meal] = (await getDay(LOGIN, DATE)).entries;
|
||||
expect(meal.templateId).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
import { ActivityEntry, ActivityUnit } from "../../types/gen/types.gen";
|
||||
import getStorage from "./storage";
|
||||
|
||||
const storage = getStorage();
|
||||
|
||||
/** Jeden cvik plánu — jak ho šablona založila. */
|
||||
export type WorkoutPlanItem = {
|
||||
/**
|
||||
* Aktivita, která z cviku vznikla. Podle ní se pozná, jestli cvik ve dni
|
||||
* zůstal a jestli se upravil — podle názvu by to nešlo, šablona může mít
|
||||
* stejný cvik dvakrát. Chybí jen u plánů dopočtených zpětně z dnešní podoby
|
||||
* šablony, kde se cvik ve dni nenašel.
|
||||
*/
|
||||
entryId?: string;
|
||||
name: string;
|
||||
unit: ActivityUnit;
|
||||
quantity: number;
|
||||
sets?: number;
|
||||
calories: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Plán jednoho použití šablony v konkrétní den — co šablona v tu chvíli slibovala.
|
||||
*
|
||||
* Bez něj by nešlo poznat odchylku od šablony: smazaná aktivita beze stopy zmizí
|
||||
* a šablona se mezitím mohla upravit, takže porovnání s její současnou podobou
|
||||
* by lhalo.
|
||||
*/
|
||||
export type WorkoutPlan = {
|
||||
templateId: string;
|
||||
/** Název šablony v době použití — šablona se mohla mezitím přejmenovat nebo smazat. */
|
||||
templateName: string;
|
||||
appliedAt: string;
|
||||
items: WorkoutPlanItem[];
|
||||
};
|
||||
|
||||
/** Klíč úložiště s plány šablon jednoho dne jednoho uživatele. */
|
||||
const getKey = (login: string, date: string): string => `workoutPlans:${login}:${date}`;
|
||||
|
||||
/**
|
||||
* Vrátí plány šablon použitých v daném dni.
|
||||
*
|
||||
* @param login přihlašovací jméno uživatele
|
||||
* @param date datum ve formátu YYYY-MM-DD
|
||||
*/
|
||||
export const getWorkoutPlans = async (login: string, date: string): Promise<WorkoutPlan[]> =>
|
||||
await storage.getData<WorkoutPlan[]>(getKey(login, date)) ?? [];
|
||||
|
||||
/**
|
||||
* Zapamatuje si plán použité šablony.
|
||||
*
|
||||
* @param login přihlašovací jméno uživatele
|
||||
* @param date den, do kterého se šablona použila
|
||||
* @param plan plán šablony
|
||||
*/
|
||||
export const recordWorkoutPlan = async (login: string, date: string, plan: WorkoutPlan): Promise<void> => {
|
||||
await storage.updateData<WorkoutPlan[]>(getKey(login, date), current => [...(current ?? []), plan]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Zahodí plány šablon, ze kterých ve dni nezůstala žádná aktivita.
|
||||
*
|
||||
* Smazání všech cviků šablony se bere jako vrácení jejího použití, ne jako
|
||||
* neodcvičený trénink — jinak by omylem použitá šablona navždy strašila ve
|
||||
* statistikách jako odchylka. Vynechání jen části cviků se jako odchylka počítá.
|
||||
*
|
||||
* @param login přihlašovací jméno uživatele
|
||||
* @param date datum dne
|
||||
* @param remaining aktivity, které ve dni zůstaly
|
||||
*/
|
||||
export const dropOrphanedPlans = async (login: string, date: string, remaining: ActivityEntry[]): Promise<void> => {
|
||||
const key = getKey(login, date);
|
||||
if (!await storage.hasData(key)) return;
|
||||
const ids = new Set(remaining.map(entry => entry.id));
|
||||
await storage.updateData<WorkoutPlan[]>(key,
|
||||
current => (current ?? []).filter(plan => plan.items.some(item => item.entryId && ids.has(item.entryId))));
|
||||
};
|
||||
Reference in new issue
Block a user