/* ════════════════════════════════════════════════════════════════════════
   MSINDAHA Operations — TICKETS
   List (dense rows, many visible at once) and Board (Kanban, drag to
   change status) views, saved filters, a detail drawer with properties,
   description, progress, history and comments, and the create/edit form.
   Every write here is shaped to pass firestore.rules for the acting role,
   and every meaningful change notifies the people involved.
════════════════════════════════════════════════════════════════════════ */

const TK_PAGE = 40;
const TICKET_PREFS_DEFAULTS = {
  view: "all",
  status: "all",
  priority: "all",
  category: "all",
  assignee: "all",
  period: EMPTY_PERIOD,
  sort: "newest",
  layout: "list",
};
const TICKET_PREFS_KEY = "msindaha_tk_prefs_v2";

/* ─── Write helpers ─────────────────────────────────────────────────── */
function ticketStatusPatch(t, next, me) {
  const now = nowISO();
  const patch = {
    status: next,
    updatedAt: now,
    lastActivity: { by: authorOf(me), at: now, action: "status" },
    // Resolution time is kept when a resolved ticket is closed, cleared on reopen.
    completedAt: DONE_TICKET.includes(next)
      ? DONE_TICKET.includes(t.status) && t.completedAt
        ? t.completedAt
        : now
      : null,
    closedAt: next === "closed" ? now : null,
  };
  if (isSharedAssignment(assignmentOf(t))) {
    const done = DONE_TICKET.includes(next);
    if (done && !t.completion?.completedBy) patch.completion = { completedBy: authorOf(me), completedAt: now };
    else if (!done && t.completion?.completedBy) patch.completion = { completedBy: null, completedAt: null };
  }
  return patch;
}

function ticketAssignmentPatch(t, next, me, users) {
  const prev = assignmentOf(t) || blankAssignment();
  const same =
    prev.mode === next.mode && sameSet(prev.uids, next.uids) && sameSet(prev.departments, next.departments);
  if (same) return { patch: {}, events: [], changed: false, newly: [], becameShared: false };
  const people = resolveAssignees(next, users);
  const patch = {
    assignment: next,
    assignee: next.mode === "individual" ? people[0] || null : null,
  };
  const depts = itemDepartments(next);
  if (!sameSet(depts, t.departments || [])) patch.departments = depts;
  if (next.mode === "multiple")
    patch.assigneeProgress = Object.fromEntries(
      next.uids.map((u) => [u, t.assigneeProgress?.[u] || { status: "pending", completedAt: null }]),
    );
  else if (t.assigneeProgress) patch.assigneeProgress = null;
  const nowShared = isSharedAssignment(next);
  const wasShared = isSharedAssignment(prev);
  if (nowShared && !wasShared) patch.completion = { completedBy: null, completedAt: null };
  else if (!nowShared && t.completion) patch.completion = null;
  const events = [
    makeEvent(
      me,
      "assign",
      hasTarget(next) ? `assigned this to ${assignmentSummaryText({ assignment: next }, users)}` : "removed the assignee",
    ),
  ];
  // Status follows the assignment: open ⇄ assigned.
  if (t.status === "open" && hasTarget(next)) patch.status = "assigned";
  else if (t.status === "assigned" && !hasTarget(next)) patch.status = "open";
  if (patch.status)
    events.push(
      makeEvent(me, "status", `changed the status from ${statusLabel(t.status)} to ${statusLabel(patch.status)}`, {
        from: t.status,
        to: patch.status,
      }),
    );
  return {
    patch,
    events,
    changed: true,
    newly: people.map((p) => p.uid).filter((u) => !prev.uids.includes(u)),
    becameShared: nowShared && !(wasShared && sameSet(prev.departments, next.departments)),
    next,
  };
}

