import React, { useState, useEffect, useMemo } from 'react'; import { format, startOfMonth, endOfMonth, eachDayOfInterval, getDay, addMonths, subMonths, } from 'date-fns'; import { ResponsiveContainer, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, } from 'recharts'; import { experimentsApi } from '../api/client'; import Modal from './ui/Modal'; import DailyStatusForm from './DailyStatusForm'; const BUILTIN_KEYS_SET = new Set(['experiment_description', 'vitals', 'treatment', 'notes']); function SubjectReadOnly({ status, template }) { const activeFields = template.filter((f) => f.active); function getVal(s, f) { if (f.builtin) return s[f.key]; return s.custom_fields?.[f.fieldId]; } const nonEmpty = activeFields.filter((f) => { const v = getVal(status, f); return v !== null && v !== undefined && String(v).trim() !== ''; }); if (nonEmpty.length === 0) return

No field values recorded.

; return (
{nonEmpty.map((f) => (
{f.label}:{' '} {String(getVal(status, f))}
))}
); } function DayPanel({ date, animals, allStatuses, template, experimentId, selectedField, fieldOptions, onStatusChange, onStatusCreate }) { const dateStr = format(date, 'yyyy-MM-dd'); const [editingAnimalId, setEditingAnimalId] = useState(null); const statusByAnimal = useMemo(() => { const map = {}; allStatuses.forEach((s) => { if (s.date.slice(0, 10) === dateStr) map[s.animal_id] = s; }); return map; }, [allStatuses, dateStr]); // Only show animals that have a status record for this day WITH a non-empty selected field const animalsWithData = animals.filter((a) => { const s = statusByAnimal[a.id]; if (!s) return false; if (!selectedField) return true; const isBuiltin = BUILTIN_KEYS_SET.has(selectedField); const val = isBuiltin ? s[selectedField] : s.custom_fields?.[selectedField]; return val !== null && val !== undefined && String(val).trim() !== ''; }); const fieldLabel = fieldOptions.find((f) => f.value === selectedField)?.label; // Bar chart: animals with analysis_summary on this day → Success/Failure/Total from that summary const barData = useMemo(() => { return animalsWithData .map((animal) => { const status = statusByAnimal[animal.id]; const summary = status?.analysis_summary; if (!summary || summary.total === 0) return null; return { name: animal.animal_name || animal.animal_id_string || animal.id.slice(0, 8), total: summary.total, success: summary.counts?.Success ?? 0, failure: summary.counts?.Failure ?? 0, }; }) .filter(Boolean); }, [animalsWithData, statusByAnimal]); return (
{animalsWithData.length === 0 && (

No data recorded for this day.

)} {/* Per-animal data for this day */} {animalsWithData.map((animal) => { const existing = statusByAnimal[animal.id]; const isEditing = editingAnimalId === animal.id; return (
{animal.animal_name ?? 'Unknown subject'} {animal.animal_id_string && ( · {animal.animal_id_string} )}
{!isEditing && ( )}
{isEditing ? ( { onStatusChange(saved); setEditingAnimalId(null); }} onCancel={() => setEditingAnimalId(null)} /> ) : ( )}
); })} {/* Bar chart: overall per-subject completeness */} {barData.length > 0 && (

Session metrics

)}
); } export default function ExperimentCalendar({ experimentId, template, allStatuses, animals, onStatusChange, onStatusCreate }) { const [currentMonth, setCurrentMonth] = useState(() => new Date()); const [selectedField, setSelectedField] = useState(null); const [calendarDays, setCalendarDays] = useState({}); const [loading, setLoading] = useState(false); const [selectedDate, setSelectedDate] = useState(null); const fieldOptions = useMemo( () => template.filter((f) => f.active).map((f) => ({ value: f.builtin ? f.key : f.fieldId, label: f.label })), [template], ); useEffect(() => { if (fieldOptions.length === 0 || selectedField) return; const weightField = fieldOptions.find((f) => f.label.toLowerCase().includes('weight')); setSelectedField(weightField ? weightField.value : fieldOptions[0]?.value ?? null); }, [fieldOptions]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { if (!selectedField) return; const isBuiltin = BUILTIN_KEYS_SET.has(selectedField); const days = {}; allStatuses.forEach((s) => { const val = isBuiltin ? s[selectedField] : s.custom_fields?.[selectedField]; if (val !== null && val !== undefined && String(val).trim() !== '') { const key = s.date.slice(0, 10); days[key] = (days[key] || 0) + 1; } }); setCalendarDays(days); }, [allStatuses, selectedField]); const monthStart = startOfMonth(currentMonth); const monthEnd = endOfMonth(currentMonth); const days = eachDayOfInterval({ start: monthStart, end: monthEnd }); const leadingPad = getDay(monthStart); return (

Daily Record Calendar

Count by field: {loading && Loading...}
{format(currentMonth, 'MMMM yyyy')}
{['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map((d) => (
{d}
))}
{Array.from({ length: leadingPad }).map((_, i) =>
)} {days.map((day) => { const dateStr = format(day, 'yyyy-MM-dd'); const count = calendarDays[dateStr] ?? 0; const hasData = count > 0; return ( ); })}
{selectedDate && ( setSelectedDate(null)} title={format(selectedDate, 'MMMM d, yyyy')} size="lg"> { onStatusChange?.(updated); setSelectedDate(null); }} onStatusCreate={(created) => { onStatusCreate?.(created); setSelectedDate(null); }} /> )}
); }