From f65a4bed2428056b107c039ff2ce468afb3be769 Mon Sep 17 00:00:00 2001 From: Corey Blais Date: Tue, 16 Jun 2026 09:31:16 -0400 Subject: [PATCH] adding weight edits --- backend/src/app.ts | 56 ++++++++++++++++++ backend/src/repositories/birdRepository.ts | 21 +++++++ frontend/src/App.tsx | 68 ++++++++++++++++++---- 3 files changed, 135 insertions(+), 10 deletions(-) diff --git a/backend/src/app.ts b/backend/src/app.ts index a52a689..468de15 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -66,6 +66,7 @@ import { updateBird, updateMemorialReminderPreference, updateMedicationForBird, + updateWeightForBird, upsertMedicationAdministrationForBird, updateVetVisitForBird, } from './repositories/birdRepository.js'; @@ -4068,6 +4069,61 @@ app.post('/api/birds/:birdId/weights', requireAuth, requireWriteAccess, requireW } }); +app.put( + '/api/birds/:birdId/weights/:weightId', + requireAuth, + requireWriteAccess, + requireWorkspaceRole(['owner', 'assistant', 'caregiver']), + async (req: Request, res: Response, next: NextFunction) => { + const parsed = weightSchema.safeParse(req.body); + + if (!parsed.success) { + res.status(400).json({ error: 'Invalid weight payload', details: parsed.error.flatten() }); + return; + } + + try { + const bird = await getBirdById(req.params.birdId, req.auth!.workspace.id); + + if (!bird) { + res.status(404).json({ error: 'Bird not found.' }); + return; + } + + if (!ensureBirdWritable(bird, res)) { + return; + } + + const weight = await updateWeightForBird( + req.params.weightId, + req.params.birdId, + parsed.data.weightGrams, + parsed.data.recordedOn, + emptyToNull(parsed.data.notes), + ); + + if (!weight) { + res.status(404).json({ error: 'Weight entry not found.' }); + return; + } + + await writeAuditLog(req.auth!, 'weight.updated', 'weight', weight.id, bird.name, { + birdId: bird.id, + weightGrams: parsed.data.weightGrams, + recordedOn: parsed.data.recordedOn, + }); + res.json({ weight: normalizeWeight(weight) }); + } catch (error) { + if (typeof error === 'object' && error && 'code' in error && error.code === '23505') { + res.status(409).json({ error: 'A weight entry already exists for that bird on that date.' }); + return; + } + + next(error); + } + }, +); + app.get('/api/birds/:birdId/vet-visits', requireAuth, async (req: Request, res: Response, next: NextFunction) => { try { const vetVisits = await listVetVisitsForBird(req.params.birdId, req.auth!.workspace.id); diff --git a/backend/src/repositories/birdRepository.ts b/backend/src/repositories/birdRepository.ts index 23e602a..991975d 100644 --- a/backend/src/repositories/birdRepository.ts +++ b/backend/src/repositories/birdRepository.ts @@ -911,6 +911,27 @@ export const createWeightForBird = async (birdId: string, weightGrams: number, r return result.rows[0] ?? null; }; +export const updateWeightForBird = async ( + weightId: string, + birdId: string, + weightGrams: number, + recordedOn: string, + notes: string | null, +) => { + const result = await db.query( + `UPDATE weight_records + SET weight_grams = $3, + recorded_on = $4, + notes = $5 + WHERE id = $1 + AND bird_id = $2 + RETURNING id, bird_id, weight_grams, recorded_on::text, notes`, + [weightId, birdId, weightGrams, recordedOn, notes], + ); + + return result.rows[0] ?? null; +}; + export const listVetVisitsForBird = async (birdId: string, workspaceId: number) => { const result = await db.query( `SELECT id, bird_id, visited_on::text, clinic_name, reason, notes diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6ee2339..6435066 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1574,6 +1574,7 @@ function App() { recordedOn: new Date().toISOString().slice(0, 10), notes: '', }); + const [editingWeightId, setEditingWeightId] = useState(''); const [vetVisitForm, setVetVisitForm] = useState({ visitedOn: new Date().toISOString().slice(0, 10), clinicName: '', @@ -3355,8 +3356,9 @@ function App() { setError(''); try { - const response = await apiFetch(`/birds/${selectedBird.id}/weights`, authToken, { - method: 'POST', + const isEditingWeight = Boolean(editingWeightId); + const response = await apiFetch(isEditingWeight ? `/birds/${selectedBird.id}/weights/${editingWeightId}` : `/birds/${selectedBird.id}/weights`, authToken, { + method: isEditingWeight ? 'PUT' : 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ weightGrams: Number(weightForm.weightGrams), @@ -3373,7 +3375,13 @@ function App() { if (!data?.weight) { throw new Error('Unable to save weight.'); } - const nextWeights = [...weights, data.weight].sort((left, right) => left.recordedOn.localeCompare(right.recordedOn)); + const nextWeights = ( + isEditingWeight ? weights.map((weight) => (weight.id === data.weight.id ? data.weight : weight)) : [...weights, data.weight] + ).sort((left, right) => left.recordedOn.localeCompare(right.recordedOn)); + const latestWeight = nextWeights.reduce( + (latest, weight) => (!latest || weight.recordedOn >= latest.recordedOn ? weight : latest), + null, + ); setWeights(nextWeights); setAllBirdWeights((current) => ({ @@ -3389,18 +3397,34 @@ function App() { bird.id === selectedBird.id ? { ...bird, - latestWeightGrams: data.weight.weightGrams, - latestRecordedOn: data.weight.recordedOn, + latestWeightGrams: latestWeight?.weightGrams ?? null, + latestRecordedOn: latestWeight?.recordedOn ?? null, } : bird, ), ); setWeightForm({ weightGrams: '', recordedOn: new Date().toISOString().slice(0, 10), notes: '' }); + setEditingWeightId(''); } catch (submitError) { setError(submitError instanceof Error ? submitError.message : 'Unable to save weight.'); } }; + const handleEditWeight = (weight: WeightRecord) => { + setEditingWeightId(weight.id); + setWeightForm({ + weightGrams: String(weight.weightGrams), + recordedOn: weight.recordedOn, + notes: weight.notes ?? '', + }); + setError(''); + }; + + const handleCancelWeightEdit = () => { + setEditingWeightId(''); + setWeightForm({ weightGrams: '', recordedOn: new Date().toISOString().slice(0, 10), notes: '' }); + }; + const handleBulkWeightValueChange = (birdId: string, weightGrams: string) => { setBulkWeightRows((current) => ({ ...current, @@ -6541,11 +6565,35 @@ function App() { placeholder="Optional notes about appetite, molt, meds, or behavior" /> - - - +
+ + {editingWeightId ? ( + + ) : null} +
+ +
+ {[...weights] + .sort((left, right) => right.recordedOn.localeCompare(left.recordedOn)) + .map((weight) => ( +
+ {formatWeight(weight.weightGrams)} + {formatDate(weight.recordedOn)} + {weight.notes || 'No notes recorded.'} +
+ +
+
+ ))} + {!weights.length ?

No weight entries recorded yet.

: null} +
+ ) : null}