Sign in to access the admin panel
Attendance Management System
Go to sheets.google.com → create a new sheet → name it Attendance.
In row 1, add these headers exactly:
Timestamp Name Phone IC First 6 Digit Class Type Session Date Time Trainer Venue
⚠️ Registration Sheet (for verification)
Create a second sheet named Registration with these headers:
Name Phone IC First 6 Digit Paid
Fill this sheet with your participants' registration data. The Paid column must be Y or N. Only participants with Paid = Y can check in.
📋 Rejected Sheet (auto-created)
A third sheet named Rejected will be created automatically the first time someone is rejected. No setup needed — headers and data are written by the script.
In your Google Sheet, click Extensions → Apps Script. Delete any existing code, then paste this:
const SHEET_NAME = "Attendance";
const REG_SHEET_NAME = "Registration"; // sheet with registration data for verification
const REJECTED_SHEET_NAME = "Rejected"; // sheet to log rejected check-ins
function doPost(e) {
try {
const data = JSON.parse(e.postData.contents);
// ── VERIFICATION ──
if (data.action === "verify") {
return verifyParticipant(data);
}
// ── LOG REJECTED ──
if (data.action === "logRejected") {
return logRejectedEntry(data);
}
// ── WRITE ATTENDANCE ──
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME);
sheet.appendRow([
new Date().toLocaleString("en-MY"),
data.name, data.phone, data.ic4, data.classType,
data.session, data.date, data.time, data.trainer, data.venue
]);
return ContentService
.createTextOutput(JSON.stringify({ result: "success" }))
.setMimeType(ContentService.MimeType.JSON);
} catch(err) {
return ContentService
.createTextOutput(JSON.stringify({ result: "error", error: err.toString() }))
.setMimeType(ContentService.MimeType.JSON);
}
}
function logRejectedEntry(data) {
try {
const ss = SpreadsheetApp.getActiveSpreadsheet();
let sheet = ss.getSheetByName(REJECTED_SHEET_NAME);
// Auto-create the sheet + headers if it doesn't exist
if (!sheet) {
sheet = ss.insertSheet(REJECTED_SHEET_NAME);
sheet.appendRow(["Timestamp", "Name", "Phone", "IC First 6 Digit", "Class Type", "Session", "Date", "Time", "Reason"]);
sheet.getRange(1, 1, 1, 9).setFontWeight("bold");
}
sheet.appendRow([
new Date().toLocaleString("en-MY"),
data.name || "", data.phone || "", data.ic4 || "", data.classType || "",
data.session || "", data.date || "", data.time || "", data.reason || ""
]);
return ContentService
.createTextOutput(JSON.stringify({ result: "success" }))
.setMimeType(ContentService.MimeType.JSON);
} catch(err) {
return ContentService
.createTextOutput(JSON.stringify({ result: "error", error: err.toString() }))
.setMimeType(ContentService.MimeType.JSON);
}
}
// ── Helper: get registration sheet rows & headers ──
function getRegData() {
const regSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(REG_SHEET_NAME);
if (!regSheet) return null;
const rows = regSheet.getDataRange().getValues();
const headers = rows[0].map(h => String(h).trim().toLowerCase());
return {
rows,
nameIdx: headers.indexOf("name"),
phoneIdx: headers.indexOf("phone"),
ic4Idx: headers.indexOf("ic first 6 digit"),
paidIdx: headers.indexOf("paid")
};
}
// ── Verify by Phone + IC (Priority 1) ──
function verifyPhoneIc(data) {
try {
const reg = getRegData();
if (!reg) return ContentService.createTextOutput(JSON.stringify({ result: "success", verified: true, mode: "open" })).setMimeType(ContentService.MimeType.JSON);
const allowUnpaid = String(data.allowUnpaid || "").toLowerCase() === "true";
// Normalise IC: uppercase S, strip spaces
const ic4Input = String(data.ic4 || "").trim().toUpperCase();
// Compare full phone number (strip non-digits, last 8 chars for flexibility)
const phoneInput = String(data.phone || "").trim().replace(/\D/g, "").slice(-8);
let matched = false;
for (let i = 1; i < reg.rows.length; i++) {
const row = reg.rows[i];
const regPhone = String(row[reg.phoneIdx] || "").replace(/\D/g, "").slice(-8);
const regIc4 = String(row[reg.ic4Idx] || "").trim().toUpperCase();
const regPaid = reg.paidIdx >= 0 ? String(row[reg.paidIdx] || "").trim().toUpperCase() : "Y";
if (regPaid !== "Y" && !allowUnpaid) continue;
if (ic4Input && regIc4 && ic4Input === regIc4 && phoneInput === regPhone) {
matched = true; break;
}
}
return ContentService.createTextOutput(JSON.stringify({ result: "success", verified: matched })).setMimeType(ContentService.MimeType.JSON);
} catch(err) {
return ContentService.createTextOutput(JSON.stringify({ result: "error", error: err.toString() })).setMimeType(ContentService.MimeType.JSON);
}
}
// ── Verify by Name + Phone (Priority 2 / Fallback) ──
function verifyNamePhone(data) {
try {
const reg = getRegData();
if (!reg) return ContentService.createTextOutput(JSON.stringify({ result: "success", verified: true, mode: "open" })).setMimeType(ContentService.MimeType.JSON);
const allowUnpaid = String(data.allowUnpaid || "").toLowerCase() === "true";
const nameInput = String(data.name || "").trim().toLowerCase();
const phoneInput = String(data.phone || "").trim().replace(/\D/g, "").slice(-8);
let matched = false;
for (let i = 1; i < reg.rows.length; i++) {
const row = reg.rows[i];
const regPhone = String(row[reg.phoneIdx] || "").replace(/\D/g, "").slice(-8);
const regName = String(row[reg.nameIdx] || "").trim().toLowerCase();
const regPaid = reg.paidIdx >= 0 ? String(row[reg.paidIdx] || "").trim().toUpperCase() : "Y";
if (regPaid !== "Y" && !allowUnpaid) continue;
if (nameInput && phoneInput && nameInput === regName && phoneInput === regPhone) {
matched = true; break;
}
}
return ContentService.createTextOutput(JSON.stringify({ result: "success", verified: matched })).setMimeType(ContentService.MimeType.JSON);
} catch(err) {
return ContentService.createTextOutput(JSON.stringify({ result: "error", error: err.toString() })).setMimeType(ContentService.MimeType.JSON);
}
}
// ── Legacy verify (kept for backward compat) ──
function verifyParticipant(data) {
// Try Phone+IC first, then Name+Phone
const r1 = verifyPhoneIc(data);
try {
const j1 = JSON.parse(r1.getContent());
if (j1.verified) return r1;
} catch(e) {}
return verifyNamePhone(data);
}
function doGet(e) {
try {
// ── Verify via GET (called from client for readable response) ──
if (e && e.parameter && (e.parameter.action === "verify" || e.parameter.action === "verifyPhoneIc")) {
return verifyPhoneIc({
phone: e.parameter.phone || "",
ic4: e.parameter.ic4 || "",
allowUnpaid: e.parameter.allowUnpaid || "false"
});
}
if (e && e.parameter && e.parameter.action === "verifyNamePhone") {
return verifyNamePhone({
name: e.parameter.name || "",
phone: e.parameter.phone || "",
allowUnpaid: e.parameter.allowUnpaid || "false"
});
}
// ── Check duplicate check-in ──
if (e && e.parameter && e.parameter.action === "checkDuplicate") {
const nameInput = String(e.parameter.name || "").trim().toLowerCase();
const phoneInput = String(e.parameter.phone || "").trim().replace(/\D/g, "").slice(-8);
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME);
const rows = sheet.getDataRange().getValues();
const headers = rows[0].map(h => String(h).trim());
const nameIdx = headers.indexOf("Name");
const phoneIdx = headers.indexOf("Phone");
let duplicate = false;
for (let i = 1; i < rows.length; i++) {
const rName = String(rows[i][nameIdx] || "").trim().toLowerCase();
const rPhone = String(rows[i][phoneIdx] || "").trim().replace(/\D/g, "").slice(-8);
if (rName === nameInput && rPhone === phoneInput) {
duplicate = true; break;
}
}
return ContentService
.createTextOutput(JSON.stringify({ result: "success", duplicate: duplicate }))
.setMimeType(ContentService.MimeType.JSON);
}
// ── Load attendance records ──
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME);
const rows = sheet.getDataRange().getValues();
const headers = rows[0];
const data = rows.slice(1).map(row => {
const obj = {};
headers.forEach((h, i) => obj[h] = row[i]);
return obj;
});
return ContentService
.createTextOutput(JSON.stringify({ result: "success", data: data }))
.setMimeType(ContentService.MimeType.JSON);
} catch(err) {
return ContentService
.createTextOutput(JSON.stringify({ result: "error", error: err.toString() }))
.setMimeType(ContentService.MimeType.JSON);
}
}
Click Deploy → New deployment
• Type: Web app
• Execute as: Me
• Who has access: Anyone
Click Deploy → copy the URL it gives you.
Paste the deployment URL into the field at the top of this page and click Save. You're done — data will now go directly to your Google Sheet.
Each session can send data to a different Google Sheet. Paste a unique Apps Script URL here, or leave blank to use the default.
Please fill in your details to record your attendance.
Your information is only used for attendance purposes.