11package com .juegodefinitivo .autobook .service ;
22
33import com .juegodefinitivo .autobook .api .dto .AssignmentView ;
4+ import com .juegodefinitivo .autobook .api .dto .ActivityAbandonmentView ;
45import com .juegodefinitivo .autobook .api .dto .ClassroomDashboardResponse ;
56import com .juegodefinitivo .autobook .api .dto .ClassroomView ;
67import com .juegodefinitivo .autobook .api .dto .GameStateResponse ;
78import com .juegodefinitivo .autobook .api .dto .StudentProgressView ;
89import com .juegodefinitivo .autobook .api .dto .StudentView ;
10+ import com .juegodefinitivo .autobook .persistence .game .GameSessionRuntimeRepository ;
911import com .juegodefinitivo .autobook .persistence .teacher .TeacherWorkspaceRepository ;
1012import org .springframework .stereotype .Service ;
1113
@@ -24,10 +26,16 @@ public class TeacherWorkspaceService {
2426
2527 private final TeacherWorkspaceRepository repository ;
2628 private final GameFacadeService gameFacadeService ;
29+ private final GameSessionRuntimeRepository runtimeRepository ;
2730
28- public TeacherWorkspaceService (TeacherWorkspaceRepository repository , GameFacadeService gameFacadeService ) {
31+ public TeacherWorkspaceService (
32+ TeacherWorkspaceRepository repository ,
33+ GameFacadeService gameFacadeService ,
34+ GameSessionRuntimeRepository runtimeRepository
35+ ) {
2936 this .repository = repository ;
3037 this .gameFacadeService = gameFacadeService ;
38+ this .runtimeRepository = runtimeRepository ;
3139 }
3240
3341 public List <ClassroomView > listClassrooms () {
@@ -122,10 +130,15 @@ public ClassroomDashboardResponse getDashboard(String classroomId, LocalDate fro
122130 List <TeacherWorkspaceRepository .StudentRow > students = repository .listStudents (classroomId );
123131 List <TeacherWorkspaceRepository .AssignmentRow > assignments = repository .listAssignments (classroomId );
124132 List <TeacherWorkspaceRepository .AttemptRow > attempts = loadAttempts (classroomId , from , to );
133+ Map <String , Instant > runtimeUpdatesBySession = runtimeRepository .findLastUpdatedAt (
134+ attempts .stream ().map (TeacherWorkspaceRepository .AttemptRow ::sessionId ).toList ()
135+ );
125136
126137 List <StudentProgressView > progressViews = new ArrayList <>();
127138 int totalAttempts = 0 ;
128139 int totalCompletedAttempts = 0 ;
140+ int totalEffectiveMinutes = 0 ;
141+ Map <String , Integer > activeByActivity = new LinkedHashMap <>();
129142 for (TeacherWorkspaceRepository .StudentRow student : students ) {
130143 List <TeacherWorkspaceRepository .AttemptRow > studentAttempts = attempts .stream ()
131144 .filter (attempt -> attempt .studentId ().equals (student .id ()))
@@ -141,8 +154,20 @@ public ClassroomDashboardResponse getDashboard(String classroomId, LocalDate fro
141154 int avgProgress = (int ) states .stream ().mapToInt (this ::progressPercent ).average ().orElse (0 );
142155 int completed = (int ) states .stream ().filter (GameStateResponse ::completed ).count ();
143156 String difficulty = dominantDifficulty (states );
157+ int totalMinutesByStudent = studentAttempts .stream ()
158+ .mapToInt (attempt -> effectiveMinutes (attempt , runtimeUpdatesBySession ))
159+ .sum ();
160+ int avgEffectiveMinutes = studentAttempts .isEmpty () ? 0 : totalMinutesByStudent / studentAttempts .size ();
144161 totalAttempts += studentAttempts .size ();
145162 totalCompletedAttempts += completed ;
163+ totalEffectiveMinutes += totalMinutesByStudent ;
164+
165+ for (GameStateResponse state : states ) {
166+ if (!state .completed ()) {
167+ String eventType = state .currentScene () == null ? "UNKNOWN" : state .currentScene ().eventType ();
168+ activeByActivity .merge (eventType , 1 , Integer ::sum );
169+ }
170+ }
146171
147172 progressViews .add (new StudentProgressView (
148173 student .id (),
@@ -152,6 +177,7 @@ public ClassroomDashboardResponse getDashboard(String classroomId, LocalDate fro
152177 avgScore ,
153178 avgCorrect ,
154179 avgProgress ,
180+ avgEffectiveMinutes ,
155181 difficulty
156182 ));
157183 }
@@ -160,6 +186,15 @@ public ClassroomDashboardResponse getDashboard(String classroomId, LocalDate fro
160186 int abandonmentRatePercent = totalAttempts == 0
161187 ? 0
162188 : (int ) ((activeAttempts / (double ) totalAttempts ) * 100 );
189+ int averageEffectiveMinutesPerAttempt = totalAttempts == 0 ? 0 : totalEffectiveMinutes / totalAttempts ;
190+ List <ActivityAbandonmentView > abandonmentByActivity = activeByActivity .entrySet ().stream ()
191+ .map (entry -> new ActivityAbandonmentView (
192+ entry .getKey (),
193+ entry .getValue (),
194+ activeAttempts == 0 ? 0 : (int ) ((entry .getValue () / (double ) activeAttempts ) * 100 )
195+ ))
196+ .sorted ((left , right ) -> Integer .compare (right .activeAttempts (), left .activeAttempts ()))
197+ .toList ();
163198
164199 return new ClassroomDashboardResponse (
165200 classroom .id (),
@@ -170,6 +205,9 @@ public ClassroomDashboardResponse getDashboard(String classroomId, LocalDate fro
170205 activeAttempts ,
171206 totalCompletedAttempts ,
172207 abandonmentRatePercent ,
208+ totalEffectiveMinutes ,
209+ averageEffectiveMinutesPerAttempt ,
210+ abandonmentByActivity ,
173211 progressViews
174212 );
175213 }
@@ -181,7 +219,7 @@ public String exportClassroomCsv(String classroomId) {
181219 public String exportClassroomCsv (String classroomId , LocalDate from , LocalDate to ) {
182220 ClassroomDashboardResponse dashboard = getDashboard (classroomId , from , to );
183221 StringBuilder csv = new StringBuilder ();
184- csv .append ("classroom_id,classroom_name,teacher,student_id,student_name,attempts,completed_attempts,avg_score,avg_correct_answers,avg_progress_percent,dominant_difficulty" )
222+ csv .append ("classroom_id,classroom_name,teacher,student_id,student_name,attempts,completed_attempts,avg_score,avg_correct_answers,avg_progress_percent,avg_effective_minutes, dominant_difficulty" )
185223 .append ('\n' );
186224 for (StudentProgressView view : dashboard .studentProgress ()) {
187225 csv .append (csv (dashboard .classroomId ())).append (',' )
@@ -194,9 +232,25 @@ public String exportClassroomCsv(String classroomId, LocalDate from, LocalDate t
194232 .append (view .averageScore ()).append (',' )
195233 .append (view .averageCorrectAnswers ()).append (',' )
196234 .append (view .averageProgressPercent ()).append (',' )
235+ .append (view .averageEffectiveMinutes ()).append (',' )
197236 .append (csv (view .dominantDifficulty ()))
198237 .append ('\n' );
199238 }
239+ csv .append ('\n' );
240+ csv .append ("summary_metric,summary_value" ).append ('\n' );
241+ csv .append ("active_attempts," ).append (dashboard .activeAttempts ()).append ('\n' );
242+ csv .append ("completed_attempts," ).append (dashboard .completedAttempts ()).append ('\n' );
243+ csv .append ("abandonment_rate_percent," ).append (dashboard .abandonmentRatePercent ()).append ('\n' );
244+ csv .append ("total_effective_reading_minutes," ).append (dashboard .totalEffectiveReadingMinutes ()).append ('\n' );
245+ csv .append ("average_effective_minutes_per_attempt," ).append (dashboard .averageEffectiveMinutesPerAttempt ()).append ('\n' );
246+ csv .append ('\n' );
247+ csv .append ("abandonment_activity,active_attempts,active_rate_percent" ).append ('\n' );
248+ for (ActivityAbandonmentView activity : dashboard .abandonmentByActivity ()) {
249+ csv .append (csv (activity .eventType ())).append (',' )
250+ .append (activity .activeAttempts ()).append (',' )
251+ .append (activity .activeRatePercent ())
252+ .append ('\n' );
253+ }
200254 return csv .toString ();
201255 }
202256
@@ -298,6 +352,13 @@ private String dominantDifficulty(List<GameStateResponse> states) {
298352 .orElse ("UNKNOWN" );
299353 }
300354
355+ private int effectiveMinutes (TeacherWorkspaceRepository .AttemptRow attempt , Map <String , Instant > runtimeUpdatesBySession ) {
356+ Instant updated = runtimeUpdatesBySession .getOrDefault (attempt .sessionId (), attempt .createdAt ());
357+ long seconds = Math .max (0L , updated .getEpochSecond () - attempt .createdAt ().getEpochSecond ());
358+ int minutes = (int ) Math .max (1 , seconds / 60 );
359+ return Math .min (minutes , 240 );
360+ }
361+
301362 private List <TeacherWorkspaceRepository .AttemptRow > loadAttempts (String classroomId , LocalDate from , LocalDate to ) {
302363 if (from == null || to == null ) {
303364 return repository .listAttemptsForClassroom (classroomId );
0 commit comments