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(
,
);