fix(statistics): feeding totals per unit instead of adding litres to kilograms

The feeding statistics summed every amount regardless of its unit, so
5 l syrup and 2 kg fondant showed as 7. The API now also returns byUnit
(units normalised: "L" = "l"), the old fields stay for compatibility,
and the statistics page shows one list per unit.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Opus 5.5 committed 2026-10-01 12:27:24 +02:00
1 parent ed0a000534
commit 2bc0db6f33
4 files changed
+92 -45

No files matched your search

@@ -212,5 +212,25 @@ describe('StatisticsService', () => {
expect(result.byColony).toEqual({ c1: 8, c2: 2 }); expect(result.byColony).toEqual({ c1: 8, c2: 2 });
expect(result.bySeason).toEqual({ '2025': 8, '2024': 2 }); expect(result.bySeason).toEqual({ '2025': 8, '2024': 2 });
}); });
it('never adds litres and kilograms together', async () => {
prisma.feeding.findMany.mockResolvedValueOnce([
{ colonyId: 'c1', date: new Date('2025-09-01'), amount: 5, unit: 'l' },
{ colonyId: 'c1', date: new Date('2025-09-15'), amount: 2, unit: 'kg' },
{
colonyId: 'c1',
date: new Date('2025-10-01'),
amount: 1,
unit: ' L ',
},
]);
const result = await service.feeding();
expect(result.byUnit).toEqual({
l: { byColony: { c1: 6 }, bySeason: { '2025': 6 } },
kg: { byColony: { c1: 2 }, bySeason: { '2025': 2 } },
});
});
}); });
}); });
@@ -206,15 +206,27 @@ export class StatisticsService {
select: { colonyId: true, date: true, amount: true, unit: true }, select: { colonyId: true, date: true, amount: true, unit: true },
}); });
// Litres and kilograms cannot be added up: totals are kept per unit
// (byUnit). byColony/bySeason stay for older clients and are only
// meaningful when a single unit is in use.
const byColony: Record<string, number> = {}; const byColony: Record<string, number> = {};
const bySeason: Record<string, number> = {}; const bySeason: Record<string, number> = {};
const byUnit: Record<
string,
{ byColony: Record<string, number>; bySeason: Record<string, number> }
> = {};
for (const feeding of feedings) { for (const feeding of feedings) {
const year = feeding.date.getFullYear().toString();
const unit = feeding.unit.trim().toLowerCase() || 'ohne Einheit';
byColony[feeding.colonyId] = byColony[feeding.colonyId] =
(byColony[feeding.colonyId] ?? 0) + feeding.amount; (byColony[feeding.colonyId] ?? 0) + feeding.amount;
const year = feeding.date.getFullYear().toString();
bySeason[year] = (bySeason[year] ?? 0) + feeding.amount; bySeason[year] = (bySeason[year] ?? 0) + feeding.amount;
const totals = (byUnit[unit] ??= { byColony: {}, bySeason: {} });
totals.byColony[feeding.colonyId] =
(totals.byColony[feeding.colonyId] ?? 0) + feeding.amount;
totals.bySeason[year] = (totals.bySeason[year] ?? 0) + feeding.amount;
} }
return { byColony, bySeason }; return { byColony, bySeason, byUnit };
} }
} }
@@ -293,49 +293,62 @@ function FeedingStatsPanel({ colonyLabels }: { colonyLabels: Map<string, string>
return ( return (
<QueryState query={query} what="Futterdaten"> <QueryState query={query} what="Futterdaten">
{(data) => ( {(data) => {
<div className="flex flex-col gap-4"> // One block per unit: litres and kilograms are never added up.
<PanelHint> const units = Object.entries(
Summe der eingetragenen Futtermengen in Liter bzw. kg, so wie sie bei der Fütterung data.byUnit ?? { [FEED_UNIT]: { byColony: data.byColony, bySeason: data.bySeason } },
erfasst wurden. );
</PanelHint> return (
<Card> <div className="flex flex-col gap-4">
<CardHeader> <PanelHint>
<CardTitle>Futterverbrauch pro Volk</CardTitle> Summe der eingetragenen Futtermengen, getrennt nach Einheit – so wie sie bei der
</CardHeader> Fütterung erfasst wurden.
<CardContent> </PanelHint>
<StatBarList {units.length === 0 ? (
items={Object.entries(data.byColony) <p className="text-sm text-muted-foreground">Noch keine Fütterungen erfasst.</p>
.sort(([, a], [, b]) => b - a) ) : null}
.slice(0, 10) {units.map(([unit, totals]) => (
.map(([colonyId, value]) => ({ <div key={unit} className="flex flex-col gap-4">
label: colonyLabels.get(colonyId) ?? colonyId, <Card>
value, <CardHeader>
unit: FEED_UNIT, <CardTitle>Futterverbrauch pro Volk ({unit})</CardTitle>
}))} </CardHeader>
formatValue={kg} <CardContent>
emptyText="Noch keine Fütterungen erfasst." <StatBarList
/> items={Object.entries(totals.byColony)
</CardContent> .sort(([, a], [, b]) => b - a)
</Card> .slice(0, 10)
<Card> .map(([colonyId, value]) => ({
<CardHeader> label: colonyLabels.get(colonyId) ?? colonyId,
<CardTitle>Futterverbrauch pro Saison</CardTitle> value,
</CardHeader> unit,
<CardContent> }))}
<StatBarList formatValue={kg}
items={sortedRecordEntries(data.bySeason).map(([year, value]) => ({ emptyText="Noch keine Fütterungen erfasst."
label: year, />
value, </CardContent>
unit: FEED_UNIT, </Card>
}))} <Card>
formatValue={kg} <CardHeader>
emptyText="Noch keine Fütterungen erfasst." <CardTitle>Futterverbrauch pro Saison ({unit})</CardTitle>
/> </CardHeader>
</CardContent> <CardContent>
</Card> <StatBarList
</div> items={sortedRecordEntries(totals.bySeason).map(([year, value]) => ({
)} label: year,
value,
unit,
}))}
formatValue={kg}
emptyText="Noch keine Fütterungen erfasst."
/>
</CardContent>
</Card>
</div>
))}
</div>
);
}}
</QueryState> </QueryState>
); );
} }
@@ -48,4 +48,6 @@ export interface HoneyStats {
export interface FeedingStats { export interface FeedingStats {
byColony: Record<string, number>; byColony: Record<string, number>;
bySeason: Record<string, number>; bySeason: Record<string, number>;
/** Totals per unit ("l", "kg", ...) - units are never added together. */
byUnit?: Record<string, { byColony: Record<string, number>; bySeason: Record<string, number> }>;
} }