adding weight edits
This commit is contained in:
@@ -64,6 +64,7 @@ import {
|
|||||||
updateBird,
|
updateBird,
|
||||||
updateMemorialReminderPreference,
|
updateMemorialReminderPreference,
|
||||||
updateMedicationForBird,
|
updateMedicationForBird,
|
||||||
|
updateWeightForBird,
|
||||||
upsertMedicationAdministrationForBird,
|
upsertMedicationAdministrationForBird,
|
||||||
updateVetVisitForBird,
|
updateVetVisitForBird,
|
||||||
} from './repositories/birdRepository.js';
|
} from './repositories/birdRepository.js';
|
||||||
@@ -4150,6 +4151,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) => {
|
app.get('/api/birds/:birdId/vet-visits', requireAuth, async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const vetVisits = await listVetVisitsForBird(req.params.birdId, req.auth!.workspace.id);
|
const vetVisits = await listVetVisitsForBird(req.params.birdId, req.auth!.workspace.id);
|
||||||
|
|||||||
@@ -895,6 +895,27 @@ export const createWeightForBird = async (birdId: string, weightGrams: number, r
|
|||||||
return result.rows[0] ?? null;
|
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<WeightRow>(
|
||||||
|
`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) => {
|
export const listVetVisitsForBird = async (birdId: string, workspaceId: number) => {
|
||||||
const result = await db.query<VetVisitRow>(
|
const result = await db.query<VetVisitRow>(
|
||||||
`SELECT id, bird_id, visited_on::text, clinic_name, reason, notes
|
`SELECT id, bird_id, visited_on::text, clinic_name, reason, notes
|
||||||
|
|||||||
+58
-10
@@ -1629,6 +1629,7 @@ function App() {
|
|||||||
recordedOn: new Date().toISOString().slice(0, 10),
|
recordedOn: new Date().toISOString().slice(0, 10),
|
||||||
notes: '',
|
notes: '',
|
||||||
});
|
});
|
||||||
|
const [editingWeightId, setEditingWeightId] = useState('');
|
||||||
const [vetVisitForm, setVetVisitForm] = useState({
|
const [vetVisitForm, setVetVisitForm] = useState({
|
||||||
visitedOn: new Date().toISOString().slice(0, 10),
|
visitedOn: new Date().toISOString().slice(0, 10),
|
||||||
clinicName: '',
|
clinicName: '',
|
||||||
@@ -3629,8 +3630,9 @@ function App() {
|
|||||||
setError('');
|
setError('');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await apiFetch(`/birds/${selectedBird.id}/weights`, authToken, {
|
const isEditingWeight = Boolean(editingWeightId);
|
||||||
method: 'POST',
|
const response = await apiFetch(isEditingWeight ? `/birds/${selectedBird.id}/weights/${editingWeightId}` : `/birds/${selectedBird.id}/weights`, authToken, {
|
||||||
|
method: isEditingWeight ? 'PUT' : 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
weightGrams: Number(weightForm.weightGrams),
|
weightGrams: Number(weightForm.weightGrams),
|
||||||
@@ -3647,7 +3649,13 @@ function App() {
|
|||||||
if (!data?.weight) {
|
if (!data?.weight) {
|
||||||
throw new Error('Unable to save 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<WeightRecord | null>(
|
||||||
|
(latest, weight) => (!latest || weight.recordedOn >= latest.recordedOn ? weight : latest),
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
setWeights(nextWeights);
|
setWeights(nextWeights);
|
||||||
setAllBirdWeights((current) => ({
|
setAllBirdWeights((current) => ({
|
||||||
@@ -3663,18 +3671,34 @@ function App() {
|
|||||||
bird.id === selectedBird.id
|
bird.id === selectedBird.id
|
||||||
? {
|
? {
|
||||||
...bird,
|
...bird,
|
||||||
latestWeightGrams: data.weight.weightGrams,
|
latestWeightGrams: latestWeight?.weightGrams ?? null,
|
||||||
latestRecordedOn: data.weight.recordedOn,
|
latestRecordedOn: latestWeight?.recordedOn ?? null,
|
||||||
}
|
}
|
||||||
: bird,
|
: bird,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
setWeightForm({ weightGrams: '', recordedOn: new Date().toISOString().slice(0, 10), notes: '' });
|
setWeightForm({ weightGrams: '', recordedOn: new Date().toISOString().slice(0, 10), notes: '' });
|
||||||
|
setEditingWeightId('');
|
||||||
} catch (submitError) {
|
} catch (submitError) {
|
||||||
setError(submitError instanceof Error ? submitError.message : 'Unable to save weight.');
|
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) => {
|
const handleBulkWeightValueChange = (birdId: string, weightGrams: string) => {
|
||||||
setBulkWeightRows((current) => ({
|
setBulkWeightRows((current) => ({
|
||||||
...current,
|
...current,
|
||||||
@@ -7045,11 +7069,35 @@ function App() {
|
|||||||
placeholder="Optional notes about appetite, molt, meds, or behavior"
|
placeholder="Optional notes about appetite, molt, meds, or behavior"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<button className="primary-button" type="submit">
|
<div className="button-row care-form-actions">
|
||||||
Save weight
|
<button className="primary-button" type="submit">
|
||||||
</button>
|
{editingWeightId ? 'Save weight changes' : 'Save weight'}
|
||||||
</form>
|
</button>
|
||||||
</section>
|
{editingWeightId ? (
|
||||||
|
<button className="secondary-button" onClick={handleCancelWeightEdit} type="button">
|
||||||
|
Cancel edit
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div className="recent-list">
|
||||||
|
{[...weights]
|
||||||
|
.sort((left, right) => right.recordedOn.localeCompare(left.recordedOn))
|
||||||
|
.map((weight) => (
|
||||||
|
<article className="vet-visit-card" key={weight.id}>
|
||||||
|
<strong>{formatWeight(weight.weightGrams)}</strong>
|
||||||
|
<span>{formatDate(weight.recordedOn)}</span>
|
||||||
|
<small>{weight.notes || 'No notes recorded.'}</small>
|
||||||
|
<div className="button-row">
|
||||||
|
<button className="secondary-button" onClick={() => handleEditWeight(weight)} type="button">
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
{!weights.length ? <p className="muted">No weight entries recorded yet.</p> : null}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user