const { useState, useEffect, useCallback, useRef } = React;

/* ─────────────────────────────────────────────────────────
   DAYBOARD — three-phase daily task board with Gmail login
   Employee portal: sign up / log in, edit own tasks only.
   Admin portal:    oversee & edit everyone, remove employees.
   ───────────────────────────────────────────────────────── */

const MODE = TaskDB.mode; // 'employee' | 'admin'
let nonce = TaskDB.nonce;  // refreshed after every auth call

const PHASES = [
  { id: "morning", label: "Morning", short: "AM", color: "#E8553F", desc: "Start of day" },
  { id: "midday", label: "Post-lunch", short: "PL", color: "#F2A93B", desc: "After lunch" },
  { id: "eod", label: "End of day", short: "EOD", color: "#3DA164", desc: "Day close" },
];

const PRIORITIES = [
  { id: "high", label: "High", color: "#E8553F", bg: "#FDEAE6" },
  { id: "medium", label: "Medium", color: "#B47B14", bg: "#FCF1DC" },
  { id: "low", label: "Low", color: "#5B6470", bg: "#EEF0F2" },
];
const prioRank = { high: 0, medium: 1, low: 2 };

/* Assigned-by-admin styling — one place, so the badge, the row tint and the
   stripe can never drift apart. Deliberately blue: nothing else on the board
   uses it (green = done, amber = in progress, purple = notes/source). */
const ADMIN_TASK = {
  color: "#1F5FBF",
  bg: "#F0F6FF",
  border: "#BBD2F7",
  label: "From admin",
};

/* Filed through the public request link. Badge only — the tinted row stays
   unique to admin tasks so that signal doesn't get diluted. */
const EXTERNAL_TASK = { color: "#0F766E", border: "#B6DED8" };

/* offset of the sticky header + control bar, shared by everything that pins */
const STICKY_TOP = "6.4rem";

/* ── date helpers ── */
const toISO = (d) => {
  const y = d.getFullYear();
  const m = String(d.getMonth() + 1).padStart(2, "0");
  const dd = String(d.getDate()).padStart(2, "0");
  return `${y}-${m}-${dd}`;
};
const todayISO = () => toISO(new Date());
const shiftDate = (iso, days) => {
  const [y, m, d] = iso.split("-").map(Number);
  const dt = new Date(y, m - 1, d);
  dt.setDate(dt.getDate() + days);
  return toISO(dt);
};
const prettyDate = (iso) => {
  const [y, m, d] = iso.split("-").map(Number);
  return new Date(y, m - 1, d).toLocaleDateString("en-IN", {
    weekday: "short", day: "numeric", month: "short", year: "numeric",
  });
};
const currentPhase = () => {
  const h = new Date().getHours();
  if (h < 13) return "morning";
  if (h < 18) return "midday";
  return "eod";
};

/* ── week / month helpers ── */
const mondayOf = (iso) => {
  const [y, m, d] = iso.split("-").map(Number);
  const dt = new Date(y, m - 1, d);
  const dow = (dt.getDay() + 6) % 7; // 0 = Monday
  dt.setDate(dt.getDate() - dow);
  return toISO(dt);
};
const weekLabel = (mondayIso) => {
  const [y, m, d] = mondayIso.split("-").map(Number);
  const f = (dt) => dt.toLocaleDateString("en-IN", { day: "numeric", month: "short" });
  return `${f(new Date(y, m - 1, d))} – ${f(new Date(y, m - 1, d + 6))}`;
};
const thisMonthKey = () => todayISO().slice(0, 7);
const monthLabel = (key) => {
  const [y, m] = key.split("-").map(Number);
  return new Date(y, m - 1, 1).toLocaleDateString("en-IN", { month: "long", year: "numeric" });
};
const shiftMonth = (key, delta) => {
  let [y, m] = key.split("-").map(Number);
  m += delta;
  while (m < 1) { m += 12; y--; }
  while (m > 12) { m -= 12; y++; }
  return `${y}-${String(m).padStart(2, "0")}`;
};

/* ── API helper (cookie auth + rolling nonce) ── */
async function api(path, opts = {}) {
  let res, data = null;
  try {
    res = await fetch(TaskDB.rest + path, {
      ...opts,
      credentials: "same-origin",
      headers: { "Content-Type": "application/json", "X-WP-Nonce": nonce, ...(opts.headers || {}) },
    });
    data = await res.json().catch(() => null);
  } catch {
    return { ok: false, status: 0, data: null };
  }
  if (data && data.nonce) nonce = data.nonce;
  return { ok: res.ok, status: res.status, data };
}
const apiError = (r, fallback) => (r.data && (r.data.message || r.data.error)) || fallback;

const emptyBoard = () => ({ members: [], tasks: {}, checkins: {} });
const initials = (name) =>
  (name || "?").split(" ").map((w) => w[0]).join("").slice(0, 2).toUpperCase();
let uid = () => Date.now().toString(36) + Math.random().toString(36).slice(2, 6);

/* ── calendar helpers ── */
const MONTHS_SHORT = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
const DOW_SHORT = ["M", "T", "W", "T", "F", "S", "S"];

/* Six full Mon-Sun weeks covering the given YYYY-MM, so the grid never jumps height. */
const monthGrid = (key) => {
  const [y, m] = key.split("-").map(Number);
  const startDow = (new Date(y, m - 1, 1).getDay() + 6) % 7; // 0 = Monday
  const cells = [];
  for (let i = 0; i < 42; i++) {
    const d = new Date(y, m - 1, 1 - startDow + i);
    cells.push({ iso: toISO(d), day: d.getDate(), inMonth: d.getMonth() === m - 1 });
  }
  return cells;
};

/* close a popover on outside click or Escape */
function useDismiss(ref, onDismiss) {
  useEffect(() => {
    const click = (e) => { if (ref.current && !ref.current.contains(e.target)) onDismiss(); };
    const key = (e) => { if (e.key === "Escape") onDismiss(); };
    document.addEventListener("mousedown", click);
    document.addEventListener("keydown", key);
    return () => { document.removeEventListener("mousedown", click); document.removeEventListener("keydown", key); };
  });
}

/* true once the page has scrolled — used to shadow the sticky header */
function useScrolled(threshold = 6) {
  const [scrolled, setScrolled] = useState(false);
  useEffect(() => {
    const on = () => setScrolled(window.scrollY > threshold);
    on();
    window.addEventListener("scroll", on, { passive: true });
    return () => window.removeEventListener("scroll", on);
  }, [threshold]);
  return scrolled;
}

/* poll a function on an interval + when the tab regains focus */
function usePoll(fn, ms) {
  const ref = useRef(fn);
  ref.current = fn;
  useEffect(() => {
    const tick = () => { if (!document.hidden) ref.current(); };
    const id = setInterval(tick, ms);
    window.addEventListener("focus", tick);
    return () => { clearInterval(id); window.removeEventListener("focus", tick); };
  }, [ms]);
}

/* ═══════════════════════════════════════════════════════ */

function App() {
  const [me, setMe] = useState(null);
  const [booting, setBooting] = useState(true);

  useEffect(() => {
    if (MODE === "intake") { setBooting(false); return; } // public form: no identity to fetch
    (async () => {
      const r = await api("me?mode=" + encodeURIComponent(MODE));
      setMe(r.data?.user || { id: 0, role: "guest" });
      setBooting(false);
    })();
  }, []);

  const logout = async () => {
    const r = await api("logout", { method: "POST" });
    setMe(r.data?.user || { id: 0, role: "guest" });
  };
  const updateUser = (patch) => setMe((prev) => ({ ...prev, ...patch }));

  if (booting) return <Shell><Centered>Loading…</Centered></Shell>;

  // Public request form — deliberately never touches the board.
  if (MODE === "intake") return <Shell><IntakeScreen /></Shell>;

  const role = me?.role || "guest";

  if (MODE === "admin") {
    if (role !== "admin") {
      return <Shell><AuthScreen kind="admin-login" current={me} onAuth={setMe} /></Shell>;
    }
    return <Board me={me} isAdmin onLogout={logout} onUpdateUser={updateUser} />;
  }

  // employee portal
  if (role === "guest") {
    return <Shell><AuthScreen kind="employee" onAuth={setMe} /></Shell>;
  }
  return <Board me={me} isAdmin={role === "admin"} onLogout={logout} onUpdateUser={updateUser} />;
}

/* ── page shell + shared styles ── */
function Shell({ children }) {
  return (
    <div className="min-h-screen" style={{ background: "#F2F3F1", color: "#16191D", fontFamily: "'IBM Plex Sans', sans-serif" }}>
      <GlobalStyle />
      {children}
    </div>
  );
}
function Centered({ children }) {
  return <p className="pt-20 text-center text-sm" style={{ color: "#5B6470" }}>{children}</p>;
}
function GlobalStyle() {
  return (
    <style>{`
      @import url('https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,500;12..96,700;12..96,800&family=IBM+Plex+Sans:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap');
      .disp { font-family: 'Bricolage Grotesque', sans-serif; }
      .mono { font-family: 'IBM Plex Mono', monospace; }
      button:focus-visible, input:focus-visible, select:focus-visible { outline: 2px solid #16191D; outline-offset: 2px; }
      @media (prefers-reduced-motion: reduce) { * { transition: none !important; animation: none !important; } }
      .task-row:hover .del-btn { opacity: 1; }
      .fade-in { animation: fi .25s ease; }
      @keyframes fi { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
      /* click-to-edit affordance: only hint at it on hover so rows stay calm */
      .editable-text { cursor: text; border-bottom: 1px dashed transparent; }
      .editable-text:hover { border-bottom-color: #B9BFC6; }
      .side-scroll { scrollbar-width: thin; }

      /* WordPress themes style bare <button> elements (often with a brand
         colour and their own padding), which beats Tailwind's utility classes.
         These two opt-in classes take the background back with !important so
         our chrome looks the same on any theme. Only background/border are
         forced — inline colour still wins for state. */
      #task-db-root button.tdb-ghost { background: transparent !important; box-shadow: none !important; text-transform: none !important; }
      #task-db-root button.tdb-ghost:hover { background: rgba(0,0,0,.05) !important; }
      #task-db-root button.tdb-plain { background: #fff !important; box-shadow: none !important; text-transform: none !important; }
      #task-db-root button.tdb-plain:hover { background: #FAFBFA !important; }
    `}</style>
  );
}

/* ── auth screen: employee (login+signup) or admin (login only) ── */
function AuthScreen({ kind, current, onAuth }) {
  const isAdmin = kind === "admin-login";
  const [tab, setTab] = useState("login"); // login | signup | reset
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState("");
  const [offerReset, setOfferReset] = useState(false);
  const [resetSent, setResetSent] = useState(false);

  const wrongPortal = isAdmin && current && current.role !== "guest" && current.role !== "admin";

  const goTab = (t) => { setTab(t); setErr(""); setOfferReset(false); setResetSent(false); };

  const submit = async () => {
    setErr(""); setOfferReset(false);
    if (!email.trim() || !password) { setErr("Email and password are required."); return; }
    if (!isAdmin && tab === "signup" && !/@gmail\.com$/i.test(email.trim())) {
      setErr("Please use a Gmail (@gmail.com) address."); return;
    }
    setBusy(true);
    const path = (!isAdmin && tab === "signup") ? "signup" : "login";
    const body = JSON.stringify({ name: name.trim(), email: email.trim(), password });
    const r = await api(path, { method: "POST", body });
    setBusy(false);
    if (!r.ok) {
      setErr(apiError(r, "Something went wrong. Try again."));
      if (r.status === 409) setOfferReset(true); // email already exists
      return;
    }
    const user = r.data.user;
    if (isAdmin && user.role !== "admin") {
      setErr("That account isn't an administrator. Use a WordPress admin account.");
      return;
    }
    onAuth(user);
  };

  const requestReset = async () => {
    setErr("");
    if (!email.trim()) { setErr("Enter your email and we'll send a reset link."); return; }
    setBusy(true);
    await api("reset", { method: "POST", body: JSON.stringify({ email: email.trim() }) });
    setBusy(false);
    setResetSent(true);
  };

  const heading = tab === "reset" ? "Reset your password"
    : isAdmin ? "Admin sign in" : tab === "login" ? "Welcome back" : "Create your account";
  const subheading = tab === "reset" ? "We'll email you a link to set a new password."
    : isAdmin ? "Sign in with a WordPress administrator account."
    : tab === "login" ? "Log in to see your team board." : "Sign up with your Gmail to join the board.";

  return (
    <div className="max-w-md mx-auto px-4 pt-16 pb-20">
      <div className="flex items-center justify-center gap-2 mb-6">
        <div className="flex gap-1">
          {PHASES.map((p) => <span key={p.id} className="w-3 h-3 rounded-full" style={{ background: p.color }} />)}
        </div>
        <h1 className="disp text-2xl font-bold tracking-tight">DayBoard</h1>
      </div>

      <div className="rounded-2xl border bg-white overflow-hidden" style={{ borderColor: "#DDE0DC" }}>
        <div className="px-6 pt-6">
          <h2 className="disp text-lg font-bold">{heading}</h2>
          <p className="text-sm mt-1" style={{ color: "#5B6470" }}>{subheading}</p>
        </div>

        {!isAdmin && tab !== "reset" && (
          <div className="flex gap-1 px-6 mt-4">
            {["login", "signup"].map((t) => (
              <button key={t} onClick={() => goTab(t)}
                className="px-3 py-1.5 rounded-lg text-sm font-medium capitalize"
                style={tab === t ? { background: "#16191D", color: "#fff" } : { background: "#EEF0F2", color: "#5B6470" }}>
                {t === "login" ? "Log in" : "Sign up"}
              </button>
            ))}
          </div>
        )}

        {tab === "reset" ? (
          <div className="p-6 space-y-3">
            {resetSent ? (
              <>
                <p className="text-sm px-3 py-2 rounded-lg" style={{ background: "#E7F4EC", color: "#2C7A4B" }}>
                  If an account exists for <strong>{email.trim()}</strong>, a password-reset link is on its way. Check your inbox (and spam).
                </p>
                <button onClick={() => goTab("login")}
                  className="w-full py-2.5 rounded-lg text-sm font-semibold text-white" style={{ background: "#16191D" }}>
                  Back to log in
                </button>
              </>
            ) : (
              <>
                <Field label="Email" type="email" value={email} onChange={setEmail}
                  placeholder="you@gmail.com" onEnter={requestReset} />
                {err && <p className="text-sm" style={{ color: "#E8553F" }}>{err}</p>}
                <button onClick={requestReset} disabled={busy}
                  className="w-full py-2.5 rounded-lg text-sm font-semibold text-white disabled:opacity-60"
                  style={{ background: "#16191D" }}>
                  {busy ? "Sending…" : "Send reset link"}
                </button>
                <p className="text-xs text-center" style={{ color: "#9AA1A9" }}>
                  <button className="underline" onClick={() => goTab("login")}>Back to log in</button>
                </p>
              </>
            )}
          </div>
        ) : (
          <div className="p-6 space-y-3">
            {!isAdmin && tab === "signup" && (
              <Field label="Full name" value={name} onChange={setName} placeholder="e.g. Kunal Shetty" />
            )}
            <Field label="Email" type="email" value={email} onChange={setEmail}
              placeholder={isAdmin ? "admin email" : "you@gmail.com"} onEnter={submit} />
            <Field label="Password" type="password" value={password} onChange={setPassword}
              placeholder={tab === "signup" ? "At least 6 characters" : "Your password"} onEnter={submit} />

            {(tab === "login" || isAdmin) && (
              <p className="text-xs text-right -mt-1">
                <button className="underline" style={{ color: "#5B6470" }} onClick={() => goTab("reset")}>Forgot password?</button>
              </p>
            )}

            {wrongPortal && (
              <p className="text-xs px-3 py-2 rounded-lg" style={{ background: "#FCF1DC", color: "#B47B14" }}>
                You're signed in as an employee. This is the admin portal — sign in with an administrator account, or use the employee board.
              </p>
            )}
            {err && <p className="text-sm" style={{ color: "#E8553F" }}>{err}</p>}
            {offerReset && (
              <p className="text-sm">
                <button className="underline" style={{ color: "#16191D" }} onClick={() => goTab(tab === "signup" ? "login" : "reset")}>
                  {tab === "signup" ? "Log in instead" : "Reset your password →"}
                </button>
              </p>
            )}

            <button onClick={submit} disabled={busy}
              className="w-full py-2.5 rounded-lg text-sm font-semibold text-white disabled:opacity-60"
              style={{ background: "#16191D" }}>
              {busy ? "Please wait…" : isAdmin ? "Sign in" : tab === "login" ? "Log in" : "Create account & enter"}
            </button>

            {!isAdmin && (
              <p className="text-xs text-center" style={{ color: "#9AA1A9" }}>
                {tab === "login"
                  ? <>New here? <button className="underline" onClick={() => goTab("signup")}>Create an account</button></>
                  : <>Already have one? <button className="underline" onClick={() => goTab("login")}>Log in</button></>}
              </p>
            )}
          </div>
        )}
      </div>

      {isAdmin && TaskDB.employeeUrl && (
        <p className="text-xs text-center mt-4" style={{ color: "#9AA1A9" }}>
          Looking for the team board? <a className="underline" href={TaskDB.employeeUrl}>Employee portal →</a>
        </p>
      )}
    </div>
  );
}

