Skip to content

Commit c8c13fa

Browse files
dsshimelclaude
andcommitted
add cohort date columns and use them in student stats
Store start/end/break dates on cohorts table so student stats uses the cohort date range instead of a hardcoded last-30-days window. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e7a2b07 commit c8c13fa

File tree

7 files changed

+78
-21
lines changed

7 files changed

+78
-21
lines changed

attendabot/backend/src/api/routes/me.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ meRouter.get("/", authenticateToken, (req: AuthRequest, res: Response) => {
1717
discordAccountId?: string;
1818
studentId?: number;
1919
studentName?: string;
20+
cohortId?: number;
2021
} = {
2122
role: user.role,
2223
name: user.username,
@@ -27,11 +28,12 @@ meRouter.get("/", authenticateToken, (req: AuthRequest, res: Response) => {
2728
if (user.role === "student" && user.discordAccountId) {
2829
const db = getDatabase();
2930
const student = db
30-
.prepare(`SELECT id, name FROM students WHERE discord_user_id = ?`)
31-
.get(user.discordAccountId) as { id: number; name: string } | undefined;
31+
.prepare(`SELECT id, name, cohort_id FROM students WHERE discord_user_id = ?`)
32+
.get(user.discordAccountId) as { id: number; name: string; cohort_id: number } | undefined;
3233
if (student) {
3334
result.studentId = student.id;
3435
result.studentName = student.name;
36+
result.cohortId = student.cohort_id;
3537
}
3638
}
3739

attendabot/backend/src/services/db.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,15 @@ function initializeTables(): void {
178178
db.exec(`ALTER TABLE students ADD COLUMN profile_image TEXT`);
179179
}
180180

181+
// Add date columns to cohorts if they don't exist
182+
const cohortCols = db.pragma("table_info(cohorts)") as Array<{ name: string }>;
183+
if (!cohortCols.some((col) => col.name === "start_date")) {
184+
db.exec(`ALTER TABLE cohorts ADD COLUMN start_date TEXT`);
185+
db.exec(`ALTER TABLE cohorts ADD COLUMN end_date TEXT`);
186+
db.exec(`ALTER TABLE cohorts ADD COLUMN break_start TEXT`);
187+
db.exec(`ALTER TABLE cohorts ADD COLUMN break_end TEXT`);
188+
}
189+
181190
// Seed default cohorts if they don't exist
182191
seedDefaultCohorts();
183192

@@ -208,6 +217,13 @@ function seedDefaultCohorts(): void {
208217
const stmt = db.prepare(`INSERT OR IGNORE INTO cohorts (name) VALUES (?)`);
209218
stmt.run("Fa2025");
210219
stmt.run("Sp2026");
220+
221+
// Set Sp2026 dates if not already set
222+
db.prepare(`
223+
UPDATE cohorts SET start_date='2026-02-02', end_date='2026-05-02',
224+
break_start='2026-03-15', break_end='2026-03-22'
225+
WHERE name='Sp2026' AND start_date IS NULL
226+
`).run();
211227
}
212228

213229
/** A message record from the database with joined channel and user data. */
@@ -437,13 +453,17 @@ export function closeDatabase(): void {
437453
export interface CohortRecord {
438454
id: number;
439455
name: string;
456+
start_date: string | null;
457+
end_date: string | null;
458+
break_start: string | null;
459+
break_end: string | null;
440460
created_at: string;
441461
}
442462

443463
/** Retrieves all cohorts from the database, ordered by name. */
444464
export function getCohorts(): CohortRecord[] {
445465
const db = getDatabase();
446-
const stmt = db.prepare(`SELECT id, name, created_at FROM cohorts ORDER BY name ASC`);
466+
const stmt = db.prepare(`SELECT id, name, start_date, end_date, break_start, break_end, created_at FROM cohorts ORDER BY name ASC`);
447467
return stmt.all() as CohortRecord[];
448468
}
449469

@@ -455,6 +475,10 @@ export function createCohort(name: string): CohortRecord {
455475
return {
456476
id: result.lastInsertRowid as number,
457477
name,
478+
start_date: null,
479+
end_date: null,
480+
break_start: null,
481+
break_end: null,
458482
created_at: new Date().toISOString(),
459483
};
460484
}

attendabot/backend/src/test/utils/testUtils.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ export function createTestDatabase(): Database.Database {
6060
CREATE TABLE IF NOT EXISTS cohorts (
6161
id INTEGER PRIMARY KEY AUTOINCREMENT,
6262
name TEXT NOT NULL UNIQUE,
63+
start_date TEXT,
64+
end_date TEXT,
65+
break_start TEXT,
66+
break_end TEXT,
6367
created_at TEXT DEFAULT CURRENT_TIMESTAMP
6468
)
6569
`);

attendabot/frontend/src/App.tsx

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import { useState, useEffect } from "react";
77
import { setUsername as storeUsername, clearSession, onAuthFailure, getMe, getCohorts, getStudentsByCohort } from "./api/client";
8-
import type { Student } from "./api/client";
8+
import type { Student, Cohort } from "./api/client";
99
import { authClient } from "./lib/auth-client";
1010
import { Login } from "./components/Login";
1111
import { MessageFeed } from "./components/MessageFeed";
@@ -27,14 +27,21 @@ function App() {
2727
const [username, setUsername] = useState<string | null>(null);
2828
const [role, setRole] = useState<"instructor" | "student" | null>(null);
2929
const [sessionInvalid, setSessionInvalid] = useState(false);
30-
const [impersonating, setImpersonating] = useState<{ discordId: string; name: string } | null>(null);
31-
const [impersonateStudents, setImpersonateStudents] = useState<{ discordId: string; name: string }[]>([]);
30+
const [impersonating, setImpersonating] = useState<{ discordId: string; name: string; cohortId: number } | null>(null);
31+
const [impersonateStudents, setImpersonateStudents] = useState<{ discordId: string; name: string; cohortId: number }[]>([]);
32+
const [cohorts, setCohorts] = useState<Cohort[]>([]);
33+
const [studentCohortId, setStudentCohortId] = useState<number | null>(null);
3234

3335
/** Fetches the user's role and identity after login. */
3436
const fetchRole = async () => {
3537
const me = await getMe();
3638
if (me) {
3739
setRole(me.role);
40+
if (me.role === "student" && me.cohortId) {
41+
setStudentCohortId(me.cohortId);
42+
const allCohorts = await getCohorts();
43+
setCohorts(allCohorts);
44+
}
3845
}
3946
};
4047

@@ -62,15 +69,16 @@ function App() {
6269
useEffect(() => {
6370
if (role !== "instructor") return;
6471
(async () => {
65-
const cohorts = await getCohorts();
72+
const allCohorts = await getCohorts();
73+
setCohorts(allCohorts);
6674
const allStudents: Student[] = [];
67-
for (const cohort of cohorts) {
75+
for (const cohort of allCohorts) {
6876
const students = await getStudentsByCohort(cohort.id);
6977
allStudents.push(...students);
7078
}
7179
const withDiscord = allStudents
7280
.filter((s) => s.discordUserId)
73-
.map((s) => ({ discordId: s.discordUserId!, name: s.name }))
81+
.map((s) => ({ discordId: s.discordUserId!, name: s.name, cohortId: s.cohortId }))
7482
.sort((a, b) => a.name.localeCompare(b.name));
7583
setImpersonateStudents(withDiscord);
7684
})();
@@ -106,11 +114,14 @@ function App() {
106114

107115
// Student portal
108116
if (role === "student") {
117+
const cohort = cohorts.find((c) => c.id === studentCohortId);
109118
return (
110119
<StudentPortal
111120
username={username}
112121
sessionInvalid={sessionInvalid}
113122
onLogout={handleLogout}
123+
cohortStartDate={cohort?.startDate ?? undefined}
124+
cohortEndDate={cohort?.endDate ?? undefined}
114125
/>
115126
);
116127
}
@@ -179,6 +190,8 @@ function App() {
179190
sessionInvalid={sessionInvalid}
180191
onLogout={handleLogout}
181192
studentDiscordId={impersonating.discordId}
193+
cohortStartDate={cohorts.find((c) => c.id === impersonating.cohortId)?.startDate ?? undefined}
194+
cohortEndDate={cohorts.find((c) => c.id === impersonating.cohortId)?.endDate ?? undefined}
182195
/>
183196
) : (
184197
<>

attendabot/frontend/src/api/client.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,10 @@ export async function syncDisplayNames(): Promise<{
227227
export interface Cohort {
228228
id: number;
229229
name: string;
230+
startDate: string | null;
231+
endDate: string | null;
232+
breakStart: string | null;
233+
breakEnd: string | null;
230234
}
231235

232236
/** A student in a cohort. */
@@ -257,9 +261,13 @@ export async function getCohorts(): Promise<Cohort[]> {
257261
const res = await fetchWithAuth(`${API_BASE}/cohorts`);
258262
if (!res.ok) return [];
259263
const data = await res.json();
260-
return data.cohorts.map((c: { id: number; name: string }) => ({
264+
return data.cohorts.map((c: { id: number; name: string; start_date: string | null; end_date: string | null; break_start: string | null; break_end: string | null }) => ({
261265
id: c.id,
262266
name: c.name,
267+
startDate: c.start_date,
268+
endDate: c.end_date,
269+
breakStart: c.break_start,
270+
breakEnd: c.break_end,
263271
}));
264272
} catch (error) {
265273
console.error(error);
@@ -957,6 +965,7 @@ export interface MeResponse {
957965
discordAccountId?: string;
958966
studentId?: number;
959967
studentName?: string;
968+
cohortId?: number;
960969
}
961970

962971
/** Fetches the current user's role and identity. */

attendabot/frontend/src/components/StudentPortal.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@ interface StudentPortalProps {
1515
sessionInvalid: boolean;
1616
onLogout: () => void;
1717
studentDiscordId?: string;
18+
cohortStartDate?: string;
19+
cohortEndDate?: string;
1820
}
1921

2022
/** Student portal with tabs for Stats, My EODs, and Simulations. */
21-
export function StudentPortal({ username, sessionInvalid, onLogout, studentDiscordId }: StudentPortalProps) {
23+
export function StudentPortal({ username, sessionInvalid, onLogout, studentDiscordId, cohortStartDate, cohortEndDate }: StudentPortalProps) {
2224
const [activeTab, setActiveTab] = useState<StudentTab>("stats");
2325
const embedded = !!studentDiscordId;
2426

@@ -47,7 +49,7 @@ export function StudentPortal({ username, sessionInvalid, onLogout, studentDisco
4749

4850
<main>
4951
{activeTab === "stats" ? (
50-
<StudentStats studentDiscordId={studentDiscordId} />
52+
<StudentStats studentDiscordId={studentDiscordId} cohortStartDate={cohortStartDate} cohortEndDate={cohortEndDate} />
5153
) : activeTab === "eods" ? (
5254
<MyEods studentDiscordId={studentDiscordId} />
5355
) : (

attendabot/frontend/src/components/StudentStats.tsx

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,33 +9,36 @@ import type { DailyStats } from "../api/client";
99

1010
interface StudentStatsProps {
1111
studentDiscordId?: string;
12+
cohortStartDate?: string;
13+
cohortEndDate?: string;
1214
}
1315

1416
/** A row in the stats table — either a single day or a weekly summary. */
1517
type StatsRow =
1618
| { type: "day"; day: DailyStats }
1719
| { type: "week"; sundayDate: string; weekDays: DailyStats[] };
1820

19-
/** Renders a table of daily stats for the student over the last 30 days. */
20-
export function StudentStats({ studentDiscordId }: StudentStatsProps) {
21+
/** Renders a table of daily stats for the student over the cohort date range (or last 30 days as fallback). */
22+
export function StudentStats({ studentDiscordId, cohortStartDate, cohortEndDate }: StudentStatsProps) {
2123
const [days, setDays] = useState<DailyStats[]>([]);
2224
const [loading, setLoading] = useState(true);
2325

2426
useEffect(() => {
2527
setLoading(true);
26-
const end = new Date();
27-
const start = new Date();
28-
start.setDate(start.getDate() - 30);
29-
const startStr = start.toISOString().split("T")[0];
30-
const endStr = end.toISOString().split("T")[0];
28+
const today = new Date().toISOString().split("T")[0];
29+
const fallbackStart = new Date();
30+
fallbackStart.setDate(fallbackStart.getDate() - 30);
31+
32+
const startStr = cohortStartDate ?? fallbackStart.toISOString().split("T")[0];
33+
const endStr = cohortEndDate && cohortEndDate < today ? cohortEndDate : today;
3134

3235
getMyStats(startStr, endStr, studentDiscordId).then((result) => {
3336
if (result) {
3437
setDays(result.days);
3538
}
3639
setLoading(false);
3740
});
38-
}, [studentDiscordId]);
41+
}, [studentDiscordId, cohortStartDate, cohortEndDate]);
3942

4043
/** Build rows: normal days for Mon-Sat, weekly summary rows replacing Sundays. */
4144
const rows: StatsRow[] = useMemo(() => {
@@ -77,7 +80,7 @@ export function StudentStats({ studentDiscordId }: StudentStatsProps) {
7780

7881
return (
7982
<div className="panel" style={{ gridColumn: "span 2" }}>
80-
<h2>Daily Stats (Last 30 Days)</h2>
83+
<h2>Daily Stats{cohortStartDate ? "" : " (Last 30 Days)"}</h2>
8184
{days.length === 0 ? (
8285
<p className="no-messages">No stats available.</p>
8386
) : (

0 commit comments

Comments
 (0)