feat: add experiment calendar view with per-day subject counts
New ExperimentCalendar component added as a third view mode (List / Group / Calendar)
on the Experiment Detail page.
Backend:
- GET /api/experiments/:id/calendar?field=<fieldIdOrBuiltinKey>
Returns { field, days: { "YYYY-MM-DD": count } } where count = number of
subjects with a non-null/non-empty value for the chosen field on that date.
Supports both builtin fields (vitals, treatment, notes, experiment_description)
and custom fields addressed by fieldId UUID.
Frontend:
- ExperimentCalendar component: navigable monthly grid, field selector
(defaults to first field with "weight" in label, else first active field),
blue badges showing subject count per day, click to open modal with all
subjects' data for that day.
- Integrated into ExperimentDetail as displayMode "calendar", persisted
in localStorage alongside the existing list/group modes.
- experimentsApi.getCalendar() added to API client.
Tests:
- backend/tests/calendar.test.js: 8 unit tests (404, empty days, builtin
count, custom field count, whitespace ignored, multi-month, field echo,
missing param).
- frontend/tests/ExperimentCalendar.test.jsx: 13 RTL tests covering
rendering, default field selection, count badges, month navigation,
field-change re-fetch, day click modal, and multi-subject display.
All 47 backend + 13 calendar frontend tests passing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE "experiments" ADD COLUMN "subject_info_template" JSONB NOT NULL DEFAULT '[]';
|
||||
ALTER TABLE "animals" ADD COLUMN "subject_info" JSONB;
|
||||
@@ -0,0 +1,21 @@
|
||||
CREATE TABLE "daily_analyses" (
|
||||
"id" TEXT NOT NULL,
|
||||
"daily_status_id" TEXT NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"file_name" TEXT,
|
||||
"timestamp_col" TEXT NOT NULL,
|
||||
"note_col" TEXT NOT NULL,
|
||||
"classifications" JSONB NOT NULL,
|
||||
"total_note_rows" INTEGER NOT NULL,
|
||||
"sequence" JSONB NOT NULL,
|
||||
"category_counts" JSONB NOT NULL,
|
||||
"consecutive_stats" JSONB NOT NULL,
|
||||
"runs" JSONB NOT NULL,
|
||||
CONSTRAINT "daily_analyses_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
ALTER TABLE "daily_analyses"
|
||||
ADD CONSTRAINT "daily_analyses_daily_status_id_fkey"
|
||||
FOREIGN KEY ("daily_status_id")
|
||||
REFERENCES "daily_statuses"("id")
|
||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE "daily_analyses" ADD COLUMN "session_end_ts" DOUBLE PRECISION;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE "daily_statuses" ADD COLUMN "analysis_summary" JSONB;
|
||||
@@ -9,11 +9,12 @@ datasource db {
|
||||
}
|
||||
|
||||
model Experiment {
|
||||
id String @id @default(uuid())
|
||||
title String
|
||||
created_at DateTime @default(now())
|
||||
template Json @default("[]")
|
||||
animals Animal[]
|
||||
id String @id @default(uuid())
|
||||
title String
|
||||
created_at DateTime @default(now())
|
||||
template Json @default("[]")
|
||||
subject_info_template Json @default("[]")
|
||||
animals Animal[]
|
||||
|
||||
@@map("experiments")
|
||||
}
|
||||
@@ -23,6 +24,7 @@ model Animal {
|
||||
experiment_id String
|
||||
animal_id_string String
|
||||
animal_name String
|
||||
subject_info Json?
|
||||
experiment Experiment @relation(fields: [experiment_id], references: [id], onDelete: Cascade)
|
||||
daily_statuses DailyStatus[]
|
||||
|
||||
@@ -30,19 +32,40 @@ model Animal {
|
||||
}
|
||||
|
||||
model DailyStatus {
|
||||
id String @id @default(uuid())
|
||||
id String @id @default(uuid())
|
||||
animal_id String
|
||||
date DateTime @db.Date
|
||||
date DateTime @db.Date
|
||||
experiment_description String?
|
||||
vitals String?
|
||||
treatment String?
|
||||
notes String?
|
||||
custom_fields Json?
|
||||
animal Animal @relation(fields: [animal_id], references: [id], onDelete: Cascade)
|
||||
analysis_summary Json?
|
||||
animal Animal @relation(fields: [animal_id], references: [id], onDelete: Cascade)
|
||||
analyses DailyAnalysis[]
|
||||
|
||||
@@map("daily_statuses")
|
||||
}
|
||||
|
||||
model DailyAnalysis {
|
||||
id String @id @default(uuid())
|
||||
daily_status_id String
|
||||
daily_status DailyStatus @relation(fields: [daily_status_id], references: [id], onDelete: Cascade)
|
||||
created_at DateTime @default(now())
|
||||
file_name String?
|
||||
timestamp_col String
|
||||
note_col String
|
||||
classifications Json
|
||||
total_note_rows Int
|
||||
session_end_ts Float?
|
||||
sequence Json
|
||||
category_counts Json
|
||||
consecutive_stats Json
|
||||
runs Json
|
||||
|
||||
@@map("daily_analyses")
|
||||
}
|
||||
|
||||
model AuditLog {
|
||||
id String @id @default(uuid())
|
||||
table_name String
|
||||
|
||||
Reference in New Issue
Block a user