function Field({ label, value, onChange, type = "text", placeholder, onEnter }) {
  return (
    <label className="block">
      <span className="mono text-[11px] uppercase tracking-wide" style={{ color: "#5B6470" }}>{label}</span>
      <input type={type} value={value} onChange={(e) => onChange(e.target.value)}
        onKeyDown={(e) => e.key === "Enter" && onEnter && onEnter()}
        placeholder={placeholder}
        className="mt-1 w-full px-3 py-2 rounded-lg border text-sm bg-white" style={{ borderColor: "#DDE0DC" }} />
    </label>
  );
}

/* ═══════════ public request form (no login) ═══════════
   Outside collaborators identify themselves once, then file tasks against a
   member and a date. They never see the board — only the names they can tag. */

const WHO_KEY = "taskdb_intake_who";
const SENT_KEY = "taskdb_intake_sent";
const readStore = (key, fallback) => {
  try { return JSON.parse(localStorage.getItem(key)) ?? fallback; } catch { return fallback; }
};
const writeStore = (key, val) => {
  try { localStorage.setItem(key, JSON.stringify(val)); } catch { /* private mode — fine, just don't remember */ }
};

function IntakeScreen() {
  const [who, setWho] = useState(() => readStore(WHO_KEY, null));
  const [members, setMembers] = useState(null); // null = loading
  const [admins, setAdmins] = useState([]);
  const [closed, setClosed] = useState(false);
  const [sent, setSent] = useState(() => readStore(SENT_KEY, []));

  useEffect(() => {
    (async () => {
      const r = await api("intake/members");
      if (r.status === 403) { setClosed(true); setMembers([]); return; }
      setMembers((r.ok && r.data && r.data.members) || []);
      setAdmins((r.ok && r.data && r.data.admins) || []);
    })();
  }, []);

  const saveWho = (w) => { setWho(w); writeStore(WHO_KEY, w); };
  const remember = (entry) => {
    const next = [entry, ...sent].slice(0, 20);
    setSent(next);
    writeStore(SENT_KEY, next);
  };

  return (
    <div className="max-w-2xl mx-auto px-4 pt-10 pb-20">
      <div className="flex items-center justify-center gap-2 mb-2">
        <div className="flex gap-1">
          {PHASES.map((p) => <span key={p.id} className="w-3 h-3 rounded-full" style={{ background: p.color }} />)}
        </div>
        <h1 className="disp text-2xl font-bold tracking-tight">DayBoard</h1>
      </div>
      <p className="text-center text-sm mb-6" style={{ color: "#5B6470" }}>
        Send a task to the {TaskDB.siteName || "DayBoard"} team.
      </p>

      {closed ? (
        <div className="rounded-2xl border bg-white p-8 text-center" style={{ borderColor: "#DDE0DC" }}>
          <p className="disp text-lg font-bold">This form is closed</p>
          <p className="text-sm mt-1" style={{ color: "#5B6470" }}>
            The team isn't accepting requests through this link right now.
          </p>
        </div>
      ) : !who ? (
        <IdentityForm onDone={saveWho} />
      ) : (
        <IntakeForm who={who} members={members} admins={admins} sent={sent} onSent={remember}
          onForget={() => saveWho(null)} />
      )}
    </div>
  );
}

function IdentityForm({ onDone }) {
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const [err, setErr] = useState("");

  const go = () => {
    if (!name.trim()) { setErr("Please enter your name."); return; }
    if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email.trim())) { setErr("Please enter a valid email address."); return; }
    onDone({ name: name.trim(), email: email.trim() });
  };

  return (
    <div className="rounded-2xl border bg-white overflow-hidden" style={{ borderColor: "#DDE0DC" }}>
      <div className="px-6 pt-6">
        <h2 className="disp text-lg font-bold">First, who are you?</h2>
        <p className="text-sm mt-1" style={{ color: "#5B6470" }}>
          We'll remember this on your device so you only fill it in once. No password, no account.
        </p>
      </div>
      <div className="p-6 space-y-3">
        <Field label="Your name" value={name} onChange={setName} placeholder="e.g. Priya Nair" onEnter={go} />
        <Field label="Your email" type="email" value={email} onChange={setEmail} placeholder="you@company.com" onEnter={go} />
        {err && <p className="text-sm" style={{ color: "#E8553F" }}>{err}</p>}
        <button onClick={go} className="w-full py-2.5 rounded-lg text-sm font-semibold text-white" style={{ background: "#16191D" }}>
          Continue
        </button>
      </div>
    </div>
  );
}

function IntakeForm({ who, members, admins, sent, onSent, onForget }) {
  const [title, setTitle] = useState("");
  const [note, setNote] = useState("");
  const [user, setUser] = useState("");
  const [prio, setPrio] = useState("medium");
  const [date, setDate] = useState(todayISO());
  const [pickerOpen, setPickerOpen] = useState(false);
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState("");
  const [okMsg, setOkMsg] = useState("");
  const pickRef = useRef(null);
  useDismiss(pickRef, () => { if (pickerOpen) setPickerOpen(false); });

  const anyone = (members || []).length + (admins || []).length > 0;
  const toAdmin = (admins || []).some((a) => String(a.id) === String(user));

  const submit = async () => {
    setErr(""); setOkMsg("");
    if (!title.trim()) { setErr("Describe the task."); return; }
    if (!user) { setErr("Choose who it's for."); return; }
    setBusy(true);
    const r = await api("intake", {
      method: "POST",
      body: JSON.stringify({
        name: who.name, email: who.email,
        title: title.trim(), note: note.trim(),
        user: Number(user), date, priority: prio,
      }),
    });
    setBusy(false);
    if (!r.ok) { setErr(apiError(r, "Couldn't send that. Try again.")); return; }
    const to = [...(members || []), ...(admins || [])].find((m) => String(m.id) === String(user));
    const toName = r.data?.assignedTo || (to && to.name) || "the team";
    onSent({ title: title.trim(), to: to ? to.name : "", date, ts: Date.now() });
    setOkMsg(toAdmin
      ? `Sent to ${toName} — it's on their admin task list for ${prettyDate(date)}.`
      : `Sent to ${toName} for ${prettyDate(date)}.`);
    setTitle(""); setNote(""); setPrio("medium");
  };

  return (
    <>
      {/* no overflow-hidden here — it would clip the calendar popover */}
      <div className="rounded-2xl border bg-white" style={{ borderColor: "#DDE0DC" }}>
        <div className="px-6 pt-6 flex items-start gap-3">
          <div className="min-w-0 flex-1">
            <h2 className="disp text-lg font-bold">Add a task</h2>
            <p className="text-sm mt-1" style={{ color: "#5B6470" }}>
              Filing as <strong>{who.name}</strong> · {who.email}{" "}
              <button onClick={onForget} className="underline ml-1" style={{ color: "#5B6470" }}>not you?</button>
            </p>
          </div>
        </div>

        <div className="p-6 space-y-3">
          <label className="block">
            <span className="mono text-[11px] uppercase tracking-wide" style={{ color: "#5B6470" }}>Task</span>
            <input value={title} onChange={(e) => setTitle(e.target.value)}
              onKeyDown={(e) => e.key === "Enter" && submit()}
              placeholder="What needs doing?"
              className="mt-1 w-full px-3 py-2 rounded-lg border text-sm bg-white" style={{ borderColor: "#DDE0DC" }} />
          </label>

          <div className="grid sm:grid-cols-2 gap-3">
            <label className="block">
              <span className="mono text-[11px] uppercase tracking-wide" style={{ color: "#5B6470" }}>Assign to</span>
              <select value={user} onChange={(e) => setUser(e.target.value)}
                className="mt-1 w-full px-3 py-2 rounded-lg border text-sm bg-white" style={{ borderColor: "#DDE0DC" }}>
                <option value="">
                  {members === null ? "Loading…" : anyone ? "Choose a person…" : "Nobody available yet"}
                </option>
                {(members || []).length > 0 && (
                  <optgroup label="Team">
                    {members.map((m) => <option key={m.id} value={m.id}>{m.name}</option>)}
                  </optgroup>
                )}
                {(admins || []).length > 0 && (
                  <optgroup label="Admins">
                    {admins.map((a) => <option key={a.id} value={a.id}>{a.name}</option>)}
                  </optgroup>
                )}
              </select>
            </label>

            <div className="block relative" ref={pickRef}>
              <span className="mono text-[11px] uppercase tracking-wide" style={{ color: "#5B6470" }}>Date</span>
              <button onClick={() => setPickerOpen((o) => !o)}
                className="tdb-plain mt-1 w-full px-3 py-2 rounded-lg border text-sm text-left flex items-center gap-2"
                style={{ borderColor: "#DDE0DC", color: "#16191D" }}>
                <span aria-hidden="true">🗓</span>
                <span className="flex-1 min-w-0 truncate">{prettyDate(date)}</span>
                {date === todayISO() && <span className="text-xs" style={{ color: "#3DA164" }}>● today</span>}
              </button>
              {pickerOpen && (
                <DatePicker mode="daily" value={date} onPick={setDate} onClose={() => setPickerOpen(false)} />
              )}
            </div>
          </div>

          <div className="grid sm:grid-cols-2 gap-3">
            <label className="block">
              <span className="mono text-[11px] uppercase tracking-wide" style={{ color: "#5B6470" }}>Priority</span>
              <select value={prio} onChange={(e) => setPrio(e.target.value)}
                className="mt-1 w-full px-3 py-2 rounded-lg border text-sm bg-white" style={{ borderColor: "#DDE0DC" }}>
                {PRIORITIES.map((p) => <option key={p.id} value={p.id}>{p.label}</option>)}
              </select>
            </label>
            <label className="block">
              <span className="mono text-[11px] uppercase tracking-wide" style={{ color: "#5B6470" }}>Note (optional)</span>
              <input value={note} onChange={(e) => setNote(e.target.value)}
                placeholder="Context, link, deadline detail…"
                className="mt-1 w-full px-3 py-2 rounded-lg border text-sm bg-white" style={{ borderColor: "#DDE0DC" }} />
            </label>
          </div>

          {err && <p className="text-sm" style={{ color: "#E8553F" }}>{err}</p>}
          {okMsg && (
            <p className="text-sm px-3 py-2 rounded-lg fade-in" style={{ background: "#E7F4EC", color: "#2C7A4B" }}>
              ✓ {okMsg}
            </p>
          )}

          <button onClick={submit} disabled={busy || !anyone}
            className="w-full py-2.5 rounded-lg text-sm font-semibold text-white disabled:opacity-60"
            style={{ background: "#16191D" }}>
            {busy ? "Sending…" : "Send task"}
          </button>
        </div>
      </div>

      {sent.length > 0 && (
        <div className="mt-5 rounded-2xl border bg-white overflow-hidden" style={{ borderColor: "#DDE0DC" }}>
          <div className="px-4 py-3 border-b" style={{ borderColor: "#EEF0EE" }}>
            <h3 className="disp font-bold text-sm">Sent from this device</h3>
          </div>
          <ul className="px-2 py-1 max-h-72 overflow-auto side-scroll">
            {sent.map((s, i) => (
              <li key={i} className="px-2 py-2 flex items-center gap-2">
                <span className="shrink-0" style={{ color: "#3DA164" }}>✓</span>
                <span className="text-sm flex-1 min-w-0 truncate">{s.title}</span>
                <span className="mono text-[10px] shrink-0" style={{ color: "#9AA1A9" }}>
                  {s.to} · {dShort(s.date)}
                </span>
              </li>
            ))}
          </ul>
        </div>
      )}

      <p className="mt-6 text-center mono text-xs" style={{ color: "#9AA1A9" }}>
        Tasks land on that person's board for the date you picked, tagged with your name.
        <br />Tag an admin instead and it goes to their admin task list.
      </p>
    </>
  );
}