/* useTicketActions — every ticket mutation, with feedback + notifications. */
function useTicketActions() {
  const app = useApp();
  const toast = useToast();
  const confirm = useConfirm();
  const { me, users, tickets } = app;
  const { arrayUnion } = window.__fb;

  const fail = (title, e) => {
    console.error(title, e);
    toast({ type: "error", title, msg: writeErrorMsg(e), duration: 8000 });
  };
  const base = (t) => ({ collName: "tickets", itemId: t._id, displayId: t.displayId });
  const notifyAssignment = (t, asg) => {
    if (!asg.changed) return;
    if (asg.newly.length)
      notifyUsers(asg.newly, { ...base(t), type: "assigned", title: `${t.displayId} was assigned to you`, msg: t.title }, me);
    if (asg.becameShared)
      notifyUsers(
        deptMemberUids(users, asg.next.departments),
        { ...base(t), type: "assigned", title: `${t.displayId} was assigned to your department`, msg: t.title },
        me,
      );
  };

  const create = async (form) => {
    const assignment = normalizeAssignment(form.assignment);
    const people = resolveAssignees(assignment, users);
    const shared = isSharedAssignment(assignment);
    const displayId = await nextDisplayId("TKT", tickets.rows);
    const now = nowISO();
    const data = plain({
      displayId,
      title: form.title.trim(),
      description: form.description.trim(),
      priority: form.priority,
      category: form.category,
      dueDate: form.dueDate || null,
      assignment,
      assignee: assignment.mode === "individual" ? people[0] || null : null,
      departments: itemDepartments(assignment),
      status: hasTarget(assignment) ? "assigned" : "open",
      completion: shared ? { completedBy: null, completedAt: null } : null,
      assigneeProgress:
        assignment.mode === "multiple"
          ? Object.fromEntries(assignment.uids.map((u) => [u, { status: "pending", completedAt: null }]))
          : null,
      createdAt: now,
      updatedAt: now,
      createdBy: authorOf(me),
      lastActivity: { by: authorOf(me), at: now, action: "created" },
      comments: [],
      ...workspaceFields(),
    });
    const ref = await tickets.add(data);
    const t = { ...data, _id: ref.id };
    if (people.length)
      notifyUsers(people.map((p) => p.uid), { ...base(t), type: "assigned", title: `${displayId} was assigned to you`, msg: data.title }, me);
    else if (shared)
      notifyUsers(
        deptMemberUids(users, assignment.departments),
        {
          ...base(t),
          type: "assigned",
          title: `New ${assignment.mode === "company" ? "company-wide" : "department"} ticket ${displayId}`,
          msg: data.title,
        },
        me,
      );
    else
      notifyUsers(
        deptEditorUids(users),
        { ...base(t), type: "new-ticket", title: `${displayId} needs an assignee`, msg: `${me.name}: ${data.title}` },
        me,
      );
    return t;
  };

  const saveEdits = async (t, form) => {
    const now = nowISO();
    const patch = {};
    const events = [];
    const title = form.title.trim();
    const description = form.description.trim();
    if (title !== (t.title || "")) {
      patch.title = title;
      events.push(makeEvent(me, "edit", `renamed this to “${title}”`));
    }
    if (description !== (t.description || "")) {
      patch.description = description;
      events.push(makeEvent(me, "edit", "updated the description"));
    }
    if (form.priority !== (t.priority || "medium")) {
      patch.priority = form.priority;
      events.push(
        makeEvent(me, "priority", `changed the priority from ${priorityLabel(t.priority)} to ${priorityLabel(form.priority)}`),
      );
    }
    if (form.category !== (t.category || "General")) {
      patch.category = form.category;
      events.push(makeEvent(me, "category", `moved this to ${form.category}`));
    }
    if ((form.dueDate || null) !== (t.dueDate || null)) {
      patch.dueDate = form.dueDate || null;
      events.push(makeEvent(me, "due", form.dueDate ? `set the due date to ${fmtDate(form.dueDate)}` : "removed the due date"));
    }
    const asg = ticketAssignmentPatch(t, normalizeAssignment(form.assignment), me, users);
    Object.assign(patch, asg.patch);
    events.push(...asg.events);
    if (!Object.keys(patch).length) return false;
    patch.updatedAt = now;
    patch.lastActivity = { by: authorOf(me), at: now, action: asg.changed ? "assignee" : "edited" };
    if (events.length) patch.comments = arrayUnion(...events);
    await tickets.update(t._id, patch);
    notifyAssignment(t, asg);
    return true;
  };

  const setStatus = async (t, next) => {
    if (!next || next === t.status) return;
    const patch = ticketStatusPatch(t, next, me);
    patch.comments = arrayUnion(
      makeEvent(me, "status", `changed the status from ${statusLabel(t.status)} to ${statusLabel(next)}`, {
        from: t.status,
        to: next,
      }),
    );
    try {
      await tickets.update(t._id, patch);
    } catch (e) {
      if (isSharedAssignment(assignmentOf(t)) && DONE_TICKET.includes(next))
        toast({ type: "warning", title: "Already resolved", msg: "Someone on the team resolved this ticket first." });
      else fail("Couldn't change the status", e);
      return;
    }
    const done = DONE_TICKET.includes(next);
    toast({
      type: done ? "success" : "info",
      title: `${t.displayId} → ${statusLabel(next)}`,
      duration: 3500,
    });
    notifyUsers(
      [creatorUidOf(t, users), ...assigneeUidsOf(t)],
      {
        ...base(t),
        type: done ? "resolved" : "status",
        title: `${t.displayId} is now ${statusLabel(next)}`,
        msg: `${me.name} updated “${t.title}”`,
      },
      me,
    );
  };

  const setAssignment = async (t, next) => {
    const asg = ticketAssignmentPatch(t, normalizeAssignment(next), me, users);
    if (!asg.changed) return;
    const now = nowISO();
    try {
      await tickets.update(t._id, {
        ...asg.patch,
        updatedAt: now,
        lastActivity: { by: authorOf(me), at: now, action: "assignee" },
        comments: arrayUnion(...asg.events),
      });
    } catch (e) {
      return fail("Couldn't reassign the ticket", e);
    }
    notifyAssignment(t, asg);
  };

  const setField = async (t, field, value, event) => {
    const now = nowISO();
    try {
      await tickets.update(t._id, {
        [field]: value,
        updatedAt: now,
        lastActivity: { by: authorOf(me), at: now, action: "edited" },
        comments: arrayUnion(event),
      });
    } catch (e) {
      fail("Couldn't save the change", e);
    }
  };
  const setPriority = (t, p) =>
    setField(t, "priority", p, makeEvent(me, "priority", `changed the priority from ${priorityLabel(t.priority)} to ${priorityLabel(p)}`));
  const setCategory = (t, c) => setField(t, "category", c, makeEvent(me, "category", `moved this to ${c}`));
  const setDue = (t, d) =>
    setField(t, "dueDate", d || null, makeEvent(me, "due", d ? `set the due date to ${fmtDate(d)}` : "removed the due date"));

  const toggleMine = async (t) => {
    const mineDone = t.assigneeProgress?.[me.uid]?.status === "completed";
    const now = nowISO();
    try {
      await tickets.update(t._id, {
        [`assigneeProgress.${me.uid}`]: { status: mineDone ? "pending" : "completed", completedAt: mineDone ? null : now },
        updatedAt: now,
        lastActivity: { by: authorOf(me), at: now, action: "progress" },
        comments: arrayUnion(makeEvent(me, "progress", mineDone ? "reopened their part" : "finished their part")),
      });
    } catch (e) {
      return fail("Couldn't update your progress", e);
    }
    if (!mineDone) {
      const uids = assigneeUidsOf(t);
      const doneCount =
        uids.filter((u) => u !== me.uid && t.assigneeProgress?.[u]?.status === "completed").length + 1;
      const all = doneCount >= uids.length;
      notifyUsers(
        [creatorUidOf(t, users)],
        {
          ...base(t),
          type: "progress",
          title: all ? `Everyone finished ${t.displayId}` : `${me.name} finished their part of ${t.displayId}`,
          msg: `${doneCount} of ${uids.length} done · ${t.title}`,
        },
        me,
      );
    }
  };

  const comment = async (t, text) => {
    const entry = plain({ text, author: authorOf(me), at: nowISO() });
    try {
      // Only `comments` changes — the shape the rules allow for anyone who
      // can read the ticket, regardless of role or assignment.
      await tickets.update(t._id, { comments: arrayUnion(entry) });
    } catch (e) {
      fail("Couldn't post your comment", e);
      throw e;
    }
    notifyUsers(
      [...participantsOf(t, users)],
      { ...base(t), type: "comment", title: `${me.name} commented on ${t.displayId}`, msg: text.slice(0, 160) },
      me,
    );
  };

  const remove = async (t) => {
    const n = realComments(t).length;
    const ok = await confirm({
      title: `Delete ${t.displayId}?`,
      message: (
        <>
          <strong>“{t.title}”</strong>
          {n ? ` and its ${plural(n, "comment")}` : ""} will be permanently deleted for everyone.
        </>
      ),
      confirmLabel: "Delete ticket",
      danger: true,
    });
    if (!ok) return false;
    const _id = t._id;
    const data = omit(t, ["_id"]);
    try {
      await tickets.remove(_id);
    } catch (e) {
      fail("Couldn't delete the ticket", e);
      return false;
    }
    toast({
      type: "success",
      title: `${t.displayId} deleted`,
      duration: 8000,
      action: {
        label: "Undo",
        onClick: () =>
          tickets
            .set(_id, data)
            .then(() => toast({ type: "success", title: `${t.displayId} restored`, duration: 3000 }))
            .catch((e) => fail("Couldn't restore the ticket", e)),
      },
    });
    return true;
  };

  return { create, saveEdits, setStatus, setAssignment, setPriority, setCategory, setDue, toggleMine, comment, remove };
}

