5-10% alert added

This commit is contained in:
Corey Blais
2026-04-16 21:44:03 -04:00
parent 0f587d070a
commit 3e76360a65
+76 -10
View File
@@ -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() {
<h2>30-day flock weight snapshot</h2>
</div>
<div className="button-row overview-alert-actions">
{outOfRangeBirds.length ? (
{totalWeightAlerts ? (
<button className="range-alert-button" onClick={handleWeightRangeAlertClick} type="button">
{outOfRangeBirds.length} weight range alert{outOfRangeBirds.length === 1 ? '' : 's'}
{totalWeightAlerts} weight alert{totalWeightAlerts === 1 ? '' : 's'}
</button>
) : null}
<p className="muted">{birdsWithRecentWeights.length} birds with recent entries</p>
@@ -2799,12 +2846,22 @@ function App() {
<span>Members still needing a first weight</span>
</article>
) : null}
{outOfRangeBirds.length ? (
{totalWeightAlerts ? (
<article className="summary-card summary-alert-card">
<span>Weight range alerts</span>
<span>Weight alerts</span>
<strong>
{outOfRangeBirds.length} bird{outOfRangeBirds.length === 1 ? '' : 's'} outside typical ranges
{totalWeightAlerts} alert{totalWeightAlerts === 1 ? '' : 's'} need review
</strong>
{outOfRangeBirds.length ? (
<span>
{outOfRangeBirds.length} bird{outOfRangeBirds.length === 1 ? '' : 's'} outside typical ranges
</span>
) : null}
{weightDropAlerts.length ? (
<span>
{weightDropAlerts.length} bird{weightDropAlerts.length === 1 ? '' : 's'} down 5-10% between recent entries
</span>
) : null}
<button className="range-alert-button" onClick={handleWeightRangeAlertClick} type="button">
Review alerts
</button>
@@ -4213,19 +4270,19 @@ function App() {
<div className="panel-header">
<div>
<p className="eyebrow">Weight alert</p>
<h2 id="weight-alert-title">Birds outside typical chart ranges</h2>
<h2 id="weight-alert-title">Birds needing weight review</h2>
</div>
<button className="secondary-button" onClick={() => setShowWeightAlertModal(false)} type="button">
Close
</button>
</div>
<p className="muted">
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.
</p>
<div className="modal-alert-list">
{outOfRangeBirds.map(({ bird, assessment }) => (
<article key={bird.id} className="summary-card summary-alert-card">
<article key={`range-${bird.id}`} className="summary-card summary-alert-card">
<strong>
{bird.name} is {assessment.status === 'below' ? 'below' : 'above'} the typical range
</strong>
@@ -4235,6 +4292,15 @@ function App() {
</span>
</article>
))}
{weightDropAlerts.map(({ bird, previousWeight, latestWeight, dropPercent }) => (
<article key={`drop-${bird.id}`} className="summary-card summary-alert-card">
<strong>{bird.name} is down {dropPercent.toFixed(1)}% between recent entries</strong>
<span>
{formatWeight(previousWeight.weightGrams)} on {formatShortDate(previousWeight.recordedOn)} to{' '}
{formatWeight(latestWeight.weightGrams)} on {formatShortDate(latestWeight.recordedOn)}
</span>
</article>
))}
</div>
</section>
</div>