/* Recruitment Desk — the product's own routing and its commit machinery. Every
   action that changes anything goes through this file, so the reversibility
   tiers are decided in one place: harmless controls (watch, react) live on their
   screens, consequential ones apply here and raise an undo toast, destructive
   ones open a confirm dialog naming the consequence, and anything needing facts
   the app does not hold opens a staged dialog first.

   The chains that matter, because they are what actually happens on a desk:
   · Booking the panel writes the interview AND moves the stage, so the ladder,
     the stepper and the badge move together.
   · Starting the checks writes the two reference requests the policy demands, so
     "1 of 2 back" is a real count from a real array rather than a label.
   · Recording an acceptance fills a seat: when the accepted offers reach the
     requisition's headcount the requisition itself becomes filled, and the
     seats figure on /requisitions moves without anything being retyped.
   · Opening an application marks it read, which is the only thing that makes
     the unread figure on the desk true. */
function App(props) {
  const options = props || {};
  const [path, setPath] = React.useState(options.startPath || "/");
  const [dark, setDark] = React.useState(options.theme === "dark");
  /* Folded by default: the desk's own screens are wide, and the destinations
     are seven icons a recruiter learns in a day. null would follow the viewport;
     true is the product's chosen default, and the reader's chevron overrides it
     either way for the session. */
  const [fold, setFold] = React.useState(true);
  const [session] = React.useState(RD_SESSION);
  const [read, setRead] = React.useState(false);
  const [toasts, setToasts] = React.useState([]);
  const [confirm, setConfirm] = React.useState(null);
  const [form, setForm] = React.useState(null);
  const [attempts, setAttempts] = React.useState(0);
  /* Settings state. `policy` is the RD_POLICY object itself: there is one copy of
     those numbers and every metric reads it, so saving mutates it and bumps a
     counter to re-render rather than threading a second copy through the tree. */
  const [policyVersion, setPolicyVersion] = React.useState(0);
  const [desk, setDesk] = React.useState({
    name: RD_ORG.name,
    replyTo: "resourcing@" + RD_ORG.domain,
    signature: "Resourcing team\nLarkspur Health\nWe answer every application, including the ones we decline.",
  });
  const [channels, setChannels] = React.useState({
    enabled: ["careers_site", "nhs_jobs", "referral", "agency", "event"],
    defaults: ["careers_site", "nhs_jobs"],
    reviewDays: 28,
    autoUnpost: true,
  });
  const [preferences, setPreferences] = React.useState({
    notifyMentions: true, notifyScorecards: true, notifyOffers: true, notifyStalled: false,
    defaultView: "ours", onlyMine: false,
  });
  const [data, setData] = React.useState({
    staff: STAFF, requisitions: REQUISITIONS, applications: APPLICATIONS,
    interviews: INTERVIEWS, notes: NOTES, adverts: ADVERTS, notifications: NOTIFICATIONS,
  });
  const palette = useCommandPalette();
  const now = RD_NOW;
  const me = staffById(data.staff, session.personId);

  React.useEffect(() => {
    document.documentElement.classList.toggle("dark", dark);
    document.body.classList.toggle("dark", dark);
  }, [dark]);

  /* Opening an application is what makes it read. */
  React.useEffect(() => {
    const match = path.match(/^\/applications\/(ap-[^/]+)$/);
    if (!match) return;
    const application = applicationById(data.applications, match[1]);
    if (application && !application.firstReadAt) patch("applications", application.id, { firstReadAt: now });
  }, [path]);

  const dismiss = (id) => setToasts((list) => list.filter((t) => t.id !== id));
  const pushToast = (toast) => {
    const id = "t" + Date.now() + Math.random().toString(36).slice(2, 6);
    setToasts((list) => list.concat([Object.assign({ id }, toast)]));
    window.setTimeout(() => dismiss(id), 9000);
  };

  /* ── Writing into the fixture ──────────────────────────────────────────── */
  const patch = (key, id, fields) => setData((d) => Object.assign({}, d, { [key]: d[key].map((r) => (r.id === id ? Object.assign({}, r, fields) : r)) }));
  const patchMany = (key, ids, fields) => setData((d) => Object.assign({}, d, { [key]: d[key].map((r) => (ids.indexOf(r.id) > -1 ? Object.assign({}, r, fields) : r)) }));
  const snapshot = () => data;
  const restore = (before) => setData(before);
  const parseMoney = (value) => Math.round(parseFloat(String(value).replace(/[^0-9.]/g, "")) * 100);
  const parseDay = (value) => (value ? d0(value) : null);
  const dayValue = (ts) => dayKey(ts);
  const addNote = (applicationId, body, kind) => ({
    id: "nt-" + Math.random().toString(36).slice(2, 7), applicationId, authorId: session.personId,
    at: now, body, kind: kind || "note", parentId: null, reactions: {}, seenBy: [],
  });

  /* ── Applications ─────────────────────────────────────────────────────── */
  const moveStage = (application, stage, extra) => {
    patch("applications", application.id, Object.assign({
      stage, stageEnteredAt: now, firstReadAt: application.firstReadAt || now,
    }, extra || {}));
  };

  const onApplicationTransition = (application, transition) => {
    const requisition = requisitionById(data.requisitions, application.requisitionId);
    const first = application.candidate.name.split(" ")[0];

    if (transition.id === "advance" && transition.to === "screening") {
      const before = snapshot();
      moveStage(application, "screening");
      pushToast({
        variant: "success", title: first + " is in screening",
        description: "Read and moved on " + formatDay(now) + ". " + RD_POLICY.stageDays.screening + " days is the service level for this step, so it is due by " + formatDayShort(now + RD_POLICY.stageDays.screening * DAY) + ".",
        onUndo: () => restore(before),
      });
      return;
    }

    if (transition.id === "advance" && transition.to === "assessment") {
      const before = snapshot();
      moveStage(application, "assessment");
      pushToast({
        variant: "success", title: "Assessment sent to " + first,
        description: (requisition.department === "digital" ? "Integration exercise" : "Case study") + " for " + requisition.title + ". Due back inside " + RD_POLICY.stageDays.assessment + " days; the queue will show it as over after that.",
        onUndo: () => restore(before),
      });
      return;
    }

    if (transition.id === "advance" && transition.to === "panel") {
      bookInterview(application, "panel", "Book the panel for " + application.candidate.name,
        "Booking it moves the application to panel. " + RD_POLICY.panelScorecards + " scorecards from different people are needed before checks can start.");
      return;
    }

    if (transition.id === "book_interview") {
      bookInterview(application, application.stage === "panel" ? "panel" : "assessment", "Book another conversation with " + application.candidate.name,
        "The stage does not change — this is a second conversation inside " + statusValue(APPLICATION_STAGE, application.stage).label.toLowerCase() + ".", true);
      return;
    }

    if (transition.id === "advance" && transition.to === "checks") {
      const score = scoresOf(application);
      setForm({
        title: "Start the checks on " + application.candidate.name,
        description: score.average.toFixed(2) + " over " + plural(score.count, "scorecard", "scorecards") + ". " + RD_POLICY.referencesRequired
          + " references are required and one must be the current employer. Nothing can be offered until both are back and the right to work is verified.",
        submitLabel: "Request the references",
        initial: {},
        fields: [
          { id: "ref1", kind: "text", label: "Current employer's reference", placeholder: "Name and where they work" },
          { id: "rel1", kind: "text", label: "Their relationship to the candidate", placeholder: "Current line manager, Meadowvale Trust" },
          { id: "ref2", kind: "text", label: "Second reference", placeholder: "Name and where they work" },
          { id: "rel2", kind: "text", label: "Their relationship to the candidate", placeholder: "Clinical supervisor" },
        ],
        validate: (v) => {
          const errors = {};
          if (!v.ref1 || v.ref1.trim().length < 4) errors.ref1 = "Who is the current employer's referee?";
          if (!v.rel1 || v.rel1.trim().length < 4) errors.rel1 = "Say how they know the candidate.";
          if (!v.ref2 || v.ref2.trim().length < 4) errors.ref2 = "A second referee is required — one is not enough to hire on.";
          if (!v.rel2 || v.rel2.trim().length < 4) errors.rel2 = "Say how they know the candidate.";
          return errors;
        },
        run: (v) => {
          const before = snapshot();
          const references = [
            { id: "rf-" + Math.random().toString(36).slice(2, 7), name: v.ref1.trim(), relationship: v.rel1.trim(), status: "requested", requestedAt: now, receivedAt: null, note: null },
            { id: "rf-" + Math.random().toString(36).slice(2, 7), name: v.ref2.trim(), relationship: v.rel2.trim(), status: "requested", requestedAt: now, receivedAt: null, note: null },
          ];
          moveStage(application, "checks", { references });
          setData((d) => Object.assign({}, d, { notes: d.notes.concat([addNote(application.id, "Checks started. References requested from " + v.ref1.trim() + " and " + v.ref2.trim() + ". No offer until both are back.", "decision")]) }));
          pushToast({
            variant: "success", title: "Checks started on " + first,
            description: "Two references requested, " + RD_POLICY.stageDays.checks + " days on the clock. Right to work is " + statusWords(RIGHT_TO_WORK, application.rightToWork) + ".",
            onUndo: () => restore(before),
          });
        },
      });
      return;
    }

    if (transition.id === "advance" && transition.to === "offer") {
      setForm({
        title: "Offer " + application.candidate.name + " the " + requisition.title.toLowerCase(),
        description: "Band is " + bandLabel(requisition) + ". They asked for "
          + (application.salaryExpectationMinor ? money(application.salaryExpectationMinor) : "nothing in particular")
          + ". Target start on the requisition is " + formatDay(requisition.targetStartAt) + ".",
        submitLabel: "Send the offer",
        initial: { salary: String(Math.round(requisition.bandMinor[0] / 100)), start: dayValue(requisition.targetStartAt), expires: dayValue(now + RD_POLICY.offerValidDays * DAY) },
        preview: (v) => {
          const minor = parseMoney(v.salary || "0");
          if (!minor || isNaN(minor)) return null;
          const { Alert, AlertTitle, AlertDescription } = window.OriginDesignSystem_7c998b;
          const position = percent(bandPosition(requisition, minor));
          const over = application.salaryExpectationMinor && minor < application.salaryExpectationMinor;
          return (
            <Alert variant={minor > requisition.bandMinor[1] ? "warning" : "info"}>
              <Icon name={minor > requisition.bandMinor[1] ? "triangle-alert" : "badge-pound-sterling"} size={16} />
              <AlertTitle>{money(minor)} — {position} up the band</AlertTitle>
              <AlertDescription>
                {minor > requisition.bandMinor[1]
                  ? "Above the top of the band. Amara Boateng signs those off before the letter goes out."
                  : over ? money(application.salaryExpectationMinor - minor) + " below what they asked for. Worth a call before the letter." : "Inside the band and at or above what they asked for."}
              </AlertDescription>
            </Alert>
          );
        },
        fields: [
          { id: "salary", kind: "text", label: "Annual salary", hint: "Whole pounds, the way the contract states it." },
          { id: "start", kind: "day", label: "Start date" },
          { id: "expires", kind: "day", label: "Valid until", hint: RD_POLICY.offerValidDays + " days is the standard on a Larkspur offer." },
          { id: "note", kind: "area", label: "Anything agreed alongside it", placeholder: "Recruitment premium, notice period, a start date they asked for." },
        ],
        validate: (v) => {
          const errors = {};
          const minor = parseMoney(v.salary || "");
          if (!minor || isNaN(minor)) errors.salary = "Enter the salary in whole pounds.";
          else if (minor < requisition.bandMinor[0] * 0.9) errors.salary = "That is below the bottom of the band — check the figure.";
          if (!parseDay(v.start)) errors.start = "Give the start date.";
          if (!parseDay(v.expires)) errors.expires = "Say when the offer runs out.";
          else if (parseDay(v.expires) < now) errors.expires = "That date has passed.";
          return errors;
        },
        run: (v) => {
          const before = snapshot();
          const offer = {
            salaryMinor: parseMoney(v.salary), startAt: parseDay(v.start), sentAt: now,
            expiresAt: d1(v.expires), acceptedAt: null, note: (v.note || "").trim(),
          };
          moveStage(application, "offer", { offer });
          pushToast({
            variant: "success", title: "Offer out to " + first,
            description: money(offer.salaryMinor) + " to start " + formatDay(offer.startAt) + ", valid " + durationWords(daysBetween(now, offer.expiresAt)) + ". It is with them now, not with us.",
            onUndo: () => restore(before),
          });
        },
      });
      return;
    }

    if (transition.id === "advance" && transition.to === "hired") {
      const health = requisitionHealth(data.applications, requisition, now);
      const fills = health.accepted + 1 >= requisition.headcount;
      setConfirm({
        title: "Record " + application.candidate.name + " as hired?",
        description: "The offer was accepted on " + formatDay(application.offer.acceptedAt) + ". Recording it closes the application and hands the person to onboarding.",
        confirmLabel: "Record the hire",
        rows: [
          { key: "app", label: "Application", value: "Hired · starting " + formatDay(application.offer.startAt) },
          { key: "seat", label: "Seats on " + requisition.ref, value: (health.accepted + 1) + " of " + requisition.headcount + (fills ? " — the requisition becomes filled" : " — " + plural(requisition.headcount - health.accepted - 1, "seat", "seats") + " still open") },
          { key: "pipeline", label: "Others in the pipeline", value: plural(health.live.length - 1, "application", "applications") + " stay live" },
        ],
        run: () => {
          const before = snapshot();
          moveStage(application, "hired");
          if (fills) patch("requisitions", requisition.id, { status: "filled" });
          pushToast({
            variant: "success", title: first + " is hired",
            description: "Starting " + formatDay(application.offer.startAt) + ". " + (fills ? requisition.ref + " is now filled." : plural(requisition.headcount - health.accepted - 1, "seat", "seats") + " left on " + requisition.ref + "."),
            onUndo: () => restore(before),
          });
        },
      });
      return;
    }

    if (transition.id === "decline") {
      const score = scoresOf(application);
      setForm({
        title: "Decline " + application.candidate.name,
        description: (score.count ? "Averaging " + score.average.toFixed(2) + " over " + plural(score.count, "scorecard", "scorecards") + ". " : "No scorecards on file. ")
          + "The reason and whether we would talk again are what the pool keeps.",
        submitLabel: "Continue",
        initial: { again: false },
        fields: [
          { id: "reason", kind: "radio", label: "Why?", options: DECLINE_REASONS },
          { id: "again", kind: "toggle", label: "For a future seat", toggleLabel: "We would talk to them again" },
          { id: "note", kind: "area", label: "What we would say", placeholder: "Specific, so the next person reading this file knows what happened.", hint: "Never sent automatically — it is for us, and it is what the talent pool shows." },
        ],
        validate: (v) => {
          const errors = {};
          if (!v.reason) errors.reason = "Pick the reason.";
          if (!v.note || v.note.trim().length < 15) errors.note = "One sentence at least — this is the whole record.";
          return errors;
        },
        run: (v) => setConfirm({
          title: "Decline " + application.candidate.name + "?",
          description: "They come off the pipeline for " + requisition.title + ". " + (v.again ? "They stay in the talent pool and can be reopened for another seat." : "They will not appear in the talent pool, so reopening means a fresh application."),
          confirmLabel: "Decline the application",
          destructive: true,
          rows: [
            { key: "reason", label: "Reason", value: labelOf(DECLINE_REASONS, v.reason) },
            { key: "pool", label: "Talent pool", value: v.again ? "Kept" : "Not kept" },
            { key: "iv", label: "Booked interviews", value: interviewsFor(data.interviews, application.id).filter(INTERVIEW_FLAGS.isBooked).length ? "Cancelled" : "None" },
          ],
          run: () => {
            const booked = interviewsFor(data.interviews, application.id).filter(INTERVIEW_FLAGS.isBooked);
            patch("applications", application.id, {
              stage: "declined", stageEnteredAt: now, lastLiveStage: application.stage,
              declineReason: v.reason, wouldReconsider: !!v.again, declineNote: v.note.trim(),
              decidedAt: now, decidedById: session.personId,
            });
            if (booked.length) patchMany("interviews", booked.map((iv) => iv.id), { status: "cancelled" });
            setData((d) => Object.assign({}, d, { notes: d.notes.concat([addNote(application.id, v.note.trim(), "decision")]) }));
            pushToast({
              title: "Declined · " + application.candidate.name,
              description: labelOf(DECLINE_REASONS, v.reason) + (v.again ? " · in the talent pool" : " · not kept") + (booked.length ? " · " + plural(booked.length, "interview", "interviews") + " cancelled" : ""),
            });
          },
        }),
      });
      return;
    }

    if (transition.id === "withdraw") {
      setForm({
        title: application.candidate.name + " has withdrawn",
        description: "Their decision, not ours. What they said is the most useful thing on this file — it is what the pool shows if we go back to them.",
        submitLabel: "Record the withdrawal",
        initial: { again: true },
        fields: [
          { id: "reason", kind: "radio", label: "Why?", options: WITHDRAW_REASONS },
          { id: "again", kind: "toggle", label: "For a future seat", toggleLabel: "They asked to be kept on file" },
          { id: "note", kind: "area", label: "What they said", placeholder: "Another offer, the travel, the rota, the money." },
        ],
        validate: (v) => {
          const errors = {};
          if (!v.reason) errors.reason = "Pick the closest reason.";
          if (!v.note || v.note.trim().length < 10) errors.note = "Write down what they told you.";
          return errors;
        },
        run: (v) => {
          const before = snapshot();
          patch("applications", application.id, {
            stage: "withdrawn", stageEnteredAt: now, lastLiveStage: application.stage,
            withdrawReason: v.reason, wouldReconsider: !!v.again, withdrawNote: v.note.trim(),
            decidedAt: now, decidedById: session.personId,
          });
          pushToast({
            title: first + " has withdrawn",
            description: labelOf(WITHDRAW_REASONS, v.reason) + ". " + requisition.title + " is down to " + plural(requisitionHealth(data.applications, requisition, now).live.length - 1, "live application", "live applications") + ".",
            onUndo: () => restore(before),
          });
        },
      });
      return;
    }

    if (transition.id === "reopen") {
      const open = data.requisitions.filter((r) => r.status === "open");
      setForm({
        title: "Reopen " + application.candidate.name + " for another seat",
        description: "This is their own record, moved — not a copy. Their documents, notes and history come with them. Reached " + statusValue(APPLICATION_STAGE, application.lastLiveStage || "applied").label.toLowerCase() + " last time.",
        submitLabel: "Reopen the application",
        initial: { requisitionId: open[0] ? open[0].id : "", stage: "screening" },
        fields: [
          { id: "requisitionId", kind: "select", label: "Which seat?", options: open.map((r) => ({ value: r.id, label: r.ref + " · " + r.title + " · " + labelOf(SITES, r.site) })) },
          { id: "stage", kind: "radio", label: "Coming back in at", options: [{ value: "applied", label: "Applied — start again from the top" }, { value: "screening", label: "Screening — we have already read them" }] },
          { id: "note", kind: "area", label: "Why now", placeholder: "What changed: a new seat, a new band, they got the experience they were missing." },
        ],
        validate: (v) => {
          const errors = {};
          if (!v.requisitionId) errors.requisitionId = "Pick the seat.";
          if (!v.note || v.note.trim().length < 10) errors.note = "Say what changed.";
          return errors;
        },
        run: (v) => {
          const before = snapshot();
          patch("applications", application.id, {
            requisitionId: v.requisitionId, stage: v.stage, stageEnteredAt: now, lastLiveStage: null,
            declineReason: null, declineNote: null, withdrawReason: null, withdrawNote: null, decidedAt: null, decidedById: null,
          });
          setData((d) => Object.assign({}, d, { notes: d.notes.concat([addNote(application.id, v.note.trim(), "decision")]) }));
          pushToast({
            variant: "success", title: first + " is live again",
            description: requisitionById(data.requisitions, v.requisitionId).title + " at " + statusValue(APPLICATION_STAGE, v.stage).label.toLowerCase() + ". They are out of the talent pool.",
            onUndo: () => restore(before),
          });
        },
      });
    }
  };

  /* One booking dialog, used by the advance-to-panel move, the "book another"
     action and the header button on /interviews. */
  const bookInterview = (application, kind, title, description, keepStage) => {
    const requisition = requisitionById(data.requisitions, application.requisitionId);
    const candidates = data.staff.filter((p) => p.role === "hiring_manager" || p.role === "panel" || p.role === "recruiter");
    setForm({
      title, description,
      submitLabel: "Book it",
      initial: {
        day: dayValue(now + 4 * DAY), time: "10:00", minutes: "60",
        panel: kind === "screening" ? [session.personId] : [requisition.hiringManagerId],
        where: kind === "screening" ? "Telephone" : "Larkspur House, room 2.4",
      },
      fields: [
        { id: "day", kind: "day", label: "Date" },
        { id: "time", kind: "time", label: "Start time" },
        { id: "minutes", kind: "select", label: "Length", options: [{ value: "30", label: "30 minutes" }, { value: "60", label: "60 minutes" }, { value: "90", label: "90 minutes" }] },
        { id: "panel", kind: "checks", label: "Who is on it", options: candidates.map((p) => ({ value: p.id, label: p.name, note: p.title })) },
        { id: "where", kind: "text", label: "Where" },
      ],
      validate: (v) => {
        const errors = {};
        const when = v.day && v.time ? new Date(v.day + "T" + v.time).getTime() : null;
        if (!when) errors.day = "Pick a date and a time.";
        else if (when < now) errors.day = "That is in the past.";
        if (!v.panel || !v.panel.length) errors.panel = "Somebody has to be in the room.";
        else if (kind === "panel" && v.panel.length < RD_POLICY.panelScorecards) errors.panel = RD_POLICY.panelScorecards + " people are needed on a panel — that is where the two scorecards come from.";
        if (!v.where || !v.where.trim()) errors.where = "Where is it? A room, a site or the telephone.";
        return errors;
      },
      run: (v) => {
        const before = snapshot();
        const when = new Date(v.day + "T" + v.time).getTime();
        const interview = {
          id: "iv-" + Math.random().toString(36).slice(2, 7), applicationId: application.id, kind,
          at: when, minutes: Number(v.minutes), panelIds: v.panel, status: "scheduled", where: v.where.trim(), note: null,
        };
        setData((d) => Object.assign({}, d, { interviews: d.interviews.concat([interview]) }));
        if (!keepStage) moveStage(application, kind === "panel" ? "panel" : "assessment");
        pushToast({
          variant: "success",
          title: (kind === "panel" ? "Panel" : kind === "assessment" ? "Assessment" : "Screening call") + " booked · " + application.candidate.name,
          description: formatWeekday(when) + " at " + formatTime(when) + ", " + v.minutes + " minutes, " + v.panel.map((id) => firstNameOf(staffById(data.staff, id))).join(" and ") + ". " + (keepStage ? "The stage has not changed." : "The application is at " + (kind === "panel" ? "panel" : "assessment") + "."),
          onUndo: () => restore(before),
        });
      },
    });
  };

  const onBookInterview = () => {
    const live = liveApplications(data.applications).filter(APPLICATION_FLAGS.awaitsUs);
    setForm({
      title: "Book an interview",
      description: "Only the " + live.length + " applications the desk owes a move on can be booked. Pick one and the right kind of conversation for its stage.",
      submitLabel: "Choose the application",
      initial: { applicationId: live[0] ? live[0].id : "" },
      fields: [{ id: "applicationId", kind: "select", label: "Application", options: live.map((a) => ({ value: a.id, label: a.candidate.name + " · " + statusValue(APPLICATION_STAGE, a.stage).label + " · " + requisitionById(data.requisitions, a.requisitionId).title })) }],
      validate: (v) => (v.applicationId ? {} : { applicationId: "Pick the application." }),
      run: (v) => {
        const application = applicationById(data.applications, v.applicationId);
        const kind = application.stage === "panel" ? "panel" : application.stage === "assessment" ? "assessment" : "screening";
        bookInterview(application, kind, "Book a " + (kind === "screening" ? "screening call" : kind) + " with " + application.candidate.name,
          "The application stays at " + statusValue(APPLICATION_STAGE, application.stage).label.toLowerCase() + "; booking a conversation does not move it on its own.", true);
      },
    });
  };

  /* ── Batches: they say exactly what they are about to do, and ask first ── */
  const onBulkAdvance = (applications, clear) => {
    if (!applications.length) return;
    setConfirm({
      title: "Move " + plural(applications.length, "application", "applications") + " on?",
      description: "Each one moves to the next step of its own process, dated today, in your name. Undo restores the batch.",
      confirmLabel: "Move " + applications.length + " on",
      rows: applications.map((a) => ({
        key: a.id, label: a.candidate.name,
        value: statusValue(APPLICATION_STAGE, a.stage).label + " → " + statusValue(APPLICATION_STAGE, STAGE_AFTER[a.stage]).label,
      })),
      run: () => {
        const before = snapshot();
        applications.forEach((a) => moveStage(a, STAGE_AFTER[a.stage]));
        clear();
        pushToast({
          variant: "success", title: plural(applications.length, "application", "applications") + " moved on",
          description: applications.map((a) => a.candidate.name.split(" ")[0]).join(", ") + ". Anything that needed a booking or a reference request is still waiting for it.",
          onUndo: () => restore(before),
        });
      },
    });
  };

  const onBulkDecline = (applications, clear) => {
    if (!applications.length) return;
    setForm({
      title: "Decline " + plural(applications.length, "application", "applications"),
      description: "One reason and one note for all of them. If they need different reasons, decline them one at a time.",
      submitLabel: "Continue",
      initial: { again: false },
      fields: [
        { id: "reason", kind: "radio", label: "Why?", options: DECLINE_REASONS },
        { id: "again", kind: "toggle", label: "For a future seat", toggleLabel: "We would talk to all of them again" },
        { id: "note", kind: "area", label: "What we would say", placeholder: "One field was much stronger than the rest — say so plainly." },
      ],
      validate: (v) => {
        const errors = {};
        if (!v.reason) errors.reason = "Pick the reason.";
        if (!v.note || v.note.trim().length < 15) errors.note = "One sentence at least.";
        return errors;
      },
      run: (v) => setConfirm({
        title: "Decline " + plural(applications.length, "application", "applications") + "?",
        description: "All of them come off their pipelines with the same reason and note. " + (v.again ? "All stay in the talent pool." : "None are kept in the pool."),
        confirmLabel: "Decline " + applications.length,
        destructive: true,
        rows: applications.map((a) => ({ key: a.id, label: a.candidate.name, value: statusValue(APPLICATION_STAGE, a.stage).label })),
        run: () => {
          const ids = applications.map((a) => a.id);
          setData((d) => Object.assign({}, d, {
            applications: d.applications.map((a) => (ids.indexOf(a.id) > -1 ? Object.assign({}, a, {
              stage: "declined", stageEnteredAt: now, lastLiveStage: a.stage, declineReason: v.reason,
              wouldReconsider: !!v.again, declineNote: v.note.trim(), decidedAt: now, decidedById: session.personId,
            }) : a)),
          }));
          clear();
          pushToast({
            title: plural(applications.length, "application", "applications") + " declined",
            description: labelOf(DECLINE_REASONS, v.reason) + ". " + (v.again ? "All " + applications.length + " are in the talent pool." : "None kept in the pool."),
          });
        },
      }),
    });
  };

  /* ── Interviews ───────────────────────────────────────────────────────── */
  const onInterviewTransition = (interview, transition) => {
    const application = applicationById(data.applications, interview.applicationId);
    const first = application.candidate.name.split(" ")[0];

    if (transition.id === "record_held") {
      const before = snapshot();
      patch("interviews", interview.id, { status: "awaiting_feedback" });
      pushToast({
        title: "Held · " + first,
        description: interview.kind === "panel"
          ? RD_POLICY.panelScorecards + " scorecards are needed from " + panelOf(data.staff, interview).map(firstNameOf).join(" and ") + ". It is on the outstanding list until they are in."
          : "Waiting on " + panelOf(data.staff, interview).map(firstNameOf).join(" and ") + " to write it up.",
        onUndo: () => restore(before),
      });
      return;
    }

    if (transition.id === "add_scorecard") {
      setForm({
        title: "Your scorecard for " + application.candidate.name,
        description: "Four areas, the same four on every requisition, so two cards can be read side by side. " + scoresOf(application).count + " of " + RD_POLICY.panelScorecards + " in so far.",
        submitLabel: "File the scorecard",
        initial: { scores: {}, recommend: "yes" },
        fields: [
          { id: "scores", kind: "scores", label: "Scores, 1 to 5" },
          { id: "recommend", kind: "radio", label: "Would you have them?", options: [{ value: "yes", label: "Yes" }, { value: "maybe", label: "Not sure" }, { value: "no", label: "No" }] },
          { id: "note", kind: "area", label: "What you saw", placeholder: "The question you asked and what they actually said. Specific beats generous.", hint: "The panel reads this, and so does whoever picks the file up in six months." },
        ],
        validate: (v) => {
          const errors = {};
          const scores = v.scores || {};
          if (SCORE_AREAS.some((area) => !scores[area.value])) errors.scores = "Score all four areas.";
          if (!v.note || v.note.trim().length < 20) errors.note = "A sentence or two — a bare score is not a scorecard.";
          return errors;
        },
        run: (v) => {
          const before = snapshot();
          const card = { byId: session.personId, at: now, scores: v.scores, note: v.note.trim(), recommend: v.recommend };
          const cards = (application.scorecards || []).concat([card]);
          patch("applications", application.id, { scorecards: cards });
          patch("interviews", interview.id, { status: "complete" });
          const score = scoresOf(Object.assign({}, application, { scorecards: cards }));
          pushToast({
            variant: "success", title: "Scorecard filed · " + first,
            description: score.count + " of " + RD_POLICY.panelScorecards + " in, averaging " + score.average.toFixed(2) + ". "
              + (score.complete ? "Checks can start." : "One more is needed before this can move."),
            onUndo: () => restore(before),
          });
        },
      });
      return;
    }

    if (transition.id === "reschedule") {
      setForm({
        title: "Reschedule the " + interview.kind + " with " + application.candidate.name,
        description: "Currently " + formatWeekday(interview.at) + " at " + formatTime(interview.at) + " · " + interview.where + ". The panel and the length stay as they are.",
        submitLabel: "Move it",
        initial: { day: dayValue(interview.at), time: formatTime(interview.at) },
        fields: [
          { id: "day", kind: "day", label: "New date" },
          { id: "time", kind: "time", label: "New start time" },
          { id: "why", kind: "area", label: "Why", placeholder: "Who asked, and what it clashed with.", hint: "Posted on the thread so the panel sees it." },
        ],
        validate: (v) => {
          const errors = {};
          const when = v.day && v.time ? new Date(v.day + "T" + v.time).getTime() : null;
          if (!when) errors.day = "Pick a date and a time.";
          else if (when < now) errors.day = "That is in the past.";
          if (!v.why || v.why.trim().length < 8) errors.why = "Say why it moved.";
          return errors;
        },
        run: (v) => {
          const before = snapshot();
          const when = new Date(v.day + "T" + v.time).getTime();
          patch("interviews", interview.id, { at: when, note: v.why.trim() });
          setData((d) => Object.assign({}, d, { notes: d.notes.concat([addNote(application.id, "Moved the " + interview.kind + " to " + formatWeekday(when) + " at " + formatTime(when) + ". " + v.why.trim(), "note")]) }));
          pushToast({ title: "Moved · " + first, description: formatWeekday(when) + " at " + formatTime(when) + ", " + interview.where + ".", onUndo: () => restore(before) });
        },
      });
      return;
    }

    if (transition.id === "record_no_show") {
      const before = snapshot();
      patch("interviews", interview.id, { status: "no_show" });
      pushToast({
        title: "No show recorded · " + first,
        description: "The application stays at " + statusValue(APPLICATION_STAGE, application.stage).label.toLowerCase() + " and nothing is decided by a missed call. Book another before you draw a conclusion.",
        onUndo: () => restore(before),
      });
      return;
    }

    if (transition.id === "cancel_interview") {
      setConfirm({
        title: "Cancel the " + interview.kind + " with " + application.candidate.name + "?",
        description: "The slot goes back to the panel and the candidate has to be told. The application does not move.",
        confirmLabel: "Cancel the interview",
        destructive: true,
        rows: [
          { key: "when", label: "Was booked", value: formatWeekday(interview.at) + " at " + formatTime(interview.at) },
          { key: "panel", label: "Panel", value: panelOf(data.staff, interview).map((p) => p.name).join(", ") },
          { key: "stage", label: "Application", value: "Stays at " + statusValue(APPLICATION_STAGE, application.stage).label.toLowerCase() },
        ],
        run: () => {
          patch("interviews", interview.id, { status: "cancelled" });
          pushToast({ title: "Cancelled · " + first, description: "Tell them today — it was " + formatWeekday(interview.at) + "." });
        },
      });
    }
  };

  /* ── Requisitions ─────────────────────────────────────────────────────── */
  const onRequisitionTransition = (requisition, transition) => {
    const health = requisitionHealth(data.applications, requisition, now);

    if (transition.id === "open_req") {
      const before = snapshot();
      patch("requisitions", requisition.id, { status: "open", openedAt: now });
      pushToast({
        variant: "success", title: requisition.title + " is open",
        description: plural(requisition.headcount, "seat", "seats") + ", target start " + formatDay(requisition.targetStartAt) + ". It accepts applications from today.",
        onUndo: () => restore(before),
      });
      return;
    }
    if (transition.id === "hold_req") {
      setForm({
        title: "Put " + requisition.title + " on hold",
        description: plural(health.live.length, "live application", "live applications") + " stay in the pipeline and have to be told. The reason shows on the requisition.",
        submitLabel: "Put on hold",
        initial: {},
        fields: [{ id: "note", kind: "area", label: "Why, and until when", placeholder: "Waiting on the establishment review; revisit in October." }],
        validate: (v) => (v.note && v.note.trim().length > 12 ? {} : { note: "Say why, and roughly when it comes back." }),
        run: (v) => {
          const before = snapshot();
          patch("requisitions", requisition.id, { status: "on_hold", holdNote: v.note.trim() });
          pushToast({ title: requisition.title + " on hold", description: v.note.trim(), onUndo: () => restore(before) });
        },
      });
      return;
    }
    if (transition.id === "resume_req") {
      const before = snapshot();
      patch("requisitions", requisition.id, { status: "open", holdNote: null });
      pushToast({
        variant: "success", title: requisition.title + " is open again",
        description: plural(health.live.length, "application", "applications") + " still live, " + health.awaitingUs + " waiting on the desk.",
        onUndo: () => restore(before),
      });
      return;
    }
    if (transition.id === "close_req") {
      setForm({
        title: "Close " + requisition.title + " without hiring",
        description: "The reason is the only record of why this seat went away.",
        submitLabel: "Continue",
        initial: {},
        fields: [{ id: "note", kind: "area", label: "Why is it closing?", placeholder: "Absorbed into another seat, funding pulled, the need went away." }],
        validate: (v) => (v.note && v.note.trim().length > 12 ? {} : { note: "Write it down — a closed requisition with no reason is a mystery next quarter." }),
        run: (v) => setConfirm({
          title: "Close " + requisition.title + "?",
          description: plural(health.live.length, "live application", "live applications") + " will be declined and the requisition cannot be reopened — a new one would have to be raised. This is not reversible.",
          confirmLabel: "Close the requisition",
          destructive: true,
          rows: health.live.map((a) => ({ key: a.id, label: a.candidate.name, value: statusValue(APPLICATION_STAGE, a.stage).label })),
          run: () => {
            const ids = health.live.map((a) => a.id);
            setData((d) => Object.assign({}, d, {
              requisitions: d.requisitions.map((r) => (r.id === requisition.id ? Object.assign({}, r, { status: "closed", closedAt: now, closedNote: v.note.trim() }) : r)),
              applications: d.applications.map((a) => (ids.indexOf(a.id) > -1 ? Object.assign({}, a, {
                stage: "declined", stageEnteredAt: now, lastLiveStage: a.stage, declineReason: "stronger_field",
                wouldReconsider: true, declineNote: "Requisition closed: " + v.note.trim(), decidedAt: now, decidedById: session.personId,
              }) : a)),
            }));
            pushToast({
              title: requisition.title + " closed",
              description: v.note.trim() + (ids.length ? " " + plural(ids.length, "application", "applications") + " declined, all kept in the talent pool." : ""),
            });
            setPath("/requisitions");
          },
        }),
      });
    }
  };

  /* ── Social: harmless and instant, so a bare control and no confirmation ── */
  const onReact = (note, value) => {
    setData((d) => Object.assign({}, d, {
      notes: d.notes.map((n) => {
        if (n.id !== note.id) return n;
        const who = (n.reactions || {})[value] || [];
        const next = who.indexOf(session.personId) > -1 ? who.filter((id) => id !== session.personId) : who.concat([session.personId]);
        return Object.assign({}, n, { reactions: Object.assign({}, n.reactions, { [value]: next }) });
      }),
    }));
  };

  const onToggleWatch = (application) => {
    const watching = (application.watcherIds || []).indexOf(session.personId) > -1;
    patch("applications", application.id, {
      watcherIds: watching ? application.watcherIds.filter((id) => id !== session.personId) : (application.watcherIds || []).concat([session.personId]),
    });
    pushToast({
      title: watching ? "No longer watching " + application.candidate.name : "Watching " + application.candidate.name,
      description: watching ? "You will not hear about this one unless you are mentioned." : "You will hear about scorecards, offers and anything that goes over its service level.",
    });
  };

  /* Consequential: a note is visible to the desk and the hiring manager the
     moment it is posted, so it applies immediately and offers an undo. */
  const onPostNote = (application, body, kind, parent) => {
    const before = snapshot();
    const note = Object.assign(addNote(application.id, body, kind), { parentId: parent ? parent.id : null });
    setData((d) => Object.assign({}, d, { notes: d.notes.concat([note]) }));
    const mentioned = mentionIds(body).map((id) => staffById(data.staff, id)).filter(Boolean);
    pushToast({
      title: parent ? "Reply posted" : "Note posted",
      description: (mentioned.length ? mentioned.map((p) => p.name).join(" and ") + " " + (mentioned.length === 1 ? "is" : "are") + " notified. " : "")
        + "Everybody following " + application.candidate.name + " sees it — " + plural(followersOf(application, data.notes, data.staff).length, "person", "people") + ".",
      onUndo: () => restore(before),
    });
  };

  const onChaseReference = (reference) => {
    const application = data.applications.find((a) => (a.references || []).some((r) => r.id === reference.id));
    const before = snapshot();
    patch("applications", application.id, {
      references: application.references.map((r) => (r.id === reference.id ? Object.assign({}, r, { status: "chased" }) : r)),
    });
    pushToast({
      title: "Chased " + reference.name,
      description: reference.relationship + " · asked " + formatDay(reference.requestedAt) + ". Still " + referenceState(application).received + " of " + RD_POLICY.referencesRequired + " back.",
      onUndo: () => restore(before),
    });
  };

  /* ── Offers: the desk's two levers on a record it does not control ────── */
  const onChaseOffer = (application) => {
    const before = snapshot();
    const left = daysBetween(now, application.offer.expiresAt);
    setData((d) => Object.assign({}, d, {
      notes: d.notes.concat([addNote(application.id, "Chased the offer — " + durationWords(left) + " on it. " + money(application.offer.salaryMinor) + " to start " + formatDay(application.offer.startAt) + ".", "note")]),
    }));
    pushToast({
      title: "Chased " + application.candidate.name.split(" ")[0],
      description: durationWords(left) + " on the offer. The chase is on the thread so the hiring manager can see it happened.",
      onUndo: () => restore(before),
    });
  };

  const onExtendOffer = (application) => {
    const requisition = requisitionById(data.requisitions, application.requisitionId);
    setForm({
      title: "Extend " + application.candidate.name + "'s offer",
      description: "It runs out " + formatDay(application.offer.expiresAt) + " — " + durationWords(daysBetween(now, application.offer.expiresAt))
        + ". Extending is the desk's decision; the money and the start date do not change here.",
      submitLabel: "Extend it",
      initial: { expires: dayValue(application.offer.expiresAt + 7 * DAY) },
      fields: [
        { id: "expires", kind: "day", label: "New date it runs out" },
        { id: "why", kind: "area", label: "Why", placeholder: "They asked for the weekend to talk it over with family.", hint: "Posted on the thread. The hiring manager sees the new date on " + requisition.ref + "." },
      ],
      validate: (v) => {
        const errors = {};
        const when = parseDay(v.expires);
        if (!when) errors.expires = "Pick the new date.";
        else if (when < now) errors.expires = "That date has passed.";
        else if (when <= application.offer.expiresAt) errors.expires = "That is not later than " + formatDay(application.offer.expiresAt) + ".";
        if (!v.why || v.why.trim().length < 10) errors.why = "Say why — an extension with no reason reads as a soft deadline.";
        return errors;
      },
      run: (v) => {
        const before = snapshot();
        const expiresAt = d1(v.expires);
        patch("applications", application.id, { offer: Object.assign({}, application.offer, { expiresAt }) });
        setData((d) => Object.assign({}, d, { notes: d.notes.concat([addNote(application.id, "Offer extended to " + formatDay(expiresAt) + ". " + v.why.trim(), "decision")]) }));
        pushToast({
          variant: "success", title: "Extended to " + formatDay(expiresAt),
          description: durationWords(daysBetween(now, expiresAt)) + " now. " + money(application.offer.salaryMinor) + " and the " + formatDay(application.offer.startAt) + " start are unchanged.",
          onUndo: () => restore(before),
        });
      },
    });
  };

  /* ── Compliance: three checks, one action each ────────────────────────── */
  const onVerifyRightToWork = (application) => {
    setForm({
      title: "Verify right to work for " + application.candidate.name,
      description: application.rightToWork === "needs_sponsorship"
        ? "This candidate needs sponsorship. Verifying here records that a certificate is in place — it does not create one, and Amara Boateng holds the establishment's certificates."
        : "Currently " + statusWords(RIGHT_TO_WORK, application.rightToWork) + ". Record what you saw, not what you were told.",
      submitLabel: "Record the check",
      initial: {},
      fields: [
        { id: "document", kind: "radio", label: "What did you see?", options: [
          { value: "passport", label: "British or Irish passport" },
          { value: "share_code", label: "Home Office share code" },
          { value: "brp", label: "Biometric residence permit" },
          { value: "sponsorship", label: "Certificate of sponsorship" },
        ] },
        { id: "note", kind: "area", label: "Anything to record", placeholder: "Seen in person on 28 August, copy on file, expiry noted." },
      ],
      validate: (v) => (v.document ? {} : { document: "Say which document you saw." }),
      run: (v) => {
        const before = snapshot();
        patch("applications", application.id, { rightToWork: "verified" });
        setData((d) => Object.assign({}, d, {
          notes: d.notes.concat([addNote(application.id, "Right to work verified — " + v.document.replace("_", " ") + "." + (v.note ? " " + v.note.trim() : ""), "decision")]),
        }));
        const missing = checksMissing(Object.assign({}, application, { rightToWork: "verified" }));
        pushToast({
          variant: "success", title: "Right to work verified · " + application.candidate.name.split(" ")[0],
          description: missing.length ? "Still outstanding: " + missing.join(" · ") + "." : "Nothing outstanding — clear to start.",
          onUndo: () => restore(before),
        });
      },
    });
  };

  const onSubmitDbs = (application) => {
    const before = snapshot();
    patch("applications", application.id, { dbs: "submitted", dbsAt: now });
    pushToast({
      title: "DBS sent for " + application.candidate.name.split(" ")[0],
      description: "With the service from today. They usually come back inside two weeks; the checks step allows " + RD_POLICY.stageDays.checks + " days.",
      onUndo: () => restore(before),
    });
  };

  const onChaseRegistration = (application) => {
    const before = snapshot();
    const days = daysBetween(now, application.registrationExpiresAt);
    setData((d) => Object.assign({}, d, {
      notes: d.notes.concat([addNote(application.id, "Asked for the renewed " + application.registration.split(" ")[0] + " registration — "
        + (days < 0 ? "lapsed " + formatDay(application.registrationExpiresAt) : "renews " + formatDay(application.registrationExpiresAt)) + ".", "note")]),
    }));
    pushToast({
      title: "Renewal asked for · " + application.candidate.name.split(" ")[0],
      description: application.registration + " " + (days < 0 ? "lapsed " + formatDay(application.registrationExpiresAt) + ". Nobody starts on a lapsed registration." : "renews " + formatDay(application.registrationExpiresAt) + " — " + durationWords(days) + "."),
      onUndo: () => restore(before),
    });
  };

  /* ── The advert ───────────────────────────────────────────────────────── */
  const patchAdvert = (requisitionId, fields) => setData((d) => Object.assign({}, d, {
    adverts: d.adverts.map((ad) => (ad.requisitionId === requisitionId ? Object.assign({}, ad, fields) : ad)),
  }));

  const onEditAdvert = (requisition, advert) => {
    setForm({
      title: "Edit the advert for " + requisition.title,
      description: "Staged: nothing changes on the live posting until you save. Band and target start are not editable here — they come from the requisition, so the advert cannot state a different figure.",
      submitLabel: "Save the wording",
      initial: {
        headline: advert.headline, body: advert.body,
        essentials: advert.essentials.join("\n"), desirables: advert.desirables.join("\n"),
      },
      fields: [
        { id: "headline", kind: "text", label: "Headline", hint: "What a candidate sees in a list of forty jobs." },
        { id: "body", kind: "area", label: "The advert", hint: "Blank line between paragraphs. Say the hard parts plainly — the drive, the on-call, the nights." },
        { id: "essentials", kind: "area", label: "Essential", hint: "One per line. These are what screening actually filters on." },
        { id: "desirables", kind: "area", label: "Desirable", hint: "One per line." },
      ],
      validate: (v) => {
        const errors = {};
        if (!v.headline || v.headline.trim().length < 10) errors.headline = "A headline that names the seat and the place.";
        if (!v.body || v.body.trim().length < 80) errors.body = "A paragraph at least — this is the whole advert.";
        if (!v.essentials || !v.essentials.trim()) errors.essentials = "At least one essential, or screening has nothing to filter on.";
        return errors;
      },
      run: (v) => {
        const before = snapshot();
        const lines = (text) => text.split("\n").map((line) => line.trim()).filter(Boolean);
        patchAdvert(requisition.id, {
          headline: v.headline.trim(), body: v.body.trim(),
          essentials: lines(v.essentials), desirables: lines(v.desirables || ""),
          updatedAt: now, updatedById: session.personId,
        });
        pushToast({
          variant: "success", title: "Advert saved",
          description: plural(lines(v.essentials).length, "essential", "essentials") + " and " + plural(lines(v.desirables || "").length, "desirable", "desirables")
            + ". " + (advert.postedTo.length ? "Live on " + plural(advert.postedTo.length, "channel", "channels") + " — the new wording is up now." : "Not posted anywhere yet."),
          onUndo: () => restore(before),
        });
      },
    });
  };

  const onPostAdvert = (requisition, advert) => {
    const unposted = SOURCES.filter((source) => source.value !== "pool" && !advert.postedTo.some((p) => p.channel === source.value));
    setForm({
      title: "Post the advert",
      description: "Applications from a channel are counted from the day it goes up, so the rate on this screen is the advert's own doing and not somebody else's referral.",
      submitLabel: "Post it",
      initial: { channel: unposted[0] ? unposted[0].value : "" },
      fields: [{ id: "channel", kind: "radio", label: "Where?", options: unposted.map((source) => ({ value: source.value, label: source.label })) }],
      validate: (v) => (v.channel ? {} : { channel: "Pick a channel." }),
      run: (v) => {
        const before = snapshot();
        patchAdvert(requisition.id, { postedTo: advert.postedTo.concat([{ channel: v.channel, postedAt: now, views: 0 }]) });
        pushToast({
          variant: "success", title: "Posted to " + labelOf(SOURCES, v.channel),
          description: "Live from today on " + plural(advert.postedTo.length + 1, "channel", "channels") + ". Views and applications from it are counted from now.",
          onUndo: () => restore(before),
        });
      },
    });
  };

  const onUnpostAdvert = (requisition, advert, row) => {
    setConfirm({
      title: "Unpublish from " + row.source.label + "?",
      description: "The advert comes down and nobody new can apply through it. What it already produced stays on the requisition.",
      confirmLabel: "Unpublish",
      destructive: true,
      rows: [
        { key: "up", label: "Up since", value: formatDay(row.posting.postedAt) + " · " + plural(row.days, "day", "days") },
        { key: "apps", label: "Applications it produced", value: plural(row.applications, "application", "applications") + (row.live ? ", " + row.live + " still live" : "") },
        { key: "other", label: "Still posted", value: advert.postedTo.length - 1 ? plural(advert.postedTo.length - 1, "channel", "channels") : "Nowhere — the seat goes dark" },
      ],
      run: () => {
        patchAdvert(requisition.id, { postedTo: advert.postedTo.filter((p) => p.channel !== row.posting.channel) });
        pushToast({
          title: "Down from " + row.source.label,
          description: advert.postedTo.length - 1
            ? "Still live on " + plural(advert.postedTo.length - 1, "channel", "channels") + "."
            : requisition.ref + " is not advertised anywhere now, and it has " + plural(requisitionHealth(data.applications, requisition, now).live.length, "live application", "live applications") + ".",
        });
      },
    });
  };

  /* ── Notifications ────────────────────────────────────────────────────── */
  const onReadNotification = (notification) => {
    if (notification.readAt) return;
    patch("notifications", notification.id, { readAt: now });
  };
  const onUnreadNotification = (notification) => patch("notifications", notification.id, { readAt: null });
  const onReadAll = () => {
    const before = snapshot();
    const unread = unreadNotifications(data.notifications);
    patchMany("notifications", unread.map((n) => n.id), { readAt: now });
    pushToast({
      title: plural(unread.length, "notification", "notifications") + " marked read",
      description: "The bell is clear. They are all still on the notifications screen, newest first.",
      onUndo: () => restore(before),
    });
  };

  /* ── Settings. Staged screens, so each one commits once, here. ────────── */
  const onSaveDesk = (draft) => {
    const before = desk;
    setDesk(draft);
    pushToast({
      variant: "success", title: "The desk is saved",
      description: draft.name + " · replies go to " + draft.replyTo + ". Every advert and message the desk sends uses these from now on.",
      onUndo: () => setDesk(before),
    });
  };

  /* One copy of the policy, mutated in place: the queue, the stepper, the
     badges and Compliance all read RD_POLICY, so they move together. */
  const onSavePolicy = (draft) => {
    const before = JSON.parse(JSON.stringify(RD_POLICY));
    const apply = (values) => { Object.assign(RD_POLICY, values); setPolicyVersion((n) => n + 1); };
    apply(draft);
    const over = stalledApplications(data.applications, now).length;
    pushToast({
      variant: "success", title: "Process saved",
      description: "Screening " + draft.stageDays.screening + " days, checks " + draft.stageDays.checks + ", offers valid " + draft.offerValidDays
        + ". " + plural(over, "application", "applications") + " now reads as over its service level.",
      onUndo: () => apply(before),
    });
  };

  const onSaveChannels = (draft) => {
    const before = channels;
    setChannels(draft);
    pushToast({
      variant: "success", title: "Channels saved",
      description: plural(draft.enabled.length, "channel", "channels") + " available, " + draft.defaults.map((v) => labelOf(SOURCES, v)).join(" and ")
        + " on by default. Postings are queried after " + draft.reviewDays + " days.",
      onUndo: () => setChannels(before),
    });
  };

  const onInviteMember = () => {
    setForm({
      title: "Invite somebody to the desk",
      description: "They get an email with a link. The role decides what they can do — see the table below the invite for exactly what.",
      submitLabel: "Send the invitation",
      initial: { role: "panel" },
      fields: [
        { id: "name", kind: "text", label: "Name", placeholder: "Ruth Calloway" },
        { id: "email", kind: "text", label: "Larkspur email", placeholder: "name@" + RD_ORG.domain },
        { id: "role", kind: "radio", label: "Role", options: DESK_ROLES.map((r) => ({ value: r.value, label: r.label })) },
      ],
      validate: (v) => {
        const errors = {};
        if (!v.name || v.name.trim().indexOf(" ") === -1) errors.name = "Full name.";
        if (!v.email || v.email.indexOf("@" + RD_ORG.domain) === -1) errors.email = "A Larkspur address — the desk holds candidate data.";
        if (!v.role) errors.role = "Pick the role.";
        return errors;
      },
      run: (v) => {
        pushToast({
          variant: "success", title: "Invitation sent to " + v.name.trim(),
          description: v.email.trim() + " · " + labelOf(DESK_ROLES, v.role) + ". They appear on this screen when they accept, not before.",
        });
      },
    });
  };

  const onChangeRole = (person) => {
    const owned = liveApplications(ownedBy(data.applications, person.id)).length;
    setForm({
      title: "Change " + person.name + "'s role",
      description: labelOf(DESK_ROLES, person.role) + " today" + (owned ? ", owning " + plural(owned, "live application", "live applications") : "") + ". A role change takes effect the next time they open the desk.",
      submitLabel: "Change the role",
      initial: { role: person.role },
      fields: [{ id: "role", kind: "radio", label: "Role", options: DESK_ROLES.map((r) => ({ value: r.value, label: r.label })) }],
      validate: (v) => (v.role && v.role !== person.role ? {} : { role: "Pick a different role." }),
      run: (v) => {
        const before = snapshot();
        patch("staff", person.id, { role: v.role });
        pushToast({
          variant: "success", title: person.name + " is now " + labelOf(DESK_ROLES, v.role).toLowerCase(),
          description: owned ? plural(owned, "application", "applications") + " stay with them — a role change does not move work." : "They own nothing at the moment, so nothing moves.",
          onUndo: () => restore(before),
        });
      },
    });
  };

  /* ── The other legitimate way in: we ask, they fill it, and an application
     appears when they submit — not when we send the link. So this writes
     nothing to the desk, and the toast says so plainly rather than leaving a
     phantom record in a status nobody could act on. */
  const onInviteCandidate = () => {
    const open = data.requisitions.filter((r) => r.status === "open");
    setForm({
      title: "Send the application form",
      description: "They get a link to the form for one requisition, valid for 14 days. Nothing appears on the desk until they submit it — there is no half-application sitting in the queue.",
      submitLabel: "Send the link",
      initial: { requisitionId: open[0] ? open[0].id : "", source: "referral" },
      fields: [
        { id: "email", kind: "text", label: "Their email", placeholder: "name@example.com", hint: "The only thing we keep until they submit." },
        { id: "name", kind: "text", label: "Their name", placeholder: "Rowan Ellis-Baptiste", hint: "So the email is not addressed to nobody." },
        { id: "requisitionId", kind: "select", label: "Which seat?", options: open.map((r) => ({ value: r.id, label: r.ref + " · " + r.title + " · " + labelOf(SITES, r.site) })) },
        { id: "source", kind: "select", label: "Where they came from", options: SOURCES.map((s) => ({ value: s.value, label: s.label })) },
        { id: "message", kind: "area", label: "What to say", placeholder: "We met at the Riverside open evening — here is the form for the community staff nurse post.", hint: "Goes into the email above the link. The form itself asks for the CV, the photograph and the right-to-work documents." },
      ],
      validate: (v) => {
        const errors = {};
        if (!v.email || v.email.indexOf("@") === -1 || v.email.indexOf(".") === -1) errors.email = "A real email address — the link is the whole point.";
        if (!v.name || v.name.trim().length < 2) errors.name = "Their name, as they gave it to you.";
        if (!v.requisitionId) errors.requisitionId = "Pick the seat the form is for.";
        if (!v.message || v.message.trim().length < 15) errors.message = "A sentence at least — a bare link from a hospital reads as a scam.";
        return errors;
      },
      run: (v) => {
        const requisition = requisitionById(data.requisitions, v.requisitionId);
        pushToast({
          variant: "success",
          title: "Form sent to " + v.name.trim(),
          description: v.email.trim() + " · " + requisition.ref + " " + requisition.title + ". The link runs out " + formatDay(now + 14 * DAY)
            + ". Nothing is on the desk yet: their application arrives at applied, owned by you, when they submit it.",
        });
      },
    });
  };

  /* ── Creating records ─────────────────────────────────────────────────── */
  const onCreateApplication = (values) => {
    const before = snapshot();
    const id = "ap-" + Math.random().toString(36).slice(2, 7);
    const application = {
      id, ref: "APP-" + id.slice(3).toUpperCase(),
      candidate: { name: values.name.trim(), initials: initialsOf(values.name.trim()), photo: values.photo || portraitFor(values.name.trim()), headline: values.headline.trim(), location: values.location.trim() || "Not stated" },
      requisitionId: values.requisitionId, stage: "applied", source: values.source,
      appliedAt: now, stageEnteredAt: now, firstReadAt: null, ownerId: session.personId,
      rightToWork: values.rightToWork, salaryExpectationMinor: values.expectation ? parseMoney(values.expectation) : null,
      registration: values.registration.trim() || null,
      documents: (values.documents || []).map((document) => ({ kind: document.kind, name: document.name, at: now })), scorecards: [], references: [], offer: null, lastLiveStage: null,
      watcherIds: [], declineReason: null, declineNote: null, wouldReconsider: false, decidedAt: null, decidedById: null,
      withdrawReason: null, withdrawNote: null,
    };
    const notes = values.note.trim() ? [addNote(id, values.note.trim(), "note")] : [];
    setData((d) => Object.assign({}, d, { applications: d.applications.concat([application]), notes: d.notes.concat(notes) }));
    pushToast({
      variant: "success", title: values.name.trim() + " is on the desk",
      description: "Applied today, unread, owned by you"
        + ((values.documents || []).length ? ", with " + plural(values.documents.length, "document", "documents") : "")
        + ". " + requisitionById(data.requisitions, values.requisitionId).title + " now has "
        + plural(requisitionHealth(data.applications, requisitionById(data.requisitions, values.requisitionId), now).live.length + 1, "live application", "live applications") + ".",
      onUndo: () => restore(before),
    });
    setPath("/applications/" + id);
  };

  const onCreateRequisition = (values) => {
    const before = snapshot();
    const ref = String(1070 + data.requisitions.length);
    const requisition = {
      id: "rq-" + ref, ref: "LH-" + ref, title: values.title.trim(), department: values.department, site: values.site,
      employmentType: values.employmentType, bandMinor: values.bandMinor,
      hiringManagerId: values.hiringManagerId, recruiterId: session.personId, headcount: Number(values.headcount),
      openedAt: now, targetStartAt: d0(values.targetStart), status: "draft",
      registration: values.registration === "none" ? null : values.registration,
      brief: values.brief.trim(), holdNote: null, closedNote: null, closedAt: null,
    };
    setData((d) => Object.assign({}, d, { requisitions: d.requisitions.concat([requisition]) }));
    pushToast({
      variant: "success", title: requisition.ref + " saved as a draft",
      description: values.title.trim() + " · " + plural(requisition.headcount, "seat", "seats") + " · " + bandLabel(requisition) + ". Open it to start accepting applications.",
      onUndo: () => restore(before),
    });
    setPath("/requisitions/" + requisition.id);
  };

  /* Your own photograph, saved onto the staff record — so it changes on every
     note, panel and account menu at once, because there is one copy of it. */
  const onSavePhoto = (photo) => {
    const before = snapshot();
    patch("staff", session.personId, { photo });
    pushToast({
      variant: "success",
      title: photo ? "Photograph saved" : "Photograph removed",
      description: photo
        ? "It is on every note you have written, every panel you sit on and the account menu."
        : "Your initials stand in wherever a face appears.",
      onUndo: () => restore(before),
    });
  };

  const onSavePreferences = (draft) => {
    const before = preferences;
    setPreferences(draft);
    pushToast({
      variant: "success", title: "Preferences saved",
      description: [draft.notifyMentions && "mentions", draft.notifyScorecards && "scorecards", draft.notifyOffers && "offers", draft.notifyStalled && "service levels"]
        .filter(Boolean).join(", ") + " · the queue opens on " + (draft.defaultView === "ours" ? "waiting on us" : draft.defaultView === "theirs" ? "waiting on others" : "everything")
        + (draft.onlyMine ? ", filtered to you." : "."),
      onUndo: () => setPreferences(before),
    });
  };

  /* ── Routing ───────────────────────────────────────────────────────────── */
  const preview = (
    <TemplateMenu product="Recruitment Desk" path={path} onNavigate={setPath} groups={RD_PAGES(data)}
      dark={dark} onToggleTheme={() => setDark(!dark)} />
  );

  if (RD_AUTH_PATHS.indexOf(path) > -1) {
    const authScreen = () => {
      if (path === "/signout") return <SignedOutScreen person={me} onNavigate={setPath} />;
      return <SignInScreen attempts={attempts} onFailedAttempt={() => setAttempts((n) => n + 1)} onAuthenticated={() => { setAttempts(0); setPath("/"); }} />;
    };
    return <>{authScreen()}{preview}</>;
  }

  const applicationMatch = path.match(/^\/applications\/(ap-[^/]+)$/);
  const requisitionMatch = path.match(/^\/requisitions\/(rq-[^/]+)$/);
  const advertMatch = path.match(/^\/requisitions\/(rq-[^/]+)\/advert$/);

  const screen = () => {
    if (path === "/") {
      return <DeskScreen data={data} now={now} session={session} onNavigate={setPath}
        onApplicationTransition={onApplicationTransition} onInterviewTransition={onInterviewTransition} onReact={onReact} />;
    }
    if (path === "/applications") {
      return <ApplicationsScreen data={data} now={now} session={session} onNavigate={setPath}
        onApplicationTransition={onApplicationTransition} onBulkAdvance={onBulkAdvance} onBulkDecline={onBulkDecline} pushToast={pushToast} />;
    }
    if (path === "/applications/new") {
      return <NewApplicationScreen data={data} now={now} session={session} onNavigate={setPath} onCreateApplication={onCreateApplication} onInviteCandidate={onInviteCandidate} />;
    }
    if (path === "/interviews") {
      return <InterviewsScreen data={data} now={now} session={session} onNavigate={setPath}
        onInterviewTransition={onInterviewTransition} onBookInterview={onBookInterview} />;
    }
    if (path === "/requisitions") {
      return <RequisitionsScreen data={data} now={now} session={session} onNavigate={setPath}
        onRequisitionTransition={onRequisitionTransition} pushToast={pushToast} />;
    }
    if (path === "/requisitions/new") {
      return <NewRequisitionScreen data={data} now={now} session={session} onNavigate={setPath} onCreateRequisition={onCreateRequisition} />;
    }
    if (path === "/pool") {
      return <PoolScreen data={data} now={now} session={session} onNavigate={setPath} onApplicationTransition={onApplicationTransition} />;
    }
    if (path === "/offers") {
      return <OffersScreen data={data} now={now} session={session} onNavigate={setPath}
        onApplicationTransition={onApplicationTransition} onChaseOffer={onChaseOffer} onExtendOffer={onExtendOffer} />;
    }
    if (path === "/compliance") {
      return <ComplianceScreen data={data} now={now} session={session} onNavigate={setPath}
        onVerifyRightToWork={onVerifyRightToWork} onSubmitDbs={onSubmitDbs} onChaseRegistration={onChaseRegistration} />;
    }
    if (path === "/insights") return <InsightsScreen data={data} now={now} onNavigate={setPath} />;
    if (path === "/team") return <TeamScreen data={data} now={now} session={session} onNavigate={setPath} />;
    if (path === "/notifications") {
      return <NotificationsScreen data={data} now={now} session={session} onNavigate={setPath}
        onReadNotification={onReadNotification} onUnreadNotification={onUnreadNotification} onReadAll={onReadAll} />;
    }
    if (path === "/settings/desk") return <DeskSettingsScreen data={data} now={now} session={session} onNavigate={setPath} desk={desk} onSaveDesk={onSaveDesk} />;
    if (path === "/settings/process") return <ProcessSettingsScreen data={data} now={now} session={session} onNavigate={setPath} policy={RD_POLICY} onSavePolicy={onSavePolicy} />;
    if (path === "/settings/channels") return <ChannelSettingsScreen data={data} now={now} session={session} onNavigate={setPath} channels={channels} onSaveChannels={onSaveChannels} />;
    if (path === "/settings/people") return <PeopleSettingsScreen data={data} now={now} session={session} onNavigate={setPath} onInviteMember={onInviteMember} onChangeRole={onChangeRole} />;
    if (path === "/me/profile") {
      return <ProfileScreen data={data} now={now} session={session} onNavigate={setPath} preferences={preferences} onSavePreferences={onSavePreferences} onSavePhoto={onSavePhoto} />;
    }
    if (applicationMatch) {
      const application = applicationById(data.applications, applicationMatch[1]);
      if (!application) return <RouteNotBuilt path={path} onNavigate={setPath} />;
      return <ApplicationScreen application={application} data={data} now={now} session={session} onNavigate={setPath}
        onApplicationTransition={onApplicationTransition} onInterviewTransition={onInterviewTransition}
        onPostNote={onPostNote} onReact={onReact} onToggleWatch={onToggleWatch} onChaseReference={onChaseReference} />;
    }
    if (advertMatch) {
      const requisition = requisitionById(data.requisitions, advertMatch[1]);
      const advert = requisition ? advertFor(data.adverts, requisition.id) : null;
      if (!requisition || !advert) return <RouteNotBuilt path={path} onNavigate={setPath} />;
      return <AdvertScreen requisition={requisition} advert={advert} data={data} now={now} session={session} onNavigate={setPath}
        onEditAdvert={onEditAdvert} onPostAdvert={onPostAdvert} onUnpostAdvert={onUnpostAdvert} />;
    }
    if (requisitionMatch) {
      const requisition = requisitionById(data.requisitions, requisitionMatch[1]);
      if (!requisition) return <RouteNotBuilt path={path} onNavigate={setPath} />;
      return <RequisitionScreen requisition={requisition} data={data} now={now} session={session} onNavigate={setPath}
        onRequisitionTransition={onRequisitionTransition} onApplicationTransition={onApplicationTransition} />;
    }
    return <RouteNotBuilt path={path} onNavigate={setPath} />;
  };

  const recordGroups = [
    {
      label: "Candidates",
      items: data.applications.map((a) => ({
        value: a.id,
        label: a.candidate.name + " · " + statusValue(APPLICATION_STAGE, a.stage).label + " · " + requisitionById(data.requisitions, a.requisitionId).title,
        route: "/applications/" + a.id,
      })),
    },
    {
      label: "Requisitions",
      items: data.requisitions.map((r) => ({
        value: r.id,
        label: r.ref + " · " + r.title + " · " + labelOf(SITES, r.site),
        route: "/requisitions/" + r.id,
      })),
    },
    {
      label: "Interviews booked",
      items: interviewsAhead(data.interviews, now).map((iv) => ({
        value: iv.id,
        label: applicationById(data.applications, iv.applicationId).candidate.name + " · " + formatWeekday(iv.at) + " " + formatTime(iv.at),
        route: "/interviews",
      })),
    },
  ];

  return (
    <RdShell
      path={path} onNavigate={setPath} dark={dark} onToggleTheme={() => setDark(!dark)}
      fold={fold} onSetFold={setFold}
      data={data} person={me} notifications={data.notifications} unread={unreadNotifications(data.notifications).length}
      onRead={onReadAll} onOpenNotifications={() => setPath("/notifications")}
      palette={palette} toasts={toasts} onDismissToast={dismiss} now={now}
      policyVersion={policyVersion}
    >
      {screen()}
      <CommandPalette
        open={palette.open} onClose={() => palette.setOpen(false)} routes={RD_ROUTES}
        recordGroups={recordGroups} onNavigate={setPath} label="Search the Recruitment Desk"
        placeholder="Search candidates, requisitions and interviews…"
        emptyMessage="Nothing matches that. Try a candidate's name, a requisition reference or a destination." />
      <ConfirmDialog request={confirm} onCancel={() => setConfirm(null)} onConfirm={() => { confirm.run(); setConfirm(null); }} />
      <FormDialog request={form} onCancel={() => setForm(null)} onSubmit={(values) => { form.run(values); setForm(null); }} />
      {preview}
    </RdShell>
  );
}
/* boot.js publishes a stub on window.RecruitmentDeskApp before anything is
   fetched, so <x-import> always resolves; the real component lands here and the
   stub swaps it in. */
window.__recruitmentDeskReal = App;
if (!window.RecruitmentDeskApp) window.RecruitmentDeskApp = App;