/* Highlights rows that teammates changed while you were looking. */
function latestActorUid(item) {
  let at = item.lastActivity?.at || "";
  let uid = item.lastActivity?.by?.uid || null;
  (item.comments || []).forEach((c) => {
    if (c.at && c.at >= at) {
      at = c.at;
      uid = c.author?.uid || null;
    }
  });
  return uid;
}
function useRecentlyChanged(rows, meUid) {
  const prev = React.useRef(null);
  const [ids, setIds] = React.useState(() => new Set());
  React.useEffect(() => {
    const map = new Map(rows.map((r) => [r._id, lastActivityAt(r)]));
    if (prev.current) {
      const changed = [];
      map.forEach((at, id) => {
        if (prev.current.get(id) === at) return;
        const r = rows.find((x) => x._id === id);
        if (r && latestActorUid(r) !== meUid) changed.push(id);
      });
      if (changed.length) {
        setIds((s) => new Set([...s, ...changed]));
        setTimeout(
          () =>
            setIds((s) => {
              const n = new Set(s);
              changed.forEach((i) => n.delete(i));
              return n;
            }),
          2600,
        );
      }
    }
    prev.current = map;
  }, [rows, meUid]);
  return ids;
}

/* The one or two most likely next steps, shown as buttons in the drawer. */
function ticketNextSteps(t, perms, allowed) {
  const can = (s) => allowed.includes(s) && s !== t.status;
  const reopenTo = hasTarget(assignmentOf(t)) ? "assigned" : "open";
  const steps = [];
  if (perms.shared && !perms.sharedDone && ticketIsActive(t) && can("resolved")) {
    steps.push({ next: "resolved", label: "Resolve for the team", icon: IC.CheckCircle, cls: "btn-success" });
    return steps;
  }
  if (["open", "assigned"].includes(t.status)) {
    if (can("in-progress")) steps.push({ next: "in-progress", label: "Start progress", icon: IC.Play, cls: "btn-primary" });
    if (can("resolved")) steps.push({ next: "resolved", label: "Resolve", icon: IC.CheckCircle, cls: "btn" });
  } else if (t.status === "in-progress") {
    if (can("resolved")) steps.push({ next: "resolved", label: "Mark resolved", icon: IC.CheckCircle, cls: "btn-success" });
  } else if (t.status === "resolved") {
    if (can("closed")) steps.push({ next: "closed", label: "Close ticket", icon: IC.Check, cls: "btn-primary" });
    if (can(reopenTo)) steps.push({ next: reopenTo, label: "Reopen", icon: IC.Reopen, cls: "btn" });
  } else if (can(reopenTo)) {
    steps.push({ next: reopenTo, label: "Reopen", icon: IC.Reopen, cls: "btn" });
  }
  return steps;
}

