feat(template): per-experiment daily record template — rename/add/hide fields, custom_fields JSONB

This commit is contained in:
Experiments DB Dev
2026-04-15 13:56:17 -04:00
parent 80fb1c6e27
commit 86a56427b7
10 changed files with 570 additions and 189 deletions
+53 -10
View File
@@ -5,6 +5,7 @@ import Button from '../components/ui/Button';
import Modal from '../components/ui/Modal';
import ConfirmDialog from '../components/ui/ConfirmDialog';
import AnimalForm from '../components/AnimalForm';
import TemplateEditor from '../components/TemplateEditor';
import Alert from '../components/ui/Alert';
import AuditLogSection from '../components/AuditLogSection';
@@ -22,6 +23,7 @@ export default function ExperimentDetail() {
const [editAnimal, setEditAnimal] = useState(null);
const [deleteAnimal, setDeleteAnimal] = useState(null);
const [deleteLoading, setDeleteLoading] = useState(false);
const [showTemplate, setShowTemplate] = useState(false);
async function load() {
setLoading(true);
@@ -69,7 +71,16 @@ export default function ExperimentDetail() {
}
}
if (loading) return <div className="max-w-5xl mx-auto px-4 py-8 text-gray-400">Loading</div>;
function handleTemplateSaved(updatedTemplate) {
setShowTemplate(false);
if (updatedTemplate) {
// Update local experiment state so everything downstream re-renders
setExperiment((prev) => ({ ...prev, template: updatedTemplate }));
flash('Record template updated.');
}
}
if (loading) return <div className="max-w-5xl mx-auto px-4 py-8 text-gray-400" aria-live="polite" aria-busy="true">Loading</div>;
if (error) return (
<div className="max-w-5xl mx-auto px-4 py-8">
<Alert type="error" message={error} />
@@ -93,12 +104,44 @@ export default function ExperimentDetail() {
{animals.length} animal{animals.length !== 1 ? 's' : ''} enrolled
</p>
</div>
<Button onClick={() => setShowAddAnimal(true)}>+ Add Animal</Button>
<div className="flex gap-2">
<Button variant="secondary" onClick={() => setShowTemplate(true)}>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
Record Template
</Button>
<Button onClick={() => setShowAddAnimal(true)}>+ Add Animal</Button>
</div>
</div>
{successMsg && <Alert type="success" message={successMsg} />}
{error && <Alert type="error" message={error} onDismiss={() => setError(null)} />}
{/* Template summary strip */}
{experiment.template && experiment.template.length > 0 && (
<div className="mb-5 flex flex-wrap gap-1.5">
{experiment.template.map((f) => (
<span
key={f.key}
className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium border ${
f.active ? 'bg-blue-50 border-blue-200 text-blue-700' : 'bg-gray-100 border-gray-200 text-gray-400'
}`}
>
{f.active ? null : <span title="hidden"></span>}
{f.label}
</span>
))}
<button
onClick={() => setShowTemplate(true)}
className="text-xs text-gray-400 hover:text-blue-600 underline underline-offset-2 transition-colors"
>
edit
</button>
</div>
)}
{animals.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 animals enrolled in this experiment yet.</p>
@@ -110,7 +153,7 @@ export default function ExperimentDetail() {
<div
key={animal.id}
className="bg-white border border-gray-200 rounded-xl p-4 flex items-center justify-between hover:border-blue-300 hover:shadow-sm transition-all cursor-pointer group"
onClick={() => navigate(`/animals/${animal.id}`)}
onClick={() => navigate(`/animals/${animal.id}`, { state: { template: experiment.template } })}
>
<div>
<div className="font-semibold text-gray-900 group-hover:text-blue-700 transition-colors">
@@ -129,19 +172,19 @@ export default function ExperimentDetail() {
</div>
)}
<AuditLogSection tableName="animals" recordId={undefined} />
<AuditLogSection tableName="animals" />
{/* Template editor modal */}
<Modal isOpen={showTemplate} onClose={() => setShowTemplate(false)} title="Daily Record Template" size="lg">
<TemplateEditor experimentId={id} onClose={handleTemplateSaved} />
</Modal>
<Modal isOpen={showAddAnimal} onClose={() => setShowAddAnimal(false)} title="Add Animal">
<AnimalForm experimentId={id} onSuccess={handleAnimalSaved} onCancel={() => setShowAddAnimal(false)} />
</Modal>
<Modal isOpen={!!editAnimal} onClose={() => setEditAnimal(null)} title="Edit Animal">
<AnimalForm
existing={editAnimal}
experimentId={id}
onSuccess={handleAnimalSaved}
onCancel={() => setEditAnimal(null)}
/>
<AnimalForm existing={editAnimal} experimentId={id} onSuccess={handleAnimalSaved} onCancel={() => setEditAnimal(null)} />
</Modal>
<ConfirmDialog