WCT Admin Portal

Sign in to access the admin panel

WCT Logo

WCT Worldwidetraining

Attendance Management System

🔧 Setup
⚙️ Admin
📋 Form
📊 Records
🚫 Rejected
● Not Connected
1

Create a Google Sheet

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.

2

Open Apps 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);
  }
}
3

Deploy as Web App

Click Deploy → New deployment
• Type: Web app
• Execute as: Me
• Who has access: Anyone
Click Deploy → copy the URL it gives you.

4

Paste the URL above

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.

Attendance Check-In

Please fill in your details to record your attendance.

Your information is only used for attendance purposes.

0
Total Rejected
0
Today
0
Sessions
🚫
No rejected entries yet. Rejected check-ins will appear here.
Total
Today
Sessions
📊
Click Refresh to load records from Google Sheets.