/* ═══════════════════════ PAGE ═══════════════════════ */
function TicketsPage({ route }) {
  const app = useApp();
  const { me, users, tickets, canEdit, navigate, closeDetail, role } = app;
  const actions = useTicketActions();
  useNow();
  const [prefsRaw, setPrefsRaw] = useLocalStorage(TICKET_PREFS_KEY, TICKET_PREFS_DEFAULTS);
  const p = { ...TICKET_PREFS_DEFAULTS, ...prefsRaw };
  const setP = (patch) => setPrefsRaw((prev) => ({ ...TICKET_PREFS_DEFAULTS, ...prev, ...patch }));
  const [search, setSearch] = React.useState("");
  const [limit, setLimit] = React.useState(TK_PAGE);
  const [form, setForm] = React.useState(null); // { mode: 'create' } | { mode: 'edit', ticket }
  const rows = tickets.rows;
  const highlighted = useRecentlyChanged(rows, me.uid);

  // #/tickets/new opens the create form (quick actions, command palette).
  React.useEffect(() => {
    if (route.id === "new") {
      setForm({ mode: "create" });
      navigate("tickets", null, { replace: true });
    }
  }, [route.id]);

  // "c" = create ticket.
  React.useEffect(() => {
    const onKey = (e) => {
      if (e.key !== "c" || e.ctrlKey || e.metaKey || e.altKey) return;
      if (isTypingTarget(document.activeElement) || __layers.length) return;
      e.preventDefault();
      setForm({ mode: "create" });
    };
    document.addEventListener("keydown", onKey);
    return () => document.removeEventListener("keydown", onKey);
  }, []);

  const viewFns = {
    all: () => true,
    mine: (t) => isAssignedToMe(t, me.uid),
    created: (t) => creatorUidOf(t, users) === me.uid,
    unassigned: (t) => isUnassigned(t),
  };
  const statusMatch = (t, s) =>
    s === "all" ? true : s === "active" ? ticketIsActive(t) : s === "done" ? ticketIsDone(t) : t.status === s;
  const q = search.trim().toLowerCase();
  const searchMatch = (t) => {
    if (!q) return true;
    const hay = `${t.displayId} ${t.title} ${t.description || ""} ${t.category || ""} ${t.createdBy?.name || ""} ${assignmentSummaryText(t, users)}`.toLowerCase();
    return q.split(/\s+/).every((w) => hay.includes(w));
  };
  const assigneeMatch = (t) =>
    p.assignee === "all"
      ? true
      : p.assignee === "unassigned"
        ? isUnassigned(t)
        : assigneeUidsOf(t).includes(p.assignee === "me" ? me.uid : p.assignee);

  const sorters = {
    newest: (a, b) => (b.createdAt || "").localeCompare(a.createdAt || ""),
    oldest: (a, b) => (a.createdAt || "").localeCompare(b.createdAt || ""),
    updated: (a, b) => lastActivityAt(b).localeCompare(lastActivityAt(a)),
    priority: (a, b) =>
      (PRIORITY_RANK[a.priority] ?? 2) - (PRIORITY_RANK[b.priority] ?? 2) || (b.createdAt || "").localeCompare(a.createdAt || ""),
    due: (a, b) => {
      if (!a.dueDate && !b.dueDate) return (b.createdAt || "").localeCompare(a.createdAt || "");
      if (!a.dueDate) return 1;
      if (!b.dueDate) return -1;
      return a.dueDate.localeCompare(b.dueDate);
    },
  };

  // Everything except the view tab — so each tab's count matches what it shows.
  const baseFiltered = React.useMemo(
    () =>
      rows.filter(
        (t) =>
          statusMatch(t, p.status) &&
          (p.priority === "all" || (t.priority || "medium") === p.priority) &&
          (p.category === "all" || (t.category || "General") === p.category) &&
          assigneeMatch(t) &&
          matchesPeriod(t, p.period, ticketIsFinished(t)) &&
          searchMatch(t),
      ),
    [rows, users, p.status, p.priority, p.category, p.assignee, JSON.stringify(p.period), q],
  );
  const filtered = React.useMemo(
    () => baseFiltered.filter(viewFns[p.view] || viewFns.all).sort(sorters[p.sort] || sorters.newest),
    [baseFiltered, p.view, p.sort],
  );
  React.useEffect(() => setLimit(TK_PAGE), [p.view, p.status, p.priority, p.category, p.assignee, JSON.stringify(p.period), q, p.sort]);

  const viewCounts = {
    all: baseFiltered.length,
    mine: baseFiltered.filter(viewFns.mine).length,
    created: baseFiltered.filter(viewFns.created).length,
    unassigned: baseFiltered.filter(viewFns.unassigned).length,
  };
  const stats = React.useMemo(() => {
    const active = rows.filter(ticketIsActive);
    return {
      total: rows.length,
      active: active.length,
      unassigned: active.filter(isUnassigned).length,
      overdue: active.filter((t) => dueInfo(t.dueDate, false)?.state === "overdue").length,
      done: rows.filter(ticketIsDone).length,
    };
  }, [rows]);
  const dueCounts = {
    overdue: rows.filter((t) => dueInfo(t.dueDate, ticketIsFinished(t))?.state === "overdue").length,
    none: rows.filter((t) => !t.dueDate).length,
  };

  const filtersActive =
    p.status !== "all" ||
    p.priority !== "all" ||
    p.category !== "all" ||
    p.assignee !== "all" ||
    p.period.kind !== "all" ||
    !!q;
  const clearFilters = () => {
    setSearch("");
    setP({ status: "all", priority: "all", category: "all", assignee: "all", period: { ...EMPTY_PERIOD } });
  };

  // Detail drawer
  const openId = route.id && route.id !== "new" ? route.id : null;
  const openTicket = openId ? rows.find((t) => t._id === openId) : null;
  const navList = openTicket && filtered.some((t) => t._id === openId) ? filtered : rows.slice().sort(sorters[p.sort]);
  const idx = openTicket ? navList.findIndex((t) => t._id === openId) : -1;
  const open = (t) => navigate("tickets", t._id);
  const goTo = (t) => t && navigate("tickets", t._id, { replace: true });

  const categories = [...new Set([...CFG.ticketCategories, ...rows.map((t) => t.category).filter(Boolean)])];
  const people = assignableUsers(users, role);

  const csvColumns = [
    { label: "ID", get: (t) => t.displayId },
    { label: "Title", get: (t) => t.title },
    { label: "Description", get: (t) => t.description || "" },
    { label: "Status", get: (t) => statusLabel(t.status) },
    { label: "Priority", get: (t) => priorityLabel(t.priority) },
    { label: "Category", get: (t) => t.category || "" },
    { label: "Assignee", get: (t) => assignmentSummaryText(t, users) },
    { label: "Due Date", get: (t) => (t.dueDate ? fmtDate(t.dueDate) : "") },
    { label: "Created By", get: (t) => t.createdBy?.name || "" },
    { label: "Created At", get: (t) => fmtDateTime(t.createdAt) },
    { label: "Resolved At", get: (t) => fmtDateTime(t.completedAt) },
    { label: "Comments", get: (t) => realComments(t).length },
  ];

  const statTiles = [
    {
      key: "active",
      label: "Active",
      value: stats.active,
      sub: `of ${stats.total} total`,
      icon: IC.Ticket,
      tone: "blue",
      on: p.status === "active",
      apply: () => setP({ status: p.status === "active" ? "all" : "active" }),
    },
    {
      key: "unassigned",
      label: "Unassigned",
      value: stats.unassigned,
      sub: stats.unassigned ? "need an owner" : "everything is owned",
      icon: IC.User,
      tone: "indigo",
      on: p.view === "unassigned",
      apply: () => setP({ view: p.view === "unassigned" ? "all" : "unassigned", status: p.view === "unassigned" ? p.status : "active" }),
    },
    {
      key: "overdue",
      label: "Overdue",
      value: stats.overdue,
      sub: stats.overdue ? "past their due date" : "nothing late",
      icon: IC.Alert,
      tone: stats.overdue ? "red" : "gray",
      on: p.period.kind === "overdue",
      apply: () => setP({ period: p.period.kind === "overdue" ? { ...EMPTY_PERIOD } : { ...EMPTY_PERIOD, kind: "overdue" } }),
    },
    {
      key: "done",
      label: "Resolved",
      value: stats.done,
      sub: "resolved or closed",
      icon: IC.CheckCircle,
      tone: "green",
      on: p.status === "done",
      apply: () => setP({ status: p.status === "done" ? "all" : "done" }),
    },
  ];

  const shown = filtered.slice(0, limit);

  return (
    <div className="page-tickets">
      <div className="page-header">
        <div>
          <h2>Tickets</h2>
          <p>Requests and issues for the {deptLabelOf(MY_DEPARTMENT)} team — assign, track and resolve.</p>
        </div>
        <div className="page-header-actions">
          <button className="btn" onClick={() => downloadCSV(`tickets-${todayISO()}.csv`, filtered, csvColumns)} disabled={!filtered.length}>
            <IC.Download /> Export
          </button>
          <button className="btn btn-primary" onClick={() => setForm({ mode: "create" })} title="New ticket (C)">
            <IC.Plus /> New ticket
          </button>
        </div>
      </div>

      <div className="stat-row stat-row-4">
        {statTiles.map((s, i) => (
          <button
            key={s.key}
            type="button"
            className={cx("stat-card", "stat-click", s.on && "selected")}
            onClick={s.apply}
            style={{ "--i": i }}
            aria-pressed={s.on}
          >
            <span className={cx("stat-icon", `tone-${s.tone}`)}>
              <s.icon />
            </span>
            <span className="stat-lbl">{s.label}</span>
            <span className="stat-val">{tickets.loaded ? s.value : <Skeleton w={34} h={24} />}</span>
            <span className="stat-sub">{s.sub}</span>
          </button>
        ))}
      </div>

      <div className="list-shell">
        <div className="list-tabs">
          <ViewTabs
            value={p.view}
            onChange={(view) => setP({ view })}
            tabs={[
              { value: "all", label: "All tickets", count: viewCounts.all },
              { value: "mine", label: "Assigned to me", count: viewCounts.mine },
              { value: "created", label: "Created by me", count: viewCounts.created },
              { value: "unassigned", label: "Unassigned", count: viewCounts.unassigned },
            ]}
          />
          <Segmented
            size="sm"
            value={p.layout}
            onChange={(layout) => setP({ layout })}
            options={[
              { value: "list", icon: IC.List, label: "List", title: "List view" },
              { value: "board", icon: IC.Board, label: "Board", title: "Board view" },
            ]}
          />
        </div>

        <div className="list-toolbar">
          <SearchInput value={search} onChange={setSearch} placeholder="Search by ID, title, person…" />
          <FilterMenu
            label="Status"
            icon={IC.Activity}
            value={p.status}
            onChange={(status) => setP({ status })}
            options={[
              { value: "all", label: "All statuses" },
              { value: "active", label: "Active (open, assigned, in progress)" },
              { value: "done", label: "Resolved or closed" },
              { value: "sep1", sep: true },
              ...TICKET_STATUSES.map((s) => ({
                value: s,
                label: statusLabel(s),
                node: <StatusBadge status={s} />,
                count: rows.filter((t) => t.status === s).length,
              })),
            ]}
          />
          <FilterMenu
            label="Priority"
            icon={IC.Flag}
            value={p.priority}
            onChange={(priority) => setP({ priority })}
            options={[
              { value: "all", label: "Any priority" },
              ...PRIORITIES.map((x) => ({
                value: x,
                label: priorityLabel(x),
                node: <PriorityTag p={x} />,
                count: rows.filter((t) => (t.priority || "medium") === x).length,
              })),
            ]}
          />
          <FilterMenu
            label="Assignee"
            icon={IC.User}
            value={p.assignee}
            onChange={(assignee) => setP({ assignee })}
            width={260}
            options={[
              { value: "all", label: "Anyone" },
              { value: "me", label: "Me" },
              { value: "unassigned", label: "Unassigned" },
              { value: "sep1", sep: true },
              ...people
                .filter((u) => u._id !== me.uid)
                .map((u) => ({
                  value: u._id,
                  label: userLabel(u),
                  node: (
                    <span className="assignee-one">
                      <Avatar name={userLabel(u)} email={u.email} size={20} />
                      {userLabel(u)}
                    </span>
                  ),
                })),
            ]}
          />
          <FilterMenu
            label="Category"
            icon={IC.Tag}
            value={p.category}
            onChange={(category) => setP({ category })}
            options={[
              { value: "all", label: "All categories" },
              ...categories.map((c) => ({ value: c, label: c, count: rows.filter((t) => (t.category || "General") === c).length })),
            ]}
          />
          <PeriodFilter value={p.period} onChange={(period) => setP({ period })} counts={dueCounts} />
          {filtersActive && (
            <button className="btn btn-ghost btn-sm" onClick={clearFilters}>
              <IC.X /> Clear
            </button>
          )}
          <span className="toolbar-spacer" />
          <Dropdown
            width={200}
            placement="bottom-end"
            trigger={({ ref, toggle, open }) => (
              <button ref={ref} type="button" className={cx("btn btn-ghost btn-sm", open && "active")} onClick={toggle}>
                <IC.ArrowUpDown />
                {{ newest: "Newest", oldest: "Oldest", updated: "Last updated", priority: "Priority", due: "Due date" }[p.sort]}
              </button>
            )}
          >
            {({ close }) => (
              <MenuList>
                <MenuLabel>Sort by</MenuLabel>
                {[
                  ["newest", "Newest first"],
                  ["oldest", "Oldest first"],
                  ["updated", "Last updated"],
                  ["priority", "Priority"],
                  ["due", "Due date (soonest)"],
                ].map(([v, l]) => (
                  <MenuItem
                    key={v}
                    checkable
                    active={p.sort === v}
                    onSelect={() => {
                      setP({ sort: v });
                      close();
                    }}
                  >
                    {l}
                  </MenuItem>
                ))}
              </MenuList>
            )}
          </Dropdown>
        </div>

        <div className="list-summary">
          {tickets.loaded ? (
            <>
              <span>
                {filtered.length === rows.length
                  ? plural(rows.length, "ticket")
                  : `${filtered.length} of ${plural(rows.length, "ticket")}`}
              </span>
              {tickets.error && <span className="text-danger"> · Live updates interrupted — retrying…</span>}
            </>
          ) : (
            <span>Loading tickets…</span>
          )}
        </div>

        {!tickets.loaded && rows.length === 0 ? (
          <TicketListSkeleton />
        ) : filtered.length === 0 ? (
          <EmptyState
            icon={IC.Ticket}
            title={rows.length === 0 ? "No tickets yet" : "No tickets match"}
            action={
              rows.length === 0 ? (
                <button className="btn btn-primary" onClick={() => setForm({ mode: "create" })}>
                  <IC.Plus /> Create the first ticket
                </button>
              ) : (
                <button className="btn" onClick={() => { clearFilters(); setP({ view: "all" }); }}>
                  <IC.X /> Clear filters
                </button>
              )
            }
          >
            {rows.length === 0
              ? "Tickets your team opens will appear here."
              : "Try a different view, or clear the filters to see everything."}
          </EmptyState>
        ) : p.layout === "board" ? (
          <TicketBoard tickets={filtered} users={users} onOpen={open} highlighted={highlighted} onMove={actions.setStatus} />
        ) : (
          <TicketList
            tickets={shown}
            total={filtered.length}
            users={users}
            onOpen={open}
            openId={openId}
            highlighted={highlighted}
            sort={p.sort}
            onSort={(sort) => setP({ sort })}
            onMore={() => setLimit((l) => l + TK_PAGE)}
          />
        )}
      </div>

      <Drawer open={!!openId} onClose={() => closeDetail("tickets")} label="Ticket details">
        <TicketDetail
          ticket={openTicket}
          loading={!tickets.loaded}
          onClose={() => closeDetail("tickets")}
          onPrev={idx > 0 ? () => goTo(navList[idx - 1]) : null}
          onNext={idx >= 0 && idx < navList.length - 1 ? () => goTo(navList[idx + 1]) : null}
          position={idx >= 0 ? { index: idx, total: navList.length } : null}
          onEdit={(t) => setForm({ mode: "edit", ticket: t })}
          actions={actions}
        />
      </Drawer>

      <TicketFormModal
        state={form}
        onClose={() => setForm(null)}
        actions={actions}
        categories={categories}
        onCreated={(t) => {
          app.toast({
            type: "success",
            title: `${t.displayId} created`,
            msg: t.title,
            action: { label: "Open", onClick: () => navigate("tickets", t._id) },
          });
        }}
      />
    </div>
  );
}

