feat: split jídel

feat: graf příjmů
This commit is contained in:
Ondřej Anděl committed 2026-10-09 15:15:04 +02:00
1 parent bc85a37fc0
commit b880b36edd
32 files changed
+2048 -262

No files matched your search

+90
View File
@@ -0,0 +1,90 @@
process.env.CALORIE_PROVIDER = 'none';
import { addMeal } from "../meals";
import { addActivity } from "../activities";
import { saveBasalCalories, saveGoals } from "../settings";
import { getIntakeStats } from "../statsService";
import { resetMemoryStorage } from "../storage/memory";
import { ActivityUnit, MealType } from "../../../types/gen/types.gen";
const LOGIN = 'tester';
const stats = () => getIntakeStats(LOGIN, '2025-03-01', '2025-03-31');
beforeEach(() => resetMemoryStorage());
describe('statistiky příjmu', () => {
it('sečte příjem po dnech a chodech včetně násobku, jídla bez kalorií jen spočítá', async () => {
await addMeal(LOGIN, { date: '2025-03-03', mealType: MealType.SNIDANE, name: 'Kaše', calories: 300 });
await addMeal(LOGIN, {
date: '2025-03-03', mealType: MealType.OBED, name: 'Čína', source: 'Chefie', calories: 600, multiplier: 0.5,
});
await addMeal(LOGIN, { date: '2025-03-04', mealType: MealType.OBED, name: 'Čína', source: 'Chefie', calories: 600 });
await addMeal(LOGIN, { date: '2025-03-04', mealType: MealType.VECERE, name: 'Neznámo', price: 9900 });
const result = await stats();
expect(result).toMatchObject({
totalCalories: 1200, entryCount: 4, missingCaloriesCount: 1, dayCount: 2, caloriesDayCount: 2,
});
expect(result.byDay.map(day => [day.period, day.calories])).toEqual([['2025-03-03', 600], ['2025-03-04', 600]]);
expect(result.byMonth).toEqual([expect.objectContaining({ period: '2025-03', calories: 1200, dayCount: 2 })]);
expect(result.byMealType).toEqual([
{ key: MealType.OBED, calories: 900, count: 2 },
{ key: MealType.SNIDANE, calories: 300, count: 1 },
]);
expect(result.bySource).toEqual([
{ key: 'Chefie', calories: 900, count: 2 },
{ key: 'Neuvedeno', calories: 300, count: 1 },
]);
});
it('výdej počítá jen za dny, kdy má nějaké jídlo kalorie', async () => {
await saveBasalCalories(LOGIN, 1800);
await addMeal(LOGIN, { date: '2025-03-03', mealType: MealType.OBED, name: 'Čína', calories: 700 });
await addActivity(LOGIN, {
date: '2025-03-03', name: 'Chůze', unit: ActivityUnit.KROKY, quantity: 5000, caloriesPer100Units: 4,
});
// Pohyb v den bez jídla do výdeje nevstoupí
await addActivity(LOGIN, {
date: '2025-03-05', name: 'Chůze', unit: ActivityUnit.KROKY, quantity: 5000, caloriesPer100Units: 4,
});
// Den, kde žádné jídlo kalorie nemá (import), by jinak vyšel jako deficit celého výdeje
await addMeal(LOGIN, { date: '2025-03-06', mealType: MealType.OBED, name: 'Neznámo', price: 9900 });
const result = await stats();
expect(result.hasBasal).toBe(true);
expect(result.burnCalories).toBe(2000);
expect(result).toMatchObject({ dayCount: 2, caloriesDayCount: 1 });
expect(result.byDay).toEqual([
expect.objectContaining({ period: '2025-03-03', burnCalories: 2000, caloriesDayCount: 1 }),
expect.objectContaining({ period: '2025-03-06', burnCalories: 0, caloriesDayCount: 0 }),
]);
});
it('bez klidového výdeje to pozná a cíl vrátí, jen když je nastavený', async () => {
await addMeal(LOGIN, { date: '2025-03-03', mealType: MealType.OBED, name: 'Čína', calories: 700 });
const before = await stats();
expect(before.hasBasal).toBe(false);
expect(before.targetDailyBalance).toBeUndefined();
await saveGoals(LOGIN, { weeklyChangeGrams: -500 });
expect((await stats()).targetDailyBalance).toBe(-550);
});
it('živiny sečte jen ze dnů, kde je jídlo má', async () => {
await addMeal(LOGIN, {
date: '2025-03-03', mealType: MealType.OBED, name: 'Kuře', weight: 200, caloriesPer100g: 150,
nutrientsPer100g: { protein: 25 },
});
await addMeal(LOGIN, { date: '2025-03-04', mealType: MealType.OBED, name: 'Čína', calories: 700 });
const result = await stats();
expect(result.totalNutrients).toEqual({ protein: 50 });
expect(result.nutrientsDayCount).toBe(1);
});
});
+19
View File
@@ -25,10 +25,29 @@ describe('skupiny jídel podle názvu a zdroje', () => {
name: 'Čína', source: 'Chefie', count: 3, lastDate: '2025-03-03',
missingCaloriesCount: 2, missingWeightCount: 2, missingNutrientsCount: 3,
values: { weight: 400, caloriesPer100g: 150 },
days: [
{ date: '2025-03-03', incomplete: true },
{ date: '2025-03-02', incomplete: true },
{ date: '2025-03-01', incomplete: true },
],
});
expect(groups[1]).toMatchObject({ source: 'doma', count: 1 });
});
it('u dnů skupiny označí ty, kde jídlu něco chybí', async () => {
await addMeal(LOGIN, {
...cina, date: '2025-03-01', weight: 400, caloriesPer100g: 150, nutrientsPer100g: { protein: 8 },
});
await addMeal(LOGIN, { ...cina, date: '2025-03-02', price: 13900 });
const [group] = await getMealGroups(LOGIN);
expect(group.days).toEqual([
{ date: '2025-03-02', incomplete: true },
{ date: '2025-03-01', incomplete: false },
]);
});
it('energii a živiny předvyplní i z jídla stejného názvu odjinud, sazbu ne', async () => {
await addMeal(LOGIN, {
...cina, date: '2025-03-01', source: 'doma', pricePer100g: 2000, caloriesPer100g: 140,
+69
View File
@@ -0,0 +1,69 @@
process.env.CALORIE_PROVIDER = 'none';
import { addEntries, addMeal, getDay, splitMeal } from "../meals";
import { IMPORT_SOURCE } from "../luncherImport";
import { resetMemoryStorage } from "../storage/memory";
import { MealType } from "../../../types/gen/types.gen";
const LOGIN = 'tester';
const DATE = '2025-03-03';
beforeEach(() => resetMemoryStorage());
describe('rozdělení jídla', () => {
it('upraví původní jídlo a hned za něj založí nové ve stejném dni', async () => {
await addMeal(LOGIN, { date: DATE, mealType: MealType.SNIDANE, name: 'Kaše', calories: 300 });
const day = await addMeal(LOGIN, {
date: DATE, mealType: MealType.OBED, name: 'Tikka masala+garlic naan', source: 'Everest', price: 19900,
nutrientsPer100g: { protein: 9 }, multiplier: 0.5,
});
const original = day.entries.find(entry => entry.mealType === MealType.OBED)!;
const result = await splitMeal(LOGIN, original.id, DATE,
{
mealType: MealType.OBED, name: 'Tikka masala', source: 'Everest', price: 19900,
weight: 400, caloriesPer100g: 150, nutrientsPer100g: { protein: 9 }, multiplier: 0.5,
},
// Datum části se ignoruje, obě zůstávají v rozdělovaném dni
{ date: '2025-03-10', mealType: MealType.OBED, name: 'Garlic naan', source: 'Everest', calories: 280 });
expect(result.entries.map(entry => entry.name)).toEqual(['Kaše', 'Tikka masala', 'Garlic naan']);
const [, updated, created] = result.entries;
expect(updated).toMatchObject({
id: original.id, createdAt: original.createdAt, price: 19900, calories: 600, multiplier: 0.5,
nutrientsPer100g: { protein: 9 },
});
expect(created.id).not.toBe(original.id);
expect(created).toMatchObject({ date: DATE, price: undefined, calories: 280 });
expect(result.totalCalories).toBe(300 + 300 + 280);
expect((await getDay(LOGIN, '2025-03-10')).entries).toHaveLength(0);
});
it('odštěpená část převezme příznak importu, ale ne jeho klíč', async () => {
await addEntries(LOGIN, [{
id: 'imp-1', createdAt: '2025-03-03T10:00:00.000Z', date: DATE, mealType: MealType.OBED,
name: 'Tikka masala+garlic naan', price: 19900, importSource: IMPORT_SOURCE, importKey: 'klic-1',
}]);
const result = await splitMeal(LOGIN, 'imp-1', DATE,
{ mealType: MealType.OBED, name: 'Tikka masala', price: 19900 },
{ mealType: MealType.OBED, name: 'Garlic naan' });
const [updated, created] = result.entries;
expect(updated).toMatchObject({ importSource: IMPORT_SOURCE, importKey: 'klic-1' });
expect(created.importSource).toBe(IMPORT_SOURCE);
expect(created.importKey).toBeUndefined();
});
it('neexistující jídlo ani chybějící název nerozdělí', async () => {
const day = await addMeal(LOGIN, { date: DATE, mealType: MealType.OBED, name: 'Čína' });
const { id } = day.entries[0];
await expect(splitMeal(LOGIN, 'neni', DATE, { mealType: MealType.OBED, name: 'A' }, { mealType: MealType.OBED, name: 'B' }))
.rejects.toThrow('nebyl nalezen');
await expect(splitMeal(LOGIN, id, DATE, { mealType: MealType.OBED, name: 'A' }, { mealType: MealType.OBED, name: ' ' }))
.rejects.toThrow('název');
expect((await getDay(LOGIN, DATE)).entries).toHaveLength(1);
});
});