// screens-actions.jsx
// Tier 2 Phase 2: Actions Page
//
// Personal "to do this week" view that unifies 5 action sources:
//   1. Pipeline Assigned Actions   (server-persisted next actions on pipeline projects)
//   2. Research Actions            (cadence steps + research_goals rows)
//   3. New Client Assigned Actions (next_action on new_clients)
//   4. Existing Project Actions    (server-persisted; placeholder until project view revamp)
//   5. Personal Action Items       (free-form user_actions)
//
// Scope is role-based with optional per-person override:
//   - "module"   - directors/MDs/CEOs see entire module
//   - "subteam"  - sub-team leaders see only their sub-team
//   - "own"      - default for analysts, RAs, interns
// Brian (admin) is still scoped to his own module — no cross-module visibility.
const { useState, useEffect, useMemo, useCallback, useRef } = React;

// ---------- Section configuration ----------
// Each "section" is one of the 5 action sources. Colors match the source:
//   Pipeline = blue (matches existing Pipeline tab accent)
//   Research = gold (matches Research tab)
//   New Client = green (matches NCT tab)
//   Project = purple (matches Projects tab)
//   Personal = neutral gray
const ACTION_TYPES = {
  pipeline: {
    label: "Pipeline",
    color: "#3b82f6",
    soft:  "rgba(59, 130, 246, 0.10)",
    short: "PIPE",
  },
  research: {
    label: "Research",
    color: "#d4a017",
    soft:  "rgba(212, 160, 23, 0.12)",
    short: "RES",
  },
  newclient: {
    label: "New Client",
    color: "#16a34a",
    soft:  "rgba(22, 163, 74, 0.10)",
    short: "NEW",
  },
  project: {
    label: "Project",
    color: "#8b5cf6",
    soft:  "rgba(139, 92, 246, 0.10)",
    short: "PROJ",
  },
  personal: {
    label: "Personal",
    color: "#64748b",
    soft:  "rgba(100, 116, 139, 0.10)",
    short: "TODO",
  },
  update: {
    label: "Update Call",
    color: "#2563eb",
    soft:  "rgba(37, 99, 235, 0.10)",
    short: "CALL",
  },
  intro: {
    label: "Intro Call",
    color: "#0891b2",
    soft:  "rgba(8, 145, 178, 0.10)",
    short: "INTRO",
  },
  conf: {
    label: "Conf Call",
    color: "#7c3aed",
    soft:  "rgba(124, 58, 237, 0.10)",
    short: "CONF",
  },
  visit: {
    label: "Visit",
    color: "#ea580c",
    soft:  "rgba(234, 88, 12, 0.10)",
    short: "VISIT",
  },
};

// ---------- Date helpers ----------
function todayStr() { const d = new Date(); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; } // LOCAL date (not UTC) so "today" is the viewer's day
function addDaysStr(dateStr, days) {
  const d = new Date(dateStr + "T00:00:00Z");
  d.setUTCDate(d.getUTCDate() + days);
  return d.toISOString().slice(0, 10);
}
function mondayOf(date) {
  const d = (date instanceof Date) ? new Date(date) : new Date(date + "T00:00:00Z");
  const dow = d.getUTCDay() || 7;
  if (dow !== 1) d.setUTCDate(d.getUTCDate() - (dow - 1));
  return d.toISOString().slice(0, 10);
}
function formatDay(dateStr) {
  return new Date(dateStr + "T00:00:00Z").toLocaleDateString("en-US", {
    weekday: "long", month: "short", day: "numeric", timeZone: "UTC",
  });
}
function formatShortDate(dateStr) {
  return new Date(dateStr + "T00:00:00Z").toLocaleDateString("en-US", {
    weekday: "short", month: "short", day: "numeric", timeZone: "UTC",
  });
}
function addBusinessDays(startDateStr, businessDays) {
  const d = new Date(startDateStr + "T00:00:00Z");
  let added = 0;
  while (added < businessDays - 1) {
    d.setUTCDate(d.getUTCDate() + 1);
    const dow = d.getUTCDay();
    if (dow !== 0 && dow !== 6) added++;
  }
  return d.toISOString().slice(0, 10);
}

// ---------- Scope derivation ----------
// Given a person, return their scope: "own" | "subteam" | "module".
// If actions_scope is explicitly set, use it; else derive from role.
function deriveScope(person, firm) {
  if (!person) return "own";
  if (person.actionsScope) return person.actionsScope;
  // If the person has no role, we can\'t reliably classify. Avoid the SUBSUBTEAMS
  // leadId heuristic in that case (it can give false positives when user.id matches
  // an unrelated sub-team\'s leadId in another module).
  const role = (person.role || "").toLowerCase();
  if (!role) return "own";
  // Module-wide visibility
  // Match full titles and common abbreviations (MD, VP, EVP, SVP, CEO, CFO)
  if (role.includes("director") || role.includes("managing director") ||
      role.includes("vice president") || role.includes("chief executive") ||
      role.includes("cfo") || role.includes("module head") ||
      /\b(md|ceo|cfo|coo|cto|evp|svp|vp)\b/.test(role)) return "module";
  // Sub-team lead detection: check if person.id is a leadId of any sub-sub-team
  if (firm && firm.SUBSUBTEAMS) {
    const isLead = firm.SUBSUBTEAMS.some(sst => sst.lead === person.id);
    if (isLead) return "subteam";
  }
  return "own";
}

// Resolve which person IDs are visible under the current scope + viewer
function resolveVisibleOwnerIds(viewer, scope, firm, viewingAsId) {
  const PEOPLE_BY_ID = firm.PEOPLE_BY_ID || {};
  const SUBSUBTEAMS = firm.SUBSUBTEAMS || [];
  const PEOPLE = firm.PEOPLE || [];
  // If the user picked a specific person in the "View as" dropdown, narrow to just them.
  // Note: this fires even when they pick themselves (so the page filters down to just self).
  if (viewingAsId) {
    return [viewingAsId];
  }
  if (scope === "module") {
    const myModule = viewer.team || (viewer.subteam ? viewer.subteam.replace(/^st-/, "") : null);
    if (!myModule) return [viewer.id];
    return PEOPLE
      .filter(p => {
        const team = p.team || (p.subteam ? p.subteam.replace(/^st-/, "") : null);
        return team === myModule;
      })
      .map(p => p.id);
  }
  if (scope === "subteam") {
    // Find which sub-sub-team the viewer leads
    const led = SUBSUBTEAMS.find(sst => sst.lead === viewer.id);
    if (!led) return [viewer.id];
    return (led.memberIds || []).concat([viewer.id]);
  }
  return [viewer.id];
}

// ---------- Outreach cycles (mirrors Research screen) ----------
const OUTREACH_CYCLES = {
  M1: { initialEmailCode: "E1", steps: [
    { day: 1,  code: "M1", type: "Mailing", owner: "MC" },
    { day: 5,  code: "E1", type: "Email",   owner: "MC" },
    { day: 6,  code: "C1", type: "Call",    owner: "Analyst" },
    { day: 8,  code: "E2", type: "Email",   owner: "MC" },
    { day: 11, code: "E3", type: "Email",   owner: "MC" },
    { day: 12, code: "C2", type: "Call",    owner: "Analyst" },
    { day: 14, code: "E4", type: "Email",   owner: "MC" },
    { day: 15, code: "C3", type: "Call",    owner: "Analyst" },
    { day: 18, code: "E9", type: "Email",   owner: "MC" },
  ]},
  eM1: { initialEmailCode: "eM1", steps: [
    { day: 1,  code: "eM1", type: "Email", owner: "MC" },
    { day: 3,  code: "E2",  type: "Email", owner: "MC" },
    { day: 4,  code: "C1",  type: "Call",  owner: "Analyst" },
    { day: 6,  code: "E3",  type: "Email", owner: "MC" },
    { day: 9,  code: "E4",  type: "Email", owner: "MC" },
    { day: 10, code: "C2",  type: "Call",  owner: "Analyst" },
    { day: 13, code: "E9",  type: "Email", owner: "MC" },
  ]},
  M2: { initialEmailCode: "E5", steps: [
    { day: 1,  code: "M2", type: "Mailing", owner: "MC" },
    { day: 5,  code: "E5", type: "Email",   owner: "MC" },
    { day: 6,  code: "C1", type: "Call",    owner: "Analyst" },
    { day: 8,  code: "E6", type: "Email",   owner: "MC" },
    { day: 9,  code: "C2", type: "Call",    owner: "Analyst" },
    { day: 11, code: "E7", type: "Email",   owner: "MC" },
    { day: 13, code: "E8", type: "Email",   owner: "MC" },
    { day: 14, code: "C3", type: "Call",    owner: "Analyst" },
    { day: 17, code: "E9", type: "Email",   owner: "MC" },
  ]},
  eM2: { initialEmailCode: "eM2", steps: [
    { day: 1,  code: "eM2", type: "Email", owner: "MC" },
    { day: 3,  code: "E6",  type: "Email", owner: "MC" },
    { day: 3,  code: "C1",  type: "Call",  owner: "Analyst" },
    { day: 4,  code: "C2",  type: "Call",  owner: "Analyst" },
    { day: 6,  code: "E7",  type: "Email", owner: "MC" },
    { day: 8,  code: "E8",  type: "Email", owner: "MC" },
    { day: 9,  code: "C3",  type: "Call",  owner: "Analyst" },
    { day: 12, code: "E9",  type: "Email", owner: "MC" },
  ]},
  CR: { initialEmailCode: "eCM1", steps: [
    { day: 1, code: "eCM1", type: "Email", owner: "MC" },
    { day: 3, code: "C1",   type: "Call",  owner: "Analyst" },
    { day: 3, code: "E2b",  type: "Email", owner: "MC" },
    { day: 4, code: "C2",   type: "Call",  owner: "Analyst" },
    { day: 6, code: "E3b",  type: "Email", owner: "MC" },
    { day: 9, code: "E9b",  type: "Email", owner: "MC" },
  ]},
};