function TicketListSkeleton() {
  return (
    <div className="tk-table">
      {Array.from({ length: 6 }).map((_, i) => (
        <div key={i} className="tk-row skeleton-row">
          <Skeleton w={16} h={16} r={4} />
          <div>
            <Skeleton w="55%" h={12} />
            <Skeleton w="35%" h={10} style={{ marginTop: 8 }} />
          </div>
          <Skeleton w={84} h={20} r={99} />
          <Skeleton w={110} h={20} r={99} />
          <Skeleton w={64} h={12} />
          <Skeleton w={44} h={12} />
        </div>
      ))}
    </div>
  );
}

/* ─── List view ─────────────────────────────────────────────────────── */
function SortHead({ label, sortKey, sort, onSort, alt }) {
  const active = sort === sortKey || sort === alt;
  return (
    <button
      type="button"
      className={cx("tk-head-btn", active && "active")}
      onClick={() => onSort(alt && sort === sortKey ? alt : sortKey)}
    >
      {label}
      {active && (sort === alt ? <IC.ChevronUp /> : <IC.ChevronDown />)}
    </button>
  );
}
function TicketList({ tickets, total, users, onOpen, openId, highlighted, sort, onSort, onMore }) {
  return (
    <div className="tk-table" role="table" aria-label="Tickets">
      <div className="tk-head" role="row">
        <span />
        <SortHead label="Ticket" sortKey="newest" alt="oldest" sort={sort} onSort={onSort} />
        <span className="tk-head-cell">Status</span>
        <span className="tk-head-cell">Assignee</span>
        <SortHead label="Due" sortKey="due" sort={sort} onSort={onSort} />
        <SortHead label="Updated" sortKey="updated" sort={sort} onSort={onSort} />
      </div>
      {tickets.map((t, i) => (
        <TicketRow
          key={t._id}
          t={t}
          users={users}
          i={i}
          onOpen={onOpen}
          selected={t._id === openId}
          flash={highlighted.has(t._id)}
        />
      ))}
      <LoadMoreSentinel onVisible={onMore} shown={tickets.length} total={total} label="tickets" />
    </div>
  );
}
function TicketRow({ t, users, i, onOpen, selected, flash }) {
  const comments = realComments(t).length;
  const excerpt = (t.description || "").replace(/\s+/g, " ").trim();
  return (
    <div
      className={cx("tk-row", selected && "selected", flash && "flash", ticketIsFinished(t) && "finished")}
      role="row"
      tabIndex={0}
      style={{ "--i": Math.min(i, 14) }}
      onClick={() => onOpen(t)}
      onKeyDown={(e) => {
        if (e.key === "Enter" || e.key === " ") {
          e.preventDefault();
          onOpen(t);
        }
      }}
    >
      <span className="tk-c-prio">
        <PriorityIcon p={t.priority || "medium"} />
      </span>
      <div className="tk-c-main">
        <div className="tk-line1">
          <span className="tk-id">{t.displayId}</span>
          <span className="tk-title">{t.title}</span>
        </div>
        <div className="tk-line2">
          <span className="tk-cat">{t.category || "General"}</span>
          <span className="dot-sep" />
          {excerpt ? <span className="tk-excerpt">{excerpt}</span> : <span>Opened by {t.createdBy?.name || "someone"}</span>}
        </div>
      </div>
      <span className="tk-c-status">
        <StatusBadge status={t.status} />
      </span>
      <span className="tk-c-assignee">
        <AssigneeCell item={t} users={users} />
      </span>
      <span className="tk-c-due">
        <DueChip due={t.dueDate} done={ticketIsFinished(t)} empty={<span className="muted-dash">—</span>} />
      </span>
      <span className="tk-c-updated">
        <span title={fmtDateTime(lastActivityAt(t))}>{timeAgo(lastActivityAt(t))}</span>
        {comments > 0 && (
          <span className="tk-comments" title={plural(comments, "comment")}>
            <IC.Comment />
            {comments}
          </span>
        )}
      </span>
    </div>
  );
}