/* ═══════════ calendar picker + period navigation ═══════════
   mode 'daily'   → value is a YYYY-MM-DD, picks that day
   mode 'weekly'  → value is the Monday, picking any day snaps to its week
   mode 'monthly' → value is YYYY-MM, picks from a 12-month grid           */

function DatePicker({ mode, value, onPick, onClose }) {
  const anchor = mode === "monthly" ? value + "-01" : value;
  const [cursor, setCursor] = useState(anchor.slice(0, 7)); // visible YYYY-MM
  const [year, setYear] = useState(Number(anchor.slice(0, 4)));
  const boxRef = useRef(null);
  const [up, setUp] = useState(false);

  // Open upward when there isn't room below, so the calendar is never cut off.
  useEffect(() => {
    const el = boxRef.current;
    if (!el) return;
    const r = el.getBoundingClientRect();
    if (r.bottom > window.innerHeight - 8 && r.top > r.height) setUp(true);
  }, []);

  const today = todayISO();
  const pick = (v) => { onPick(v); onClose(); };
  const pos = `fade-in absolute right-0 z-50 ${up ? "bottom-full mb-2" : "mt-2"}`;

  /* month grid ─ used by the monthly mode */
  if (mode === "monthly") {
    return (
      <div ref={boxRef} className={`${pos} w-64 rounded-xl border bg-white shadow-xl p-3`}
        style={{ borderColor: "#DDE0DC" }}>
        <div className="flex items-center justify-between mb-2">
          <button onClick={() => setYear(year - 1)} aria-label="Previous year" className="tdb-ghost px-2 py-1 rounded">‹</button>
          <span className="disp font-bold text-sm">{year}</span>
          <button onClick={() => setYear(year + 1)} aria-label="Next year" className="tdb-ghost px-2 py-1 rounded">›</button>
        </div>
        <div className="grid grid-cols-3 gap-1">
          {MONTHS_SHORT.map((label, i) => {
            const key = `${year}-${String(i + 1).padStart(2, "0")}`;
            const on = key === value;
            const isNow = key === thisMonthKey();
            return (
              <button key={key} onClick={() => pick(key)}
                className="py-2 rounded-lg text-xs font-medium transition-colors"
                style={on
                  ? { background: "#16191D", color: "#fff" }
                  : { background: isNow ? "#E7F4EC" : "transparent", color: isNow ? "#2C7A4B" : "#16191D" }}>
                {label}
              </button>
            );
          })}
        </div>
        <button onClick={() => pick(thisMonthKey())}
          className="tdb-plain mt-2 w-full py-1.5 rounded-lg text-xs font-medium border" style={{ borderColor: "#DDE0DC", color: "#5B6470" }}>
          This month
        </button>
      </div>
    );
  }

  const cells = monthGrid(cursor);
  const weekEnd = mode === "weekly" ? shiftDate(value, 6) : null;
  const inSelectedWeek = (iso) => mode === "weekly" && iso >= value && iso <= weekEnd;

  return (
    <div ref={boxRef} className={`${pos} w-[17rem] rounded-xl border bg-white shadow-xl p-3`}
      style={{ borderColor: "#DDE0DC" }}>
      <div className="flex items-center justify-between mb-2">
        <button onClick={() => setCursor(shiftMonth(cursor, -1))} aria-label="Previous month"
          className="tdb-ghost px-2 py-1 rounded">‹</button>
        <span className="disp font-bold text-sm">{monthLabel(cursor)}</span>
        <button onClick={() => setCursor(shiftMonth(cursor, 1))} aria-label="Next month"
          className="tdb-ghost px-2 py-1 rounded">›</button>
      </div>

      <div className="grid grid-cols-7 gap-0.5 mb-1">
        {DOW_SHORT.map((d, i) => (
          <div key={i} className="mono text-[10px] text-center py-1" style={{ color: "#9AA1A9" }}>{d}</div>
        ))}
      </div>

      <div className="grid grid-cols-7 gap-0.5">
        {cells.map((c) => {
          const selected = mode === "daily" ? c.iso === value : inSelectedWeek(c.iso);
          const isToday = c.iso === today;
          return (
            <button key={c.iso} onClick={() => pick(mode === "weekly" ? mondayOf(c.iso) : c.iso)}
              title={prettyDate(c.iso)}
              className="h-8 rounded-lg text-xs font-medium transition-colors hover:bg-black/5"
              style={{
                background: selected ? "#16191D" : "transparent",
                color: selected ? "#fff" : c.inMonth ? "#16191D" : "#C4C9C4",
                boxShadow: isToday && !selected ? "inset 0 0 0 1.5px #3DA164" : "none",
              }}>
              {c.day}
            </button>
          );
        })}
      </div>

      <button onClick={() => pick(mode === "weekly" ? mondayOf(today) : today)}
        className="mt-2 w-full py-1.5 rounded-lg text-xs font-medium border" style={{ borderColor: "#DDE0DC", color: "#5B6470" }}>
        {mode === "weekly" ? "This week" : "Today"}
      </button>
    </div>
  );
}

/* ‹ label › where the label opens the calendar. The arrows still step one
   period at a time — the calendar is an addition, not a replacement. */
function PeriodNav({ mode, value, label, isCurrent, currentWord, onPrev, onNext, onPick, compact }) {
  const [open, setOpen] = useState(false);
  const ref = useRef(null);
  // The wrapper (not just the popover) owns dismissal, so a second click on the
  // label closes it instead of mousedown-closing then click-reopening.
  useDismiss(ref, () => { if (open) setOpen(false); });
  return (
    <div ref={ref} className="flex items-center gap-1 relative">
      <button onClick={onPrev} aria-label="Previous" className="tdb-ghost px-2 py-1 rounded">‹</button>
      <button onClick={() => setOpen((o) => !o)} aria-expanded={open} aria-haspopup="dialog"
        title="Open calendar"
        className={`tdb-ghost mono ${compact ? "text-xs" : "text-sm"} px-3 py-1 rounded flex items-center gap-1.5`}>
        <span aria-hidden="true">🗓</span>
        {label}
        {isCurrent && <span className="text-xs" style={{ color: "#3DA164" }}>● {currentWord}</span>}
      </button>
      <button onClick={onNext} aria-label="Next" className="tdb-ghost px-2 py-1 rounded">›</button>
      {open && <DatePicker mode={mode} value={value} onPick={onPick} onClose={() => setOpen(false)} />}
    </div>
  );
}

/* ═══════════════════════════════════════════════════════ */

