Skip to content
Draft
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
*.gs text eol=lf
*.html text eol=lf
*.json text eol=lf
*.bat text eol=crlf
251 changes: 152 additions & 99 deletions Code.gs
Original file line number Diff line number Diff line change
@@ -1,118 +1,171 @@
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();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider creating a function safeTrim(..) to avoid repetition

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." };
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Improve message to indicate expected format

"Invalid time format, expected HH:MM"

}

/* 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 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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment is good, a constant is better -- LIGHT_GREEN.

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);
}
}
Loading