The Problem
In a global or remote work environment, employees often get confused between the Web Server time and their Local System time. Standard attendance forms can feel static, leading to the common question: “Is the system recording my check-in time accurately based on my current location?”
The Solution
I created a real-time System Observer using a Custom HTML Block and a Client Script. This provides immediate visual confirmation by showing live world clocks for our main hubs alongside a “Smart Clock” that detects the user’s specific system environment.
Key Technical Features:
-
Observer Logic: Uses a
setIntervalloop on the client side to keep the UI in sync without redundant REST API calls to the server. -
Environmental Handshake: Leverages the
Intlbrowser API to resolve the user’s Timezone ID (e.g.,Asia/Qatar) and automatically fetch the corresponding country flag from FlagCDN. -
Vector-based UI: The analog clock is rendered using pure HTML/CSS. The hands move via direct CSSOM (CSS Object Model) transformations (
rotate) calculated from the doctype’s time field. -
HR Friendly: Reduces internal support tickets by giving employees full transparency over the time-tracking logic before they hit “Save.”
Implementation Details
1. The HTML Block
Create a HTML field in your Employee Checkin doctype and paste the following. It uses a clean table layout and includes lightweight iframes for global hub tracking.
HTML
<div class="footer-clocks-container" style="padding: 15px 0; background-color: #f9f9f9; border: 1px solid #d1d8dd; border-radius: 8px;">
<table style="width: 100%; margin: 0 auto; border-collapse: collapse;">
<tr>
<td style="width: 25%; text-align: center; vertical-align: middle;">
<iframe src="https://free.timeanddate.com/clock/iaa9j5le/n53/szw80/szh80/hbw0/hfc000/cf100/hgr0/fav0/fiv0/mqcfff/mql15/mqw4/mqd94/mhcfff/mhl15/mhw4/mhd94/mmv0/hhcbbb/hmcddd/hsceee" frameborder="0" width="80" height="80"></iframe>
<div class="digital-time" id="cairo-digital" style="font-size: 11px; font-weight: bold; color: #555; margin-top: 2px;">--:-- --</div>
<div style="font-size: 10px; font-weight: bold; color: #999; display: flex; align-items: center; justify-content: center; gap: 4px;">
<img src="https://flagcdn.com/w20/eg.png" width="16" height="12"> Cairo
</div>
</td>
<td style="width: 25%; text-align: center; vertical-align: middle;">
<iframe src="https://free.timeanddate.com/clock/iaa9j5le/n107/szw80/szh80/hbw0/hfc000/cf100/hgr0/fav0/fiv0/mqcfff/mql15/mqw4/mqd94/mhcfff/mhl15/mhw4/mhd94/mmv0/hhcbbb/hmcddd/hsceee" frameborder="0" width="80" height="80"></iframe>
<div class="digital-time" id="istanbul-digital" style="font-size: 11px; font-weight: bold; color: #555; margin-top: 2px;">--:-- --</div>
<div style="font-size: 10px; font-weight: bold; color: #999; display: flex; align-items: center; justify-content: center; gap: 4px;">
<img src="https://flagcdn.com/w20/tr.png" width="16" height="12"> Istanbul
</div>
</td>
<td style="width: 25%; text-align: center; vertical-align: middle;">
<iframe src="https://free.timeanddate.com/clock/iaa9j5le/n60/szw80/szh80/hbw0/hfc000/cf100/hgr0/fav0/fiv0/mqcfff/mql15/mqw4/mqd94/mhcfff/mhl15/mhw4/mhd94/mmv0/hhcbbb/hmcddd/hsceee" frameborder="0" width="80" height="80"></iframe>
<div class="digital-time" id="casablanca-digital" style="font-size: 11px; font-weight: bold; color: #555; margin-top: 2px;">--:-- --</div>
<div style="font-size: 10px; font-weight: bold; color: #999; display: flex; align-items: center; justify-content: center; gap: 4px;">
<img src="https://flagcdn.com/w20/ma.png" width="16" height="12"> Casablanca
</div>
</td>
<td style="width: 25%; text-align: center; vertical-align: middle;">
<div id="checkin-analog-face" style="--_w: 80px; --_sz: 10px; --_r: 32px; width: var(--_w); height: var(--_w); border-radius: 50%; margin: 0 auto; position: relative; background: #000; color: #fff;">
<div style="position: absolute; width: 100%; height: 100%; font-family: sans-serif;">
<time style="position: absolute; width: var(--_sz); height: var(--_sz); font-size: 8px; display: grid; place-content: center; left: 50%; top: 50%; transform: translate(-50%, -50%) rotate(270deg) translate(var(--_r)) rotate(-270deg);">12</time>
<time style="position: absolute; width: var(--_sz); height: var(--_sz); font-size: 8px; display: grid; place-content: center; left: 50%; top: 50%; transform: translate(-50%, -50%) rotate(0deg) translate(var(--_r)) rotate(0deg);">3</time>
<time style="position: absolute; width: var(--_sz); height: var(--_sz); font-size: 8px; display: grid; place-content: center; left: 50%; top: 50%; transform: translate(-50%, -50%) rotate(90deg) translate(var(--_r)) rotate(-90deg);">6</time>
<time style="position: absolute; width: var(--_sz); height: var(--_sz); font-size: 8px; display: grid; place-content: center; left: 50%; top: 50%; transform: translate(-50%, -50%) rotate(180deg) translate(var(--_r)) rotate(-180deg);">9</time>
</div>
<div class="hand-hour" style="position: absolute; bottom: 50%; left: 50%; width: 3px; height: 18px; background: #fff; transform-origin: bottom; border-radius: 4px;"></div>
<div class="hand-min" style="position: absolute; bottom: 50%; left: 50%; width: 2px; height: 26px; background: #fff; transform-origin: bottom; border-radius: 4px;"></div>
<div style="position: absolute; top: 50%; left: 50%; width: 4px; height: 4px; background: #fff; border-radius: 50%; transform: translate(-50%, -50%); z-index: 10;"></div>
</div>
<div id="checkin-digital-val" style="font-size: 11px; font-weight: bold; color: #1a73e8; margin-top: 5px;">--:-- --</div>
<div id="checkin-tz-display" style="font-size: 10px; font-weight: bold; color: #555; display: flex; align-items: center; justify-content: center; gap: 4px;">
Loading...
</div>
</td>
</tr>
</table>
</div>
2. The Client Script
Add this to a Client Script for the Employee Checkin doctype.
JavaScript
frappe.ui.form.on('Employee Checkin', {
refresh: function(frm) {
start_world_clocks_timer(frm);
render_fixed_checkin_clock(frm);
},
time: function(frm) {
render_fixed_checkin_clock(frm);
}
});
function start_world_clocks_timer(frm) {
if (window.worldClockInterval) clearInterval(window.worldClockInterval);
window.worldClockInterval = setInterval(() => {
const zones = {
'cairo-digital': 'Africa/Cairo',
'istanbul-digital': 'Europe/Istanbul',
'casablanca-digital': 'Africa/Casablanca'
};
Object.keys(zones).forEach(id => {
try {
let time_str = new Date().toLocaleTimeString('en-US', {
timeZone: zones[id], hour: 'numeric', minute: '2-digit', hour12: true
});
const el = document.getElementById(id);
if (el) el.innerText = time_str;
} catch (e) {}
});
}, 1000);
}
function render_fixed_checkin_clock(frm) {
let display_text = frm.get_field('time').$input.val() || "";
let tz_info = frm.get_field('time').$wrapper.find('.help-box').text() || "Local Time";
if (display_text) {
let match = display_text.match(/(\d{2}):(\d{2}):(\d{2})/);
if (match) {
let h = parseInt(match[1]);
let m = parseInt(match[2]);
let hDeg = (h % 12) * 30 + (m * 0.5);
let mDeg = m * 6;
const $wrapper = $('.footer-clocks-container');
if ($wrapper.length) {
$wrapper.find('.hand-hour').css('transform', `translateX(-50%) rotate(${hDeg}deg)`);
$wrapper.find('.hand-min').css('transform', `translateX(-50%) rotate(${mDeg}deg)`);
let ampm = h >= 12 ? 'PM' : 'AM';
let h12 = h % 12 || 12;
let mStr = m < 10 ? '0' + m : m;
document.getElementById('checkin-digital-val').innerText = `${h12}:${mStr} ${ampm}`;
// --- Intelligent Flag Logic ---
let flag_code = "un";
let tz_lower = tz_info.toLowerCase();
if (tz_lower.includes("cairo")) flag_code = "eg";
else if (tz_lower.includes("istanbul")) flag_code = "tr";
else if (tz_lower.includes("casablanca")) flag_code = "ma";
else if (tz_lower.includes("qatar")) flag_code = "qa";
else if (tz_lower.includes("dubai") || tz_lower.includes("uae")) flag_code = "ae";
else if (tz_lower.includes("saudi") || tz_lower.includes("riyadh")) flag_code = "sa";
const tzLabel = document.getElementById('checkin-tz-display');
if (tzLabel) {
tzLabel.innerHTML = `<img src="https://flagcdn.com/w20/${flag_code}.png" width="16" height="12" onerror="this.style.display='none'"> ${tz_info}`;
}
}
}
}
}
I hope this helps the community build more localized and user-friendly ERPNext interfaces! Feel free to ask any questions about the logic below.
