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; varianceGrams: number;
}; };
type WeightDropAlert = {
bird: Bird;
previousWeight: WeightRecord;
latestWeight: WeightRecord;
dropPercent: number;
};
type PhotoCropState = { type PhotoCropState = {
sourceDataUrl: string; sourceDataUrl: string;
fileName: string; fileName: string;
@@ -405,6 +412,8 @@ const formatDateTime = (value: string | null) => {
const formatWeight = (value: number | null) => (value ? `${value.toFixed(1)} g` : 'Pending'); 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 formatRange = (minGrams: number, maxGrams: number) => `${minGrams.toFixed(0)}-${maxGrams.toFixed(0)} g`;
const parseDateValue = (value: string) => new Date(`${value}T00:00:00`); 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_WIDTH = 520;
const OVERVIEW_HEIGHT = 220; const OVERVIEW_HEIGHT = 220;
const OVERVIEW_PADDING = { top: 20, right: 18, bottom: 36, left: 52 }; const OVERVIEW_PADDING = { top: 20, right: 18, bottom: 36, left: 52 };
@@ -995,6 +1004,44 @@ function App() {
[birdWeightAssessments, birds], [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 filteredSpeciesOptions = useMemo(() => {
const query = birdForm.species.trim().toLowerCase(); const query = birdForm.species.trim().toLowerCase();
@@ -2531,7 +2578,7 @@ function App() {
}; };
const handleWeightRangeAlertClick = () => { const handleWeightRangeAlertClick = () => {
if (!outOfRangeBirds.length) { if (!totalWeightAlerts) {
return; return;
} }
setShowWeightAlertModal(true); setShowWeightAlertModal(true);
@@ -2719,9 +2766,9 @@ function App() {
<h2>30-day flock weight snapshot</h2> <h2>30-day flock weight snapshot</h2>
</div> </div>
<div className="button-row overview-alert-actions"> <div className="button-row overview-alert-actions">
{outOfRangeBirds.length ? ( {totalWeightAlerts ? (
<button className="range-alert-button" onClick={handleWeightRangeAlertClick} type="button"> <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> </button>
) : null} ) : null}
<p className="muted">{birdsWithRecentWeights.length} birds with recent entries</p> <p className="muted">{birdsWithRecentWeights.length} birds with recent entries</p>
@@ -2799,12 +2846,22 @@ function App() {
<span>Members still needing a first weight</span> <span>Members still needing a first weight</span>
</article> </article>
) : null} ) : null}
{outOfRangeBirds.length ? ( {totalWeightAlerts ? (
<article className="summary-card summary-alert-card"> <article className="summary-card summary-alert-card">
<span>Weight range alerts</span> <span>Weight alerts</span>
<strong> <strong>
{outOfRangeBirds.length} bird{outOfRangeBirds.length === 1 ? '' : 's'} outside typical ranges {totalWeightAlerts} alert{totalWeightAlerts === 1 ? '' : 's'} need review
</strong> </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"> <button className="range-alert-button" onClick={handleWeightRangeAlertClick} type="button">
Review alerts Review alerts
</button> </button>
@@ -4213,19 +4270,19 @@ function App() {
<div className="panel-header"> <div className="panel-header">
<div> <div>
<p className="eyebrow">Weight alert</p> <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> </div>
<button className="secondary-button" onClick={() => setShowWeightAlertModal(false)} type="button"> <button className="secondary-button" onClick={() => setShowWeightAlertModal(false)} type="button">
Close Close
</button> </button>
</div> </div>
<p className="muted"> <p className="muted">
These alerts use the BirdSupplies species chart as a general reference. If a reading is unexpected or concerning, please consult your Range alerts use the BirdSupplies species chart as a general reference. Drop alerts compare the two most recent recorded days and
veterinarian. flag a 5-10% decrease. If a reading is unexpected or concerning, please consult your veterinarian.
</p> </p>
<div className="modal-alert-list"> <div className="modal-alert-list">
{outOfRangeBirds.map(({ bird, assessment }) => ( {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> <strong>
{bird.name} is {assessment.status === 'below' ? 'below' : 'above'} the typical range {bird.name} is {assessment.status === 'below' ? 'below' : 'above'} the typical range
</strong> </strong>
@@ -4235,6 +4292,15 @@ function App() {
</span> </span>
</article> </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> </div>
</section> </section>
</div> </div>