nhemmerson / Zendesk SLA Threshold Override (Pacific-only)

// ==UserScript==
// @name         Zendesk SLA Threshold Override (Pacific-only)
// @namespace    http://tampermonkey.net/
// @version      0.2
// @description  Turn SLA badges yellow at 1 h remaining and red at 15 min, interpreting times in Pacific Time regardless of your local zone.
// @match        https://*.zendesk.com/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // thresholds
    const ONE_HOUR   =  60 * 60 * 1000;
    const FIFTEEN_MIN= 15 * 60 * 1000;

    // returns true if PT was on DST for the given date
    function isPacificDST(year, monthIndex, day) {
        // monthIndex: 0=Jan … 11=Dec
        if (monthIndex < 2 || monthIndex > 10) return false;       // Jan, Feb, Dec → no DST
        if (monthIndex > 2 && monthIndex < 10) return true;        // Apr–Oct → DST
        // Mar or Nov → compute the “nth Sunday”
        if (monthIndex === 2) { // March: starts 2nd Sunday
            const mar1 = new Date(Date.UTC(year,2,1));
            const firstSun = 1 + ((7 - mar1.getUTCDay()) % 7);
            return day >= (firstSun + 7);
        }
        // November: ends on the 1st Sunday
        const nov1 = new Date(Date.UTC(year,10,1));
        const firstSunNov = 1 + ((7 - nov1.getUTCDay()) % 7);
        return day < firstSunNov;
    }

    // parse a datetime like "2025-05-16T17:00:00" **as** Pacific Time
    function parseAsPacific(dtstr) {
        // only match bare datetimes (no offset suffix)
        const m = dtstr.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2}))?$/);
        if (!m) return new Date(dtstr); // fallback to built-in
        const [, ys, ms, ds, hs, mins, ss] = m;
        const y  = +ys,
              mi = +ms - 1,
              d  = +ds,
              h  = +hs,
              n  = +mins,
              s  = ss ? +ss : 0;

        const dst = isPacificDST(y, mi, d);
        const offsetMin = dst ? 420 : 480;   // getTimezoneOffset minutes for PT
        // Date.UTC() treats the components as UTC; adding offsetMin minutes shifts it into PT’s actual wall-clock
        const utcMs = Date.UTC(y, mi, d, h, n, s) + offsetMin * 60_000;
        return new Date(utcMs);
    }

    function updateBadges() {
        const now = Date.now();

        document
          .querySelectorAll('span[data-garden-id="tags.tag_view"] time[datetime]')
          .forEach(timeEl => {
            const raw = timeEl.getAttribute('datetime');
            // if it has a “Z” or explicit ±HH:MM, native parsing is already correct
            const expiry = /Z$|[+\-]\d{2}:\d{2}$/.test(raw)
                         ? new Date(raw)
                         : parseAsPacific(raw);

            const remaining = expiry.getTime() - now;
            const badge = timeEl.closest('span[data-garden-id="tags.tag_view"]');
            if (!badge) return;

            // reset
            badge.style.backgroundColor = '';
            badge.style.color           = '';

            if (remaining <= 0 || remaining <= FIFTEEN_MIN) {
                // ≤ 15 min or overdue → RED
                badge.style.backgroundColor = '#d9534f';
                badge.style.color           = 'white';
            }
            else if (remaining <= ONE_HOUR) {
                // ≤ 1 h → YELLOW
                badge.style.backgroundColor = '#f0ad4e';
                badge.style.color           = 'black';
            }
            else {
                // > 1 h → GREEN
                badge.style.backgroundColor = '#5cb85c';
                badge.style.color           = 'white';
            }
        });
    }

    // run once shortly after page load…
    setTimeout(updateBadges, 2000);
    // …and every 30 s thereafter
    setInterval(updateBadges, 30000);

})();