001package com.studentgui.apppages; 002 003import java.awt.BorderLayout; 004import java.awt.Font; 005import java.awt.GridBagConstraints; 006import java.awt.GridBagLayout; 007import java.awt.Insets; 008import java.awt.event.ActionEvent; 009import java.awt.event.KeyEvent; 010import java.sql.SQLException; 011import java.time.LocalDate; 012import java.util.List; 013 014import javax.swing.JButton; 015import javax.swing.JLabel; 016import javax.swing.JOptionPane; 017import javax.swing.JPanel; 018import javax.swing.JScrollPane; 019import javax.swing.SwingUtilities; 020 021import org.slf4j.Logger; 022import org.slf4j.LoggerFactory; 023 024/** 025 * Braille skills progression assessment page. 026 * 027 * <p>Provides a comprehensive user interface for tracking student proficiency across 028 * 64 standardized Braille skills organized into 8 progressive phases following the 029 * Mangold Developmental Program sequence:</p> 030 * 031 * <ul> 032 * <li><b>Phase 1 (P1_1–P1_4):</b> Foundational tracking and discrimination skills</li> 033 * <li><b>Phase 2 (P2_1–P2_15):</b> Mangold letter progression (G C L → V J)</li> 034 * <li><b>Phase 3 (P3_1–P3_15):</b> Contractions, wordsigns, and Grade 2 Braille basics</li> 035 * <li><b>Phase 4 (P4_1–P4_4):</b> Indicators (Grade 1, capitals, numeric mode, typeform)</li> 036 * <li><b>Phase 5 (P5_1–P5_4):</b> Document formatting (page numbers, headings, lists, poetry)</li> 037 * <li><b>Phase 6 (P6_1–P6_7):</b> Basic Nemeth Math Code (operations, shapes, fractions)</li> 038 * <li><b>Phase 7 (P7_1–P7_8):</b> Advanced Math (algebra, indices, radicals, functions, Greek)</li> 039 * <li><b>Phase 8 (P8_1–P8_7):</b> Higher mathematics (modifiers, calculus, probability)</li> 040 * </ul> 041 * 042 * <p><b>Data Flow and Persistence:</b></p> 043 * <ul> 044 * <li>Each skill is represented by a {@link com.studentgui.uicomp.PhaseScoreField} accepting integer scores (0–4 typical range)</li> 045 * <li>On submission, values are persisted to the normalized schema via {@link com.studentgui.apphelpers.Database#insertAssessmentResults}</li> 046 * <li>A timestamped JSON export is written to {@code StudentDataFiles/<student>/Sessions/Braille/}</li> 047 * <li>Time-series plots are generated per phase group and saved as PNG images to {@code plots/}</li> 048 * <li>Markdown and HTML reports are generated combining all phase plots with legend and metadata</li> 049 * </ul> 050 * 051 * <p><b>Generated Artifacts:</b></p> 052 * <ul> 053 * <li><b>JSON session file:</b> {@code Braille-<sessionId>-<timestamp>.json}</li> 054 * <li><b>Phase plots:</b> {@code Braille-<sessionId>-<date>-P<N>.png} (8 phase groups)</li> 055 * <li><b>Markdown report:</b> {@code reports/Braille-<sessionId>-<date>.md}</li> 056 * <li><b>HTML report:</b> {@code reports/Braille-<sessionId>-<date>.html} with embedded plots and color-coded legends</li> 057 * </ul> 058 * 059 * <p>The shared {@link JLineGraph} component visualizes recent session trends for the selected 060 * student, grouped by phase to prevent overcrowding. This page implements {@link com.studentgui.app.DateChangeListener} 061 * and {@link com.studentgui.app.StudentChangeListener} to refresh data when the global student or date selection changes.</p> 062 * 063 * @see com.studentgui.apphelpers.Database 064 * @see JLineGraph 065 * @see com.studentgui.uicomp.PhaseScoreField 066 */ 067public class Braille extends JPanel implements com.studentgui.app.DateChangeListener, com.studentgui.app.StudentChangeListener { 068 private static final Logger LOG = LoggerFactory.getLogger(Braille.class); 069 070 /** Array of input components representing each Braille skill. */ 071 private final com.studentgui.uicomp.PhaseScoreField[] skillFields; 072 /** Parts list for Braille (code,label) */ 073 private final String[][] parts; 074 /** Flat list of part codes (derived from parts) */ 075 private final String[] partCodes; 076 /** Shared graph used to plot recent results. */ 077 private final JLineGraph lineGraph; // Reference to the JLineGraph instance 078 /** Selected student display name (may be null or placeholder). */ 079 private String studentNameParam; 080 /** Session date used when creating progress sessions. */ 081 private LocalDate dateParam; 082 /** Title label component displayed in the page header. */ 083 private JLabel titleLabel; 084 /** Base title text for the Braille page; a date suffix may be appended for display. */ 085 private final String baseTitle = "Braille Skills Progression"; 086 087 /** 088 * Construct the Braille skills page for a given student and date. 089 * 090 * @param studentName the selected student name (may be null before selection) 091 * @param date the session date to use when creating a progress session 092 * @param lineGraph shared graph component used to display recent results 093 */ 094 public Braille(final String studentName, final LocalDate date, final JLineGraph lineGraph) { 095 this.lineGraph = lineGraph; // Use the passed in graph instance 096 this.studentNameParam = (studentName == null || studentName.trim().isEmpty()) ? com.studentgui.apphelpers.Helpers.defaultStudent() : studentName; 097 this.dateParam = date != null ? date : LocalDate.now(); 098 setLayout(new BorderLayout()); 099 100 // Detailed Braille parts (code, visible label) 101 this.parts = new String[][]{ 102 {"P1_1","1.1. Track left to right"},{"P1_2","1.2. Track top to bottom"},{"P1_3","1.3. Discriminate shapes"},{"P1_4","1.4. Discriminate braille characters"}, 103 {"P2_1","2.1. Mangold Progression: G C L"},{"P2_2","2.2. Mangold Progression: D Y"},{"P2_3","2.3. Mangold Progression: A B"},{"P2_4","2.4. Mangold Progression: S"}, 104 {"P2_5","2.5. Mangold Progression: W"},{"P2_6","2.6. Mangold Progression: P O"},{"P2_7","2.7. Mangold Progression: K"},{"P2_8","2.8. Mangold Progression: R"}, 105 {"P2_9","2.9. Mangold Progression: M E"},{"P2_10","2.10. Mangold Progression: H"},{"P2_11","2.11. Mangold Progression: N X"},{"P2_12","2.12. Mangold Progression: Z F"}, 106 {"P2_13","2.13. Mangold Progression: U T"},{"P2_14","2.14. Mangold Progression: Q I"},{"P2_15","2.15. Mangold Progression: V J"}, 107 {"P3_1","3.1. Alphabetic Wordsigns"},{"P3_2","3.2. Braille Numbers"},{"P3_3","3.3. Punctuation"},{"P3_4","3.4. Strong Contractions (AND OF FOR WITH THE)"}, 108 {"P3_5","3.5. Strong Groupsigns (CH GH SH TH WH ED ER OU OW ST AR ING)"},{"P3_6","3.6. Strong Wordsigns (CH SH TH WH OU ST)"},{"P3_7","3.7. Lower Groupsigns (BE CON DIS)"}, 109 {"P3_8","3.8. Lower Groupsigns (EA BB CC FF GG)"},{"P3_9","3.9. Lower Groupsigns/Wordsigns (EN IN)"},{"P3_10","3.10. Lower Wordsigns (BE HIS WAS WERE)"}, 110 {"P3_11","3.11. Dot 5 Contractions"},{"P3_12","3.12. Dot 45 Contractions"},{"P3_13","3.13. Dot 456 Contractions"},{"P3_14","3.14. Final Letter Groupsigns"}, 111 {"P3_15","3.15. Shortform Words"},{"P4_1","4.1. Grade 1 Indicators"},{"P4_2","4.2. Capitals Indicators"},{"P4_3","4.3. Numeric Mode and Spatial math"}, 112 {"P4_4","4.4. Typeform Indicators (ITALIC SCRIPT UNDERLINE BOLDFACE)"},{"P5_1","5.1. Page Numbering"},{"P5_2","5.2. Headings"},{"P5_3","5.3. Lists"}, 113 {"P5_4","5.4. Poety / Drama"},{"P6_1","6.1. Operation and Comparison Signs"},{"P6_2","6.2. Grade 1 Mode"},{"P6_3","6.3. Special Print Symbols"}, 114 {"P6_4","6.4. Omission Marks"},{"P6_5","6.5. Shape Indicators"},{"P6_6","6.6. Roman Numerals"},{"P6_7","6.7. Fractions"}, 115 {"P7_1","7.1. Grade 1 Mode and Algebra"},{"P7_2","7.2. Grade 1 Mode and Fractions"},{"P7_3","7.3. Advanced Operation and Comparison Signs"},{"P7_4","7.4. Indices"}, 116 {"P7_5","7.5. Roots and Radicals"},{"P7_6","7.6. Miscellaneous Shape Indicators"},{"P7_7","7.7. Functions"},{"P7_8","7.8. Greek letters"}, 117 {"P8_1","8.1. Functions"},{"P8_2","8.2. Modifiers Bars and Dots"},{"P8_3","8.3. Modifiers Arrows and Limits"},{"P8_4","8.4. Probability"}, 118 {"P8_5","8.5. Calculus: Differentiation"},{"P8_6","8.6. Calculus: Integration"},{"P8_7","8.7. Vertical Bars"} 119 }; 120 this.partCodes = new String[this.parts.length]; 121 for (int i = 0; i < this.parts.length; i++) { 122 this.partCodes[i] = this.parts[i][0]; 123 } 124 125 // Panel for data entry 126 JPanel dataEntryPanel = new JPanel(); 127 dataEntryPanel.setLayout(new GridBagLayout()); 128 JPanel view = new JPanel(new BorderLayout()); 129 view.add(dataEntryPanel, BorderLayout.NORTH); 130 view.setBorder(javax.swing.BorderFactory.createEmptyBorder(20,20,20,20)); 131 JScrollPane dataEntryScrollPane = new JScrollPane(view); 132 dataEntryScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); 133 dataEntryScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); 134 dataEntryScrollPane.getAccessibleContext().setAccessibleName("Braille data entry scroll pane"); 135 136 GridBagConstraints gbc = new GridBagConstraints(); 137 gbc.insets = new Insets(2, 2, 2, 2); 138 gbc.fill = GridBagConstraints.HORIZONTAL; 139 gbc.weightx = 1.0; 140 gbc.weighty = 0.0; 141 142 this.titleLabel = new JLabel(baseTitle); 143 this.titleLabel.setFont(this.titleLabel.getFont().deriveFont(Font.BOLD, 28f)); 144 gbc.gridx = 0; 145 gbc.gridy = 0; 146 gbc.gridwidth = GridBagConstraints.REMAINDER; 147 dataEntryPanel.add(this.titleLabel, gbc); 148 149 gbc.gridy = 1; 150 gbc.gridwidth = GridBagConstraints.REMAINDER; 151 gbc.ipady = 20; 152 dataEntryPanel.add(new JPanel(), gbc); 153 154 // compute longest label width to align inputs 155 String[] labels = java.util.Arrays.stream(parts).map(x->x[1]).toArray(String[]::new); 156 int maxPx = com.studentgui.uicomp.PhaseScoreField.computeMaxLabelPixelWidth(com.studentgui.uicomp.PhaseScoreField.getLabelFont(), labels); 157 com.studentgui.uicomp.PhaseScoreField.setGlobalLabelWidth(Math.min(320, Math.max(140, maxPx + 50))); 158 skillFields = new com.studentgui.uicomp.PhaseScoreField[this.parts.length]; 159 for (int i = 0; i < this.parts.length; i++) { 160 gbc.gridy = i + 2; 161 gbc.gridx = 0; 162 gbc.gridwidth = 1; 163 com.studentgui.uicomp.PhaseScoreField skillField = new com.studentgui.uicomp.PhaseScoreField(this.parts[i][1], 0); 164 skillField.setName("braille_" + this.parts[i][0]); 165 skillField.getAccessibleContext().setAccessibleName(this.parts[i][1]); 166 skillField.setToolTipText("Enter a numeric score for " + this.parts[i][1]); 167 gbc.gridx = 0; gbc.gridwidth = 2; gbc.insets = new Insets(2, 2, 2, 2); 168 dataEntryPanel.add(skillField, gbc); 169 skillFields[i] = skillField; 170 gbc.gridx = 2; gbc.insets = new Insets(2, 0, 2, 2); 171 dataEntryPanel.add(new JPanel(), gbc); 172 } 173 174 gbc.gridy = this.parts.length + 3; 175 gbc.gridx = 0; 176 gbc.gridwidth = GridBagConstraints.REMAINDER; 177 gbc.weighty = 1.0; 178 dataEntryPanel.add(new JPanel(), gbc); 179 180 // Place Submit and Open Latest side-by-side (match IOS/ScreenReader style) 181 gbc.gridy = this.parts.length + 4; 182 gbc.weighty = 0.0; 183 gbc.gridx = 0; 184 gbc.gridwidth = 1; 185 gbc.anchor = GridBagConstraints.WEST; 186 JButton submitDataButton = new JButton("Submit Data"); 187 submitDataButton.setPreferredSize(new java.awt.Dimension(0, 32)); 188 submitDataButton.addActionListener((ActionEvent e) -> { submitData(); refreshGraph(); }); 189 submitDataButton.setMnemonic(KeyEvent.VK_S); 190 submitDataButton.setToolTipText("Save Braille scores for the selected student (Alt+S)"); 191 submitDataButton.getAccessibleContext().setAccessibleName("Submit Braille Data"); 192 dataEntryPanel.add(submitDataButton, gbc); 193 194 gbc.gridx = 1; 195 JButton openLatestBtn = new JButton("Open Latest Plot"); 196 openLatestBtn.setPreferredSize(new java.awt.Dimension(0, 32)); 197 openLatestBtn.addActionListener((ActionEvent e) -> { 198 java.nio.file.Path p = com.studentgui.apphelpers.Helpers.latestPlotPath(this.studentNameParam, "Braille"); 199 if (p == null) { 200 com.studentgui.apphelpers.UiNotifier.show("No Braille plot found for student"); 201 } else { 202 try { 203 java.awt.Desktop.getDesktop().open(p.toFile()); 204 } catch (java.io.IOException | UnsupportedOperationException | SecurityException ex) { 205 com.studentgui.apphelpers.UiNotifier.show("Unable to open plot: " + p.getFileName().toString()); 206 } 207 } 208 }); 209 dataEntryPanel.add(openLatestBtn, gbc); 210 211 // consume remaining columns (if any) so layout stays compact 212 gbc.gridx = 2; gbc.gridwidth = GridBagConstraints.REMAINDER; gbc.anchor = GridBagConstraints.WEST; 213 dataEntryPanel.add(new JPanel(), gbc); 214 215 add(dataEntryScrollPane, BorderLayout.CENTER); 216 217 // Add existing graph reference 218 add(lineGraph, BorderLayout.SOUTH); 219 220 SwingUtilities.invokeLater(() -> { 221 dataEntryPanel.setPreferredSize(dataEntryPanel.getPreferredSize()); 222 updateTitleDate(); 223 revalidate(); 224 }); 225 226 com.studentgui.apphelpers.Helpers.createFolderHierarchy(); 227 initDatabase(); 228 refreshGraph(); 229 } 230 231 /** 232 * Ensure the Braille progress-type and its assessment parts exist in the 233 * canonical schema. Safe to call repeatedly. 234 */ 235 private void initDatabase() { 236 // Ensure normalized schema parts for Braille exist 237 try { 238 int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("Braille"); 239 // Use the canonical part codes defined in this.parts 240 com.studentgui.apphelpers.Database.ensureAssessmentParts(ptId, this.partCodes); 241 } catch (SQLException e) { 242 LOG.error("Error initializing Braille parts", e); 243 } 244 } 245 246 /** 247 * Read entered skill values and persist them as a new progress session. 248 * Performs integer validation and informs the user on invalid input. 249 * 250 * Implementation note: arrays used to call {@code insertAssessmentResults} 251 * are allocated dynamically based on the actual number of parts 252 * ({@code partCodes.length}) so that the stored columns exactly match the 253 * plotted series. This fixes a previous issue where fixed-size arrays 254 * could become out-of-sync with the parts list. 255 */ 256 private void submitData() { 257 if (this.studentNameParam == null || this.studentNameParam.trim().isEmpty()) { 258 JOptionPane.showMessageDialog(this, "Please select a student before submitting Braille data.", "Missing student", JOptionPane.WARNING_MESSAGE); 259 return; 260 } 261 262 try { 263 int studentId = com.studentgui.apphelpers.Database.getOrCreateStudent(this.studentNameParam); 264 int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("Braille"); 265 int sessionId = com.studentgui.apphelpers.Database.createProgressSession(studentId, ptId, this.dateParam); 266 267 // Allocate arrays based on the actual number of parts so that 268 // the submitted data and plotted series stay in sync. 269 String[] codes = new String[this.partCodes.length]; 270 int[] scores = new int[this.partCodes.length]; 271 for (int i = 0; i < this.partCodes.length; i++) { 272 codes[i] = this.partCodes[i]; 273 scores[i] = skillFields[i].getValue(); 274 } 275 com.studentgui.apphelpers.Database.insertAssessmentResults(sessionId, ptId, codes, scores); 276 LOG.info("Data submitted successfully via normalized schema."); 277 com.studentgui.apphelpers.UiNotifier.show("Braille data saved."); 278 com.studentgui.apphelpers.dto.AssessmentPayload payload = new com.studentgui.apphelpers.dto.AssessmentPayload(sessionId, codes, scores); 279 java.nio.file.Path jsonOut = com.studentgui.apphelpers.SessionJsonWriter.writeSessionJson(this.studentNameParam, "Braille", payload, sessionId); 280 if (jsonOut == null) { 281 LOG.warn("Unable to save Braille session JSON for sessionId={}", sessionId); 282 } 283 try { 284 java.nio.file.Path plotsOut = com.studentgui.apphelpers.Helpers.studentPlotsDir(this.studentNameParam); 285 java.nio.file.Path reportsOut = com.studentgui.apphelpers.Helpers.studentReportsDir(this.studentNameParam); 286 java.nio.file.Files.createDirectories(plotsOut); 287 java.nio.file.Files.createDirectories(reportsOut); 288 java.time.format.DateTimeFormatter df = java.time.format.DateTimeFormatter.ISO_DATE; 289 String dateStr = this.dateParam != null ? this.dateParam.format(df) : java.time.LocalDate.now().toString(); 290 String baseName = "Braille-" + sessionId + "-" + dateStr; 291 292 com.studentgui.apphelpers.Database.ResultsWithDates rwd = com.studentgui.apphelpers.Database.fetchLatestAssessmentResultsWithDates(this.studentNameParam, "Braille", Integer.MAX_VALUE); 293 java.util.Map<String, java.nio.file.Path> groups = null; 294 String[] labels = new String[this.parts.length]; 295 for (int i = 0; i < this.parts.length; i++) { 296 labels[i] = this.parts[i][1]; 297 } 298 if (rwd != null && rwd.rows != null && !rwd.rows.isEmpty()) { 299 lineGraph.updateWithGroupedDataByDate(rwd.dates, rwd.rows, this.partCodes, labels); 300 groups = lineGraph.saveGroupedCharts(plotsOut, baseName, 1000, 240); 301 java.time.LocalDate headerDate = rwd.dates.get(rwd.dates.size() - 1); 302 dateStr = headerDate.format(df); 303 } else { 304 java.util.List<java.util.List<Integer>> rowsList = new java.util.ArrayList<>(); 305 java.util.List<Integer> latest = new java.util.ArrayList<>(); 306 for (int v : scores) { 307 latest.add(v); 308 } 309 rowsList.add(latest); 310 lineGraph.updateWithGroupedData(rowsList, this.partCodes); 311 groups = lineGraph.saveGroupedCharts(plotsOut, baseName, 1000, 240); 312 } 313 314 if (groups == null) { 315 groups = new java.util.LinkedHashMap<>(); 316 } 317 StringBuilder md = new StringBuilder(); 318 md.append("# ").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("\n\n"); 319 for (java.util.Map.Entry<String, java.nio.file.Path> e : groups.entrySet()) { 320 md.append("## ").append(e.getKey()).append("\n\n"); 321 md.append(".append(e.getValue().getFileName().toString()).append(")\n\n"); 322 } 323 java.nio.file.Path mdFile = reportsOut.resolve(baseName + ".md"); 324 // images live in ../plots relative to reports 325 String mdText = md.toString().replace("; 326 java.nio.file.Files.writeString(mdFile, mdText, java.nio.charset.StandardCharsets.UTF_8); 327 328 // HTML report using shared palette 329 try { 330 String[] palette = JLineGraph.PALETTE_HEX; 331 java.util.LinkedHashMap<String, java.util.List<Integer>> groupsIdx = new java.util.LinkedHashMap<>(); 332 for (int i = 0; i < this.partCodes.length; i++) { 333 String code = this.partCodes[i]; 334 String grp = code != null && code.contains("_") ? code.split("_")[0] : code; 335 groupsIdx.computeIfAbsent(grp, k -> new java.util.ArrayList<>()).add(i); 336 } 337 StringBuilder html = new StringBuilder(); 338 html.append("<!doctype html><html><head><meta charset=\"utf-8\"><title>"); 339 html.append(this.studentNameParam == null ? "Student Report" : this.studentNameParam).append(" - ").append(dateStr).append("</title>"); 340 html.append("<style>body{font-family:sans-serif;margin:20px;} img{max-width:100%;height:auto;border:1px solid #ccc;margin-bottom:8px;} .legend{max-height:160px;overflow:auto;border:1px solid #ddd;padding:8px;margin-bottom:24px;} .legend-item{display:flex;align-items:center;gap:8px;padding:4px 0;} .swatch{width:18px;height:12px;border:1px solid #333;display:inline-block}</style>"); 341 html.append("</head><body>"); 342 html.append("<h1>").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("</h1>"); 343 for (java.util.Map.Entry<String, java.nio.file.Path> e2 : groups.entrySet()) { 344 String grp = e2.getKey(); 345 String imgName = e2.getValue().getFileName().toString(); 346 html.append("<h2>").append(grp).append("</h2>"); 347 html.append("<div class=\"plot\"><img src=\"./").append(imgName).append("\" alt=\"").append(grp).append("\"></div>"); 348 java.util.List<Integer> idxs = groupsIdx.getOrDefault(grp, new java.util.ArrayList<>()); 349 html.append("<div class=\"legend\">"); 350 for (int s = 0; s < idxs.size(); s++) { 351 int idx = idxs.get(s); 352 String code = this.partCodes[idx]; 353 String human = this.parts[idx][1]; 354 String seriesName = code + " - " + human; 355 String color = palette[s % palette.length]; 356 html.append("<div class=\"legend-item\">"); 357 html.append("<span class=\"swatch\" style=\"background:"); 358 html.append(color); 359 html.append(";\"></span>"); 360 html.append("<div>"); 361 html.append(seriesName); 362 html.append("</div></div>"); 363 } 364 html.append("</div>"); 365 } 366 html.append("</body></html>"); 367 java.nio.file.Path htmlFile = reportsOut.resolve(baseName + ".html"); 368 String htmlStr = html.toString().replace("src=\"./", "src=\"../plots/"); 369 java.nio.file.Files.writeString(htmlFile, htmlStr, java.nio.charset.StandardCharsets.UTF_8); 370 LOG.info("Wrote Braille HTML session report {}", htmlFile); 371 } catch (java.io.IOException ioex) { 372 LOG.warn("Unable to write Braille HTML report: {}", ioex.toString()); 373 } 374 375 LOG.info("Wrote Braille session report {} with {} group images", mdFile, groups.size()); 376 } catch (java.io.IOException | SQLException ex) { 377 LOG.warn("Unable to save Braille per-phase plots or markdown report: {}", ex.toString()); 378 } 379 } catch (SQLException e) { 380 LOG.error("Unexpected error submitting braille data", e); 381 JOptionPane.showMessageDialog(this, "Database error saving Braille data: " + e.getMessage(), "Database error", JOptionPane.ERROR_MESSAGE); 382 } 383 } 384 /** 385 * Fetch recent assessment sessions and update the shared graph view. 386 */ 387 private void refreshGraph() { 388 try { 389 List<List<Integer>> allSkillValues = com.studentgui.apphelpers.Database.fetchLatestAssessmentResults(this.studentNameParam, "Braille", 5); 390 // Note: pages should supply the selected student name; here the existing code used a passed-in studentName variable 391 // We will try to use the first skill field's content as a student name fallback; in the UI flow this should be provided. 392 // For now use a placeholder when no student is selected. 393 if (allSkillValues != null && !allSkillValues.isEmpty()) { 394 lineGraph.updateWithGroupedData(allSkillValues, this.partCodes); 395 // Write to the consolidated per-run data dumps file when enabled 396 if (Boolean.parseBoolean(com.studentgui.apphelpers.Settings.get("dump.enabled", "false"))) { 397 try { 398 String appHome = System.getProperty("APP_HOME", com.studentgui.apphelpers.Helpers.APP_HOME.toString()); 399 String ts = System.getProperty("LOG_TS", String.valueOf(java.time.Instant.now().getEpochSecond())); 400 java.nio.file.Path logDir = java.nio.file.Paths.get(appHome).resolve("logs"); 401 java.nio.file.Files.createDirectories(logDir); 402 java.nio.file.Path logFile = logDir.resolve("data_dumps_" + ts + ".log"); 403 StringBuilder sb = new StringBuilder(); 404 java.time.format.DateTimeFormatter dtf = java.time.format.DateTimeFormatter.ISO_DATE_TIME; 405 sb.append("[Braille]").append(System.lineSeparator()); 406 sb.append(java.time.LocalDateTime.now().format(dtf)).append(" - student=").append(this.studentNameParam).append(System.lineSeparator()); 407 sb.append("data=").append(allSkillValues.toString()).append(System.lineSeparator()); 408 sb.append(System.lineSeparator()); 409 java.nio.file.Files.writeString(logFile, sb.toString(), java.nio.charset.StandardCharsets.UTF_8, java.nio.file.StandardOpenOption.CREATE, java.nio.file.StandardOpenOption.APPEND); 410 } catch (java.io.IOException ioe) { 411 LOG.trace("Unable to write braille load log: {}", ioe.toString()); 412 } 413 } 414 } else { 415 LOG.info("No data to plot; showing grouped placeholders."); 416 lineGraph.showEmptyGrouped(this.partCodes); 417 } 418 } catch (SQLException e) { 419 LOG.error("SQL error refreshing braille graph", e); 420 } 421 } 422 423 @Override 424 /** 425 * Update the displayed date for the Braille page and refresh content. 426 * 427 * Stores `dateParam` and schedules a UI refresh on the Swing EDT so the 428 * graph and title reflect the new date selection. 429 * 430 * @param newDate the date to display (may be null to use current date) 431 */ 432 public void dateChanged(final LocalDate newDate) { 433 this.dateParam = newDate; 434 SwingUtilities.invokeLater(() -> { 435 refreshGraph(); 436 updateTitleDate(); 437 }); 438 } 439 440 @Override 441 /** 442 * Update the selected student for the Braille page and refresh content. 443 * 444 * Sets `studentNameParam` and posts a refresh task to the Swing EDT to 445 * reload data and update the page title. 446 * 447 * @param newStudent student identifier (name or id) to display; may be null 448 */ 449 public void studentChanged(final String newStudent) { 450 this.studentNameParam = newStudent != null ? newStudent : "Unknown Student"; 451 SwingUtilities.invokeLater(() -> { 452 refreshGraph(); 453 updateTitleDate(); 454 }); 455 } 456 457 /** 458 * Update the page title label to include the current session date. 459 * Falls back to base title if date formatting fails. 460 */ 461 private void updateTitleDate() { 462 try { 463 String dateStr = this.dateParam != null ? this.dateParam.toString() : java.time.LocalDate.now().toString(); 464 this.titleLabel.setText(baseTitle + " - " + dateStr); 465 } catch (Exception ex) { 466 this.titleLabel.setText(baseTitle); 467 } 468 } 469 470 471}