diff --git a/frontend/src/components/ExperimentCalendar.jsx b/frontend/src/components/ExperimentCalendar.jsx index ea058e6..3a3c05f 100644 --- a/frontend/src/components/ExperimentCalendar.jsx +++ b/frontend/src/components/ExperimentCalendar.jsx @@ -36,7 +36,7 @@ function SubjectReadOnly({ status, template }) { ); } -function DayPanel({ date, animals, allStatuses, template, experimentId, onStatusChange, onStatusCreate }) { +function DayPanel({ date, animals, allStatuses, template, experimentId, selectedField, fieldOptions, onStatusChange, onStatusCreate }) { const dateStr = format(date, 'yyyy-MM-dd'); const [editingAnimalId, setEditingAnimalId] = useState(null); @@ -48,8 +48,33 @@ function DayPanel({ date, animals, allStatuses, template, experimentId, onStatus 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; @@ -92,6 +117,35 @@ function DayPanel({ date, animals, allStatuses, template, experimentId, onStatus
); })} + + {/* Bar chart: overall per-subject completeness */} + {barData.length > 0 && ( +
+

+ Overall completeness by subject + {fieldLabel && — {fieldLabel}} +

+ + + + + + + + + + + + +
+ {barData.map((d) => ( + + {d.name}: {d.pct}% complete + + ))} +
+
+ )}
); } @@ -133,28 +187,6 @@ export default function ExperimentCalendar({ experimentId, template, allStatuses const days = eachDayOfInterval({ start: monthStart, end: monthEnd }); const leadingPad = getDay(monthStart); - 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]); - return (

Daily Record Calendar

@@ -183,7 +215,7 @@ export default function ExperimentCalendar({ experimentId, template, allStatuses
- {['Sun','Mon','Tue','Wed','Thu','Fri','Sat'].map((d) => ( + {['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map((d) => (
{d}
))}
@@ -214,38 +246,6 @@ export default function ExperimentCalendar({ experimentId, template, allStatuses })} - {barData.length > 0 && ( -
-

- Field completeness by subject - {fieldOptions.find((f) => f.value === selectedField) && ( - - — {fieldOptions.find((f) => f.value === selectedField).label} - - )} -

- - - - - - - - - - - - -
- {barData.map((d) => ( - - {d.name}: {d.pct}% complete - - ))} -
-
- )} - {selectedDate && ( setSelectedDate(null)} title={format(selectedDate, 'MMMM d, yyyy')} size="lg"> { onStatusChange?.(updated); setSelectedDate(null); }} onStatusCreate={(created) => { onStatusCreate?.(created); setSelectedDate(null); }} /> diff --git a/frontend/tests/ExperimentCalendar.test.jsx b/frontend/tests/ExperimentCalendar.test.jsx index 7327b9c..b3b1f23 100644 --- a/frontend/tests/ExperimentCalendar.test.jsx +++ b/frontend/tests/ExperimentCalendar.test.jsx @@ -200,7 +200,7 @@ describe('day click modal', () => { fireEvent.click(screen.getByTestId(`day-${dateStr}`)); await waitFor(() => { const dialog = screen.getByRole('dialog'); - expect(within(dialog).getByText('Rat A')).toBeInTheDocument(); + expect(within(dialog).getAllByText('Rat A').length).toBeGreaterThan(0); }); }); @@ -238,16 +238,16 @@ describe('day click modal', () => { fireEvent.click(screen.getByTestId(`day-${dateStr}`)); await waitFor(() => { const dialog = screen.getByRole('dialog'); - expect(within(dialog).getByText('Rat A')).toBeInTheDocument(); - expect(within(dialog).getByText('Rat B')).toBeInTheDocument(); // shown even without data + expect(within(dialog).getAllByText('Rat A').length).toBeGreaterThan(0); + expect(within(dialog).getAllByText('Rat B').length).toBeGreaterThan(0); }); }); }); -// ── Bar chart ───────────────────────────────────────────────────────────────── +// ── Bar chart (inside day modal) ────────────────────────────────────────────── -describe('bar chart', () => { - it('renders bar chart when animals have statuses', () => { +describe('bar chart inside day modal', () => { + it('renders bar chart inside modal when animals have statuses', async () => { const dateStr = todayStr(); const statuses = [ makeStatus('a1000000-0000-0000-0000-000000000001', dateStr, { 'ww000000-0000-0000-0000-000000000099': '325' }), @@ -255,10 +255,25 @@ describe('bar chart', () => { render( , ); - expect(screen.getByTestId('bar-chart')).toBeInTheDocument(); + fireEvent.click(screen.getByTestId(`day-${dateStr}`)); + await waitFor(() => { + const dialog = screen.getByRole('dialog'); + expect(within(dialog).getByTestId('bar-chart')).toBeInTheDocument(); + }); }); - it('does not render bar chart when no animals have statuses', () => { + it('does not render bar chart in modal when no animals have overall statuses', async () => { + const dateStr = todayStr(); + // Give Rat A a status today so the day is clickable, but use a different animal not in barData + const statuses = [ + makeStatus('a1000000-0000-0000-0000-000000000001', dateStr, { 'ww000000-0000-0000-0000-000000000099': '325' }), + ]; + // Use animals list with no matching statuses for barData by filtering to empty + render( + , + ); + // No days enabled since animals=[] means no bar chart, but day is also not enabled + // Just verify no bar chart on empty statuses/animals render( , );