function Board({ me, isAdmin, onLogout, onUpdateUser }) {
  const [date, setDate] = useState(todayISO());
  const [board, setBoard] = useState(emptyBoard());
  const [loading, setLoading] = useState(true);
  const [saveState, setSaveState] = useState("idle"); // idle | saving | saved | error
  const [manageOpen, setManageOpen] = useState(false);
  const [filterPrio, setFilterPrio] = useState("all");
  const [view, setView] = useState("daily"); // daily | weekly | monthly | profile
  const [weekStart, setWeekStart] = useState(mondayOf(todayISO()));
  const [monthKey, setMonthKey] = useState(thisMonthKey());
  const [assigned, setAssigned] = useState({ weekly: {}, monthly: {} });
  const scrolled = useScrolled();
  const boardRef = useRef(board);
  boardRef.current = board;
  const dateRef = useRef(date);
  dateRef.current = date;
  const savingRef = useRef(false);

  const isToday = date === todayISO();
  const activePhase = currentPhase();
  const canEdit = (memberId) => isAdmin || String(memberId) === String(me.id);

  /* ── load board for the day (silent = no spinner, used by auto-refresh) ── */
  const load = useCallback(async (d, opts = {}) => {
    const { silent = false } = opts;
    if (!silent) setLoading(true);
    const [b, w, m] = await Promise.all([
      api("board?date=" + encodeURIComponent(d)),
      api(`period?type=weekly&period=${mondayOf(d)}`),
      api(`period?type=monthly&period=${d.slice(0, 7)}`),
    ]);
    const newBoard = b.ok && b.data ? { members: b.data.members || [], tasks: b.data.tasks || {}, checkins: b.data.checkins || {} } : emptyBoard();
    boardRef.current = newBoard;
    setBoard(newBoard);
    setAssigned({
      weekly: (w.ok && w.data && w.data.tasks) ? w.data.tasks : {},
      monthly: (m.ok && m.data && m.data.tasks) ? m.data.tasks : {},
    });
    if (!silent) setLoading(false);
  }, []);
  useEffect(() => { load(date); }, [date, load]);

  // Auto-refresh: poll quietly every 20s (skip while a save is in flight).
  usePoll(() => { if (!savingRef.current) load(dateRef.current, { silent: true }); }, 20000);

  /* ── persist one member's slice (server enforces ownership) ── */
  const saveSlice = async (memberId, next) => {
    boardRef.current = next; // keep the ref current so rapid edits don't clobber each other
    savingRef.current = true;
    setBoard(next);
    setSaveState("saving");
    const r = await api("slice", {
      method: "POST",
      body: JSON.stringify({
        date, user: memberId,
        tasks: next.tasks[memberId] || [],
        checkins: next.checkins[memberId] || {},
      }),
    });
    savingRef.current = false;
    setSaveState(r.ok ? "saved" : "error");
    if (r.ok) setTimeout(() => setSaveState("idle"), 1500);
  };

  /* ── mutations (all scoped to a member) ── */
  const addTask = (memberId, title, priority, source = null) => {
    if (!canEdit(memberId) || !title.trim()) return;
    const next = structuredClone(boardRef.current);
    if (!next.tasks[memberId]) next.tasks[memberId] = [];
    next.tasks[memberId].push({
      id: uid(), title: title.trim(), priority,
      done: false, donePhase: null, createdAt: Date.now(),
      ...(source ? { source } : {}),
    });
    saveSlice(memberId, next);
  };
  const toggleTask = (memberId, taskId) => {
    if (!canEdit(memberId)) return;
    const next = structuredClone(boardRef.current);
    const t = (next.tasks[memberId] || []).find((x) => x.id === taskId);
    if (!t) return;
    t.done = !t.done;
    t.donePhase = t.done ? activePhase : null;
    if (t.done) t.inProgress = false; // done supersedes in-progress
    saveSlice(memberId, next);
  };
  const toggleInProgress = (memberId, taskId) => {
    if (!canEdit(memberId)) return;
    const next = structuredClone(boardRef.current);
    const t = (next.tasks[memberId] || []).find((x) => x.id === taskId);
    if (!t) return;
    t.inProgress = !t.inProgress;
    if (t.inProgress) { t.done = false; t.donePhase = null; } // can't be in-progress and done
    saveSlice(memberId, next);
  };
  const deleteTask = (memberId, taskId) => {
    if (!canEdit(memberId)) return;
    const list = boardRef.current.tasks[memberId] || [];
    const t = list.find((x) => x.id === taskId);
    if (!t || (t.admin && !isAdmin)) return; // admin-assigned tasks can't be removed
    const next = structuredClone(boardRef.current);
    next.tasks[memberId] = (next.tasks[memberId] || []).filter((x) => x.id !== taskId);
    saveSlice(memberId, next);
  };
  const setTaskPriority = (memberId, taskId, priority) => {
    if (!canEdit(memberId)) return;
    const next = structuredClone(boardRef.current);
    const t = (next.tasks[memberId] || []).find((x) => x.id === taskId);
    if (!t) return;
    t.priority = priority;
    saveSlice(memberId, next);
  };
  /* Rename a task. Admin-assigned tasks are read-only for the employee — the
     server enforces the same rule, this just avoids a pointless round trip. */
  const setTaskTitle = (memberId, taskId, title) => {
    if (!canEdit(memberId) || !title.trim()) return;
    const next = structuredClone(boardRef.current);
    const t = (next.tasks[memberId] || []).find((x) => x.id === taskId);
    if (!t || (t.admin && !isAdmin)) return;
    t.title = title.trim();
    saveSlice(memberId, next);
  };
  const setTaskNote = (memberId, taskId, note) => {
    if (!canEdit(memberId)) return;
    const next = structuredClone(boardRef.current);
    const t = (next.tasks[memberId] || []).find((x) => x.id === taskId);
    if (!t) return;
    t.note = note;
    saveSlice(memberId, next);
  };
  const toggleCheckin = (memberId, phaseId) => {
    if (!canEdit(memberId)) return;
    const next = structuredClone(boardRef.current);
    const cur = next.checkins[memberId];
    next.checkins[memberId] = (cur && !Array.isArray(cur)) ? cur : {}; // guard: server may send [] for empty
    next.checkins[memberId][phaseId] = !next.checkins[memberId][phaseId];
    saveSlice(memberId, next);
  };
  const carryOver = async (memberId) => {
    if (!canEdit(memberId)) return;
    const r = await api("board?date=" + encodeURIComponent(shiftDate(date, -1)));
    const prevTasks = (r.data && r.data.tasks && r.data.tasks[memberId]) || [];
    const pending = prevTasks.filter((t) => !t.done);
    if (!pending.length) return;
    const next = structuredClone(boardRef.current);
    if (!next.tasks[memberId]) next.tasks[memberId] = [];
    const existing = new Set(next.tasks[memberId].map((t) => t.title));
    pending.forEach((t) => {
      if (!existing.has(t.title)) {
        next.tasks[memberId].push({ ...t, id: uid(), done: false, donePhase: null, carried: true });
      }
    });
    saveSlice(memberId, next);
  };
  const removeMember = async (id, name) => {
    if (!isAdmin) return;
    if (!window.confirm(`Remove ${name}? Their account and tasks will be deleted.`)) return;
    const r = await api("member", { method: "DELETE", body: JSON.stringify({ user: id }) });
    if (r.ok) load(date);
  };

  /* ── derived stats ── */
  const { members } = board;
  const allTasks = members.flatMap((m) => board.tasks[m.id] || []);
  const doneCount = allTasks.filter((t) => t.done).length;
  const pct = allTasks.length ? Math.round((doneCount / allTasks.length) * 100) : 0;
  const highPending = allTasks.filter((t) => !t.done && t.priority === "high").length;
  const notCheckedIn = members.filter((m) => !board.checkins[m.id]?.[activePhase]);

  /* ── period navigation (adapts to the active view) ── */
  let navMode, navValue, periodLabel, isCurrentPeriod, currentWord, prevPeriod, nextPeriod, pickPeriod;
  if (view === "daily") {
    navMode = "daily"; navValue = date;
    periodLabel = prettyDate(date); isCurrentPeriod = isToday; currentWord = "today";
    prevPeriod = () => setDate(shiftDate(date, -1));
    nextPeriod = () => setDate(shiftDate(date, 1));
    pickPeriod = (v) => setDate(v);
  } else if (view === "weekly") {
    navMode = "weekly"; navValue = weekStart;
    periodLabel = "Week of " + weekLabel(weekStart); isCurrentPeriod = weekStart === mondayOf(todayISO()); currentWord = "this week";
    prevPeriod = () => setWeekStart(shiftDate(weekStart, -7));
    nextPeriod = () => setWeekStart(shiftDate(weekStart, 7));
    pickPeriod = (v) => setWeekStart(v);
  } else {
    navMode = "monthly"; navValue = monthKey;
    periodLabel = monthLabel(monthKey); isCurrentPeriod = monthKey === thisMonthKey(); currentWord = "this month";
    prevPeriod = () => setMonthKey(shiftMonth(monthKey, -1));
    nextPeriod = () => setMonthKey(shiftMonth(monthKey, 1));
    pickPeriod = (v) => setMonthKey(v);
  }

  const TABS = [["daily", "Daily"], ["weekly", "Weekly"], ["monthly", "Monthly"],
    ...(isAdmin ? [["report", "Team report"]] : [["profile", "My Profile"]])];

  /* ─────────────────────────────────────── */
  return (
    <Shell>
      {/* header + control bar travel together and stay pinned to the top */}
      <div className="sticky top-0 z-40 transition-shadow"
        style={{ boxShadow: scrolled ? "0 6px 20px rgba(0,0,0,.13)" : "none" }}>
      <header className="border-b" style={{ background: "#16191D", borderColor: "#16191D" }}>
        <div className="max-w-7xl mx-auto px-4 py-3 flex flex-wrap items-center gap-4">
          <div className="flex items-center gap-3">
            <div className="flex gap-1">
              {PHASES.map((p) => <span key={p.id} className="w-3 h-3 rounded-full" style={{ background: p.color }} />)}
            </div>
            <h1 className="disp text-xl font-bold text-white tracking-tight">DayBoard</h1>
            {isAdmin && <span className="mono text-[10px] px-2 py-0.5 rounded-full" style={{ background: "#E8553F", color: "#fff" }}>ADMIN</span>}
          </div>

          <div className="flex items-center gap-3 ml-auto">
            <span className="mono text-xs" style={{ color: saveState === "error" ? "#E8553F" : "#8B939E" }}>
              {saveState === "saving" ? "saving…" : saveState === "saved" ? "saved ✓" : saveState === "error" ? "save failed" : ""}
            </span>
            <button onClick={() => load(date)} className="text-xs px-3 py-1.5 rounded border border-white/20 text-white hover:bg-white/10">
              Refresh
            </button>
            {isAdmin && (
              <button onClick={() => setManageOpen(!manageOpen)}
                className="text-xs px-3 py-1.5 rounded font-medium" style={{ background: "#FFFFFF", color: "#16191D" }}>
                Manage team
              </button>
            )}
            <NotificationBell />
            <div className="flex items-center gap-2 pl-2 border-l border-white/15">
              <span className="text-xs text-white/80 hidden sm:block">{me.name || me.email}</span>
              <button onClick={onLogout} className="text-xs px-3 py-1.5 rounded border border-white/20 text-white hover:bg-white/10">
                Log out
              </button>
            </div>
          </div>
        </div>
      </header>

      {/* view tabs + period navigation — pinned under the header */}
      <div className="border-b" style={{ background: "rgba(242,243,241,.92)", backdropFilter: "blur(8px)", borderColor: "#DFE2DE" }}>
        <div className="max-w-7xl mx-auto px-4 py-2 flex flex-wrap items-center gap-3">
          <div className="flex gap-1 p-1 rounded-xl" style={{ background: "#E6E8E5" }}>
            {TABS.map(([v, label]) => (
              <button key={v} onClick={() => setView(v)}
                className="px-3 py-1.5 rounded-lg text-sm font-medium transition-colors"
                style={view === v ? { background: "#16191D", color: "#fff" } : { background: "transparent", color: "#5B6470" }}>
                {label}
              </button>
            ))}
          </div>
          {view !== "profile" && view !== "report" && (
            <div className="ml-auto">
              <PeriodNav mode={navMode} value={navValue} label={periodLabel}
                isCurrent={isCurrentPeriod} currentWord={currentWord}
                onPrev={prevPeriod} onNext={nextPeriod} onPick={pickPeriod} />
            </div>
          )}
        </div>
      </div>
      </div>

      <main className="max-w-7xl mx-auto px-4 pb-16">
        <div className="lg:flex lg:gap-6 lg:items-start">
        <div className="lg:flex-1 min-w-0">
        {/* manage team (admin only) */}
        {isAdmin && manageOpen && (
          <section className="fade-in mt-4 p-4 rounded-xl border bg-white" style={{ borderColor: "#DDE0DC" }}>
            <h2 className="disp font-bold mb-1">Team members</h2>
            <p className="text-xs mb-3" style={{ color: "#5B6470" }}>
              Employees join by signing up at the employee portal. Remove an account here.
            </p>
            <div className="flex flex-wrap gap-2">
              {members.map((m) => (
                <span key={m.id} className="flex items-center gap-2 pl-3 pr-1 py-1 rounded-full text-sm border" style={{ borderColor: "#DDE0DC" }}>
                  <span>{m.name} <span className="mono text-[11px]" style={{ color: "#9AA1A9" }}>{m.email}</span></span>
                  <button onClick={() => removeMember(m.id, m.name)} aria-label={`Remove ${m.name}`}
                    className="w-5 h-5 rounded-full text-xs hover:bg-red-50" style={{ color: "#E8553F" }}>✕</button>
                </span>
              ))}
              {!members.length && <span className="text-sm" style={{ color: "#5B6470" }}>No employees yet — share the employee portal link so they can sign up.</span>}
            </div>
            {TaskDB.employeeUrl && (
              <p className="mono text-[11px] mt-3" style={{ color: "#9AA1A9" }}>
                Employee portal: <a className="underline" href={TaskDB.employeeUrl}>{TaskDB.employeeUrl}</a>
              </p>
            )}
          </section>
        )}

        {view === "profile" && <ProfileView me={me} onRename={onUpdateUser} />}
        {view === "report" && <TeamReport />}

        {(view === "weekly" || view === "monthly") && (
          <PeriodView type={view} period={view === "weekly" ? weekStart : monthKey}
            me={me} isAdmin={isAdmin} setSaveState={setSaveState} />
        )}

        {view === "daily" && <>
        {/* overview — pinned under the control bar (md+; on phones the wrapped
            header makes a fixed offset unreliable, so it just scrolls) */}
        <div className="md:sticky z-30 pt-4 pb-3" style={{ top: STICKY_TOP, background: "#F2F3F1" }}>
          <section className="grid grid-cols-2 md:grid-cols-4 gap-3">
            <Stat label="Tasks today" value={allTasks.length} />
            <Stat label="Completed" value={`${doneCount} · ${pct}%`} accent="#3DA164" />
            <Stat label="High priority pending" value={highPending} accent={highPending ? "#E8553F" : undefined} />
            <Stat label={`Not checked in (${PHASES.find((p) => p.id === activePhase)?.label})`}
              value={isToday ? (notCheckedIn.length ? notCheckedIn.map((m) => m.name.split(" ")[0]).join(", ") : "Everyone in ✓") : "—"}
              small accent={isToday && notCheckedIn.length ? "#B47B14" : undefined} />
          </section>

          <div className="mt-3 h-2 rounded-full overflow-hidden" style={{ background: "#E2E5E1" }}>
            <div className="h-full rounded-full transition-all" style={{ width: `${pct}%`, background: "#3DA164" }} />
          </div>
        </div>

        {/* filter */}
        <div className="mt-2 flex items-center gap-2 text-sm">
          <span className="mono text-xs" style={{ color: "#5B6470" }}>FILTER</span>
          {["all", "high", "medium", "low"].map((p) => (
            <button key={p} onClick={() => setFilterPrio(p)}
              className="px-3 py-1 rounded-full text-xs font-medium border capitalize"
              style={filterPrio === p
                ? { background: "#16191D", color: "#fff", borderColor: "#16191D" }
                : { background: "#fff", color: "#5B6470", borderColor: "#DDE0DC" }}>
              {p}
            </button>
          ))}
        </div>

        {/* member cards */}
        {loading ? (
          <Centered>Loading board…</Centered>
        ) : !members.length ? (
          <div className="mt-10 text-center">
            <p className="disp text-lg font-bold">No employees on the board yet</p>
            <p className="text-sm mt-1" style={{ color: "#5B6470" }}>
              {isAdmin ? "Share the employee portal link so your team can sign up." : "Once teammates sign up they'll appear here."}
            </p>
          </div>
        ) : (
          <div className="mt-5 grid md:grid-cols-2 gap-4 items-start">
            {members
              .slice()
              .sort((a, b) => (String(a.id) === String(me.id) ? -1 : String(b.id) === String(me.id) ? 1 : 0))
              .map((m) => (
                <MemberCard key={m.id} member={m} tasks={board.tasks[m.id] || []}
                  checkins={board.checkins[m.id] || {}} filterPrio={filterPrio}
                  isToday={isToday} activePhase={activePhase}
                  editable={canEdit(m.id)} isSelf={String(m.id) === String(me.id)}
                  assignedWeekly={assigned.weekly[m.id] || []} assignedMonthly={assigned.monthly[m.id] || []}
                  isAdminView={isAdmin}
                  onAdd={addTask} onToggle={toggleTask} onDelete={deleteTask}
                  onPriority={setTaskPriority} onCheckin={toggleCheckin} onCarry={carryOver} onNote={setTaskNote}
                  onProgress={toggleInProgress} onRename={setTaskTitle} />
              ))}
          </div>
        )}

        <p className="mt-10 text-center mono text-xs" style={{ color: "#9AA1A9" }}>
          Check-in rhythm: <span style={{ color: "#E8553F" }}>● morning</span> → <span style={{ color: "#F2A93B" }}>● post-lunch</span> → <span style={{ color: "#3DA164" }}>● end of day</span>.
          <br />
          <span style={{ color: ADMIN_TASK.color }}>★ Blue rows are assigned by the admin</span> — tick and annotate them, but they can't be edited or removed.
          {isAdmin ? " Admin view — you can edit everyone." : " Click any of your own task titles to edit them."}
        </p>
        </>}
        </div>
        <aside className="lg:w-80 shrink-0 mt-6 lg:mt-4 lg:sticky" style={{ top: STICKY_TOP }}>
          <SidePanels me={me} isAdmin={isAdmin} setSaveState={setSaveState} />
        </aside>
        </div>
      </main>
    </Shell>
  );
}

/* ── stat card ── */
function Stat({ label, value, accent, small }) {
  return (
    <div className="p-3 rounded-xl border bg-white" style={{ borderColor: "#DDE0DC" }}>
      <div className="mono text-[11px] uppercase tracking-wide" style={{ color: "#5B6470" }}>{label}</div>
      <div className={`disp font-bold mt-1 ${small ? "text-sm leading-snug" : "text-2xl"}`} style={{ color: accent || "#16191D" }}>
        {value}
      </div>
    </div>
  );
}

/* ── inline single-line editor: click the text, Enter or click-away saves,
      Escape cancels. Used for task titles, suggestions and requests. ── */
function InlineText({ value, editable, onSave, className = "", style, lockHint }) {
  const [editing, setEditing] = useState(false);
  const [val, setVal] = useState(value || "");
  useEffect(() => { if (!editing) setVal(value || ""); }, [value, editing]);

  const commit = () => {
    setEditing(false);
    const v = val.trim();
    if (v && v !== value) onSave(v);
  };

  if (editing) {
    return (
      <input autoFocus value={val} onChange={(e) => setVal(e.target.value)}
        onKeyDown={(e) => {
          if (e.key === "Enter") commit();
          if (e.key === "Escape") { setVal(value || ""); setEditing(false); }
        }}
        onBlur={commit}
        aria-label="Edit text"
        className="w-full flex-1 min-w-0 px-2 py-0.5 rounded border text-sm bg-white"
        style={{ borderColor: "#16191D" }} />
    );
  }
  return (
    <span onClick={() => editable && setEditing(true)}
      title={editable ? "Click to edit" : lockHint || undefined}
      className={`${className} ${editable ? "editable-text" : ""}`} style={style}>
      {value}
    </span>
  );
}

/* ── member card ── */
/* ── per-task note (e.g. "in progress", "blocked") ── */
function TaskNote({ note, editable, onSave }) {
  const [editing, setEditing] = useState(false);
  const [val, setVal] = useState(note || "");
  useEffect(() => { setVal(note || ""); }, [note]);

  const commit = () => {
    setEditing(false);
    const v = val.trim();
    if (v !== (note || "")) onSave(v);
  };

  if (!editable) {
    return note ? <div className="ml-7 mt-1 text-xs" style={{ color: "#7A5AF8" }}>📝 {note}</div> : null;
  }
  if (editing) {
    return (
      <div className="ml-7 mt-1">
        <input autoFocus value={val} onChange={(e) => setVal(e.target.value)}
          onKeyDown={(e) => { if (e.key === "Enter") commit(); if (e.key === "Escape") { setVal(note || ""); setEditing(false); } }}
          onBlur={commit}
          placeholder="e.g. in progress, blocked, waiting on…"
          className="w-full px-2 py-1 rounded border text-xs bg-white" style={{ borderColor: "#DDE0DC" }} />
      </div>
    );
  }
  return (
    <button onClick={() => setEditing(true)} className="ml-7 mt-1 block text-xs text-left"
      style={{ color: note ? "#7A5AF8" : "#9AA1A9" }}>
      {note ? <>📝 {note}</> : "＋ note"}
    </button>
  );
}