/* ─── Board view (drag a card to another column to change its status) ─ */
const BOARD_COLUMNS = ["open", "assigned", "in-progress", "resolved", "closed"];
function TicketBoard({ tickets, users, onOpen, highlighted, onMove }) {
  const app = useApp();
  const [dragId, setDragId] = React.useState(null);
  const [over, setOver] = React.useState(null);
  const other = tickets.filter((t) => !BOARD_COLUMNS.includes(t.status));
  const cols = [...BOARD_COLUMNS, ...(other.length ? ["other"] : [])];
  const dragged = dragId ? tickets.find((t) => t._id === dragId) : null;
  const allowedFor = (t) => allowedTicketStatuses(t, ticketPerms(t, app), app);
  const canDrop = (col) => dragged && col !== "other" && col !== dragged.status && allowedFor(dragged).includes(col);
  return (
    <div className="board">
      {cols.map((col) => {
        const items = col === "other" ? other : tickets.filter((t) => t.status === col);
        const droppable = canDrop(col);
        return (
          <div
            key={col}
            className={cx("board-col", dragged && (droppable ? "can-drop" : "no-drop"), over === col && droppable && "drag-over")}
            onDragOver={(e) => {
              if (!droppable) return;
              e.preventDefault();
              e.dataTransfer.dropEffect = "move";
              if (over !== col) setOver(col);
            }}
            onDragLeave={(e) => {
              if (!e.currentTarget.contains(e.relatedTarget)) setOver((o) => (o === col ? null : o));
            }}
            onDrop={(e) => {
              e.preventDefault();
              const t = dragged;
              setOver(null);
              setDragId(null);
              if (t && droppable) onMove(t, col);
            }}
          >
            <div className="board-col-head">
              {col === "other" ? (
                <span className="badge status-badge tone-gray">
                  <span className="status-dot" />
                  Rejected / Cancelled
                </span>
              ) : (
                <StatusBadge status={col} />
              )}
              <span className="board-count">{items.length}</span>
            </div>
            <div className="board-col-body">
              {items.slice(0, 80).map((t) => {
                const draggable = allowedFor(t).length > 0;
                const comments = realComments(t).length;
                return (
                  <div
                    key={t._id}
                    className={cx("tk-card", highlighted.has(t._id) && "flash", dragId === t._id && "dragging", draggable && "draggable")}
                    draggable={draggable}
                    onDragStart={(e) => {
                      e.dataTransfer.effectAllowed = "move";
                      e.dataTransfer.setData("text/plain", t._id);
                      setDragId(t._id);
                    }}
                    onDragEnd={() => {
                      setDragId(null);
                      setOver(null);
                    }}
                    onClick={() => onOpen(t)}
                    role="button"
                    tabIndex={0}
                    onKeyDown={(e) => e.key === "Enter" && onOpen(t)}
                  >
                    <div className="tk-card-top">
                      <span className="tk-id">{t.displayId}</span>
                      <PriorityIcon p={t.priority || "medium"} />
                    </div>
                    <div className="tk-card-title">{t.title}</div>
                    <div className="tk-card-foot">
                      <DueChip due={t.dueDate} done={ticketIsFinished(t)} compact />
                      {comments > 0 && (
                        <span className="tk-comments">
                          <IC.Comment />
                          {comments}
                        </span>
                      )}
                      <span className="flex-spacer" />
                      <AssigneeCell item={t} users={users} showName={false} size={22} />
                    </div>
                  </div>
                );
              })}
              {items.length > 80 && <div className="board-more">+{items.length - 80} more — narrow the filters</div>}
              {items.length === 0 && <div className="board-empty">{droppable ? "Drop here" : "No tickets"}</div>}
            </div>
          </div>
        );
      })}
    </div>
  );
}

