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.JComboBox; 015import javax.swing.JLabel; 016import javax.swing.JOptionPane; 017import javax.swing.JPanel; 018import javax.swing.JScrollPane; 019import javax.swing.JTextArea; 020import javax.swing.JTextField; 021import javax.swing.SwingUtilities; 022 023import org.slf4j.Logger; 024import org.slf4j.LoggerFactory; 025 026/** 027 * Structured parent/guardian contact log with validation and freeform notes. 028 * 029 * <p>Provides a comprehensive contact tracking form with structured fields for documenting 030 * communications with parents, guardians, and family members. Unlike the freeform notes pages 031 * ({@link SessionNotes}, {@link Observations}), this page captures both structured metadata 032 * and narrative details to support later reporting and documentation requirements.</p> 033 * 034 * <p><b>Structured Fields:</b></p> 035 * <ul> 036 * <li><b>Guardian Name:</b> Full name of the parent/guardian contacted</li> 037 * <li><b>Contact Method:</b> Dropdown selection (Phone, Email, In Person, Other)</li> 038 * <li><b>Phone Number:</b> Contact phone number (validated format: 7-20 chars, digits/+/()-/space)</li> 039 * <li><b>Email Address:</b> Contact email (validated format: basic email regex pattern)</li> 040 * <li><b>Contact Response:</b> Brief summary of the guardian's response or concerns</li> 041 * <li><b>Contact General:</b> High-level topic or category of the contact (e.g., "Progress Update", "IEP Discussion")</li> 042 * <li><b>Contact Specific:</b> Specific items discussed or action points (e.g., "Discussed Braille materials order")</li> 043 * <li><b>Notes:</b> Multi-line freeform notes area for detailed narrative</li> 044 * </ul> 045 * 046 * <p><b>Validation and Error Handling:</b></p> 047 * <ul> 048 * <li>Email validation: Triggers warning if Contact Method is "Email" and email field doesn't match {@code ^[^@\s]+@[^@\s]+\.[^@\s]+$}</li> 049 * <li>Phone validation: Triggers warning if Contact Method is "Phone" and phone doesn't match {@code ^[0-9+()\-\s]{7,20}$}</li> 050 * <li>Validation failures display warning dialogs and do not persist data until corrected</li> 051 * </ul> 052 * 053 * <p><b>Data Persistence:</b></p> 054 * <ul> 055 * <li>Structured fields persisted via {@link com.studentgui.apphelpers.Database#saveContactLog} to {@code ContactLog} table</li> 056 * <li>Notes also saved to {@code ProgressSession.notes} column via {@link com.studentgui.apphelpers.Database#saveSessionNotes}</li> 057 * <li>JSON export: {@code StudentDataFiles/<student>/Sessions/ContactLog/ContactLog-<sessionId>-<timestamp>.json}</li> 058 * <li>Load Last Contact button retrieves most recent contact record via {@link com.studentgui.apphelpers.Database#fetchLatestContactLog}</li> 059 * </ul> 060 * 061 * <p>No plots are generated (contact logs are non-quantitative). The shared {@link JLineGraph} component 062 * is absent from this page's layout. This page does not implement listener interfaces and operates 063 * on static student/date parameters.</p> 064 * 065 * @see com.studentgui.apphelpers.Database#saveContactLog 066 * @see com.studentgui.apphelpers.Database#fetchLatestContactLog 067 * @see com.studentgui.apphelpers.dto.ContactPayload 068 */ 069public class ContactLog extends JPanel { 070 private static final Logger LOG = LoggerFactory.getLogger(ContactLog.class); 071 /** Text area where the user enters contact notes for the selected student. */ 072 private final JTextArea notesArea; 073 // additional contact fields 074 /** Guardian or parent name associated with the student. */ 075 private final JTextField guardianField; 076 /** Phone number used for contact. */ 077 private final JTextField phoneField; 078 /** Email address used for contact. */ 079 private final JTextField emailField; 080 /** Method of contact (Phone/Email/In Person/Other). */ 081 private final JComboBox<String> contactMethodCombo; 082 /** Short description of the response received during contact. */ 083 private final JTextField contactResponseField; 084 /** High-level/general contact notes (summary). */ 085 private final JTextField contactGeneralField; 086 /** Specific items or action points discussed during contact. */ 087 private final JTextField contactSpecificField; 088 089 /** Selected student display name associated with this page instance (may be null). */ 090 private final String studentNameParam; 091 092 /** Session date to associate with saved notes from this page. */ 093 private final LocalDate dateParam; 094 095 /** 096 * Construct a ContactLog page for the provided student and date. 097 * 098 * @param studentName selected student display name (may be null) 099 * @param date session date to associate with saved notes 100 * @param graph shared graph component shown under the editor 101 */ 102 public ContactLog(String studentName, LocalDate date, JLineGraph graph) { 103 this.studentNameParam = (studentName == null || studentName.trim().isEmpty()) ? com.studentgui.apphelpers.Helpers.defaultStudent() : studentName; 104 this.dateParam = date; 105 setLayout(new BorderLayout()); 106 107 JPanel p = new JPanel(new GridBagLayout()); 108 JPanel view = new JPanel(new BorderLayout()); 109 view.add(p, BorderLayout.NORTH); 110 view.setBorder(javax.swing.BorderFactory.createEmptyBorder(20,20,20,20)); 111 JScrollPane scroll = new JScrollPane(view); 112 scroll.getAccessibleContext().setAccessibleName("Contact Log data entry scroll pane"); 113 GridBagConstraints gbc = new GridBagConstraints(); gbc.insets=new Insets(2,2,2,2); gbc.fill = GridBagConstraints.HORIZONTAL; gbc.anchor = GridBagConstraints.NORTHWEST; 114 JLabel title = new JLabel("Contact Log"); title.setFont(title.getFont().deriveFont(Font.BOLD,28f)); title.setHorizontalAlignment(JLabel.LEFT); gbc.gridx=0; gbc.gridy=0; gbc.gridwidth=2; p.add(title, gbc); 115 116 // Structured contact fields (placed above notes) 117 int row = 1; 118 gbc.gridwidth = 1; 119 int globalLabel = com.studentgui.uicomp.PhaseScoreField.getGlobalLabelWidth(); 120 gbc.gridx = 0; gbc.gridy = row; JLabel guardianLabel = new JLabel("Guardian Name:"); guardianLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 24)); guardianLabel.setPreferredSize(new java.awt.Dimension(globalLabel, guardianLabel.getPreferredSize().height)); p.add(guardianLabel, gbc); 121 guardianField = new JTextField(24); guardianField.setName("contactlog_guardian"); gbc.gridx = 1; p.add(guardianField, gbc); 122 row++; 123 gbc.gridx = 0; gbc.gridy = row; JLabel methodLabel = new JLabel("Contact Method:"); methodLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 24)); methodLabel.setPreferredSize(new java.awt.Dimension(globalLabel, methodLabel.getPreferredSize().height)); p.add(methodLabel, gbc); 124 contactMethodCombo = new JComboBox<>(new String[]{"Phone","Email","In Person","Other"}); contactMethodCombo.setName("contactlog_method"); gbc.gridx = 1; p.add(contactMethodCombo, gbc); 125 row++; 126 gbc.gridx = 0; gbc.gridy = row; JLabel phoneLabel = new JLabel("Phone Number:"); phoneLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 24)); phoneLabel.setPreferredSize(new java.awt.Dimension(globalLabel, phoneLabel.getPreferredSize().height)); p.add(phoneLabel, gbc); 127 phoneField = new JTextField(18); phoneField.setName("contactlog_phone"); gbc.gridx = 1; p.add(phoneField, gbc); 128 row++; 129 gbc.gridx = 0; gbc.gridy = row; JLabel emailLabel = new JLabel("Email Address:"); emailLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 24)); emailLabel.setPreferredSize(new java.awt.Dimension(globalLabel, emailLabel.getPreferredSize().height)); p.add(emailLabel, gbc); 130 emailField = new JTextField(24); emailField.setName("contactlog_email"); gbc.gridx = 1; p.add(emailField, gbc); 131 row++; 132 gbc.gridx = 0; gbc.gridy = row; JLabel responseLabel = new JLabel("Contact Response:"); responseLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 24)); responseLabel.setPreferredSize(new java.awt.Dimension(globalLabel, responseLabel.getPreferredSize().height)); p.add(responseLabel, gbc); 133 contactResponseField = new JTextField(24); contactResponseField.setName("contactlog_response"); gbc.gridx = 1; p.add(contactResponseField, gbc); 134 row++; 135 gbc.gridx = 0; gbc.gridy = row; JLabel generalLabel = new JLabel("Contact General:"); generalLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 24)); generalLabel.setPreferredSize(new java.awt.Dimension(globalLabel, generalLabel.getPreferredSize().height)); p.add(generalLabel, gbc); 136 contactGeneralField = new JTextField(24); contactGeneralField.setName("contactlog_general"); gbc.gridx = 1; p.add(contactGeneralField, gbc); 137 row++; 138 gbc.gridx = 0; gbc.gridy = row; JLabel specificLabel = new JLabel("Contact Specific:"); specificLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 24)); specificLabel.setPreferredSize(new java.awt.Dimension(globalLabel, specificLabel.getPreferredSize().height)); p.add(specificLabel, gbc); 139 contactSpecificField = new JTextField(24); contactSpecificField.setName("contactlog_specific"); gbc.gridx = 1; p.add(contactSpecificField, gbc); 140 row++; 141 142 // Notes label + text area with accessibility 143 gbc.gridx = 0; gbc.gridy = row; gbc.gridwidth = 2; JLabel notesLabel = new JLabel("Notes:"); notesLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 24)); notesLabel.setPreferredSize(new java.awt.Dimension(globalLabel, notesLabel.getPreferredSize().height)); p.add(notesLabel, gbc); 144 row++; 145 146 gbc.gridx = 0; gbc.gridy = row; gbc.gridwidth = 2; notesArea = new JTextArea(8,40); notesArea.setLineWrap(true); notesArea.setWrapStyleWord(true); notesArea.setToolTipText("Enter contact notes for the student"); notesArea.getAccessibleContext().setAccessibleName("Contact notes"); JScrollPane notesScroll = new JScrollPane(notesArea); notesScroll.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); p.add(notesScroll, gbc); 147 notesArea.setName("contactlog_notes"); 148 notesLabel.setLabelFor(notesArea); 149 150 row++; 151 gbc.gridx = 0; gbc.gridy = row; gbc.gridwidth = 1; JButton save = new JButton("Save Contact"); 152 save.addActionListener((ActionEvent e)-> saveContact()); 153 save.setToolTipText("Save contact notes to the database"); 154 save.setMnemonic(KeyEvent.VK_S); 155 save.getAccessibleContext().setAccessibleName("Save Contact Notes"); 156 save.setName("contactlog_save"); 157 p.add(save, gbc); 158 159 gbc.gridx = 1; JButton load = new JButton("Load Last Contact"); 160 load.addActionListener((ActionEvent e) -> loadLastContact()); 161 load.setToolTipText("Load the most recent contact for the selected student"); 162 load.setName("contactlog_load"); 163 p.add(load, gbc); 164 165 add(scroll, BorderLayout.CENTER); 166 167 SwingUtilities.invokeLater(()->{ p.setPreferredSize(p.getPreferredSize()); revalidate(); }); 168 169 com.studentgui.apphelpers.Helpers.createFolderHierarchy(); 170 } 171 172 private void loadLastContact() { 173 try { 174 com.studentgui.apphelpers.dto.ContactPayload p = com.studentgui.apphelpers.Database.fetchLatestContactLog(this.studentNameParam); 175 if (p == null) { 176 com.studentgui.apphelpers.UiNotifier.show("No contact found for this student."); 177 return; 178 } 179 guardianField.setText(p.guardian != null ? p.guardian : ""); 180 String method = p.method != null ? p.method : ""; 181 if (method != null) { 182 contactMethodCombo.setSelectedItem(method); 183 } 184 phoneField.setText(p.phone != null ? p.phone : ""); 185 emailField.setText(p.email != null ? p.email : ""); 186 contactResponseField.setText(p.response != null ? p.response : ""); 187 contactGeneralField.setText(p.general != null ? p.general : ""); 188 contactSpecificField.setText(p.specific != null ? p.specific : ""); 189 notesArea.setText(p.notes != null ? p.notes : ""); 190 } catch (SQLException ex) { 191 LOG.error("Error loading last contact", ex); 192 JOptionPane.showMessageDialog(this, "Database error loading contact: " + ex.getMessage(), "Database error", JOptionPane.ERROR_MESSAGE); 193 } 194 } 195 196 /** 197 * Persist the contact notes entered into the notes area as a session note 198 * for the selected student. Shows a confirmation dialog on success and 199 * error dialogs on failure. 200 */ 201 private void saveContact() { 202 try { 203 int studentId = com.studentgui.apphelpers.Database.getOrCreateStudent(this.studentNameParam); 204 int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("ContactLog"); 205 int sessionId = com.studentgui.apphelpers.Database.createProgressSession(studentId, ptId, this.dateParam); 206 207 String notes = notesArea.getText(); 208 String guardian = guardianField.getText(); 209 String method = (String) contactMethodCombo.getSelectedItem(); 210 String phone = phoneField.getText(); 211 String email = emailField.getText(); 212 String response = contactResponseField.getText(); 213 String general = contactGeneralField.getText(); 214 String specific = contactSpecificField.getText(); 215 216 // Basic validation 217 if (method != null && method.equals("Email") && (email == null || !email.matches("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$"))) { 218 JOptionPane.showMessageDialog(this, "Please enter a valid email address.", "Validation", JOptionPane.WARNING_MESSAGE); 219 return; 220 } 221 if (method != null && method.equals("Phone") && (phone == null || !phone.matches("^[0-9+()\\-\s]{7,20}$"))) { 222 JOptionPane.showMessageDialog(this, "Please enter a valid phone number.", "Validation", JOptionPane.WARNING_MESSAGE); 223 return; 224 } 225 226 // Save both the free-form notes field on ProgressSession and structured ContactLog row 227 com.studentgui.apphelpers.Database.saveSessionNotes(sessionId, notes); 228 com.studentgui.apphelpers.Database.saveContactLog(sessionId, this.studentNameParam, this.dateParam.toString(), guardian, method, phone, email, response, general, specific, notes); 229 LOG.info("Saved contact log for {}", studentNameParam); 230 com.studentgui.apphelpers.UiNotifier.show("Contact log saved."); 231 com.studentgui.apphelpers.dto.ContactPayload payload = new com.studentgui.apphelpers.dto.ContactPayload(sessionId, guardian, method, phone, email, response, general, specific, notes); 232 java.nio.file.Path jsonOut = com.studentgui.apphelpers.SessionJsonWriter.writeSessionJson(this.studentNameParam, "ContactLog", payload, sessionId); 233 if (jsonOut == null) { 234 LOG.warn("Unable to save ContactLog session JSON for sessionId={}", sessionId); 235 } 236 } catch (SQLException ex) { 237 LOG.error("Error saving contact log", ex); 238 javax.swing.JOptionPane.showMessageDialog(this, "Database error saving contact log: " + ex.getMessage(), "Database error", javax.swing.JOptionPane.ERROR_MESSAGE); 239 } 240 } 241}