// mytasks.jsx — My Work (cross-project) — hero + smart sections + side rail

const MW_MONTHS = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
const MW_WEEKDAYS_LONG = ["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"];
const MW_WEEKDAYS_SHORT = ["Mon","Tue","Wed","Thu","Fri","Sat","Sun"];

// Compute "today" from the actual current date so dates in fixtures align
// with whatever date the app is running on, not a frozen demo date.
function mwToday() {
  const d = new Date();
  const m = d.getMonth();
  const day = d.getDate();
  const wd = d.getDay();
  return {
    m:      MW_MONTHS[m],
    mIdx:   m,
    d:      day,
    year:   d.getFullYear(),
    weekday: MW_WEEKDAYS_LONG[wd],
    label:  `${MW_WEEKDAYS_LONG[wd]}, ${MW_MONTHS[m]} ${day}`,
  };
}

// Parse short due like "Apr 24" into {mIdx, d}; null if not parseable.
function mwParseDue(s) {
  if (!s || s === "—") return null;
  const m = String(s).match(/(\w+)\s+(\d+)/);
  if (!m) return null;
  const mIdx = MW_MONTHS.indexOf(m[1]);
  if (mIdx < 0) return null;
  return { mIdx, d: parseInt(m[2], 10) };
}

// Compare two {mIdx, d} dates assuming the same year (or wrap-aware via "today").
// Returns ordinal day-of-year delta (positive = future, negative = past).
function mwDaysFromToday(due, today) {
  if (!due) return null;
  // Use a real Date so month-day math handles year wrap (Dec → Jan).
  const a = new Date(today.year, today.mIdx, today.d);
  const b = new Date(today.year, due.mIdx, due.d);
  // If due is before "today" by more than 6 months, assume next-year carry.
  const diff = Math.round((b - a) / 86400000);
  if (diff < -180) {
    const c = new Date(today.year + 1, due.mIdx, due.d);
    return Math.round((c - a) / 86400000);
  }
  return diff;
}

function mwDueBucket(t, today) {
  // Returns one of: overdue | today | week | later | someday | done
  if (t.status === "done") return "done";
  const due = mwParseDue(t.due);
  if (!due) return "someday";
  const delta = mwDaysFromToday(due, today);
  if (delta < 0)  return "overdue";
  if (delta === 0) return "today";
  if (delta <= 5) return "week";
  if (delta <= 30) return "later";
  return "someday";
}

// Tasks "blocked by" someone else (not the user) go into a special bucket
function mwIsWaiting(t) { return t.status === "blocked" && !!t.blockedBy; }

function mwBucket(t, today) {
  if (t.status === "done") return "done";
  if (mwIsWaiting(t))       return "waiting";
  return mwDueBucket(t, today);
}

