diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..12b911d --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +*.gs text eol=lf +*.html text eol=lf +*.json text eol=lf +*.bat text eol=crlf \ No newline at end of file diff --git a/Code.gs b/Code.gs index 096de57..ff49d9a 100644 --- a/Code.gs +++ b/Code.gs @@ -1,118 +1,176 @@ function doGet() { - return HtmlService.createTemplateFromFile("Index") - .evaluate() - .setTitle("Robotics Club Ops") - .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL) - .addMetaTag("viewport", "width=device-width, initial-scale=1"); + return HtmlService.createTemplateFromFile("Index") + .evaluate() + .setTitle("Robotics Club Ops") + .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL) + .addMetaTag("viewport", "width=device-width, initial-scale=1"); } -// Standard HTML Service include function -// This allows you to inject content from other files into Index.html +// This allows us to modularize our HTML/CSS/JS function include(filename) { - return HtmlService.createHtmlOutputFromFile(filename).getContent(); + return HtmlService.createHtmlOutputFromFile(filename).getContent(); } -/* 1. GET DATA FOR DROPDOWN */ +// SPREADSHEET COLUMN SETTINGS +// Update these numbers if columns are added or moved in the "Students" sheet (1 = A, 2 = B, ...) +// Note that Java constants are zero-indexed, while Google Sheets columns are 1-indexed. +// These are the 1-indexed column numbers. +const COL_NAME = 1; +const COL_GRADE = 2; +const COL_STATUS = 3; +const COL_LAST_IN = 4; +const COL_LAST_OUT = 5; +const COL_GRADE_CHANGED = 6; +const COL_DATE_ADDED = 7; + +/** Get data for dropdown */ function getStudentList() { - const ss = SpreadsheetApp.getActiveSpreadsheet(); - const sheet = ss.getSheetByName("Students"); + const ss = SpreadsheetApp.getActiveSpreadsheet(); + const sheet = ss.getSheetByName("Students"); + + if (!sheet || sheet.getLastRow() <= 1) return {}; + const data = sheet.getDataRange().getValues(); + + // Remove headers + if (data.length > 0) data.shift(); + let studentMap = {}; + + // Build a map of normalized name -> { displayName, grade } + data.forEach((row) => { + const nameValue = row[COL_NAME - 1]; + const gradeValue = row[COL_GRADE - 1]; + + // Skip empty rows + if (nameValue) { + const displayName = String(nameValue).trim(); + const normName = displayName.toLowerCase(); + + studentMap[normName] = { + displayName: displayName, + grade: gradeValue + }; + } + }); + + return studentMap; +} - // Handle empty sheet case - if (sheet.getLastRow() <= 1) return {}; +/** Handle form submission */ +function processForm(formObject) { + const ss = SpreadsheetApp.getActiveSpreadsheet(); + const studentSheet = ss.getSheetByName("Students"); + const logsSheet = ss.getSheetByName("Logs"); - const data = sheet.getDataRange().getValues(); + // Error handling for missing sheets + if (!studentSheet || !logsSheet) { + return { status: "ERROR", message: "Missing required sheet(s)." }; + } - // Remove headers - if (data.length > 0) data.shift(); + // Sanitize and normalize input + let name = String(formObject.studentName || "").trim(); + let normName = name.toLowerCase(); + let grade = String(formObject.grade || "").trim(); + const type = String(formObject.checkType || "").trim(); // "Check In" or "Check Out" + const notes = String(formObject.notes || "").trim(); + const timeString = String(formObject.manualTime || "").trim(); // HH:mm + + // Validate Grade + if (!["9", "10", "11", "12"].includes(grade)) { + return { status: "ERROR", message: "Invalid grade." }; + } - let studentMap = {}; - data.forEach((row) => { - // Name is Col A (index 0), Grade is Col B (index 1) - if (row[0]) { - studentMap[row[0]] = row[1]; + // Validate Name + if (!name) { + return { status: "ERROR", message: "Name required." }; } - }); - return studentMap; -} + // Validate Time format (HH:mm) + if (!/^\d{2}:\d{2}$/.test(timeString)) { + return { status: "ERROR", message: "Invalid time format." }; + } -/* 2. HANDLE FORM SUBMISSION */ -function processForm(formObject) { - const ss = SpreadsheetApp.getActiveSpreadsheet(); - const studentSheet = ss.getSheetByName("Students"); - const logsSheet = ss.getSheetByName("Logs"); - - const name = formObject.studentName; - const grade = formObject.grade; - const type = formObject.checkType; // "Check In" or "Check Out" - const notes = formObject.notes || ""; - const timeString = formObject.manualTime; // HH:mm - - const now = new Date(); - const dateString = Utilities.formatDate( - now, - Session.getScriptTimeZone(), - "yyyy-MM-dd", - ); - const timestamp = Utilities.formatDate( - now, - Session.getScriptTimeZone(), - "yyyy-MM-dd HH:mm:ss", - ); - - // A. UPDATE STUDENT RECORD (Status, Last Seen, Grade, etc.) - updateStudentRecord(studentSheet, name, grade, type, timestamp); - - // B. LOG DATA TO LOGS TAB - // Logs: Name, Grade, Type, Time, Date, Notes - logsSheet.appendRow([name, grade, type, timeString, dateString, notes]); - - return { status: "SUCCESS", type: type }; + const now = new Date(); + + if (timeString) { + const [hours, minutes] = timeString.split(':'); + now.setHours(parseInt(hours, 10), parseInt(minutes, 10), 0, 0); + } + + // If manual time is provided, override the hours and minutes of 'now' + const dateString = Utilities.formatDate( + now, + Session.getScriptTimeZone(), + "yyyy-MM-dd", + ); + + const timestamp = Utilities.formatDate( + now, + Session.getScriptTimeZone(), + "yyyy-MM-dd HH:mm:ss", + ); + + // Update student record (Status, Last Seen, Grade, etc.) + try { + updateStudentRecord(studentSheet, name, grade, type, timestamp); + // Log data to Logs tab + logsSheet.appendRow([name, grade, type, timeString, dateString, notes]); + return { status: "SUCCESS", type: type }; + } catch (e) { + return { status: "ERROR", message: "Sheet operation failed: " + e.message }; + } } -/* 3. CORE LOGIC FOR UPDATING STUDENTS SHEET */ +/** Core logic for updating Students sheet */ function updateStudentRecord(sheet, name, newGrade, type, timestamp) { - const data = sheet.getDataRange().getValues(); - let rowIndex = -1; - - // 1. FIND ROW - for (let i = 1; i < data.length; i++) { - if (data[i][0] === name) { - rowIndex = i + 1; // Convert 0-index to Sheet Row Number - break; + const data = sheet.getDataRange().getValues(); + let rowIndex = -1; + let normName = String(name).trim().toLowerCase(); + + // Find row (case-insensitive, trim) + for (let i = 1; i < data.length; i++) { + if (String(data[i][COL_NAME - 1]).trim().toLowerCase() === normName) { + rowIndex = i + 1; + break; + } + } + + // If new student, add them + if (rowIndex === -1) { + // Defend against missing columns by ensuring newRow has enough elements + let maxCol = Math.max(COL_DATE_ADDED, sheet.getLastColumn()); + let newRow = new Array(maxCol).fill(""); + + newRow[COL_NAME - 1] = name; + newRow[COL_GRADE - 1] = newGrade; + newRow[COL_DATE_ADDED - 1] = timestamp; + + sheet.appendRow(newRow); + rowIndex = sheet.getLastRow(); // Get the new row number + } + + // Check for grade change + const gradeCell = sheet.getRange(rowIndex, COL_GRADE); + const currentGrade = gradeCell.getValue(); + + // Only update if Grade is different + if (String(currentGrade) !== String(newGrade)) { + gradeCell.setValue(newGrade); + // Update "Grade Last Changed" + sheet.getRange(rowIndex, COL_GRADE_CHANGED).setValue(timestamp); + } + + // Update Status and Last Seen + const statusCell = sheet.getRange(rowIndex, COL_STATUS); + + if (type === "Check In") { + // Set Status text and color + statusCell.setValue("Checked In").setBackground("#d9ead3"); // Light green + // Update Last Check In + sheet.getRange(rowIndex, COL_LAST_IN).setValue(timestamp); + } else { + // Set Status text and color + statusCell.setValue("Checked Out").setBackground("#f4cccc"); // Light red + // Update Last Check Out + sheet.getRange(rowIndex, COL_LAST_OUT).setValue(timestamp); } - } - - // 2. IF NEW STUDENT (Add them) - if (rowIndex === -1) { - // Append: Name, Grade, Status(empty), LastIn(empty), LastOut(empty), GradeChanged(empty), DateAdded - sheet.appendRow([name, newGrade, "", "", "", "", timestamp]); - rowIndex = sheet.getLastRow(); // Get the new row number - } - - // 3. CHECK FOR GRADE CHANGE (Col B -> Column 2) - const gradeCell = sheet.getRange(rowIndex, 2); - const currentGrade = gradeCell.getValue(); - - // Only update if grade is different - if (String(currentGrade) !== String(newGrade)) { - gradeCell.setValue(newGrade); - // Update "Grade Last Changed" (Col F -> Column 6) - sheet.getRange(rowIndex, 6).setValue(timestamp); - } - - // 4. UPDATE STATUS & LAST SEEN - const statusCell = sheet.getRange(rowIndex, 3); // Col C - - if (type === "Check In") { - // Set Status Text & Color - statusCell.setValue("Checked In").setBackground("#d9ead3"); // Light Green - // Update Last Check In (Col D -> Column 4) - sheet.getRange(rowIndex, 4).setValue(timestamp); - } else { - // Set Status Text & Color - statusCell.setValue("Checked Out").setBackground("#f4cccc"); // Light Red - // Update Last Check Out (Col E -> Column 5) - sheet.getRange(rowIndex, 5).setValue(timestamp); - } } diff --git a/Index.html b/Index.html index 75cb6fc..64126e1 100644 --- a/Index.html +++ b/Index.html @@ -1,121 +1,147 @@ -
+ +