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