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;
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 * Abacus computational skills assessment page.
025 *
026 * <p>Provides a structured interface for evaluating student proficiency with the Cranmer
027 * Abacus across 22 standardized skills organized into 8 progressive competency phases:</p>
028 *
029 * <ul>
030 *   <li><b>Phase 1 (P1_1–P1_4):</b> Foundational bead manipulation (setting, clearing, place value, vocabulary)</li>
031 *   <li><b>Phase 2 (P2_1–P2_3):</b> Single-digit addition (direct and indirect methods)</li>
032 *   <li><b>Phase 3 (P3_1–P3_3):</b> Single-digit subtraction (direct and indirect methods)</li>
033 *   <li><b>Phase 4 (P4_1–P4_2):</b> Multiplication with multi-digit operands</li>
034 *   <li><b>Phase 5 (P5_1–P5_2):</b> Division with multi-digit operands</li>
035 *   <li><b>Phase 6 (P6_1–P6_4):</b> Decimal arithmetic (all four operations)</li>
036 *   <li><b>Phase 7 (P7_1–P7_4):</b> Fraction arithmetic (all four operations)</li>
037 *   <li><b>Phase 8 (P8_1–P8_2):</b> Advanced operations (percentages, square roots)</li>
038 * </ul>
039 *
040 * <p><b>Data Persistence and Export:</b></p>
041 * <ul>
042 *   <li>Skill scores are captured via {@link com.studentgui.uicomp.PhaseScoreField} components (integer 0–4 typical)</li>
043 *   <li>Submit button persists values to normalized schema using {@link com.studentgui.apphelpers.Database#insertAssessmentResults}</li>
044 *   <li>Session data exported to timestamped JSON in {@code StudentDataFiles/<student>/Sessions/Abacus/}</li>
045 *   <li>Per-phase time-series plots generated and saved to {@code plots/} directory</li>
046 *   <li>Comprehensive Markdown and HTML reports generated with embedded phase plots and color-coded legends</li>
047 * </ul>
048 *
049 * <p><b>Report Artifacts:</b></p>
050 * <ul>
051 *   <li><b>JSON export:</b> {@code Abacus-<sessionId>-<timestamp>.json} with session envelope</li>
052 *   <li><b>Phase group plots:</b> {@code Abacus-<sessionId>-<date>-P<N>.png} (8 PNG images)</li>
053 *   <li><b>Markdown report:</b> {@code reports/Abacus-<sessionId>-<date>.md} with relative image links</li>
054 *   <li><b>HTML report:</b> {@code reports/Abacus-<sessionId>-<date>.html} with inline styles and legends</li>
055 * </ul>
056 *
057 * <p>The shared {@link JLineGraph} visualizes recent session trends, grouping skills by phase prefix
058 * to maintain chart readability. Implements {@link com.studentgui.app.DateChangeListener} and
059 * {@link com.studentgui.app.StudentChangeListener} for dynamic updates when global selections change.</p>
060 *
061 * @see com.studentgui.apphelpers.Database
062 * @see JLineGraph
063 * @see com.studentgui.uicomp.PhaseScoreField
064 */
065public class Abacus extends JPanel implements com.studentgui.app.DateChangeListener, com.studentgui.app.StudentChangeListener {
066    private static final Logger LOG = LoggerFactory.getLogger(Abacus.class);
067
068    /** Array of input components for each skill. */
069    private final com.studentgui.uicomp.PhaseScoreField[] skillFields;
070    /** Canonical list of abacus assessment parts: code and display label. */
071    private final String[][] parts;
072    /** Shared graph component used to visualize recent results. */
073    private final JLineGraph lineGraph; // Reference to the JLineGraph instance
074    /** Selected student display name (may be null). */
075    private String studentNameParam;
076    /** Session date associated with persisted progress. */
077    private LocalDate dateParam;
078    /**
079     * Title label shown at the top of the page.
080     */
081    private JLabel titleLabel;
082    /**
083     * Base title text used when rendering the page header (date suffixes are appended).
084     */
085    private final String baseTitle = "Abacus Skills Progression";
086
087    /**
088     * Construct the Abacus page for the given student and session date.
089     *
090     * @param studentName the selected student's display name (may be null before selection)
091     * @param date the date to associate with created progress sessions
092     * @param lineGraph the shared graph component used to visualize results
093     */
094    public Abacus(final String studentName, final LocalDate date, final JLineGraph lineGraph) {
095    this.studentNameParam = (studentName == null || studentName.trim().isEmpty()) ? com.studentgui.apphelpers.Helpers.defaultStudent() : studentName;
096        this.dateParam = date;
097        this.lineGraph = lineGraph; // Use the passed in graph instance
098        setLayout(new BorderLayout());
099
100        // Initialize skills array and layout using canonical abacus parts
101        this.parts = new String[][]{
102            {"P1_1","1.1 Setting Numbers"},{"P1_2","1.2 Clearing Beads"},{"P1_3","1.3 Place Value"},{"P1_4","1.4 Vocabulary"},
103            {"P2_1","2.1 Addition of Single Digit Numbers"},{"P2_2","2.2 Direct Addition"},{"P2_3","2.3 Indirect Addition"},
104            {"P3_1","3.1 Subtraction of Single Digit Numbers"},{"P3_2","3.2 Direct Subtraction"},{"P3_3","3.3 Indirect Subtraction"},
105            {"P4_1","4.1 Multiplication – 2+ Digit Multiplicand 1-Digit Multiplier"},{"P4_2","4.2 Multiplication – 2+ Digit Multiplicand AND Multiplier"},
106            {"P5_1","5.1 Division – 2+ Digit Dividend 1-Digit Divisor"},{"P5_2","5.2 Division – 2+ Digit Dividend AND 1 Digit Divisor"},
107            {"P6_1","6.1 Addition of Decimals"},{"P6_2","6.2 Subtraction of Decimals"},{"P6_3","6.3 Multiplication of Decimals"},{"P6_4","6.4 Division of Decimals"},
108            {"P7_1","7.1 Addition of Fractions"},{"P7_2","7.2 Subtraction of Fractions"},{"P7_3","7.3 Multiplication of Fractions"},{"P7_4","7.4 Division of Fractions"},
109            {"P8_1","8.1 Percent"},{"P8_2","8.2 Square Root"}
110        };
111
112        // Panel for data entry
113        JPanel dataEntryPanel = new JPanel();
114        dataEntryPanel.setLayout(new GridBagLayout());
115    JPanel view = new JPanel(new BorderLayout());
116    view.add(dataEntryPanel, BorderLayout.NORTH);
117    view.setBorder(javax.swing.BorderFactory.createEmptyBorder(20,20,20,20));
118    JScrollPane dataEntryScrollPane = new JScrollPane(view);
119    dataEntryScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
120    dataEntryScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
121    dataEntryScrollPane.getAccessibleContext().setAccessibleName("Abacus data entry scroll pane");
122
123    GridBagConstraints gbc = new GridBagConstraints();
124    gbc.insets = new Insets(2, 2, 2, 2);
125        gbc.fill = GridBagConstraints.HORIZONTAL;
126        gbc.weightx = 1.0;
127        gbc.weighty = 0.0;
128
129    this.titleLabel = new JLabel(baseTitle);
130    this.titleLabel.setFont(this.titleLabel.getFont().deriveFont(Font.BOLD, 28f));
131        gbc.gridx = 0;
132        gbc.gridy = 0;
133        gbc.gridwidth = GridBagConstraints.REMAINDER;
134        dataEntryPanel.add(titleLabel, gbc);
135
136        gbc.gridy = 1;
137        gbc.gridwidth = GridBagConstraints.REMAINDER;
138        gbc.ipady = 20;
139        dataEntryPanel.add(new JPanel(), gbc);
140
141    // visual spacing controlled by PhaseScoreField and layout
142
143    String[] labels = java.util.Arrays.stream(this.parts).map(x->x[1]).toArray(String[]::new);
144        int maxPx = com.studentgui.uicomp.PhaseScoreField.computeMaxLabelPixelWidth(com.studentgui.uicomp.PhaseScoreField.getLabelFont(), labels);
145        com.studentgui.uicomp.PhaseScoreField.setGlobalLabelWidth(Math.min(320, Math.max(140, maxPx + 50)));
146    skillFields = new com.studentgui.uicomp.PhaseScoreField[this.parts.length];
147    for (int i = 0; i < this.parts.length; i++) {
148            gbc.gridy = i + 2;
149            gbc.gridx = 0;
150            gbc.gridwidth = 1;
151            com.studentgui.uicomp.PhaseScoreField field = new com.studentgui.uicomp.PhaseScoreField(this.parts[i][1], 0);
152            field.setName("abacus_" + this.parts[i][0]);
153            field.getAccessibleContext().setAccessibleName(this.parts[i][1]);
154            field.setToolTipText("Enter a numeric score for " + this.parts[i][1]);
155            gbc.gridx = 0; gbc.gridwidth = 2; gbc.insets = new Insets(2, 2, 2, 2);
156            dataEntryPanel.add(field, gbc);
157            skillFields[i] = field;
158            gbc.gridx = 2; gbc.gridwidth = 1; gbc.insets = new Insets(2, 0, 2, 2);
159            dataEntryPanel.add(new JPanel(), gbc);
160        }
161
162    gbc.gridy = this.parts.length + 3;
163        gbc.gridx = 0;
164        gbc.gridwidth = GridBagConstraints.REMAINDER;
165        gbc.weighty = 1.0;
166        dataEntryPanel.add(new JPanel(), gbc);
167
168    // Place Submit and Open Latest side-by-side with IOS-like height
169    gbc.gridy = this.parts.length + 4;
170    gbc.weighty = 0.0;
171    gbc.gridx = 0;
172    gbc.gridwidth = 1;
173    JButton submitDataButton = new JButton("Submit Data");
174    submitDataButton.setPreferredSize(new java.awt.Dimension(0, 32));
175    submitDataButton.addActionListener((ActionEvent e) -> { submitData(); refreshGraph(); });
176    submitDataButton.setMnemonic(KeyEvent.VK_S);
177    submitDataButton.setToolTipText("Save Abacus scores for the selected student (Alt+S)");
178    submitDataButton.getAccessibleContext().setAccessibleName("Submit Abacus Data");
179    dataEntryPanel.add(submitDataButton, gbc);
180
181    gbc.gridx = 1;
182    JButton openLatestBtn = new JButton("Open Latest Plot");
183    openLatestBtn.setPreferredSize(new java.awt.Dimension(0, 32));
184    openLatestBtn.addActionListener((ActionEvent e) -> {
185        java.nio.file.Path p = com.studentgui.apphelpers.Helpers.latestPlotPath(this.studentNameParam, "Abacus");
186        if (p == null) {
187            com.studentgui.apphelpers.UiNotifier.show("No Abacus plot found for student");
188        } else {
189            try {
190                java.awt.Desktop.getDesktop().open(p.toFile());
191            } catch (java.io.IOException | UnsupportedOperationException | SecurityException ex) {
192                com.studentgui.apphelpers.UiNotifier.show("Unable to open plot: " + p.getFileName().toString());
193            }
194        }
195    });
196    dataEntryPanel.add(openLatestBtn, gbc);
197
198    gbc.gridx = 2; gbc.gridwidth = GridBagConstraints.REMAINDER;
199    dataEntryPanel.add(new JPanel(), gbc);
200
201        add(dataEntryScrollPane, BorderLayout.CENTER);
202
203        // Add existing graph reference
204        add(lineGraph, BorderLayout.SOUTH);
205
206        SwingUtilities.invokeLater(() -> {
207            dataEntryPanel.setPreferredSize(dataEntryPanel.getPreferredSize());
208            updateTitleDate();
209            revalidate();
210        });
211
212        // Ensure application folders and DB schema exist before DB operations
213        com.studentgui.apphelpers.Helpers.createFolderHierarchy();
214        initDatabase();
215        refreshGraph();
216    }
217
218    /**
219     * Ensure the canonical progress-type and assessment parts for Abacus exist
220     * in the normalized database schema. Safe to call multiple times.
221     */
222    private void initDatabase() {
223        try {
224            int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("Abacus");
225            // Use the canonical part codes declared on this page so parts are created
226            // with the expected codes like "P1_1", "P1_2", ...
227            String[] codes = new String[this.parts.length];
228            for (int i = 0; i < this.parts.length; i++) {
229                codes[i] = this.parts[i][0];
230            }
231            com.studentgui.apphelpers.Database.ensureAssessmentParts(ptId, codes);
232            try {
233                com.studentgui.apphelpers.Database.cleanupAssessmentParts(ptId, codes);
234            } catch (SQLException se) {
235                LOG.warn("Could not cleanup legacy parts for Abacus", se);
236            }
237        } catch (SQLException e) {
238            LOG.error("SQL error initializing Abacus parts", e);
239        }
240    }
241
242    /**
243     * Read input fields, validate numeric input, and persist the values as a
244     * new progress session for the selected student.
245     */
246    private void submitData() {
247        if (studentNameParam == null || studentNameParam.trim().isEmpty()) {
248            JOptionPane.showMessageDialog(this, "Please select a student before submitting Abacus data.", "Missing student", JOptionPane.WARNING_MESSAGE);
249            return;
250        }
251
252        try {
253            int studentId = com.studentgui.apphelpers.Database.getOrCreateStudent(studentNameParam);
254            int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("Abacus");
255            int sessionId = com.studentgui.apphelpers.Database.createProgressSession(studentId, ptId, dateParam);
256
257            String[] codes = new String[this.parts.length];
258            int[] scores = new int[this.parts.length];
259            for (int i = 0; i < this.parts.length; i++) {
260                codes[i] = this.parts[i][0];
261                scores[i] = skillFields[i].getValue();
262            }
263            com.studentgui.apphelpers.Database.insertAssessmentResults(sessionId, ptId, codes, scores);
264            LOG.info("Data submitted successfully via normalized schema.");
265            com.studentgui.apphelpers.UiNotifier.show("Abacus data saved.");
266            // Also persist this session as a JSON file in the student's folder (timestamped per-session)
267            com.studentgui.apphelpers.dto.AssessmentPayload payload = new com.studentgui.apphelpers.dto.AssessmentPayload(sessionId, codes, scores);
268            java.nio.file.Path jsonOut = com.studentgui.apphelpers.SessionJsonWriter.writeSessionJson(this.studentNameParam, "Abacus", payload, sessionId);
269            if (jsonOut == null) {
270                LOG.warn("Unable to save Abacus session JSON for sessionId={}", sessionId);
271            }
272            // Generate per-phase PNGs (time-series) and a markdown report for this session
273            try {
274                java.nio.file.Path plotsOut = com.studentgui.apphelpers.Helpers.studentPlotsDir(this.studentNameParam);
275                java.nio.file.Path reportsOut = com.studentgui.apphelpers.Helpers.studentReportsDir(this.studentNameParam);
276                java.nio.file.Files.createDirectories(plotsOut);
277                java.nio.file.Files.createDirectories(reportsOut);
278                java.time.format.DateTimeFormatter df = java.time.format.DateTimeFormatter.ISO_DATE;
279                String dateStr = (this.dateParam != null ? this.dateParam.format(df) : java.time.LocalDate.now().toString());
280                String baseName = "Abacus-" + sessionId + "-" + dateStr;
281
282                // Fetch recent dated sessions (oldest first) to build time-series plots.
283                com.studentgui.apphelpers.Database.ResultsWithDates rwd = com.studentgui.apphelpers.Database.fetchLatestAssessmentResultsWithDates(this.studentNameParam, "Abacus", Integer.MAX_VALUE);
284
285                java.util.Map<String, java.nio.file.Path> groups = null;
286                if (rwd != null && rwd.rows != null && !rwd.rows.isEmpty()) {
287                        // Build human-friendly labels from this.parts and render time-series grouped charts
288                        String[] labels = new String[this.parts.length];
289                        for (int i = 0; i < this.parts.length; i++) {
290                            labels[i] = this.parts[i][1];
291                        }
292                        lineGraph.updateWithGroupedDataByDate(rwd.dates, rwd.rows, codes, labels);
293                    // Persist each group as a PNG (time-series image)
294                    groups = lineGraph.saveGroupedCharts(plotsOut, baseName, 1000, 240);
295                    // Use the most-recent session date for the report header if available
296                    java.time.LocalDate headerDate = rwd.dates.get(rwd.dates.size() - 1);
297                    dateStr = headerDate.format(df);
298                } else {
299                    // Fallback: render only the latest session snapshot
300                    java.util.List<java.util.List<Integer>> rows = new java.util.ArrayList<>();
301                    java.util.List<Integer> latest = new java.util.ArrayList<>();
302                    for (int v : scores) {
303                        latest.add(v);
304                    }
305                    rows.add(latest);
306                    lineGraph.updateWithGroupedData(rows, codes);
307                    groups = lineGraph.saveGroupedCharts(plotsOut, baseName, 1000, 240);
308                }
309
310                // Generate markdown report
311                if (groups == null) {
312                    groups = new java.util.LinkedHashMap<>();
313                }
314                StringBuilder md = new StringBuilder();
315                md.append("# ").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("\n\n");
316                for (java.util.Map.Entry<String, java.nio.file.Path> e : groups.entrySet()) {
317                    md.append("## ").append(e.getKey()).append("\n\n");
318                    md.append("![](./").append(e.getValue().getFileName().toString()).append(")\n\n");
319                }
320                java.nio.file.Path mdFile = reportsOut.resolve(baseName + ".md");
321                // images live in ../plots relative to reports
322                String mdText = md.toString().replace("![](./", "![](../plots/");
323                java.nio.file.Files.writeString(mdFile, mdText, java.nio.charset.StandardCharsets.UTF_8);
324                LOG.info("Wrote Abacus session report {} with {} group images", mdFile, groups.size());
325                // Also produce a simple HTML report that embeds the PNGs and
326                // shows a scrollable legend under each plot.
327                try {
328                    String[] palette = new String[] {"#1b9e77","#d95f02","#7570b3","#e7298a","#66a61e","#e6ab02","#a6761d","#666666"};
329
330                    // Build a map of group -> list of part indexes to recreate legend order
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
338                    StringBuilder html = new StringBuilder();
339                    html.append("<!doctype html>\n<html><head><meta charset=\"utf-8\"><title>");
340                    html.append(this.studentNameParam == null ? "Student Report" : this.studentNameParam).append(" - ").append(dateStr);
341                    html.append("</title>");
342                    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>");
343                    html.append("</head><body>");
344                    html.append("<h1>").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("</h1>");
345
346                    for (java.util.Map.Entry<String, java.nio.file.Path> e2 : groups.entrySet()) {
347                        String grp = e2.getKey();
348                        String imgName = e2.getValue().getFileName().toString();
349                        html.append("<h2>").append(grp).append("</h2>");
350                        html.append("<div class=\"plot\"><img src=\"./").append(imgName).append("\" alt=\"").append(grp).append("\"></div>");
351
352                        // legend for this group
353                        java.util.List<Integer> idxs = groupsIdx.getOrDefault(grp, new java.util.ArrayList<>());
354                        html.append("<div class=\"legend\">");
355                        for (int s = 0; s < idxs.size(); s++) {
356                            int idx = idxs.get(s);
357                            String code = codes[idx];
358                            String human = this.parts[idx][1];
359                            String seriesName = code + " - " + human;
360                            String color = palette[s % palette.length];
361                            html.append("<div class=\"legend-item\"><span class=\"swatch\" style=\"background:" + color + ";\"></span>");
362                            html.append("<div>").append(seriesName).append("</div></div>");
363                        }
364                        html.append("</div>");
365                    }
366
367                    html.append("</body></html>");
368                    java.nio.file.Path htmlFile = reportsOut.resolve(baseName + ".html");
369                    // adjust image src to point to ../plots
370                    String htmlStr = html.toString().replace("src=\"./", "src=\"../plots/");
371                    java.nio.file.Files.writeString(htmlFile, htmlStr, java.nio.charset.StandardCharsets.UTF_8);
372                    LOG.info("Wrote Abacus HTML session report {}", htmlFile);
373                } catch (java.io.IOException ioex) {
374                    LOG.warn("Unable to write HTML report: {}", ioex.toString());
375                }
376            } catch (java.io.IOException | SQLException ex) {
377                LOG.warn("Unable to save Abacus per-phase plots or markdown report: {}", ex.toString());
378            }
379        } catch (SQLException e) {
380            LOG.error("SQL error in submitData", e);
381            JOptionPane.showMessageDialog(this, "Database error saving Abacus data: " + e.getMessage(), "Database error", JOptionPane.ERROR_MESSAGE);
382        }
383    }
384
385    /**
386     * Load recent assessment sessions for the selected student and update the
387     * shared {@link JLineGraph} with the returned metric series.
388     */
389    private void refreshGraph() {
390        try {
391            com.studentgui.apphelpers.Database.ResultsWithDates rwd = com.studentgui.apphelpers.Database.fetchLatestAssessmentResultsWithDates(studentNameParam, "Abacus", Integer.MAX_VALUE);
392            String[] codes = new String[this.parts.length];
393            for (int i = 0; i < this.parts.length; i++) {
394                codes[i] = this.parts[i][0];
395            }
396            if (rwd != null && rwd.rows != null && !rwd.rows.isEmpty()) {
397                // Use the date-aware grouped plotter so X axis is dates and each
398                // skill within a phase is a separate line series.
399                String[] labels = new String[this.parts.length];
400                for (int i = 0; i < this.parts.length; i++) {
401                    labels[i] = this.parts[i][1];
402                }
403                lineGraph.updateWithGroupedDataByDate(rwd.dates, rwd.rows, codes, labels);
404                LOG.debug("Graph updated with {} dated sessions", rwd.rows.size());
405            } else {
406                LOG.info("No data to plot; showing grouped placeholders.");
407                lineGraph.showEmptyGrouped(codes);
408            }
409        } catch (SQLException e) {
410            LOG.error("SQL error refreshing graph", e);
411        }
412    }
413    @Override
414    /**
415     * Update the displayed date for the Abacus page and refresh content.
416     *
417     * Records `dateParam` and schedules a UI refresh on the Swing EDT so the
418     * graph and title display the selected date. Note: some pages refresh
419     * recent sessions independent of the selected date; this call keeps the
420     * saved date in sync for subsequent actions.
421     *
422     * @param newDate the date to display (may be null to use current date)
423     */
424
425    public void dateChanged(final LocalDate newDate) {
426        this.dateParam = newDate;
427        // When the global date changes, update the graph to reflect any
428        // date-related logic (most refreshGraph implementations load
429        // recent sessions independent of the selected session date, but
430        // updating here keeps the saved date in sync for future submits).
431        SwingUtilities.invokeLater(this::refreshGraph);
432    }
433
434    @Override
435    /**
436     * Update the selected student for the Abacus page and refresh content.
437     *
438     * Sets `studentNameParam` and posts a UI refresh on the Swing EDT to
439     * reload data and update the page title.
440     *
441     * @param newStudent student identifier (name or id) to display; may be null
442     */
443
444    public void studentChanged(final String newStudent) {
445        this.studentNameParam = newStudent;
446        SwingUtilities.invokeLater(() -> {
447            refreshGraph();
448            updateTitleDate();
449        });
450    }
451
452        private void updateTitleDate() {
453            try {
454                String dateStr = this.dateParam != null ? this.dateParam.toString() : java.time.LocalDate.now().toString();
455                this.titleLabel.setText(baseTitle + " - " + dateStr);
456            } catch (Exception ex) {
457                this.titleLabel.setText(baseTitle);
458            }
459        }
460    
461
462}