/* Claims 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 live on their screens,
   consequential ones apply here and raise an undo toast, irreversible ones open
   a confirm dialog, and anything needing two or three fields opens a staged
   dialog. */
function App(props) {
  const options = props || {};
  const [path, setPath] = React.useState(options.startPath || "/");
  const [dark, setDark] = React.useState(options.theme === "dark");
  const [read, setRead] = React.useState(NOTIFICATIONS_READ);
  const [toasts, setToasts] = React.useState([]);
  const [confirm, setConfirm] = React.useState(null);
  const [form, setForm] = React.useState(null);
  const [data, setData] = React.useState({ claims: CLAIMS, claimants: CLAIMANTS, recoveries: RECOVERIES, notifications: NOTIFICATIONS });
  const [user, setUser] = React.useState(CLAIMS_USER);
  const [desk, setDesk] = React.useState(DESK_DEFAULTS);
  /* Which claimant a new claim is being opened for, set by the claimant
     record's own button. The create screen reads it once and clears it on
     submit — it is a prefill, not a route parameter. */
  const [newClaimFor, setNewClaimFor] = React.useState(null);
  /* One way in, so a prefill can never be inherited: every entry point states
     the claimant it means, and leaving the screen unsaved drops it. */
  const openNewClaim = (claimantId) => { setNewClaimFor(claimantId || null); setPath("/claims/new"); };
  React.useEffect(() => { if (path !== "/claims/new" && newClaimFor) setNewClaimFor(null); }, [path]);
  const palette = useCommandPalette();
  const now = CLAIMS_NOW;

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

  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: id }, toast)]));
    window.setTimeout(() => dismiss(id), 8000);
  };

  /* A committing action writes into the fixture and re-renders, so every figure
     that claims to derive from it actually moves. */
  const patchClaim = (id, fields) => setData((d) => Object.assign({}, d, { claims: d.claims.map((c) => (c.id === id ? Object.assign({}, c, fields) : c)) }));
  const restoreClaim = (snapshot) => setData((d) => Object.assign({}, d, { claims: d.claims.map((c) => (c.id === snapshot.id ? snapshot : c)) }));
  const patchRecovery = (id, fields) => setData((d) => Object.assign({}, d, { recoveries: d.recoveries.map((r) => (r.id === id ? Object.assign({}, r, fields) : r)) }));
  const restoreRecovery = (snapshot) => setData((d) => Object.assign({}, d, { recoveries: d.recoveries.map((r) => (r.id === snapshot.id ? snapshot : r)) }));
  const withNote = (claim, text) => (claim.notes || []).concat([{ at: now, who: CLAIMS_USER.name, text: text }]);
  const nextReference = () => {
    const numbers = settledClaims(data.claims)
      .map((c) => Number(String(c.settlement.reference || "").split("-")[2]))
      .filter((n) => !isNaN(n));
    return "PAY-26-" + (Math.max.apply(null, numbers.concat([3400])) + 1);
  };

  /* ── Claim transitions ─────────────────────────────────────────────── */
  const onClaimTransition = (claim, transition) => {
    if (transition.id === "chase_documents" || transition.id === "chase_survey") {
      const missing = CLAIM_FLAGS.missingMandatory(claim).map((m) => labelOf(DOCUMENT_TYPES, m).toLowerCase());
      const who = transition.id === "chase_survey" ? "the surveyor" : "the customer";
      patchClaim(claim.id, { chasedAt: now, notes: withNote(claim, "Chased " + who + (missing.length ? " for " + missing.join(", ") + "." : ".")) });
      pushToast({ title: "Chased " + who, description: claim.id + " · " + claim.customer, onUndo: () => restoreClaim(claim) });
      return;
    }
    if (transition.id === "chase_signoff") {
      const payable = payableOf(claim);
      patchClaim(claim.id, { chasedAt: now, notes: withNote(claim, "Chased " + CLAIMS_MANAGER.name + " for sign-off on " + formatMoney(payable) + ".") });
      pushToast({ title: "Chased " + CLAIMS_MANAGER.name, description: claim.id + " · " + formatMoney(payable) + " waiting on a signature", onUndo: () => restoreClaim(claim) });
      return;
    }
    /* Taking a decision back is reversible, so it raises an undo toast rather
       than a confirm dialog. The decision is cleared, not kept: a withdrawn
       decision that stayed on the record would show a payable figure nobody
       has approved. */
    if (transition.id === "withdraw_decision") {
      patchClaim(claim.id, {
        status: "ready_for_decision",
        decision: null,
        notes: withNote(claim, "Decision withdrawn from sign-off before " + CLAIMS_MANAGER.name + " signed it. Back on this desk as ready for decision."),
      });
      pushToast({ title: "Decision withdrawn", description: claim.id + " is back on your desk", onUndo: () => restoreClaim(claim) });
      return;
    }
    if (transition.id === "escalate") {
      setConfirm({
        title: "Escalate " + claim.id + " to legal?",
        description: "Legal takes the file and this desk stops work on it. The time bar keeps running — " + formatCountdown(CLAIM_FLAGS.timeBarAt(claim), now) + " — and only legal can hand it back.",
        confirmLabel: "Escalate to legal",
        run: () => {
          patchClaim(claim.id, { status: "escalated_legal", notes: withNote(claim, "Escalated to legal.") });
          pushToast({ title: "Escalated to legal", description: claim.id + " · " + claim.customer });
        },
      });
      return;
    }
    if (transition.id === "recall") {
      setConfirm({
        title: "Recall " + claim.id + " from legal?",
        description: "The file comes back to your desk as ready for decision, and legal closes its own matter. " + formatCountdown(CLAIM_FLAGS.timeBarAt(claim), now) + " on the time bar.",
        confirmLabel: "Recall from legal",
        run: () => {
          patchClaim(claim.id, { status: "ready_for_decision", notes: withNote(claim, "Recalled from legal.") });
          pushToast({ title: "Recalled from legal", description: claim.id + " is back on your desk" });
        },
      });
    }
  };

  const onDocumentAction = (claim, document, transition) => {
    const label = labelOf(DOCUMENT_TYPES, document.type);
    const next = transition.id === "mark_illegible"
      ? { status: "illegible", note: "Rejected as illegible by " + CLAIMS_USER.name + "." }
      : { status: "requested", receivedAt: null, note: "Requested again on " + formatAbsolute(now) + "." };
    patchClaim(claim.id, {
      documents: claim.documents.map((d) => (d.type === document.type ? Object.assign({}, d, next) : d)),
      notes: withNote(claim, transition.id === "mark_illegible" ? label + " rejected as illegible." : label + " requested again."),
    });
    pushToast({
      title: transition.id === "mark_illegible" ? label + " marked illegible" : label + " requested again",
      description: claim.id + " · " + claim.customer,
      onUndo: () => restoreClaim(claim),
    });
  };

  const onSaveAssessment = (claim, adjustments) => {
    patchClaim(claim.id, {
      assessment: Object.assign({}, adjustments, { assessedBy: CLAIMS_USER.name, assessedAt: now }),
      status: claim.status === "ready_for_decision" ? claim.status : claim.status,
    });
  };

  const onRecordDecision = (claim, decision) => {
    const recorded = {
      outcome: decision.outcome,
      decidedAt: now,
      decidedBy: CLAIMS_USER.name,
      signedOffBy: decision.needsSignoff ? null : null,
      rejectReason: decision.rejectReason,
      justification: decision.justification,
    };
    if (decision.outcome === "reject") {
      patchClaim(claim.id, { status: "rejected", decision: recorded, notes: withNote(claim, "Claim rejected — " + labelOf(REJECT_REASONS, decision.rejectReason) + ".") });
      pushToast({ title: "Claim rejected", description: claim.id + " · " + labelOf(REJECT_REASONS, decision.rejectReason) });
      return;
    }
    if (decision.needsSignoff) {
      patchClaim(claim.id, {
        status: "awaiting_signoff",
        decision: recorded,
        notes: withNote(claim, "Decision of " + formatMoney(decision.amountCents) + " sent to " + CLAIMS_MANAGER.name + " for sign-off."),
      });
      pushToast({ variant: "success", title: "Sent for sign-off", description: formatMoney(decision.amountCents) + " · with " + CLAIMS_MANAGER.name });
      return;
    }
    patchClaim(claim.id, {
      status: "approved_awaiting_payment",
      decision: recorded,
      settlement: { status: "unscheduled", amountCents: decision.amountCents },
      notes: withNote(claim, labelOf(DECISION_OUTCOMES, decision.outcome) + " at " + formatMoney(decision.amountCents) + "."),
    });
    pushToast({ variant: "success", title: "Claim approved", description: formatMoney(decision.amountCents) + " to " + claim.customer + " · now on the payments desk" });
  };

  /* ── Settlements ───────────────────────────────────────────────────── */
  const onSettlementTransition = (claim, transition) => {
    if (transition.id === "schedule_payment") {
      const runAt = nextRunDate(now);
      const reference = nextReference();
      patchClaim(claim.id, { settlement: Object.assign({}, claim.settlement, { status: "scheduled", scheduledFor: runAt, reference: reference, returnedAt: undefined, returnReason: undefined }) });
      pushToast({ variant: "success", title: "Payment scheduled", description: formatMoney(claim.settlement.amountCents) + " to " + claim.customer + " on " + formatAbsolute(runAt) + " · " + reference, onUndo: () => restoreClaim(claim) });
      return;
    }
    if (transition.id === "send_payment") {
      setConfirm({
        title: "Send " + formatMoney(claim.settlement.amountCents) + " to " + claim.customer + "?",
        description: "The money leaves the account today and cannot be recalled. " + claim.customer + " is told the claim is settled, and the offer goes to them for acceptance.",
        confirmLabel: "Send payment",
        run: () => {
          patchClaim(claim.id, {
            status: "paid",
            settlement: Object.assign({}, claim.settlement, { status: "sent", sentAt: now }),
            response: { status: "awaiting_response", offerSentAt: now },
            notes: withNote(claim, "Payment of " + formatMoney(claim.settlement.amountCents) + " sent, " + claim.settlement.reference + "."),
          });
          pushToast({ title: "Payment sent", description: formatMoney(claim.settlement.amountCents) + " to " + claim.customer + " · " + claim.settlement.reference });
        },
      });
    }
  };

  /* ── Customer responses ────────────────────────────────────────────── */
  const onResponseTransition = (claim, transition) => {
    if (transition.id === "record_response") {
      setForm({
        title: "Record the customer's response",
        description: claim.customer + " · " + claim.id + " · we offered " + formatMoney(claim.settlement.amountCents) + ".",
        submitLabel: "Record response",
        initial: {},
        fields: [
          { id: "answer", kind: "radio", label: "What did they say?", options: [{ value: "accepted", label: "Accepted the offer" }, { value: "disputed", label: "Disputed the offer" }] },
          { id: "demand", kind: "money", label: "What they hold us to", hint: "Their figure, above our " + formatMoney(claim.settlement.amountCents) + " offer.", when: (v) => v.answer === "disputed" },
          { id: "note", kind: "text", label: "In their words", placeholder: "Quote the letter or the call, so the file carries their reasoning.", when: (v) => v.answer === "disputed" },
        ],
        validate: (v) => {
          const errors = {};
          if (!v.answer) errors.answer = "Pick what the customer said.";
          if (v.answer === "disputed") {
            const demand = parseMoney(v.demand || "");
            if (isNaN(demand) || demand <= claim.settlement.amountCents) errors.demand = "A dispute means they want more than the " + formatMoney(claim.settlement.amountCents) + " offered.";
            if (!v.note || v.note.trim().length < 20) errors.note = "Record what they actually said — at least a sentence.";
          }
          return errors;
        },
        run: (v) => {
          if (v.answer === "accepted") {
            patchClaim(claim.id, { response: Object.assign({}, claim.response, { status: "accepted", respondedAt: now }), notes: withNote(claim, "Customer accepted the offer of " + formatMoney(claim.settlement.amountCents) + ".") });
            pushToast({ variant: "success", title: "Offer accepted", description: claim.id + " · " + claim.customer });
            return;
          }
          const demand = parseMoney(v.demand);
          patchClaim(claim.id, {
            response: Object.assign({}, claim.response, { status: "disputed", respondedAt: now, demandCents: demand, note: v.note.trim() }),
            notes: withNote(claim, "Customer disputed the offer and holds us to " + formatMoney(demand) + "."),
          });
          pushToast({ title: "Dispute recorded", description: formatMoney(demand - claim.settlement.amountCents) + " now in dispute · " + claim.id });
        },
      });
      return;
    }
    if (transition.id === "reopen_assessment") {
      patchClaim(claim.id, { status: "ready_for_decision", notes: withNote(claim, "Assessment reopened after the customer disputed the offer.") });
      pushToast({ title: "Assessment reopened", description: claim.id + " is back on your desk — the payment already made stands", onUndo: () => restoreClaim(claim) });
      return;
    }
    if (transition.id === "escalate") onClaimTransition(claim, transition);
  };

  /* ── Recoveries ────────────────────────────────────────────────────── */
  const onRecoveryTransition = (recovery, transition) => {
    if (transition.id === "file_notice") {
      patchRecovery(recovery.id, {
        status: "filed",
        noticeServedAt: now,
        log: recovery.log.concat([{ at: now, who: CLAIMS_USER.name, text: "Notice of claim served on " + recovery.counterparty.name + " for " + formatMoney(recovery.soughtCents) + ", with the survey report and proof of payment." }]),
      });
      pushToast({ variant: "success", title: "Notice filed", description: recovery.id + " · " + recovery.counterparty.name, onUndo: () => restoreRecovery(recovery) });
      return;
    }
    if (transition.id === "record_recovery") {
      setForm({
        title: "Record money recovered",
        description: recovery.counterparty.name + " · " + recovery.id + " · " + formatMoney(recovery.soughtCents) + " sought.",
        submitLabel: "Record recovery",
        initial: {},
        fields: [
          { id: "amount", kind: "money", label: "Amount recovered", hint: "Anything up to the " + formatMoney(recovery.soughtCents) + " sought." },
          { id: "note", kind: "text", label: "On what basis", placeholder: "How they settled, and on what limitation." },
        ],
        validate: (v) => {
          const errors = {};
          const amount = parseMoney(v.amount || "");
          if (isNaN(amount) || amount <= 0) errors.amount = "Enter the amount they have paid.";
          else if (amount > recovery.soughtCents) errors.amount = "You cannot recover more than the " + formatMoney(recovery.soughtCents) + " sought.";
          if (!v.note || v.note.trim().length < 20) errors.note = "Record the basis — at least a sentence.";
          return errors;
        },
        run: (v) => {
          const amount = parseMoney(v.amount);
          patchRecovery(recovery.id, {
            status: "recovered", recoveredCents: amount, offerCents: amount, closedAt: now,
            log: recovery.log.concat([{ at: now, who: recovery.counterparty.name, text: v.note.trim() }]),
          });
          pushToast({ variant: "success", title: "Recovery recorded", description: formatMoney(amount) + " back from " + recovery.counterparty.name });
        },
      });
      return;
    }
    if (transition.id === "write_off") {
      setConfirm({
        title: "Write off " + formatMoney(RECOVERY_FLAGS.shortfall(recovery)) + " against " + recovery.counterparty.name + "?",
        description: "The file closes and the shortfall stays with us. It counts against the recovery rate, and the contract deadline of " + formatAbsolute(recovery.fileByAt) + " cannot be reopened once it passes.",
        confirmLabel: "Write off recovery",
        destructive: true,
        run: () => {
          patchRecovery(recovery.id, {
            status: "written_off", closedAt: now,
            log: recovery.log.concat([{ at: now, who: CLAIMS_USER.name, text: "Written off at " + formatMoney(RECOVERY_FLAGS.shortfall(recovery)) + " — not worth pursuing further." }]),
          });
          pushToast({ title: "Recovery written off", description: recovery.id + " · " + recovery.counterparty.name });
        },
      });
    }
  };

  /* ── Creating records ─────────────────────────────────────────────── */
  /* Ids continue the series already in the fixture rather than restarting: the
     next claim after CLM-26-0212 is CLM-26-0213, on the desk and in the
     breadcrumb alike. */
  const nextId = (rows, prefix) => {
    const numbers = rows.map((r) => (String(r.id).indexOf(prefix) === 0 ? Number(String(r.id).slice(prefix.length)) : NaN)).filter((n) => !isNaN(n));
    return prefix + String(Math.max.apply(null, numbers.concat([0])) + 1).padStart(4, "0");
  };
  const slugify = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");

  const onCreateClaim = (input) => {
    const id = nextId(data.claims, "CLM-26-");
    const existing = input.claimantId ? claimantById(data.claimants, input.claimantId) : null;
    let claimantId = existing ? existing.id : slugify(input.newClaimant.name);
    if (!existing && claimantById(data.claimants, claimantId)) claimantId = claimantId + "-" + id.slice(-4);
    const name = existing ? existing.name : input.newClaimant.name;
    const claim = {
      id: id, customer: name, claimedAt: now, deliveredAt: input.deliveredAt,
      bl: input.bl, container: input.container, vessel: input.vessel, voyage: input.voyage,
      commodity: input.commodity, packages: input.packages, weightKg: input.weightKg,
      pol: input.pol, pod: input.pod, incident: input.incident,
      status: "awaiting_documents", claimedCents: input.claimedCents, assessment: null,
      /* The whole checklist is opened as requested — the file starts waiting on
         the claimant, which is what awaiting_documents means. */
      documents: DOCUMENT_TYPES.map((type) => ({ type: type.value, status: "requested", receivedAt: null, note: "Requested when the file was opened." })),
      notes: [{ at: now, who: user.name, text: "File opened from the claim letter. All " + DOCUMENT_TYPES.length + " documents requested from " + name + "." }],
    };
    setData((d) => Object.assign({}, d, {
      claims: d.claims.concat([claim]),
      claimants: existing ? d.claimants : d.claimants.concat([{
        id: claimantId, name: name, contact: input.newClaimant.contact, email: input.newClaimant.email,
        since: now, lane: input.newClaimant.lane, terms: "Spot booking · bill of lading terms",
      }]),
    }));
    setNewClaimFor(null);
    pushToast({ variant: "success", title: "Claim opened", description: id + " · " + name + " · " + formatMoney(input.claimedCents) + " claimed" });
    setPath("/claims/" + id + "/evidence");
  };

  const onCreateRecovery = (input) => {
    const claim = claimById(data.claims, input.claimId);
    const id = nextId(data.recoveries, "REC-26-");
    /* soughtCents reads the settlement rather than storing a second figure. */
    const sought = claim.settlement.amountCents;
    const recovery = {
      id: id, claimId: claim.id, counterparty: input.counterparty, basis: input.basis,
      soughtCents: sought, recoveredCents: 0, offerCents: null,
      status: "to_file", openedAt: now, fileByAt: input.fileByAt,
      log: [{ at: now, who: user.name, text: "Recovery opened against " + input.counterparty.name + " on the settlement of " + formatMoney(sought) + " paid on " + claim.id + ". Notice not yet served." }],
    };
    setData((d) => Object.assign({}, d, {
      recoveries: d.recoveries.concat([recovery]),
      claims: d.claims.map((c) => (c.id === claim.id ? Object.assign({}, c, { recoveryId: id }) : c)),
    }));
    pushToast({ variant: "success", title: "Recovery opened", description: id + " · " + formatMoney(sought) + " against " + input.counterparty.name });
    setPath("/recovery/" + id);
  };

  /* ── Notifications ─────────────────────────────────────────────────── */
  /* Read state is per notification, so the bar's count, the panel and the page
     all derive from one list rather than from a single "seen" flag. */
  const unread = unreadNotifications(data.notifications, read);
  const markRead = (id) => setRead((ids) => (ids.indexOf(id) > -1 ? ids : ids.concat([id])));
  const onOpenNotification = (n) => { markRead(n.id); setPath(n.route); };
  const onReadAll = () => {
    const snapshot = read;
    setRead(data.notifications.map((n) => n.id));
    pushToast({ title: "Marked read", description: formatPlural(unread.length, "notification") + " cleared", onUndo: () => setRead(snapshot) });
  };

  /* ── Routing ───────────────────────────────────────────────────────── */
  const claimMatch = path.match(/^\/claims\/([^/]+)(\/(evidence|decision))?$/);
  const recoveryMatch = path.match(/^\/recovery\/([^/]+)$/);
  const claimantMatch = path.match(/^\/claimants\/([^/]+)$/);
  const settingsMatch = path === "/settings" ? "profile" : (path.match(/^\/settings\/(authority|alerts|desk)$/) || [])[1];
  const claim = claimMatch ? claimById(data.claims, claimMatch[1]) : null;
  const recovery = recoveryMatch ? recoveryById(data.recoveries, recoveryMatch[1]) : null;
  const claimant = claimantMatch ? claimantById(data.claimants, claimantMatch[1]) : null;

  const onSaveProfile = (draft) => {
    const snapshot = user;
    setUser(Object.assign({}, user, { name: draft.name.trim(), role: draft.role.trim(), email: draft.email.trim(), avatar: draft.avatar || null }));
    pushToast({ variant: "success", title: "Profile saved", description: draft.name.trim() + " · " + draft.role.trim(), onUndo: () => setUser(snapshot) });
  };
  const onSaveAlerts = (draft) => {
    const snapshot = user;
    setUser(Object.assign({}, user, { alerts: draft.alerts }));
    pushToast({ variant: "success", title: "Alerts saved", description: draft.alerts.length + " kinds of alert will be emailed to " + user.email, onUndo: () => setUser(snapshot) });
  };
  const onSaveDesk = (draft) => {
    const snapshot = desk;
    setDesk(draft);
    const landing = CLAIMS_DESTINATIONS.find((r) => r.path === draft.landing);
    pushToast({ variant: "success", title: "Desk saved", description: "Signing in opens " + (landing ? landing.label : "Work queue"), onUndo: () => setDesk(snapshot) });
  };

  /* Auth has no navigation owner, so it is returned before the shell. */
  /* Harness chrome, not product chrome: the floating page menu is how a reader
     reaches every registered screen, including the auth ones the shell cannot
     render. It sits outside the shell so it is on those screens too. */
  const preview = (
    <TemplateMenu product="Claims Desk" path={path} onNavigate={setPath} groups={CLAIMS_PAGES(data)}
      dark={dark} onToggleTheme={() => setDark(!dark)} />
  );

  if (CLAIMS_AUTH_PATHS.indexOf(path) > -1) {
    return (
      <>
        {path === "/signin"
          ? <SignInScreen carrier={CLAIMS_CARRIER} user={user} onSignIn={() => setPath(desk.landing)} />
          : <SignedOutScreen carrier={CLAIMS_CARRIER} user={user} onSignIn={() => setPath("/signin")} />}
        {preview}
      </>
    );
  }

  const screen = () => {
    if (path === "/") return <QueueScreen data={data} now={now} desk={desk} onNavigate={setPath} onNewClaim={openNewClaim} onChase={(c) => onClaimTransition(c, claimTransitions(c)[0])} pushToast={pushToast} />;
    if (path === "/signoff") return <SignoffScreen data={data} now={now} onNavigate={setPath} onTransition={onClaimTransition} />;
    if (path === "/payments") return <PaymentsScreen data={data} now={now} onNavigate={setPath} onSettlementTransition={onSettlementTransition} pushToast={pushToast} />;
    if (path === "/responses") return <ResponsesScreen data={data} now={now} onNavigate={setPath} onResponseTransition={onResponseTransition} pushToast={pushToast} />;
    if (path === "/recovery") return <RecoveryScreen data={data} now={now} onNavigate={setPath} onRecoveryTransition={onRecoveryTransition} pushToast={pushToast} />;
    if (path === "/notifications") return <NotificationsScreen items={data.notifications} readIds={read} user={user} now={now} onNavigate={setPath} onOpen={onOpenNotification} onReadAll={onReadAll} />;
    if (path === "/claims/new") return <NewClaimScreen data={data} now={now} forClaimant={newClaimFor} onNavigate={setPath} onCreate={onCreateClaim} />;
    if (path === "/recovery/new") return <NewRecoveryScreen data={data} now={now} onNavigate={setPath} onCreate={onCreateRecovery} />;
    if (claim) {
      const view = claimMatch[3] || "assessment";
      if (view === "evidence") return <EvidenceScreen claim={claim} now={now} onNavigate={setPath} onTransition={onClaimTransition} onDocumentAction={onDocumentAction} />;
      if (view === "decision") return <DecisionScreen claim={claim} now={now} onNavigate={setPath} onTransition={onClaimTransition} onRecordDecision={onRecordDecision} />;
      return <ClaimScreen claim={claim} data={data} now={now} onNavigate={setPath} onTransition={onClaimTransition} onSaveAssessment={onSaveAssessment} pushToast={pushToast} />;
    }
    if (claimant) return <ClaimantScreen claimant={claimant} data={data} now={now} onNavigate={setPath} onNewClaim={openNewClaim} />;
    if (settingsMatch) {
      return (
        <SettingsScreen
          view={settingsMatch} user={user} desk={desk} data={data} now={now} onNavigate={setPath}
          onSaveProfile={onSaveProfile} onSaveAlerts={onSaveAlerts} onSaveDesk={onSaveDesk}
        />
      );
    }
    if (recovery) return <RecoveryDetailScreen recovery={recovery} data={data} now={now} onNavigate={setPath} onRecoveryTransition={onRecoveryTransition} />;
    return <RouteNotBuilt path={path} onNavigate={setPath} />;
  };

  const recordGroups = [
    { label: "Claims you can decide now", items: decidableClaims(data.claims).map((c) => ({ value: c.id, label: c.id + " · " + c.customer + " · " + formatMoney(payableOf(c) || c.claimedCents), route: "/claims/" + c.id + "/decision" })) },
    { label: "Waiting on a signature", items: awaitingSignoff(data.claims).map((c) => ({ value: c.id, label: c.id + " · " + c.customer + " · " + formatMoney(payableOf(c)), route: "/signoff" })) },
    { label: "Payments to schedule", items: settlementsToSchedule(data.claims).map((c) => ({ value: c.id, label: c.id + " · " + c.customer + " · " + formatMoney(c.settlement.amountCents), route: "/payments" })) },
    { label: "Recoveries to file", items: recoveriesToFile(data.recoveries).map((r) => ({ value: r.id, label: r.id + " · " + r.counterparty.name + " · " + formatMoney(r.soughtCents), route: "/recovery/" + r.id })) },
    { label: "Claimants", items: data.claimants.map((p) => ({ value: p.id, label: p.name + " · " + formatPlural(claimsOfClaimant(data.claims, p).length, "claim"), route: "/claimants/" + p.id })) },
  ];

  return (
    <ClaimsShell
      user={user}
      path={path}
      onNavigate={setPath}
      dark={dark}
      onToggleTheme={() => setDark(!dark)}
      data={data}
      notifications={data.notifications}
      readIds={read}
      unread={unread.length}
      onRead={onReadAll}
      onOpenNotification={onOpenNotification}
      palette={palette}
      toasts={toasts}
      onDismissToast={dismiss}
    >
      {screen()}
      <CommandPalette
        open={palette.open}
        onClose={() => palette.setOpen(false)}
        routes={CLAIMS_ROUTES}
        recordGroups={recordGroups}
        onNavigate={setPath}
        placeholder="Search claims, containers, recoveries…"
      />
      <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}
    </ClaimsShell>
  );
}
/* boot.js publishes a stub on window.ClaimsDeskApp before anything is fetched,
   so <x-import> always resolves; the real component lands here and the stub
   swaps it in. */
window.__claimsDeskReal = App;
if (!window.ClaimsDeskApp) window.ClaimsDeskApp = App;
