11/**
22 * @fileoverview Displays daily stats for the student: attendance, PRs, EOD.
3+ * Sundays are replaced with a weekly summary row aggregating Mon-Sat.
34 */
45
5- import { useState , useEffect } from "react" ;
6+ import { useState , useEffect , useMemo } from "react" ;
67import { getMyStats } from "../api/client" ;
78import type { DailyStats } from "../api/client" ;
89
910interface StudentStatsProps {
1011 studentDiscordId ?: string ;
1112}
1213
14+ /** A row in the stats table — either a single day or a weekly summary. */
15+ type StatsRow =
16+ | { type : "day" ; day : DailyStats }
17+ | { type : "week" ; sundayDate : string ; weekDays : DailyStats [ ] } ;
18+
1319/** Renders a table of daily stats for the student over the last 30 days. */
1420export function StudentStats ( { studentDiscordId } : StudentStatsProps ) {
1521 const [ days , setDays ] = useState < DailyStats [ ] > ( [ ] ) ;
@@ -31,6 +37,35 @@ export function StudentStats({ studentDiscordId }: StudentStatsProps) {
3137 } ) ;
3238 } , [ studentDiscordId ] ) ;
3339
40+ /** Build rows: normal days for Mon-Sat, weekly summary rows replacing Sundays. */
41+ const rows : StatsRow [ ] = useMemo ( ( ) => {
42+ // days is sorted DESC by date
43+ const result : StatsRow [ ] = [ ] ;
44+ // Group days by their week (Sunday = week boundary)
45+ // For each Sunday, collect the preceding Mon-Sat
46+ const daysByDate = new Map ( days . map ( ( d ) => [ d . date , d ] ) ) ;
47+
48+ for ( const day of days ) {
49+ const date = new Date ( day . date + "T12:00:00" ) ;
50+ const dow = date . getDay ( ) ; // 0 = Sunday
51+ if ( dow === 0 ) {
52+ // Collect Mon-Sat of this week (Mon = Sunday-6, Sat = Sunday-1)
53+ const weekDays : DailyStats [ ] = [ ] ;
54+ for ( let offset = 6 ; offset >= 1 ; offset -- ) {
55+ const d = new Date ( date ) ;
56+ d . setDate ( d . getDate ( ) - offset ) ;
57+ const key = d . toISOString ( ) . split ( "T" ) [ 0 ] ;
58+ const found = daysByDate . get ( key ) ;
59+ if ( found ) weekDays . push ( found ) ;
60+ }
61+ result . push ( { type : "week" , sundayDate : day . date , weekDays } ) ;
62+ } else {
63+ result . push ( { type : "day" , day } ) ;
64+ }
65+ }
66+ return result ;
67+ } , [ days ] ) ;
68+
3469 if ( loading ) {
3570 return (
3671 < div className = "panel" style = { { gridColumn : "span 2" } } >
@@ -58,47 +93,93 @@ export function StudentStats({ studentDiscordId }: StudentStatsProps) {
5893 </ tr >
5994 </ thead >
6095 < tbody >
61- { days . map ( ( day ) => (
62- < tr key = { day . date } >
63- < td >
64- { new Date ( day . date + "T12:00:00" ) . toLocaleDateString ( "en-US" , {
65- weekday : "short" ,
66- month : "short" ,
67- day : "numeric" ,
68- } ) }
69- </ td >
70- < td >
71- { day . attendancePosted ? (
72- < span style = { { color : day . attendanceOnTime ? "#2d6a2e" : "#8B6914" } } >
73- { day . attendanceOnTime ? "On time" : "Late" }
74- </ span >
75- ) : (
76- < span style = { { color : "var(--color-error)" } } > Missed</ span >
77- ) }
78- </ td >
79- < td >
80- { day . middayPrPosted ? (
81- < span style = { { color : "#2d6a2e" } } >
82- { day . middayPrCount } PR{ day . middayPrCount !== 1 ? "s" : "" }
83- </ span >
84- ) : (
85- < span style = { { color : "var(--color-error)" } } > None</ span >
86- ) }
87- </ td >
88- < td >
89- { day . eodPosted ? (
90- < span style = { { color : "#2d6a2e" } } > Posted</ span >
91- ) : (
92- < span style = { { color : "var(--color-error)" } } > Missing</ span >
93- ) }
94- </ td >
95- < td style = { { fontWeight : 700 } } > { day . totalPrCount } </ td >
96- </ tr >
97- ) ) }
96+ { rows . map ( ( row ) =>
97+ row . type === "day" ? (
98+ < DayRow key = { row . day . date } day = { row . day } />
99+ ) : (
100+ < WeekRow key = { row . sundayDate } sundayDate = { row . sundayDate } weekDays = { row . weekDays } />
101+ ) ,
102+ ) }
98103 </ tbody >
99104 </ table >
100105 </ div >
101106 ) }
102107 </ div >
103108 ) ;
104109}
110+
111+ /** Renders a single day row. */
112+ function DayRow ( { day } : { day : DailyStats } ) {
113+ return (
114+ < tr >
115+ < td >
116+ { new Date ( day . date + "T12:00:00" ) . toLocaleDateString ( "en-US" , {
117+ weekday : "short" ,
118+ month : "short" ,
119+ day : "numeric" ,
120+ } ) }
121+ </ td >
122+ < td >
123+ { day . attendancePosted ? (
124+ < span style = { { color : day . attendanceOnTime ? "#2d6a2e" : "#8B6914" } } >
125+ { day . attendanceOnTime ? "On time" : "Late" }
126+ </ span >
127+ ) : (
128+ < span style = { { color : "var(--color-error)" } } > Missed</ span >
129+ ) }
130+ </ td >
131+ < td >
132+ { day . middayPrPosted ? (
133+ < span style = { { color : "#2d6a2e" } } >
134+ { day . middayPrCount } PR{ day . middayPrCount !== 1 ? "s" : "" }
135+ </ span >
136+ ) : (
137+ < span style = { { color : "var(--color-error)" } } > None</ span >
138+ ) }
139+ </ td >
140+ < td >
141+ { day . eodPosted ? (
142+ < span style = { { color : "#2d6a2e" } } > Posted</ span >
143+ ) : (
144+ < span style = { { color : "var(--color-error)" } } > Missing</ span >
145+ ) }
146+ </ td >
147+ < td style = { { fontWeight : 700 } } > { day . totalPrCount } </ td >
148+ </ tr >
149+ ) ;
150+ }
151+
152+ /** Renders a weekly summary row replacing a Sunday. */
153+ function WeekRow ( { sundayDate, weekDays } : { sundayDate : string ; weekDays : DailyStats [ ] } ) {
154+ const onTime = weekDays . filter ( ( d ) => d . attendancePosted && d . attendanceOnTime ) . length ;
155+ const late = weekDays . filter ( ( d ) => d . attendancePosted && ! d . attendanceOnTime ) . length ;
156+ const missed = weekDays . filter ( ( d ) => ! d . attendancePosted ) . length ;
157+ const middayDays = weekDays . filter ( ( d ) => d . middayPrPosted ) . length ;
158+ const eodDays = weekDays . filter ( ( d ) => d . eodPosted ) . length ;
159+ const totalPrs = weekDays . reduce ( ( sum , d ) => sum + d . totalPrCount , 0 ) ;
160+
161+ // Find Mon date for the label (Sunday - 6)
162+ const sun = new Date ( sundayDate + "T12:00:00" ) ;
163+ const mon = new Date ( sun ) ;
164+ mon . setDate ( mon . getDate ( ) - 6 ) ;
165+ const fmt = ( d : Date ) =>
166+ d . toLocaleDateString ( "en-US" , { month : "short" , day : "numeric" } ) ;
167+
168+ return (
169+ < tr style = { { background : "var(--color-platinum)" } } >
170+ < td style = { { fontWeight : 700 } } >
171+ Week: { fmt ( mon ) } – { fmt ( sun ) }
172+ </ td >
173+ < td >
174+ < span style = { { color : "#2d6a2e" } } > { onTime } </ span >
175+ { " / " }
176+ < span style = { { color : "#8B6914" } } > { late } </ span >
177+ { " / " }
178+ < span style = { { color : "var(--color-error)" } } > { missed } </ span >
179+ </ td >
180+ < td > { middayDays } /6</ td >
181+ < td > { eodDays } /6</ td >
182+ < td style = { { fontWeight : 700 } } > { totalPrs } </ td >
183+ </ tr >
184+ ) ;
185+ }
0 commit comments