// ─────────────────────────────────────────────────────────────
// Top-level view
// ─────────────────────────────────────────────────────────────
function MyTasksView({ onOpen, currentUserId, tasksFromApp, onUpdate, onDelete, query = "", setQuery, projects = [], setProjects, statuses = [], setStatuses }) {
  // CRITICAL: source from the tasksFromApp prop (FlowboardApp's React
  // state), not from window.ALL_TASKS. Local edits via updateTask()
  // patch the React state immediately, but they don't touch ALL_TASKS —
  // and the SSE echo suppression added in v1.0.0 means our own writes
  // never come back through realtime.js to update ALL_TASKS either.
  // Falling back to ALL_TASKS keeps the view live for the very first
  // render (before FlowboardApp has hydrated its tasks state).
  const liveTasks = (Array.isArray(tasksFromApp) && tasksFromApp.length)
    ? tasksFromApp
    : (typeof ALL_TASKS !== "undefined" ? ALL_TASKS : []) || [];
  const [activeFilter, setActiveFilter] = React.useState("all"); // all | overdue | today | week | waiting | done
  // Hide-done preference for the home page. Persisted to localStorage
  // so each user's choice sticks across reloads (mirrors the project
  // task views' "Hide done" toggle). Defaults to ON — most users
  // don't want a long tail of completed work cluttering their home.
  // Setting it to false brings completed tasks back into every list
  // / week / project / table view at once.
  const [hideDone, setHideDone] = React.useState(() => {
    try { return localStorage.getItem("fb.mw.hideDone") !== "0"; }
    catch { return true; }
  });
  React.useEffect(() => {
    try { localStorage.setItem("fb.mw.hideDone", hideDone ? "1" : "0"); } catch {}
  }, [hideDone]);

  // ── Live clock (IST) ──────────────────────────────────────────────
  // Ticks every second. Locked to Asia/Kolkata via Intl.DateTimeFormat
  // so it shows IST whether the user is in Bangalore or Brooklyn.
  // Pauses when the tab isn't visible (no point burning a render every
  // second on a hidden tab) and re-syncs the moment focus comes back.
  const [now, setNow] = React.useState(() => new Date());
  React.useEffect(() => {
    let timer = null;
    function start() {
      stop();
      // Set immediately so the moment the tab becomes visible the
      // clock catches up to "now" before the first tick.
      setNow(new Date());
      timer = setInterval(() => setNow(new Date()), 1000);
    }
    function stop() { if (timer) { clearInterval(timer); timer = null; } }
    function onVis() {
      if (document.visibilityState === "visible") start();
      else stop();
    }
    start();
    document.addEventListener("visibilitychange", onVis);
    return () => {
      stop();
      document.removeEventListener("visibilitychange", onVis);
    };
  }, []);
  // Pull the signed-in user's timezone preference (migration 026 +
  // PATCH /api/users/me). Falls back to IST when missing — both
  // because that's the workspace default and because legacy users
  // who haven't picked a zone yet should still see a sensible time.
  // Read from api.user so the user's own setting takes effect
  // immediately (the cache is patched on save and on SSE fan-out).
  const userTz = (() => {
    try {
      const u = (window.api && window.api.getUser && window.api.getUser()) || null;
      if (u && u.timezone) return u.timezone;
    } catch {}
    return "Asia/Kolkata";
  })();

  // Memoize the formatter — Intl objects are slow to construct but
  // cheap to reuse, and `now` ticks 60× per minute. Re-derived when
  // the user changes their timezone via the topbar menu, so the
  // clock honors the new zone without a reload.
  const clockFmt = React.useMemo(
    () => {
      try {
        return new Intl.DateTimeFormat("en-IN", {
          timeZone: userTz,
          hour: "2-digit", minute: "2-digit", second: "2-digit",
          hour12: true,
        });
      } catch {
        // Bad IANA string (shouldn't happen — backend validates)
        // — fall through to IST so the clock keeps ticking.
        return new Intl.DateTimeFormat("en-IN", {
          timeZone: "Asia/Kolkata",
          hour: "2-digit", minute: "2-digit", second: "2-digit",
          hour12: true,
        });
      }
    },
    [userTz]
  );
  const clockText = clockFmt.format(now);
  // Short label next to the time — strip the region prefix so the
  // chip stays compact. "Asia/Kolkata" → "Kolkata", "America/New_York"
  // → "New York". UTC stays "UTC".
  const tzShort = userTz === "UTC"
    ? "UTC"
    : (userTz.split("/").pop() || userTz).replace(/_/g, " ");
  // Default view is the full Table — same component as the project Tasks
  // tab — so My Work gives you a single sortable / filterable list of
  // every task assigned to you, with inline edit + bin-delete.
  const [viewMode, setViewMode] = React.useState("table");        // table | list | week | project
  const [doneIds, setDoneIds] = React.useState(new Set());
  const [snoozedIds, setSnoozedIds] = React.useState(new Set());
  const [menu, setMenu] = React.useState(null);
  const closeMenu = () => setMenu(null);

  // Bumps whenever ALL_TASKS is patched in place by realtime.js. Used as
  // a useMemo dependency so the derived lists below recompute when a
  // teammate edits a task (or the current user edits one in another view).
  const [tasksVersion, setTasksVersion] = React.useState(0);
  React.useEffect(() => {
    function onPatched() { setTasksVersion(v => v + 1); }
    window.addEventListener("flowboard:tasks:patched", onPatched);
    window.addEventListener("flowboard:refreshed",     onPatched);
    return () => {
      window.removeEventListener("flowboard:tasks:patched", onPatched);
      window.removeEventListener("flowboard:refreshed",     onPatched);
    };
  }, []);

  // Open-from-my-work hands the drawer the canonical task object from
  // ALL_TASKS (matching by the unprefixed id). Without this, the drawer
  // received a remapped object whose `id` was "m_<realId>", so any edit
  // routed through onUpdate("m_T123", …) → 404 on the API and silent
  // no-op locally. Now drawer edits hit the real id and persist.
  function handleOpen(t) {
    if (!t) return;
    const realId = t.taskId || t.id;
    const real = liveTasks.find(x => x.id === realId) || t;
    onOpen && onOpen(real);
  }

  const today = React.useMemo(mwToday, []);

  const markDone = (id) => setDoneIds(s => { const n = new Set(s); n.add(id); return n; });
  const snooze   = (id) => setSnoozedIds(s => { const n = new Set(s); n.add(id); return n; });

  // Build the "my tasks" list for the impersonated/active user from live
  // ALL_TASKS, so acting-as switching reflows the page. Falls back to the
  // bootstrap-prebaked MY_TASKS if ALL_TASKS isn't populated yet.
  const myTasksLive = React.useMemo(() => {
    const all = liveTasks;
    if (!all || !all.length) return MY_TASKS || [];
    if (!currentUserId) return MY_TASKS || [];
    // Dedupe by task id at the rendering boundary. The upstream
    // tasks state can briefly hold the same task under tempId AND
    // realId during the optimistic-add → server-confirm window
    // (the create handler swaps in place but an SSE echo can race
    // and push the realId before the swap fires). Without this
    // guard, the cross-project list shows the same task twice — a
    // confusing dupe that survives until the next bootstrap reload.
    const seen = new Set();
    const out = [];
    for (const t of all) {
      if (!t || !t.id || seen.has(t.id)) continue;
      if (!t.sprint && t.status === "backlog") continue;       // parked
      const isOwner    = t.owners    && t.owners.includes(currentUserId);
      const isReviewer = t.reviewers && t.reviewers.includes(currentUserId);
      if (!isOwner && !isReviewer) continue;
      seen.add(t.id);
      out.push({
        id: "m_" + t.id,
        taskId: t.id,
        name: t.name,
        status: t.status,
        prio: t.prio,
        due: t.due,
        points: t.points,
        owners: t.owners,
        reviewers: t.reviewers,
        project: t.projectName,
        projectId: t.projectId,
        projectColor: t.projectColor,
        epic: t.epicTitle,
        epicColor: t.epicColor,
        source: t.sprint ? "sprint" : "backlog",
        sprintLabel: t.sprintLabel,
        completedAt: t.completedAt,
        blockedBy: t.blockedBy,
        comments: t.comments,
      });
    }
    return out;
  }, [currentUserId, tasksVersion, liveTasks]);

  // Apply local mutations
  const tasks = React.useMemo(() => myTasksLive.map(t => {
    if (doneIds.has(t.id))    return { ...t, status: "done", _justDone: true };
    if (snoozedIds.has(t.id)) {
      // Snooze to one week out from today, formatted "Mon DD"
      const s = new Date(today.year, today.mIdx, today.d + 7);
      const due = `${MW_MONTHS[s.getMonth()]} ${String(s.getDate()).padStart(2,"0")}`;
      return { ...t, due, _snoozed: true };
    }
    return t;
  }), [myTasksLive, doneIds, snoozedIds, today]);

  // Search + project + status filter
  const q = (query || "").trim().toLowerCase();
  const visible = tasks.filter(t => {
    if (q && !t.name.toLowerCase().includes(q)
          && !(t.project || "").toLowerCase().includes(q)
          && !(t.epic || "").toLowerCase().includes(q)) return false;
    if (projects.length && !projects.includes(t.project)) return false;
    if (statuses.length && !statuses.includes(t.status)) return false;
    // Hide-done lever — applies to every list/week/table/project view
    // EXCEPT when the user explicitly clicked the "Done" stat chip,
    // in which case the user wants to see done tasks specifically.
    if (hideDone && activeFilter !== "done" && t.status === "done") return false;
    return true;
  });

  // Buckets
  const buckets = React.useMemo(() => {
    const out = { overdue: [], today: [], week: [], later: [], someday: [], waiting: [], done: [] };
    for (const t of visible) out[mwBucket(t, today)].push(t);
    return out;
  }, [visible, today]);

  const counts = {
    overdue: buckets.overdue.length,
    today:   buckets.today.length,
    week:    buckets.week.length + buckets.today.length,
    waiting: buckets.waiting.length,
    done:    buckets.done.length,
  };

  const currentUser = (PEOPLE || []).find(p => p.id === currentUserId)
                   || (PEOPLE && PEOPLE[0])
                   || null;
  // Project filter source — show every project visible to the user (from
  // the global PROJECTS array, populated by /api/bootstrap with visibility
  // already applied). Previously this only listed projects the user had
  // existing tasks in, so a freshly-joined project with no assignments
  // yet would never appear in the filter.
  const projectNames = React.useMemo(() => {
    const all = (typeof PROJECTS !== "undefined" ? PROJECTS : null) || [];
    const namesFromTasks = myTasksLive.map(t => t.project).filter(Boolean);
    // Merge: every visible project + any project name that shows up on a
    // task but isn't in PROJECTS (e.g. cross-workspace edge case).
    const seen = new Set();
    const out = [];
    for (const p of all)              if (p.name && !seen.has(p.name)) { seen.add(p.name); out.push(p.name); }
    for (const n of namesFromTasks)   if (!seen.has(n))                 { seen.add(n);      out.push(n);      }
    return out;
  }, [myTasksLive, tasksVersion]);

  // Derived side-rail data — reviews are tasks where the user is a reviewer
  // and the task is sitting in review. Streak counts consecutive days with
  // any completion by the user. Mentions stay empty until comment data is
  // wired into the bootstrap.
  const reviews = React.useMemo(() => {
    const all = liveTasks;
    if (!all || !currentUserId) return [];
    return all
      .filter(t => (t.reviewers || []).includes(currentUserId) && t.status === "review")
      .slice(0, 6)
      .map(t => ({
        id: "rv_" + t.id,
        taskId: t.id,
        type: "pr",
        title: t.name,
        author: (t.owners && t.owners[0]) || null,
        project: t.projectName,
        projectColor: t.projectColor,
        added: "Awaiting your review",
        at: t.updated || "",
      }));
  }, [currentUserId, tasksVersion, liveTasks]);

  const streak = React.useMemo(() => {
    const all = liveTasks;
    if (!all || !currentUserId) return { current: 0, best: 0, thisWeek: 0, lastWeek: 0 };
    const mine = all.filter(t => (t.owners || []).includes(currentUserId) && t.completedAt);
    const days = new Set();
    for (const t of mine) {
      const d = new Date(t.completedAt);
      if (isNaN(d)) continue;
      days.add(d.toISOString().slice(0, 10));
    }
    // Current streak — consecutive days back from today (and yesterday, in
    // case nothing is closed yet today).
    const t0 = new Date(today.year, today.mIdx, today.d);
    let current = 0;
    let started = false;
    for (let i = 0; i < 365; i++) {
      const d = new Date(t0); d.setDate(t0.getDate() - i);
      const k = d.toISOString().slice(0, 10);
      if (days.has(k)) { current++; started = true; }
      else if (started) break;
      else if (i > 1) break; // give a 1-day grace before we conclude streak=0
    }
    // Best run
    const sorted = Array.from(days).sort();
    let best = 0, run = 0, prev = null;
    for (const k of sorted) {
      if (!prev) { run = 1; prev = k; best = 1; continue; }
      const a = new Date(prev), b = new Date(k);
      const diff = Math.round((b - a) / 86400000);
      run = diff === 1 ? run + 1 : 1;
      best = Math.max(best, run);
      prev = k;
    }
    best = Math.max(best, current);
    // Points this/last calendar week (Mon-anchored)
    const monday = new Date(t0);
    const dow = monday.getDay() || 7; // Mon=1..Sun=7
    monday.setDate(monday.getDate() - (dow - 1));
    monday.setHours(0,0,0,0);
    const lastMonday = new Date(monday); lastMonday.setDate(monday.getDate() - 7);
    let thisWeek = 0, lastWeek = 0;
    for (const t of mine) {
      const d = new Date(t.completedAt);
      if (isNaN(d)) continue;
      const pts = t.points || 0;
      if (d >= monday) thisWeek += pts;
      else if (d >= lastMonday) lastWeek += pts;
    }
    return { current, best, thisWeek, lastWeek };
  }, [currentUserId, today, tasksVersion, liveTasks]);

  const greeting = (() => {
    const h = new Date().getHours();
    if (h < 5)  return "Working late";
    if (h < 12) return "Good morning";
    if (h < 18) return "Good afternoon";
    return "Good evening";
  })();

  // Filter passthrough — which buckets to render
  function visibleBuckets() {
    if (activeFilter === "overdue") return ["overdue"];
    if (activeFilter === "today")   return ["today"];
    if (activeFilter === "week")    return ["today", "week"];
    if (activeFilter === "waiting") return ["waiting"];
    if (activeFilter === "done")    return ["done"];
    return ["overdue", "today", "week", "waiting", "later", "someday", "done"];
  }

  return (
    <div className="mw-wrap">
      {/* ══ Decorative background blobs ══ */}
      <div className="mw-bg-fx" aria-hidden="true">
        <span className="mw-blob mw-blob-a"/>
        <span className="mw-blob mw-blob-b"/>
        <span className="mw-blob mw-blob-c"/>
      </div>

      {/* ══ Hero ══ */}
      <div className="mw-hero">
        <div className="mw-hero-left">
          <div className="mw-hero-date">
            <span className="mw-dot-pulse" aria-hidden="true"/>{today.label}
            {/* Live clock pinned to the user's chosen timezone (set
                via the topbar avatar menu → "Set time zone"). Defaults
                to IST. tabular-nums keeps digits from jittering as
                seconds tick. */}
            <span className="mw-hero-clock"
                  title={userTz}
                  style={{ marginLeft: 10, fontVariantNumeric: "tabular-nums", color: "var(--ink-muted, #676879)", fontWeight: 600 }}>
              {clockText} <span style={{ fontWeight: 500, opacity: .7 }}>{tzShort}</span>
            </span>
          </div>
          <h1 className="mw-hero-title">
            {greeting}, <span className="mw-hero-name">{currentUser?.name?.split(" ")[0] || "there"}</span>
            <span className="mw-hero-wave" aria-hidden="true">👋</span>
          </h1>
          <div className="mw-hero-sub">
            {counts.today > 0
              ? <>You have <strong>{counts.today}</strong> task{counts.today === 1 ? "" : "s"} due today</>
              : counts.week > 0
                ? <>Nothing due today — <strong>{counts.week}</strong> task{counts.week === 1 ? "" : "s"} due this week</>
                : "Nothing due today — a good day to get ahead."}
            {counts.overdue > 0 && <> · <span className="mw-hero-warn"><Icons.Alert size={11}/> {counts.overdue} overdue</span></>}
          </div>
        </div>

        <div className="mw-hero-right">
          <div className={`mw-streak ${streak.current > 0 ? "is-on" : "is-off"}`}
               title={`Best streak: ${streak.best} day${streak.best === 1 ? "" : "s"}`}>
            <div className="mw-streak-num">{streak.current}</div>
            <div>
              <div className="mw-streak-label">day streak</div>
              <div className="mw-streak-sub">{streak.current > 0 ? "keep it lit" : "start one today"}</div>
            </div>
          </div>

          <div className="mw-view-toggle">
            {[
              { id: "table",   label: "Table",   icon: "List" },
              { id: "list",    label: "Grouped", icon: "Sort" },
              { id: "week",    label: "Week",    icon: "Calendar" },
              { id: "project", label: "Project", icon: "Folder" },
            ].map(v => {
              const Ic = Icons[v.icon];
              return (
                <button key={v.id} className={viewMode === v.id ? "is-active" : ""} onClick={() => setViewMode(v.id)}>
                  {Ic && <Ic size={12}/>} {v.label}
                </button>
              );
            })}
          </div>
        </div>
      </div>

      {/* ══ Stat filters + search ══ */}
      <div className="mw-stats-row">
        {[
          { id: "all",     label: "All open", count: visible.filter(t => t.status !== "done").length, tone: "slate" },
          { id: "overdue", label: "Overdue",  count: counts.overdue, tone: "red" },
          { id: "today",   label: "Today",    count: counts.today,   tone: "orange" },
          { id: "week",    label: "This week",count: counts.week,    tone: "blue" },
          { id: "waiting", label: "Waiting",  count: counts.waiting, tone: "purple" },
          { id: "done",    label: "Done",     count: counts.done,    tone: "green" },
        ].map(s => (
          <button key={s.id}
                  className={`mw-stat tone-${s.tone} ${activeFilter === s.id ? "is-active" : ""}`}
                  onClick={() => setActiveFilter(s.id)}>
            <div className="mw-stat-num">{s.count}</div>
            <div className="mw-stat-label">{s.label}</div>
          </button>
        ))}

        <div style={{ flex: 1 }}/>

        {/* Hide-done toggle — persisted per-user via localStorage.
            Sits next to the search input so it's reachable from every
            view (table / list / week / project). When the user clicks
            the "Done" stat chip we show done tasks regardless, since
            they explicitly asked for that view. */}
        <button
          type="button"
          className={"mw-hide-done" + (hideDone ? " is-on" : "")}
          onClick={() => setHideDone(v => !v)}
          title={hideDone
            ? "Done tasks are hidden — click to show them"
            : "Done tasks are visible — click to hide them"}>
          <Icons.Check size={13}/>
          <span>{hideDone ? "Hide done" : "Show done"}</span>
        </button>

        <div className="mw-search">
          <Icons.Search size={13}/>
          <input value={query || ""} onChange={e => setQuery && setQuery(e.target.value)} placeholder="Search…"/>
          {q && <button onClick={() => setQuery && setQuery("")}><Icons.Close size={11}/></button>}
        </div>

        <button className={`btn ${menu?.key === "project" || projects.length ? "btn-active" : ""}`}
                onClick={e => setMenu({ key: "project", anchor: e.currentTarget })}>
          <Icons.Folder size={13}/> Project
          {projects.length > 0 && <span className="btn-count">{projects.length}</span>}
        </button>

        <button className={`btn ${menu?.key === "status" || statuses.length ? "btn-active" : ""}`}
                onClick={e => setMenu({ key: "status", anchor: e.currentTarget })}>
          <Icons.Sort size={13}/> Status
          {statuses.length > 0 && <span className="btn-count">{statuses.length}</span>}
        </button>

        {menu?.key === "project" && (
          <Popover anchor={menu.anchor} onClose={closeMenu}>
            <div style={{ minWidth: 200 }}>
              {projectNames.map(p => {
                const on = projects.includes(p);
                return (
                  <div key={p} className="popover-item" onClick={() => {
                    const next = on ? projects.filter(x => x !== p) : [...projects, p];
                    setProjects && setProjects(next);
                  }}>
                    <span style={{ flex: 1 }}>{p}</span>
                    {on && <Icons.Check size={13} style={{ color: "var(--brand)" }}/>}
                  </div>
                );
              })}
              {projects.length > 0 && (
                <div className="popover-item" style={{ color: "var(--brand)", borderTop: "1px solid var(--border)" }}
                     onClick={() => { setProjects && setProjects([]); closeMenu(); }}>
                  <Icons.Close size={12}/> Clear project filter
                </div>
              )}
            </div>
          </Popover>
        )}

        {menu?.key === "status" && (
          <Popover anchor={menu.anchor} onClose={closeMenu}>
            <div style={{ minWidth: 200 }}>
              {STATUSES.map(s => {
                const on = statuses.includes(s.id);
                return (
                  <div key={s.id} className="popover-item" onClick={() => {
                    const next = on ? statuses.filter(x => x !== s.id) : [...statuses, s.id];
                    setStatuses && setStatuses(next);
                  }}>
                    <span className="mw-status" style={{ "--sc": s.color, background: `color-mix(in srgb, ${s.color || "#888"} 14%, white)`, color: s.color || "#444", marginRight: 8 }}>
                      {s.label}
                    </span>
                    <span style={{ flex: 1 }}/>
                    {on && <Icons.Check size={13} style={{ color: "var(--brand)" }}/>}
                  </div>
                );
              })}
              {statuses.length > 0 && (
                <div className="popover-item" style={{ color: "var(--brand)", borderTop: "1px solid var(--border)" }}
                     onClick={() => { setStatuses && setStatuses([]); closeMenu(); }}>
                  <Icons.Close size={12}/> Clear status filter
                </div>
              )}
            </div>
          </Popover>
        )}
      </div>

      {/* ══ Body — single column (side rail removed per design feedback) ══ */}
      <div className="mw-body mw-body-single">
        <div className="mw-main">
          {viewMode === "table"   && <MwTableView   currentUserId={currentUserId} query={q} projects={projects} statuses={statuses} hideDone={hideDone}
                                                    activeFilter={activeFilter} today={today}
                                                    tasksFromApp={liveTasks}
                                                    onOpen={onOpen} onUpdate={onUpdate} onDelete={onDelete}
                                                    tasksVersion={tasksVersion}/>}
          {viewMode === "list"    && <MwListView    buckets={buckets} visible={visibleBuckets()} onOpen={handleOpen} onDone={markDone} onSnooze={snooze}/>}
          {viewMode === "week"    && <MwWeekView    tasks={visible} onOpen={handleOpen} today={today}/>}
          {viewMode === "project" && <MwProjectView tasks={visible} onOpen={handleOpen} onDone={markDone} onSnooze={snooze} today={today}/>}
        </div>
      </div>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// Table view — same component as the project Tasks tab, scoped
// to tasks where the current user is owner or reviewer across
// every project they can see. Inline edits + bin-delete go
// through the same handlers as the project view, so a status
// change here shows up in My Work and the project Tasks tab
// simultaneously (real-time).
// ─────────────────────────────────────────────────────────────
function MwTableView({ currentUserId, query, projects = [], statuses = [], hideDone = false, activeFilter, today, tasksFromApp, onOpen, onUpdate, onDelete, tasksVersion }) {
  // Pull canonical task objects (NOT the my-work-shaped remap) so the
  // TableView's inline editors persist correctly via real ids. Reads
  // from FlowboardApp's React-state `tasks` prop the parent passes down
  // — that's the same array the project Tasks tab uses, so optimistic
  // edits ripple here within the same React tick.
  const myTasks = React.useMemo(() => {
    const all = Array.isArray(tasksFromApp) ? tasksFromApp
              : (typeof ALL_TASKS !== "undefined" ? ALL_TASKS : []) || [];
    if (!currentUserId) return [];
    // Dedupe by id at the boundary — see comment in myTasksLive
    // above for the optimistic-add / SSE-echo race that can
    // briefly leave the same task in the array twice.
    const seen = new Set();
    const out = [];
    for (const t of all) {
      if (!t || !t.id || seen.has(t.id)) continue;
      if (!t.sprint && t.status === "backlog") continue;     // parked
      const isOwner    = t.owners    && t.owners.includes(currentUserId);
      const isReviewer = t.reviewers && t.reviewers.includes(currentUserId);
      if (!isOwner && !isReviewer) continue;
      seen.add(t.id);
      out.push(t);
    }
    return out;
  }, [currentUserId, tasksVersion, tasksFromApp]);

  // Apply the same filters the rest of My Work respects: search query,
  // project name multi-select, status multi-select, and the active stat
  // chip (overdue / today / week / waiting / done / all).
  const filtered = React.useMemo(() => {
    let list = myTasks;
    const q = (query || "").trim().toLowerCase();
    if (q) {
      list = list.filter(t =>
        (t.name        || "").toLowerCase().includes(q) ||
        (t.projectName || "").toLowerCase().includes(q) ||
        (t.epicTitle   || "").toLowerCase().includes(q)
      );
    }
    if (projects.length) list = list.filter(t => projects.includes(t.projectName));
    if (statuses.length) list = list.filter(t => statuses.includes(t.status));

    // Hide-done lever from the parent — applies in the table view too,
    // EXCEPT when the user explicitly clicked the "Done" stat chip
    // (then they want to see done work specifically).
    if (hideDone && activeFilter !== "done") {
      list = list.filter(t => t.status !== "done");
    }

    if (activeFilter && activeFilter !== "all") {
      list = list.filter(t => {
        const shaped = { status: t.status, due: t.due, blockedBy: t.blockedBy };
        const bucket = mwBucket(shaped, today);
        if (activeFilter === "week")    return bucket === "today" || bucket === "week";
        if (activeFilter === "overdue") return bucket === "overdue";
        if (activeFilter === "today")   return bucket === "today";
        if (activeFilter === "waiting") return bucket === "waiting";
        if (activeFilter === "done")    return bucket === "done";
        return true;
      });
    }
    return list;
  }, [myTasks, query, projects, statuses, activeFilter, today, hideDone]);

  // Cross-project epic list — the global EPICS spans every project, so
  // pass it through. TableView builds orphan-epic groups from any task
  // whose epicId isn't in the list, which keeps cross-workspace edge
  // cases visible.
  const epics = (typeof EPICS !== "undefined" ? EPICS : []) || [];

  const [collapsed, setCollapsed] = React.useState(new Set());
  const [selected, setSelected]   = React.useState(new Set());

  function toggleEpic(id) {
    setCollapsed(s => { const n = new Set(s); n.has(id) ? n.delete(id) : n.add(id); return n; });
  }
  function onSelect(id, on) {
    setSelected(s => { const n = new Set(s); on ? n.add(id) : n.delete(id); return n; });
  }
  // Drag-drop reorder across projects is undefined (positions are scoped
  // per project+status bucket). We just disable onMove here; the user
  // can still reorder in the project Tasks tab.
  function onMove() { /* no-op in My Work table */ }

  if (!filtered.length) {
    return (
      <EmptyState
        icon="Check"
        title="You're all caught up"
        sub={query ? "No tasks match your search." : "No tasks assigned to you yet — explore a project to pick up work."}
      />
    );
  }

  return (
    <div className="mw-tableview">
      <TableView
        tasks={filtered}
        epics={epics}
        collapsed={collapsed}
        onToggle={toggleEpic}
        onUpdate={onUpdate}
        onMove={onMove}
        onOpen={onOpen}
        onDelete={onDelete}
        selected={selected}
        onSelect={onSelect}
        onAddTask={null}
        defaultSprint={null}
        hideSprintCol={false}
        /* Cross-project Home/My Work table — hide the Points column.
           Points are a project-level estimation signal that's not
           meaningful when tasks from many projects are mixed; the
           Updated / Due / Sprint columns already carry the relevant
           context and dropping Pts gives the title cell more room. */
        hidden={new Set(["points"])}
        sprints={typeof SPRINTS !== "undefined" ? SPRINTS : []}
        showProjectPill={true}
        currentUserId={currentUserId}
      />
      {/* Same bulk-action bar as the project Tasks tab — operates on the
          per-MwTableView selection, so cross-project bulk edits are
          allowed (epic / sprint pickers will note when the selection
          spans multiple projects). */}
      {typeof BulkActionBar !== "undefined" && (
        <BulkActionBar
          selected={selected}
          allTasks={filtered}
          people={typeof PEOPLE !== "undefined" ? PEOPLE : []}
          epics={typeof EPICS !== "undefined" ? EPICS : []}
          sprints={typeof SPRINTS !== "undefined" ? SPRINTS : []}
          onUpdate={onUpdate}
          onDelete={onDelete}
          onClear={() => setSelected(new Set())}/>
      )}
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// List view (default)
// ─────────────────────────────────────────────────────────────
const MW_SECTION_META = {
  overdue: { label: "Overdue",          tone: "red",    hint: "Reschedule or complete" },
  today:   { label: "Due today",        tone: "orange", hint: "Focus list" },
  week:    { label: "Later this week",  tone: "blue",   hint: null },
  waiting: { label: "Waiting on others",tone: "purple", hint: "Blocked — needs unblock" },
  later:   { label: "Next week",        tone: "slate",  hint: null },
  someday: { label: "No due date",      tone: "slate",  hint: "Backlog of personal items" },
  done:    { label: "Recently completed", tone: "green", hint: "This week" },
};

function MwListView({ buckets, visible, onOpen, onDone, onSnooze }) {
  const hasAny = visible.some(k => buckets[k]?.length);
  if (!hasAny) {
    return (
      <EmptyState
        icon="Check"
        title="You're all caught up"
        sub="No tasks match this filter. Take a break, or switch to a different view."
      />
    );
  }
  return (
    <div className="mw-sections">
      {visible.map(key => {
        const items = buckets[key] || [];
        if (!items.length) return null;
        const meta = MW_SECTION_META[key];
        return (
          <section key={key} className={`mw-section tone-${meta.tone}`}>
            <header className="mw-sec-head">
              <span className="mw-sec-dot"/>
              <h3>{meta.label}</h3>
              <span className="mw-sec-count">{items.length}</span>
              {meta.hint && <span className="mw-sec-hint">{meta.hint}</span>}
            </header>
            <div className="mw-list">
              {items.map(t => <MwTaskRow key={t.id} task={t} onOpen={onOpen} onDone={onDone} onSnooze={onSnooze} bucket={key}/>)}
            </div>
          </section>
        );
      })}
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// Task row (rich, with inline actions)
// ─────────────────────────────────────────────────────────────
function MwTaskRow({ task, onOpen, onDone, onSnooze, bucket }) {
  const isDone = task.status === "done";
  return (
    <div className={`mw-row ${isDone ? "is-done" : ""} ${task._justDone ? "just-done" : ""}`}
         data-prio={task.prio || "none"}
         data-bucket={bucket || ""}>
      <button className="mw-check" onClick={(e) => { e.stopPropagation(); !isDone && onDone && onDone(task.id); }}
              title={isDone ? "Completed" : "Mark complete"}>
        {isDone ? <Icons.Check size={12}/> : null}
      </button>

      <div className="mw-row-body" onClick={() => onOpen && onOpen(task)}>
        <div className="mw-row-line1">
          <PriorityDot prio={task.prio}/>
          <span className="mw-name">{task.name}</span>
          {task.mentions > 0 && <span className="mw-badge mentions" title={`${task.mentions} @-mention${task.mentions>1?"s":""}`}>@{task.mentions}</span>}
          {task.reviewers?.length > 0 && <span className="mw-badge review" title="Review requested">review</span>}
          {task._snoozed && <span className="mw-badge snoozed">snoozed</span>}
        </div>

        <div className="mw-row-line2">
          <span className="mw-project" style={{ color: "var(--ink-body)" }}>
            <span className="mw-proj-dot" style={{ background: task.projectColor }}/>
            {task.project}
          </span>
          {task.epic && (
            <span className="mw-epic" style={{ "--ec": task.epicColor || "var(--epic-1)" }}>
              {task.epic}
            </span>
          )}
          {task.sprintLabel && <span className="mw-chip">{task.sprintLabel}</span>}
          {task.blockedBy && <span className="mw-blocked"><Icons.Alert size={10}/> {task.blockedBy}</span>}
        </div>
      </div>

      <div className="mw-row-right">
        <MwStatusBadge status={task.status}/>
        {task.due !== "—" && <div className={`mw-due ${bucket}`}>{task.due}</div>}
        <div className="mw-pts">{task.points} pt</div>
        <div className="mw-row-actions">
          {!isDone && (
            <>
              <button className="mw-icon-btn" title="Snooze to next week" onClick={(e) => { e.stopPropagation(); onSnooze && onSnooze(task.id); }}>
                <Icons.Clock size={12}/>
              </button>
              <button className="mw-icon-btn" title="Open task" onClick={(e) => { e.stopPropagation(); onOpen && onOpen(task); }}>
                <Icons.Arrow size={12}/>
              </button>
            </>
          )}
        </div>
      </div>
    </div>
  );
}

function PriorityDot({ prio }) {
  const colors = { critical: "#e2445c", high: "#fd713d", medium: "#fdab3d", low: "#579bfc", none: "#c4c7d0" };
  return <span className="mw-prio-dot" style={{ background: colors[prio] || colors.none }} title={prio}/>;
}

function MwStatusBadge({ status }) {
  const s = STATUSES.find(x => x.id === status);
  if (!s) return null;
  return (
    <span className="mw-status" style={{ "--sc": s.color, background: `color-mix(in srgb, ${s.color} 14%, white)`, color: s.color }}>
      {s.label}
    </span>
  );
}

// ─────────────────────────────────────────────────────────────
// Week view
// ─────────────────────────────────────────────────────────────
function MwWeekView({ tasks, onOpen, today = mwToday() }) {
  // Build 7-day grid starting on Monday of the current week.
  const t0 = new Date(today.year, today.mIdx, today.d);
  const dow = t0.getDay() || 7;
  const monday = new Date(t0); monday.setDate(t0.getDate() - (dow - 1));
  const days = [];
  for (let i = 0; i < 7; i++) {
    const d = new Date(monday); d.setDate(monday.getDate() + i);
    days.push({
      key: `${d.getMonth()}-${d.getDate()}`,
      mon: MW_MONTHS[d.getMonth()],
      mIdx: d.getMonth(),
      d: d.getDate(),
      isToday: d.getMonth() === today.mIdx && d.getDate() === today.d,
      weekday: MW_WEEKDAYS_SHORT[i],
      tasks: [],
    });
  }
  const overflow = [];
  for (const t of tasks) {
    if (t.status === "done") continue;
    const due = mwParseDue(t.due);
    if (!due) { overflow.push(t); continue; }
    const day = days.find(d => d.mIdx === due.mIdx && d.d === due.d);
    if (day) day.tasks.push(t);
    else {
      const delta = mwDaysFromToday(due, today);
      if (delta < 0) days[0].tasks.push({ ...t, _wasOverdue: true });
      else overflow.push(t);
    }
  }

  return (
    <div className="mw-week">
      <div className="mw-week-grid">
        {days.map(day => (
          <div key={day.key} className={`mw-day ${day.isToday ? "is-today" : ""}`}>
            <div className="mw-day-head">
              <span className="mw-day-name">{day.weekday}</span>
              <span className="mw-day-num">{day.mon} {day.d}</span>
              {day.tasks.length > 0 && <span className="mw-day-count">{day.tasks.length}</span>}
            </div>
            <div className="mw-day-body">
              {day.tasks.map(t => (
                <div key={t.id} className={`mw-day-card prio-${t.prio} ${t._wasOverdue ? "was-overdue" : ""}`}
                     style={{ "--pc": t.projectColor }}
                     onClick={() => onOpen && onOpen(t)}>
                  <div className="mw-day-name-line">
                    <PriorityDot prio={t.prio}/>
                    {t.name}
                  </div>
                  <div className="mw-day-meta">
                    <span className="mw-proj-dot" style={{ background: t.projectColor }}/>
                    {t.project}
                  </div>
                </div>
              ))}
              {day.tasks.length === 0 && <div className="mw-day-empty">Clear</div>}
            </div>
          </div>
        ))}
      </div>
      {overflow.length > 0 && (
        <div className="mw-overflow">
          <div className="mw-overflow-head">
            <Icons.Clock size={13}/> No due date — {overflow.length} item{overflow.length===1?"":"s"}
          </div>
          <div className="mw-overflow-chips">
            {overflow.map(t => (
              <button key={t.id} className="mw-overflow-chip" onClick={() => onOpen && onOpen(t)}>
                <PriorityDot prio={t.prio}/>
                {t.name}
                <span className="mw-proj-dot" style={{ background: t.projectColor, marginLeft: 4 }}/>
              </button>
            ))}
          </div>
        </div>
      )}
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// By Project view
// ─────────────────────────────────────────────────────────────
function MwProjectView({ tasks, onOpen, onDone, onSnooze, today = mwToday() }) {
  const groups = {};
  for (const t of tasks) {
    const name = t.project || "No project";
    if (!groups[name]) groups[name] = { name, color: t.projectColor || "#a3a8b6", items: [] };
    groups[name].items.push(t);
  }
  const entries = Object.values(groups).sort((a, b) => b.items.length - a.items.length);
  if (!entries.length) {
    return (
      <EmptyState icon="Folder" title="No project work yet" sub="Tasks assigned to you will appear here grouped by project."/>
    );
  }
  return (
    <div className="mw-sections">
      {entries.map(g => {
        const pts = g.items.reduce((s, t) => s + (t.points || 0), 0);
        const donePts = g.items.filter(t => t.status === "done").reduce((s, t) => s + (t.points || 0), 0);
        const pct = pts > 0 ? Math.round((donePts / pts) * 100) : 0;
        return (
          <section key={g.name} className="mw-section tone-slate">
            <header className="mw-sec-head mw-proj-head">
              <span className="mw-proj-dot big" style={{ background: g.color }}/>
              <h3>{g.name}</h3>
              <span className="mw-sec-count">{g.items.length}</span>
              <span className="mw-proj-progress">
                <span className="mw-proj-bar"><span style={{ width: `${pct}%`, background: g.color }}/></span>
                <span className="mw-proj-pct">{pct}%</span>
              </span>
            </header>
            <div className="mw-list">
              {g.items.map(t => <MwTaskRow key={t.id} task={t} onOpen={onOpen} onDone={onDone} onSnooze={onSnooze} bucket={mwBucket(t, today)}/>)}
            </div>
          </section>
        );
      })}
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// Side rail
// ─────────────────────────────────────────────────────────────
function MwWeekSnapshot({ visible, counts, today = mwToday(), streak = { thisWeek: 0, lastWeek: 0 } }) {
  const allOpen = visible.filter(t => t.status !== "done").length;
  const totalPts = visible.reduce((s, t) => s + (t.points || 0), 0);
  const donePts  = visible.filter(t => t.status === "done").reduce((s, t) => s + (t.points || 0), 0);
  const pct = totalPts > 0 ? Math.round((donePts / totalPts) * 100) : 0;

  // Compute the Mon → Sun label for this week
  const t0 = new Date(today.year, today.mIdx, today.d);
  const dow = t0.getDay() || 7;
  const mon = new Date(t0); mon.setDate(t0.getDate() - (dow - 1));
  const sun = new Date(mon); sun.setDate(mon.getDate() + 6);
  const sameMonth = mon.getMonth() === sun.getMonth();
  const weekLabel = sameMonth
    ? `${MW_MONTHS[mon.getMonth()]} ${mon.getDate()} – ${sun.getDate()}`
    : `${MW_MONTHS[mon.getMonth()]} ${mon.getDate()} – ${MW_MONTHS[sun.getMonth()]} ${sun.getDate()}`;

  const delta = (streak.thisWeek || 0) - (streak.lastWeek || 0);

  return (
    <section className="mw-side-card">
      <header className="mw-side-head">
        <h4>This week</h4>
        <span className="mw-side-link">{weekLabel}</span>
      </header>
      <div className="mw-week-snap">
        <div className="mw-ring" style={{ "--pct": pct }}>
          <svg viewBox="0 0 36 36">
            <circle cx="18" cy="18" r="15.915" fill="none" stroke="#eef0f4" strokeWidth="3.2"/>
            <circle cx="18" cy="18" r="15.915" fill="none" stroke="#00ca72" strokeWidth="3.2"
                    strokeDasharray={`${pct} ${100 - pct}`} strokeDashoffset="25" strokeLinecap="round"/>
          </svg>
          <div className="mw-ring-center">
            <div className="mw-ring-num">{pct}%</div>
            <div className="mw-ring-lbl">done</div>
          </div>
        </div>
        <div className="mw-week-stats">
          <div><strong>{donePts}</strong> of <strong>{totalPts}</strong> pts completed</div>
          <div className={`mw-week-delta ${delta < 0 ? "is-down" : ""}`}>
            <Icons.Arrow size={10}/>
            {delta >= 0 ? "+" : ""}{delta} pt{Math.abs(delta) === 1 ? "" : "s"} vs last week
          </div>
          <div className="mw-open-line">{allOpen} task{allOpen === 1 ? "" : "s"} still open</div>
        </div>
      </div>
    </section>
  );
}

function MwMentions({ onOpen }) {
  const list = (typeof MY_MENTIONS !== "undefined" ? MY_MENTIONS : []) || [];
  return (
    <section className="mw-side-card">
      <header className="mw-side-head">
        <h4>Waiting on your reply</h4>
        <span className="mw-side-link">{list.length}</span>
      </header>
      <div className="mw-mentions">
        {list.length === 0 && (
          <div className="mw-side-empty">
            <Icons.MessageSq size={14}/>
            <span>No mentions waiting on you.</span>
          </div>
        )}
        {list.map(m => {
          const person = (PEOPLE || []).find(p => p.id === m.from);
          return (
            <div key={m.id} className="mw-mention"
                 onClick={() => onOpen && onOpen({ name: m.taskName, project: m.project, projectColor: m.projectColor, status: "progress", prio: "medium", due: "—", points: 0, owners: [m.from], epic: null })}>
              <Avatar person={person} size={26}/>
              <div className="mw-mention-body">
                <div className="mw-mention-line1">
                  <strong>{person?.name?.split(" ")[0] || "Someone"}</strong>
                  <span className="mw-mention-at">{m.at}</span>
                </div>
                <div className="mw-mention-snippet">{m.snippet}</div>
                <div className="mw-mention-ctx">
                  <span className="mw-proj-dot" style={{ background: m.projectColor }}/>
                  {m.taskName}
                </div>
              </div>
            </div>
          );
        })}
      </div>
    </section>
  );
}

function MwReviews({ reviews = [], onOpen }) {
  const list = reviews && reviews.length
    ? reviews
    : ((typeof MY_REVIEWS !== "undefined" ? MY_REVIEWS : []) || []);
  return (
    <section className="mw-side-card">
      <header className="mw-side-head">
        <h4>Reviews assigned</h4>
        <span className="mw-side-link">{list.length}</span>
      </header>
      <div className="mw-reviews">
        {list.length === 0 && (
          <div className="mw-side-empty">
            <Icons.Eye size={14}/>
            <span>No pending reviews. Nice.</span>
          </div>
        )}
        {list.map(r => {
          const person = (PEOPLE || []).find(p => p.id === r.author);
          return (
            <div key={r.id} className="mw-review"
                 onClick={() => {
                   if (!onOpen || !r.taskId) return;
                   const t = (typeof ALL_TASKS !== "undefined" ? ALL_TASKS : []).find(x => x.id === r.taskId);
                   if (t) onOpen(t);
                 }}
                 style={{ cursor: r.taskId ? "pointer" : "default" }}>
              <span className={`mw-review-kind k-${r.type}`}>
                {r.type === "pr" ? "PR" : r.type === "design" ? "Design" : "Task"}
              </span>
              <div className="mw-review-body">
                <div className="mw-review-title">{r.title}</div>
                <div className="mw-review-meta">
                  {person && <Avatar person={person} size={16}/>}
                  {person && <span>{person.name?.split(" ")[0]}</span>}
                  {person && <span className="sep">·</span>}
                  {r.projectColor && <span className="mw-proj-dot" style={{ background: r.projectColor }}/>}
                  {r.project && <><span>{r.project}</span><span className="sep">·</span></>}
                  <span className="mw-review-added">{r.added}</span>
                  {r.at && <><span className="sep">·</span><span>{r.at}</span></>}
                </div>
              </div>
              <button className="mw-review-cta" onClick={(e) => {
                e.stopPropagation();
                if (!onOpen || !r.taskId) return;
                const t = (typeof ALL_TASKS !== "undefined" ? ALL_TASKS : []).find(x => x.id === r.taskId);
                if (t) onOpen(t);
              }}>Review</button>
            </div>
          );
        })}
      </div>
    </section>
  );
}

Object.assign(window, { MyTasksView });
