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:
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> }>;
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user