5-10% alert added
This commit is contained in:
+76
-10
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user