[Show & Tell] Dynamic Multi-Timezone & Client-Side Clock Observer for Employee Check-ins

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 setInterval loop on the client side to keep the UI in sync without redundant REST API calls to the server.

  • Environmental Handshake: Leverages the Intl browser 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.

Adobe Express - clockfinal

2 Likes