function MemberCard({ member, tasks, checkins, filterPrio, isToday, activePhase, editable, isSelf,
  assignedWeekly = [], assignedMonthly = [], isAdminView,
  onAdd, onToggle, onDelete, onPriority, onCheckin, onCarry, onNote, onProgress, onRename }) {
  const [title, setTitle] = useState("");
  const [prio, setPrio] = useState("medium");

  const visible = tasks
    .filter((t) => filterPrio === "all" || t.priority === filterPrio)
    .sort((a, b) => (a.done - b.done) || (prioRank[a.priority] - prioRank[b.priority]) || (a.createdAt - b.createdAt));

  const done = tasks.filter((t) => t.done).length;
  const fromAdmin = tasks.filter((t) => t.admin).length;
  const submit = () => { onAdd(member.id, title, prio); setTitle(""); };

  // Assigned weekly/monthly tasks the member can pull into today (not yet done,
  // not already on today's list).
  const todayTitles = new Set(tasks.map((t) => t.title));
  const assignedOptions = [
    ...assignedWeekly.filter((t) => !t.done && !todayTitles.has(t.title)).map((t) => ({ source: "weekly", title: t.title })),
    ...assignedMonthly.filter((t) => !t.done && !todayTitles.has(t.title)).map((t) => ({ source: "monthly", title: t.title })),
  ];
  const pickAssigned = (e) => {
    const i = e.target.value;
    if (i === "") return;
    const o = assignedOptions[i];
    onAdd(member.id, o.title, prio, o.source);
    e.target.value = "";
  };

  return (
    <article className="rounded-xl border bg-white overflow-hidden" style={{ borderColor: isSelf ? "#16191D" : "#DDE0DC" }}>
      <div className="p-4 flex items-center gap-3 border-b" style={{ borderColor: "#EEF0EE" }}>
        <div className="w-9 h-9 rounded-full flex items-center justify-center text-xs font-semibold text-white shrink-0"
          style={{ background: "#16191D" }}>
          {initials(member.name)}
        </div>
        <div className="min-w-0">
          <h3 className="disp font-bold leading-tight truncate">
            {member.name}{isSelf && <span className="mono text-[10px] ml-2" style={{ color: "#3DA164" }}>YOU</span>}
          </h3>
          <div className="mono text-[11px] flex items-center gap-2" style={{ color: "#5B6470" }}>
            <span>{done}/{tasks.length} done</span>
            {fromAdmin > 0 && (
              <span className="px-1.5 rounded" style={{ background: ADMIN_TASK.bg, color: ADMIN_TASK.color }}
                title={`${fromAdmin} task${fromAdmin > 1 ? "s" : ""} assigned by the admin`}>
                ★ {fromAdmin}
              </span>
            )}
          </div>
        </div>

        {/* tri-dot phase tracker */}
        <div className="ml-auto flex gap-1.5" role="group" aria-label="Daily check-ins">
          {PHASES.map((p) => {
            const on = !!checkins[p.id];
            const isCurrent = isToday && p.id === activePhase;
            return (
              <button key={p.id} onClick={() => editable && onCheckin(member.id, p.id)}
                disabled={!editable}
                title={`${p.label} check-in${editable ? (on ? " (done — click to undo)" : "") : " (read-only)"}`}
                className="w-7 h-7 rounded-full border-2 flex items-center justify-center text-[10px] font-bold transition-all"
                style={{
                  borderColor: p.color,
                  background: on ? p.color : "#fff",
                  color: on ? "#fff" : p.color,
                  cursor: editable ? "pointer" : "default",
                  opacity: editable || on ? 1 : 0.55,
                  boxShadow: isCurrent && !on && editable ? `0 0 0 3px ${p.color}33` : "none",
                }}>
                {on ? "✓" : ""}
              </button>
            );
          })}
        </div>
      </div>

      <ul className="px-2 py-1">
        {visible.map((t) => {
          const P = PRIORITIES.find((x) => x.id === t.priority) || PRIORITIES[1];
          const phase = PHASES.find((p) => p.id === t.donePhase);
          // Assigned by the admin: the employee may tick, annotate and
          // re-prioritise it, but not rename or delete it.
          const locked = !!t.admin && !isAdminView;
          return (
            <li key={t.id}
              className="task-row group px-2 py-2 rounded-lg hover:bg-black/[0.03]"
              style={t.admin ? {
                background: ADMIN_TASK.bg,
                boxShadow: `inset 3px 0 0 ${ADMIN_TASK.color}`,
                marginBottom: 2,
              } : undefined}>
              <div className="flex items-center gap-2.5">
                <button onClick={() => editable && onToggle(member.id, t.id)} disabled={!editable}
                  aria-label={t.done ? "Mark not done" : "Mark done"}
                  className="w-5 h-5 rounded border-2 flex items-center justify-center text-[11px] shrink-0 transition-all"
                  style={{
                    borderColor: t.done ? "#3DA164" : "#C4C9C4",
                    background: t.done ? "#3DA164" : "#fff",
                    color: "#fff",
                    cursor: editable ? "pointer" : "default",
                  }}>
                  {t.done ? "✓" : ""}
                </button>

                <span className="text-sm flex-1 min-w-0 flex items-center flex-wrap gap-x-2">
                  <InlineText value={t.title} editable={editable && !locked}
                    onSave={(v) => onRename(member.id, t.id, v)}
                    lockHint="Assigned by the admin — you can tick it off but not change it"
                    className={t.done ? "line-through" : ""}
                    style={{ color: t.done ? "#9AA1A9" : "#16191D" }} />
                  {t.admin && (
                    <span className="mono text-[10px] px-1.5 py-0.5 rounded shrink-0"
                      style={{ background: "#fff", color: ADMIN_TASK.color, border: `1px solid ${ADMIN_TASK.border}` }}
                      title="Assigned by the admin — cannot be removed">
                      ★ {ADMIN_TASK.label}
                    </span>
                  )}
                  {t.external && (
                    <span className="mono text-[10px] px-1.5 py-0.5 rounded shrink-0"
                      style={{ background: "#fff", color: EXTERNAL_TASK.color, border: `1px solid ${EXTERNAL_TASK.border}` }}
                      title={`Filed via the request form by ${t.byName || "someone"}${t.byEmail ? ` (${t.byEmail})` : ""}`}>
                      ↗ {t.byName || "external"}
                    </span>
                  )}
                  {t.source && <span className="mono text-[10px]" style={{ color: "#7A5AF8" }}>◆ {t.source}</span>}
                  {t.carried && <span className="mono text-[10px]" style={{ color: "#B47B14" }}>↻ carried</span>}
                </span>

                {(() => {
                  const wip = !!t.inProgress && !t.done;
                  if (!editable) {
                    return wip ? <span className="text-[10px] font-medium rounded-full px-2 py-0.5 shrink-0" style={{ background: "#FCF1DC", color: "#B47B14" }}>● In progress</span> : null;
                  }
                  if (t.done) return null;
                  return (
                    <button onClick={() => onProgress(member.id, t.id)} title="Toggle 'in progress'"
                      className="text-[10px] font-medium rounded-full px-2 py-0.5 border shrink-0 transition-colors"
                      style={wip
                        ? { background: "#FCF1DC", color: "#B47B14", borderColor: "#F2D8A0" }
                        : { background: "#fff", color: "#9AA1A9", borderColor: "#DDE0DC" }}>
                      ● In progress
                    </button>
                  );
                })()}

                {phase && (
                  <span className="w-2 h-2 rounded-full shrink-0" title={`Completed: ${phase.label}`} style={{ background: phase.color }} />
                )}

                {editable ? (
                  <select value={t.priority} onChange={(e) => onPriority(member.id, t.id, e.target.value)}
                    aria-label="Priority"
                    className="text-[11px] font-medium rounded-full px-2 py-0.5 border-0 cursor-pointer"
                    style={{ background: P.bg, color: P.color }}>
                    {PRIORITIES.map((p) => <option key={p.id} value={p.id}>{p.label}</option>)}
                  </select>
                ) : (
                  <span className="text-[11px] font-medium rounded-full px-2 py-0.5" style={{ background: P.bg, color: P.color }}>{P.label}</span>
                )}

                {editable && (locked ? (
                  <span className="w-5 h-5 text-[11px] flex items-center justify-center shrink-0"
                    title="Assigned by the admin — it can't be removed" aria-label="Locked by the admin"
                    style={{ color: ADMIN_TASK.color }}>🔒</span>
                ) : (
                  <button onClick={() => onDelete(member.id, t.id)} aria-label="Delete task"
                    className="del-btn opacity-0 transition-opacity w-5 h-5 text-xs rounded hover:bg-red-50 shrink-0"
                    style={{ color: "#E8553F" }}>✕</button>
                ))}
              </div>

              <TaskNote note={t.note} editable={editable} onSave={(v) => onNote(member.id, t.id, v)} />
            </li>
          );
        })}
        {!visible.length && (
          <li className="px-2 py-3 text-sm" style={{ color: "#9AA1A9" }}>
            {tasks.length ? "No tasks match this filter." : "No tasks yet for this day."}
          </li>
        )}
      </ul>

      {editable && (
        <div className="p-3 border-t flex flex-wrap gap-2" style={{ borderColor: "#EEF0EE", background: "#FAFBFA" }}>
          <input value={title} onChange={(e) => setTitle(e.target.value)}
            onKeyDown={(e) => e.key === "Enter" && submit()}
            placeholder="Add a task…"
            className="flex-1 min-w-0 px-3 py-1.5 rounded-lg border text-sm bg-white" style={{ borderColor: "#DDE0DC" }} />
          {assignedOptions.length > 0 && (
            <select value="" onChange={pickAssigned} aria-label="Add from assigned weekly/monthly tasks"
              className="text-xs rounded-lg border px-2 bg-white max-w-[9rem]" style={{ borderColor: "#DDE0DC", color: "#5B6470" }}>
              <option value="">＋ Assigned…</option>
              {assignedOptions.some((o) => o.source === "weekly") && (
                <optgroup label="Weekly">
                  {assignedOptions.map((o, i) => o.source === "weekly" && <option key={i} value={i}>{o.title}</option>)}
                </optgroup>
              )}
              {assignedOptions.some((o) => o.source === "monthly") && (
                <optgroup label="Monthly">
                  {assignedOptions.map((o, i) => o.source === "monthly" && <option key={i} value={i}>{o.title}</option>)}
                </optgroup>
              )}
            </select>
          )}
          <select value={prio} onChange={(e) => setPrio(e.target.value)} aria-label="New task priority"
            className="text-xs rounded-lg border px-2 bg-white" style={{ borderColor: "#DDE0DC" }}>
            {PRIORITIES.map((p) => <option key={p.id} value={p.id}>{p.label}</option>)}
          </select>
          <button onClick={submit} className="px-3 py-1.5 rounded-lg text-sm font-medium text-white" style={{ background: "#16191D" }}>
            Add
          </button>
          <button onClick={() => onCarry(member.id)} title="Carry over pending tasks from the previous day"
            className="px-2.5 py-1.5 rounded-lg text-sm border bg-white" style={{ borderColor: "#DDE0DC", color: "#5B6470" }}>
            ↻
          </button>
        </div>
      )}
    </article>
  );
}

/* ═══════════ weekly / monthly view ═══════════ */

function PeriodView({ type, period, me, isAdmin, setSaveState }) {
  const [data, setData] = useState({ members: [], tasks: {} });
  const [loading, setLoading] = useState(true);
  const busyRef = useRef(false);
  const periodRef = useRef(period);
  periodRef.current = period;

  const load = useCallback(async (p, silent) => {
    if (!silent) setLoading(true);
    const r = await api(`period?type=${type}&period=${encodeURIComponent(p)}`);
    setData(r.ok && r.data ? { members: r.data.members || [], tasks: r.data.tasks || {} } : { members: [], tasks: {} });
    if (!silent) setLoading(false);
  }, [type]);
  useEffect(() => { load(period); }, [period, load]);
  usePoll(() => { if (!busyRef.current) load(periodRef.current, true); }, 25000);

  const flash = (ok) => { setSaveState(ok ? "saved" : "error"); if (ok) setTimeout(() => setSaveState("idle"), 1500); };

  const addTask = async (user, title) => {
    if (!isAdmin || !title.trim()) return;
    busyRef.current = true;
    setSaveState("saving");
    const r = await api("period", { method: "POST", body: JSON.stringify({ action: "add", type, period, user, title: title.trim() }) });
    busyRef.current = false;
    flash(r.ok);
    if (r.ok) load(period, true);
  };
  const toggle = async (user, taskId) => {
    if (!(isAdmin || String(user) === String(me.id))) return;
    setData((prev) => {
      const n = structuredClone(prev);
      const t = (n.tasks[user] || []).find((x) => x.id === taskId);
      if (t) t.done = !t.done;
      return n;
    });
    busyRef.current = true;
    setSaveState("saving");
    const r = await api("period", { method: "POST", body: JSON.stringify({ action: "toggle", type, period, user, taskId }) });
    busyRef.current = false;
    flash(r.ok);
    if (!r.ok) load(period, true);
  };
  const rename = async (user, taskId, title) => {
    if (!isAdmin || !title.trim()) return;
    setData((prev) => {
      const n = structuredClone(prev);
      const t = (n.tasks[user] || []).find((x) => x.id === taskId);
      if (t) t.title = title.trim();
      return n;
    });
    busyRef.current = true;
    setSaveState("saving");
    const r = await api("period", { method: "POST", body: JSON.stringify({ action: "rename", type, period, user, taskId, title: title.trim() }) });
    busyRef.current = false;
    flash(r.ok);
    if (!r.ok) load(period, true);
  };
  const del = async (user, taskId) => {
    if (!isAdmin) return;
    setData((prev) => {
      const n = structuredClone(prev);
      n.tasks[user] = (n.tasks[user] || []).filter((x) => x.id !== taskId);
      return n;
    });
    busyRef.current = true;
    setSaveState("saving");
    const r = await api("period", { method: "POST", body: JSON.stringify({ action: "delete", type, period, user, taskId }) });
    busyRef.current = false;
    flash(r.ok);
    if (!r.ok) load(period, true);
  };

  const all = data.members.flatMap((m) => data.tasks[m.id] || []);
  const done = all.filter((t) => t.done).length;
  const pct = all.length ? Math.round((done / all.length) * 100) : 0;
  const word = type === "weekly" ? "Weekly" : "Monthly";

  return (
    <>
      <section className={`mt-4 grid grid-cols-2 ${isAdmin ? "md:grid-cols-4" : "md:grid-cols-3"} gap-3`}>
        <Stat label={`${word} tasks`} value={all.length} />
        <Stat label="Completed" value={`${done} · ${pct}%`} accent="#3DA164" />
        <Stat label="Outstanding" value={all.length - done} accent={all.length - done ? "#B47B14" : undefined} />
        {isAdmin && <Stat label="Members" value={data.members.length} />}
      </section>
      <div className="mt-3 h-2 rounded-full overflow-hidden" style={{ background: "#E2E5E1" }}>
        <div className="h-full rounded-full transition-all" style={{ width: `${pct}%`, background: "#3DA164" }} />
      </div>

      {loading ? (
        <Centered>Loading…</Centered>
      ) : !data.members.length ? (
        <div className="mt-10 text-center">
          <p className="disp text-lg font-bold">{isAdmin ? "No employees yet" : `No ${word.toLowerCase()} tasks`}</p>
          <p className="text-sm mt-1" style={{ color: "#5B6470" }}>
            {isAdmin ? "Members appear here once they sign up." : "Your admin hasn't assigned any yet."}
          </p>
        </div>
      ) : (
        <div className="mt-5 grid md:grid-cols-2 gap-4 items-start">
          {data.members
            .slice()
            .sort((a, b) => (String(a.id) === String(me.id) ? -1 : String(b.id) === String(me.id) ? 1 : 0))
            .map((m) => (
              <PeriodCard key={m.id} member={m} tasks={data.tasks[m.id] || []}
                isSelf={String(m.id) === String(me.id)}
                canTick={isAdmin || String(m.id) === String(me.id)} canAssign={isAdmin}
                onAdd={addTask} onToggle={toggle} onDelete={del} onRename={rename} />
            ))}
        </div>
      )}

      <p className="mt-10 text-center mono text-xs" style={{ color: "#9AA1A9" }}>
        {isAdmin
          ? `Assign each member's ${word.toLowerCase()} tasks; they tick off what they complete.`
          : `Your ${word.toLowerCase()} tasks, assigned by your admin. Pull one into a day from the Daily tab; tick it here when fully done.`}
      </p>
    </>
  );
}

