How I Built an Interactive Employee Attendance Tracker in Zoho Creator
Attendance tracking is a core part of running any business — but let’s be honest — most attendance systems are either too rigid or too plain.
I wanted something different:
✅ A modern-looking interface
✅ Real-time updates to Zoho Creator
✅ Visual feedback with charts
✅ Simple controls to mark employees Present or Absent
✅ Smart logic to prevent repeated updates within 9 hours
So, I built a custom Zoho Creator Widget that does exactly that.
In this post, I’ll take you through the entire step-by-step process — from fetching data to updating records — so you can implement it in your own Creator apps.
1. Tools & Libraries Used
Before we dive in, here’s what powers the widget:
-
Zoho Creator Widget SDK v1 – to interact with Creator data (
getAllRecords,updateRecord) -
Chart.js – to visualize attendance with a pie chart
-
HTML, CSS & JavaScript – for structure, styling, and logic
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://js.zohostatic.com/creator/widgets/version/1.0/widgetsdk-min.js"></script>
2. Configuring the Widget
We start by setting the application name and report link name.
Make sure reportName matches the link name of your Zoho Creator report, not just the display title.
const CONFIG = {
appName: "cresher-operation-management",
reportName: "All_Employees"
};
3. Initializing Zoho Creator SDK
The widget won’t work until the SDK is initialized.
ZOHO.CREATOR.init()
.then(loadEmployees)
.catch(err => console.error("ZOHO init err:", err));
Once initialized, we call loadEmployees() to fetch our data.
4. Fetching Data from Zoho Creator
We use getAllRecords() to pull up to 200 employee records at once.
function loadEmployees() {
ZOHO.CREATOR.API.getAllRecords({
appName: CONFIG.appName,
reportName: CONFIG.reportName,
page: 1,
pageSize: 200
}).then(res => {
if (res && res.code === 3000) {
employees = res.data;
renderTable();
}
}).catch(err => console.error("getAllRecords error:", err));
}
Now we have the data in an array called employees.
5. Rendering the Employee Table
The renderTable() function dynamically creates HTML rows for each employee:
-
Name, designation, and phone number
-
Attendance status
-
Two buttons — Present and Absent
Clicking a button updates the record. Clicking the row opens the popup with details.
6. Preventing Multiple Updates (9-Hour Lock)
One challenge: We don’t want users marking attendance multiple times a day.
Solution → Use localStorage to store a timestamp after an update.
function applyButtonState(recordId, button) {
const key = `attendance_lock_${recordId}`;
const lockTime = localStorage.getItem(key);
if (lockTime) {
const diff = Date.now() - parseInt(lockTime);
if (diff < 9 * 60 * 60 * 1000) {
button.disabled = true;
button.classList.add("disabled-btn");
return;
}
}
}
This logic disables the button for 9 hours after marking attendance.
7. Updating Records in Zoho Creator
When a user clicks Present or Absent, we send updated values to Zoho Creator using updateRecord().
ZOHO.CREATOR.API.updateRecord({
appName: CONFIG.appName,
reportName: CONFIG.reportName,
id: emp.ID,
data: {
data: {
No_of_Days_Present: newPresent,
No_of_Days_Absent: newAbsent,
Attendance: status,
Attendance_Marked: true
}
}
})
After a successful update:
-
The table cell updates instantly
-
A success alert is shown
-
The 9-hour lock is applied
8. The Employee Details Popup
Clicking an employee row opens a modal with:
-
Employee name & ID
-
Salary details
-
Designation
-
A pie chart showing Present vs Absent days
-
Present/Absent buttons (also with 9-hour lock)
9. Visualizing Data with Chart.js
The popup uses Chart.js for a clean attendance summary.
chartInstance = new Chart(ctx, {
type: 'pie',
data: {
labels: ['Present', 'Absent'],
datasets: [{
data: [p, a],
backgroundColor: ['#4caf50', '#f44336']
}]
}
});
If no data exists, it shows a grey "No Data" chart.
10. Styling the Interface
CSS is what makes the widget look modern:
-
Orange header row for the table
-
Hover effects on rows
-
Smooth button animations
-
Responsive popup design
11. Final Output
Here’s what the widget delivers:
✅ Real-time employee list
✅ One-click attendance marking
✅ Auto-lock to prevent multiple updates
✅ Detailed employee profile popup
✅ Attendance pie chart
12. Possible Enhancements
If you want to extend this widget:
-
Add search & filter functionality
-
Export attendance as Excel/CSV
-
Bulk mark attendance for all employees
-
Integrate with Zoho CRM to sync records
Conclusion
This project shows just how powerful Zoho Creator widgets can be when you combine them with APIs and modern UI techniques.
The same approach works for:
-
Project tracking
-
Inventory updates
-
Customer feedback management
With this structure, you can pull and update any Zoho Creator form data in real time.
Code:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Employee Attendance Tracker</title>
<!-- Chart.js -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<!-- Zoho Creator Widget SDK (v1) -->
<script src="https://js.zohostatic.com/creator/widgets/version/1.0/widgetsdk-min.js"></script>
<style>
body { font-family: Arial, sans-serif; background: #f5f7fa; padding: 18px; }
h2 { color: #ff9800; margin-bottom: 12px; }
table { width: 100%; border-collapse: collapse; background: white; box-shadow: 0 1px 4px rgba(0,0,0,0.06); }
th, td { padding: 10px 8px; border-bottom: 1px solid #eee; text-align: left; vertical-align: middle; }
th { background: #ff9800; color: white; text-align: left; }
tr:hover { background: #fafafa; }
.name-cell { display:flex; gap:10px; align-items:center; }
.present-btn, .absent-btn {
border:none; padding:6px 10px; border-radius:4px; cursor:pointer; transition: background 0.3s, transform 0.2s;
}
.present-btn { background:#4caf50; color:white; }
.absent-btn { background:#f44336; color:white; }
.present-btn:hover:not(:disabled) { background:#45a049; transform: scale(1.05); }
.absent-btn:hover:not(:disabled) { background:#e53935; transform: scale(1.05); }
.disabled-btn {
opacity: 0.6;
cursor: not-allowed;
background: #b0b0b0 !important;
color: #fff !important;
}
.modal { display:none; position:fixed; inset:0; background:rgba(0,0,0,0.45); justify-content:center; align-items:center; }
.modal-content { width:400px; max-width:92%; background:white; padding:18px; border-radius:8px; box-shadow:0 6px 24px rgba(0,0,0,0.15); position:relative; }
.close-btn { position:absolute; right:12px; top:8px; cursor:pointer; font-size:18px; color:#666; }
.meta-grid { display:grid; grid-template-columns: 1fr 1fr; gap:8px; margin-top:8px; font-size:13px; }
.chart-container { max-width:260px; margin: 10px auto; }
</style>
</head>
<body>
<h2>Employee Attendance Tracker</h2>
<table id="employeeTable" aria-live="polite">
<thead>
<tr>
<th>Employee</th>
<th>Designation</th>
<th>Phone</th>
<th>Attendance</th> <!-- New column -->
<th style="text-align:center">Actions</th>
</tr>
</thead>
<tbody></tbody>
</table>
<!-- Modal / Popup -->
<div id="employeeModal" class="modal" role="dialog" aria-modal="true">
<div class="modal-content">
<span class="close-btn" onclick="closeModal()">×</span>
<div id="employeeDetailsBlock">
<div>
<div id="popupName" style="font-weight:700; font-size:16px;"></div>
<div id="popupEmpId" style="color:#555; font-size:13px;"></div>
</div>
<div class="chart-container">
<canvas id="attendanceChart" width="260" height="260"></canvas>
</div>
<div class="meta-grid">
<div><strong>Total Salary</strong><div id="popupTotalSalary"></div></div>
<div><strong>Take Home</strong><div id="popupTakeHome"></div></div>
<div><strong>Advance Paid</strong><div id="popupAdvancePaid"></div></div>
<div><strong>Salary Type</strong><div id="popupSalaryType"></div></div>
<div style="grid-column:1/3"><strong>Designation</strong><div id="popupDesignation"></div></div>
</div>
<div style="margin-top:12px; text-align:center;">
<button id="popupPresentBtn" class="present-btn">Present</button>
<button id="popupAbsentBtn" class="absent-btn">Absent</button>
</div>
</div>
</div>
</div>
<script>
const CONFIG = {
appName: "cresher-operation-management",
reportName: "All_Employees"
};
let employees = [];
let selectedEmployee = null;
let chartInstance = null;
function getVal(field) {
if (field === null || field === undefined) return "";
if (typeof field === "string" || typeof field === "number") return field;
if (Array.isArray(field)) return field[0]?.download_url || field[0]?.url || field[0]?.name || "";
if (field.display_value !== undefined) return field.display_value;
return String(field);
}
ZOHO.CREATOR.init().then(loadEmployees).catch(err => console.error("ZOHO init err:", err));
function loadEmployees() {
ZOHO.CREATOR.API.getAllRecords({
appName: CONFIG.appName,
reportName: CONFIG.reportName,
page: 1,
pageSize: 200
}).then(res => {
if (res && res.code === 3000) {
employees = res.data;
renderTable();
}
}).catch(err => console.error("getAllRecords error:", err));
}
function renderTable() {
const tbody = document.querySelector("#employeeTable tbody");
tbody.innerHTML = "";
employees.forEach(emp => {
const name = getVal(emp.Name) || getVal(emp.Full_Name) || "—";
const empId = getVal(emp.Add_Designation) || "—";
const phone = getVal(emp.Phone_Number) || "";
const attendance = getVal(emp.Attendance) || "—"; // NEW field
const tr = document.createElement("tr");
tr.innerHTML = `
<td><div class="name-cell">${escapeHtml(name)}</div></td>
<td>${escapeHtml(empId)}</td>
<td>${escapeHtml(phone)}</td>
<td class="attendance-cell">${escapeHtml(attendance)}</td> <!-- NEW column -->
<td style="text-align:center">
<button class="present-btn">Present</button>
<button class="absent-btn">Absent</button>
</td>
`;
const presentBtn = tr.querySelector(".present-btn");
const absentBtn = tr.querySelector(".absent-btn");
applyButtonState(emp.ID, presentBtn);
applyButtonState(emp.ID, absentBtn);
presentBtn.addEventListener("click", e => { e.stopPropagation(); markAttendance(emp, "Present", tr); });
absentBtn.addEventListener("click", e => { e.stopPropagation(); markAttendance(emp, "Absent", tr); });
tr.addEventListener("click", () => openModal(emp));
tbody.appendChild(tr);
});
}
function applyButtonState(recordId, button) {
const key = `attendance_lock_${recordId}`;
const lockTime = localStorage.getItem(key);
if (lockTime) {
const diff = Date.now() - parseInt(lockTime);
if (diff < 9 * 60 * 60 * 1000) {
button.disabled = true;
button.classList.add("disabled-btn");
button.title = "Attendance already marked. Will be available after 9 hours.";
const timeRemaining = (9 * 60 * 60 * 1000) - diff;
setTimeout(() => {
button.disabled = false;
button.classList.remove("disabled-btn");
button.title = "";
}, timeRemaining);
return;
}
}
button.disabled = false;
button.classList.remove("disabled-btn");
button.title = "";
}
function markAttendance(emp, status, rowElement) {
const key = `attendance_lock_${emp.ID}`;
const lockTime = localStorage.getItem(key);
if (lockTime && Date.now() - parseInt(lockTime) < 9 * 60 * 60 * 1000) return;
const currPresent = parseInt(getVal(emp.No_of_Days_Present) || 0, 10) || 0;
const currAbsent = parseInt(getVal(emp.No_of_Days_Absent) || 0, 10) || 0;
const newPresent = status === "Present" ? currPresent + 1 : currPresent;
const newAbsent = status === "Absent" ? currAbsent + 1 : currAbsent;
const payload = { data: { No_of_Days_Present: newPresent, No_of_Days_Absent: newAbsent, Attendance: status, Attendance_Marked: true } };
ZOHO.CREATOR.API.updateRecord({
appName: CONFIG.appName,
reportName: CONFIG.reportName,
id: emp.ID,
data: payload
}).then(res => {
if (res && res.code === 3000) {
emp.No_of_Days_Present = newPresent;
emp.No_of_Days_Absent = newAbsent;
emp.Attendance = status; // update local data
localStorage.setItem(key, Date.now().toString());
// Update table cell instantly
if (rowElement) {
rowElement.querySelector(".attendance-cell").innerText = status;
}
alert(`${getVal(emp.Name)} marked as ${status}`);
if (selectedEmployee && selectedEmployee.ID === emp.ID) {
selectedEmployee = emp;
renderPopupFor(emp);
}
}
});
}
function openModal(emp) {
selectedEmployee = emp;
renderPopupFor(emp);
document.getElementById("employeeModal").style.display = "flex";
}
function renderPopupFor(emp) {
document.getElementById("popupName").innerText = getVal(emp.Name) || getVal(emp.Full_Name) || "—";
document.getElementById("popupEmpId").innerText = "ID: " + (getVal(emp.Employee_Id) || emp.ID);
document.getElementById("popupTotalSalary").innerText = getVal(emp.Total_Salary) || "";
document.getElementById("popupTakeHome").innerText = getVal(emp.Take_Home) || "";
document.getElementById("popupAdvancePaid").innerText = getVal(emp.Advance_Paid) || "";
document.getElementById("popupSalaryType").innerText = getVal(emp.Salary_Type) || "";
document.getElementById("popupDesignation").innerText = getVal(emp.Add_Designation) || "";
const p = parseInt(getVal(emp.No_of_Days_Present) || 0, 10) || 0;
const a = parseInt(getVal(emp.No_of_Days_Absent) || 0, 10) || 0;
renderChart(p, a);
const presentBtn = document.getElementById("popupPresentBtn");
const absentBtn = document.getElementById("popupAbsentBtn");
applyButtonState(emp.ID, presentBtn);
applyButtonState(emp.ID, absentBtn);
presentBtn.onclick = e => { e.stopPropagation(); markAttendance(emp, "Present"); };
absentBtn.onclick = e => { e.stopPropagation(); markAttendance(emp, "Absent"); };
}
function renderChart(p, a) {
const ctx = document.getElementById('attendanceChart').getContext('2d');
if (chartInstance) chartInstance.destroy();
const data = (p === 0 && a === 0) ? [1] : [p, a];
const colors = (p === 0 && a === 0) ? ['#ccc'] : ['#4caf50', '#f44336'];
chartInstance = new Chart(ctx, {
type: 'pie',
data: { labels: (p === 0 && a === 0) ? ['No Data'] : ['Present', 'Absent'], datasets: [{ data, backgroundColor: colors }] },
options: { responsive: true, maintainAspectRatio: false }
});
}
function closeModal() { document.getElementById("employeeModal").style.display = "none"; }
function escapeHtml(s) {
return String(s || "").replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":"'"}[m]));
}
</script>
</body>
</html>

Useful
ReplyDeleteThank You π
DeleteHelpful resource
ReplyDeleteThank you mam
Delete