// ---------- Main screen ----------
function ActionsScreen({ scope: outerScope, setScope, user }) {
  const F = window.VAULT_FIRM;
  const PEOPLE_BY_ID = F.PEOPLE_BY_ID || {};

  // Resolve viewer + their scope.
  // VAULT_ACCOUNTS uses fields {email, name, role, access, personId} — no "id".
  // personId can be like "p_bscott" or "p_bscott_gmail" — strip the "p_" prefix and
  // trim any "_gmail" suffix so it matches PEOPLE_BY_ID keys like "bscott".
  const viewer = useMemo(() => {
    if (!user) return null;
    // Try a series of strategies to map auth user -> person record
    const candidates = [];
    if (user.id) candidates.push(user.id);
    if (user.personId) {
      candidates.push(user.personId);
      candidates.push(user.personId.replace(/^p_/, ""));
      candidates.push(user.personId.replace(/^p_/, "").replace(/_gmail$/, ""));
    }
    // Try email local-part as last resort (e.g. "bscott@harveyllc.com" -> "bscott")
    if (user.email) candidates.push(user.email.split("@")[0]);

    for (const cand of candidates) {
      if (PEOPLE_BY_ID[cand]) {
        return { ...PEOPLE_BY_ID[cand], _authUser: user };
      }
    }
    // Couldn\'t map - fall back to auth identity. Scope will default to "own".
    return {
      id: user.id || user.personId || user.email || "unknown",
      name: user.name || user.email || "Unknown user",
      role: user.role || null,
      _authUser: user,
    };
  }, [user]);

  const viewerScope = useMemo(() => deriveScope(viewer, F), [viewer]);

  // If the viewer can see others (module/subteam scope), they can pick a specific person
  // to "view as". null = aggregated view across scope.
  const [viewingAsId, setViewingAsId] = useState(undefined); // undefined = not yet initialized; defaults to self in effect below

  // Compute visible owner ids
  const visibleOwnerIds = useMemo(
    () => resolveVisibleOwnerIds(viewer || { id: "" }, viewerScope, F, viewingAsId),
    [viewer, viewerScope, viewingAsId]
  );

  // Default the Action Dashboard to the viewer's OWN actions. Module heads / sub-team leads
  // can switch to "Everyone" or a specific person via the View-as picker.
  useEffect(() => {
    if (viewingAsId === undefined && viewer && viewer.id) setViewingAsId(viewer.id);
  }, [viewer, viewingAsId]);

  // Date range: current week (Mon) through next 2 weeks
  const [anchorDate] = useState(() => todayStr());
  const weekStart = useMemo(() => mondayOf(anchorDate), [anchorDate]);
  const rangeStart = useMemo(() => addDaysStr(weekStart, -28), [weekStart]); // include 4 wks of overdue
  const rangeEnd   = useMemo(() => addDaysStr(weekStart, 21), [weekStart]); // 3 weeks ahead

  // ---------- Data loading ----------
  const [userActions, setUserActions] = useState([]);
  const [pipelineActions, setPipelineActions] = useState([]);
  const [projectActions, setProjectActions] = useState([]);
  const [newClientActions, setNewClientActions] = useState([]);
  const [researchMailings, setResearchMailings] = useState([]);
  const [cadenceDone, setCadenceDone] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [refreshKey, setRefreshKey] = useState(0);
  const reload = useCallback(() => setRefreshKey(k => k + 1), []);
  // Soft reload: only blank the screen on the very first load. Subsequent
  // add/delete/complete refetches keep the current data visible (no white flash).
  const hydratedRef = React.useRef(false);

  useEffect(() => {
    if (!viewer || viewingAsId === undefined || visibleOwnerIds.length === 0) return;
    let cancelled = false;
    if (!hydratedRef.current) setLoading(true);
    setError(null);

    const fetchAll = async () => {
      try {
        // Translate visibleOwnerIds (Vault person IDs) -> emails via VAULT_ACCOUNTS.
        // A single Vault person may have MULTIPLE auth accounts (e.g., Brian has
        // both bscott@harveyllc.com and brscottuw@gmail.com -- both normalize to "bscott").
        // We need to include ALL their emails so we catch picks saved under any of them.
        const accounts = window.VAULT_ACCOUNTS || [];
        const visibleEmails = visibleOwnerIds.flatMap(pid => {
          return accounts
            .filter(a => {
              if (!a.personId) return false;
              const stripped = a.personId.replace(/^p_/, "").replace(/_gmail$/, "");
              return stripped === pid || a.personId === pid;
            })
            .map(a => a.email)
            .filter(Boolean);
        });

        // Personal / Pipeline picks / Project / NewClient actions in parallel.
        // Also pull pipeline rows so we can resolve harvey_id -> project name.
        // Scope the pipeline fetch to the viewer's module -- picks are module-scoped by RLS,
        // so firm-wide rows are never needed here (was a full-table fetch).
        const viewerModule = (viewer && (viewer.team || (viewer.subteam ? viewer.subteam.replace(/^st-/, "") : null))) || null;
        const [ua, ppicks, prja, nca, prows] = await Promise.all([
          window.VaultAPI.listUserActions(visibleOwnerIds, rangeStart, rangeEnd).catch(() => []),
          visibleEmails.length > 0
            ? window.VaultAPI.listPipelinePicksAsActions(visibleEmails, rangeStart, rangeEnd).catch(() => [])
            : Promise.resolve([]),
          window.VaultAPI.listProjectActions(visibleOwnerIds, rangeStart, rangeEnd).catch(() => []),
          window.VaultAPI.listNewClientActions(visibleOwnerIds, rangeStart, rangeEnd).catch(() => []),
          window.VaultAPI.getPipelineRows(viewerModule ? [viewerModule] : null, null, null, null).catch(() => []),
        ]);
        if (cancelled) return;
        setUserActions(ua);
        // Map pipeline picks back to person IDs + project names
        const emailToPersonId = new Map();
        accounts.forEach(a => {
          if (a.email && a.personId) {
            emailToPersonId.set(a.email.toLowerCase(),
              a.personId.replace(/^p_/, "").replace(/_gmail$/, ""));
          }
        });
        const pipelineRowsById = new Map();
        (prows || []).forEach(r => {
          if (r.harveyId != null) pipelineRowsById.set(Number(r.harveyId), r);
        });
        setPipelineActions(ppicks.map(p => {
          const row = pipelineRowsById.get(Number(p.harveyId));
          return {
            id: `pick:${p.harveyId}:${p.ownerEmail}`,
            ownerId: emailToPersonId.get((p.ownerEmail || "").toLowerCase()) || p.ownerEmail,
            harveyId: p.harveyId,
            projectName: row ? (row.target || row.buyer || "") : "",
            nextAction: p.nextAction || "",
            court: p.courtPerson || null,
            dueDate: p.dueDate,
            done: false,
            updatedAt: p.updatedAt,
          };
        }));
        setProjectActions(prja);
        setNewClientActions(nca);

        // Research: scan past 5 weeks of research_goals for completed mailings
        // (the cadence forecast feeds Actions page through these)
        const moduleOfViewer = viewer.team || (viewer.subteam ? viewer.subteam.replace(/^st-/, "") : null);
        if (moduleOfViewer) {
          const fetchPromises = [];
          for (let i = 0; i < 5; i++) {
            const wk = addDaysStr(weekStart, -i * 7);
            fetchPromises.push(window.VaultAPI.listResearchGoals(moduleOfViewer, wk));
          }
          const weekResults = await Promise.all(fetchPromises);
          if (cancelled) return;
          const allMailings = [];
          weekResults.forEach((rows, idx) => {
            const wk = addDaysStr(weekStart, -idx * 7);
            rows.forEach(r => {
              if (r.section === "mailings" && r.done && visibleOwnerIds.includes(r.ownerId)) {
                allMailings.push({ ...r, sentWeek: wk });
              }
            });
          });
          setResearchMailings(allMailings);

          // Now load done-tracking for those mailings
          if (allMailings.length > 0) {
            const done = await window.VaultAPI.listCadenceDone(allMailings.map(m => m.id))
              .catch(() => []);
            if (!cancelled) setCadenceDone(done);
          } else {
            setCadenceDone([]);
          }
        } else {
          setResearchMailings([]);
          setCadenceDone([]);
        }

        hydratedRef.current = true;
        setLoading(false);
      } catch (err) {
        if (cancelled) return;
        setError(err.message || String(err));
        setLoading(false);
      }
    };

    fetchAll();
    return () => { cancelled = true; };
  }, [JSON.stringify(visibleOwnerIds), rangeStart, rangeEnd, refreshKey, viewer]);

  // ---------- Synthesize cadence actions ----------
  // For each completed mailing, generate Analyst-owned cadence steps,
  // skipping those marked done in cadence_actions_done.
  const cadenceActions = useMemo(() => {
    const doneSet = new Set(cadenceDone.map(d => `${d.mailingId}:${d.stepCode}:${d.stepDay}`));
    const out = [];
    researchMailings.forEach(m => {
      const cycleKey = (m.cycle && OUTREACH_CYCLES[m.cycle]) ? m.cycle : "M1";
      const cycle = OUTREACH_CYCLES[cycleKey];
      const startDate = m.doneDate || m.sentWeek;
      cycle.steps.forEach(step => {
        if (step.owner !== "Analyst") return; // only show analyst-owned steps as actions
        const dueDate = addBusinessDays(startDate, step.day);
        const doneKey = `${m.id}:${step.code}:${step.day}`;
        out.push({
          type: "research",
          id: `cadence:${m.id}:${step.code}:${step.day}`,
          mailingId: m.id,
          stepCode: step.code,
          stepDay: step.day,
          stepType: step.type,
          cycle: cycleKey,
          projectName: m.projectName,
          ownerId: m.ownerId,
          dueDate,
          done: doneSet.has(doneKey),
          isCadence: true, // flag for the done-toggle handler
        });
      });
    });
    return out;
  }, [researchMailings, cadenceDone]);

  // ---------- Unify all actions ----------
  // Normalize every action source into a common shape:
  //   { type, id, ownerId, projectName, description, dueDate, done, isCadence?, ... }
  const allActions = useMemo(() => {
    const out = [];

    // Pipeline
    pipelineActions.forEach(a => {
      out.push({
        type: "pipeline",
        id: a.id,
        ownerId: a.ownerId,
        harveyId: a.harveyId,
        projectName: a.projectName || "(pipeline project)",
        description: a.nextAction || "(no action set)",
        court: a.court,
        dueDate: a.dueDate || todayStr(),
        done: a.done,
        sourceRow: a,
      });
    });

    // Research cadence + manual research goals
    cadenceActions.forEach(a => out.push(a));

    // New Client
    newClientActions.forEach(a => {
      if (!a.nextActionDate) return;
      out.push({
        type: "newclient",
        id: a.id,
        ownerId: a.owner,
        projectName: a.project,
        description: a.nextAction || a.notes || "(no description)",
        dueDate: a.nextActionDate,
        done: false, // new_clients doesn't have a per-action done flag; the next_action just gets replaced
        sourceRow: a,
      });
    });

    // Project Actions
    projectActions.forEach(a => {
      out.push({
        type: "project",
        id: a.id,
        ownerId: a.ownerId,
        projectName: a.projectName,
        description: a.nextAction || "(no description)",
        dueDate: a.dueDate || todayStr(),
        done: a.done,
        sourceRow: a,
      });
    });

    // Personal user actions
    userActions.forEach(a => {
      out.push({
        type: "personal",
        id: a.id,
        ownerId: a.ownerId,
        projectName: a.projectName,
        description: a.description,
        dueDate: a.dueDate,
        done: a.done,
        priority: a.priority,
        sourceRow: a,
      });
    });

    return out.sort((a, b) => a.dueDate.localeCompare(b.dueDate));
  }, [pipelineActions, cadenceActions, newClientActions, projectActions, userActions]);

  // ---------- Filtering UI state ----------
  const [showDone, setShowDone] = useState(false);
  const [typeFilter, setTypeFilter] = useState("all");

  const filteredActions = useMemo(() => {
    return allActions.filter(a => {
      if (!showDone && a.done) return false;
      if (typeFilter !== "all" && a.type !== typeFilter) return false;
      return true;
    });
  }, [allActions, showDone, typeFilter]);

  // ---------- Categorize by type for the new layout ----------
  const weekEnd = useMemo(() => addDaysStr(weekStart, 6), [weekStart]);
  const today = todayStr();

  // For each category, return sorted-by-date items, separated by status (overdue / this week / later)
  const byCategory = useMemo(() => {
    const out = {};
    Object.keys(ACTION_TYPES).forEach(key => { out[key] = { overdue: [], thisWeek: [], later: [], done: [] }; });
    filteredActions.forEach(a => {
      const bucket = out[a.type];
      if (!bucket) return;
      if (a.done) {
        bucket.done.push(a);
      } else if (a.dueDate < weekStart) {
        bucket.overdue.push(a);
      } else if (a.dueDate <= weekEnd) {
        bucket.thisWeek.push(a);
      } else {
        bucket.later.push(a);
      }
    });
    return out;
  }, [filteredActions, weekStart, weekEnd]);



  // Completed this week roll-up (across all categories)
  const completedThisWeek = useMemo(() => {
    return allActions.filter(a => a.done && a.dueDate >= weekStart && a.dueDate <= weekEnd)
      .sort((a, b) => b.dueDate.localeCompare(a.dueDate));
  }, [allActions, weekStart, weekEnd]);

  // ---- CRs (Custom Re-approaches) ----
  const [crs, setCrs] = useState([]);
  const [crModalOpen, setCrModalOpen] = useState(false);
  const [crRefresh, setCrRefresh] = useState(0);
  const [crOutcomeNote, setCrOutcomeNote] = useState("");
  async function saveCrOutcome() {
    const cr = crDoneReminder, txt = crOutcomeNote.trim();
    setCrDoneReminder(null); setCrOutcomeNote("");
    if (!cr || !txt) return;
    try {
      const merged = (cr.note ? cr.note + "\n" : "") + "Outcome: " + txt;
      await window.VaultAPI.updateCR(cr.id, { note: merged });
      setCrRefresh(x => x + 1);
    } catch (e) { window.VaultUI.toast("error", "Note not saved: " + (e.message || e)); }
  }
  const [crDoneReminder, setCrDoneReminder] = useState(null); // completed CR → reminder to update Harvey

  const crPeople = useMemo(() => {
    const my = viewer && (viewer.team || (viewer.subteam ? viewer.subteam.replace(/^st-/, "") : null));
    const list = (F.PEOPLE || []).filter(p => { const t = p.team || (p.subteam ? p.subteam.replace(/^st-/, "") : null); return t === my; });
    return list.length ? list : (viewer ? [viewer] : []);
  }, [viewer]);

  useEffect(() => {
    if (!viewer || viewingAsId === undefined || visibleOwnerIds.length === 0) { setCrs([]); return; }
    let alive = true;
    window.VaultAPI.listCRs(visibleOwnerIds, viewingAsId || viewer.id).then(r => { if (alive) setCrs(r || []); }).catch(() => {});
    return () => { alive = false; };
  }, [JSON.stringify(visibleOwnerIds), viewingAsId, viewer, crRefresh]);

  const crCanAct = (cr) => !!viewer && (viewer.id === cr.ownerId || viewerScope === "module");
  async function acceptCR(cr)   { try { await window.VaultAPI.updateCR(cr.id, { status: "accepted", acceptedAt: new Date().toISOString() }); setCrRefresh(x => x + 1); } catch (e) { window.VaultUI.toast("error", "Failed: " + (e.message || e)); } }
  async function completeCR(cr) { try { await window.VaultAPI.updateCR(cr.id, { status: "done", completedBy: viewer.id, completedAt: new Date().toISOString() }); setCrRefresh(x => x + 1); setCrDoneReminder(cr); } catch (e) { window.VaultUI.toast("error", "Failed: " + (e.message || e)); } }
  async function declineCR(cr)  { try { await window.VaultAPI.updateCR(cr.id, { status: "declined" }); setCrRefresh(x => x + 1); } catch (e) { window.VaultUI.toast("error", "Failed: " + (e.message || e)); } }
  async function reopenCR(cr)   { try { await window.VaultAPI.updateCR(cr.id, { status: "requested", acceptedAt: null, completedBy: null, completedAt: null }); setCrRefresh(x => x + 1); } catch (e) { window.VaultUI.toast("error", "Failed: " + (e.message || e)); } }
  async function importCRsFromFile(file) {
    if (!file) return;
    try {
      const text = await file.text();
      const rows = parseCSV(text);
      if (!rows.length) { window.VaultUI.toast("error", "No rows found in that CSV."); return; }
      const existingRows = await window.VaultAPI.listCRsForReconcile();
      const byHid = {}; existingRows.forEach(x => { byHid[x.harveyEventId] = x; });
      let created = 0, markedDone = 0, skipped = 0, errors = 0;
      const nowIso = new Date().toISOString();
      for (const r of rows) {
        const hid = String(r.harvey_event_id || "").trim();
        if (!hid) { errors++; continue; }
        const who = String(r.logged_by || "").trim();
        const ex = byHid[hid];
        // Harvey's `completed` flag is the source of truth: true = the re-approach is done.
        const isDone = /^(true|1|yes|t)$/i.test(String(r.completed || "").trim());
        if (isDone) {
          // Never sits in the queue as open. Mark an existing open one done; otherwise leave history out.
          if (ex && ex.status !== "done" && ex.status !== "declined") {
            try { await window.VaultAPI.updateCR(ex.id, { status: "done", completedAt: nowIso, completedBy: who || null }); markedDone++; }
            catch (e) { errors++; }
          } else { skipped++; }
          continue;
        }
        // Open CR (completed = false)
        if (ex) { const tid = String(r.target_id || "").trim(); if (tid) window.VaultAPI.backfillCRTarget(hid, tid); skipped++; continue; }
        try {
          await window.VaultAPI.createCR({
            target: r.target || "(untitled)",
            ownerId: who, requesterId: who,
            dueDate: String(r.cr_date || "").trim() || null,
            note: String(r.note || "").trim() || null,
            module: String(r.module || "").trim() || null,
            status: "requested", harveyEventId: hid,
            harveyTargetId: String(r.target_id || "").trim() || null,
          });
          byHid[hid] = { id: null, harveyEventId: hid, status: "requested" }; created++;
        } catch (e) { errors++; }
      }
      setCrRefresh(x => x + 1);
      window.VaultUI.toast("success", "Import — " + created + " new, " + markedDone + " marked done, " + skipped + " unchanged" + (errors ? ", " + errors + " error(s)" : "") + ".");
    } catch (e) { window.VaultUI.toast("error", "Import failed: " + (e.message || e)); }
  }

  // Done CRs are kept in the DB but never shown in the active queue.
  const visibleCrs = crs.filter(c => c.status !== "done");
  const hiddenDoneCount = crs.length - visibleCrs.length;

  // ---------- "Today" flattened view: one prioritized list across all sources ----------
  // View-mode state must be declared BEFORE the memos that depend on it.
  const [actionView, setActionView] = useState("todo"); // "todo" | "calendar" | "categories"

  // ---------- Calendar feed ("Events This Week") ----------
  const [calEvents, setCalEvents] = useState(null); // null=unloaded, []=none, [...]=events
  const [calStatus, setCalStatus] = useState("idle"); // idle | loading | ok | error | nofeed
  const [showCalSettings, setShowCalSettings] = useState(false);
  const [expandedDays, setExpandedDays] = useState({}); // { 'YYYY-MM-DD': true }
  const [selectedCalDay, setSelectedCalDay] = useState(null); // day selected on the Calendar grid
  // Click-to-highlight on calendar schedules: focused events stay vivid,
  // everything else on that schedule greys out. Click again to un-focus.
  const [calFocus, setCalFocus] = useState(() => new Set());
  const toggleCalFocus = (k) => setCalFocus(prev => {
    const next = new Set(prev);
    if (next.has(k)) next.delete(k); else next.add(k);
    return next;
  });
  const [calUrlInput, setCalUrlInput] = useState("");
  const calPersonId = viewer && viewer.id;

  const loadCalendar = useCallback(async () => {
    if (!calPersonId) return;
    setCalStatus("loading");
    try {
      const feed = await window.VaultAPI.getCalendarFeed(calPersonId);
      if (!feed || !feed.ics_url) { setCalStatus("nofeed"); setCalEvents(null); return; }
      setCalUrlInput(feed.ics_url);
      const events = await window.VaultAPI.fetchCalendarEvents(feed.ics_url);
      if (events === null) { setCalStatus("error"); setCalEvents(null); return; }
      setCalEvents(events); setCalStatus("ok");
    } catch (e) { setCalStatus("error"); }
  }, [calPersonId]);

  useEffect(() => { if (calPersonId) loadCalendar(); }, [calPersonId, loadCalendar]);

  async function saveCalUrl() {
    const url = calUrlInput.trim();
    const provider = /office365|outlook|microsoft/i.test(url) ? "outlook" : (/google|calendar\.google/i.test(url) ? "google" : "other");
    try {
      await window.VaultAPI.saveCalendarFeed(calPersonId, url, provider);
      setShowCalSettings(false);
      loadCalendar();
    } catch (e) { window.VaultUI.toast("error", "Couldn't save calendar URL: " + (e.message || e)); }
  }

  // events grouped by day within this week (Mon..Sun)
  const eventsThisWeek = useMemo(() => {
    if (!calEvents) return {};
    const out = {};
    calEvents.forEach(ev => {
      const d = ev.start && ev.start.date;
      if (!d || d < weekStart || d > weekEnd) return;
      (out[d] = out[d] || []).push(ev);
    });
    Object.values(out).forEach(list => list.sort((a, b) => (a.start.ts || 0) - (b.start.ts || 0)));
    return out;
  }, [calEvents, weekStart, weekEnd]);

  // events grouped by day across the next 2 weeks (for the Calendar tab)
  const eventsTwoWeeks = useMemo(() => {
    if (!calEvents) return [];
    const end = addDaysStr(today, 14);
    const byDay = {};
    calEvents.forEach(ev => {
      const d = ev.start && ev.start.date;
      if (!d || d < today || d > end) return;
      (byDay[d] = byDay[d] || []).push(ev);
    });
    return Object.keys(byDay).sort().map(d => ({
      date: d,
      events: byDay[d].sort((a, b) => (a.start.ts || 0) - (b.start.ts || 0)),
    }));
  }, [calEvents, today]);

  // Full 14-day grid (Mon-of-this-week .. +13) for the Calendar tab grid view; includes empty days.
  const calGrid = useMemo(() => {
    const byDay = {};
    (calEvents || []).forEach(ev => { const d = ev.start && ev.start.date; if (d) (byDay[d] = byDay[d] || []).push(ev); });
    const start = mondayOf(today);
    const cells = [];
    for (let i = 0; i < 14; i++) {
      const date = addDaysStr(start, i);
      const events = (byDay[date] || []).slice().sort((a, b) => (a.start.ts || 0) - (b.start.ts || 0));
      cells.push({ date, events });
    }
    return cells;
  }, [calEvents, today]);

  // To Do list: today's items first, then overdue (no this-week)
  const todoFlat = useMemo(() => {
    const d10 = (s) => { const str = String(s || "").trim(); if (/^\d{4}-\d{2}-\d{2}/.test(str)) return str.slice(0, 10); const us = str.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})/); if (us) return `${us[3]}-${us[1].padStart(2, "0")}-${us[2].padStart(2, "0")}`; const d = new Date(str); return isNaN(d) ? str : d.toISOString().slice(0, 10); };
    const active = filteredActions.filter(a => showDone ? true : !a.done);
    const today_ = active.filter(a => d10(a.dueDate) === today).map(a => ({ ...a, _bucket: "today" }));
    const overdue = active.filter(a => d10(a.dueDate) < today).map(a => ({ ...a, _bucket: "overdue" }))
      .sort((a, b) => d10(a.dueDate).localeCompare(d10(b.dueDate)));
    return { today: today_, overdue };
  }, [filteredActions, today, showDone]);

  // ---------- Forward-looking touchpoint forecast ("Days to Update Call") ----------
  // Parses FUTURE calendar events, classifies the touchpoint type, extracts the company,
  // and (for Update Calls) matches against real Vault clients to identify the project.
  const touchpoints = useMemo(() => {
    if (!calEvents) return [];
    const clients = (window.HARVEY_CLIENTS || []);
    // normalize a name for matching: lowercase, strip punctuation + common suffixes
    const norm = (s) => (s || "").toLowerCase()
      .replace(/\(.*?\)/g, " ")                       // drop parentheticals
      .replace(/\b(llc|inc|ltd|lp|corp|co|holdings?|group|company|companies|the)\b/g, " ")
      .replace(/[^a-z0-9 ]/g, " ").replace(/\s+/g, " ").trim();
    // build a lookup of normalized client name -> client record
    const clientIndex = clients.map(c => ({ rec: c, n: norm(c.client) })).filter(x => x.n.length >= 3);
    const matchClient = (raw) => {
      const n = norm(raw);
      if (n.length < 3) return null;
      // exact-ish: client name contains the calendar token or vice-versa
      let best = null, bestLen = 0;
      for (const { rec, n: cn } of clientIndex) {
        if (cn === n || cn.startsWith(n + " ") || n.startsWith(cn + " ") || cn.includes(" " + n + " ") || cn === n) {
          if (n.length > bestLen) { best = rec; bestLen = n.length; }
        }
      }
      if (best) return best;
      // looser: first significant token matches (e.g. "Select Oil" vs "Select Oil Tools")
      for (const { rec, n: cn } of clientIndex) {
        const a = n.split(" ")[0], b = cn.split(" ")[0];
        if (a.length >= 4 && a === b && Math.abs(cn.length - n.length) < 14) return rec;
      }
      return null;
    };
    const classify = (title) => {
      const t = (title || "").toLowerCase();
      if (/\bupdate call\b/.test(t)) return "update";
      if (/conference call|conf call/.test(t)) return "conf";
      if (/intro call/.test(t)) return "intro";
      if (/\bvisit|site visit|on-?site\b/.test(t)) return "visit";
      return null; // only forecast these touchpoint types
    };
    // pull the "A / B" company portion out of a title like "Update Call: A / B"
    const extractSides = (title) => {
      let s = (title || "").replace(/^\s*\d{1,2}:\d{2}\s*(am|pm)?\s*/i, "");      // strip leading time
      s = s.replace(/^(cc|cf|uc|ic)\s*[-–:]\s*/i, "");                            // strip code prefix
      s = s.replace(/^(update call|conference call|conf call|intro call|visit|site visit)\s*[:\-–]\s*/i, ""); // strip type label
      const parts = s.split("/").map(x => x.trim()).filter(Boolean);
      return parts;
    };
    const td = today;
    const out = [];
    calEvents.forEach(ev => {
      const type = classify(ev.title);
      if (!type) return;
      const d = ev.start && ev.start.date;
      if (!d || d < td) return; // forward-looking only
      const sides = extractSides(ev.title);
      if (sides.length === 0) return;
      let project = null, company = null, matched = null;
      if (type === "update") {
        // project = whichever side matches a real client (could be either side)
        for (const side of sides) { const m = matchClient(side); if (m) { matched = m; project = m.client; break; } }
        if (!project) project = sides[0]; // fall back to left side if no match
        company = project;
      } else {
        // intro/conf/visit: target company = LEFT side
        company = sides[0];
        matched = matchClient(company);
        project = matched ? matched.client : company;
      }
      const days = Math.round((new Date(d + "T00:00:00Z") - new Date(td + "T00:00:00Z")) / 86400000);
      const codeMap = { update: "Update Call", conf: "CC", intro: "Intro Call", visit: "Visit" };
      out.push({
        type, date: d, days,
        title: ev.title,
        label: type === "conf" ? `CC - ${company}` : (matched ? matched.client : company),
        sub: codeMap[type],
        matched: !!matched,
        leadId: matched ? matched.lead : null,
      });
    });
    // de-dupe to the soonest occurrence per project+type, then sort soonest-first
    const seen = {};
    const deduped = out.sort((a, b) => a.days - b.days).filter(x => {
      const k = x.type + "|" + x.label.toLowerCase();
      if (seen[k]) return false; seen[k] = true; return true;
    });
    return deduped;
  }, [calEvents, today]);

  // Unified chronological list: ported actions + calendar touchpoints, color-tinted by source.
  const unifiedActions = useMemo(() => {
    const d10 = (x) => { const t = String(x || "").trim(); if (/^\d{4}-\d{2}-\d{2}/.test(t)) return t.slice(0, 10); const u = t.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})/); if (u) return `${u[3]}-${u[1].padStart(2, "0")}-${u[2].padStart(2, "0")}`; const d = new Date(t); return isNaN(d) ? t : d.toISOString().slice(0, 10); };
    const ported = filteredActions.map(a => ({ ...a, _bucket: (!a.done && d10(a.dueDate) < today) ? "overdue" : "active" }));
    const cal = (typeFilter === "all" ? (touchpoints || []) : []).map((tp, i) => ({
      type: tp.type, id: `cal:${tp.type}:${tp.date}:${i}`, dueDate: tp.date,
      description: tp.label, subLabel: tp.title, isCalendar: true, matched: tp.matched, done: false, _bucket: "active",
    }));
    return [...ported, ...cal].sort((a, b) => { if (a.done !== b.done) return a.done ? 1 : -1; return d10(a.dueDate).localeCompare(d10(b.dueDate)); });
  }, [filteredActions, touchpoints, typeFilter, today]);
  const [todayBucketFilter, setTodayBucketFilter] = useState("all"); // retained for compatibility
  // Priority: overdue (oldest first) → due today → rest of this week. Ignores category.
  const todayFlat = useMemo(() => {
    const active = filteredActions.filter(a => showDone ? true : !a.done);
    // normalize ANY date format to YYYY-MM-DD: ISO datetime, plain ISO, or US MM/DD/YYYY
    const d10 = (s) => {
      const str = String(s || "").trim();
      if (/^\d{4}-\d{2}-\d{2}/.test(str)) return str.slice(0, 10);
      const us = str.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})/);
      if (us) return `${us[3]}-${us[1].padStart(2, "0")}-${us[2].padStart(2, "0")}`;
      const d = new Date(str);
      return isNaN(d) ? str : d.toISOString().slice(0, 10);
    };
    const bucketOf = (a) => { const dd = d10(a.dueDate); return dd < today ? "overdue" : (dd === today ? "today" : (dd <= weekEnd ? "week" : "later")); };
    const rank = { overdue: 0, today: 1, week: 2, later: 3 };
    const mapped = active.map(a => ({ ...a, _bucket: bucketOf(a) }));
    return mapped.filter(a => a._bucket !== "later")
      .sort((a, b) => (rank[a._bucket] - rank[b._bucket]) || d10(a.dueDate).localeCompare(d10(b.dueDate)));
  }, [filteredActions, today, weekEnd, showDone]);

  const todayVisible = useMemo(() => {
    return (todayBucketFilter === "all") ? todayFlat : todayFlat.filter(a => a._bucket === todayBucketFilter);
  }, [todayFlat, todayBucketFilter]);

  // Daily progress: of everything that was due by end of today (done + still-active overdue/today),
  // how many are done? Drives the progress ring + "all clear" celebration.
  const dailyProgress = useMemo(() => {
    const dueByToday = allActions.filter(a => a.dueDate <= today && (a.type !== "newclient")); // newclient toggles elsewhere
    const done = dueByToday.filter(a => a.done).length;
    const total = dueByToday.length;
    return { done, total, pct: total ? Math.round((done / total) * 100) : 100, remaining: total - done };
  }, [allActions, today]);

  const [completedExpanded, setCompletedExpanded] = useState(false);

  // ---------- Personal action CRUD ----------
  const [showAddPersonal, setShowAddPersonal] = useState(false);
  const [editingPersonal, setEditingPersonal] = useState(null); // {id, description, dueDate, ...} or null

  const togglePersonalDone = useCallback(async (action) => {
    const newDone = !action.done;
    try {
      await window.VaultAPI.updateUserAction(action.id, {
        done: newDone,
        doneDate: newDone ? todayStr() : null,
      });
      reload();
    } catch (err) { window.VaultUI.toast("error", "Failed to update: " + (err.message || err)); }
  }, [reload]);

  const togglePipelineDone = useCallback(async (action) => {
    // Pipeline picks don\'t have an explicit "done" flag. Treating "done" = clear my pick
    // (the action goes away, freeing the row to either be re-assigned or remain blank).
    if (!(await window.VaultUI.confirm({ message: "Mark this Pipeline action as done? It will be cleared from your pipeline picks.", confirmLabel: "Mark done" }))) return;
    try {
      await window.VaultAPI.upsertPipelinePick(action.harveyId, null, null, null);
      reload();
    } catch (err) { window.VaultUI.toast("error", "Failed to update: " + (err.message || err)); }
  }, [reload]);

  const toggleProjectDone = useCallback(async (action) => {
    const newDone = !action.done;
    try {
      await window.VaultAPI.updateProjectAction(action.id, {
        done: newDone, doneDate: newDone ? todayStr() : null,
      });
      reload();
    } catch (err) { window.VaultUI.toast("error", "Failed to update: " + (err.message || err)); }
  }, [reload]);

  const toggleCadenceDone = useCallback(async (action) => {
    try {
      if (action.done) {
        await window.VaultAPI.unmarkCadenceDone(action.mailingId, action.stepCode, action.stepDay);
      } else {
        await window.VaultAPI.markCadenceDone(action.mailingId, action.stepCode, action.stepDay, viewer?.id);
      }
      reload();
    } catch (err) { window.VaultUI.toast("error", "Failed to update: " + (err.message || err)); }
  }, [reload, viewer]);

  const [completingNC, setCompletingNC] = useState(null); // New Client action being completed (opens "set new action" modal)
  const [completingPipeline, setCompletingPipeline] = useState(null); // Pipeline action being completed (opens "set new action" modal)

  // Universal handler
  const toggleDone = useCallback((action) => {
    switch (action.type) {
      case "personal": return togglePersonalDone(action);
      case "pipeline": setCompletingPipeline(action); return;
      case "project":  return toggleProjectDone(action);
      case "research": return toggleCadenceDone(action);
      case "newclient":
        setCompletingNC(action);
        return;
    }
  }, [togglePersonalDone, togglePipelineDone, toggleProjectDone, toggleCadenceDone]);

  const [pendingDelete, setPendingDelete] = useState(null); // {id, description} or null
  const deletePersonal = useCallback((id, description) => {
    setPendingDelete({ id, description: description || "this action" });
  }, []);
  const confirmDelete = useCallback(async () => {
    if (!pendingDelete) return;
    const id = pendingDelete.id;
    setPendingDelete(null);
    try {
      await window.VaultAPI.deleteUserAction(id);
      reload();
    } catch (err) { setPendingDelete({ id, error: "Failed to delete: " + (err.message || err) }); }
  }, [pendingDelete, reload]);

  // Open the edit modal for a personal action. Pulls the source row from the unified action.
  const editPersonal = useCallback((action) => {
    if (action.type !== "personal") return;
    setEditingPersonal(action.sourceRow);
  }, []);

  // Build people picker source for "view as"
  const peoplePickerList = useMemo(() => {
    const ids = resolveVisibleOwnerIds(viewer || { id: "" }, viewerScope, F, null);
    const list = ids.map(id => PEOPLE_BY_ID[id] || (id === viewer?.id ? viewer : null)).filter(Boolean);
    // Always ensure the viewer themselves is in the list, even if their id doesn\'t resolve in PEOPLE_BY_ID
    if (viewer && viewer.id && !list.some(p => p.id === viewer.id)) {
      list.unshift(viewer);
    }
    return list.sort((a, b) => (a.name || "").localeCompare(b.name || ""));
  }, [viewer, viewerScope]);

  // ---------- Render ----------
  if (!viewer) {
    return (
      <div className="card card-pad" style={{ padding: 40, textAlign: "center", color: "var(--muted)" }}>
        Loading user context…
      </div>
    );
  }

  const totalCount = filteredActions.length;

  return (
    <div style={{ padding: "20px 28px 60px" }}>
      {/* Header (PageToolbar) */}
      <window.PageToolbar
        title={"Actions — " + (viewingAsId && viewingAsId !== viewer.id
          ? "Viewing as " + (PEOPLE_BY_ID[viewingAsId]?.name || viewingAsId)
          : viewer.name)}
        subtitle={"Scope: " + viewerScope + " · Week of " + formatShortDate(weekStart) + " – " + formatShortDate(weekEnd)
          + (viewingAsId && viewingAsId !== viewer.id ? " · read-only view" : "")}>
        <div className="row" style={{ gap: 10, alignItems: "center", flexWrap: "wrap" }}>
          {/* Person picker (only if scope is broader than own) */}
          {viewerScope !== "own" && peoplePickerList.length > 1 && (
            <div className="row" style={{ gap: 6, alignItems: "center" }}>
              <span className="muted small">View as:</span>
              <select value={(viewingAsId === undefined ? (viewer && viewer.id) : viewingAsId) || ""}
                onChange={e => setViewingAsId(e.target.value || null)}
                style={{ padding: "5px 8px", border: "1px solid var(--line-2)", borderRadius: 4, fontFamily: "inherit", fontSize: 12, background: "var(--surface)", color: "var(--ink)" }}>
                <option value="">Everyone ({peoplePickerList.length})</option>
                {peoplePickerList.map(p => (
                  <option key={p.id} value={p.id}>{p.name}</option>
                ))}
              </select>
            </div>
          )}
          {/* Type filter */}
          <div className="row" style={{ gap: 6, alignItems: "center" }}>
            <span className="muted small">Type:</span>
            <select value={typeFilter}
              onChange={e => setTypeFilter(e.target.value)}
              style={{ padding: "5px 8px", border: "1px solid var(--line-2)", borderRadius: 4, fontFamily: "inherit", fontSize: 12, background: "var(--surface)", color: "var(--ink)" }}>
              <option value="all">All types</option>
              {Object.entries(ACTION_TYPES).map(([key, def]) => (
                <option key={key} value={key}>{def.label}</option>
              ))}
            </select>
          </div>
          {/* Show done toggle */}
          <label style={{ display: "flex", gap: 5, alignItems: "center", fontSize: 12 }}>
            <input type="checkbox" checked={showDone} onChange={e => setShowDone(e.target.checked)}/>
            <span className="muted small">Show done</span>
          </label>
          {/* Add personal action */}
          <button className="btn primary small" onClick={() => setShowAddPersonal(true)}>
            + Add Action
          </button>
        </div>
      </window.PageToolbar>

      {/* Progress + clickable summary */}
      {!loading && !error && (() => {
        const d10 = (s) => { const str = String(s || "").trim(); if (/^\d{4}-\d{2}-\d{2}/.test(str)) return str.slice(0, 10); const us = str.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})/); if (us) return `${us[3]}-${us[1].padStart(2, "0")}-${us[2].padStart(2, "0")}`; const d = new Date(str); return isNaN(d) ? str : d.toISOString().slice(0, 10); };
        const act = filteredActions.filter(a => !a.done);
        const activeCount = act.length;
        const overdueCount = act.filter(a => d10(a.dueDate) < today).length;
        const todayCount = act.filter(a => d10(a.dueDate) === today).length;
        const weekCount = act.filter(a => { const dd = d10(a.dueDate); return dd > today && dd <= weekEnd; }).length;
        const ring = (() => {
          const r = 26, c = 2 * Math.PI * r, off = c * (1 - dailyProgress.pct / 100);
          const color = dailyProgress.pct >= 100 ? "#16a34a" : "var(--accent)";
          return (
            <svg width="64" height="64" viewBox="0 0 64 64" style={{ flexShrink: 0 }}>
              <circle cx="32" cy="32" r={r} fill="none" stroke="var(--line-2)" strokeWidth="6"/>
              <circle cx="32" cy="32" r={r} fill="none" stroke={color} strokeWidth="6" strokeLinecap="round"
                strokeDasharray={c} strokeDashoffset={off} transform="rotate(-90 32 32)" style={{ transition: "stroke-dashoffset .5s ease" }}/>
              <text x="32" y="36" textAnchor="middle" fontSize="15" fontWeight="700" fill="var(--ink)">{dailyProgress.pct}%</text>
            </svg>
          );
        })();
        const Stat = ({ label, value, color }) => (
          <div className="action-stat" style={{ display: "flex", flexDirection: "column", gap: 2, padding: "6px 14px", minWidth: 70 }}>
            <span style={{ fontSize: 22, fontWeight: 700, color, lineHeight: 1 }}>{value}</span>
            <span style={{ fontSize: 11, color: "var(--muted)", fontWeight: 600 }}>{label}</span>
          </div>
        );
        return (
          <div className="card" style={{ padding: 16, marginBottom: 16, display: "flex", alignItems: "center", gap: 20, flexWrap: "wrap" }}>
            {ring}
            <div style={{ flex: 1, minWidth: 0 }}>
              <div style={{ fontSize: 14, fontWeight: 600, color: "var(--ink)", marginBottom: 2 }}>
                {dailyProgress.remaining <= 0 ? "All clear — you're caught up 🎉" : `${dailyProgress.remaining} to clear today`}
              </div>
              <div style={{ fontSize: 12, color: "var(--muted)" }}>{dailyProgress.done} of {dailyProgress.total} done</div>
            </div>
            <div className="action-stats" style={{ display: "flex", gap: 4, flexWrap: "wrap" }}>
              <Stat label="Overdue" value={overdueCount} color="var(--risk)"/>
              <Stat label="Today" value={todayCount} color="var(--accent)"/>
              <Stat label="This Week" value={weekCount} color="#6366f1"/>
              <Stat label="Done (wk)" value={completedThisWeek.length} color="#16a34a"/>
            </div>
          </div>
        );
      })()}

      {/* View toggle: Today (flattened) vs By Category */}
      {!loading && !error && (
        <div style={{ display: "inline-flex", gap: 2, background: "var(--surface-2)", borderRadius: 9, padding: 3, marginBottom: 16 }}>
          {[["todo", "To Do"], ["calendar", "Calendar"], ["categories", "Action Items"], ["crs", "CR Queue"]].map(([k, lbl]) => (
            <button key={k} onClick={() => { setActionView(k); if (k === "todo") setTodayBucketFilter("all"); }} style={{ padding: "7px 18px", borderRadius: 7, border: "none", cursor: "pointer", fontSize: 13, fontWeight: 600, background: actionView === k ? "var(--surface)" : "transparent", color: actionView === k ? "var(--accent)" : "var(--muted)", boxShadow: actionView === k ? "0 1px 2px rgba(0,0,0,.06)" : "none" }}>{lbl}</button>
          ))}
        </div>
      )}

      {loading && <div className="card card-pad muted" style={{ textAlign: "center", padding: 40 }}>Loading actions…</div>}
      {error && <div className="card card-pad" style={{ textAlign: "center", padding: 40, color: "var(--risk)" }}>Error: {error}</div>}

      {/* TODAY'S CALENDAR — single-day slice on the To Do tab */}
      {!loading && !error && actionView === "todo" && (
        <div className="card" style={{ marginBottom: 16, overflow: "hidden" }}>
          <div className="row" style={{ padding: "11px 16px", justifyContent: "space-between", alignItems: "center", borderBottom: "1px solid var(--line-2)" }}>
            <div className="row" style={{ gap: 8, alignItems: "center" }}>
              <strong style={{ fontSize: 13.5, color: "var(--ink)" }}>Today's Calendar</strong>
              {calFocus.size > 0 && (
                <button onClick={() => setCalFocus(new Set())} className="btn ghost small" style={{ fontSize: 11.5 }}>Clear highlights</button>
              )}
              {calStatus === "ok" && <span className="muted small">· {new Date(today + "T00:00:00Z").toLocaleDateString("en-US", { weekday: "long", month: "short", day: "numeric", timeZone: "UTC" })}</span>}
            </div>
            <button onClick={() => setShowCalSettings(true)} className="btn ghost small" style={{ fontSize: 12 }}>
              {calStatus === "nofeed" ? "+ Connect calendar" : "Calendar settings"}
            </button>
          </div>
          <div style={{ padding: "12px 16px" }}>
            {calStatus === "loading" && <div className="muted small">Loading your calendar…</div>}
            {calStatus === "nofeed" && <div className="muted small">Connect your Outlook or Google calendar to see today's meetings alongside your actions.</div>}
            {calStatus === "error" && (
              <div className="small" style={{ color: "var(--ink-2)" }}>
                <span style={{ color: "var(--risk)", fontWeight: 600 }}>Couldn't load this calendar feed.</span> Check the URL in <button onClick={() => setShowCalSettings(true)} style={{ border: "none", background: "transparent", color: "var(--accent)", cursor: "pointer", padding: 0, fontSize: "inherit", textDecoration: "underline" }}>calendar settings</button>.
              </div>
            )}
            {calStatus === "ok" && (() => {
              const evs = (eventsThisWeek[today] || []);
              if (evs.length === 0) return <div style={{ fontSize: 12.5, color: "var(--muted)" }}>No calendar events today.</div>;
              return (
                <div style={{ display: "flex", flexDirection: "column", gap: 7 }}>
                  {evs.map((ev, i) => {
                    const c = classifyEvent(ev.title);
                    const k = today + "#" + i;
                    const dim = calFocus.size > 0 && !calFocus.has(k);
                    return (
                      <div key={i} title={ev.title} onClick={() => toggleCalFocus(k)}
                        style={{ display: "flex", alignItems: "baseline", gap: 12, borderLeft: `3px solid ${c.color}`, padding: "2px 4px 2px 10px", cursor: "pointer", borderRadius: 6, opacity: dim ? 0.28 : 1, filter: dim ? "grayscale(1)" : "none", background: calFocus.has(k) ? "rgba(25,110,167,.07)" : "transparent", transition: "opacity .15s ease" }}>
                        <span style={{ fontSize: 12, fontWeight: 700, color: c.color, minWidth: 64, flexShrink: 0 }}>
                          {!ev.start.allDay && ev.start.iso ? new Date(ev.start.iso).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" }) : "All day"}
                        </span>
                        <span style={{ fontSize: 13, color: "var(--ink)" }}>{ev.title || "(busy)"}</span>
                      </div>
                    );
                  })}
                </div>
              );
            })()}
          </div>
        </div>
      )}

      {/* CALENDAR TAB — 2-week agenda */}
      {!loading && !error && actionView === "calendar" && (
        <div className="card" style={{ marginBottom: 16, overflow: "hidden" }}>
          <div className="row" style={{ padding: "11px 16px", justifyContent: "space-between", alignItems: "center", borderBottom: "1px solid var(--line-2)" }}>
            <div className="row" style={{ gap: 8, alignItems: "center" }}>
              <strong style={{ fontSize: 13.5, color: "var(--ink)" }}>Next 2 Weeks</strong>
              {calStatus === "ok" && <span className="muted small">· from your calendar</span>}
            </div>
            <div className="row" style={{ gap: 8, alignItems: "center" }}>
              {calStatus === "ok" && selectedCalDay && selectedCalDay !== today && (
                <button onClick={() => setSelectedCalDay(null)} className="btn ghost small" style={{ fontSize: 12 }}>Jump to today</button>
              )}
              <button onClick={() => setShowCalSettings(true)} className="btn ghost small" style={{ fontSize: 12 }}>
                {calStatus === "nofeed" ? "+ Connect calendar" : "Calendar settings"}
              </button>
            </div>
          </div>
          <div style={{ padding: "8px 16px 12px" }}>
            {calStatus === "loading" && <div className="muted small" style={{ padding: "8px 0" }}>Loading your calendar…</div>}
            {calStatus === "nofeed" && <div className="muted small" style={{ padding: "8px 0" }}>Connect your Outlook or Google calendar to see the next two weeks of meetings.</div>}
            {calStatus === "error" && (
              <div className="small" style={{ color: "var(--ink-2)", padding: "8px 0" }}>
                <span style={{ color: "var(--risk)", fontWeight: 600 }}>Couldn't load this calendar feed.</span> Check the URL in <button onClick={() => setShowCalSettings(true)} style={{ border: "none", background: "transparent", color: "var(--accent)", cursor: "pointer", padding: 0, fontSize: "inherit", textDecoration: "underline" }}>calendar settings</button>.
              </div>
            )}
            {calStatus === "ok" && (() => {
              const dayShown = selectedCalDay || today;
              const dayCell = calGrid.find(c => c.date === dayShown) || { date: dayShown, events: [] };
              const dObj = new Date(dayShown + "T00:00:00Z");
              return (
              <div>
                <div style={{ display: "grid", gridTemplateColumns: "repeat(7, 1fr)", gap: 6 }}>
                  {["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"].map(w => (
                    <div key={w} style={{ textAlign: "center", fontSize: 10.5, fontWeight: 700, color: "var(--muted)", textTransform: "uppercase", letterSpacing: ".04em", paddingBottom: 4 }}>{w}</div>
                  ))}
                  {calGrid.map(cell => {
                    const cObj = new Date(cell.date + "T00:00:00Z");
                    const isToday = cell.date === today;
                    const isSel = cell.date === dayShown;
                    const isPast = cell.date < today;
                    const dots = cell.events.slice(0, 4).map(ev => classifyEvent(ev.title).color);
                    return (
                      <button key={cell.date} onClick={() => setSelectedCalDay(cell.date)}
                        style={{ textAlign: "left", border: `1px solid ${isSel ? "var(--accent)" : "var(--line-2)"}`, boxShadow: isSel ? "0 0 0 1px var(--accent)" : "none", borderRadius: 9, background: isToday ? "rgba(25,110,167,.07)" : "#fff", padding: "7px 8px", minHeight: 64, cursor: "pointer", opacity: isPast ? 0.5 : 1, display: "flex", flexDirection: "column", gap: 5 }}>
                        <div style={{ fontSize: 13, fontWeight: 700, color: isToday ? "var(--accent)" : "var(--ink)" }}>{cObj.getUTCDate()}</div>
                        {cell.events.length === 0 ? (
                          <span style={{ fontSize: 10, color: "var(--muted)" }}>&nbsp;</span>
                        ) : (
                          <div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
                            <div style={{ display: "flex", gap: 3, flexWrap: "wrap" }}>
                              {dots.map((col, i) => <span key={i} style={{ width: 6, height: 6, borderRadius: "50%", background: col }}/>)}
                            </div>
                            <span style={{ fontSize: 10, color: "var(--muted)", fontWeight: 600 }}>{cell.events.length} event{cell.events.length !== 1 ? "s" : ""}</span>
                          </div>
                        )}
                      </button>
                    );
                  })}
                </div>

                {/* Day schedule — follows whichever day is selected on the grid */}
                <div style={{ marginTop: 14, borderTop: "1px solid var(--line-2)", paddingTop: 10 }}>
                  <div className="row" style={{ alignItems: "center", gap: 10, paddingBottom: 10 }}>
                    <div style={{ fontSize: 13.5, fontWeight: 700, color: "var(--ink)" }}>
                      {dObj.toLocaleDateString("en-US", { weekday: "long", month: "long", day: "numeric", timeZone: "UTC" })}
                    </div>
                    {calFocus.size > 0 && (
                      <button onClick={() => setCalFocus(new Set())} className="btn ghost small" style={{ fontSize: 11.5 }}>Clear highlights</button>
                    )}
                  </div>
                  {dayCell.events.length === 0 ? (
                    <div className="muted small" style={{ padding: "2px 0 6px" }}>No events scheduled this day.</div>
                  ) : (
                    <div style={{ display: "flex", flexDirection: "column", gap: 7 }}>
                      {dayCell.events.map((ev, i) => {
                        const c = classifyEvent(ev.title);
                        const k = dayShown + "#" + i;
                        const dim = calFocus.size > 0 && !calFocus.has(k);
                        return (
                          <div key={i} title={ev.title} onClick={() => toggleCalFocus(k)}
                            style={{ display: "flex", alignItems: "baseline", gap: 10, borderLeft: `3px solid ${c.color}`, padding: "2px 4px 2px 9px", cursor: "pointer", borderRadius: 6, opacity: dim ? 0.28 : 1, filter: dim ? "grayscale(1)" : "none", background: calFocus.has(k) ? "rgba(25,110,167,.07)" : "transparent", transition: "opacity .15s ease" }}>
                            <span style={{ fontSize: 11.5, fontWeight: 700, color: c.color, minWidth: 58, flexShrink: 0 }}>
                              {!ev.start.allDay && ev.start.iso ? new Date(ev.start.iso).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" }) : "All day"}
                            </span>
                            <span style={{ fontSize: 12.5, color: "var(--ink)" }}>{ev.title || "(busy)"}</span>
                          </div>
                        );
                      })}
                    </div>
                  )}
                </div>
              </div>
              );
            })()}
          </div>
        </div>
      )}

      {/* UPCOMING TOUCHPOINTS — forward-looking countdown from the calendar */}
      {!loading && !error && actionView === "calendar" && calStatus === "ok" && touchpoints.some(tp => tp.type === "update") && (
        <div className="card" style={{ marginBottom: 16, overflow: "hidden" }}>
          <div style={{ padding: "11px 16px", borderBottom: "1px solid var(--line-2)" }}>
            <strong style={{ fontSize: 13.5, color: "var(--ink)" }}>Update Call Cadence</strong>
            <span className="muted small" style={{ marginLeft: 8 }}>· next update calls from your calendar</span>
          </div>
          <div style={{ padding: "6px 0" }}>
            {touchpoints.filter(tp => tp.type === "update").slice(0, 12).map((tp, i) => {
              const tColor = { update: "#2563eb", conf: "#7c3aed", intro: "#0891b2", visit: "#ea580c" }[tp.type] || "#64748b";
              const dayLabel = tp.days === 0 ? "Today" : tp.days === 1 ? "Tomorrow" : `${tp.days} days`;
              const F = window.VAULT_FIRM;
              const lead = tp.leadId && F.PEOPLE_BY_ID ? F.PEOPLE_BY_ID[tp.leadId] : null;
              return (
                <div key={i} style={{ display: "flex", alignItems: "center", gap: 12, padding: "9px 16px", borderTop: i ? "1px solid var(--line-2)" : "none" }}>
                  <span style={{ fontSize: 9.5, fontWeight: 700, padding: "3px 7px", borderRadius: 5, background: tColor + "1a", color: tColor, minWidth: 70, textAlign: "center", textTransform: "uppercase", letterSpacing: ".03em" }}>{tp.sub}</span>
                  <div style={{ flex: "1 1 auto", minWidth: 0 }}>
                    <div style={{ fontSize: 13.5, fontWeight: 600, color: "var(--ink)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
                      {tp.label}
                      {!tp.matched && <span title="Not matched to a Vault client" style={{ marginLeft: 6, fontSize: 10, color: "var(--muted)", fontWeight: 400 }}>(unmatched)</span>}
                    </div>
                    <div style={{ fontSize: 11, color: "var(--muted)" }}>{tp.title}</div>
                  </div>
                  <div style={{ textAlign: "right", flexShrink: 0 }}>
                    <div style={{ fontSize: 15, fontWeight: 700, color: tp.days <= 2 ? tColor : "var(--ink)" }}>{dayLabel}</div>
                    <div style={{ fontSize: 10.5, color: "var(--muted)" }}>{new Date(tp.date + "T00:00:00Z").toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC" })}</div>
                  </div>
                </div>
              );
            })}
          </div>
        </div>
      )}

      {/* TO DO — today's actions first, then overdue */}
      {!loading && !error && actionView === "todo" && (
        (todoFlat.today.length + todoFlat.overdue.length) === 0 ? (
          <div className="card" style={{ padding: "48px 20px", textAlign: "center" }}>
            <div style={{ fontSize: 40, marginBottom: 8 }}>✓</div>
            <div style={{ fontSize: 16, fontWeight: 600, color: "var(--ink)" }}>All clear</div>
            <div style={{ fontSize: 13, color: "var(--muted)", marginTop: 4 }}>Nothing due today and nothing overdue.</div>
          </div>
        ) : (
          [
            { key: "today", label: "Today", color: "var(--accent)", items: todoFlat.today },
            { key: "overdue", label: "Overdue", color: "var(--risk)", items: todoFlat.overdue },
          ].map(grp => {
            if (grp.items.length === 0) return null;
            return (
              <div key={grp.key} style={{ marginBottom: 16 }}>
                <div style={{ display: "flex", alignItems: "center", gap: 8, padding: "0 4px 8px" }}>
                  <span style={{ width: 8, height: 8, borderRadius: "50%", background: grp.color }}/>
                  <span style={{ fontSize: 12.5, fontWeight: 700, color: grp.color, textTransform: "uppercase", letterSpacing: ".04em" }}>{grp.label}</span>
                  <span style={{ fontSize: 12, color: "var(--muted)", fontWeight: 600 }}>{grp.items.length}</span>
                </div>
                <div className="card" style={{ overflow: "hidden" }}>
                  {grp.items.map((a, i) => (
                    <TodayRow key={`${a.type}-${a.id}-${i}`} action={a}
                      toggleDone={toggleDone} deletePersonal={deletePersonal} editPersonal={editPersonal}
                      isReadOnly={!!(viewingAsId && viewingAsId !== viewer.id)} today={today} weekStart={weekStart}/>
                  ))}
                </div>
              </div>
            );
          })
        )
      )}

      {!loading && !error && actionView === "categories" && (
        <>
          {/* Unified chronological list across all sources, color-tinted by source */}
          {unifiedActions.length === 0 ? (
            <div className="card" style={{ padding: "40px 20px", textAlign: "center" }}>
              <div style={{ fontSize: 15, fontWeight: 600, color: "var(--ink)" }}>No action items</div>
              <div style={{ fontSize: 13, color: "var(--muted)", marginTop: 4 }}>Nothing scheduled in range.</div>
            </div>
          ) : (
            <div className="card" style={{ overflow: "hidden" }}>
              {unifiedActions.map((a, i) => (
                <TodayRow key={`${a.type}-${a.id}-${i}`} action={a} tinted
                  toggleDone={toggleDone} deletePersonal={deletePersonal} editPersonal={editPersonal}
                  isReadOnly={!!(viewingAsId && viewingAsId !== viewer.id)} today={today} weekStart={weekStart}/>
              ))}
            </div>
          )}

          {completedThisWeek.length > 0 && (
            <div className="card" style={{ marginTop: 20, overflow: "hidden" }}>
              <div className="row" onClick={() => setCompletedExpanded(!completedExpanded)}
                style={{ padding: "10px 14px", borderBottom: completedExpanded ? "1px solid var(--line-2)" : "none", justifyContent: "space-between", alignItems: "center", cursor: "pointer", userSelect: "none" }}>
                <div>
                  <strong style={{ fontSize: 13, color: "var(--good, #16a34a)" }}>✓ Completed This Week</strong>
                  <span className="muted small" style={{ marginLeft: 8 }}>{completedThisWeek.length} done</span>
                </div>
                <span className="muted small">{completedExpanded ? "Hide" : "Show"}</span>
              </div>
              {completedExpanded && (
                <div>
                  {completedThisWeek.map(a => (
                    <ActionRow key={a.id} action={a}
                      toggleDone={toggleDone}
                      deletePersonal={deletePersonal}
                      editPersonal={editPersonal}
                      isReadOnly={viewingAsId && viewingAsId !== viewer.id}
                      showDate={true}
                    />
                  ))}
                </div>
              )}
            </div>
          )}

          {/* Empty state */}
          {totalCount === 0 && completedThisWeek.length === 0 && (
            <div className="card card-pad" style={{ padding: 40, textAlign: "center", color: "var(--muted)" }}>
              <strong style={{ fontSize: 14 }}>No actions yet</strong>
              <div style={{ marginTop: 8 }}>
                Use the + Add Action button to add your first to-do, or check back after creating
                pipeline/project actions or completing research mailings to generate cadence steps.
              </div>
            </div>
          )}
        </>
      )}

      {!loading && !error && actionView === "crs" && (
        <>
          <div className="row" style={{ justifyContent: "space-between", alignItems: "center", marginBottom: 12 }}>
            <div className="muted small">{visibleCrs.length} active CR{visibleCrs.length !== 1 ? "s" : ""} · Requested → Accepted{hiddenDoneCount ? " · " + hiddenDoneCount + " done (hidden)" : ""}</div>
            <div className="row" style={{ gap: 8 }}>
              <button className="btn" onClick={() => setCrModalOpen(true)} style={{ fontSize: 12 }}>+ New CR</button>
            </div>
          </div>
          {visibleCrs.length === 0 ? (
            <div className="card" style={{ padding: "40px 20px", textAlign: "center" }}>
              <div style={{ fontSize: 15, fontWeight: 600, color: "var(--ink)" }}>No CRs</div>
              <div style={{ fontSize: 13, color: "var(--muted)", marginTop: 4 }}>Use “+ New CR” to assign a custom re-approach.</div>
            </div>
          ) : (
            <div className="card" style={{ overflow: "hidden" }}>
              {visibleCrs.map(cr => {
                const assignee = PEOPLE_BY_ID[cr.ownerId];
                const requester = PEOPLE_BY_ID[cr.requesterId];
                const isOpen = cr.status !== "done" && cr.status !== "declined";
                const overdue = isOpen && cr.dueDate && cr.dueDate < today;
                const stColor = cr.status === "done" ? "#16a34a" : cr.status === "declined" ? "#dc2626" : cr.status === "accepted" ? "#2563eb" : "#d4a017";
                const stLabel = cr.status === "done" ? "Done" : cr.status === "declined" ? "Declined" : cr.status === "accepted" ? "Accepted" : "Requested";
                const mine = !!viewer && viewer.id === cr.ownerId;
                const canManage = mine || viewerScope === "module";
                return (
                  <div key={cr.id} style={{ display: "flex", alignItems: "center", gap: 12, padding: "10px 14px", borderTop: "1px solid var(--line-2)", background: overdue ? "rgba(220,38,38,0.06)" : "transparent" }}>
                    <div style={{ flex: 1, minWidth: 0 }}>
                      <div style={{ fontSize: 13.5, fontWeight: 600, color: "var(--ink)", textDecoration: cr.status === "declined" ? "line-through" : "none" }}>
                        {cr.harveyTargetId
                          ? <a href={"https://db.harveyllc.com/company/" + cr.harveyTargetId + "/target"} target="_blank" rel="noopener noreferrer" style={{ color: "var(--accent)", textDecoration: "none" }}>{cr.target}</a>
                          : cr.target}
                        {cr.note && <span className="muted" style={{ fontWeight: 400, marginLeft: 6 }}>— {cr.note}</span>}
                      </div>
                      <div style={{ fontSize: 11, color: "var(--muted)", marginTop: 1 }}>
                        {requester ? requester.name : cr.requesterId} → {assignee ? assignee.name : cr.ownerId}
                        {cr.dueDate && <span style={{ marginLeft: 8, color: overdue ? "#dc2626" : "var(--muted)", fontWeight: overdue ? 700 : 400 }}>due {cr.dueDate}{overdue ? " · overdue" : ""}</span>}
                      </div>
                    </div>
                    <span style={{ fontSize: 10, fontWeight: 700, color: stColor, background: stColor + "1a", padding: "2px 8px", borderRadius: 10, whiteSpace: "nowrap" }}>{stLabel}</span>
                    <div className="row" style={{ gap: 6 }}>
                      {mine && cr.status === "requested" && (<>
                        <button className="btn" style={{ fontSize: 11, padding: "3px 9px" }} onClick={() => acceptCR(cr)}>Accept</button>
                        <button className="btn ghost" style={{ fontSize: 11, padding: "3px 9px" }} onClick={() => declineCR(cr)}>Decline</button>
                      </>)}
                      {mine && cr.status === "accepted" && (
                        <button className="btn" style={{ fontSize: 11, padding: "3px 9px" }} onClick={() => completeCR(cr)}>Mark Done</button>
                      )}
                      {canManage && (cr.status === "done" || cr.status === "declined") && (
                        <button className="btn ghost" style={{ fontSize: 11, padding: "3px 9px" }} onClick={() => reopenCR(cr)}>Reopen</button>
                      )}
                    </div>
                  </div>
                );
              })}
            </div>
          )}
        </>
      )}

      {/* Add personal action modal */}
      {showAddPersonal && (
        <AddPersonalActionModal
          viewer={viewer}
          viewingAsId={viewingAsId}
          peoplePickerList={peoplePickerList}
          onClose={() => setShowAddPersonal(false)}
          onCreated={() => { setShowAddPersonal(false); reload(); }}
        />
      )}

      {/* New CR modal */}
      {crModalOpen && (
        <CRModal viewer={viewer} crPeople={crPeople}
          onClose={() => setCrModalOpen(false)}
          onCreated={() => { setCrModalOpen(false); setCrRefresh(x => x + 1); }}/>
      )}

      {/* Edit personal action modal */}
      {editingPersonal && (
        <EditPersonalActionModal
          existing={editingPersonal}
          peoplePickerList={peoplePickerList}
          onClose={() => setEditingPersonal(null)}
          onSaved={() => { setEditingPersonal(null); reload(); }}
          onDeleted={() => { setEditingPersonal(null); reload(); }}
        />
      )}

      {/* Complete New Client action → set next action date + note */}
      {completingNC && (
        <CompleteNewClientModal
          action={completingNC}
          onClose={() => setCompletingNC(null)}
          onSaved={() => { setCompletingNC(null); window.VaultUI.toast("success", "New action set on the New Client Report."); reload(); }}
        />
      )}

      {/* CR marked done → remind to update the Harvey database */}
      {crDoneReminder && (
        <div onClick={() => setCrDoneReminder(null)} style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.4)", zIndex: 1000, display: "flex", alignItems: "center", justifyContent: "center", padding: 16 }}>
          <div onClick={e => e.stopPropagation()} style={{ background: "var(--surface)", padding: 20, borderRadius: 8, width: 420, maxWidth: "92vw", boxShadow: "0 12px 40px rgba(0,0,0,0.2)" }}>
            <h3 style={{ margin: 0, marginBottom: 6, fontSize: 16, fontWeight: 600 }}>Marked Done — Update the Harvey Database</h3>
            <div className="muted small" style={{ marginBottom: 14 }}>
              Log the outcome of this re-approach for <strong>{crDoneReminder.target}</strong> in Harvey so the database stays the source of truth.
            </div>
            <textarea placeholder="Outcome note (optional) — saved onto this CR in Vault"
              value={crOutcomeNote} onChange={e => setCrOutcomeNote(e.target.value)} rows={2}
              style={{ width: "100%", boxSizing: "border-box", padding: "6px 10px", fontSize: 13, fontFamily: "inherit",
                border: "1px solid var(--line-2)", borderRadius: 4, background: "var(--surface)", color: "var(--ink)",
                resize: "vertical", marginBottom: 12 }}/>
            <div className="row" style={{ justifyContent: "space-between", gap: 8, alignItems: "center" }}>
              {crDoneReminder.harveyTargetId
                ? <a href={"https://db.harveyllc.com/company/" + crDoneReminder.harveyTargetId + "/target"} target="_blank" rel="noopener noreferrer" className="btn" style={{ textDecoration: "none" }}>Open {crDoneReminder.target} profile ↗</a>
                : <span className="muted small">No linked Harvey profile.</span>}
              <button className="btn ghost" onClick={saveCrOutcome}>Got it</button>
            </div>
          </div>
        </div>
      )}

      {/* Complete Pipeline action → set next Team Action, Owner, Due */}
      {completingPipeline && (
        <CompletePipelineModal
          action={completingPipeline}
          onClose={() => setCompletingPipeline(null)}
          onSaved={() => { setCompletingPipeline(null); window.VaultUI.toast("success", "New action set on the Pipeline."); reload(); }}
        />
      )}

      {/* In-Vault delete confirmation (replaces the browser confirm popup) */}
      {pendingDelete && (
        <div onClick={() => setPendingDelete(null)} style={{ position: "fixed", inset: 0, background: "rgba(20,30,45,.45)", zIndex: 1000, display: "flex", alignItems: "center", justifyContent: "center", padding: 16 }}>
          <div onClick={e => e.stopPropagation()} style={{ background: "var(--surface)", borderRadius: 14, width: "100%", maxWidth: 380, boxShadow: "0 20px 60px rgba(0,0,0,.25)", overflow: "hidden" }}>
            <div style={{ padding: "20px 22px 8px" }}>
              <div style={{ fontSize: 16, fontWeight: 600, color: "var(--ink)" }}>Delete this action?</div>
              <div style={{ fontSize: 13, color: "var(--muted)", marginTop: 6 }}>
                {pendingDelete.error
                  ? <span style={{ color: "var(--risk)" }}>{pendingDelete.error}</span>
                  : <>“{pendingDelete.description}” will be permanently removed. This can’t be undone.</>}
              </div>
            </div>
            <div style={{ display: "flex", gap: 10, padding: "14px 22px 18px", justifyContent: "flex-end" }}>
              <button onClick={() => setPendingDelete(null)} style={{ padding: "9px 16px", borderRadius: 9, border: "1px solid var(--line-2)", background: "var(--surface)", color: "var(--ink-2)", fontSize: 14, cursor: "pointer" }}>Cancel</button>
              <button onClick={confirmDelete} style={{ padding: "9px 18px", borderRadius: 9, border: "none", background: "var(--risk)", color: "#fff", fontSize: 14, fontWeight: 600, cursor: "pointer" }}>Delete</button>
            </div>
          </div>
        </div>
      )}
      {/* Calendar feed settings */}
      {showCalSettings && (
        <div onClick={() => setShowCalSettings(false)} style={{ position: "fixed", inset: 0, background: "rgba(20,30,45,.45)", zIndex: 1000, display: "flex", alignItems: "center", justifyContent: "center", padding: 16 }}>
          <div onClick={e => e.stopPropagation()} style={{ background: "var(--surface)", borderRadius: 14, width: "100%", maxWidth: 480, boxShadow: "0 20px 60px rgba(0,0,0,.25)", overflow: "hidden" }}>
            <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", padding: "16px 20px", borderBottom: "1px solid var(--line-2)" }}>
              <strong style={{ fontSize: 16, color: "var(--ink)" }}>Connect your calendar</strong>
              <button onClick={() => setShowCalSettings(false)} style={{ border: "none", background: "transparent", fontSize: 22, color: "var(--muted)", cursor: "pointer", lineHeight: 1 }}>×</button>
            </div>
            <div style={{ padding: 20 }}>
              <div style={{ fontSize: 12.5, color: "var(--ink-2)", marginBottom: 4, fontWeight: 600 }}>Calendar feed URL (.ics)</div>
              <input value={calUrlInput} onChange={e => setCalUrlInput(e.target.value)} placeholder="https://outlook.office365.com/owa/calendar/.../calendar.ics"
                style={{ width: "100%", padding: "10px 12px", borderRadius: 8, border: "1px solid var(--line-2)", fontSize: 12.5, color: "var(--ink)", background: "var(--surface)", boxSizing: "border-box" }}/>
              <div style={{ fontSize: 11.5, color: "var(--muted)", marginTop: 12, lineHeight: 1.5 }}>
                <strong>Outlook:</strong> Calendar → Settings → Shared calendars → Publish a calendar → choose “Can view all details” → copy the <strong>ICS</strong> link.<br/>
                <strong>Google:</strong> Calendar settings → your calendar → “Integrate calendar” → copy the <strong>Secret address in iCal format</strong>.<br/>
                This is read-only and only shows on your dashboard.
              </div>
              <div style={{ display: "flex", gap: 10, marginTop: 18 }}>
                <button onClick={saveCalUrl} disabled={!calUrlInput.trim()} style={{ padding: "9px 18px", borderRadius: 9, border: "none", background: "var(--accent)", color: "#fff", fontSize: 14, fontWeight: 600, cursor: calUrlInput.trim() ? "pointer" : "default", opacity: calUrlInput.trim() ? 1 : 0.5 }}>Save & load</button>
                {calUrlInput.trim() && <button onClick={() => { setCalUrlInput(""); window.VaultAPI.saveCalendarFeed(calPersonId, "", "other"); setShowCalSettings(false); setCalStatus("nofeed"); setCalEvents(null); }} style={{ padding: "9px 16px", borderRadius: 9, border: "1px solid var(--line-2)", background: "var(--surface)", color: "var(--ink-2)", fontSize: 14, cursor: "pointer" }}>Disconnect</button>}
              </div>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}
function SummaryStat({ label, value, color }) {
  return (
    <div style={{ display: "flex", flexDirection: "column" }}>
      <span className="muted small" style={{ fontSize: 10, textTransform: "uppercase", letterSpacing: "0.05em" }}>{label}</span>
      <span style={{ fontSize: 18, fontWeight: 600, marginTop: 2, color }}>{value}</span>
    </div>
  );
}

// ---------- Category card ----------
// One per ACTION_TYPES key. Inside, items grouped by Overdue / This Week / Later.
function CategoryCard({ typeKey, def, bucket, activeCount, today, weekStart, weekEnd, toggleDone, deletePersonal, editPersonal, viewerId, viewingAsId }) {
  const isReadOnly = viewingAsId && viewingAsId !== viewerId;
  const [collapsed, setCollapsed] = useState(false);

  // If completely empty, render a minimal card so user still sees the category exists
  if (activeCount === 0) {
    return (
      <div className="card" style={{ marginBottom: 14, overflow: "hidden", opacity: 0.55 }}>
        <div className="row" style={{ padding: "10px 14px", justifyContent: "space-between", alignItems: "center" }}>
          <div className="row" style={{ gap: 8, alignItems: "center" }}>
            <span style={{ width: 10, height: 10, borderRadius: 2, background: def.color }}/>
            <strong style={{ fontSize: 13, color: def.color }}>{def.label}</strong>
          </div>
          <span className="muted small">No active items</span>
        </div>
      </div>
    );
  }

  return (
    <div className="card" style={{ marginBottom: 14, overflow: "hidden", borderLeft: `3px solid ${def.color}` }}>
      <div className="row" onClick={() => setCollapsed(!collapsed)}
        style={{ padding: "10px 14px", borderBottom: !collapsed ? "1px solid var(--line-2)" : "none",
                 justifyContent: "space-between", alignItems: "center", cursor: "pointer", userSelect: "none", background: def.soft }}>
        <div className="row" style={{ gap: 8, alignItems: "center" }}>
          <strong style={{ fontSize: 14, color: def.color }}>{def.label}</strong>
          <span className="muted small">{activeCount} action{activeCount === 1 ? "" : "s"}</span>
          {bucket.overdue.length > 0 && (
            <span style={{ fontSize: 10, fontWeight: 600, padding: "2px 6px", borderRadius: 4,
              background: "rgba(239, 68, 68, 0.10)", color: "var(--risk)" }}>
              {bucket.overdue.length} overdue
            </span>
          )}
        </div>
        <span className="muted small">{collapsed ? "Show" : "Hide"}</span>
      </div>
      {!collapsed && (
        <div>
          {bucket.overdue.length > 0 && (
            <SubBucket title="Overdue" color="var(--risk)" items={bucket.overdue}
              toggleDone={toggleDone} deletePersonal={deletePersonal} editPersonal={editPersonal}
              isReadOnly={isReadOnly} today={today}/>
          )}
          {bucket.thisWeek.length > 0 && (
            <SubBucket title="This Week" color="var(--accent)" items={bucket.thisWeek}
              toggleDone={toggleDone} deletePersonal={deletePersonal} editPersonal={editPersonal}
              isReadOnly={isReadOnly} today={today}/>
          )}
          {bucket.later.length > 0 && (
            <SubBucket title="Later" color="var(--muted)" items={bucket.later}
              toggleDone={toggleDone} deletePersonal={deletePersonal} editPersonal={editPersonal}
              isReadOnly={isReadOnly} today={today}/>
          )}
        </div>
      )}
    </div>
  );
}

// ---------- Sub-bucket label + rows ----------
function SubBucket({ title, color, items, toggleDone, deletePersonal, editPersonal, isReadOnly, today }) {
  return (
    <>
      <div className="row" style={{ padding: "6px 14px", background: "var(--surface-2)", justifyContent: "space-between" }}>
        <span style={{ fontSize: 10, textTransform: "uppercase", letterSpacing: "0.05em", color, fontWeight: 600 }}>{title}</span>
        <span className="muted small" style={{ fontSize: 10 }}>{items.length}</span>
      </div>
      {items.map(a => (
        <ActionRow key={a.id} action={a}
          toggleDone={toggleDone}
          deletePersonal={deletePersonal}
          editPersonal={editPersonal}
          isReadOnly={isReadOnly}
          showDate={true}
          today={today}
        />
      ))}
    </>
  );
}

// ---------- One action row ----------
// Inject completion animation keyframes once (CSS animation runs independent of React re-render timing)
if (typeof document !== "undefined" && !document.getElementById("vault-complete-anim")) {
  const _s = document.createElement("style");
  _s.id = "vault-complete-anim";
  _s.textContent = "@keyframes vaultComplete{0%{background:rgba(22,163,74,0);opacity:1;transform:translateX(0)}25%{background:rgba(22,163,74,0.14);opacity:1;transform:translateX(0)}100%{background:rgba(22,163,74,0);opacity:0;transform:translateX(14px)}}.vault-completing{animation:vaultComplete .42s ease forwards}";
  document.head.appendChild(_s);
}

function classifyEvent(title) {
  const t = (title || "").toLowerCase();
  if (/\bupdate call\b/.test(t)) return { key: "update", color: "#2563eb", label: "Update" };
  if (/conference call|conf call/.test(t)) return { key: "conf", color: "#7c3aed", label: "Conf" };
  if (/intro call/.test(t)) return { key: "intro", color: "#0891b2", label: "Intro" };
  if (/\bcall\b/.test(t)) return { key: "call", color: "#0d9488", label: "Call" };
  if (/\bvisit|site visit|on-?site\b/.test(t)) return { key: "visit", color: "#ea580c", label: "Visit" };
  if (/meeting|mtg/.test(t)) return { key: "meeting", color: "#475569", label: "Mtg" };
  if (/\bpto|out\b|graduation|vacation/.test(t)) return { key: "ooo", color: "#94a3b8", label: "OOO" };
  return { key: "other", color: "#64748b", label: "" };
}

function relDue(dueDate, today) {
  const d = new Date(dueDate + "T00:00:00Z"), t = new Date((today || todayStr()) + "T00:00:00Z");
  const days = Math.round((d - t) / 86400000);
  if (days < 0) return { label: `${Math.abs(days)}d overdue`, color: "var(--risk)", weight: 700 };
  if (days === 0) return { label: "Today", color: "var(--accent)", weight: 700 };
  if (days === 1) return { label: "Tomorrow", color: "var(--ink-2)", weight: 600 };
  return { label: d.toLocaleDateString("en-US", { weekday: "short", timeZone: "UTC" }), color: "var(--muted)", weight: 500 };
}

function TodayRow({ action, toggleDone, deletePersonal, editPersonal, isReadOnly, today, weekStart, tinted }) {
  const F = window.VAULT_FIRM;
  const def = ACTION_TYPES[action.type] || ACTION_TYPES.personal;
  const owner = F.PEOPLE_BY_ID[action.ownerId];
  const ownerName = owner ? owner.name : action.ownerId;
  const rel = relDue(action.dueDate, today);
  const [hover, setHover] = React.useState(false);
  const [completing, setCompleting] = React.useState(false);
  const [checked, setChecked] = React.useState(false);
  const canCheck = !isReadOnly && !action.isCalendar;

  // overdue intensity: a thin left edge bar that deepens the later it is (1 → 14+ days)
  const overdueDays = (() => {
    if (action._bucket !== "overdue") return 0;
    const d = new Date(String(action.dueDate).slice(0, 10) + "T00:00:00Z");
    const t = new Date((today || "") + "T00:00:00Z");
    return Math.max(0, Math.round((t - d) / 86400000));
  })();
  const edge = overdueDays > 0 ? `rgba(239,68,68,${Math.min(0.9, 0.25 + overdueDays / 18)})` : "transparent";

  function handleCheck() {
    if (!canCheck) return;
    if (action.type === "newclient" || action.type === "pipeline") { toggleDone(action); return; } // opens "Action Completed! Set New Action?" modal
    if (!action.done) {
      setChecked(true);       // instant tick
      setCompleting(true);    // applies .vault-completing class → CSS keyframe plays
      setTimeout(() => { toggleDone(action); }, 430); // commit after the .42s animation
    } else {
      toggleDone(action);
    }
  }

  return (
    <div onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
      className={completing ? "vault-completing" : ""}
      style={{
        display: "flex", alignItems: "center", gap: 11, padding: "11px 16px",
        borderTop: "1px solid var(--line-2)", borderLeft: `3px solid ${edge}`,
        background: hover && !completing ? "var(--surface-2)" : (tinted ? def.soft : "transparent"),
        opacity: !completing && action.done ? 0.5 : 1,
        transition: "background .12s ease",
      }}>
      <input type="checkbox" checked={action.done || checked || completing} disabled={!canCheck}
        onChange={handleCheck}
        title={action.type === "newclient" ? "Complete & set next action" : "Mark done"}
        style={{ cursor: canCheck ? "pointer" : "not-allowed", width: 17, height: 17, flexShrink: 0, accentColor: def.color }}/>

      <span style={{ fontSize: 9.5, fontWeight: 700, padding: "2px 6px", borderRadius: 4, background: def.soft, color: def.color, letterSpacing: ".05em", flexShrink: 0, textTransform: "uppercase", minWidth: 40, textAlign: "center" }}>{def.short}</span>

      {action.isCadence && action.stepCode && (
        <span style={{ fontSize: 9.5, fontWeight: 600, padding: "2px 6px", borderRadius: 4, background: action.stepType === "Call" ? "var(--accent-soft)" : "var(--surface-2)", color: action.stepType === "Call" ? "var(--accent)" : "var(--ink-2)", flexShrink: 0 }}>{action.stepCode}</span>
      )}

      <div style={{ flex: "1 1 auto", minWidth: 0 }}>
        <div style={{ fontSize: 13.5, fontWeight: 500, color: "var(--ink)", textDecoration: action.done ? "line-through" : "none", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
          {action.description}
          {action.isCalendar && action.matched === false && <span className="muted" style={{ marginLeft: 6, fontWeight: 400, fontSize: 10 }}>(unmatched)</span>}
          {action.isCadence && action.projectName && <span className="muted" style={{ marginLeft: 6, fontWeight: 400 }}>— {action.projectName}</span>}
        </div>
        <div style={{ fontSize: 11, color: "var(--muted)", marginTop: 1 }}>
          {action.subLabel ? action.subLabel : (<>{!action.isCadence && action.projectName && <span>{action.projectName} · </span>}{ownerName}</>)}
        </div>
      </div>

      {/* hover quick-actions */}
      {hover && action.type === "personal" && !isReadOnly && (
        <div style={{ display: "flex", gap: 4, flexShrink: 0 }}>
          <button onClick={() => editPersonal(action)} title="Edit" style={{ border: "none", background: "transparent", color: "var(--muted)", cursor: "pointer", fontSize: 12, padding: "2px 4px" }}>Edit</button>
          <button onClick={() => deletePersonal(action.id, action.description)} title="Delete" style={{ border: "none", background: "transparent", color: "var(--risk)", cursor: "pointer", fontSize: 14, padding: "2px 4px" }}>×</button>
        </div>
      )}

      <span style={{ fontSize: 11.5, fontWeight: rel.weight, color: action.done ? "var(--muted)" : rel.color, flexShrink: 0, minWidth: 78, textAlign: "right" }}>{action.done ? "Done" : rel.label}</span>
    </div>
  );
}

function ActionRow({ action, toggleDone, deletePersonal, editPersonal, isReadOnly, showDate, today }) {
  const F = window.VAULT_FIRM;
  const def = ACTION_TYPES[action.type] || ACTION_TYPES.personal;
  const owner = F.PEOPLE_BY_ID[action.ownerId];
  const ownerName = owner ? owner.name : action.ownerId;

  return (
    <div style={{
      display: "flex",
      alignItems: "center",
      gap: 10,
      padding: "8px 14px",
      borderTop: "1px solid var(--line-2)",
      opacity: action.done ? 0.55 : 1,
    }}>
      {/* Done checkbox */}
      <input type="checkbox"
        checked={action.done}
        disabled={isReadOnly}
        onChange={() => toggleDone(action)}
        title={action.type === "newclient" ? "Complete & set next action" : "Mark done"}
        style={{ cursor: isReadOnly ? "not-allowed" : "pointer", width: 15, height: 15, flexShrink: 0 }}/>

      {/* Type badge */}
      <span style={{
        fontSize: 10, fontWeight: 600,
        padding: "2px 6px", borderRadius: 4,
        background: def.soft, color: def.color,
        letterSpacing: "0.05em", flexShrink: 0,
        textTransform: "uppercase", minWidth: 38, textAlign: "center",
      }}>{def.short}</span>

      {/* Cadence-specific step code */}
      {action.isCadence && (
        <span style={{
          fontSize: 10, fontWeight: 600,
          padding: "2px 6px", borderRadius: 4,
          background: action.stepType === "Call" ? "var(--accent-soft)" : "var(--surface-2)",
          color: action.stepType === "Call" ? "var(--accent)" : "var(--ink-2)",
          flexShrink: 0,
        }}>{action.stepCode}</span>
      )}

      {/* Description + project + owner */}
      <div style={{ flex: "1 1 auto", minWidth: 0 }}>
        <div style={{
          fontSize: 13,
          textDecoration: action.done ? "line-through" : "none",
          whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis",
        }}>
          {action.description}
          {action.isCadence && action.projectName && (
            <span className="muted" style={{ marginLeft: 6 }}>— {action.projectName}</span>
          )}
        </div>
        <div style={{ fontSize: 11, color: "var(--muted)", marginTop: 1 }}>
          {!action.isCadence && action.projectName && <span>{action.projectName} · </span>}
          {ownerName}
          {action.court && <span> · Court: {action.court}</span>}
          {action.priority && action.priority !== "normal" && (
            <span style={{ marginLeft: 4, color: action.priority === "high" ? "var(--risk)" : "var(--muted)" }}>
              · {action.priority.toUpperCase()}
            </span>
          )}
        </div>
      </div>

      {/* Date stamp - styled per overdue/today/future */}
      {showDate && (() => {
        const isOverdue = !action.done && action.dueDate < (today || todayStr());
        const isToday = action.dueDate === (today || todayStr());
        const dateColor = isOverdue ? "var(--risk)" : (isToday ? "var(--accent)" : "var(--muted)");
        return (
          <span style={{
            fontFamily: "var(--f-mono)", fontSize: 11, flexShrink: 0,
            color: dateColor,
            fontWeight: (isOverdue || isToday) ? 600 : 400,
          }}>
            {formatShortDate(action.dueDate)}
          </span>
        );
      })()}

      {/* Edit + Delete (personal only) */}
      {action.type === "personal" && !isReadOnly && (
        <>
          {editPersonal && (
            <button onClick={() => editPersonal(action)}
              className="btn ghost small"
              style={{ padding: "2px 6px", fontSize: 12, flexShrink: 0, color: "var(--muted)" }}
              title="Edit">✎</button>
          )}
          <button onClick={() => deletePersonal(action.id, action.description)}
            className="btn ghost small"
            style={{ color: "var(--risk)", padding: "2px 6px", fontSize: 12, flexShrink: 0 }}
            title="Delete">×</button>
        </>
      )}
    </div>
  );
}

// ---------- Add personal action modal ----------
function AddPersonalActionModal({ viewer, viewingAsId, peoplePickerList, onClose, onCreated }) {
  // Default ownerId to viewer.id, falling back to first valid picker entry
  const initialOwner = viewingAsId
    || (viewer && viewer.id ? viewer.id : null)
    || (peoplePickerList[0]?.id || null);
  const [ownerId, setOwnerId] = useState(initialOwner);
  const [kind, setKind] = useState("personal"); // "personal" -> user_actions, "project" -> project_actions
  const [projectPick, setProjectPick] = useState("");
  const [description, setDescription] = useState("");
  const [dueDate, setDueDate] = useState(todayStr());
  const [projectName, setProjectName] = useState("");
  const [priority, setPriority] = useState("normal");
  const [notes, setNotes] = useState("");
  const [saving, setSaving] = useState(false);

  const submit = async () => {
    if (!description.trim()) { window.VaultUI.toast("error", "Description is required."); return; }
    if (!dueDate) { window.VaultUI.toast("error", "Due date is required."); return; }
    if (!ownerId) {
      window.VaultUI.toast("error", "Assigned-to is required. If you don't see yourself in the dropdown, your user account may not be linked to a Vault person — flag for admin.");
      return;
    }
    if (kind === "project" && !projectPick && !projectName.trim()) {
      window.VaultUI.toast("error", "Pick a project (or type a project name) for a Project action.");
      return;
    }
    setSaving(true);
    try {
      const moduleHandle = viewer.team || (viewer.subteam ? viewer.subteam.replace(/^st-/, "") : null);
      if (kind === "project") {
        const proj = (window.HARVEY_CLIENTS || []).find(p => String(p.id) === String(projectPick)) || null;
        await window.VaultAPI.createProjectAction({
          ownerId,
          projectId: proj ? proj.id : null,
          projectName: proj ? proj.client : (projectName.trim() || null),
          module: moduleHandle,
          nextAction: description.trim(),
          dueDate,
          notes: notes.trim() || null,
        });
      } else {
        await window.VaultAPI.createUserAction({
          ownerId,
          module: moduleHandle,
          projectName: projectName.trim() || null,
          description: description.trim(),
          dueDate,
          priority,
          notes: notes.trim() || null,
        });
      }
      onCreated();
    } catch (err) {
      setSaving(false);
      window.VaultUI.toast("error", "Failed to create action: " + (err.message || err));
    }
  };

  // Modal backdrop + dialog
  return ReactDOM.createPortal(
    <div onClick={onClose} style={{
      position: "fixed", inset: 0, background: "rgba(0,0,0,0.4)",
      display: "flex", alignItems: "center", justifyContent: "center", padding: 16,
      zIndex: 1000,
    }}>
      <div onClick={e => e.stopPropagation()} style={{
        background: "var(--surface)", padding: 20, borderRadius: 8,
        width: 480, maxWidth: "92vw", maxHeight: "90vh", overflowY: "auto", boxShadow: "0 12px 40px rgba(0,0,0,0.2)",
      }}>
        <h3 style={{ margin: 0, marginBottom: 14, fontSize: 16, fontWeight: 500 }}>Add Action</h3>

        <Field label="Type">
          <div className="row" style={{ gap: 6 }}>
            {[["personal", "Personal"], ["project", "Project"]].map(([k, lbl]) => (
              <button key={k} onClick={() => setKind(k)}
                style={{ flex: 1, padding: "6px 10px", fontSize: 13, cursor: "pointer", borderRadius: 4,
                  border: "1px solid " + (kind === k ? "var(--accent)" : "var(--line-2)"),
                  background: kind === k ? "var(--accent-soft)" : "var(--surface)",
                  color: kind === k ? "var(--accent)" : "var(--ink)", fontWeight: kind === k ? 600 : 400 }}>
                {lbl}
              </button>
            ))}
          </div>
        </Field>

        <Field label="Description *">
          <input type="text" value={description}
            onChange={e => setDescription(e.target.value)}
            placeholder="What needs to happen?"
            autoFocus
            style={fieldInputStyle}/>
        </Field>

        <div className="kpi-grid one-col" style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
          <Field label="Due date *">
            <input type="date" value={dueDate}
              onChange={e => setDueDate(e.target.value)}
              style={fieldInputStyle}/>
          </Field>
          <Field label="Priority">
            <select value={priority} onChange={e => setPriority(e.target.value)} style={fieldInputStyle}>
              <option value="low">Low</option>
              <option value="normal">Normal</option>
              <option value="high">High</option>
            </select>
          </Field>
        </div>

        {peoplePickerList.length > 1 && (
          <Field label="Assigned to">
            <select value={ownerId} onChange={e => setOwnerId(e.target.value)} style={fieldInputStyle}>
              {peoplePickerList.map(p => (
                <option key={p.id} value={p.id}>{p.name}</option>
              ))}
            </select>
          </Field>
        )}

        {kind === "project" ? (
          <Field label="Project *">
            <select value={projectPick} onChange={e => setProjectPick(e.target.value)} style={fieldInputStyle}>
              <option value="">Select a project…</option>
              {((window.HARVEY_CLIENTS || []).slice().sort((a, b) => String(a.client).localeCompare(String(b.client)))).map(p => (
                <option key={p.id} value={p.id}>{p.client}{p.type ? " · " + p.type : ""}</option>
              ))}
            </select>
          </Field>
        ) : (
          <Field label="Project (optional)">
            <input type="text" value={projectName}
              onChange={e => setProjectName(e.target.value)}
              placeholder="Project / client name"
              style={fieldInputStyle}/>
          </Field>
        )}

        <Field label="Notes (optional)">
          <textarea value={notes}
            onChange={e => setNotes(e.target.value)}
            rows={2}
            style={{ ...fieldInputStyle, resize: "vertical", fontFamily: "inherit" }}/>
        </Field>

        <div className="row" style={{ justifyContent: "flex-end", gap: 8, marginTop: 16 }}>
          <button className="btn ghost" onClick={onClose} disabled={saving}>Cancel</button>
          <button className="btn primary" onClick={submit} disabled={saving}>
            {saving ? "Saving…" : "Save"}
          </button>
        </div>
      </div>
    </div>,
    document.body
  );
}

const fieldInputStyle = {
  width: "100%",
  padding: "6px 10px",
  border: "1px solid var(--line-2)",
  borderRadius: 4,
  fontFamily: "inherit",
  fontSize: 13,
  background: "var(--surface)",
  color: "var(--ink)",
  boxSizing: "border-box",
};

function parseCSV(text) {
  const rows = []; let i = 0, field = "", row = [], inQ = false;
  const pushF = () => { row.push(field); field = ""; };
  const pushR = () => { pushF(); rows.push(row); row = []; };
  while (i < text.length) {
    const c = text[i];
    if (inQ) {
      if (c === '"') { if (text[i + 1] === '"') { field += '"'; i += 2; continue; } inQ = false; i++; continue; }
      field += c; i++; continue;
    }
    if (c === '"') { inQ = true; i++; continue; }
    if (c === ",") { pushF(); i++; continue; }
    if (c === "\r") { i++; continue; }
    if (c === "\n") { pushR(); i++; continue; }
    field += c; i++;
  }
  if (field.length || row.length) pushR();
  if (!rows.length) return [];
  const header = rows[0].map(h => String(h).trim());
  return rows.slice(1)
    .filter(r => r.length && r.some(v => v !== ""))
    .map(r => { const o = {}; header.forEach((h, idx) => { o[h] = r[idx] != null ? r[idx] : ""; }); return o; });
}

function CRModal({ viewer, crPeople, onClose, onCreated }) {
  const initialAssignee = (crPeople[0] && crPeople[0].id) || (viewer && viewer.id) || null;
  const [target, setTarget] = useState("");
  const [ownerId, setOwnerId] = useState(initialAssignee);
  const [dueDate, setDueDate] = useState(todayStr());
  const [note, setNote] = useState("");
  const [saving, setSaving] = useState(false);

  const submit = async () => {
    if (!target.trim()) { window.VaultUI.toast("error", "Target / company is required."); return; }
    if (!ownerId) { window.VaultUI.toast("error", "Assignee is required."); return; }
    setSaving(true);
    try {
      const moduleHandle = viewer.team || (viewer.subteam ? viewer.subteam.replace(/^st-/, "") : null);
      await window.VaultAPI.createCR({ target: target.trim(), ownerId, requesterId: viewer.id, module: moduleHandle, dueDate: dueDate || null, note: note.trim() || null, status: "requested" });
      onCreated();
    } catch (err) { setSaving(false); window.VaultUI.toast("error", "Failed to create CR: " + (err.message || err)); }
  };

  return ReactDOM.createPortal(
    <div onClick={onClose} style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.4)", display: "flex", alignItems: "center", justifyContent: "center", padding: 16, zIndex: 1000 }}>
      <div onClick={e => e.stopPropagation()} style={{ background: "var(--surface)", padding: 20, borderRadius: 8, width: 480, maxWidth: "92vw", maxHeight: "90vh", overflowY: "auto", boxShadow: "0 12px 40px rgba(0,0,0,0.2)" }}>
        <h3 style={{ margin: 0, marginBottom: 14, fontSize: 16, fontWeight: 500 }}>New CR (Custom Re-approach)</h3>
        <Field label="Target / company *">
          <input type="text" value={target} onChange={e => setTarget(e.target.value)} placeholder="e.g. Bochi" autoFocus style={fieldInputStyle}/>
        </Field>
        <div className="kpi-grid one-col" style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
          <Field label="Assign to *">
            <select value={ownerId || ""} onChange={e => setOwnerId(e.target.value)} style={fieldInputStyle}>
              {crPeople.map(p => (<option key={p.id} value={p.id}>{p.name}</option>))}
            </select>
          </Field>
          <Field label="Due date">
            <input type="date" value={dueDate} onChange={e => setDueDate(e.target.value)} style={fieldInputStyle}/>
          </Field>
        </div>
        <Field label="Note">
          <textarea value={note} onChange={e => setNote(e.target.value)} rows={3} placeholder="Context for the re-approach" style={{ ...fieldInputStyle, resize: "vertical" }}/>
        </Field>
        <div className="row" style={{ justifyContent: "flex-end", gap: 8, marginTop: 14 }}>
          <button className="btn ghost" onClick={onClose} disabled={saving}>Cancel</button>
          <button className="btn" onClick={submit} disabled={saving}>{saving ? "Saving…" : "Create CR"}</button>
        </div>
      </div>
    </div>,
    document.body
  );
}


// ---------- Complete New Client action → set next action date + note ----------
function CompleteNewClientModal({ action, onClose, onSaved }) {
  const src = action.sourceRow || {};
  const [dueDate, setDueDate] = useState("");
  const [note, setNote] = useState(src.notes || "");
  const [saving, setSaving] = useState(false);

  const submit = async () => {
    setSaving(true);
    try {
      await window.VaultAPI.updateNewClient(action.id, {
        nextActionDate: dueDate || null,
        notes: note,
      });
      onSaved();
    } catch (err) {
      setSaving(false);
      window.VaultUI.toast("error", "Failed to update: " + (err.message || err));
    }
  };

  return ReactDOM.createPortal(
    <div onClick={onClose} style={{
      position: "fixed", inset: 0, background: "rgba(0,0,0,0.4)",
      display: "flex", alignItems: "center", justifyContent: "center", padding: 16, zIndex: 1000,
    }}>
      <div onClick={e => e.stopPropagation()} style={{
        background: "var(--surface)", padding: 20, borderRadius: 8,
        width: 440, maxWidth: "92vw", maxHeight: "90vh", overflowY: "auto", boxShadow: "0 12px 40px rgba(0,0,0,0.2)",
      }}>
        <h3 style={{ margin: 0, marginBottom: 6, fontSize: 16, fontWeight: 600 }}>Action Completed! Set New Action?</h3>
        <div className="muted small" style={{ marginBottom: 14 }}>
          {src.project || action.projectName || "New client"} — the date and note below replace the Next Action Date and Notes on the New Client Report. Leave the date blank to clear the next action.
        </div>
        <Field label="Next action date">
          <input type="date" value={dueDate} onChange={e => setDueDate(e.target.value)} autoFocus style={fieldInputStyle}/>
        </Field>
        <Field label="Note (replaces the New Client note)">
          <textarea value={note} onChange={e => setNote(e.target.value)} rows={3} placeholder="Context / outcome…" style={{ ...fieldInputStyle, resize: "vertical" }}/>
        </Field>
        <div className="row" style={{ justifyContent: "flex-end", gap: 8, marginTop: 14 }}>
          <button className="btn ghost" onClick={onClose} disabled={saving}>Cancel</button>
          <button className="btn" onClick={submit} disabled={saving}>{saving ? "Saving…" : "Save"}</button>
        </div>
      </div>
    </div>,
    document.body
  );
}

// ---------- Complete Pipeline action → set next Team Action / Owner / Due ----------
function CompletePipelineModal({ action, onClose, onSaved }) {
  const src = action.sourceRow || {};
  const F = window.VAULT_FIRM || {};
  const ownerOptions = ((F.DEAL_PEOPLE) || []).filter(p => !p.synth).sort((a, b) => (a.name || "").localeCompare(b.name || ""));
  const groups = window.TEAM_ACTION_GROUPS || [];
  const [teamAction, setTeamAction] = useState("");
  const [owner, setOwner] = useState(src.court || "");
  const [dueDate, setDueDate] = useState("");
  const [saving, setSaving] = useState(false);

  const submit = async () => {
    setSaving(true);
    try {
      await window.VaultAPI.upsertPipelinePick(
        action.harveyId,
        teamAction || null,
        owner || null,
        dueDate || null
      );
      onSaved();
    } catch (err) {
      setSaving(false);
      window.VaultUI.toast("error", "Failed to update: " + (err.message || err));
    }
  };

  return ReactDOM.createPortal(
    <div onClick={onClose} style={{
      position: "fixed", inset: 0, background: "rgba(0,0,0,0.4)",
      display: "flex", alignItems: "center", justifyContent: "center", padding: 16, zIndex: 1000,
    }}>
      <div onClick={e => e.stopPropagation()} style={{
        background: "var(--surface)", padding: 20, borderRadius: 8,
        width: 460, maxWidth: "92vw", maxHeight: "90vh", overflowY: "auto", boxShadow: "0 12px 40px rgba(0,0,0,0.2)",
      }}>
        <h3 style={{ margin: 0, marginBottom: 6, fontSize: 16, fontWeight: 600 }}>Action Completed! Set New Action?</h3>
        <div className="muted small" style={{ marginBottom: 14 }}>
          {action.projectName || "Pipeline target"} — set the next Team Action, Owner, and Due date. Leave all blank to just clear the action.
        </div>
        <Field label="Team Action">
          <select value={teamAction} onChange={e => setTeamAction(e.target.value)} autoFocus style={fieldInputStyle}>
            <option value="">— None —</option>
            {groups.map(g => (
              <optgroup key={g.label} label={g.label}>
                {g.options.map(o => <option key={o} value={o}>{o}</option>)}
              </optgroup>
            ))}
          </select>
        </Field>
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
          <Field label="Action Owner">
            <select value={owner} onChange={e => setOwner(e.target.value)} style={fieldInputStyle}>
              <option value="">Unassigned</option>
              {ownerOptions.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
            </select>
          </Field>
          <Field label="Team Due">
            <input type="date" value={dueDate} onChange={e => setDueDate(e.target.value)} style={fieldInputStyle}/>
          </Field>
        </div>
        <div className="row" style={{ justifyContent: "flex-end", gap: 8, marginTop: 14 }}>
          <button className="btn ghost" onClick={onClose} disabled={saving}>Cancel</button>
          <button className="btn" onClick={submit} disabled={saving}>{saving ? "Saving…" : "Save"}</button>
        </div>
      </div>
    </div>,
    document.body
  );
}

function Field({ label, children }) {
  return (
    <div style={{ marginBottom: 10 }}>
      <div className="muted small" style={{ fontSize: 11, marginBottom: 3 }}>{label}</div>
      {children}
    </div>
  );
}

// ---------- Edit personal action modal ----------
function EditPersonalActionModal({ existing, peoplePickerList, onClose, onSaved, onDeleted }) {
  const [ownerId, setOwnerId] = useState(existing.ownerId);
  const [description, setDescription] = useState(existing.description || "");
  const [dueDate, setDueDate] = useState(existing.dueDate || todayStr());
  const [projectName, setProjectName] = useState(existing.projectName || "");
  const [priority, setPriority] = useState(existing.priority || "normal");
  const [notes, setNotes] = useState(existing.notes || "");
  const [saving, setSaving] = useState(false);

  const submit = async () => {
    if (!description.trim()) { window.VaultUI.toast("error", "Description is required."); return; }
    if (!dueDate) { window.VaultUI.toast("error", "Due date is required."); return; }
    if (!ownerId) { window.VaultUI.toast("error", "Assigned-to is required."); return; }
    setSaving(true);
    try {
      await window.VaultAPI.updateUserAction(existing.id, {
        ownerId,
        projectName: projectName.trim() || null,
        description: description.trim(),
        dueDate,
        priority,
        notes: notes.trim() || null,
      });
      onSaved();
    } catch (err) {
      setSaving(false);
      window.VaultUI.toast("error", "Failed to save: " + (err.message || err));
    }
  };

  const handleDelete = async () => {
    if (!(await window.VaultUI.confirm({ message: "Delete this action?", danger: true }))) return;
    try {
      await window.VaultAPI.deleteUserAction(existing.id);
      onDeleted();
    } catch (err) { window.VaultUI.toast("error", "Failed to delete: " + (err.message || err)); }
  };

  return ReactDOM.createPortal(
    <div onClick={onClose} style={{
      position: "fixed", inset: 0, background: "rgba(0,0,0,0.4)",
      display: "flex", alignItems: "center", justifyContent: "center", padding: 16,
      zIndex: 1000,
    }}>
      <div onClick={e => e.stopPropagation()} style={{
        background: "var(--surface)", padding: 20, borderRadius: 8,
        width: 480, maxWidth: "92vw", maxHeight: "90vh", overflowY: "auto", boxShadow: "0 12px 40px rgba(0,0,0,0.2)",
      }}>
        <h3 style={{ margin: 0, marginBottom: 14, fontSize: 16, fontWeight: 500 }}>Edit Personal Action</h3>

        <Field label="Description *">
          <input type="text" value={description}
            onChange={e => setDescription(e.target.value)}
            autoFocus
            style={fieldInputStyle}/>
        </Field>

        <div className="kpi-grid one-col" style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
          <Field label="Due date *">
            <input type="date" value={dueDate}
              onChange={e => setDueDate(e.target.value)}
              style={fieldInputStyle}/>
          </Field>
          <Field label="Priority">
            <select value={priority} onChange={e => setPriority(e.target.value)} style={fieldInputStyle}>
              <option value="low">Low</option>
              <option value="normal">Normal</option>
              <option value="high">High</option>
            </select>
          </Field>
        </div>

        {peoplePickerList.length > 1 && (
          <Field label="Assigned to">
            <select value={ownerId} onChange={e => setOwnerId(e.target.value)} style={fieldInputStyle}>
              {peoplePickerList.map(p => (
                <option key={p.id} value={p.id}>{p.name}</option>
              ))}
            </select>
          </Field>
        )}

        <Field label="Project (optional)">
          <input type="text" value={projectName}
            onChange={e => setProjectName(e.target.value)}
            placeholder="Project / client name"
            style={fieldInputStyle}/>
        </Field>

        <Field label="Notes (optional)">
          <textarea value={notes}
            onChange={e => setNotes(e.target.value)}
            rows={2}
            style={{ ...fieldInputStyle, resize: "vertical", fontFamily: "inherit" }}/>
        </Field>

        <div className="row" style={{ justifyContent: "space-between", gap: 8, marginTop: 16 }}>
          <button className="btn ghost" onClick={handleDelete} disabled={saving}
            style={{ color: "var(--risk)" }}>Delete</button>
          <div className="row" style={{ gap: 8 }}>
            <button className="btn ghost" onClick={onClose} disabled={saving}>Cancel</button>
            <button className="btn primary" onClick={submit} disabled={saving}>
              {saving ? "Saving…" : "Save changes"}
            </button>
          </div>
        </div>
      </div>
    </div>,
    document.body
  );
}

window.ActionsScreen = ActionsScreen;