/* ═══════════════════════ DETAIL DRAWER ═══════════════════════ */
function TicketDetail({ ticket, loading, onClose, onPrev, onNext, position, onEdit, actions }) {
  const app = useApp();
  const { me, users, role } = app;
  const [busy, setBusy] = React.useState(false);
  useDetailKeys(!!ticket, { onPrev, onNext });
  if (!ticket) return <DetailNotFound what="ticket" onClose={onClose} loading={loading} />;
  const t = ticket;
  const perms = ticketPerms(t, app);
  const allowed = allowedTicketStatuses(t, perms, app);
  const steps = ticketNextSteps(t, perms, allowed);
  const run = async (fn) => {
    setBusy(true);
    try {
      await fn();
    } finally {
      setBusy(false);
    }
  };
  const copyLink = async () => {
    const url = `${location.origin}${location.pathname}#/tickets/${t._id}`;
    const ok = await copyText(url);
    app.toast({ type: ok ? "success" : "error", title: ok ? "Link copied" : "Couldn't copy the link", duration: 2500 });
  };
  const creator = t.createdBy || {};

  return (
    <div className="detail">
      <div className="drawer-hdr">
        <div className="drawer-crumb">
          <IC.Ticket />
          <span>Tickets</span>
          <IC.ChevronRight />
          <span className="tk-id">{t.displayId}</span>
        </div>
        <div className="drawer-actions">
          {position && (
            <span className="drawer-pos">
              {position.index + 1} / {position.total}
            </span>
          )}
          <button className="icon-btn" onClick={onPrev} disabled={!onPrev} title="Previous (K)" aria-label="Previous ticket">
            <IC.ChevronUp />
          </button>
          <button className="icon-btn" onClick={onNext} disabled={!onNext} title="Next (J)" aria-label="Next ticket">
            <IC.ChevronDown />
          </button>
          <button className="icon-btn" onClick={copyLink} title="Copy link" aria-label="Copy link">
            <IC.Link />
          </button>
          {(perms.edit || perms.del) && (
            <Dropdown
              width={200}
              placement="bottom-end"
              trigger={({ ref, toggle }) => (
                <button ref={ref} className="icon-btn" onClick={toggle} title="More actions" aria-label="More actions">
                  <IC.More />
                </button>
              )}
            >
              {({ close }) => (
                <MenuList>
                  {perms.edit && (
                    <MenuItem
                      icon={IC.Edit}
                      onSelect={() => {
                        close();
                        onEdit(t);
                      }}
                    >
                      Edit ticket
                    </MenuItem>
                  )}
                  {perms.del && (
                    <MenuItem
                      icon={IC.Trash}
                      danger
                      onSelect={async () => {
                        close();
                        if (await actions.remove(t)) onClose();
                      }}
                    >
                      Delete ticket
                    </MenuItem>
                  )}
                </MenuList>
              )}
            </Dropdown>
          )}
          <span className="hdr-sep" />
          <button className="icon-btn" onClick={onClose} title="Close (Esc)" aria-label="Close">
            <IC.X />
          </button>
        </div>
      </div>

      <div className="drawer-body">
        <div className="detail-main">
          <div className="detail-badges">
            <StatusBadge status={t.status} />
            <PriorityTag p={t.priority || "medium"} />
            {perms.shared && <DeptPill assignment={assignmentOf(t)} />}
            {perms.isAssignee && <span className="pill-me">Assigned to you</span>}
          </div>
          <h2 className="detail-title">{t.title}</h2>
          <div className="detail-sub">
            <Avatar name={creator.name} email={creator.email} size={20} />
            <span>
              <b>{creator.name || "Someone"}</b> opened this <span title={fmtDateTime(t.createdAt)}>{timeAgo(t.createdAt)}</span>
            </span>
            {perms.edit && (
              <button className="link-btn" onClick={() => onEdit(t)}>
                <IC.Edit /> Edit
              </button>
            )}
          </div>

          {steps.length > 0 && (
            <div className="detail-cta">
              {steps.map((s) => (
                <button key={s.next} className={cx("btn", s.cls)} disabled={busy} onClick={() => run(() => actions.setStatus(t, s.next))}>
                  <s.icon /> {s.label}
                </button>
              ))}
            </div>
          )}

          <CompletionLine completion={t.completion} verb="Resolved" />

          <section className="detail-section">
            <div className="detail-section-head">
              <h4>Description</h4>
            </div>
            <div className={cx("detail-desc", !t.description && "empty")}>
              {t.description ? <RichText text={t.description} /> : "No description provided."}
            </div>
          </section>

          <AssigneeProgress
            item={t}
            users={users}
            meUid={me.uid}
            canToggle={perms.toggleMine}
            busy={busy}
            onToggleMine={() => run(() => actions.toggleMine(t))}
          />

          <ActivitySection item={t} kind="ticket" me={me} onComment={(text) => actions.comment(t, text)} />
        </div>

        <aside className="detail-props">
          <div className="props-title">Details</div>
          <PropRow label="Status" icon={IC.Activity}>
            <StatusMenu status={t.status} allowed={allowed} onChange={(s) => actions.setStatus(t, s)} />
          </PropRow>
          <PropRow label="Priority" icon={IC.Flag}>
            <PriorityMenu value={t.priority || "medium"} disabled={!perms.edit} onChange={(v) => actions.setPriority(t, v)} />
          </PropRow>
          <PropRow label="Assignee" icon={IC.User}>
            <AssigneeMenu
              item={t}
              users={users}
              role={role}
              meUid={me.uid}
              disabled={!perms.assign}
              onChange={(a) => actions.setAssignment(t, a)}
              onAdvanced={() => onEdit(t)}
            />
          </PropRow>
          <PropRow label="Due date" icon={IC.Calendar}>
            {perms.edit ? (
              <DateField value={t.dueDate || ""} onChange={(d) => actions.setDue(t, d)} variant="prop" placeholder="Set due date" />
            ) : (
              <DueChip due={t.dueDate} done={ticketIsFinished(t)} empty={<span className="muted">No due date</span>} />
            )}
          </PropRow>
          <PropRow label="Category" icon={IC.Tag}>
            <CategoryMenu
              value={t.category || "General"}
              options={CFG.ticketCategories}
              disabled={!perms.edit}
              onChange={(c) => actions.setCategory(t, c)}
            />
          </PropRow>
          <PropRow label="Department" icon={IC.Layers}>
            <span className="prop-text">{(t.departments || [MY_DEPARTMENT]).map(deptLabelOf).join(", ")}</span>
          </PropRow>
          <div className="props-sep" />
          <PropRow label="Created by" icon={IC.User}>
            <span className="assignee-one">
              <Avatar name={creator.name} email={creator.email} size={20} />
              <span className="assignee-name">{creator.name || "—"}</span>
            </span>
          </PropRow>
          <PropRow label="Created" icon={IC.Clock}>
            <span className="prop-text" title={fmtDateTime(t.createdAt)}>
              {fmtDate(t.createdAt)} · {fmtTime(t.createdAt)}
            </span>
          </PropRow>
          <PropRow label="Updated" icon={IC.Refresh}>
            <span className="prop-text" title={fmtDateTime(lastActivityAt(t))}>
              {timeAgo(lastActivityAt(t))}
            </span>
          </PropRow>
          {t.completedAt && (
            <PropRow label="Resolved" icon={IC.CheckCircle}>
              <span className="prop-text" title={fmtDateTime(t.completedAt)}>
                {fmtDate(t.completedAt)}
              </span>
            </PropRow>
          )}
          {t.closedAt && (
            <PropRow label="Closed" icon={IC.Check}>
              <span className="prop-text" title={fmtDateTime(t.closedAt)}>
                {fmtDate(t.closedAt)}
              </span>
            </PropRow>
          )}
          <div className="props-hint">
            <Kbd>J</Kbd> <Kbd>K</Kbd> next / previous · <Kbd>Esc</Kbd> close
          </div>
        </aside>
      </div>
    </div>
  );
}

