From 3e76360a6549c5c945932e3dcbc176cd01ecc0fa Mon Sep 17 00:00:00 2001 From: Corey Blais Date: Thu, 16 Apr 2026 21:44:03 -0400 Subject: [PATCH] 5-10% alert added --- frontend/src/App.tsx | 86 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 76 insertions(+), 10 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6b19c55..f19846a 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -210,6 +210,13 @@ type OutOfRangeBirdWeightAssessment = { varianceGrams: number; }; +type WeightDropAlert = { + bird: Bird; + previousWeight: WeightRecord; + latestWeight: WeightRecord; + dropPercent: number; +}; + type PhotoCropState = { sourceDataUrl: string; fileName: string; @@ -405,6 +412,8 @@ const formatDateTime = (value: string | null) => { const formatWeight = (value: number | null) => (value ? `${value.toFixed(1)} g` : 'Pending'); const formatRange = (minGrams: number, maxGrams: number) => `${minGrams.toFixed(0)}-${maxGrams.toFixed(0)} g`; const parseDateValue = (value: string) => new Date(`${value}T00:00:00`); +const daysBetweenDates = (startDate: string, endDate: string) => + Math.abs(parseDateValue(endDate).getTime() - parseDateValue(startDate).getTime()) / (24 * 60 * 60 * 1000); const OVERVIEW_WIDTH = 520; const OVERVIEW_HEIGHT = 220; const OVERVIEW_PADDING = { top: 20, right: 18, bottom: 36, left: 52 }; @@ -995,6 +1004,44 @@ function App() { [birdWeightAssessments, birds], ); + const weightDropAlerts = useMemo( + () => + birds + .map((bird) => { + const birdWeights = [...(allBirdWeights[bird.id] ?? [])].sort( + (firstEntry, secondEntry) => parseDateValue(firstEntry.recordedOn).getTime() - parseDateValue(secondEntry.recordedOn).getTime(), + ); + + if (birdWeights.length < 2) { + return null; + } + + const latestWeight = birdWeights[birdWeights.length - 1]; + const previousWeight = birdWeights[birdWeights.length - 2]; + + if (previousWeight.weightGrams <= 0 || daysBetweenDates(previousWeight.recordedOn, latestWeight.recordedOn) > 2) { + return null; + } + + const dropPercent = ((previousWeight.weightGrams - latestWeight.weightGrams) / previousWeight.weightGrams) * 100; + + if (dropPercent < 5 || dropPercent > 10) { + return null; + } + + return { + bird, + previousWeight, + latestWeight, + dropPercent, + }; + }) + .filter((alert): alert is WeightDropAlert => alert !== null), + [allBirdWeights, birds], + ); + + const totalWeightAlerts = outOfRangeBirds.length + weightDropAlerts.length; + const filteredSpeciesOptions = useMemo(() => { const query = birdForm.species.trim().toLowerCase(); @@ -2531,7 +2578,7 @@ function App() { }; const handleWeightRangeAlertClick = () => { - if (!outOfRangeBirds.length) { + if (!totalWeightAlerts) { return; } setShowWeightAlertModal(true); @@ -2719,9 +2766,9 @@ function App() {

30-day flock weight snapshot

- {outOfRangeBirds.length ? ( + {totalWeightAlerts ? ( ) : null}

{birdsWithRecentWeights.length} birds with recent entries

@@ -2799,12 +2846,22 @@ function App() { Members still needing a first weight ) : null} - {outOfRangeBirds.length ? ( + {totalWeightAlerts ? (
- Weight range alerts + Weight alerts - {outOfRangeBirds.length} bird{outOfRangeBirds.length === 1 ? '' : 's'} outside typical ranges + {totalWeightAlerts} alert{totalWeightAlerts === 1 ? '' : 's'} need review + {outOfRangeBirds.length ? ( + + {outOfRangeBirds.length} bird{outOfRangeBirds.length === 1 ? '' : 's'} outside typical ranges + + ) : null} + {weightDropAlerts.length ? ( + + {weightDropAlerts.length} bird{weightDropAlerts.length === 1 ? '' : 's'} down 5-10% between recent entries + + ) : null} @@ -4213,19 +4270,19 @@ function App() {

Weight alert

-

Birds outside typical chart ranges

+

Birds needing weight review

- These alerts use the BirdSupplies species chart as a general reference. If a reading is unexpected or concerning, please consult your - veterinarian. + Range alerts use the BirdSupplies species chart as a general reference. Drop alerts compare the two most recent recorded days and + flag a 5-10% decrease. If a reading is unexpected or concerning, please consult your veterinarian.

{outOfRangeBirds.map(({ bird, assessment }) => ( -
+
{bird.name} is {assessment.status === 'below' ? 'below' : 'above'} the typical range @@ -4235,6 +4292,15 @@ function App() {
))} + {weightDropAlerts.map(({ bird, previousWeight, latestWeight, dropPercent }) => ( +
+ {bird.name} is down {dropPercent.toFixed(1)}% between recent entries + + {formatWeight(previousWeight.weightGrams)} on {formatShortDate(previousWeight.recordedOn)} to{' '} + {formatWeight(latestWeight.weightGrams)} on {formatShortDate(latestWeight.recordedOn)} + +
+ ))}