function PeriodCard({ member, tasks, isSelf, canTick, canAssign, onAdd, onToggle, onDelete, onRename }) {
  const [title, setTitle] = useState("");
  const done = tasks.filter((t) => t.done).length;
  const pct = tasks.length ? Math.round((done / tasks.length) * 100) : 0;
  const sorted = tasks.slice().sort((a, b) => (a.done - b.done) || (a.createdAt - b.createdAt));
  const submit = () => { onAdd(member.id, title); setTitle(""); };

  return (
    <article className="rounded-xl border bg-white overflow-hidden" style={{ borderColor: isSelf ? "#16191D" : "#DDE0DC" }}>
      <div className="p-4 border-b" style={{ borderColor: "#EEF0EE" }}>
        <div className="flex items-center gap-3">
          <div className="w-9 h-9 rounded-full flex items-center justify-center text-xs font-semibold text-white shrink-0" style={{ background: "#16191D" }}>
            {initials(member.name)}
          </div>
          <div className="min-w-0 flex-1">
            <h3 className="disp font-bold leading-tight truncate">
              {member.name}{isSelf && <span className="mono text-[10px] ml-2" style={{ color: "#3DA164" }}>YOU</span>}
            </h3>
            <div className="mono text-[11px]" style={{ color: "#5B6470" }}>{done}/{tasks.length} done · {pct}%</div>
          </div>
        </div>
        <div className="mt-2 h-1.5 rounded-full overflow-hidden" style={{ background: "#E2E5E1" }}>
          <div className="h-full rounded-full transition-all" style={{ width: `${pct}%`, background: "#3DA164" }} />
        </div>
      </div>

      <ul className="px-2 py-1">
        {sorted.map((t) => (
          <li key={t.id} className="task-row group flex items-center gap-2.5 px-2 py-2 rounded-lg hover:bg-black/[0.03]">
            <button onClick={() => canTick && onToggle(member.id, t.id)} disabled={!canTick}
              aria-label={t.done ? "Mark not done" : "Mark done"}
              className="w-5 h-5 rounded border-2 flex items-center justify-center text-[11px] shrink-0 transition-all"
              style={{
                borderColor: t.done ? "#3DA164" : "#C4C9C4",
                background: t.done ? "#3DA164" : "#fff",
                color: "#fff",
                cursor: canTick ? "pointer" : "default",
              }}>
              {t.done ? "✓" : ""}
            </button>
            <span className="text-sm flex-1 min-w-0">
              <InlineText value={t.title} editable={canAssign}
                onSave={(v) => onRename(member.id, t.id, v)}
                lockHint="Assigned by the admin"
                className={t.done ? "line-through" : ""}
                style={{ color: t.done ? "#9AA1A9" : "#16191D" }} />
            </span>
            {canAssign && (
              <button onClick={() => onDelete(member.id, t.id)} aria-label="Delete task"
                className="del-btn opacity-0 transition-opacity w-5 h-5 text-xs rounded hover:bg-red-50 shrink-0"
                style={{ color: "#E8553F" }}>✕</button>
            )}
          </li>
        ))}
        {!tasks.length && (
          <li className="px-2 py-3 text-sm" style={{ color: "#9AA1A9" }}>
            {canAssign ? "No tasks assigned yet — add one below." : "No tasks assigned yet."}
          </li>
        )}
      </ul>

      {canAssign && (
        <div className="p-3 border-t flex gap-2" style={{ borderColor: "#EEF0EE", background: "#FAFBFA" }}>
          <input value={title} onChange={(e) => setTitle(e.target.value)}
            onKeyDown={(e) => e.key === "Enter" && submit()}
            placeholder="Assign a task…"
            className="flex-1 min-w-0 px-3 py-1.5 rounded-lg border text-sm bg-white" style={{ borderColor: "#DDE0DC" }} />
          <button onClick={submit} className="px-3 py-1.5 rounded-lg text-sm font-medium text-white" style={{ background: "#16191D" }}>
            Add
          </button>
        </div>
      )}
    </article>
  );
}

/* ═══════════ personal profile (weekly / monthly task breakdown) ═══════════ */

const dShort = (iso) => {
  const [y, m, d] = iso.split("-").map(Number);
  return new Date(y, m - 1, d).toLocaleDateString("en-IN", { weekday: "short", day: "numeric", month: "short" });
};

function ProfileView({ me, onRename }) {
  const [scope, setScope] = useState("weekly"); // weekly | monthly
  const [anchor, setAnchor] = useState(todayISO());
  const [data, setData] = useState(null);
  const [editingName, setEditingName] = useState(false);
  const [nameVal, setNameVal] = useState(me.name || "");
  const [savingName, setSavingName] = useState(false);

  const saveName = async () => {
    const v = nameVal.trim();
    if (!v || v === me.name) { setEditingName(false); return; }
    setSavingName(true);
    const r = await api("rename", { method: "POST", body: JSON.stringify({ name: v }) });
    setSavingName(false);
    setEditingName(false);
    if (r.ok && onRename) onRename({ name: r.data.name || v });
  };

  const load = useCallback(async (sc, a) => {
    setData(null);
    const r = await api(`profile?scope=${sc}&date=${encodeURIComponent(a)}`);
    setData(r.ok && r.data ? r.data : { counts: { done: 0, inProgress: 0, pending: 0 }, buckets: { done: [], inProgress: [], pending: [] } });
  }, []);
  useEffect(() => { load(scope, anchor); }, [scope, anchor, load]);

  const isCurrent = scope === "weekly" ? mondayOf(anchor) === mondayOf(todayISO()) : anchor.slice(0, 7) === thisMonthKey();
  const label = scope === "weekly" ? "Week of " + weekLabel(mondayOf(anchor)) : monthLabel(anchor.slice(0, 7));
  const prev = () => setAnchor(scope === "weekly" ? shiftDate(anchor, -7) : shiftMonth(anchor.slice(0, 7), -1) + "-01");
  const next = () => setAnchor(scope === "weekly" ? shiftDate(anchor, 7) : shiftMonth(anchor.slice(0, 7), 1) + "-01");

  const counts = data ? data.counts : { done: 0, inProgress: 0, pending: 0 };
  const total = counts.done + counts.inProgress + counts.pending;
  const pct = total ? Math.round((counts.done / total) * 100) : 0;

  const GROUPS = [
    { key: "done", label: "Completed", color: "#3DA164" },
    { key: "inProgress", label: "In progress", color: "#B47B14" },
    { key: "pending", label: "Not completed", color: "#5B6470" },
  ];

  return (
    <>
      <div className="mt-4 flex flex-wrap items-center gap-3">
        <div className="flex gap-1 p-1 rounded-xl" style={{ background: "#E6E8E5" }}>
          {[["weekly", "This week"], ["monthly", "This month"]].map(([s, l]) => (
            <button key={s} onClick={() => setScope(s)}
              className="px-3 py-1.5 rounded-lg text-xs font-medium"
              style={scope === s ? { background: "#16191D", color: "#fff" } : { background: "transparent", color: "#5B6470" }}>
              {l}
            </button>
          ))}
        </div>
        <div className="ml-auto">
          <PeriodNav mode={scope} value={scope === "weekly" ? mondayOf(anchor) : anchor.slice(0, 7)}
            label={label} isCurrent={isCurrent} currentWord="now"
            onPrev={prev} onNext={next}
            onPick={(v) => setAnchor(scope === "weekly" ? v : v + "-01")} />
        </div>
      </div>

      <div className="mt-4 flex items-center gap-3">
        <div className="w-12 h-12 rounded-full flex items-center justify-center text-sm font-semibold text-white shrink-0" style={{ background: "#16191D" }}>
          {initials(me.name)}
        </div>
        <div className="min-w-0">
          {editingName ? (
            <div className="flex items-center gap-2">
              <input autoFocus value={nameVal} onChange={(e) => setNameVal(e.target.value)}
                onKeyDown={(e) => { if (e.key === "Enter") saveName(); if (e.key === "Escape") { setNameVal(me.name || ""); setEditingName(false); } }}
                className="px-2 py-1 rounded border text-sm bg-white" style={{ borderColor: "#DDE0DC" }} />
              <button onClick={saveName} disabled={savingName}
                className="px-3 py-1 rounded-lg text-xs font-medium text-white disabled:opacity-60" style={{ background: "#16191D" }}>
                {savingName ? "Saving…" : "Save"}
              </button>
            </div>
          ) : (
            <h2 className="disp text-lg font-bold leading-tight flex items-center gap-2">
              {me.name}
              <button onClick={() => { setNameVal(me.name || ""); setEditingName(true); }}
                className="text-xs font-normal underline" style={{ color: "#5B6470" }}>edit name</button>
            </h2>
          )}
          <div className="mono text-[11px]" style={{ color: "#5B6470" }}>{me.email}</div>
        </div>
      </div>

      <section className="mt-4 grid grid-cols-2 md:grid-cols-4 gap-3">
        <Stat label="Completed" value={counts.done} accent="#3DA164" />
        <Stat label="In progress" value={counts.inProgress} accent={counts.inProgress ? "#B47B14" : undefined} />
        <Stat label="Not completed" value={counts.pending} accent={counts.pending ? "#E8553F" : undefined} />
        <Stat label="Completion" value={`${pct}%`} />
      </section>
      <div className="mt-3 h-2 rounded-full overflow-hidden" style={{ background: "#E2E5E1" }}>
        <div className="h-full rounded-full transition-all" style={{ width: `${pct}%`, background: "#3DA164" }} />
      </div>

      {data === null ? (
        <Centered>Loading…</Centered>
      ) : !total ? (
        <div className="mt-10 text-center">
          <p className="disp text-lg font-bold">No tasks {scope === "weekly" ? "this week" : "this month"}</p>
          <p className="text-sm mt-1" style={{ color: "#5B6470" }}>Add tasks on the Daily tab and they'll show up here.</p>
        </div>
      ) : (
        <div className="mt-5 grid md:grid-cols-3 gap-4 items-start">
          {GROUPS.map((g) => {
            const items = (data.buckets[g.key] || []).slice().sort((a, b) => a.date < b.date ? -1 : a.date > b.date ? 1 : 0);
            return (
              <article key={g.key} className="rounded-xl border bg-white overflow-hidden" style={{ borderColor: "#DDE0DC" }}>
                <div className="p-3 border-b flex items-center gap-2" style={{ borderColor: "#EEF0EE" }}>
                  <span className="w-2.5 h-2.5 rounded-full" style={{ background: g.color }} />
                  <h3 className="disp font-bold text-sm">{g.label}</h3>
                  <span className="mono text-[11px] ml-auto" style={{ color: "#5B6470" }}>{items.length}</span>
                </div>
                <ul className="px-2 py-1">
                  {items.map((t, i) => {
                    const P = PRIORITIES.find((x) => x.id === t.priority) || PRIORITIES[1];
                    return (
                      <li key={i} className="px-2 py-2 rounded-lg hover:bg-black/[0.03]"
                        style={t.admin ? { background: ADMIN_TASK.bg, boxShadow: `inset 3px 0 0 ${ADMIN_TASK.color}` } : undefined}>
                        <div className="flex items-center gap-2">
                          <span className="text-sm flex-1 min-w-0" style={{ color: "#16191D" }}>
                            {t.title}
                            {t.admin && <span className="mono text-[10px] ml-2" style={{ color: ADMIN_TASK.color }}>★ {ADMIN_TASK.label}</span>}
                          </span>
                          <span className="text-[10px] font-medium rounded-full px-2 py-0.5 shrink-0" style={{ background: P.bg, color: P.color }}>{P.label}</span>
                        </div>
                        <div className="mono text-[10px] mt-0.5" style={{ color: "#9AA1A9" }}>
                          {dShort(t.date)}{t.note ? <span style={{ color: "#7A5AF8" }}> · 📝 {t.note}</span> : null}
                        </div>
                      </li>
                    );
                  })}
                  {!items.length && <li className="px-2 py-3 text-sm" style={{ color: "#9AA1A9" }}>None.</li>}
                </ul>
              </article>
            );
          })}
        </div>
      )}

      <p className="mt-10 text-center mono text-xs" style={{ color: "#9AA1A9" }}>
        Your daily tasks across the {scope === "weekly" ? "week" : "month"}. A task counts once per day it appears on.
      </p>
    </>
  );
}

/* ═══════════ admin: what each employee completed ═══════════ */

