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.LinkedHashMap; 013import java.util.Map; 014 015import javax.swing.JButton; 016import javax.swing.JLabel; 017import javax.swing.JPanel; 018import javax.swing.JScrollPane; 019import javax.swing.SwingUtilities; 020 021import org.slf4j.Logger; 022import org.slf4j.LoggerFactory; 023 024import com.studentgui.uicomp.PhaseScoreField; 025 026/** 027 * iOS and iPadOS assistive technology proficiency assessment page. 028 * 029 * <p>Provides structured evaluation of iOS/iPadOS device usage skills across 030 * 41 competencies organized into 6 functional domains:</p> 031 * 032 * <ul> 033 * <li><b>Phase 1 (P1_1–P1_9): Device Basics and VoiceOver Fundamentals</b> 034 * <ul> 035 * <li>Power management, VoiceOver activation/deactivation</li> 036 * <li>Core gestures (tap, swipe, rotor) for icon navigation and interaction</li> 037 * <li>Home screen management, document handling, keyboarding basics</li> 038 * <li>Control Center, App Switcher, and system-level navigation</li> 039 * </ul> 040 * </li> 041 * <li><b>Phase 2 (P2_1–P2_6): Word Processing and Document Creation</b> 042 * <ul> 043 * <li>Creating, editing, and saving text documents</li> 044 * <li>Reading and navigating within documents using VoiceOver</li> 045 * <li>Menu bar interaction, text/image copy-paste workflows</li> 046 * <li>Proofreading and editing strategies with assistive technology</li> 047 * </ul> 048 * </li> 049 * <li><b>Phase 3 (P3_1–P3_5): Spreadsheet and Data Visualization</b> 050 * <ul> 051 * <li>Spreadsheet concepts and terminology (rows, columns, cells, formulas)</li> 052 * <li>Data entry, editing, and spreadsheet navigation with VoiceOver</li> 053 * <li>Creating and interpreting charts/graphs from data</li> 054 * </ul> 055 * </li> 056 * <li><b>Phase 4 (P4_1–P4_5): Presentation Software</b> 057 * <ul> 058 * <li>Creating and structuring presentations with accessible workflows</li> 059 * <li>Editing slides, adding multimedia content (images, audio)</li> 060 * <li>Presenting slides effectively using assistive technology</li> 061 * <li>Sharing and exporting presentations</li> 062 * </ul> 063 * </li> 064 * <li><b>Phase 5 (P5_1–P5_7): Digital Citizenship and Online Safety</b> 065 * <ul> 066 * <li>Acceptable Use Policies, digital citizenship principles</li> 067 * <li>Online safety, privacy awareness, copyright/plagiarism concepts</li> 068 * <li>Recognizing and responding to cyberbullying</li> 069 * </ul> 070 * </li> 071 * <li><b>Phase 6 (P6_1–P6_11): Device Management and Connectivity</b> 072 * <ul> 073 * <li>App installation, updates, deletion, storage management</li> 074 * <li>Accessibility settings configuration and customization</li> 075 * <li>Screen Time controls, Parental Controls</li> 076 * <li>Connectivity features: Bluetooth, Wi-Fi, AirDrop, Personal Hotspot</li> 077 * </ul> 078 * </li> 079 * </ul> 080 * 081 * <p><b>Data Management and Artifacts:</b></p> 082 * <ul> 083 * <li>Scores captured via {@link PhaseScoreField} components (typically 0–4 integer range)</li> 084 * <li>Persisted to normalized schema using {@link com.studentgui.apphelpers.Database#insertAssessmentResults}</li> 085 * <li>JSON session export: {@code StudentDataFiles/<student>/Sessions/iOS/iOS-<sessionId>-<timestamp>.json}</li> 086 * <li>Phase-grouped time-series PNG plots saved to {@code plots/} directory</li> 087 * <li>Markdown and HTML reports generated with embedded plots and color-coded legends</li> 088 * </ul> 089 * 090 * <p>The shared {@link JLineGraph} visualizes recent session trends grouped by phase prefix 091 * to maintain chart readability. This page operates on static student/date parameters and 092 * does not implement listener interfaces for dynamic updates.</p> 093 * 094 * @see com.studentgui.apphelpers.Database 095 * @see JLineGraph 096 * @see PhaseScoreField 097 */ 098public class IOS extends JPanel { 099 private static final Logger LOG = LoggerFactory.getLogger(IOS.class); 100 /** Mapping of iOS assessment part codes to their input components. */ 101 private final Map<String, PhaseScoreField> inputs = new LinkedHashMap<>(); 102 103 /** Selected student display name used for saves and plots (may be null). */ 104 private final String studentNameParam; 105 106 /** Session date to associate with saved iOS progress entries. */ 107 private final LocalDate dateParam; 108 109 /** Shared graph component for plotting recent iOS assessment sessions. */ 110 private final JLineGraph graph; 111 112 /** 113 * Construct the iOS page for the given student and date. 114 * 115 * @param studentName selected student name (may be null) 116 * @param date session date to associate with saved progress 117 * @param graph shared graph used to visualize recent sessions 118 */ 119 public IOS(String studentName, LocalDate date, JLineGraph graph) { 120 this.studentNameParam = (studentName == null || studentName.trim().isEmpty()) ? com.studentgui.apphelpers.Helpers.defaultStudent() : studentName; 121 this.dateParam = date; 122 this.graph = graph; 123 setLayout(new BorderLayout()); 124 125 JPanel p = new JPanel(new GridBagLayout()); 126 JPanel view = new JPanel(new BorderLayout()); 127 view.add(p, BorderLayout.NORTH); 128 view.setBorder(javax.swing.BorderFactory.createEmptyBorder(20,20,20,20)); 129 JScrollPane scroll = new JScrollPane(view); 130 scroll.getAccessibleContext().setAccessibleName("iOS data entry scroll pane"); 131 GridBagConstraints gbc = new GridBagConstraints(); gbc.insets=new Insets(2,2,2,2); gbc.fill = GridBagConstraints.HORIZONTAL; gbc.anchor = GridBagConstraints.NORTHWEST; gbc.weightx = 1.0; 132 133 JLabel title = new JLabel("iOS / iPad OS Skills"); 134 title.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 28)); 135 title.getAccessibleContext().setAccessibleName("iOS Skills Title"); 136 title.setHorizontalAlignment(JLabel.LEFT); 137 gbc.gridx=0; gbc.gridy=0; gbc.gridwidth=2; p.add(title, gbc); 138 139 String[][] parts = new String[][]{ 140 {"P1_1","1.1 Turn Device On/Off"},{"P1_2","1.2 Turn VoiceOver On/Off"},{"P1_3","1.3 Gestures to Click Icons"}, 141 {"P1_4","1.4 Home Screen Icons to Open Documents"},{"P1_5","1.5 Save Documents"},{"P1_6","1.6 Online Tools/Resources"}, 142 {"P1_7","1.7 Keyboarding"},{"P1_8","1.8 Use Different Elements"},{"P1_9","1.9 Control Center, App Switcher..."}, 143 {"P2_1","2.1 Write, edit save"},{"P2_2","2.2 Read, Navigate Document"},{"P2_3","2.3 Use Menubar"}, 144 {"P2_4","2.4 Highlight text, copy and paste text"},{"P2_5","2.5 Copy and paste images"},{"P2_6","2.6 Proofread and edit"}, 145 {"P3_1","3.1 Describe Spreadsheet"},{"P3_2","3.2 Explain terms and concepts"},{"P3_3","3.3 Enter/Edit data"}, 146 {"P3_4","3.4 Navigate Spreadsheet"},{"P3_5","3.5 Create Graphs"},{"P4_1","4.1 Create Presentation"}, 147 {"P4_2","4.2 Edit Slides"},{"P4_3","4.3 Add Images"},{"P4_4","4.4 Present Slides"},{"P4_5","4.5 Share Presentation"}, 148 {"P5_1","5.1 Acceptable Use Policy"},{"P5_2","5.2 Digital Citizenship"},{"P5_3","5.3 Online Safety"}, 149 {"P5_4","5.4 Copyright"},{"P5_5","5.5 Plagiarism"},{"P5_6","5.6 Privacy"},{"P5_7","5.7 Cyberbullying"}, 150 {"P6_1","6.1 Install Apps"},{"P6_2","6.2 Update Apps"},{"P6_3","6.3Delete Apps"},{"P6_4","6.4 Manage Storage"}, 151 {"P6_5","6.5 Accessibility Settings"},{"P6_6","6.6 Screen Time"},{"P6_7","6.7 Parental Controls"},{"P6_8","6.8 Bluetooth"}, 152 {"P6_9","6.9 Wi-Fi"},{"P6_10","6.10 AirDrop"},{"P6_11","6.11 Hotspot"} 153 }; 154 155 java.awt.Font labelFont = com.studentgui.uicomp.PhaseScoreField.getLabelFont(); 156 String[] labels = java.util.Arrays.stream(parts).map(x->x[1]).toArray(String[]::new); 157 int maxPx = com.studentgui.uicomp.PhaseScoreField.computeMaxLabelPixelWidth(labelFont, labels); 158 com.studentgui.uicomp.PhaseScoreField.setGlobalLabelWidth(Math.min(360, Math.max(200, maxPx + 50))); 159 int row = 1; 160 for (String[] part : parts) { 161 gbc.gridx = 0; gbc.gridy = row; gbc.gridwidth = 2; 162 PhaseScoreField tf = new PhaseScoreField(part[1], 0); 163 tf.setToolTipText("Enter whole number score for " + part[1]); 164 tf.getAccessibleContext().setAccessibleName(part[1]); 165 tf.setName("ios_" + part[0]); 166 p.add(tf, gbc); 167 inputs.put(part[0], tf); 168 row++; 169 } 170 // Place Save and Open Latest side-by-side (Braille style) 171 gbc.gridx = 0; gbc.gridy = row; gbc.gridwidth = 1; gbc.anchor = GridBagConstraints.WEST; 172 JButton save = new JButton("Save iOS Data"); 173 save.setPreferredSize(new java.awt.Dimension(0, 32)); 174 save.addActionListener((ActionEvent e) -> { save(); plot(); }); 175 save.setToolTipText("Save iOS assessment for selected student"); 176 save.setMnemonic(KeyEvent.VK_S); 177 save.getAccessibleContext().setAccessibleName("Save iOS Data"); 178 p.add(save, gbc); 179 180 gbc.gridx = 1; 181 JButton openLatest = new JButton("Open Latest Plot"); 182 openLatest.setPreferredSize(new java.awt.Dimension(0, 32)); 183 openLatest.addActionListener((ActionEvent e) -> { 184 java.nio.file.Path pth = com.studentgui.apphelpers.Helpers.latestPlotPath(this.studentNameParam, "iOS"); 185 if (pth == null) { 186 com.studentgui.apphelpers.UiNotifier.show("No iOS plot found for student"); 187 } else { 188 try { 189 java.awt.Desktop.getDesktop().open(pth.toFile()); 190 } catch (java.io.IOException | UnsupportedOperationException | SecurityException ex) { 191 com.studentgui.apphelpers.UiNotifier.show("Unable to open plot: " + pth.getFileName().toString()); 192 } 193 } 194 }); 195 p.add(openLatest, gbc); 196 197 // consume remaining columns (if any) so layout stays compact and buttons are not clipped 198 gbc.gridx = 2; gbc.gridwidth = GridBagConstraints.REMAINDER; gbc.anchor = GridBagConstraints.WEST; 199 p.add(new JPanel(), gbc); 200 row++; 201 202 add(scroll, BorderLayout.CENTER); 203 add(graph, BorderLayout.SOUTH); 204 205 SwingUtilities.invokeLater(()->{ 206 view.setPreferredSize(view.getPreferredSize()); 207 scroll.getViewport().setViewPosition(new java.awt.Point(0,0)); 208 revalidate(); 209 }); 210 211 SwingUtilities.invokeLater(() -> { 212 for (var f: inputs.values()) LOG.debug("IOS field {} labelWidth={} spinnerX={} gap={}", f.getLabel(), f.getLabelWrapWidth(), f.getSpinnerX(), f.getActualGap()); 213 }); 214 215 com.studentgui.apphelpers.Helpers.createFolderHierarchy(); 216 initParts(); 217 } 218 219 /** 220 * Ensure the iOS progress-type and part rows exist in the normalized 221 * database schema. 222 */ 223 private void initParts() { 224 try { 225 int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("iOS"); 226 java.util.Set<String> keys = inputs.keySet(); 227 String[] codes = new String[keys.size()]; 228 int idx = 0; 229 for (String k : keys) { 230 codes[idx++] = k; 231 } 232 com.studentgui.apphelpers.Database.ensureAssessmentParts(ptId, codes); 233 } catch (SQLException ex) { 234 LOG.error("Error ensuring iOS assessment parts", ex); 235 } 236 } 237 238 /** 239 * Validate inputs and persist them as a new progress session for the 240 * selected student. 241 */ 242 private void save() { 243 if (this.studentNameParam == null || this.studentNameParam.trim().isEmpty()) { 244 javax.swing.JOptionPane.showMessageDialog(this, "Please select a student before saving iOS data.", "Missing student", javax.swing.JOptionPane.WARNING_MESSAGE); 245 return; 246 } 247 248 try { 249 int studentId = com.studentgui.apphelpers.Database.getOrCreateStudent(this.studentNameParam); 250 int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("iOS"); 251 int sessionId = com.studentgui.apphelpers.Database.createProgressSession(studentId, ptId, this.dateParam); 252 java.util.Set<String> keys = inputs.keySet(); 253 String[] codes = new String[keys.size()]; int idx = 0; for (String k: keys) codes[idx++] = k; 254 int[] scores = new int[codes.length]; 255 for (int i = 0; i < codes.length; i++) { 256 scores[i] = inputs.get(codes[i]).getValue(); 257 } 258 com.studentgui.apphelpers.Database.insertAssessmentResults(sessionId, ptId, codes, scores); 259 LOG.info("iOS data saved for {}", this.studentNameParam); 260 com.studentgui.apphelpers.UiNotifier.show("iOS data saved."); 261 com.studentgui.apphelpers.dto.AssessmentPayload payload = new com.studentgui.apphelpers.dto.AssessmentPayload(sessionId, codes, scores); 262 java.nio.file.Path jsonOut = com.studentgui.apphelpers.SessionJsonWriter.writeSessionJson(this.studentNameParam, "iOS", payload, sessionId); 263 if (jsonOut == null) LOG.warn("Unable to save iOS session JSON for sessionId={}", sessionId); 264 try { 265 java.nio.file.Path plotsOut = com.studentgui.apphelpers.Helpers.studentPlotsDir(this.studentNameParam); 266 java.nio.file.Path reportsOut = com.studentgui.apphelpers.Helpers.studentReportsDir(this.studentNameParam); 267 java.nio.file.Files.createDirectories(plotsOut); 268 java.nio.file.Files.createDirectories(reportsOut); 269 java.time.format.DateTimeFormatter df = java.time.format.DateTimeFormatter.ISO_DATE; 270 String dateStr = this.dateParam != null ? this.dateParam.format(df) : java.time.LocalDate.now().toString(); 271 String baseName = "iOS-" + sessionId + "-" + dateStr; 272 273 com.studentgui.apphelpers.Database.ResultsWithDates rwd = com.studentgui.apphelpers.Database.fetchLatestAssessmentResultsWithDates(this.studentNameParam, "iOS", Integer.MAX_VALUE); 274 java.util.Map<String, java.nio.file.Path> groups = null; 275 String[] labels = new String[codes.length]; 276 for (int i = 0; i < codes.length; i++) { 277 labels[i] = inputs.get(codes[i]).getLabel(); 278 } 279 // codes already built above as 'codes' 280 if (rwd != null && rwd.rows != null && !rwd.rows.isEmpty()) { 281 graph.updateWithGroupedDataByDate(rwd.dates, rwd.rows, codes, labels); 282 groups = graph.saveGroupedCharts(plotsOut, baseName, 1000, 240); 283 java.time.LocalDate headerDate = rwd.dates.get(rwd.dates.size() - 1); 284 dateStr = headerDate.format(df); 285 } else { 286 java.util.List<java.util.List<Integer>> rowsList = new java.util.ArrayList<>(); 287 java.util.List<Integer> latest = new java.util.ArrayList<>(); 288 for (String c : codes) { 289 latest.add(inputs.get(c).getValue()); 290 } 291 rowsList.add(latest); 292 graph.updateWithGroupedData(rowsList, codes); 293 groups = graph.saveGroupedCharts(plotsOut, baseName, 1000, 240); 294 } 295 296 if (groups == null) { 297 groups = new java.util.LinkedHashMap<>(); 298 } 299 StringBuilder md = new StringBuilder(); 300 md.append("# ").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("\n\n"); 301 for (java.util.Map.Entry<String, java.nio.file.Path> e : groups.entrySet()) { 302 md.append("## ").append(e.getKey()).append("\n\n"); 303 md.append(".append(e.getValue().getFileName().toString()).append(")\n\n"); 304 } 305 java.nio.file.Path mdFile = reportsOut.resolve(baseName + ".md"); 306 java.nio.file.Files.writeString(mdFile, md.toString(), java.nio.charset.StandardCharsets.UTF_8); 307 308 try { 309 String[] palette = JLineGraph.PALETTE_HEX; 310 java.util.LinkedHashMap<String, java.util.List<Integer>> groupsIdx = new java.util.LinkedHashMap<>(); 311 for (int i = 0; i < codes.length; i++) { 312 String code = codes[i]; 313 String grp = code != null && code.contains("_") ? code.split("_")[0] : code; 314 groupsIdx.computeIfAbsent(grp, k -> new java.util.ArrayList<>()).add(i); 315 } 316 StringBuilder html = new StringBuilder(); 317 html.append("<!doctype html><html><head><meta charset=\"utf-8\"><title>"); 318 html.append(this.studentNameParam == null ? "Student Report" : this.studentNameParam).append(" - ").append(dateStr).append("</title>"); 319 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>"); 320 html.append("</head><body>"); 321 html.append("<h1>").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("</h1>"); 322 for (java.util.Map.Entry<String, java.nio.file.Path> e2 : groups.entrySet()) { 323 String grp = e2.getKey(); 324 String imgName = e2.getValue().getFileName().toString(); 325 html.append("<h2>").append(grp).append("</h2>"); 326 html.append("<div class=\"plot\"><img src=\"../plots/").append(imgName).append("\" alt=\"").append(grp).append("\"></div>"); 327 java.util.List<Integer> idxs = groupsIdx.getOrDefault(grp, new java.util.ArrayList<>()); 328 html.append("<div class=\"legend\">"); 329 for (int s = 0; s < idxs.size(); s++) { 330 int itemIdx = idxs.get(s); 331 String code = codes[itemIdx]; 332 String human = labels[itemIdx]; 333 String seriesName = code + " - " + human; 334 String color = palette[s % palette.length]; 335 html.append("<div class=\"legend-item\">"); 336 html.append("<span class=\"swatch\" style=\"background:"); 337 html.append(color); 338 html.append(";\"></span>"); 339 html.append("<div>"); 340 html.append(seriesName); 341 html.append("</div></div>"); 342 } 343 html.append("</div>"); 344 } 345 html.append("</body></html>"); 346 java.nio.file.Path htmlFile = reportsOut.resolve(baseName + ".html"); 347 java.nio.file.Files.writeString(htmlFile, html.toString(), java.nio.charset.StandardCharsets.UTF_8); 348 LOG.info("Wrote iOS HTML session report {}", htmlFile); 349 } catch (java.io.IOException ioex) { 350 LOG.warn("Unable to write iOS HTML report: {}", ioex.toString()); 351 } 352 } catch (java.io.IOException ioe) { 353 LOG.warn("Unable to save iOS per-phase plots or markdown report: {}", ioe.toString()); 354 } 355 } catch (SQLException ex) { 356 LOG.error("Error saving iOS data", ex); 357 javax.swing.JOptionPane.showMessageDialog(this, "Database error saving iOS data: " + ex.getMessage(), "Database error", javax.swing.JOptionPane.ERROR_MESSAGE); 358 } 359 } 360 361 /** 362 * Fetch recent iOS assessment sessions and update the shared graph view. 363 */ 364 private void plot() { 365 LOG.info("Plot requested for {}", studentNameParam); 366 try { 367 java.util.List<java.util.List<Integer>> data = com.studentgui.apphelpers.Database.fetchLatestAssessmentResults(this.studentNameParam, "iOS", 20); 368 if (data != null && !data.isEmpty()) { 369 // Build codes array in the same order as inputs were created 370 String[] codes = new String[inputs.size()]; 371 int idx = 0; for (String k: inputs.keySet()) codes[idx++] = k; 372 graph.updateWithGroupedData(data, codes); 373 // Save static PNG 374 if (this.studentNameParam != null && !this.studentNameParam.trim().isEmpty()) { 375 try { 376 java.nio.file.Path out = com.studentgui.apphelpers.Helpers.APP_HOME.resolve("StudentDataFiles").resolve(com.studentgui.apphelpers.Helpers.safeName(this.studentNameParam)).resolve("plots"); 377 java.nio.file.Files.createDirectories(out); 378 java.time.format.DateTimeFormatter df = java.time.format.DateTimeFormatter.ISO_DATE; 379 String dateStr = (this.dateParam != null ? this.dateParam.format(df) : java.time.LocalDate.now().toString()); 380 java.nio.file.Path file = out.resolve("iOS-" + dateStr + ".png"); 381 graph.saveChart(file, 800, 400); 382 LOG.info("Saved iOS plot to {}", file); 383 // Do not auto-open the plot here; only save it. Opening is handled 384 // by submit/save handlers or the Open Latest button. 385 com.studentgui.apphelpers.UiNotifier.show("iOS plot saved to " + file.toString()); 386 } catch (java.io.IOException ex) { LOG.warn("Unable to save iOS plot image: {}", ex.toString()); } 387 } 388 } else { 389 LOG.info("No iOS data to plot for {}", studentNameParam); 390 } 391 } catch (SQLException ex) { 392 LOG.error("Error fetching iOS data for plot", ex); 393 } 394 } 395}