163 lines
5.6 KiB
React
163 lines
5.6 KiB
React
import React, { useEffect, useState } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { experimentsApi } from '../api/client';
|
|
import Button from '../components/ui/Button';
|
|
import Modal from '../components/ui/Modal';
|
|
import ConfirmDialog from '../components/ui/ConfirmDialog';
|
|
import ExperimentForm from '../components/ExperimentForm';
|
|
import Alert from '../components/ui/Alert';
|
|
import { format } from 'date-fns';
|
|
import AuditLogSection from '../components/AuditLogSection';
|
|
|
|
export default function Dashboard() {
|
|
const navigate = useNavigate();
|
|
const [experiments, setExperiments] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState(null);
|
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
|
const [editTarget, setEditTarget] = useState(null);
|
|
const [deleteTarget, setDeleteTarget] = useState(null);
|
|
const [deleteLoading, setDeleteLoading] = useState(false);
|
|
const [successMsg, setSuccessMsg] = useState(null);
|
|
|
|
async function loadExperiments() {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const data = await experimentsApi.list();
|
|
setExperiments(data);
|
|
} catch (err) {
|
|
setError(err.message);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
useEffect(() => { loadExperiments(); }, []);
|
|
|
|
function flash(msg) {
|
|
setSuccessMsg(msg);
|
|
setTimeout(() => setSuccessMsg(null), 3000);
|
|
}
|
|
|
|
function handleCreated(exp) {
|
|
setShowCreateModal(false);
|
|
loadExperiments();
|
|
flash(`Experiment "${exp.title}" created.`);
|
|
}
|
|
|
|
function handleUpdated(exp) {
|
|
setEditTarget(null);
|
|
loadExperiments();
|
|
flash(`Experiment "${exp.title}" updated.`);
|
|
}
|
|
|
|
async function handleDelete() {
|
|
setDeleteLoading(true);
|
|
try {
|
|
await experimentsApi.delete(deleteTarget.id);
|
|
setDeleteTarget(null);
|
|
loadExperiments();
|
|
flash('Experiment deleted.');
|
|
} catch (err) {
|
|
setError(err.message);
|
|
setDeleteTarget(null);
|
|
} finally {
|
|
setDeleteLoading(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="max-w-5xl mx-auto px-4 py-8">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-gray-900">Experiments</h1>
|
|
<p className="text-sm text-gray-500 mt-0.5">Manage all animal experimental studies</p>
|
|
</div>
|
|
<Button onClick={() => setShowCreateModal(true)}>
|
|
+ New Experiment
|
|
</Button>
|
|
</div>
|
|
|
|
{successMsg && <Alert type="success" message={successMsg} className="mb-4" />}
|
|
{error && <Alert type="error" message={error} onDismiss={() => setError(null)} />}
|
|
|
|
{loading && (
|
|
<div className="text-center py-16 text-gray-400">Loading experiments…</div>
|
|
)}
|
|
|
|
{!loading && experiments.length === 0 && (
|
|
<div className="text-center py-20 border-2 border-dashed border-gray-200 rounded-xl">
|
|
<p className="text-gray-400 text-lg mb-2">No experiments yet</p>
|
|
<p className="text-gray-400 text-sm mb-4">Create your first experiment to get started.</p>
|
|
<Button onClick={() => setShowCreateModal(true)}>+ New Experiment</Button>
|
|
</div>
|
|
)}
|
|
|
|
{!loading && experiments.length > 0 && (
|
|
<div className="grid gap-4">
|
|
{experiments.map((exp) => (
|
|
<div
|
|
key={exp.id}
|
|
className="bg-white border border-gray-200 rounded-xl p-5 flex items-center justify-between hover:border-blue-300 hover:shadow-sm transition-all cursor-pointer group"
|
|
onClick={() => navigate(`/experiments/${exp.id}`)}
|
|
>
|
|
<div>
|
|
<h2 className="font-semibold text-gray-900 group-hover:text-blue-700 transition-colors">
|
|
{exp.title}
|
|
</h2>
|
|
<div className="text-sm text-gray-500 mt-0.5 flex gap-4">
|
|
<span>{exp._count?.animals ?? 0} animal{exp._count?.animals !== 1 ? 's' : ''}</span>
|
|
<span>Created {format(new Date(exp.created_at), 'MMM d, yyyy')}</span>
|
|
</div>
|
|
</div>
|
|
<div className="flex gap-2" onClick={(e) => e.stopPropagation()}>
|
|
<Button
|
|
size="sm"
|
|
variant="secondary"
|
|
onClick={() => setEditTarget(exp)}
|
|
>
|
|
Edit
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
variant="danger"
|
|
onClick={() => setDeleteTarget(exp)}
|
|
>
|
|
Delete
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
<AuditLogSection tableName="experiments" />
|
|
|
|
{/* Create modal */}
|
|
<Modal isOpen={showCreateModal} onClose={() => setShowCreateModal(false)} title="New Experiment">
|
|
<ExperimentForm onSuccess={handleCreated} onCancel={() => setShowCreateModal(false)} />
|
|
</Modal>
|
|
|
|
{/* Edit modal */}
|
|
<Modal isOpen={!!editTarget} onClose={() => setEditTarget(null)} title="Edit Experiment">
|
|
<ExperimentForm
|
|
existing={editTarget}
|
|
onSuccess={handleUpdated}
|
|
onCancel={() => setEditTarget(null)}
|
|
/>
|
|
</Modal>
|
|
|
|
{/* Delete confirmation */}
|
|
<ConfirmDialog
|
|
isOpen={!!deleteTarget}
|
|
onClose={() => setDeleteTarget(null)}
|
|
onConfirm={handleDelete}
|
|
loading={deleteLoading}
|
|
title="Delete Experiment"
|
|
message={`Are you sure you want to delete "${deleteTarget?.title}"? All associated animals and daily statuses will be permanently removed.`}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|