function TeamReport() {
  const [scope, setScope] = useState("daily"); // daily | weekly | monthly
  const [anchor, setAnchor] = useState(todayISO());
  const [data, setData] = useState(null);
  const [openId, setOpenId] = useState(null);

  const scopeRef = useRef({ scope, anchor });
  scopeRef.current = { scope, anchor };

  const load = useCallback(async (sc, a, silent) => {
    if (!silent) setData(null);
    const r = await api(`report?scope=${sc}&date=${encodeURIComponent(a)}`);
    setData(r.ok && r.data ? r.data : { members: [], totals: { done: 0, inProgress: 0, pending: 0, total: 0, pct: 0 } });
  }, []);
  useEffect(() => { load(scope, anchor); }, [scope, anchor, load]);
  // A monthly report reads 31 days x every member, so refresh it far less
  // eagerly than the live board. Refresh also fires on tab focus.
  usePoll(() => load(scopeRef.current.scope, scopeRef.current.anchor, true), 60000);

  /* one nav for three scopes */
  const nav = {
    daily: {
      mode: "daily", value: anchor, label: prettyDate(anchor),
      isCurrent: anchor === todayISO(), word: "today",
      prev: () => setAnchor(shiftDate(anchor, -1)),
      next: () => setAnchor(shiftDate(anchor, 1)),
      pick: (v) => setAnchor(v),
    },
    weekly: {
      mode: "weekly", value: mondayOf(anchor), label: "Week of " + weekLabel(mondayOf(anchor)),
      isCurrent: mondayOf(anchor) === mondayOf(todayISO()), word: "this week",
      prev: () => setAnchor(shiftDate(anchor, -7)),
      next: () => setAnchor(shiftDate(anchor, 7)),
      pick: (v) => setAnchor(v),
    },
    monthly: {
      mode: "monthly", value: anchor.slice(0, 7), label: monthLabel(anchor.slice(0, 7)),
      isCurrent: anchor.slice(0, 7) === thisMonthKey(), word: "this month",
      prev: () => setAnchor(shiftMonth(anchor.slice(0, 7), -1) + "-01"),
      next: () => setAnchor(shiftMonth(anchor.slice(0, 7), 1) + "-01"),
      pick: (v) => setAnchor(v + "-01"),
    },
  }[scope];

  const totals = data ? data.totals : { done: 0, inProgress: 0, pending: 0, total: 0, pct: 0 };
  // Best completion rate first, but a member with nothing assigned isn't "100%".
  const rows = (data ? data.members : []).slice().sort((a, b) => (b.total ? b.pct : -1) - (a.total ? a.pct : -1) || b.done - a.done);

  return (
    <>
      <div className="mt-4 flex flex-wrap items-center gap-3">
        <div className="flex gap-1 p-1 rounded-xl" style={{ background: "#E6E8E5" }}>
          {[["daily", "Daily"], ["weekly", "Weekly"], ["monthly", "Monthly"]].map(([s, l]) => (
            <button key={s} onClick={() => setScope(s)}
              className="px-3 py-1.5 rounded-lg text-xs font-medium"
              style={scope === s ? { background: "#16191D", color: "#fff" } : { background: "transparent", color: "#5B6470" }}>
              {l}
            </button>
          ))}
        </div>
        <div className="ml-auto">
          <PeriodNav mode={nav.mode} value={nav.value} label={nav.label}
            isCurrent={nav.isCurrent} currentWord={nav.word}
            onPrev={nav.prev} onNext={nav.next} onPick={nav.pick} />
        </div>
      </div>

      <section className="mt-4 grid grid-cols-2 md:grid-cols-5 gap-3">
        <Stat label="Team completion" value={`${totals.pct}%`} accent="#3DA164" />
        <Stat label="Tasks completed" value={totals.done} accent="#3DA164" />
        <Stat label="In progress" value={totals.inProgress} accent={totals.inProgress ? "#B47B14" : undefined} />
        <Stat label="Not completed" value={totals.pending} accent={totals.pending ? "#E8553F" : undefined} />
        <Stat label="From admin done" value={`${totals.adminDone || 0}/${totals.adminAssigned || 0}`} accent={ADMIN_TASK.color} />
      </section>
      <div className="mt-3 h-2 rounded-full overflow-hidden" style={{ background: "#E2E5E1" }}>
        <div className="h-full rounded-full transition-all" style={{ width: `${totals.pct}%`, background: "#3DA164" }} />
      </div>

      {data === null ? (
        <Centered>Loading report…</Centered>
      ) : !rows.length ? (
        <div className="mt-10 text-center">
          <p className="disp text-lg font-bold">No employees yet</p>
          <p className="text-sm mt-1" style={{ color: "#5B6470" }}>Members appear here once they sign up.</p>
        </div>
      ) : (
        <div className="mt-5 space-y-3">
          {rows.map((m, i) => {
            const isOpen = openId === m.id;
            return (
              <article key={m.id} className="rounded-xl border bg-white overflow-hidden" style={{ borderColor: "#DDE0DC" }}>
                <button onClick={() => setOpenId(isOpen ? null : m.id)}
                  className="w-full text-left p-4 flex items-center gap-3 hover:bg-black/[0.02]">
                  <span className="mono text-[11px] w-5 shrink-0" style={{ color: "#9AA1A9" }}>{i + 1}</span>
                  <span className="w-9 h-9 rounded-full flex items-center justify-center text-xs font-semibold text-white shrink-0"
                    style={{ background: "#16191D" }}>{initials(m.name)}</span>

                  <span className="min-w-0 flex-1">
                    <span className="disp font-bold leading-tight block truncate">{m.name}</span>
                    <span className="mono text-[11px] flex flex-wrap gap-x-3" style={{ color: "#5B6470" }}>
                      <span style={{ color: "#3DA164" }}>{m.done} done</span>
                      {m.inProgress > 0 && <span style={{ color: "#B47B14" }}>{m.inProgress} in progress</span>}
                      {m.pending > 0 && <span style={{ color: "#E8553F" }}>{m.pending} pending</span>}
                      {m.adminAssigned > 0 && (
                        <span style={{ color: ADMIN_TASK.color }}>★ {m.adminDone}/{m.adminAssigned} from admin</span>
                      )}
                      {m.assigned && m.assigned.total > 0 && (
                        <span style={{ color: "#7A5AF8" }}>◆ {m.assigned.done}/{m.assigned.total} {scope}</span>
                      )}
                    </span>
                  </span>

                  <span className="hidden sm:block w-28 shrink-0">
                    <span className="block h-2 rounded-full overflow-hidden" style={{ background: "#E2E5E1" }}>
                      <span className="block h-full rounded-full transition-all"
                        style={{ width: `${m.pct}%`, background: m.pct >= 80 ? "#3DA164" : m.pct >= 40 ? "#F2A93B" : "#E8553F" }} />
                    </span>
                  </span>
                  <span className="disp font-bold text-lg w-14 text-right shrink-0"
                    style={{ color: m.total ? "#16191D" : "#C4C9C4" }}>
                    {m.total ? `${m.pct}%` : "—"}
                  </span>
                  <span className="shrink-0" style={{ color: "#9AA1A9" }}>{isOpen ? "▾" : "▸"}</span>
                </button>

                {isOpen && (
                  <div className="fade-in border-t px-4 py-3" style={{ borderColor: "#EEF0EE", background: "#FAFBFA" }}>
                    <div className="mono text-[10px] uppercase tracking-wide mb-2" style={{ color: "#5B6470" }}>
                      Completed {scope === "daily" ? "this day" : scope === "weekly" ? "this week" : "this month"} ({m.done})
                    </div>
                    {!m.completed.length ? (
                      <p className="text-sm" style={{ color: "#9AA1A9" }}>Nothing completed in this period.</p>
                    ) : (
                      <ul className="space-y-1">
                        {m.completed.map((t, k) => {
                          const P = PRIORITIES.find((x) => x.id === t.priority) || PRIORITIES[1];
                          return (
                            <li key={k} className="flex items-center gap-2 py-1">
                              <span className="shrink-0" style={{ color: "#3DA164" }}>✓</span>
                              <span className="text-sm flex-1 min-w-0">
                                {t.title}
                                {t.admin && <span className="mono text-[10px] ml-2" style={{ color: ADMIN_TASK.color }}>★ from admin</span>}
                              </span>
                              {t.note && <span className="text-[11px] hidden md:block" style={{ color: "#7A5AF8" }}>📝 {t.note}</span>}
                              <span className="text-[10px] font-medium rounded-full px-2 py-0.5 shrink-0" style={{ background: P.bg, color: P.color }}>{P.label}</span>
                              {scope !== "daily" && <span className="mono text-[10px] w-24 text-right shrink-0" style={{ color: "#9AA1A9" }}>{dShort(t.date)}</span>}
                            </li>
                          );
                        })}
                      </ul>
                    )}
                  </div>
                )}
              </article>
            );
          })}
        </div>
      )}

      <p className="mt-10 text-center mono text-xs" style={{ color: "#9AA1A9" }}>
        Completion is measured across each member's daily tasks for the selected period. Click a member to see exactly what they finished.
      </p>
    </>
  );
}

/* ═══════════ side panels: tasks for the admin + suggestions ═══════════
   Both live in one card behind a tab strip so the sidebar stays a readable
   height. Both stay mounted while hidden so their counts keep polling. */

function SidePanels({ me, isAdmin, setSaveState }) {
  const [tab, setTab] = useState("requests");
  const [counts, setCounts] = useState({ requests: 0, suggestions: 0 });
  const count = useCallback((key, n) => setCounts((c) => (c[key] === n ? c : { ...c, [key]: n })), []);

  const TABS = [
    ["requests", "For the admin"],
    ["suggestions", "Suggestions"],
  ];

  return (
    <div className="rounded-xl border bg-white overflow-hidden" style={{ borderColor: "#DDE0DC" }}>
      <div className="flex" style={{ background: "#F7F8F7", borderBottom: "1px solid #EEF0EE" }}>
        {TABS.map(([id, label]) => {
          const on = tab === id;
          return (
            <button key={id} onClick={() => setTab(id)}
              className="flex-1 px-3 py-2.5 text-xs font-semibold flex items-center justify-center gap-1.5 transition-colors"
              style={{
                background: on ? "#fff" : "transparent",
                color: on ? "#16191D" : "#5B6470",
                borderBottom: on ? "2px solid #16191D" : "2px solid transparent",
              }}>
              {label}
              {counts[id] > 0 && (
                <span className="mono text-[10px] px-1.5 rounded-full"
                  style={{ background: on ? "#FCF1DC" : "#E6E8E5", color: on ? "#B47B14" : "#5B6470" }}>
                  {counts[id]}
                </span>
              )}
            </button>
          );
        })}
      </div>

      <div className={tab === "requests" ? "" : "hidden"}>
        <Requests me={me} isAdmin={isAdmin} setSaveState={setSaveState} onCount={(n) => count("requests", n)} />
      </div>
      <div className={tab === "suggestions" ? "" : "hidden"}>
        <Suggestions me={me} isAdmin={isAdmin} setSaveState={setSaveState} onCount={(n) => count("suggestions", n)} />
      </div>
    </div>
  );
}

/* ═══════════ tasks employees assign UP to an admin ═══════════ */

function Requests({ me, isAdmin, setSaveState, onCount }) {
  const [list, setList] = useState(null); // null = loading
  const [admins, setAdmins] = useState([]);
  const [text, setText] = useState("");
  const [to, setTo] = useState("0");
  const [prio, setPrio] = useState("medium");

  const busyRef = useRef(false);
  const load = useCallback(async () => {
    const r = await api("requests");
    if (!r.ok || !r.data) { setList([]); return; }
    setList(r.data.requests || []);
    setAdmins(r.data.admins || []);
  }, []);
  useEffect(() => { load(); }, [load]);
  usePoll(() => { if (!busyRef.current) load(); }, 25000);

  const open = (list || []).filter((s) => !s.done).length;
  useEffect(() => { if (onCount) onCount(open); }, [open, onCount]);

  const flash = (ok) => { setSaveState(ok ? "saved" : "error"); if (ok) setTimeout(() => setSaveState("idle"), 1500); };
  const send = async (body, optimistic) => {
    if (optimistic) setList(optimistic);
    busyRef.current = true;
    setSaveState("saving");
    const r = await api("requests", { method: "POST", body: JSON.stringify(body) });
    busyRef.current = false;
    flash(r.ok);
    if (!r.ok || !optimistic) load();
    return r;
  };

  const add = async () => {
    if (!text.trim()) return;
    const r = await send({ action: "add", text: text.trim(), to: Number(to), priority: prio });
    if (r.ok) { setText(""); setPrio("medium"); }
  };
  const toggle = (id) => {
    if (!isAdmin) return;
    send({ action: "toggle", id }, list.map((s) => (s.id === id ? { ...s, done: !s.done } : s)));
  };
  const edit = (id, v) => send({ action: "edit", id, text: v }, list.map((s) => (s.id === id ? { ...s, text: v } : s)));
  const del = (id) => send({ action: "delete", id }, list.filter((s) => s.id !== id));

  const items = (list || []).slice().sort((a, b) => (a.done - b.done) || (prioRank[a.priority] - prioRank[b.priority]) || (a.createdAt - b.createdAt));

  return (
    <div>
      <div className="p-4 border-b" style={{ borderColor: "#EEF0EE" }}>
        <h2 className="disp font-bold flex items-center gap-2">
          Tasks for the admin
          {open > 0 && <span className="mono text-[10px] px-2 py-0.5 rounded-full" style={{ background: "#FCF1DC", color: "#B47B14" }}>{open} open</span>}
        </h2>
        <p className="text-xs mt-0.5" style={{ color: "#5B6470" }}>
          {isAdmin
            ? "Raised by the team for you — tick them off as you go."
            : "Need something from an admin? Assign it here and tag who it's for."}
        </p>
      </div>

      <div className="p-3 border-b space-y-2" style={{ borderColor: "#EEF0EE", background: "#FAFBFA" }}>
        <input value={text} onChange={(e) => setText(e.target.value)} onKeyDown={(e) => e.key === "Enter" && add()}
          placeholder="e.g. Approve the leave request…"
          className="w-full px-3 py-1.5 rounded-lg border text-sm bg-white" style={{ borderColor: "#DDE0DC" }} />
        <div className="flex gap-2">
          <select value={to} onChange={(e) => setTo(e.target.value)} aria-label="Tag an admin"
            className="flex-1 min-w-0 text-xs rounded-lg border px-2 py-1.5 bg-white" style={{ borderColor: "#DDE0DC", color: "#5B6470" }}>
            <option value="0">@ Any admin</option>
            {admins.map((a) => <option key={a.id} value={a.id}>@ {a.name}</option>)}
          </select>
          <select value={prio} onChange={(e) => setPrio(e.target.value)} aria-label="Priority"
            className="text-xs rounded-lg border px-2 py-1.5 bg-white" style={{ borderColor: "#DDE0DC", color: "#5B6470" }}>
            {PRIORITIES.map((p) => <option key={p.id} value={p.id}>{p.label}</option>)}
          </select>
          <button onClick={add} className="px-3 py-1.5 rounded-lg text-sm font-medium text-white shrink-0" style={{ background: "#16191D" }}>
            Add
          </button>
        </div>
      </div>

      <ul className="side-scroll px-2 py-1 max-h-[26rem] overflow-auto">
        {list === null ? (
          <li className="px-2 py-3 text-sm" style={{ color: "#9AA1A9" }}>Loading…</li>
        ) : !items.length ? (
          <li className="px-2 py-3 text-sm" style={{ color: "#9AA1A9" }}>
            {isAdmin ? "Nothing requested yet." : "Nothing yet — assign your first task to an admin above."}
          </li>
        ) : items.map((s) => {
          const P = PRIORITIES.find((x) => x.id === s.priority) || PRIORITIES[1];
          const isMine = String(s.byId) === String(me.id);
          const canEdit = isAdmin || isMine;
          return (
            <li key={s.id} className="task-row group flex items-start gap-2.5 px-2 py-2 rounded-lg hover:bg-black/[0.03]">
              <button onClick={() => toggle(s.id)} disabled={!isAdmin}
                aria-label={s.done ? "Mark not done" : "Mark done"}
                title={isAdmin ? "" : "Only an admin can tick these off"}
                className="mt-0.5 w-5 h-5 rounded border-2 flex items-center justify-center text-[11px] shrink-0"
                style={{ borderColor: s.done ? "#3DA164" : "#C4C9C4", background: s.done ? "#3DA164" : "#fff", color: "#fff", cursor: isAdmin ? "pointer" : "default" }}>
                {s.done ? "✓" : ""}
              </button>
              <div className="flex-1 min-w-0">
                <div className="flex items-start gap-2">
                  <InlineText value={s.text} editable={canEdit} onSave={(v) => edit(s.id, v)}
                    className={`text-sm flex-1 min-w-0 ${s.done ? "line-through" : ""}`}
                    style={{ color: s.done ? "#9AA1A9" : "#16191D" }} />
                  {!s.done && (
                    <span className="text-[10px] font-medium rounded-full px-2 py-0.5 shrink-0" style={{ background: P.bg, color: P.color }}>{P.label}</span>
                  )}
                </div>
                <div className="mono text-[10px] mt-0.5 flex flex-wrap gap-x-2" style={{ color: "#9AA1A9" }}>
                  <span title={s.external && s.byEmail ? s.byEmail : undefined}>
                    — {s.by}{isMine ? " (you)" : ""}
                    {s.external && <span style={{ color: EXTERNAL_TASK.color }}> ↗ external</span>}
                  </span>
                  <span style={{ color: ADMIN_TASK.color }}>@ {s.to || "any admin"}</span>
                  {s.date && <span>· due {dShort(s.date)}</span>}
                </div>
                {s.note && (
                  <div className="text-[11px] mt-0.5" style={{ color: "#7A5AF8" }}>📝 {s.note}</div>
                )}
              </div>
              {canEdit && (
                <button onClick={() => del(s.id)} aria-label="Delete request"
                  className="del-btn opacity-0 transition-opacity w-5 h-5 text-xs rounded hover:bg-red-50 shrink-0" style={{ color: "#E8553F" }}>✕</button>
              )}
            </li>
          );
        })}
      </ul>
    </div>
  );
}