/* ═══════════════════════ CREATE / EDIT FORM ═══════════════════════ */
function TicketFormModal({ state, onClose, actions, categories, onCreated }) {
  const app = useApp();
  const { users, role, me, canEdit } = app;
  const open = !!state;
  const editing = state?.mode === "edit" ? state.ticket : null;
  const blank = () => ({
    title: "",
    description: "",
    priority: "medium",
    category: CFG.ticketCategories[0] || "General",
    dueDate: "",
    assignment: blankAssignment(),
  });
  const [form, setForm] = React.useState(blank);
  const [initial, setInitial] = React.useState(blank);
  const [errors, setErrors] = React.useState({});
  const [busy, setBusy] = React.useState(false);

  React.useEffect(() => {
    if (!open) return;
    const f = editing
      ? {
          title: editing.title || "",
          description: editing.description || "",
          priority: editing.priority || "medium",
          category: editing.category || "General",
          dueDate: editing.dueDate || "",
          assignment: assignmentOf(editing) || blankAssignment(),
        }
      : blank();
    setForm(f);
    setInitial(f);
    setErrors({});
    setBusy(false);
  }, [open, editing?._id]);

  const set = (k, v) => {
    setForm((f) => ({ ...f, [k]: v }));
    if (errors[k]) setErrors((e) => ({ ...e, [k]: null }));
  };
  const dirty = open && JSON.stringify(form) !== JSON.stringify(initial);

  const submit = async () => {
    const errs = {};
    if (!form.title.trim()) errs.title = "Give the ticket a short title.";
    else if (form.title.trim().length > 200) errs.title = "Keep the title under 200 characters.";
    const aErr = assignmentError(form.assignment);
    if (aErr) errs.assignment = aErr;
    setErrors(errs);
    if (Object.keys(errs).length) return;
    setBusy(true);
    try {
      if (editing) {
        const changed = await actions.saveEdits(editing, form);
        app.toast({ type: "success", title: changed ? `${editing.displayId} updated` : "No changes to save", duration: 3000 });
      } else {
        const t = await actions.create(form);
        onCreated?.(t);
      }
      setInitial(form);
      onClose();
    } catch (e) {
      console.error(e);
      app.toast({ type: "error", title: editing ? "Couldn't save the ticket" : "Couldn't create the ticket", msg: writeErrorMsg(e), duration: 8000 });
    } finally {
      setBusy(false);
    }
  };

  return (
    <Modal
      open={open}
      onClose={onClose}
      dirty={dirty && !busy}
      size="lg"
      icon={editing ? IC.Edit : IC.Ticket}
      title={editing ? `Edit ${editing.displayId}` : "New ticket"}
      subtitle={editing ? "Changes are recorded in the ticket's history." : "Describe the request — you can assign it now or later."}
      footer={
        <>
          <span className="modal-footer-hint">
            <Kbd>Ctrl</Kbd> + <Kbd>Enter</Kbd> to save
          </span>
          <button className="btn btn-ghost" onClick={onClose} disabled={busy}>
            Cancel
          </button>
          <button className="btn btn-primary" onClick={submit} disabled={busy}>
            {busy ? <Spinner size={14} /> : <IC.Check />}
            {editing ? "Save changes" : "Create ticket"}
          </button>
        </>
      }
    >
      <div
        className="form-stack"
        onKeyDown={(e) => {
          if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
            e.preventDefault();
            submit();
          }
        }}
      >
        <Field label="Title" required error={errors.title}>
          <input
            className="form-input input-lg"
            placeholder="e.g. Salary difference in the March payslip"
            value={form.title}
            maxLength={220}
            onChange={(e) => set("title", e.target.value)}
            data-autofocus
          />
        </Field>
        <Field label="Description" hint="Add context, steps, amounts or links — anything that helps resolve it.">
          <AutosizeTextarea
            minRows={4}
            maxRows={12}
            placeholder="What's happening, who is affected, and what's needed?"
            value={form.description}
            onChange={(e) => set("description", e.target.value)}
          />
        </Field>
        <Field label="Priority">
          <PrioritySelector value={form.priority} onChange={(v) => set("priority", v)} />
        </Field>
        <div className="form-grid">
          <Field label="Category">
            <select className="form-select" value={form.category} onChange={(e) => set("category", e.target.value)}>
              {categories.map((c) => (
                <option key={c} value={c}>
                  {c}
                </option>
              ))}
            </select>
          </Field>
          <Field label="Due date">
            <DateField value={form.dueDate} onChange={(d) => set("dueDate", d)} placeholder="No due date" variant="input" />
          </Field>
        </div>
        <Field label="Assign to" error={errors.assignment}>
          <AssignmentPicker
            value={form.assignment}
            onChange={(a) => set("assignment", a)}
            users={users}
            role={role}
            meUid={me.uid}
            canAssignBeyondSelf={canEdit}
          />
        </Field>
      </div>
    </Modal>
  );
}
