001package com.studentgui.apppages; 002import java.awt.BorderLayout; 003import java.awt.Font; 004import java.awt.GridBagConstraints; 005import java.awt.GridBagLayout; 006import java.awt.Insets; 007import java.awt.event.ActionEvent; 008import java.awt.event.KeyEvent; 009import java.sql.SQLException; 010import java.time.LocalDate; 011import java.util.List; 012 013import javax.swing.JButton; 014import javax.swing.JLabel; 015import javax.swing.JOptionPane; 016import javax.swing.JPanel; 017import javax.swing.JScrollPane; 018import javax.swing.SwingUtilities; 019 020import org.slf4j.Logger; 021import org.slf4j.LoggerFactory; 022 023/** 024 * HumanWare BrailleNote Touch Plus (BNT+) proficiency assessment page. 025 * 026 * <p>Evaluates student competency with the BrailleNote Touch Plus refreshable braille notetaker 027 * and productivity device across 52 skills organized into 12 functional domains:</p> 028 * 029 * <ul> 030 * <li><b>Phase 1 (P1_1–P1_9): Device Fundamentals and Core Applications</b> 031 * <ul> 032 * <li>Physical layout (braille keyboard, navigation keys, touchscreen, ports)</li> 033 * <li>Setup procedures and universal commands (power, mode switching, context menus)</li> 034 * <li>BNT+ navigation paradigm (gestures, quick keys, braille commands)</li> 035 * <li>File management (folders, copy/paste, rename, delete)</li> 036 * <li>Word processor (KeyWord): document creation, editing, formatting</li> 037 * <li>Email (KeyMail): compose, send, receive, attachments</li> 038 * <li>Internet browsing (KeyWeb): navigation, bookmarks, forms</li> 039 * <li>Calculator and KeyMath (arithmetic, scientific functions)</li> 040 * </ul> 041 * </li> 042 * <li><b>Phase 2 (P2_1–P2_7): Productivity Suite Applications</b> 043 * <ul> 044 * <li>Calendar management (appointments, reminders, recurring events)</li> 045 * <li>KeyBRF (Braille file viewer/editor)</li> 046 * <li>KeyFiles (file explorer and organizer)</li> 047 * <li>KeyMail (advanced email features)</li> 048 * <li>KeyWeb (advanced browsing, accessibility modes)</li> 049 * <li>KeyCalc (spreadsheet concepts)</li> 050 * <li>KeyWord (advanced formatting, styles, tables)</li> 051 * </ul> 052 * </li> 053 * <li><b>Phase 3 (P3_1–P3_7): Advanced Applications and Accessibility</b> 054 * <ul> 055 * <li>KeySlides (presentation creation and delivery)</li> 056 * <li>KeyCode (text editor with syntax highlighting for programming)</li> 057 * <li>Third-party app integration (Dropbox, Google Drive, OneDrive)</li> 058 * <li>Braille input configuration (computer braille, contracted, literary)</li> 059 * <li>Braille output settings (display mode, translation tables)</li> 060 * <li>Device settings and preferences</li> 061 * <li>Accessibility features (speech output, magnification, contrast)</li> 062 * </ul> 063 * </li> 064 * <li><b>Phase 4 (P4_1–P4_3): Advanced File and Cloud Management</b></li> 065 * <li><b>Phase 5 (P5_1–P5_4): Collaboration and Export Workflows</b></li> 066 * <li><b>Phase 6 (P6_1–P6_3): App Ecosystem and Troubleshooting</b></li> 067 * <li><b>Phase 7 (P7_1–P7_4): Automation and Customization</b></li> 068 * <li><b>Phase 8 (P8_1–P8_5): Peripheral Integration</b> (Bluetooth/USB devices, displays, audio/video)</li> 069 * <li><b>Phase 9 (P9_1–P9_4): Security and Network Configuration</b></li> 070 * <li><b>Phase 10 (P10_1–P10_3): Speech Engine Customization</b></li> 071 * <li><b>Phase 11 (P11_1–P11_5): Maintenance and Support</b> (firmware, diagnostics, warranty)</li> 072 * <li><b>Phase 12 (P12_1–P12_4): Community and Online Resources</b></li> 073 * </ul> 074 * 075 * <p><b>Data Management and Artifacts:</b></p> 076 * <ul> 077 * <li>Scores captured via {@link com.studentgui.uicomp.PhaseScoreField} (integer 0–4 typical)</li> 078 * <li>Persisted to normalized schema via {@link com.studentgui.apphelpers.Database#insertAssessmentResults}</li> 079 * <li>JSON export: {@code StudentDataFiles/<student>/Sessions/BrailleNote/BrailleNote-<sessionId>-<timestamp>.json}</li> 080 * <li>Phase-grouped time-series plots: {@code plots/BrailleNote-<sessionId>-<date>-P<N>.png} (12 phase groups)</li> 081 * <li>Markdown and HTML reports with embedded plots and color-coded legends</li> 082 * </ul> 083 * 084 * <p>The shared {@link JLineGraph} visualizes recent session trends grouped by phase prefix. 085 * Implements {@link com.studentgui.app.DateChangeListener} and {@link com.studentgui.app.StudentChangeListener} 086 * for dynamic updates when global student/date selections change.</p> 087 * 088 * @see com.studentgui.apphelpers.Database 089 * @see JLineGraph 090 * @see com.studentgui.uicomp.PhaseScoreField 091 */ 092public class BrailleNote extends JPanel implements com.studentgui.app.DateChangeListener, com.studentgui.app.StudentChangeListener { 093 private static final Logger LOG = LoggerFactory.getLogger(BrailleNote.class); 094 095 /** Inputs for each BrailleNote skill. */ 096 private final com.studentgui.uicomp.PhaseScoreField[] skillFields; 097 /** Canonical assessment part codes and labels for BrailleNote. */ 098 private final String[][] parts; 099 /** Shared graph component for plotting results. */ 100 private final JLineGraph lineGraph; // Reference to the JLineGraph instance 101 /** Display name of the selected student (may be null). */ 102 private String studentNameParam; 103 /** Header title label for this page. */ 104 private JLabel titleLabel; 105 /** Base page title string used when rendering the header (date appended). */ 106 private final String baseTitle = "BrailleNote Skills Progression"; 107 /** Session date associated with persisted progress. */ 108 private LocalDate dateParam; 109 110 /** 111 * Create the BrailleNote page for a specific student and date. 112 * 113 * @param studentName the selected student name (may be null until a student is chosen) 114 * @param date the date for the session (used when creating a progress session) 115 * @param lineGraph shared graph component used to display recent results 116 */ 117 public BrailleNote(String studentName, LocalDate date, JLineGraph lineGraph) { 118 this.studentNameParam = (studentName == null || studentName.trim().isEmpty()) ? com.studentgui.apphelpers.Helpers.defaultStudent() : studentName; 119 this.dateParam = date; 120 this.lineGraph = lineGraph; // Use the passed in graph instance 121 setLayout(new BorderLayout()); 122 123 this.parts = new String[][]{ 124 {"P1_1","1.1 Physical Layout"},{"P1_2","1.2 Setup/Universal Commands"},{"P1_3","1.3 BNT+ Navigation"},{"P1_4","1.4 File Management"},{"P1_5","1.5 Word Processor"},{"P1_6","1.6 Email"},{"P1_7","1.7 Internet"},{"P1_8","1.8 Calculator"},{"P1_9","1.9 KeyMath"}, 125 {"P2_1","2.1 Calendar"},{"P2_2","2.2 KeyBRF"},{"P2_3","2.3 KeyFiles"},{"P2_4","2.4 KeyMail"},{"P2_5","2.5 KeyWeb"},{"P2_6","2.6 KeyCalc"},{"P2_7","2.7 KeyWord"}, 126 {"P3_1","3.1 KeySlides"},{"P3_2","3.2 KeyCode"},{"P3_3","3.3 Third Party Apps"},{"P3_4","3.4 Braille Input"},{"P3_5","3.5 Braille Output"},{"P3_6","3.6 Settings"},{"P3_7","3.7 Accessibility"}, 127 {"P4_1","4.1 Advanced File Management"},{"P4_2","4.2 Cloud Integration"},{"P4_3","4.3 Device Maintenance"}, 128 {"P5_1","5.1 Collaboration"},{"P5_2","5.2 Export/Import"},{"P5_3","5.3 Printing"},{"P5_4","5.4 Backup"}, 129 {"P6_1","6.1 App Installation"},{"P6_2","6.2 App Updates"},{"P6_3","6.3 Troubleshooting"}, 130 {"P7_1","7.1 Custom Shortcuts"},{"P7_2","7.2 Macros"},{"P7_3","7.3 Scripting"},{"P7_4","7.4 Automation"}, 131 {"P8_1","8.1 Bluetooth Devices"},{"P8_2","8.2 USB Devices"},{"P8_3","8.3 External Displays"},{"P8_4","8.4 Audio Output"},{"P8_5","8.5 Video Output"}, 132 {"P9_1","9.1 Security"},{"P9_2","9.2 User Accounts"},{"P9_3","9.3 Parental Controls"},{"P9_4","9.4 Network Settings"}, 133 {"P10_1","10.1 Speech Settings"},{"P10_2","10.2 Voice Profiles"},{"P10_3","10.3 Language Support"}, 134 {"P11_1","11.1 Firmware Updates"},{"P11_2","11.2 Diagnostics"},{"P11_3","11.3 Logs"},{"P11_4","11.4 Support"},{"P11_5","11.5 Warranty"}, 135 {"P12_1","12.1 Community Resources"},{"P12_2","12.2 Online Help"},{"P12_3","12.3 User Forums"},{"P12_4","12.4 Feedback"} 136 }; 137 138 // Panel for data entry 139 JPanel dataEntryPanel = new JPanel(); 140 dataEntryPanel.setLayout(new GridBagLayout()); 141 JScrollPane dataEntryScrollPane = new JScrollPane(dataEntryPanel); 142 dataEntryScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); 143 dataEntryScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); 144 dataEntryScrollPane.getAccessibleContext().setAccessibleName("BrailleNote data entry scroll pane"); 145 146 GridBagConstraints gbc = new GridBagConstraints(); 147 gbc.insets = new Insets(5, 5, 5, 5); 148 gbc.fill = GridBagConstraints.HORIZONTAL; 149 gbc.weightx = 1.0; 150 gbc.weighty = 0.0; 151 152 this.titleLabel = new JLabel(baseTitle); 153 this.titleLabel.setFont(this.titleLabel.getFont().deriveFont(Font.BOLD, 28f)); 154 gbc.gridx = 0; 155 gbc.gridy = 0; 156 gbc.gridwidth = GridBagConstraints.REMAINDER; 157 dataEntryPanel.add(this.titleLabel, gbc); 158 159 gbc.gridy = 1; 160 gbc.gridwidth = GridBagConstraints.REMAINDER; 161 gbc.ipady = 20; 162 dataEntryPanel.add(new JPanel(), gbc); 163 164 // layout spacing handled by PhaseScoreField 165 166 // compute pixel width using font metrics so labels align precisely 167 String[] labelsArr = java.util.Arrays.stream(parts).map(x->x[1]).toArray(String[]::new); 168 int maxPx = com.studentgui.uicomp.PhaseScoreField.computeMaxLabelPixelWidth(com.studentgui.uicomp.PhaseScoreField.getLabelFont(), labelsArr); 169 com.studentgui.uicomp.PhaseScoreField.setGlobalLabelWidth(Math.min(320, Math.max(140, maxPx + 50))); 170 skillFields = new com.studentgui.uicomp.PhaseScoreField[parts.length]; 171 for (int i = 0; i < parts.length; i++) { 172 gbc.gridy = i + 2; 173 gbc.gridx = 0; 174 gbc.gridwidth = 1; 175 com.studentgui.uicomp.PhaseScoreField field = new com.studentgui.uicomp.PhaseScoreField(parts[i][1], 0); 176 field.setName("braillenote_" + parts[i][0]); 177 field.getAccessibleContext().setAccessibleName(parts[i][1]); 178 field.setToolTipText("Enter a numeric score for " + parts[i][1]); 179 gbc.gridx = 0; gbc.gridwidth = 2; gbc.insets = new Insets(5, 5, 5, 5); 180 dataEntryPanel.add(field, gbc); 181 skillFields[i] = field; 182 gbc.gridx = 2; gbc.gridwidth = 1; gbc.insets = new Insets(5, 0, 5, 5); 183 dataEntryPanel.add(new JPanel(), gbc); 184 } 185 186 gbc.gridy = parts.length + 3; 187 gbc.gridx = 0; 188 gbc.gridwidth = GridBagConstraints.REMAINDER; 189 gbc.weighty = 1.0; 190 dataEntryPanel.add(new JPanel(), gbc); 191 192 gbc.gridy = parts.length + 4; 193 gbc.weighty = 0.0; 194 // layout spacing handled by PhaseScoreField 195 // Place Submit and Open Latest side-by-side like IOS/ScreenReader 196 gbc.gridy = parts.length + 4; gbc.gridx = 0; gbc.gridwidth = 1; gbc.anchor = GridBagConstraints.WEST; 197 JButton submitDataButton = new JButton("Submit Data"); 198 submitDataButton.setPreferredSize(new java.awt.Dimension(0, 32)); 199 submitDataButton.addActionListener((ActionEvent e) -> { submitData(); refreshGraph(); }); 200 submitDataButton.setMnemonic(KeyEvent.VK_S); 201 submitDataButton.setToolTipText("Save BrailleNote scores for the selected student (Alt+S)"); 202 submitDataButton.getAccessibleContext().setAccessibleName("Submit BrailleNote Data"); 203 dataEntryPanel.add(submitDataButton, gbc); 204 205 gbc.gridx = 1; gbc.gridwidth = 1; gbc.anchor = GridBagConstraints.WEST; 206 JButton openLatestBtn = new JButton("Open Latest Plot"); 207 openLatestBtn.setPreferredSize(new java.awt.Dimension(0, 32)); 208 openLatestBtn.addActionListener((ActionEvent e) -> { 209 java.nio.file.Path p = com.studentgui.apphelpers.Helpers.latestPlotPath(this.studentNameParam, "BrailleNote"); 210 if (p == null) { 211 com.studentgui.apphelpers.UiNotifier.show("No BrailleNote plot found for student"); 212 } else { 213 try { 214 java.awt.Desktop.getDesktop().open(p.toFile()); 215 } catch (java.io.IOException | UnsupportedOperationException | SecurityException ex) { 216 com.studentgui.apphelpers.UiNotifier.show("Unable to open plot: " + p.getFileName().toString()); 217 } 218 } 219 }); 220 dataEntryPanel.add(openLatestBtn, gbc); 221 222 add(dataEntryScrollPane, BorderLayout.CENTER); 223 224 // Add existing graph reference 225 add(lineGraph, BorderLayout.SOUTH); 226 227 SwingUtilities.invokeLater(() -> { 228 dataEntryPanel.setPreferredSize(dataEntryPanel.getPreferredSize()); 229 updateTitleDate(); 230 revalidate(); 231 }); 232 233 // Ensure application folders and DB schema exist 234 com.studentgui.apphelpers.Helpers.createFolderHierarchy(); 235 initDatabase(); 236 refreshGraph(); 237 } 238 239 /** 240 * Ensure the progress-type and assessment part rows for BrailleNote exist 241 * in the normalized schema. This is safe to call repeatedly. 242 */ 243 private void initDatabase() { 244 try { 245 int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("BrailleNote"); 246 String[] codes = new String[this.parts.length]; 247 for (int i = 0; i < this.parts.length; i++) { 248 codes[i] = this.parts[i][0]; 249 } 250 com.studentgui.apphelpers.Database.ensureAssessmentParts(ptId, codes); 251 } catch (SQLException e) { 252 LOG.error("SQL error initializing braille note parts", e); 253 } 254 } 255 256 /** 257 * Read the values entered into the skill fields and persist them to the 258 * database as a new progress session. Validation is performed to ensure 259 * numeric integer input; users are prompted on invalid values. 260 */ 261 private void submitData() { 262 if (this.studentNameParam == null || this.studentNameParam.trim().isEmpty()) { 263 JOptionPane.showMessageDialog(this, "Please select a student before submitting BrailleNote data.", "Missing student", JOptionPane.WARNING_MESSAGE); 264 return; 265 } 266 267 try { 268 int studentId = com.studentgui.apphelpers.Database.getOrCreateStudent(this.studentNameParam); 269 int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("BrailleNote"); 270 int sessionId = com.studentgui.apphelpers.Database.createProgressSession(studentId, ptId, this.dateParam); 271 String[] codes = new String[parts.length]; 272 int[] scores = new int[parts.length]; 273 for (int i = 0; i < parts.length && i < skillFields.length; i++) { 274 codes[i] = parts[i][0]; 275 scores[i] = skillFields[i].getValue(); 276 } 277 com.studentgui.apphelpers.Database.insertAssessmentResults(sessionId, ptId, codes, scores); 278 LOG.info("Data submitted successfully via normalized schema."); 279 com.studentgui.apphelpers.UiNotifier.show("BrailleNote data saved."); 280 com.studentgui.apphelpers.dto.AssessmentPayload payload = new com.studentgui.apphelpers.dto.AssessmentPayload(sessionId, codes, scores); 281 java.nio.file.Path jsonOut = com.studentgui.apphelpers.SessionJsonWriter.writeSessionJson(this.studentNameParam, "BrailleNote", payload, sessionId); 282 if (jsonOut == null) { 283 LOG.warn("Unable to save BrailleNote session JSON for sessionId={}", sessionId); 284 } 285 try { 286 java.nio.file.Path plotsOut = com.studentgui.apphelpers.Helpers.studentPlotsDir(this.studentNameParam); 287 java.nio.file.Path reportsOut = com.studentgui.apphelpers.Helpers.studentReportsDir(this.studentNameParam); 288 java.nio.file.Files.createDirectories(plotsOut); 289 java.nio.file.Files.createDirectories(reportsOut); 290 java.time.format.DateTimeFormatter df = java.time.format.DateTimeFormatter.ISO_DATE; 291 String dateStr = this.dateParam != null ? this.dateParam.format(df) : java.time.LocalDate.now().toString(); 292 String baseName = "BrailleNote-" + sessionId + "-" + dateStr; 293 294 com.studentgui.apphelpers.Database.ResultsWithDates rwd = com.studentgui.apphelpers.Database.fetchLatestAssessmentResultsWithDates(this.studentNameParam, "BrailleNote", Integer.MAX_VALUE); 295 java.util.Map<String, java.nio.file.Path> groups = null; 296 String[] labels = new String[this.parts.length]; 297 for (int i = 0; i < this.parts.length; i++) { 298 labels[i] = this.parts[i][1]; 299 } 300 if (rwd != null && rwd.rows != null && !rwd.rows.isEmpty()) { 301 lineGraph.updateWithGroupedDataByDate(rwd.dates, rwd.rows, codes, labels); 302 groups = lineGraph.saveGroupedCharts(plotsOut, baseName, 1000, 240); 303 java.time.LocalDate headerDate = rwd.dates.get(rwd.dates.size() - 1); 304 dateStr = headerDate.format(df); 305 } else { 306 java.util.List<java.util.List<Integer>> rowsList = new java.util.ArrayList<>(); 307 java.util.List<Integer> latest = new java.util.ArrayList<>(); 308 for (int v : scores) { 309 latest.add(v); 310 } 311 rowsList.add(latest); 312 lineGraph.updateWithGroupedData(rowsList, codes); 313 groups = lineGraph.saveGroupedCharts(plotsOut, baseName, 1000, 240); 314 } 315 316 if (groups == null) { 317 groups = new java.util.LinkedHashMap<>(); 318 } 319 StringBuilder md = new StringBuilder(); 320 md.append("# ").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("\n\n"); 321 for (java.util.Map.Entry<String, java.nio.file.Path> e : groups.entrySet()) { 322 md.append("## ").append(e.getKey()).append("\n\n"); 323 md.append(".append(e.getValue().getFileName().toString()).append(")\n\n"); 324 } 325 java.nio.file.Path mdFile = reportsOut.resolve(baseName + ".md"); 326 String mdText = md.toString().replace("; 327 java.nio.file.Files.writeString(mdFile, mdText, java.nio.charset.StandardCharsets.UTF_8); 328 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 < codes.length; i++) { 333 String code = codes[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 = codes[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 BrailleNote HTML session report {}", htmlFile); 371 } catch (java.io.IOException ioex) { 372 LOG.warn("Unable to write BrailleNote HTML report: {}", ioex.toString()); 373 } 374 } catch (java.io.IOException ioe) { 375 LOG.warn("Unable to save BrailleNote per-phase plots or markdown report: {}", ioe.toString()); 376 } 377 } catch (SQLException e) { 378 LOG.error("SQL error saving braille note data", e); 379 JOptionPane.showMessageDialog(this, "Database error saving BrailleNote data: " + e.getMessage(), "Database error", JOptionPane.ERROR_MESSAGE); 380 } 381 } 382 383 /** 384 * Query the most recent assessment sessions for this student and update 385 * the shared {@link JLineGraph} with the returned values. 386 */ 387 private void refreshGraph() { 388 try { 389 List<List<Integer>> allSkillValues = com.studentgui.apphelpers.Database.fetchLatestAssessmentResults(studentNameParam, "BrailleNote", 5); 390 if (allSkillValues != null && !allSkillValues.isEmpty()) { 391 String[] codes = new String[this.parts.length]; 392 for (int i = 0; i < this.parts.length; i++) { 393 codes[i] = this.parts[i][0]; 394 } 395 lineGraph.updateWithGroupedData(allSkillValues, codes); 396 // Write to the consolidated per-run data dumps file when enabled 397 if (Boolean.parseBoolean(com.studentgui.apphelpers.Settings.get("dump.enabled", "false"))) { 398 try { 399 String appHome = System.getProperty("APP_HOME", com.studentgui.apphelpers.Helpers.APP_HOME.toString()); 400 String ts = System.getProperty("LOG_TS", String.valueOf(java.time.Instant.now().getEpochSecond())); 401 java.nio.file.Path logDir = java.nio.file.Paths.get(appHome).resolve("logs"); 402 java.nio.file.Files.createDirectories(logDir); 403 java.nio.file.Path logFile = logDir.resolve("data_dumps_" + ts + ".log"); 404 StringBuilder sb = new StringBuilder(); 405 sb.append("[BrailleNote]").append(System.lineSeparator()); 406 sb.append(java.time.Instant.now().toString()).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 BrailleNote load log: {}", ioe.toString()); 412 } 413 } 414 } else { 415 LOG.info("No data to plot."); 416 // Ensure the graph shows grouped placeholders matching the 417 // canonical assessment part ordering so the UI displays 418 // one subchart per P# prefix even with no sessions. 419 String[] codes = new String[this.parts.length]; 420 for (int i = 0; i < this.parts.length; i++) { 421 codes[i] = this.parts[i][0]; 422 } 423 lineGraph.showEmptyGrouped(codes); 424 } 425 } catch (SQLException e) { 426 LOG.error("SQL error refreshing braille note graph", e); 427 } 428 } 429 430 @Override 431 /** 432 * Update the currently displayed date for this page and refresh the UI. 433 * 434 * Sets the internal `dateParam` and schedules a refresh of the graph and 435 * title on the Swing Event Dispatch Thread. 436 * 437 * @param newDate the date to display (may be null to use the current date) 438 */ 439 440 public void dateChanged(LocalDate newDate) { 441 this.dateParam = newDate; 442 SwingUtilities.invokeLater(() -> { 443 refreshGraph(); 444 updateTitleDate(); 445 }); 446 } 447 448 @Override 449 /** 450 * Update the selected student for this page and refresh the UI. 451 * 452 * Sets the internal `studentNameParam` and schedules a refresh of the 453 * graph and title on the Swing Event Dispatch Thread. 454 * 455 * @param newStudent student identifier (name or id) to display; may be null 456 */ 457 458 public void studentChanged(String newStudent) { 459 this.studentNameParam = newStudent; 460 SwingUtilities.invokeLater(() -> { 461 refreshGraph(); 462 updateTitleDate(); 463 }); 464 } 465 466 private void updateTitleDate() { 467 try { 468 String dateStr = this.dateParam != null ? this.dateParam.toString() : java.time.LocalDate.now().toString(); 469 this.titleLabel.setText(baseTitle + " - " + dateStr); 470 } catch (Exception ex) { 471 this.titleLabel.setText(baseTitle); 472 } 473 } 474 475 476}