328 lines
12 KiB
TypeScript
Executable File
328 lines
12 KiB
TypeScript
Executable File
import { useState, useEffect } from 'react'
|
|
import { experimentManagement, repetitionManagement, userManagement, type Experiment, type ExperimentRepetition, type User } from '../lib/supabase'
|
|
import { RepetitionDataEntryInterface } from './RepetitionDataEntryInterface'
|
|
|
|
export function DataEntry() {
|
|
const [experiments, setExperiments] = useState<Experiment[]>([])
|
|
const [experimentRepetitions, setExperimentRepetitions] = useState<Record<string, ExperimentRepetition[]>>({})
|
|
const [selectedRepetition, setSelectedRepetition] = useState<{ experiment: Experiment; repetition: ExperimentRepetition } | null>(null)
|
|
const [currentUser, setCurrentUser] = useState<User | null>(null)
|
|
const [loading, setLoading] = useState(true)
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
useEffect(() => {
|
|
loadData()
|
|
}, [])
|
|
|
|
const loadData = async () => {
|
|
try {
|
|
setLoading(true)
|
|
setError(null)
|
|
|
|
const [experimentsData, userData] = await Promise.all([
|
|
experimentManagement.getAllExperiments(),
|
|
userManagement.getCurrentUser()
|
|
])
|
|
|
|
setExperiments(experimentsData)
|
|
setCurrentUser(userData)
|
|
|
|
// Load repetitions for each experiment
|
|
const repetitionsMap: Record<string, ExperimentRepetition[]> = {}
|
|
for (const experiment of experimentsData) {
|
|
try {
|
|
const repetitions = await repetitionManagement.getExperimentRepetitions(experiment.id)
|
|
repetitionsMap[experiment.id] = repetitions
|
|
} catch (err) {
|
|
console.error(`Failed to load repetitions for experiment ${experiment.id}:`, err)
|
|
repetitionsMap[experiment.id] = []
|
|
}
|
|
}
|
|
setExperimentRepetitions(repetitionsMap)
|
|
} catch (err: any) {
|
|
setError(err.message || 'Failed to load data')
|
|
console.error('Load data error:', err)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const handleRepetitionSelect = (experiment: Experiment, repetition: ExperimentRepetition) => {
|
|
setSelectedRepetition({ experiment, repetition })
|
|
}
|
|
|
|
const handleBackToList = () => {
|
|
setSelectedRepetition(null)
|
|
}
|
|
|
|
const getAllRepetitionsWithExperiments = () => {
|
|
const allRepetitions: Array<{ experiment: Experiment; repetition: ExperimentRepetition }> = []
|
|
|
|
experiments.forEach(experiment => {
|
|
const repetitions = experimentRepetitions[experiment.id] || []
|
|
repetitions.forEach(repetition => {
|
|
allRepetitions.push({ experiment, repetition })
|
|
})
|
|
})
|
|
|
|
return allRepetitions
|
|
}
|
|
|
|
const categorizeRepetitions = () => {
|
|
const allRepetitions = getAllRepetitionsWithExperiments()
|
|
const now = new Date()
|
|
|
|
const past = allRepetitions.filter(({ repetition }) =>
|
|
repetition.completion_status || (repetition.scheduled_date && new Date(repetition.scheduled_date) < now)
|
|
)
|
|
|
|
const inProgress = allRepetitions.filter(({ repetition }) =>
|
|
!repetition.completion_status &&
|
|
repetition.scheduled_date &&
|
|
new Date(repetition.scheduled_date) <= now &&
|
|
new Date(repetition.scheduled_date) > new Date(now.getTime() - 24 * 60 * 60 * 1000)
|
|
)
|
|
|
|
const upcoming = allRepetitions.filter(({ repetition }) =>
|
|
!repetition.completion_status &&
|
|
repetition.scheduled_date &&
|
|
new Date(repetition.scheduled_date) > now
|
|
)
|
|
|
|
return { past, inProgress, upcoming }
|
|
}
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="p-6">
|
|
<div className="flex items-center justify-center h-64">
|
|
<div className="text-center">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
|
|
<p className="mt-4 text-gray-600">Loading experiments...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="p-6">
|
|
<div className="bg-red-50 border border-red-200 rounded-md p-4">
|
|
<div className="text-sm text-red-700">{error}</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (selectedRepetition) {
|
|
return (
|
|
<RepetitionDataEntryInterface
|
|
experiment={selectedRepetition.experiment}
|
|
repetition={selectedRepetition.repetition}
|
|
currentUser={currentUser!}
|
|
onBack={handleBackToList}
|
|
/>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="p-6">
|
|
<div className="mb-6">
|
|
<h1 className="text-2xl font-bold text-gray-900">Data Entry</h1>
|
|
<p className="mt-1 text-sm text-gray-500">
|
|
Select a repetition to enter measurement data
|
|
</p>
|
|
</div>
|
|
|
|
{/* Repetitions organized by status - flat list */}
|
|
{(() => {
|
|
const { past: pastRepetitions, inProgress: inProgressRepetitions, upcoming: upcomingRepetitions } = categorizeRepetitions()
|
|
|
|
return (
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
|
{/* Past/Completed Repetitions */}
|
|
<div className="bg-white shadow overflow-hidden sm:rounded-lg">
|
|
<div className="px-4 py-5 sm:px-6 border-b border-gray-200">
|
|
<h2 className="text-xl font-bold text-gray-900 flex items-center">
|
|
<span className="w-4 h-4 bg-green-500 rounded-full mr-3"></span>
|
|
Past/Completed ({pastRepetitions.length})
|
|
</h2>
|
|
<p className="mt-1 text-sm text-gray-500">
|
|
Completed or past scheduled repetitions
|
|
</p>
|
|
</div>
|
|
<div className="p-4">
|
|
<div className="space-y-3 max-h-96 overflow-y-auto">
|
|
{pastRepetitions.map(({ experiment, repetition }) => (
|
|
<RepetitionCard
|
|
key={repetition.id}
|
|
experiment={experiment}
|
|
repetition={repetition}
|
|
onSelect={handleRepetitionSelect}
|
|
status="past"
|
|
/>
|
|
))}
|
|
{pastRepetitions.length === 0 && (
|
|
<p className="text-sm text-gray-500 italic text-center py-8">
|
|
No completed repetitions
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* In Progress Repetitions */}
|
|
<div className="bg-white shadow overflow-hidden sm:rounded-lg">
|
|
<div className="px-4 py-5 sm:px-6 border-b border-gray-200">
|
|
<h2 className="text-xl font-bold text-gray-900 flex items-center">
|
|
<span className="w-4 h-4 bg-blue-500 rounded-full mr-3"></span>
|
|
In Progress ({inProgressRepetitions.length})
|
|
</h2>
|
|
<p className="mt-1 text-sm text-gray-500">
|
|
Currently scheduled or active repetitions
|
|
</p>
|
|
</div>
|
|
<div className="p-4">
|
|
<div className="space-y-3 max-h-96 overflow-y-auto">
|
|
{inProgressRepetitions.map(({ experiment, repetition }) => (
|
|
<RepetitionCard
|
|
key={repetition.id}
|
|
experiment={experiment}
|
|
repetition={repetition}
|
|
onSelect={handleRepetitionSelect}
|
|
status="in-progress"
|
|
/>
|
|
))}
|
|
{inProgressRepetitions.length === 0 && (
|
|
<p className="text-sm text-gray-500 italic text-center py-8">
|
|
No repetitions in progress
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Upcoming Repetitions */}
|
|
<div className="bg-white shadow overflow-hidden sm:rounded-lg">
|
|
<div className="px-4 py-5 sm:px-6 border-b border-gray-200">
|
|
<h2 className="text-xl font-bold text-gray-900 flex items-center">
|
|
<span className="w-4 h-4 bg-yellow-500 rounded-full mr-3"></span>
|
|
Upcoming ({upcomingRepetitions.length})
|
|
</h2>
|
|
<p className="mt-1 text-sm text-gray-500">
|
|
Future scheduled repetitions
|
|
</p>
|
|
</div>
|
|
<div className="p-4">
|
|
<div className="space-y-3 max-h-96 overflow-y-auto">
|
|
{upcomingRepetitions.map(({ experiment, repetition }) => (
|
|
<RepetitionCard
|
|
key={repetition.id}
|
|
experiment={experiment}
|
|
repetition={repetition}
|
|
onSelect={handleRepetitionSelect}
|
|
status="upcoming"
|
|
/>
|
|
))}
|
|
{upcomingRepetitions.length === 0 && (
|
|
<p className="text-sm text-gray-500 italic text-center py-8">
|
|
No upcoming repetitions
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
})()}
|
|
|
|
{experiments.length === 0 && (
|
|
<div className="text-center py-12">
|
|
<div className="text-gray-500">
|
|
No experiments available for data entry
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// RepetitionCard component for displaying individual repetitions
|
|
interface RepetitionCardProps {
|
|
experiment: Experiment
|
|
repetition: ExperimentRepetition
|
|
onSelect: (experiment: Experiment, repetition: ExperimentRepetition) => void
|
|
status: 'past' | 'in-progress' | 'upcoming'
|
|
}
|
|
|
|
function RepetitionCard({ experiment, repetition, onSelect, status }: RepetitionCardProps) {
|
|
const getStatusColor = () => {
|
|
switch (status) {
|
|
case 'past':
|
|
return 'border-green-200 bg-green-50 hover:bg-green-100'
|
|
case 'in-progress':
|
|
return 'border-blue-200 bg-blue-50 hover:bg-blue-100'
|
|
case 'upcoming':
|
|
return 'border-yellow-200 bg-yellow-50 hover:bg-yellow-100'
|
|
default:
|
|
return 'border-gray-200 bg-gray-50 hover:bg-gray-100'
|
|
}
|
|
}
|
|
|
|
const getStatusIcon = () => {
|
|
switch (status) {
|
|
case 'past':
|
|
return '✓'
|
|
case 'in-progress':
|
|
return '▶'
|
|
case 'upcoming':
|
|
return '⏰'
|
|
default:
|
|
return '○'
|
|
}
|
|
}
|
|
|
|
return (
|
|
<button
|
|
onClick={() => onSelect(experiment, repetition)}
|
|
className={`w-full text-left p-4 border-2 rounded-lg hover:shadow-lg transition-all duration-200 ${getStatusColor()}`}
|
|
>
|
|
<div className="flex items-center justify-between mb-3">
|
|
<div className="flex items-center space-x-3">
|
|
{/* Large, bold experiment number */}
|
|
<span className="text-2xl font-bold text-gray-900">
|
|
#{experiment.experiment_number}
|
|
</span>
|
|
{/* Smaller repetition number */}
|
|
<span className="text-lg font-semibold text-gray-700">
|
|
Rep #{repetition.repetition_number}
|
|
</span>
|
|
<span className="text-lg">{getStatusIcon()}</span>
|
|
</div>
|
|
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${repetition.schedule_status === 'scheduled'
|
|
? 'bg-blue-100 text-blue-800'
|
|
: 'bg-yellow-100 text-yellow-800'
|
|
}`}>
|
|
{repetition.schedule_status === 'pending schedule' ? 'Pending' : repetition.schedule_status}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Experiment details */}
|
|
<div className="text-sm text-gray-600 mb-2">
|
|
{experiment.soaking_duration_hr}h soaking • {experiment.air_drying_time_min}min drying
|
|
</div>
|
|
|
|
{repetition.scheduled_date && (
|
|
<div className="text-sm text-gray-600 mb-2">
|
|
<strong>Scheduled:</strong> {new Date(repetition.scheduled_date).toLocaleString()}
|
|
</div>
|
|
)}
|
|
|
|
<div className="text-xs text-gray-500">
|
|
Click to enter data for this repetition
|
|
</div>
|
|
</button>
|
|
)
|
|
}
|