/* ═══════════ suggestions panel ═══════════ */

function Suggestions({ me, isAdmin, setSaveState, onCount }) {
  const [list, setList] = useState(null); // null = loading
  const [text, setText] = useState("");

  const busyRef = useRef(false);
  const load = useCallback(async () => {
    const r = await api("suggestions");
    setList(r.ok && r.data ? (r.data.suggestions || []) : []);
  }, []);
  useEffect(() => { load(); }, [load]);
  usePoll(() => { if (!busyRef.current) load(); }, 25000);

  const flash = (ok) => { setSaveState(ok ? "saved" : "error"); if (ok) setTimeout(() => setSaveState("idle"), 1500); };

  const add = async () => {
    if (!text.trim()) return;
    busyRef.current = true;
    setSaveState("saving");
    const r = await api("suggestions", { method: "POST", body: JSON.stringify({ action: "add", text: text.trim() }) });
    busyRef.current = false;
    flash(r.ok);
    if (r.ok) { setText(""); load(); }
  };
  const toggle = async (id) => {
    if (!isAdmin) return;
    setList((prev) => prev.map((s) => (s.id === id ? { ...s, done: !s.done } : s)));
    busyRef.current = true;
    setSaveState("saving");
    const r = await api("suggestions", { method: "POST", body: JSON.stringify({ action: "toggle", id }) });
    busyRef.current = false;
    flash(r.ok);
    if (!r.ok) load();
  };
  const edit = async (id, v) => {
    setList((prev) => prev.map((s) => (s.id === id ? { ...s, text: v } : s)));
    busyRef.current = true;
    setSaveState("saving");
    const r = await api("suggestions", { method: "POST", body: JSON.stringify({ action: "edit", id, text: v }) });
    busyRef.current = false;
    flash(r.ok);
    if (!r.ok) load();
  };
  const del = async (id) => {
    setList((prev) => prev.filter((s) => s.id !== id));
    busyRef.current = true;
    setSaveState("saving");
    const r = await api("suggestions", { method: "POST", body: JSON.stringify({ action: "delete", id }) });
    busyRef.current = false;
    flash(r.ok);
    if (!r.ok) load();
  };

  const items = (list || []).slice().sort((a, b) => (a.done - b.done) || (a.createdAt - b.createdAt));
  const open = (list || []).filter((s) => !s.done).length;
  useEffect(() => { if (onCount) onCount(open); }, [open, onCount]);

  return (
    <div>
      <div className="p-4 border-b" style={{ borderColor: "#EEF0EE" }}>
        <h2 className="disp font-bold flex items-center gap-2">
          Suggestions
          {open > 0 && <span className="mono text-[10px] px-2 py-0.5 rounded-full" style={{ background: "#FCF1DC", color: "#B47B14" }}>{open} open</span>}
        </h2>
        <p className="text-xs mt-0.5" style={{ color: "#5B6470" }}>
          {isAdmin ? "Ideas from the team — tick the ones you've done." : "Share an idea. The developer ticks it off when it's done."}
        </p>
      </div>

      <div className="p-3 border-b" style={{ borderColor: "#EEF0EE", background: "#FAFBFA" }}>
        <div className="flex gap-2">
          <input value={text} onChange={(e) => setText(e.target.value)} onKeyDown={(e) => e.key === "Enter" && add()}
            placeholder="Write a suggestion…"
            className="flex-1 min-w-0 px-3 py-1.5 rounded-lg border text-sm bg-white" style={{ borderColor: "#DDE0DC" }} />
          <button onClick={add} className="px-3 py-1.5 rounded-lg text-sm font-medium text-white" style={{ background: "#16191D" }}>Add</button>
        </div>
      </div>

      <ul className="side-scroll px-2 py-1 max-h-[26rem] overflow-auto">
        {list === null ? (
          <li className="px-2 py-3 text-sm" style={{ color: "#9AA1A9" }}>Loading…</li>
        ) : !items.length ? (
          <li className="px-2 py-3 text-sm" style={{ color: "#9AA1A9" }}>No suggestions yet.</li>
        ) : items.map((s) => {
          const canDelete = isAdmin || String(s.byId) === String(me.id);
          return (
            <li key={s.id} className="task-row group flex items-start gap-2.5 px-2 py-2 rounded-lg hover:bg-black/[0.03]">
              <button onClick={() => isAdmin && toggle(s.id)} disabled={!isAdmin}
                aria-label={s.done ? "Mark not done" : "Mark done"}
                title={isAdmin ? "" : "Only the developer ticks these"}
                className="mt-0.5 w-5 h-5 rounded border-2 flex items-center justify-center text-[11px] shrink-0"
                style={{ borderColor: s.done ? "#3DA164" : "#C4C9C4", background: s.done ? "#3DA164" : "#fff", color: "#fff", cursor: isAdmin ? "pointer" : "default" }}>
                {s.done ? "✓" : ""}
              </button>
              <div className="flex-1 min-w-0">
                <InlineText value={s.text} editable={canDelete} onSave={(v) => edit(s.id, v)}
                  className={`text-sm ${s.done ? "line-through" : ""}`}
                  style={{ color: s.done ? "#9AA1A9" : "#16191D" }} />
                <div className="mono text-[10px]" style={{ color: "#9AA1A9" }}>— {s.by}{String(s.byId) === String(me.id) ? " (you)" : ""}</div>
              </div>
              {canDelete && (
                <button onClick={() => del(s.id)} aria-label="Delete suggestion"
                  className="del-btn opacity-0 transition-opacity w-5 h-5 text-xs rounded hover:bg-red-50 shrink-0" style={{ color: "#E8553F" }}>✕</button>
              )}
            </li>
          );
        })}
      </ul>
    </div>
  );
}

/* ═══════════ notification bell (persistent inbox) ═══════════ */

const timeAgo = (ts) => {
  if (!ts) return "";
  const s = Math.floor((Date.now() - ts) / 1000);
  if (s < 60) return "just now";
  const m = Math.floor(s / 60); if (m < 60) return m + "m ago";
  const h = Math.floor(m / 60); if (h < 24) return h + "h ago";
  const d = Math.floor(h / 24); return d + "d ago";
};
const dayBucket = (ts) => {
  const now = new Date();
  const startToday = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
  if (ts >= startToday) return "Today";
  if (ts >= startToday - 86400000) return "Yesterday";
  return "Earlier";
};
const noteIcon = (title) => {
  const t = (title || "").toLowerCase();
  if (t.includes("assigned")) return "🗓️";
  if (t.includes("updated")) return "✏️";
  if (t.includes("new task")) return "📋";
  return "🔔";
};

function NotificationBell() {
  const [items, setItems] = useState([]);
  const [unread, setUnread] = useState(0);
  const [open, setOpen] = useState(false);
  const [toast, setToast] = useState(null);
  const prevUnread = useRef(0);

  const load = useCallback(async () => {
    const r = await api("notifications");
    if (!r.ok || !r.data) return;
    const list = r.data.notifications || [];
    const u = r.data.unread || 0;
    if (u > prevUnread.current) {
      const newest = list.find((n) => !n.read);
      if (newest) { setToast(newest); setTimeout(() => setToast(null), 6000); }
    }
    prevUnread.current = u;
    setItems(list);
    setUnread(u);
  }, []);
  useEffect(() => { load(); }, [load]);
  usePoll(load, 20000);

  const toggle = async () => {
    const next = !open;
    setOpen(next);
    if (next && unread > 0) {
      await api("notifications", { method: "POST", body: JSON.stringify({ action: "read_all" }) });
      setItems((xs) => xs.map((n) => ({ ...n, read: true })));
      setUnread(0);
      prevUnread.current = 0;
    }
  };
  const clearAll = async () => {
    await api("notifications", { method: "POST", body: JSON.stringify({ action: "clear" }) });
    setItems([]); setUnread(0); prevUnread.current = 0;
  };

  return (
    <div className="relative">
      <button onClick={toggle} aria-label="Notifications"
        className="relative text-sm px-2.5 py-1.5 rounded border border-white/20 text-white hover:bg-white/10">
        🔔
        {unread > 0 && (
          <span className="absolute -top-1.5 -right-1.5 min-w-[18px] h-[18px] px-1 rounded-full text-[10px] font-bold flex items-center justify-center"
            style={{ background: "#E8553F", color: "#fff" }}>
            {unread > 9 ? "9+" : unread}
          </span>
        )}
      </button>

      {open && (
        <div className="absolute right-0 mt-2 w-80 max-h-[28rem] overflow-auto rounded-2xl border bg-white shadow-xl z-50"
          style={{ borderColor: "#DDE0DC", color: "#16191D" }}>
          <div className="flex items-center justify-between px-4 py-3 border-b sticky top-0 bg-white" style={{ borderColor: "#EEF0EE" }}>
            <div>
              <div className="disp font-bold text-sm">Notifications</div>
              <div className="text-[11px]" style={{ color: "#9AA1A9" }}>
                {unread > 0 ? `${unread} unread` : "You're all caught up"}
              </div>
            </div>
            <div className="flex items-center gap-2">
              {items.length > 0 && <button onClick={clearAll} className="text-xs underline" style={{ color: "#5B6470" }}>Clear all</button>}
              <button onClick={() => setOpen(false)} aria-label="Close" className="w-6 h-6 rounded-full hover:bg-black/5" style={{ color: "#5B6470" }}>✕</button>
            </div>
          </div>
          <ul className="py-1">
            {!items.length ? (
              <li className="px-4 py-8 text-sm text-center" style={{ color: "#9AA1A9" }}>No notifications yet.</li>
            ) : items.map((n, i) => {
              const bucket = dayBucket(n.ts);
              const showHead = i === 0 || dayBucket(items[i - 1].ts) !== bucket;
              return (
                <React.Fragment key={n.id}>
                  {showHead && (
                    <li className="px-4 pt-3 pb-1 mono text-[10px] uppercase tracking-wide" style={{ color: "#9AA1A9" }}>{bucket}</li>
                  )}
                  <li className="px-4 py-2.5 flex gap-3 items-start" style={{ background: n.read ? "#fff" : "#F7F5FF" }}>
                    <span className="w-8 h-8 rounded-full flex items-center justify-center text-sm shrink-0" style={{ background: "#EDEAFB" }}>
                      {noteIcon(n.title)}
                    </span>
                    <div className="min-w-0 flex-1">
                      <div className="flex items-baseline gap-2">
                        <span className="text-sm font-semibold truncate">{n.title || "Notification"}</span>
                        <span className="mono text-[10px] ml-auto shrink-0" style={{ color: "#9AA1A9" }}>{timeAgo(n.ts)}</span>
                      </div>
                      {n.text && <div className="text-xs mt-0.5 break-words" style={{ color: "#5B6470" }}>{n.text}</div>}
                    </div>
                  </li>
                </React.Fragment>
              );
            })}
          </ul>
        </div>
      )}

      {toast && (
        <div className="fixed top-16 right-4 z-50 fade-in flex gap-3 items-start px-4 py-3 rounded-xl shadow-lg bg-white border max-w-xs"
          style={{ borderColor: "#DDE0DC", color: "#16191D" }}>
          <span className="w-8 h-8 rounded-full flex items-center justify-center text-sm shrink-0" style={{ background: "#EDEAFB" }}>{noteIcon(toast.title)}</span>
          <div className="min-w-0">
            <div className="text-sm font-semibold">{toast.title || "Notification"}</div>
            {toast.text && <div className="text-xs mt-0.5" style={{ color: "#5B6470" }}>{toast.text}</div>}
          </div>
        </div>
      )}
    </div>
  );
}

/* ── mount ── */
const taskDbRoot = document.getElementById("task-db-root");
if (taskDbRoot) {
  ReactDOM.createRoot(taskDbRoot).render(<App />);
}
