176 lines
6.5 KiB
React
176 lines
6.5 KiB
React
import React, { useEffect, useState } from 'react';
|
|
import { useParams, useNavigate, Link } from 'react-router-dom';
|
|
import { animalsApi, dailyStatusesApi } from '../api/client';
|
|
import { format } from 'date-fns';
|
|
import Button from '../components/ui/Button';
|
|
import Modal from '../components/ui/Modal';
|
|
import ConfirmDialog from '../components/ui/ConfirmDialog';
|
|
import DailyStatusForm from '../components/DailyStatusForm';
|
|
import Alert from '../components/ui/Alert';
|
|
import AuditLogSection from '../components/AuditLogSection';
|
|
|
|
export default function AnimalDetail() {
|
|
const { id } = useParams();
|
|
const navigate = useNavigate();
|
|
|
|
const [animal, setAnimal] = useState(null);
|
|
const [statuses, setStatuses] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState(null);
|
|
const [successMsg, setSuccessMsg] = useState(null);
|
|
|
|
const [showAddStatus, setShowAddStatus] = useState(false);
|
|
const [editStatus, setEditStatus] = useState(null);
|
|
const [deleteStatus, setDeleteStatus] = useState(null);
|
|
const [deleteLoading, setDeleteLoading] = useState(false);
|
|
|
|
async function load() {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const [animalData, statusData] = await Promise.all([
|
|
animalsApi.get(id),
|
|
dailyStatusesApi.list(id),
|
|
]);
|
|
setAnimal(animalData);
|
|
setStatuses(statusData);
|
|
} catch (err) {
|
|
setError(err.message);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
useEffect(() => { load(); }, [id]);
|
|
|
|
function flash(msg) {
|
|
setSuccessMsg(msg);
|
|
setTimeout(() => setSuccessMsg(null), 3000);
|
|
}
|
|
|
|
function handleStatusSaved() {
|
|
setShowAddStatus(false);
|
|
setEditStatus(null);
|
|
load();
|
|
flash(editStatus ? 'Daily status updated.' : 'Daily status logged.');
|
|
}
|
|
|
|
async function handleDeleteStatus() {
|
|
setDeleteLoading(true);
|
|
try {
|
|
await dailyStatusesApi.delete(deleteStatus.id);
|
|
setDeleteStatus(null);
|
|
load();
|
|
flash('Daily status removed.');
|
|
} catch (err) {
|
|
setError(err.message);
|
|
setDeleteStatus(null);
|
|
} finally {
|
|
setDeleteLoading(false);
|
|
}
|
|
}
|
|
|
|
if (loading) return <div className="max-w-5xl mx-auto px-4 py-8 text-gray-400">Loading…</div>;
|
|
if (error) return (
|
|
<div className="max-w-5xl mx-auto px-4 py-8">
|
|
<Alert type="error" message={error} />
|
|
<Button variant="secondary" className="mt-4" onClick={() => navigate(-1)}>← Back</Button>
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<div className="max-w-5xl mx-auto px-4 py-8">
|
|
{/* Breadcrumb */}
|
|
<nav className="text-sm text-gray-500 mb-4">
|
|
<Link to="/" className="hover:text-blue-600">Experiments</Link>
|
|
<span className="mx-2">/</span>
|
|
<Link to={`/experiments/${animal.experiment_id}`} className="hover:text-blue-600">
|
|
Experiment
|
|
</Link>
|
|
<span className="mx-2">/</span>
|
|
<span className="text-gray-900 font-medium">{animal.animal_name}</span>
|
|
</nav>
|
|
|
|
<div className="flex items-start justify-between mb-6">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-gray-900">{animal.animal_name}</h1>
|
|
<p className="text-sm text-gray-500 mt-0.5">
|
|
ID: <span className="font-mono">{animal.animal_id_string}</span>
|
|
{' · '}
|
|
{statuses.length} daily status {statuses.length !== 1 ? 'entries' : 'entry'}
|
|
</p>
|
|
</div>
|
|
<Button onClick={() => setShowAddStatus(true)}>+ Log Daily Status</Button>
|
|
</div>
|
|
|
|
{successMsg && <Alert type="success" message={successMsg} />}
|
|
{error && <Alert type="error" message={error} onDismiss={() => setError(null)} />}
|
|
|
|
{statuses.length === 0 ? (
|
|
<div className="text-center py-16 border-2 border-dashed border-gray-200 rounded-xl">
|
|
<p className="text-gray-400 mb-3">No daily statuses logged yet.</p>
|
|
<Button onClick={() => setShowAddStatus(true)}>+ Log First Status</Button>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{statuses.map((s) => (
|
|
<div key={s.id} className="bg-white border border-gray-200 rounded-xl p-5">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<span className="font-semibold text-gray-900">
|
|
{format(new Date(s.date), 'MMMM d, yyyy')}
|
|
</span>
|
|
<div className="flex gap-2">
|
|
<Button size="sm" variant="secondary" onClick={() => setEditStatus(s)}>Edit</Button>
|
|
<Button size="sm" variant="danger" onClick={() => setDeleteStatus(s)}>Delete</Button>
|
|
</div>
|
|
</div>
|
|
<dl className="grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-2 text-sm">
|
|
{[
|
|
['Experiment Description', s.experiment_description],
|
|
['Vitals', s.vitals],
|
|
['Treatment', s.treatment],
|
|
['Notes', s.notes],
|
|
]
|
|
.filter(([, v]) => v)
|
|
.map(([label, value]) => (
|
|
<div key={label} className="flex flex-col">
|
|
<dt className="text-xs font-medium text-gray-500 uppercase tracking-wide">{label}</dt>
|
|
<dd className="text-gray-800 mt-0.5 whitespace-pre-wrap">{value}</dd>
|
|
</div>
|
|
))}
|
|
</dl>
|
|
{!s.experiment_description && !s.vitals && !s.treatment && !s.notes && (
|
|
<p className="text-sm text-gray-400 italic">No details recorded for this date.</p>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
<AuditLogSection tableName="daily_statuses" recordId={undefined} />
|
|
|
|
<Modal isOpen={showAddStatus} onClose={() => setShowAddStatus(false)} title="Log Daily Status" size="lg">
|
|
<DailyStatusForm animalId={id} onSuccess={handleStatusSaved} onCancel={() => setShowAddStatus(false)} />
|
|
</Modal>
|
|
|
|
<Modal isOpen={!!editStatus} onClose={() => setEditStatus(null)} title="Edit Daily Status" size="lg">
|
|
<DailyStatusForm
|
|
existing={editStatus}
|
|
animalId={id}
|
|
onSuccess={handleStatusSaved}
|
|
onCancel={() => setEditStatus(null)}
|
|
/>
|
|
</Modal>
|
|
|
|
<ConfirmDialog
|
|
isOpen={!!deleteStatus}
|
|
onClose={() => setDeleteStatus(null)}
|
|
onConfirm={handleDeleteStatus}
|
|
loading={deleteLoading}
|
|
title="Delete Daily Status"
|
|
message={`Delete the status entry for ${deleteStatus ? format(new Date(deleteStatus.date), 'MMMM d, yyyy') : ''}?`}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|