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]);
const barData = useMemo(() => {
if (!selectedField) return [];
const isBuiltin = BUILTIN_KEYS_SET.has(selectedField);
return animals
.map((animal) => {
const statuses = allStatuses.filter((s) => s.animal_id === animal.id);
const total = statuses.length;
const success = statuses.filter((s) => {
const val = isBuiltin ? s[selectedField] : s.custom_fields?.[selectedField];
return val !== null && val !== undefined && String(val).trim() !== '';
}).length;
return {
name: animal.animal_name || animal.animal_id_string || animal.id.slice(0, 8),
total,
success,
failure: total - success,
pct: total > 0 ? Math.round((success / total) * 100) : 0,
};
})
.filter((d) => d.total > 0);
}, [animals, allStatuses, selectedField]);
const fieldLabel = fieldOptions.find((f) => f.value === selectedField)?.label;
return (
{/* Per-animal data for this day */}
{animals.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 ? (
{
if (existing) { onStatusChange(saved); } else { onStatusCreate(saved); }
setEditingAnimalId(null);
}}
onCancel={() => setEditingAnimalId(null)}
/>
) : existing ? (
) : (
No data recorded for this day.
)}
);
})}
{/* Bar chart: overall per-subject completeness */}
{barData.length > 0 && (
Overall completeness by subject
{fieldLabel && — {fieldLabel}}
{barData.map((d) => (
{d.name}: {d.pct}% complete
))}
)}
);
}
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); }}
/>
)}
);
}