// punch-fab.jsx — globally-floating quick check-in / check-out widget
//
// Lives at bottom-right of the viewport, just above the chat bubble.
// One-click access to:
//   * current state (in / on break / out) with a coloured pill
//   * live counter of hours worked today (ticks every second)
//   * action buttons: Punch in / Start break / End break / Punch out
//
// Self-mounts on DOMContentLoaded so it's available everywhere —
// inside the People module, on the project board, in CRM, etc.
// Pulls state from /api/people/me on mount + polls every 30s, plus
// re-fetches after every successful punch. Hides itself entirely
// when:
//   * not logged in (no /api/people/me response)
//   * migration 044 hasn't applied (the endpoint returns null today)
//   * the user explicitly minimises it via the × on the popover
//     (preference persists in localStorage)

(function () {
  if (typeof window === "undefined") return;

  // ── helpers ────────────────────────────────────────────────
  function pad(n) { return String(n).padStart(2, "0"); }
  function fmtDuration(mins) {
    if (!isFinite(mins) || mins < 0) return "0m";
    const h = Math.floor(mins / 60);
    const m = Math.round(mins % 60);
    if (h && m) return `${h}h ${m}m`;
    if (h)      return `${h}h`;
    return `${m}m`;
  }
  function parseHHMM(s) {
    if (!s) return null;
    const m = /^(\d{1,2}):(\d{2})$/.exec(String(s));
    return m ? Number(m[1]) * 60 + Number(m[2]) : null;
  }

  // ── React component ────────────────────────────────────────
  function PunchFab() {
    const [today, setToday]   = React.useState(null);
    const [open,  setOpen]    = React.useState(false);
    const [now,   setNow]     = React.useState(new Date());
    const [pending, setPending] = React.useState(false);
    // Whether the signed-in user is a tracked employee (migration 048).
    // null = unknown until the first me() resolves. The server is the
    // authoritative source: non-tracked people (incl. managers/owners
    // who aren't marked as employees) get isEmployee:false and we hide
    // the whole FAB. Defaults to "show" for older backends that don't
    // send the flag yet.
    const [isEmp, setIsEmp] = React.useState(null);

    // Track the signed-in user so the FAB resets state when someone
    // else logs in on this browser. Without this, user A's `today`
    // sticks around for user B (and the "Hide widget" preference
    // user A set would silently hide it for user B too).
    const currentUserId = React.useMemo(() => {
      try {
        const u = window.api && window.api.getUser && window.api.getUser();
        return (u && u.id) || null;
      } catch { return null; }
    }, []);
    const [signedInId, setSignedInId] = React.useState(currentUserId);

    // Per-user hide preference. The toggle that controls this lives
    // on the My Day page now (used to be a button inside the popover
    // but that left users stranded with no way to bring it back).
    // We listen for `flowboard:punchFab:visibility` so the My Day
    // toggle can flip it without a reload.
    const hiddenKey = signedInId ? `fb.punchFab.hidden:${signedInId}` : "fb.punchFab.hidden";
    const [hidden, setHidden] = React.useState(() => {
      try { return localStorage.getItem(hiddenKey) === "1"; } catch { return false; }
    });
    React.useEffect(() => {
      try { setHidden(localStorage.getItem(hiddenKey) === "1"); } catch { setHidden(false); }
    }, [hiddenKey]);
    React.useEffect(() => {
      function onVis(ev) {
        const v = ev && ev.detail && ev.detail.hidden;
        setHidden(!!v);
      }
      window.addEventListener("flowboard:punchFab:visibility", onVis);
      return () => window.removeEventListener("flowboard:punchFab:visibility", onVis);
    }, []);

    // Live tick.
    React.useEffect(() => {
      const id = setInterval(() => setNow(new Date()), 1000);
      return () => clearInterval(id);
    }, []);

    // Re-evaluate the signed-in user every 5 s. If the cached user
    // id changes (login / logout / impersonate), we wipe the FAB
    // state and re-fetch so the widget shows the right person's data.
    React.useEffect(() => {
      const id = setInterval(() => {
        try {
          const u = window.api && window.api.getUser && window.api.getUser();
          const nextId = (u && u.id) || null;
          if (nextId !== signedInId) {
            setSignedInId(nextId);
            setToday(null);
            setBootstrapped(false);
          }
        } catch {}
      }, 5_000);
      return () => clearInterval(id);
    }, [signedInId]);

    // Fetch state on mount + poll every 30s.
    //
    // Reliability rules:
    //   - A SUCCESSFUL me() response is always authoritative — if the
    //     server says today is null (e.g. user has no workspace or no
    //     schedule today), we hide. We DO NOT fall back to cached
    //     optimistic state. Otherwise the FAB diverges from the main
    //     My day hero, which is the bug screenshot the user reported
    //     where the popover said "Currently working / FIRST IN 05:12"
    //     while the hero said "Not clocked in".
    //   - A FAILED me() (network glitch, 401, 503) only nulls the
    //     state on the very first attempt — once we've shown the
    //     widget for this user, transient errors keep the prior
    //     view alive.
    const [bootstrapped, setBootstrapped] = React.useState(false);
    const reload = React.useCallback(async () => {
      if (!window.api || !window.api.people || !window.api.people.me) return;
      try {
        const r = await window.api.people.me();
        // Trust the server: if today is null, show nothing; if it's an
        // object, render it. No optimistic carry-over from prior state.
        setToday(r && r.today ? r.today : null);
        // Authoritative employee flag. undefined (old backend) → show.
        setIsEmp(r ? (r.isEmployee !== false) : true);
        setBootstrapped(true);
      } catch (e) {
        // Migration pending or auth failure. If we've never loaded
        // successfully, stay hidden. Otherwise keep the prior state.
        if (!bootstrapped) setToday(null);
      }
    }, [bootstrapped]);
    React.useEffect(() => { reload(); }, [reload, signedInId]);
    React.useEffect(() => {
      const id = setInterval(() => {
        if (document.visibilityState === "visible") reload();
      }, 30_000);
      return () => clearInterval(id);
    }, [reload]);
    // Re-fetch immediately when the tab regains focus — bridges the
    // gap between a punch on another tab/device and the next poll.
    React.useEffect(() => {
      function onVis() { if (document.visibilityState === "visible") reload(); }
      document.addEventListener("visibilitychange", onVis);
      window.addEventListener("focus", onVis);
      // Cross-tab nudge: any tab that punches dispatches this event.
      window.addEventListener("flowboard:punched", reload);
      return () => {
        document.removeEventListener("visibilitychange", onVis);
        window.removeEventListener("focus", onVis);
        window.removeEventListener("flowboard:punched", reload);
      };
    }, [reload]);

    // ── Lunch-break voice reminder ────────────────────────────
    // At 13:00 and 13:10 (local time), if the employee is checked in
    // (not on break, not checked out), speak a reminder using the Web
    // Speech API. Each time fires at most once per day per device,
    // tracked in localStorage. Skipped if SpeechSynthesis isn't
    // available (some browsers / muted audio context).
    React.useEffect(() => {
      if (!today) return;
      if (isEmp === false) return;
      // External collaborators don't get the internal lunch reminder.
      try {
        const u = window.api && window.api.getUser && window.api.getUser();
        if (u && u.wsRole === "guest") return;
      } catch {}
      // Re-derive "currently in" the same way the JSX does below so
      // this stays consistent with the FAB's idea of state.
      const liveSeg  = (today.segments || []).find(s => s.live);
      const onBreak  = !!today.onBreak
        || (liveSeg && liveSeg.kind === "break")
        || today.lastPunchKind === "break_start";
      const inByStat = today.status === "in" || today.status === "remote" || today.status === "late";
      const inByPun  = !!today.in
        && (today.lastPunchKind === "in" || today.lastPunchKind === "break_end");
      const stateNow = onBreak ? "break" : ((inByStat || inByPun) ? "in" : "out");
      if (stateNow !== "in") return;
      if (typeof window === "undefined" || !window.speechSynthesis) return;

      const ymd = new Date().toISOString().slice(0, 10);
      const userId = (() => {
        try { return (window.api.getUser() || {}).id || ""; } catch { return ""; }
      })();
      // Two prompts: 13:00 (first nudge) and 13:10 (gentle follow-up).
      const slots = [
        { hh: 13, mm:  0, key: "lunch1", line: "It's one o'clock. Time for your lunch break." },
        { hh: 13, mm: 10, key: "lunch2", line: "Friendly reminder — please take a break for lunch now." },
      ];
      const timers = [];
      const today00 = new Date(); today00.setHours(0, 0, 0, 0);
      for (const s of slots) {
        const storageKey = `fb.lunchVoice:${userId}:${ymd}:${s.key}`;
        let alreadyFired = false;
        try { alreadyFired = window.localStorage.getItem(storageKey) === "1"; } catch {}
        if (alreadyFired) continue;
        const fireAt = new Date(today00); fireAt.setHours(s.hh, s.mm, 0, 0);
        const delay  = fireAt.getTime() - Date.now();
        if (delay < -60_000) continue;            // missed by > 1 min, skip
        const wait   = Math.max(0, delay);
        const tid = setTimeout(() => {
          try {
            const u = new window.SpeechSynthesisUtterance(s.line);
            u.rate = 0.95; u.pitch = 1.0; u.volume = 1.0;
            window.speechSynthesis.speak(u);
            window.localStorage.setItem(storageKey, "1");
          } catch {}
        }, wait);
        timers.push(tid);
      }
      return () => { for (const t of timers) clearTimeout(t); };
    }, [today, isEmp]);

    // Hide while signed out / no data yet / user dismissed it.
    if (hidden) return null;
    // Non-tracked users (is_employee = 0, migration 048) don't punch —
    // hide the whole widget for them, including managers/owners who
    // aren't themselves marked as employees.
    if (isEmp === false) return null;
    if (!today) return null;
    if (!window.api || !window.api.people || !window.api.people.punch) return null;

    // Per-user module-access flag — if an admin turned People OFF for
    // this user via Admin → Module access, hide the FAB too. Owners
    // always have access. Reads the user object via window.api.getUser
    // since the FAB is mounted outside the React tree that holds _me.
    try {
      const u = window.api.getUser && window.api.getUser();
      const ma = u && u.moduleAccess;
      const role = u && u.wsRole;
      if (role !== "owner" && ma && ma.people === false) return null;
    } catch {}

    // Derive state from today + live now.
    // IMPORTANT: status alone isn't enough — on a HOLIDAY or LEAVE day
    // an employee can still be clocked in (the resolver gives holiday/
    // leave precedence over 'in' in the status field). Honor
    // lastPunchKind / today.in so check-out + break stay reachable.
    const live = (today.segments || []).find(s => s.live);
    const isOnBreak = !!today.onBreak
      || (live && live.kind === "break")
      || today.lastPunchKind === "break_start";
    const isClockedInByStatus = today.status === "in" || today.status === "remote" || today.status === "late";
    const isClockedInByPunch  = !!today.in
      && (today.lastPunchKind === "in" || today.lastPunchKind === "break_end");
    const isClockedIn = isClockedInByStatus || isClockedInByPunch;
    const state = isOnBreak ? "break" : (isClockedIn ? "in" : "out");

    // Live worked-minutes — the server's hoursLogged ticks once per
    // poll, but we want a smooth counter that updates each second.
    // Compute the open work segment locally from the user's local
    // wall clock + the server's reported firstIn.
    let workedMins = (today.hoursLogged || 0) * 60;
    if (state === "in" && today.in) {
      // Add the in-progress chunk since the latest break_end (or
      // first in if no break this session). The server's hoursLogged
      // captured up to the last poll, so we extrapolate.
      const lastWorkStart = (() => {
        const segs = today.segments || [];
        // Find the last 'work' segment marked live; fall back to first in.
        const liveSeg = segs.find(s => s.live && s.kind === "work");
        if (liveSeg) return liveSeg.from;
        return parseHHMM(today.in);
      })();
      if (lastWorkStart != null) {
        const nowMins = now.getHours() * 60 + now.getMinutes();
        const delta = Math.max(0, nowMins - lastWorkStart);
        // hoursLogged from server already includes everything up to
        // the live segment's start (closed segments), so just add
        // the live delta. To avoid double-counting we re-derive from
        // closed segments only.
        const closedWork = segs => segs
          .filter(s => s.kind === "work" && !s.live)
          .reduce((a, s) => a + (s.to - s.from), 0);
        workedMins = closedWork(today.segments || []) + delta;
      }
    }

    const breakMins = (today.breakMins || 0);

    async function punch(kind) {
      if (pending) return;
      setPending(true);
      // Optimistic local state flip — flip immediately so the
      // popover's button row updates without waiting for the server
      // round-trip. If the punch fails, the reload re-syncs from
      // the server. This is what makes the FAB feel "instant" when
      // you tap Start break and care more about not losing the
      // popover than about waiting 200ms for the server answer.
      const optimistic = (() => {
        if (!today) return null;
        const t = { ...today };
        if (kind === 'in')          { t.status = 'in'; t.onBreak = false; t.lastPunchKind = 'in'; }
        else if (kind === 'out')    { t.status = 'out'; t.onBreak = false; t.lastPunchKind = 'out'; }
        else if (kind === 'break_start') { t.onBreak = true; t.lastPunchKind = 'break_start'; }
        else if (kind === 'break_end')   { t.onBreak = false; t.lastPunchKind = 'break_end'; }
        return t;
      })();
      if (optimistic) setToday(optimistic);
      try {
        await window.api.people.punch({ kind });
        await reload();
        // Tell the rest of the app — main page hero etc. — to refresh.
        try { window.dispatchEvent(new CustomEvent("flowboard:punched", { detail: { kind } })); } catch {}
        if (window.fbToast) {
          window.fbToast(({ in: "Punched in", out: "Punched out",
                             break_start: "Break started",
                             break_end: "Break ended" })[kind] || "Punch saved", 2200);
        }
      } catch (e) {
        // Roll back to the pre-click state and surface the error.
        if (today) setToday(today);
        const body = e && e.body;
        const errCode = body && body.error;
        const msg = (body && (body.message || body.error)) || (e && e.message) || "Punch failed";
        const human = /migration_pending/i.test(String(errCode))
          ? "Punch storage isn't ready — apply migration 044 on the server."
          : msg;
        if (window.fbToast) window.fbToast(human, 4500);
      } finally {
        setPending(false);
      }
    }

    // ── pill (collapsed) ────────────────────────────────────
    const pillMeta =
      state === "in"    ? { label: "Working",  bg: "linear-gradient(180deg, #0ea15c, #066039)", fg: "#fff", dot: "#5fe6a5" } :
      state === "break" ? { label: "On break", bg: "linear-gradient(180deg, #f7a82a, #b86b00)", fg: "#fff", dot: "#ffd86b" } :
                          { label: "Punch in", bg: "linear-gradient(180deg, #353b4d, #1c2033)", fg: "#fff", dot: "#9aa0ae" };

    return (
      <>
        {!open && (
          <button type="button"
                  className="zp-fab-punch"
                  onClick={() => setOpen(true)}
                  title={state === "in" ? "Working — click for options"
                       : state === "break" ? "On break — click for options"
                       : "Tap to punch in"}>
            <span className="zp-fab-dot" style={{ background: pillMeta.dot }}/>
            <span className="zp-fab-label" style={{ color: pillMeta.fg }}>{pillMeta.label}</span>
            {state !== "out" && (
              <span className="zp-fab-time">{fmtDuration(workedMins)}</span>
            )}
            <style>{`
              .zp-fab-punch {
                position: fixed;
                right: 88px; bottom: 22px;
                z-index: 9000;
                display: inline-flex; align-items: center; gap: 8px;
                padding: 9px 14px;
                border: 0; border-radius: 999px;
                background: ${pillMeta.bg};
                color: white;
                font: 600 13px/1 var(--font-sans, "Figtree", system-ui, sans-serif);
                box-shadow: 0 8px 22px rgba(15,23,41,.30), 0 2px 4px rgba(0,0,0,.16);
                cursor: pointer;
                transition: transform .12s, box-shadow .12s;
              }
              .zp-fab-punch:hover { transform: translateY(-1px); box-shadow: 0 10px 26px rgba(15,23,41,.34), 0 2px 4px rgba(0,0,0,.18); }
              .zp-fab-dot {
                width: 8px; height: 8px; border-radius: 999px;
                box-shadow: 0 0 0 0 currentColor;
              }
              .zp-fab-punch:not([data-state="out"]) .zp-fab-dot {
                animation: zp-fab-pulse 1.8s infinite;
              }
              .zp-fab-time {
                font-variant-numeric: tabular-nums;
                font-weight: 700;
                padding-left: 8px; margin-left: 2px;
                border-left: 1px solid rgba(255,255,255,.25);
              }
              @keyframes zp-fab-pulse {
                0%   { box-shadow: 0 0 0 0   rgba(255,255,255,.55); }
                70%  { box-shadow: 0 0 0 8px rgba(255,255,255,0); }
                100% { box-shadow: 0 0 0 0   rgba(255,255,255,0); }
              }
              @media (max-width: 720px) {
                .zp-fab-punch { right: 70px; bottom: 18px; padding: 7px 11px; }
              }
            `}</style>
          </button>
        )}

        {open && (
          <div className="zp-fab-pop" onMouseDown={(e) => e.stopPropagation()}>
            <div className="zp-fab-pop-head">
              <div>
                <div className="zp-fab-pop-eyebrow">CHECK-IN · {now.toLocaleDateString("en-US",{ weekday:"short", month:"short", day:"numeric" }).toUpperCase()}</div>
                <div className="zp-fab-pop-title">
                  <span className="zp-fab-pop-dot" style={{ background: pillMeta.dot }}/>
                  {state === "in" ? "Currently working" : state === "break" ? "On break" : "Not clocked in"}
                </div>
                <div className="zp-fab-pop-sub">
                  {today.schedule
                    ? <>Scheduled <b>{today.schedule.from} – {today.schedule.to}</b></>
                    : <>No schedule today</>}
                </div>
              </div>
              <button type="button"
                      className="zp-fab-pop-close"
                      onClick={() => setOpen(false)}
                      title="Collapse">×</button>
            </div>

            <div className="zp-fab-pop-stats">
              <div className="zp-fab-stat">
                <div className="zp-fab-stat-label">Worked</div>
                <div className="zp-fab-stat-value">{fmtDuration(workedMins)}</div>
              </div>
              <div className="zp-fab-stat">
                <div className="zp-fab-stat-label">Break</div>
                <div className="zp-fab-stat-value">{breakMins}m</div>
              </div>
              <div className="zp-fab-stat">
                <div className="zp-fab-stat-label">First in</div>
                <div className="zp-fab-stat-value">{today.in || "—"}</div>
              </div>
            </div>

            <div className="zp-fab-pop-actions">
              {state === "in" && (
                <>
                  <button className="zp-fab-btn zp-fab-btn-warn" disabled={pending}
                          onClick={() => punch("break_start")}>Start break</button>
                  <button className="zp-fab-btn zp-fab-btn-danger" disabled={pending}
                          onClick={() => punch("out")}>Punch out</button>
                </>
              )}
              {state === "break" && (
                <>
                  <button className="zp-fab-btn zp-fab-btn-primary" disabled={pending}
                          onClick={() => punch("break_end")}>End break · resume</button>
                  <button className="zp-fab-btn zp-fab-btn-danger" disabled={pending}
                          onClick={() => punch("out")}>Punch out</button>
                </>
              )}
              {state === "out" && (
                <button className="zp-fab-btn zp-fab-btn-primary" disabled={pending}
                        onClick={() => punch("in")}>
                  {today.out ? "Punch back in" : "Punch in"}
                </button>
              )}
            </div>

            <div className="zp-fab-pop-foot">
              <button type="button"
                      className="zp-fab-link"
                      onClick={() => {
                        try {
                          window.localStorage.setItem("fb.peopleTab", "day");
                          window.dispatchEvent(new CustomEvent("flowboard:nav", {
                            detail: { view: "people", peopleTab: "day" }
                          }));
                        } catch {}
                        setOpen(false);
                      }}>
                Open My time →
              </button>
            </div>

            <style>{ZP_FAB_POP_CSS}</style>
          </div>
        )}
      </>
    );
  }

  const ZP_FAB_POP_CSS = `
  .zp-fab-pop {
    position: fixed;
    right: 88px; bottom: 22px;
    z-index: 9000;
    width: min(320px, 92vw);
    background: white;
    color: #0f1729;
    border-radius: 14px;
    box-shadow: 0 18px 50px rgba(15,23,41,.28), 0 4px 10px rgba(0,0,0,.10);
    font: 400 13px/1.4 var(--font-sans, "Figtree", system-ui, sans-serif);
    overflow: hidden;
  }
  @media (max-width: 720px) {
    .zp-fab-pop { right: 12px; left: 12px; width: auto; bottom: 14px; }
  }
  .zp-fab-pop-head {
    display: flex; gap: 10px;
    padding: 14px 16px 10px;
    border-bottom: 1px solid #eef0f5;
  }
  .zp-fab-pop-eyebrow {
    font-size: 10.5px; font-weight: 700; letter-spacing: .12em;
    color: #6c7385; margin-bottom: 4px;
  }
  .zp-fab-pop-title {
    display: flex; align-items: center; gap: 8px;
    font-weight: 700; font-size: 15px; color: #0f1729;
  }
  .zp-fab-pop-dot {
    width: 9px; height: 9px; border-radius: 999px;
    box-shadow: 0 0 0 3px rgba(0,0,0,.04);
  }
  .zp-fab-pop-sub {
    margin-top: 4px;
    font-size: 12px; color: #676879;
  }
  .zp-fab-pop-sub b { color: #0f1729; }
  .zp-fab-pop-close {
    margin-left: auto; align-self: flex-start;
    background: transparent; border: 0;
    width: 26px; height: 26px; border-radius: 7px;
    font-size: 18px; line-height: 1; color: #676879;
    cursor: pointer;
  }
  .zp-fab-pop-close:hover { background: rgba(0,0,0,.05); color: #0f1729; }

  .zp-fab-pop-stats {
    display: grid; grid-template-columns: repeat(3, 1fr);
    gap: 8px;
    padding: 12px 16px;
    background: #fafbfd;
    border-bottom: 1px solid #eef0f5;
  }
  .zp-fab-stat-label {
    font-size: 10px; font-weight: 700; letter-spacing: .08em;
    text-transform: uppercase; color: #6c7385;
    margin-bottom: 4px;
  }
  .zp-fab-stat-value {
    font-size: 16px; font-weight: 800; color: #0f1729;
    font-variant-numeric: tabular-nums; letter-spacing: -0.01em;
  }

  .zp-fab-pop-actions {
    display: flex; gap: 8px; padding: 12px 16px;
  }
  .zp-fab-btn {
    flex: 1;
    padding: 9px 12px;
    border: 0; border-radius: 8px;
    font: 600 13px/1 var(--font-sans, "Figtree", system-ui, sans-serif);
    cursor: pointer;
    transition: transform .1s, box-shadow .1s;
  }
  .zp-fab-btn:hover { transform: translateY(-1px); }
  .zp-fab-btn:disabled { opacity: .55; cursor: wait; transform: none; }
  .zp-fab-btn-primary { background: #0073ea; color: white; box-shadow: 0 2px 4px rgba(0,115,234,.32); }
  .zp-fab-btn-warn    { background: #fff4e0; color: #8a5a14; border: 1px solid rgba(138,90,20,.22); }
  .zp-fab-btn-danger  { background: rgba(226,68,92,.10); color: #8a1024; border: 1px solid rgba(226,68,92,.34); }

  .zp-fab-pop-foot {
    display: flex; justify-content: space-between; align-items: center;
    padding: 8px 14px 12px;
    border-top: 1px solid #eef0f5;
    background: #fafbfd;
  }
  .zp-fab-link {
    background: transparent; border: 0; cursor: pointer;
    font: 600 12px/1 var(--font-sans, "Figtree", system-ui, sans-serif);
    color: #0073ea; padding: 4px 6px; border-radius: 5px;
  }
  .zp-fab-link:hover { background: rgba(0,115,234,.08); }
  .zp-fab-link-quiet { color: #6c7385; }
  .zp-fab-link-quiet:hover { background: rgba(0,0,0,.04); color: #0f1729; }
  `;

  // ── Ava — daily briefing (HR coach + scrum master) ─────────
  // On the first check-in of the day Ava pops a MORNING STAND-UP
  // (overdue + due today, plus a light attendance nod). On check-out
  // after 5pm she pops an END-OF-DAY WRAP-UP (completed today, still
  // pending/overdue, due tomorrow, + a nudge to update statuses). Each
  // shows at most once per day per mode (localStorage flag keyed by
  // user + date + mode). Triggers: `flowboard:punched` (in / out) from
  // any surface, and page load while already checked in.
  function hrUserId() {
    try { const u = window.api && window.api.getUser && window.api.getUser(); return (u && u.id) || "anon"; }
    catch { return "anon"; }
  }
  function hrLocalDate() {
    const d = new Date();
    return d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0");
  }
  const HRG_ACCENT = { ok: "#21a366", info: "#0073ea", watch: "#f0b429", concern: "#e2445c" };

  function HrGreeter() {
    const [brief, setBrief] = React.useState(null);
    const [coach, setCoach] = React.useState("Ava");
    const [open, setOpen] = React.useState(false);
    const [loading, setLoading] = React.useState(false);
    const [mode, setMode] = React.useState("checkin");

    const flagKey = (m) => "fb.brief." + m + ":" + hrUserId() + ":" + hrLocalDate();
    const shownToday = (m) => { try { return localStorage.getItem(flagKey(m)) === "1"; } catch { return false; } };
    const markShown = (m) => { try { localStorage.setItem(flagKey(m), "1"); } catch {} };

    const showBrief = React.useCallback(async (m) => {
      if (!(window.api && window.api.people && window.api.people.hr && window.api.people.hr.brief)) return;
      if (shownToday(m)) return;
      setMode(m); setBrief(null); setLoading(true); setOpen(true);
      try {
        const r = await window.api.people.hr.brief(m);
        if (r && r.coach) setCoach(r.coach);
        if (r && r.brief) { setBrief(r.brief); markShown(m); }
        else { setOpen(false); }
      } catch { setOpen(false); }
      finally { setLoading(false); }
    }, []);

    // Punch-driven triggers (any surface).
    React.useEffect(() => {
      function onPunch(e) {
        const kind = e && e.detail && e.detail.kind;
        if (kind === "in")  setTimeout(() => showBrief("checkin"), 600);
        else if (kind === "out" && new Date().getHours() >= 17) setTimeout(() => showBrief("checkout"), 600);
      }
      window.addEventListener("flowboard:punched", onPunch);
      return () => window.removeEventListener("flowboard:punched", onPunch);
    }, [showBrief]);

    // On mount: if already checked in (and not yet out) today, give the
    // morning brief so a page reload still surfaces it once.
    React.useEffect(() => {
      let alive = true;
      (async () => {
        if (!(window.api && window.api.people && window.api.people.me)) return;
        try {
          const r = await window.api.people.me();
          if (!alive) return;
          if (r && r.isEmployee !== false && r.today && r.today.in && !r.today.out) showBrief("checkin");
        } catch {}
      })();
      return () => { alive = false; };
    }, [showBrief]);

    function close() { setOpen(false); }
    function openMyWork() {
      try { window.dispatchEvent(new CustomEvent("flowboard:nav", { detail: { view: "home" } })); } catch {}
      setOpen(false);
    }

    if (!open) return null;
    const sev = (brief && brief.severity) || "info";
    const accent = HRG_ACCENT[sev] || "#0073ea";
    const modeLabel = mode === "checkout" ? "End-of-day wrap-up" : "Morning stand-up";
    const t = (brief && brief.tasks) || {};

    function section(label, items, tone) {
      if (!items || !items.length) return null;
      return (
        <div className="zp-hrg-sec" key={label}>
          <div className={"zp-hrg-sec-h zp-hrg-" + tone}>
            <span className="zp-hrg-sec-dot"/>{label}<span className="zp-hrg-sec-n">{items.length}</span>
          </div>
          {items.slice(0, 5).map((it, i) => (
            <div className="zp-hrg-task" key={i}>
              <span className="zp-hrg-task-name">{it.name}</span>
              {it.due ? <span className="zp-hrg-task-due">{it.due}</span> : null}
            </div>
          ))}
          {items.length > 5 && <div className="zp-hrg-more">+{items.length - 5} more</div>}
        </div>
      );
    }

    const modal = (
      <div className="zp-hrg-overlay" onMouseDown={close}>
        <div className="zp-hrg-card" style={{ borderTopColor: accent }} onMouseDown={(e) => e.stopPropagation()}>
          <button className="zp-hrg-x" onClick={close} title="Dismiss">×</button>
          <div className="zp-hrg-head">
            <div className="zp-hrg-avatar" style={{ background: accent + "1f", color: accent }}>
              <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M12 3 L13.6 10.4 L21 12 L13.6 13.6 L12 21 L10.4 13.6 L3 12 L10.4 10.4 Z"/></svg>
            </div>
            <div>
              <div className="zp-hrg-coach">{coach}</div>
              <div className="zp-hrg-tag">{modeLabel}</div>
            </div>
          </div>

          {loading ? (
            <div className="zp-hrg-loading">
              <span className="zp-hrg-spin"/> {coach} is reviewing your day…
            </div>
          ) : brief ? (
            <>
              {brief.headline && <div className="zp-hrg-headline">{brief.headline}</div>}
              <div className="zp-hrg-msg">{brief.message}</div>
              <div className="zp-hrg-sections">
                {mode === "checkin" ? (
                  <>
                    {section("Overdue", t.overdue, "concern")}
                    {section("Due today", t.dueToday, "info")}
                  </>
                ) : (
                  <>
                    {section("Completed today", t.completedToday, "ok")}
                    {section("Still overdue", t.overdue, "concern")}
                    {section("Due tomorrow", t.dueTomorrow, "watch")}
                  </>
                )}
              </div>
              <div className="zp-hrg-actions">
                <button className="zp-hrg-link" onClick={openMyWork}>Open my work →</button>
                <button className="zp-hrg-btn" style={{ background: accent }} onClick={close}>Got it</button>
              </div>
            </>
          ) : null}
          <style>{ZP_HRG_CSS}</style>
        </div>
      </div>
    );
    return (ReactDOM && ReactDOM.createPortal) ? ReactDOM.createPortal(modal, document.body) : modal;
  }

  const ZP_HRG_CSS = `
  .zp-hrg-overlay {
    position: fixed; inset: 0; z-index: 9500;
    background: rgba(15,23,41,.42);
    display: flex; align-items: center; justify-content: center;
    padding: 20px;
    animation: zp-hrg-fade .14s ease-out;
  }
  @keyframes zp-hrg-fade { from { opacity: 0; } to { opacity: 1; } }
  .zp-hrg-card {
    position: relative;
    width: min(420px, 94vw);
    background: #fff; color: #0f1729;
    border-radius: 14px; border-top: 4px solid #0073ea;
    box-shadow: 0 24px 60px rgba(15,23,41,.34), 0 6px 14px rgba(0,0,0,.12);
    padding: 20px 22px 18px;
    font: 400 13px/1.5 var(--font-sans, "Figtree", system-ui, sans-serif);
    animation: zp-hrg-pop .16s ease-out;
  }
  @keyframes zp-hrg-pop { from { transform: translateY(8px) scale(.98); opacity: .6; } to { transform: none; opacity: 1; } }
  .zp-hrg-x {
    position: absolute; top: 10px; right: 12px;
    background: transparent; border: 0; cursor: pointer;
    width: 28px; height: 28px; border-radius: 7px;
    font-size: 19px; line-height: 1; color: #8a90a0;
  }
  .zp-hrg-x:hover { background: rgba(0,0,0,.05); color: #0f1729; }
  .zp-hrg-head { display: flex; align-items: center; gap: 11px; }
  .zp-hrg-avatar {
    width: 38px; height: 38px; border-radius: 50%; flex: none;
    display: flex; align-items: center; justify-content: center;
  }
  .zp-hrg-coach { font-size: 14px; font-weight: 800; color: #0f1729; }
  .zp-hrg-tag {
    font-size: 9.5px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase;
    color: #6c7385;
  }
  .zp-hrg-sev { margin-left: auto; font-size: 11px; font-weight: 800; }
  .zp-hrg-headline { font-size: 16px; font-weight: 800; color: #0f1729; margin-top: 14px; letter-spacing: -0.01em; }
  .zp-hrg-msg { font-size: 13.5px; line-height: 1.6; color: #3a4154; margin-top: 6px; }
  .zp-hrg-actions { display: flex; align-items: center; gap: 10px; margin-top: 18px; }
  .zp-hrg-btn {
    margin-left: auto;
    border: 0; border-radius: 9px; cursor: pointer;
    color: #fff; font: 700 13.5px/1 var(--font-sans, "Figtree", system-ui, sans-serif);
    padding: 11px 22px; box-shadow: 0 2px 6px rgba(15,23,41,.18);
    transition: transform .1s, filter .15s;
  }
  .zp-hrg-btn:hover { transform: translateY(-1px); filter: brightness(1.04); }
  .zp-hrg-link {
    background: transparent; border: 0; cursor: pointer;
    font: 700 12.5px/1 var(--font-sans, "Figtree", system-ui, sans-serif);
    color: #0073ea; padding: 6px 4px; border-radius: 6px;
  }
  .zp-hrg-link:hover { background: rgba(0,115,234,.08); }

  /* loading + task sections */
  .zp-hrg-loading {
    display: flex; align-items: center; gap: 10px;
    color: #676879; font-size: 13px; padding: 22px 2px 8px;
  }
  .zp-hrg-spin {
    width: 16px; height: 16px; border-radius: 50%;
    border: 2px solid rgba(0,115,234,.25); border-top-color: #0073ea;
    animation: zp-hrg-spin 0.7s linear infinite;
  }
  @keyframes zp-hrg-spin { to { transform: rotate(360deg); } }
  .zp-hrg-sections { margin-top: 14px; display: flex; flex-direction: column; gap: 12px; max-height: 46vh; overflow-y: auto; }
  .zp-hrg-sec-h {
    display: flex; align-items: center; gap: 7px;
    font-size: 11px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase;
    margin-bottom: 6px;
  }
  .zp-hrg-sec-dot { width: 8px; height: 8px; border-radius: 50%; background: currentColor; flex: none; }
  .zp-hrg-sec-n {
    margin-left: 4px; font-size: 10.5px; font-weight: 800; color: #fff;
    background: #888; min-width: 16px; height: 16px;
    border-radius: 99px; display: inline-flex; align-items: center; justify-content: center; padding: 0 5px;
  }
  .zp-hrg-ok      { color: #1a8a55; }
  .zp-hrg-info    { color: #0073ea; }
  .zp-hrg-watch   { color: #a8740f; }
  .zp-hrg-concern { color: #c5354b; }
  .zp-hrg-ok      .zp-hrg-sec-n { background: #1a8a55; }
  .zp-hrg-info    .zp-hrg-sec-n { background: #0073ea; }
  .zp-hrg-watch   .zp-hrg-sec-n { background: #a8740f; }
  .zp-hrg-concern .zp-hrg-sec-n { background: #c5354b; }
  .zp-hrg-task {
    display: flex; align-items: center; gap: 8px;
    padding: 6px 10px; border: 1px solid #eef0f5; border-radius: 8px;
    background: #fafbfd; margin-bottom: 5px;
  }
  .zp-hrg-task-name { font-size: 12.5px; color: #0f1729; font-weight: 500; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .zp-hrg-task-due { font-size: 11px; color: #676879; font-variant-numeric: tabular-nums; flex: none; }
  .zp-hrg-more { font-size: 11.5px; color: #8a90a0; padding: 2px 2px 0; }
  `;

  // ── mount ──────────────────────────────────────────────────
  function FabRoot() {
    return (
      <>
        <PunchFab/>
        <HrGreeter/>
      </>
    );
  }
  function mount() {
    if (document.getElementById("zp-punch-fab-host")) return;
    const node = document.createElement("div");
    node.id = "zp-punch-fab-host";
    document.body.appendChild(node);
    try {
      if (ReactDOM.createRoot) ReactDOM.createRoot(node).render(<FabRoot/>);
      else ReactDOM.render(<FabRoot/>, node);
    } catch (e) {
      console.warn("[punch-fab] mount failed:", e && e.message);
    }
  }
  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", mount);
  } else {
    mount();
  }

  // Public toggle so anyone can resurrect the widget after Hide. Wipes
  // both the legacy global key and the per-user key for the currently
  // signed-in account.
  window.zpShowPunchFab = function () {
    try {
      localStorage.removeItem("fb.punchFab.hidden");
      const u = window.api && window.api.getUser && window.api.getUser();
      if (u && u.id) localStorage.removeItem("fb.punchFab.hidden:" + u.id);
    } catch {}
    location.reload();
  };
})();
