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("![](./", "![](../plots/");
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}