// The competition — one going-to-market, its suppliers, and what they hold
// (0123-0125, SRC-5).
//
// WHAT THIS PANE IS FOR. PRODUCT.md §4 item 4 has said "nothing leaves the
// building" through every audit cycle this repository has had: a sourcing
// document could be built, counted character by character, fingerprinted and
// downloaded, and there was no way to put it in front of a supplier. This is
// the screen that ends that.
//
// FOUR SURFACES, and the order is the order of the work:
//
//   the competitions   open one, see who is in it, close it when it is done
//   who is competing   invite and withdraw suppliers  (procurement's, and the
//                      reason the seventh role exists — Mike, 2026-08-29)
//   what they hold     which supplier has which VERSION of the paper, at what
//                      address, with the fingerprint of the bytes. The
//                      differentiator's evidence, as a list rather than an
//                      investigation.
//   the address book   who we can write to at each supplier
//
// EVERY ACT HERE IS THE DATABASE'S TO REFUSE. This file contains no permission
// logic: it draws what the reads answer, and when an act is refused it shows
// the sentence the doorway returned. A role that may not invite still sees the
// competition — the control is simply not drawn for them, which is the rack's
// own rule about not offering a door onto a refusal.

const { useState } = React;

// The vocabulary is 0094's, rendered by label rather than by key. A screen
// that printed `rfq` would be showing somebody the database's spelling.
const KIND = { rfp: 'Request for Proposal', rfq: 'Request for Quotation' };

// A competition's three states, in the words a person would use.
const STATE_WORD = { open: 'open', closed: 'closed', cancelled: 'cancelled' };

function SourcingEventPane({ me }) {
  const [openId, openEvent] = useAddressedRecord('competitions');

  const events = usePane(() => API.sourcingEvents());
  const invitations = usePane(() => API.sourcingInvitations());
  const issues = usePane(() => API.sourcingIssues());
  const responses = usePane(() => API.sourcingResponses());
  const evaluations = usePane(() => API.sourcingEvaluations());
  const comparison = usePane(() => API.sourcingComparison());
  const contacts = usePane(() => API.supplierContacts());
  const runs = usePane(() => API.sourcingRuns());

  if (events.status === 'loading') return <Loading />;
  if (events.status === 'failed') return <LoadFailed reason={events.reason} />;

  // WHO MAY DO WHAT, read off the role and matching the policies exactly.
  // Inviting and issuing are procurement's and Legal's (0124, 0125); opening a
  // competition is also the requester's, because it is the front of their own
  // job and 0096 already lets them build the document.
  const mayInvite = ['procurement', 'legal_reviewer', 'legal_admin'].includes(me.role);
  const mayOpen = mayInvite || me.role === 'requester';
  const mayCurate = me.role === 'procurement';

  const rows = events.rows || [];
  const open = openId ? rows.find((e) => String(e.event_id) === String(openId)) : null;

  if (open) {
    return (
      <OneCompetition
        event={open} me={me} mayInvite={mayInvite} mayOpen={mayOpen}
        invitations={(invitations.rows || []).filter(
          (i) => String(i.event_id) === String(open.event_id))}
        issues={(issues.rows || []).filter(
          (i) => String(i.event_id) === String(open.event_id))}
        responses={(responses.rows || []).filter(
          (r) => String(r.event_id) === String(open.event_id))}
        evaluations={(evaluations.rows || []).filter(
          (e) => String(e.event_id) === String(open.event_id))}
        comparison={(comparison.rows || []).filter(
          (c) => String(c.event_id) === String(open.event_id))}
        runs={(runs.rows || []).filter(
          (r) => String(r.event_id) === String(open.event_id))}
        contacts={contacts.rows || []}
        onClose={() => openEvent(null)}
        onChanged={() => {
          events.reload(); invitations.reload();
          issues.reload(); responses.reload();
          evaluations.reload(); comparison.reload();
        }}
      />
    );
  }

  return (
    <div>
      <PaneHead
        title="Competitions"
        kicker="going to market"
        sub="One row per competition: what is being bought, when responses are due, and how many suppliers are in it."
      />
      <CompetitionsAndTheirFigures
        rows={rows} issues={issues.rows || []}
        onOpen={(id) => openEvent(String(id))}
        beforeList={mayOpen ? <OpenCompetition onOpened={() => events.reload()} /> : null} />
      {mayCurate && (
        <AddressBook contacts={contacts.rows || []} onChanged={() => contacts.reload()} />
      )}
    </div>
  );
}

// ── The figures ───────────────────────────────────────────────────────────
// EVERY FIGURE HERE EITHER REACHES THE SET IT COUNTED OR IS NOT DRAWN. The
// rack's rule and the desk's, kept at a third site. `suppliers invited` is a
// SUM OVER competitions and the list below it is a list OF competitions — two
// different units — so it carries no drill rather than a drill that would land
// on the wrong set.
// THE FIGURES AND THE LIST SHARE ONE FILTER, which is the only way a figure can
// honestly be a control: pressing "competitions open" narrows the list below to
// exactly the rows it counted, in the same unit. A figure that led somewhere
// else, or to a differently-computed set, is the trap F8 is entirely about.
function CompetitionsAndTheirFigures({ rows, issues, onOpen, beforeList }) {
  // `view` is not optional decoration: the-view-you-come-back-to.test.mjs
  // refuses a list without one, because a saved-view feature that reaches some
  // lists and not others is worse than one that reaches none — somebody learns
  // to rely on it and then loses their place on the list that lacks it.
  const filter = useListFilter(rows, {
    view: 'competitions:list',
    fields: ['title', 'agreement_id', 'opened_by', 'event_type'],
    facet: 'state',
  });
  const live = rows.filter((e) => e.state === 'open');
  const invited = rows.reduce((n, e) => n + (e.suppliers_invited || 0), 0);
  const issued = new Set(issues.map((i) => i.issue_id)).size;

  return (
    <div>
      <div className="grid gap-3 mt-4" data-testid="competition-counts"
           style={{ gridTemplateColumns: 'repeat(auto-fit,minmax(180px,1fr))' }}>
        {/* `to` IS A FUNCTION, not a URL. openableRow calls it — passing a
            string made React take the href as an onClick handler, which is one
            of the two bugs only driving the running application found. */}
        <StatBox label="competitions open" n={live.length}
                 to={() => { filter.setQ(''); filter.setPick('open'); }}
                 describe={`show the ${live.length} open competitions`} />
        <StatBox label="competitions in all" n={rows.length}
                 to={() => { filter.setQ(''); filter.setPick(''); }}
                 describe={`show all ${rows.length} competitions`} />
        {/* NO `to` ON EITHER OF THESE, deliberately. This one counts SUPPLIER
            INVITATIONS and the list below counts COMPETITIONS — a different
            unit, so a drill would land on a set that is not what was counted. */}
        <StatBox label="supplier invitations" n={invited} />
        {/* And this counts VERSIONS ISSUED, across competitions. Same reason. */}
        <StatBox label="versions issued" n={issued} />
      </div>
      {beforeList}
      <CompetitionList rows={rows} filter={filter} onOpen={onOpen} />
    </div>
  );
}

function CompetitionList({ rows, filter, onOpen }) {
  if (!rows.length) {
    return (
      <p className="muted mt-6" data-testid="no-competitions">
        No competition has been opened yet. Opening one is how a built document
        reaches a supplier.
      </p>
    );
  }
  return (
    <div className="mt-6">
      <ListFilter filter={filter} placeholder="search by title, deal or who opened it"
                  testid="competition-filter" />
      <table className="w-full mt-3" data-testid="competition-list">
        <thead>
          <tr>
            <th>what is being bought</th><th>kind</th><th>state</th>
            <th>responses due</th><th>suppliers</th><th>opened by</th>
          </tr>
        </thead>
        <tbody>
          {filter.shown.map((e) => (
            <tr key={e.event_id} {...openableRow(() => onOpen(e.event_id),
                  `open the competition for ${e.title}`)}>
              <td>
                <div>{e.title}</div>
                {e.awarded_response_id && (
                  <span className="pill pill-open text-xs mt-1" data-testid={`competition-awarded-pill-${e.event_id}`}>
                    ★ Awarded: {e.awarded_supplier_name || 'Winner'}
                  </span>
                )}
              </td>
              <td>{KIND[e.event_type] || e.event_type}</td>
              <td><span className={`pill pill-${e.state}`}>{STATE_WORD[e.state]}</span></td>
              <td>{e.responses_due_on || <span className="muted">not set</span>}</td>
              <td>
                {e.suppliers_invited}
                {e.suppliers_withdrawn > 0 && (
                  <span className="muted"> · {e.suppliers_withdrawn} withdrew</span>
                )}
              </td>
              <td className="muted">{e.opened_by}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

// ── Opening one ───────────────────────────────────────────────────────────
function OpenCompetition({ onOpened }) {
  const [form, setForm] = useState({
    event_type: 'rfp', title: '', agreement_id: '',
    questions_close_on: '', responses_due_on: '',
  });
  const [failed, setFailed] = useState(null);
  const { busy, run } = useActs();
  const set = (k) => (e) => setForm({ ...form, [k]: e.target.value });

  return (
    <details className="card mt-6" data-testid="open-competition">
      <summary className="h2">Open a competition</summary>
      <div className="mt-3 grid gap-3"
           style={{ gridTemplateColumns: 'repeat(auto-fit,minmax(240px,1fr))' }}>
        <label className="block">
          <span className="tag">what is being bought</span>
          <input className="mt-1 w-full" data-testid="event-title"
                 value={form.title} onChange={set('title')} />
        </label>
        <label className="block">
          <span className="tag">kind</span>
          <select className="mt-1 w-full" data-testid="event-kind"
                  value={form.event_type} onChange={set('event_type')}>
            <option value="rfp">{KIND.rfp}</option>
            <option value="rfq">{KIND.rfq}</option>
          </select>
        </label>
        <label className="block">
          <span className="tag">deal reference, if there is one yet</span>
          {/* Optional, and the field says so: a competition can precede the
              agreement record, which is the ordinary procurement sequence. */}
          <input className="mt-1 w-full" data-testid="event-deal"
                 value={form.agreement_id} onChange={set('agreement_id')} />
        </label>
        <label className="block">
          <span className="tag">responses due</span>
          <input type="date" className="mt-1 w-full" data-testid="event-due"
                 value={form.responses_due_on} onChange={set('responses_due_on')} />
        </label>
      </div>
      {failed && <p className="refusal mt-3" data-testid="open-refused">{failed}</p>}
      <ActButton className="btn btn-primary mt-3" data-testid="open-competition-go"
                 disabled={busy || !form.title.trim()}
                 onClick={() => run('open', async () => {
                   const r = await API.openSourcingEvent({
                     event_type: form.event_type,
                     title: form.title.trim(),
                     agreement_id: form.agreement_id.trim() || null,
                     questions_close_on: form.questions_close_on || null,
                     responses_due_on: form.responses_due_on || null,
                   });
                   if (!r.ok) { setFailed(r.reason); return; }
                   setFailed(null);
                   setForm({ event_type: 'rfp', title: '', agreement_id: '',
                             questions_close_on: '', responses_due_on: '' });
                   onOpened();
                 })}>
        {busy ? 'opening…' : 'open this competition'}
      </ActButton>
    </details>
  );
}

// ── One competition ───────────────────────────────────────────────────────
function OneCompetition({ event, me, mayInvite, mayOpen, invitations, issues, responses,
                          evaluations, comparison, runs, contacts, onClose, onChanged }) {
  const live = invitations.filter((i) => !i.withdrawn_at);
  const gone = invitations.filter((i) => i.withdrawn_at);
  // One row per VERSION, newest first — the register returns one row per
  // recipient, so the versions are folded here rather than counted twice.
  const versions = [...new Map(issues.map((i) => [i.issue_no, i])).values()]
    .sort((a, b) => b.issue_no - a.issue_no);

  return (
    <div>
      <PaneHead
        title={event.title}
        kicker={`${KIND[event.event_type] || event.event_type} · ${STATE_WORD[event.state]}`}
        sub={event.agreement_id
          ? `For deal ${event.agreement_id}. Opened by ${event.opened_by}.`
          : `No deal reference yet — a competition can precede the agreement record. Opened by ${event.opened_by}.`}
        right={<button className="btn" data-testid="close-competition-view"
                       onClick={onClose}>back to competitions</button>}
      />

      {event.state === 'closed' && event.awarded_response_id ? (
        <div className="card mt-4 p-4 bg-subtle" data-testid="competition-awarded-banner" style={{ borderLeft: '4px solid #10b981' }}>
          <div className="flex items-baseline justify-between flex-wrap gap-2">
            <div>
              <span className="tag font-bold uppercase" style={{ color: '#059669' }}>★ Decision: Awarded Winner</span>
              <h3 className="h2 mt-1">{event.awarded_supplier_name || 'Winning Supplier'}</h3>
            </div>
            <div className="text-xs muted">
              Awarded by <strong>{event.awarded_by}</strong> · {since(event.awarded_at)}
            </div>
          </div>
          <div className="mt-3 text-sm">
            <span className="font-semibold text-xs text-muted block mb-1">Award Rationale:</span>
            <p className="italic bg-card p-2 rounded" data-testid="awarded-rationale-text">"{event.award_rationale}"</p>
          </div>
          {event.agreement_id && (
            <div className="mt-3 text-xs flex items-center gap-2">
              <span className="tag">Deal Room</span>
              <span>Agreement <strong>{event.agreement_id}</strong> is active in negotiation.</span>
            </div>
          )}
        </div>
      ) : event.state !== 'open' && (
        <p className="muted mt-3" data-testid="competition-ended">
          This competition is {STATE_WORD[event.state]}
          {event.closed_reason ? `: ${event.closed_reason}` : ''}. Nobody else
          joins it and nothing further is issued for it.
        </p>
      )}

      <WhoIsCompeting
        event={event} live={live} gone={gone} mayInvite={mayInvite}
        onChanged={onChanged} />

      {/* CLOSING, which a-built-thing-has-a-way-in.test.mjs caught me building
          and never wiring up. The act existed in the doorway and no screen
          called it, which is the exact shape that census counts: a capability
          nobody can use. Offered to whoever may open one — the requester whose
          competition it is, Legal, and procurement. */}
      {mayOpen && event.state === 'open' && (
        <CloseCompetition event={event} onClosed={onChanged} />
      )}

      <WhatTheyHold versions={versions} issues={issues}
                    mayDeliver={mayInvite} onDelivered={onChanged} />

      {mayInvite && event.state === 'open' && (
        <IssueTheDocument
          event={event} runs={runs} versions={versions}
          recipients={live.length}
          contactsFor={(supplierId) => contacts.filter(
            (c) => String(c.supplier_id) === String(supplierId)).length}
          live={live}
          onIssued={onChanged} />
      )}

      <SupplierResponses
        event={event}
        responses={responses || []}
        liveInvitations={live}
        versions={versions}
        mayInvite={mayInvite}
        onChanged={onChanged} />

      <BidComparisonMatrix
        event={event}
        me={me}
        comparisonRows={comparison || []}
        evaluations={evaluations || []}
        mayEvaluate={mayOpen}
        onChanged={onChanged} />
    </div>
  );
}

function WhoIsCompeting({ event, live, gone, mayInvite, onChanged }) {
  const [failed, setFailed] = useState(null);
  const { busy, run } = useActs();
  return (
    <section className="mt-6">
      <h2 className="h2">Who is competing</h2>
      {!live.length && (
        <p className="muted mt-2" data-testid="nobody-competing">
          Nobody has been invited yet. A document cannot be issued to an empty
          competition — the act is refused rather than sent to nought people.
        </p>
      )}
      {live.length > 0 && (
        <div style={{ overflowX: 'auto' }}>
        <table className="w-full mt-2" data-testid="competing-list">
          <thead><tr><th>supplier</th><th>invited by</th><th>invited</th><th /></tr></thead>
          <tbody>
            {live.map((i) => (
              <tr key={i.invitation_id}>
                <td>{i.supplier_name}</td>
                <td className="muted">{i.invited_by}</td>
                <td className="muted">{since(i.invited_at)}</td>
                <td>
                  {mayInvite && event.state === 'open' && (
                    <WithdrawSupplier
                      invitation={i} busy={busy} run={run}
                      onFailed={setFailed} onDone={onChanged} />
                  )}
                </td>
              </tr>
            ))}
          </tbody>
        </table>
        </div>
      )}
      {gone.length > 0 && (
        <details className="mt-3" data-testid="withdrawn-list">
          <summary className="muted">{gone.length} withdrew</summary>
          <ul className="mt-2">
            {gone.map((i) => (
              <li key={i.invitation_id} className="muted">
                {i.supplier_name} — {i.withdrawn_note} ({i.withdrawn_by})
              </li>
            ))}
          </ul>
        </details>
      )}
      {failed && <p className="refusal mt-3" data-testid="invite-refused">{failed}</p>}
      {mayInvite && event.state === 'open' && (
        <InviteSupplier event={event} onFailed={setFailed} onDone={onChanged} />
      )}
    </section>
  );
}

function InviteSupplier({ event, onFailed, onDone }) {
  const suppliers = usePane(() => API.supplierNames());
  const [pick, setPick] = useState('');
  const { busy, run } = useActs();
  if (suppliers.status !== 'loaded') return null;
  return (
    <div className="card mt-3" data-testid="invite-supplier">
      <label className="block">
        <span className="tag">invite a supplier to this competition</span>
        <select className="mt-1 w-full" data-testid="invite-pick"
                value={pick} onChange={(e) => setPick(e.target.value)}>
          <option value="">choose a supplier</option>
          {(suppliers.rows || []).map((s) => (
            <option key={s.supplier_id} value={s.supplier_id}>{s.name}</option>
          ))}
        </select>
      </label>
      <ActButton className="btn btn-primary mt-3" data-testid="invite-go"
                 disabled={busy || !pick}
                 onClick={() => run('invite', async () => {
                   const r = await API.inviteSupplier({
                     event_id: event.event_id, supplier_id: Number(pick) });
                   if (!r.ok) { onFailed(r.reason); return; }
                   onFailed(null); setPick(''); onDone();
                 })}>
        {busy ? 'inviting…' : 'invite'}
      </ActButton>
    </div>
  );
}

function WithdrawSupplier({ invitation, busy, run, onFailed, onDone }) {
  const [note, setNote] = useState('');
  const [asking, setAsking] = useState(false);
  if (!asking) {
    return (
      <button className="btn btn-quiet" data-testid={`withdraw-${invitation.invitation_id}`}
              onClick={() => setAsking(true)}>withdraw</button>
    );
  }
  return (
    <div>
      {/* THE REASON IS REQUIRED BY THE DATABASE, and the form asks for it
          rather than letting the refusal be the first anybody hears of it. */}
      <input className="w-full" placeholder="why they are being withdrawn"
             data-testid={`withdraw-note-${invitation.invitation_id}`}
             value={note} onChange={(e) => setNote(e.target.value)} />
      <ActButton className="btn mt-2" disabled={busy || !note.trim()}
                 data-testid={`withdraw-go-${invitation.invitation_id}`}
                 onClick={() => run('withdraw', async () => {
                   const r = await API.withdrawSupplier({
                     invitation_id: invitation.invitation_id, note: note.trim() });
                   if (!r.ok) { onFailed(r.reason); return; }
                   onFailed(null); setAsking(false); onDone();
                 })}>confirm</ActButton>
    </div>
  );
}

// ── Closing one ───────────────────────────────────────────────────────────
// A competition that has run its course, or one being abandoned. TWO OUTCOMES
// AND THEY ARE DIFFERENT: `closed` is the ordinary end, `cancelled` is stopping
// without a result — and the database requires a reason for the second, because
// a competition that stopped competing without one is exactly what a supplier
// disputes. The form asks for it rather than letting the refusal be the first
// anybody hears of it.
//
// There is no reopening, by design: the trigger refuses it, and the screen says
// so rather than offering a control that would be refused.
function CloseCompetition({ event, onClosed }) {
  const [state, setState] = useState('closed');
  const [reason, setReason] = useState('');
  const [failed, setFailed] = useState(null);
  const { busy, run } = useActs();
  const needsReason = state === 'cancelled';
  return (
    <details className="card mt-6" data-testid="close-competition">
      <summary className="h2">End this competition</summary>
      <p className="muted mt-2">
        Once ended, nobody else joins it and nothing further is issued for it.
        It cannot be reopened — going back to market is a new competition.
      </p>
      <label className="block mt-3">
        <span className="tag">how it ended</span>
        <select className="mt-1 w-full" data-testid="close-state"
                value={state} onChange={(e) => setState(e.target.value)}>
          <option value="closed">closed — it ran its course</option>
          <option value="cancelled">cancelled — stopped without a result</option>
        </select>
      </label>
      <label className="block mt-3">
        <span className="tag">
          {needsReason ? 'why it was cancelled' : 'a note, if you want one'}
        </span>
        <input className="mt-1 w-full" data-testid="close-reason"
               value={reason} onChange={(e) => setReason(e.target.value)} />
      </label>
      {failed && <p className="refusal mt-3" data-testid="close-refused">{failed}</p>}
      <ActButton className="btn btn-primary mt-3" data-testid="close-competition-go"
                 disabled={busy || (needsReason && !reason.trim())}
                 onClick={() => run('close', async () => {
                   const r = await API.closeSourcingEvent({
                     event_id: event.event_id, state,
                     reason: reason.trim() || null });
                   if (!r.ok) { setFailed(r.reason); return; }
                   setFailed(null); onClosed();
                 })}>
        {busy ? 'ending…' : (needsReason ? 'cancel this competition' : 'close this competition')}
      </ActButton>
    </details>
  );
}

// ── What they hold ────────────────────────────────────────────────────────
// THE DIFFERENTIATOR'S EVIDENCE. "Did this supplier see the terms before they
// bid?" is this table, not an investigation.
function WhatTheyHold({ versions, issues, mayDeliver, onDelivered }) {
  if (!versions.length) {
    return (
      <section className="mt-8">
        <h2 className="h2">What the suppliers hold</h2>
        <p className="muted mt-2" data-testid="nothing-issued">
          Nothing has been issued yet. Until it is, no supplier has seen the paper.
        </p>
      </section>
    );
  }
  return (
    <section className="mt-8">
      <h2 className="h2">What the suppliers hold</h2>
      {versions.map((v) => {
        const got = issues.filter((i) => i.issue_no === v.issue_no);
        return (
          <div key={v.issue_no} className="card mt-3" data-testid={`version-${v.issue_no}`}>
            <div className="flex justify-between">
              <strong>Version {v.issue_no}</strong>
              <span className="muted">
                issued by {v.issued_by} · {since(v.issued_at)}
              </span>
            </div>
            {v.change_note && (
              <p className="mt-1" data-testid={`change-note-${v.issue_no}`}>
                What changed: {v.change_note}
              </p>
            )}
            {/* The fingerprint of the bytes, so what a supplier holds can be
                checked against the record rather than taken on trust. */}
            <p className="muted mt-1 mono">
              document {v.document_sha256.slice(0, 12)}… · dated {v.document_date}
            </p>
            {/* THE TABLE SCROLLS INSIDE ITSELF, and this is a repair rather
                than a flourish. Measured at 375: the four columns want 410
                pixels BEFORE this change and 440 after it, and the surrounding
                card clips rather than scrolls — so the delivery column, which
                is where the failure reason now lives, was simply unreadable on
                a phone. The page itself never scrolled sideways (that is
                S324's defect and registry.css already fixed it), which is
                exactly why this one was invisible: nothing moved, the content
                was just gone.
                `auditor.jsx` takes the same shape for the same reason. At any
                width that fits, `auto` draws nothing. */}
            <div style={{ overflowX: 'auto' }}>
            <table className="w-full mt-2">
              <thead><tr><th>supplier</th><th>person</th><th>address</th><th>delivery</th></tr></thead>
              <tbody>
                {got.map((r) => (
                  <tr key={`${r.issue_id}-${r.contact_id}`}>
                    <td>{r.supplier_name}</td>
                    <td>{r.contact_name}</td>
                    <td className="mono">{r.address_used}</td>
                    <td>
                      {/* THREE ANSWERS, NOT TWO. Nobody has tried is not the
                          same as sent, and saying "sent" would be a claim the
                          record cannot support. */}
                      {r.outcome === 'sent' && (
                        <span className="pill pill-open">
                          sent{r.attempted_at ? ` · ${since(r.attempted_at)}` : ''}
                        </span>
                      )}
                      {/* THE REASON, NOT JUST THE VERDICT. A buyer told only
                          "failed" can do nothing; told the mailbox was
                          unavailable, or that no mail server is configured,
                          they can. 0127 appends `failure` to the register for
                          this line alone. */}
                      {r.outcome === 'failed' && (
                        <div>
                          <span className="pill pill-cancelled">failed</span>
                          {r.failure && (
                            <div className="muted mt-1">{r.failure}</div>
                          )}
                        </div>
                      )}
                      {!r.outcome && <span className="muted">not yet handed over</span>}
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
            </div>
            {mayDeliver && (
              <SendThisVersion issue={v} recipients={got} onDelivered={onDelivered} />
            )}
          </div>
        );
      })}
    </section>
  );
}

// ── Sending it ────────────────────────────────────────────────────────────
// THE LAST INCH. Issuing said who is to receive this version; this hands it
// over and writes down what happened, per person.
//
// THE CONTROL IS SHOWN ONLY WHERE THERE IS SOMETHING TO SEND — a version
// everybody already holds offers nothing to press, because a button that
// always answers "nothing to do" teaches people to stop pressing. It is shown
// for a FAILED recipient as well as an untried one: 0127 lets a failed
// delivery be attempted again, and a screen that hid the retry would leave the
// register permanently wrong about who holds the paper.
function SendThisVersion({ issue, recipients, onDelivered }) {
  const [said, setSaid] = useState(null);
  const [failed, setFailed] = useState(null);
  const { busy, run } = useActs();

  const waiting = recipients.filter((r) => r.outcome !== 'sent');
  const retries = waiting.filter((r) => r.outcome === 'failed').length;
  if (!waiting.length) {
    return (
      <p className="muted mt-2" data-testid={`all-delivered-${issue.issue_no}`}>
        Everybody on this version holds it.
      </p>
    );
  }

  return (
    <div className="mt-3">
      <ActButton className="btn mt-1" data-testid={`deliver-${issue.issue_no}`}
                 disabled={busy}
                 onClick={() => run('deliver', async () => {
                   const r = await API.deliverIssue({ issue_id: issue.issue_id });
                   if (!r.ok) { setFailed(r.reason); setSaid(null); return; }
                   setFailed(null);
                   setSaid(r.data);
                   onDelivered();
                 })}>
        {busy ? 'sending…'
              : retries
                ? `try ${waiting.length} again`
                : `send to ${waiting.length}`}
      </ActButton>
      {/* COUNTS, NOT AN ADJECTIVE. "Sent" alone would hide the half that
          failed, which is the half somebody has to act on. */}
      {said && (
        <p className="muted mt-2" data-testid={`delivery-said-${issue.issue_no}`}>
          {said.sent} sent · {said.failed} failed
        </p>
      )}
      {failed && (
        <p className="ink-high mt-2" data-testid={`delivery-failed-${issue.issue_no}`}>
          {failed}
        </p>
      )}
    </div>
  );
}

// ── Issuing ───────────────────────────────────────────────────────────────
function IssueTheDocument({ event, runs, versions, recipients, live, contactsFor, onIssued }) {
  const [pick, setPick] = useState('');
  const [note, setNote] = useState('');
  const [failed, setFailed] = useState(null);
  const { busy, run } = useActs();
  const reissue = versions.length > 0;

  // Who among the invited has nobody to write to. Said BEFORE the act rather
  // than as a refusal afterwards, because "nobody would receive this" is a
  // sentence somebody can act on only if they know which supplier it is about.
  const unreachable = live.filter((i) => contactsFor(i.supplier_id) === 0);

  return (
    <section className="mt-8">
      <h2 className="h2">{reissue ? 'Issue a new version' : 'Issue the document'}</h2>
      {!runs.length && (
        <p className="muted mt-2" data-testid="nothing-to-issue">
          No document has been built for this competition yet. Build one on the
          sourcing screen, then issue it here.
        </p>
      )}
      {unreachable.length > 0 && (
        <p className="muted mt-2" data-testid="unreachable-suppliers">
          {unreachable.map((i) => i.supplier_name).join(', ')} — nobody at{' '}
          {unreachable.length === 1 ? 'this supplier' : 'these suppliers'} has a
          contact recorded, so they would receive nothing. Add a contact in the
          address book first.
        </p>
      )}
      {runs.length > 0 && (
        <div className="card mt-3" data-testid="issue-document">
          <label className="block">
            <span className="tag">which document</span>
            <select className="mt-1 w-full" data-testid="issue-pick"
                    value={pick} onChange={(e) => setPick(e.target.value)}>
              <option value="">choose a built document</option>
              {runs.map((r) => (
                <option key={r.sourcing_run_id} value={r.sourcing_run_id}>
                  {r.sourcing_run_id} — built {since(r.built_at)}
                </option>
              ))}
            </select>
          </label>
          {reissue && (
            <label className="block mt-3">
              {/* REQUIRED ON A REISSUE, and the form says why. Suppliers
                  holding the last version will ask. */}
              <span className="tag">
                what changed since version {versions[0].issue_no}
              </span>
              <input className="mt-1 w-full" data-testid="issue-note"
                     value={note} onChange={(e) => setNote(e.target.value)} />
            </label>
          )}
          <p className="muted mt-2" data-testid="issue-recipients">
            This will go to everybody still competing — {recipients}{' '}
            {recipients === 1 ? 'supplier' : 'suppliers'}. Version{' '}
            {reissue ? versions[0].issue_no : 1} stays exactly as it was sent.
          </p>
          {failed && <p className="refusal mt-3" data-testid="issue-refused">{failed}</p>}
          <ActButton className="btn btn-primary mt-3" data-testid="issue-go"
                     disabled={busy || !pick || (reissue && !note.trim())}
                     onClick={() => run('issue', async () => {
                       const r = await API.issueDocument({
                         event_id: event.event_id,
                         sourcing_run_id: pick,
                         change_note: reissue ? note.trim() : null,
                       });
                       if (!r.ok) { setFailed(r.reason); return; }
                       setFailed(null); setPick(''); setNote(''); onIssued();
                     })}>
            {busy ? 'issuing…' : (reissue ? 'issue this version' : 'issue to the suppliers')}
          </ActButton>
        </div>
      )}
    </section>
  );
}

// ── The address book ──────────────────────────────────────────────────────
// Procurement's, per Mike 2026-08-29. 0126 also gave this role the act that
// creates a supplier company. It lives here because Procurement has no broad
// supplier/deal-count pane: a fresh address book still needs a first company
// before anybody can be invited or given an address.
function AddressBook({ contacts, onChanged }) {
  const suppliers = usePane(() => API.supplierNames());
  const [form, setForm] = useState({ supplier_id: '', full_name: '', job_title: '', address: '' });
  const [failed, setFailed] = useState(null);
  const acts = useActs();
  const { busy, run } = acts;
  const set = (k) => (e) => setForm({ ...form, [k]: e.target.value });

  return (
    <details className="card mt-8" data-testid="address-book">
      <summary className="h2">The address book — who we can write to</summary>
      <p className="muted mt-2">
        A supplier is a company; this is the person at it. Until somebody here
        has an address, a document issued to that supplier reaches nobody.
      </p>
      {contacts.length > 0 && (
        <table className="w-full mt-3" data-testid="contact-list">
          <thead><tr><th>supplier</th><th>person</th><th>role</th><th>address</th><th /></tr></thead>
          <tbody>
            {contacts.map((c) => (
              <tr key={c.contact_id}>
                <td>{c.supplier_name}</td>
                <td>{c.full_name}</td>
                <td className="muted">{c.job_title || '—'}</td>
                <td className="mono">{c.address}</td>
                <td>
                  <RemoveContact contact={c} onFailed={setFailed} onDone={onChanged} />
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      )}
      {suppliers.status === 'loaded' && (<>
        <details className="panel p-3 mt-4" data-testid="competition-new-supplier">
          <summary className="tag">add a supplier company</summary>
          <div className="mt-3">
            <NewSupplier acts={acts} onDone={() => suppliers.reload()} />
          </div>
        </details>
        <div className="mt-4 grid gap-3"
             style={{ gridTemplateColumns: 'repeat(auto-fit,minmax(240px,1fr))' }}>
          <label className="block">
            <span className="tag">supplier</span>
            <select className="mt-1 w-full" data-testid="contact-supplier"
                    value={form.supplier_id} onChange={set('supplier_id')}>
              <option value="">choose a supplier</option>
              {(suppliers.rows || []).map((s) => (
                <option key={s.supplier_id} value={s.supplier_id}>{s.name}</option>
              ))}
            </select>
          </label>
          <label className="block">
            <span className="tag">their name</span>
            <input className="mt-1 w-full" data-testid="contact-name"
                   value={form.full_name} onChange={set('full_name')} />
          </label>
          <label className="block">
            <span className="tag">what they do there</span>
            <input className="mt-1 w-full" data-testid="contact-title"
                   value={form.job_title} onChange={set('job_title')} />
          </label>
          <label className="block">
            <span className="tag">email address</span>
            <input className="mt-1 w-full" data-testid="contact-address"
                   value={form.address} onChange={set('address')} />
          </label>
        </div>
      </>)}
      {failed && <p className="refusal mt-3" data-testid="contact-refused">{failed}</p>}
      <ActButton className="btn btn-primary mt-3" data-testid="add-contact-go"
                 disabled={busy || !form.supplier_id || !form.full_name.trim()
                           || !form.address.trim()}
                 onClick={() => run('contact', async () => {
                   const r = await API.addSupplierContact({
                     supplier_id: Number(form.supplier_id),
                     full_name: form.full_name.trim(),
                     job_title: form.job_title.trim() || null,
                     address: form.address.trim(),
                   });
                   if (!r.ok) { setFailed(r.reason); return; }
                   setFailed(null);
                   setForm({ supplier_id: '', full_name: '', job_title: '', address: '' });
                   onChanged();
                 })}>
        {busy ? 'adding…' : 'add this person'}
      </ActButton>
    </details>
  );
}

function RemoveContact({ contact, onFailed, onDone }) {
  const [asking, setAsking] = useState(false);
  const [note, setNote] = useState('');
  const { busy, run } = useActs();
  if (!asking) {
    return (
      <button className="btn btn-quiet" data-testid={`remove-contact-${contact.contact_id}`}
              onClick={() => setAsking(true)}>remove</button>
    );
  }
  return (
    <div>
      <input className="w-full" placeholder="why (optional)"
             data-testid={`remove-note-${contact.contact_id}`}
             value={note} onChange={(e) => setNote(e.target.value)} />
      {/* REMOVAL IS NOT DELETION, and the screen says so rather than letting
          somebody think the record has gone. */}
      <p className="muted">They stay on any document already issued to them.</p>
      <ActButton className="btn mt-2" disabled={busy}
                 data-testid={`remove-contact-go-${contact.contact_id}`}
                 onClick={() => run('remove', async () => {
                   const r = await API.removeSupplierContact({
                     contact_id: contact.contact_id, note: note.trim() || null });
                   if (!r.ok) { onFailed(r.reason); return; }
                   onFailed(null); setAsking(false); onDone();
                 })}>confirm</ActButton>
    </div>
  );
}

// ── Supplier responses and proposals (Phase 2a, 0132) ─────────────────────
function SupplierResponses({ event, responses, liveInvitations, versions, mayInvite, onChanged }) {
  const [failed, setFailed] = useState(null);
  const liveResponses = responses.filter((r) => r.is_live);

  return (
    <section className="mt-8" data-testid="sourcing-responses-section">
      <div className="flex justify-between items-baseline">
        <h2 className="h2">Supplier responses & proposals</h2>
        <span className="caption">
          {responses.length} received {liveResponses.length !== responses.length ? `(${liveResponses.length} active)` : ''}
        </span>
      </div>

      {!responses.length && (
        <p className="muted mt-2" data-testid="no-responses">
          No supplier responses have been recorded yet.
        </p>
      )}

      {responses.length > 0 && (
        <div style={{ overflowX: 'auto' }}>
          <table className="w-full mt-2" data-testid="responses-list">
            <thead>
              <tr>
                <th>supplier</th>
                <th>issue answered</th>
                <th>proposal summary</th>
                <th>document</th>
                <th>received</th>
                <th>status</th>
                <th />
              </tr>
            </thead>
            <tbody>
              {responses.map((r) => (
                <tr key={r.response_id} data-testid={`response-row-${r.response_id}`}>
                  <td><strong>{r.supplier_name}</strong></td>
                  <td>Issue {r.issue_no}</td>
                  <td>{r.proposal_summary || <span className="muted">none</span>}</td>
                  <td>
                    {r.document_id ? (
                      <span className="mono text-xs">
                        {r.document_filename || `doc-${r.document_id}`} ({r.document_size ? `${Math.round(r.document_size / 1024)} KB` : 'stored'})
                      </span>
                    ) : (
                      <span className="muted">no attachment</span>
                    )}
                  </td>
                  <td>
                    <span>{since(r.received_at)}</span>
                    {r.is_late && (
                      <div className="mt-1">
                        <span className="pill pill-cancelled" data-testid="response-late-badge">late</span>
                      </div>
                    )}
                  </td>
                  <td>
                    {r.is_live ? (
                      <span className="pill pill-open">active</span>
                    ) : (
                      <div>
                        <span className="pill pill-cancelled">withdrawn</span>
                        {r.withdrawal_reason && (
                          <div className="muted text-xs mt-1">{r.withdrawal_reason}</div>
                        )}
                      </div>
                    )}
                  </td>
                  <td>
                    {mayInvite && r.is_live && event.state === 'open' && (
                      <WithdrawResponse
                        response={r}
                        onFailed={setFailed}
                        onDone={onChanged} />
                    )}
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      )}

      {failed && <p className="refusal mt-3" data-testid="response-refused">{failed}</p>}

      {mayInvite && event.state === 'open' && liveInvitations.length > 0 && versions.length > 0 && (
        <RecordSupplierResponse
          event={event}
          liveInvitations={liveInvitations}
          versions={versions}
          onFailed={setFailed}
          onDone={onChanged} />
      )}
    </section>
  );
}

function RecordSupplierResponse({ event, liveInvitations, versions, onFailed, onDone }) {
  const [supplierId, setSupplierId] = useState('');
  const [issueId, setIssueId] = useState(versions[0]?.issue_id ? String(versions[0].issue_id) : '');
  const [summary, setSummary] = useState('');
  const [file, setFile] = useState(null);
  const { busy, run } = useActs();

  return (
    <details className="card mt-4" data-testid="record-response-box">
      <summary className="font-semibold text-sm">Record a supplier response</summary>
      <div className="mt-3">
        <label className="block">
          <span className="tag">supplier</span>
          <select className="mt-1 w-full" data-testid="response-supplier-select"
                  value={supplierId} onChange={(e) => setSupplierId(e.target.value)}>
            <option value="">choose responding supplier</option>
            {liveInvitations.map((i) => (
              <option key={i.supplier_id} value={i.supplier_id}>{i.supplier_name}</option>
            ))}
          </select>
        </label>

        <label className="block mt-3">
          <span className="tag">version answered</span>
          <select className="mt-1 w-full" data-testid="response-issue-select"
                  value={issueId} onChange={(e) => setIssueId(e.target.value)}>
            {versions.map((v) => (
              <option key={v.issue_id} value={v.issue_id}>Issue {v.issue_no} ({v.document_date})</option>
            ))}
          </select>
        </label>

        <label className="block mt-3">
          <span className="tag">proposal summary / key terms</span>
          <textarea className="mt-1 w-full" rows="3" data-testid="response-summary-input"
                    placeholder="Summary of pricing, delivery commitments, and terms..."
                    value={summary} onChange={(e) => setSummary(e.target.value)} />
        </label>

        <label className="block mt-3">
          <span className="tag">attached proposal document (optional)</span>
          <input type="file" className="mt-1 w-full text-xs" data-testid="response-file-input"
                 onChange={(e) => setFile(e.target.files?.[0] || null)} />
        </label>

        <ActButton className="btn btn-primary mt-3" data-testid="record-response-go"
                   disabled={busy || !supplierId || !issueId}
                   onClick={() => run('record-response', async () => {
                     const r = file
                       ? await API.uploadSourcingResponse(
                           event.event_id, Number(supplierId), Number(issueId), summary.trim() || null, file)
                       : await API.recordSourcingResponse({
                           event_id: event.event_id,
                           supplier_id: Number(supplierId),
                           issue_id: Number(issueId),
                           proposal_summary: summary.trim() || null,
                         });
                     if (!r.ok) { onFailed(r.reason); return; }
                     onFailed(null);
                     setSupplierId('');
                     setSummary('');
                     setFile(null);
                     onDone();
                   })}>
          {busy ? 'recording…' : 'record response'}
        </ActButton>
      </div>
    </details>
  );
}

function WithdrawResponse({ response, onFailed, onDone }) {
  const [asking, setAsking] = useState(false);
  const [reason, setReason] = useState('');
  const { busy, run } = useActs();

  if (!asking) {
    return (
      <button className="btn btn-quiet text-xs" data-testid={`withdraw-response-${response.response_id}`}
              onClick={() => setAsking(true)}>withdraw bid</button>
    );
  }

  return (
    <div className="mt-1">
      <input className="w-full text-xs" placeholder="reason for bid withdrawal"
             data-testid={`withdraw-response-reason-${response.response_id}`}
             value={reason} onChange={(e) => setReason(e.target.value)} />
      <div className="flex gap-2 mt-1">
        <ActButton className="btn text-xs" disabled={busy || !reason.trim()}
                   data-testid={`withdraw-response-confirm-${response.response_id}`}
                   onClick={() => run('withdraw-response', async () => {
                     const r = await API.withdrawSourcingResponse({
                       response_id: response.response_id,
                       reason: reason.trim(),
                     });
                     if (!r.ok) { onFailed(r.reason); return; }
                     onFailed(null);
                     setAsking(false);
                     onDone();
                   })}>confirm</ActButton>
        <button className="btn btn-quiet text-xs" onClick={() => setAsking(false)}>cancel</button>
      </div>
    </div>
  );
}

// ── Bid Comparison Matrix and Response Evaluations (Phase 2b/2c, 0133/0134) ─
function BidComparisonMatrix({ event, me, comparisonRows, evaluations, mayEvaluate, onChanged }) {
  const [evaluatingResponse, setEvaluatingResponse] = useState(null);
  const [awardingResponse, setAwardingResponse] = useState(null);
  const [viewingEvaluations, setViewingEvaluations] = useState(null);
  const [failed, setFailed] = useState(null);

  const total = comparisonRows.length;
  const respondedCount = comparisonRows.filter((r) => r.response_status === 'active').length;
  const evaluatedCount = comparisonRows.filter((r) => Number(r.evaluation_count) > 0).length;

  const exportMatrix = () => {
    downloadCsv({
      stem: `sourcing-bid-comparison-event-${event.event_id}`,
      head: [
        'supplier_name', 'invitation_status', 'latest_issue_no', 'delivery_outcome',
        'response_status', 'response_issue_no', 'received_at', 'is_late',
        'proposal_summary', 'document_filename', 'evaluation_count', 'average_score',
        'latest_score', 'latest_notes', 'is_awarded', 'award_rationale', 'awarded_by', 'awarded_at',
      ],
      rows: comparisonRows,
      total: comparisonRows.length,
      cell: (r, k) => {
        if (k === 'is_late') return r.is_late ? 'late' : 'on-time';
        if (k === 'latest_issue_no') return r.latest_issue_no ? `Issue ${r.latest_issue_no}` : 'none';
        if (k === 'response_issue_no') return r.response_issue_no ? `Issue ${r.response_issue_no}` : 'none';
        if (k === 'is_awarded') return r.is_awarded ? 'yes' : 'no';
        return r[k] ?? '';
      },
      by: me.person,
    });
  };

  return (
    <section className="mt-8" data-testid="sourcing-bid-comparison-section">
      <div className="flex justify-between items-baseline flex-wrap gap-2">
        <div>
          <h2 className="h2">Bid Comparison Matrix</h2>
          <p className="muted text-xs mt-1">
            Side-by-side comparison of supplier proposal terms, timeliness, evaluations, and award selection.
          </p>
        </div>
        <div className="flex items-center gap-3">
          <span className="caption">
            {respondedCount} of {total} responded · {evaluatedCount} evaluated
          </span>
          {total > 0 && (
            <button className="btn btn-quiet text-xs" data-testid="export-comparison-csv"
                    onClick={exportMatrix}>
              {csvLabel(total, total)}
            </button>
          )}
        </div>
      </div>

      {!comparisonRows.length && (
        <p className="muted mt-2" data-testid="no-comparison-rows">
          No suppliers invited to compare yet.
        </p>
      )}

      {comparisonRows.length > 0 && (
        <div style={{ overflowX: 'auto' }}>
          <table className="w-full mt-3" data-testid="comparison-matrix-table">
            <thead>
              <tr>
                <th>Supplier</th>
                <th>Version Held</th>
                <th>Submission Status</th>
                <th>Proposal Summary / Key Terms</th>
                <th>Attachment</th>
                <th>Evaluation & Score</th>
                <th />
              </tr>
            </thead>
            <tbody>
              {comparisonRows.map((r) => {
                // A pending supplier has no response id. The three state
                // holders also start at null, so `null === null` used to open
                // both forms before a proposal existed. A response is the
                // subject of either act; without one there is only a pending
                // invitation to draw.
                const hasResponse = r.response_id !== null && r.response_id !== undefined;
                const isEvaluating = hasResponse && evaluatingResponse === r.response_id;
                const isAwarding = hasResponse && awardingResponse === r.response_id;
                const isViewing = hasResponse && viewingEvaluations === r.response_id;
                const respEvals = evaluations.filter((e) => String(e.response_id) === String(r.response_id));

                return (
                  <React.Fragment key={r.supplier_id}>
                    <tr data-testid={`matrix-row-${r.supplier_id}`} className={r.is_awarded ? 'bg-subtle' : ''}>
                      <td>
                        <strong>{r.supplier_name}</strong>
                        {r.is_awarded && (
                          <div className="mt-1">
                            <span className="pill pill-open text-xs font-bold" data-testid={`matrix-awarded-badge-${r.supplier_id}`}>
                              ★ Awarded Winner
                            </span>
                          </div>
                        )}
                        {r.invitation_status === 'withdrawn' && (
                          <div className="mt-1">
                            <span className="pill pill-cancelled text-xs">withdrawn invite</span>
                          </div>
                        )}
                      </td>
                      <td>
                        {r.latest_issue_no ? (
                          <div>
                            <span>Issue {r.latest_issue_no}</span>
                            {r.delivery_outcome && (
                              <span className={`pill ml-1 text-xs ${r.delivery_outcome === 'sent' ? 'pill-open' : 'pill-cancelled'}`}>
                                {r.delivery_outcome}
                              </span>
                            )}
                          </div>
                        ) : (
                          <span className="muted">none</span>
                        )}
                      </td>
                      <td>
                        {r.response_status === 'active' && (
                          <div>
                            <span className="pill pill-open">Issue {r.response_issue_no}</span>
                            <div className="text-xs mt-1">
                              <span>{since(r.received_at)}</span>
                              {r.is_late ? (
                                <span className="pill pill-cancelled ml-1 text-xs" data-testid="matrix-late-badge">late</span>
                              ) : (
                                <span className="pill pill-open ml-1 text-xs" data-testid="matrix-ontime-badge">on time</span>
                              )}
                            </div>
                          </div>
                        )}
                        {r.response_status === 'withdrawn' && (
                          <div>
                            <span className="pill pill-cancelled">withdrawn</span>
                            {r.response_withdrawal_reason && (
                              <div className="muted text-xs mt-1">{r.response_withdrawal_reason}</div>
                            )}
                          </div>
                        )}
                        {r.response_status === 'pending' && (
                          <span className="muted">pending</span>
                        )}
                      </td>
                      <td style={{ maxWidth: '320px' }}>
                        {r.proposal_summary ? (
                          <span className="text-sm">{r.proposal_summary}</span>
                        ) : (
                          <span className="muted">no summary</span>
                        )}
                      </td>
                      <td>
                        {r.document_id ? (
                          <span className="mono text-xs">
                            {r.document_filename || `doc-${r.document_id}`} ({r.document_size ? `${Math.round(r.document_size / 1024)} KB` : 'stored'})
                          </span>
                        ) : (
                          <span className="muted">—</span>
                        )}
                      </td>
                      <td>
                        {Number(r.evaluation_count) > 0 ? (
                          <div>
                            <div className="flex items-center gap-1">
                              <span className="pill pill-open font-bold" data-testid={`matrix-score-${r.supplier_id}`}>
                                {r.average_score !== null ? `${r.average_score}/100` : 'scored'}
                              </span>
                              <button className="btn btn-quiet text-xs"
                                      data-testid={`toggle-evaluations-${r.response_id}`}
                                      onClick={() => setViewingEvaluations(isViewing ? null : r.response_id)}>
                                {isViewing ? 'hide' : `${r.evaluation_count} review${r.evaluation_count > 1 ? 's' : ''}`}
                              </button>
                            </div>
                            {r.latest_notes && (
                              <div className="muted text-xs mt-1 italic line-clamp-2">
                                "{r.latest_notes}"
                              </div>
                            )}
                          </div>
                        ) : (
                          <span className="muted text-xs">not evaluated</span>
                        )}
                      </td>
                      <td>
                        <div className="flex items-center gap-1">
                          {mayEvaluate && r.response_status === 'active' && event.state === 'open' && (
                            <>
                              <button className="btn btn-quiet text-xs"
                                      data-testid={`evaluate-btn-${r.response_id}`}
                                      onClick={() => {
                                        setAwardingResponse(null);
                                        setEvaluatingResponse(isEvaluating ? null : r.response_id);
                                      }}>
                                {isEvaluating ? 'cancel' : '+ score'}
                              </button>
                              <button className="btn text-xs font-semibold"
                                      style={{ color: '#d97706', borderColor: '#fcd34d' }}
                                      data-testid={`award-btn-${r.response_id}`}
                                      onClick={() => {
                                        setEvaluatingResponse(null);
                                        setAwardingResponse(isAwarding ? null : r.response_id);
                                      }}>
                                {isAwarding ? 'cancel' : '★ award'}
                              </button>
                            </>
                          )}
                        </div>
                      </td>
                    </tr>

                    {/* Inline Evaluation Form */}
                    {isEvaluating && (
                      <tr>
                        <td colSpan="7" className="bg-subtle p-3">
                          <RecordEvaluationForm
                            responseId={r.response_id}
                            supplierName={r.supplier_name}
                            onDone={() => {
                              setEvaluatingResponse(null);
                              onChanged();
                            }}
                            onCancel={() => setEvaluatingResponse(null)}
                            onFailed={setFailed}
                          />
                        </td>
                      </tr>
                    )}

                    {/* Inline Award Form */}
                    {isAwarding && (
                      <tr>
                        <td colSpan="7" className="bg-subtle p-3">
                          <AwardProposalForm
                            eventId={event.event_id}
                            responseId={r.response_id}
                            supplierName={r.supplier_name}
                            hasAgreement={Boolean(event.agreement_id)}
                            onDone={() => {
                              setAwardingResponse(null);
                              onChanged();
                            }}
                            onCancel={() => setAwardingResponse(null)}
                            onFailed={setFailed}
                          />
                        </td>
                      </tr>
                    )}

                    {/* Inline Evaluations View */}
                    {isViewing && (
                      <tr>
                        <td colSpan="7" className="bg-subtle p-3">
                          <div className="card" data-testid={`evaluations-detail-${r.response_id}`}>
                            <h4 className="font-semibold text-xs mb-2">Evaluator Reviews for {r.supplier_name}</h4>
                            <table className="w-full text-xs">
                              <thead>
                                <tr>
                                  <th>Evaluator</th>
                                  <th>Role</th>
                                  <th>Score</th>
                                  <th>Notes</th>
                                  <th>Date</th>
                                </tr>
                              </thead>
                              <tbody>
                                {respEvals.map((ev) => (
                                  <tr key={ev.evaluation_id}>
                                    <td><strong>{ev.evaluated_by}</strong></td>
                                    <td><span className="tag">{ev.evaluated_role}</span></td>
                                    <td>{ev.score !== null ? `${ev.score}/100` : '—'}</td>
                                    <td>{ev.notes || <span className="muted">—</span>}</td>
                                    <td className="muted">{since(ev.evaluated_at)}</td>
                                  </tr>
                                ))}
                              </tbody>
                            </table>
                          </div>
                        </td>
                      </tr>
                    )}
                  </React.Fragment>
                );
              })}
            </tbody>
          </table>
        </div>
      )}

      {failed && <p className="refusal mt-3" data-testid="evaluation-refused">{failed}</p>}
    </section>
  );
}

function RecordEvaluationForm({ responseId, supplierName, onDone, onCancel, onFailed }) {
  const [score, setScore] = useState('');
  const [notes, setNotes] = useState('');
  const { busy, run } = useActs();

  return (
    <div className="card" data-testid={`record-evaluation-form-${responseId}`}>
      <h4 className="font-semibold text-xs mb-2">Record Evaluation for {supplierName}</h4>
      <div className="grid gap-3" style={{ gridTemplateColumns: '120px 1fr' }}>
        <label className="block">
          <span className="tag">Score (0–100)</span>
          <input type="number" min="0" max="100" className="mt-1 w-full text-xs"
                 placeholder="0–100" data-testid="evaluation-score-input"
                 value={score} onChange={(e) => setScore(e.target.value)} />
        </label>
        <label className="block">
          <span className="tag">Evaluator Notes / Criteria Rationale</span>
          <textarea rows="2" className="mt-1 w-full text-xs"
                    placeholder="Assessment of technical suitability, commercial terms, risk factors..."
                    data-testid="evaluation-notes-input"
                    value={notes} onChange={(e) => setNotes(e.target.value)} />
        </label>
      </div>
      <div className="flex gap-2 mt-3">
        <ActButton className="btn btn-primary text-xs" data-testid="evaluation-submit-go"
                   disabled={busy || (!score && !notes.trim())}
                   onClick={() => run('record-eval', async () => {
                     const r = await API.recordEvaluation({
                       response_id: Number(responseId),
                       score: score !== '' ? Number(score) : null,
                       notes: notes.trim() || null,
                     });
                     if (!r.ok) { onFailed(r.reason); return; }
                     onFailed(null);
                     onDone();
                   })}>
          {busy ? 'recording…' : 'save evaluation'}
        </ActButton>
        <button className="btn btn-quiet text-xs" onClick={onCancel}>cancel</button>
      </div>
    </div>
  );
}

function AwardProposalForm({ eventId, responseId, supplierName, hasAgreement, onDone, onCancel, onFailed }) {
  const [rationale, setRationale] = useState('');
  const [agreementTitle, setAgreementTitle] = useState(`${supplierName} Agreement`);
  const [agreementId, setAgreementId] = useState('');
  const [createDeal, setCreateDeal] = useState(!hasAgreement);
  const { busy, run } = useActs();

  return (
    <div className="card" data-testid={`award-proposal-form-${responseId}`} style={{ borderLeft: '4px solid #f59e0b' }}>
      <h4 className="font-semibold text-xs mb-1">Award Competition to {supplierName}</h4>
      <p className="muted text-xs mb-3">
        The system computes arithmetic; the buyer decides the winner. Awarding closes this competition and records an append-only decision.
      </p>

      <label className="block mb-3">
        <span className="tag">Award Rationale (Required)</span>
        <textarea
          rows="3"
          className="mt-1 w-full text-xs"
          placeholder="Explain the decision and why this proposal was selected (pricing, technical score, terms alignment)..."
          data-testid="award-rationale-input"
          value={rationale}
          onChange={(e) => setRationale(e.target.value)}
        />
      </label>

      {!hasAgreement && (
        <div className="card p-3 mb-3 bg-subtle">
          <label className="flex items-center gap-2 text-xs font-semibold cursor-pointer mb-2">
            <input
              type="checkbox"
              checked={createDeal}
              onChange={(e) => setCreateDeal(e.target.checked)}
              data-testid="award-create-deal-checkbox"
            />
            <span>Seed Agreement in Deal Room</span>
          </label>

          {createDeal && (
            <div className="grid gap-2 mt-2" style={{ gridTemplateColumns: '1fr 140px' }}>
              <label className="block">
                <span className="tag text-xs">Agreement Title</span>
                <input
                  className="mt-1 w-full text-xs"
                  placeholder="e.g. Master Services Agreement"
                  data-testid="award-agreement-title-input"
                  value={agreementTitle}
                  onChange={(e) => setAgreementTitle(e.target.value)}
                />
              </label>
              <label className="block">
                <span className="tag text-xs">Agreement ID (optional)</span>
                <input
                  className="mt-1 w-full text-xs"
                  placeholder="Auto (AG-EVT-...)"
                  data-testid="award-agreement-id-input"
                  value={agreementId}
                  onChange={(e) => setAgreementId(e.target.value)}
                />
              </label>
            </div>
          )}
        </div>
      )}

      <div className="flex gap-2">
        <ActButton
          className="btn btn-primary text-xs"
          data-testid="confirm-award-btn"
          disabled={busy || !rationale.trim()}
          onClick={() => run('award-sourcing', async () => {
            const r = await API.awardSourcingEvent({
              event_id: Number(eventId),
              response_id: Number(responseId),
              rationale: rationale.trim(),
              agreement_title: (!hasAgreement && createDeal && agreementTitle.trim()) ? agreementTitle.trim() : null,
              agreement_id: (!hasAgreement && createDeal && agreementId.trim()) ? agreementId.trim() : null,
            });
            if (!r.ok) { onFailed(r.reason); return; }
            onFailed(null);
            onDone();
          })}
        >
          {busy ? 'awarding…' : '★ Confirm Award & Close Competition'}
        </ActButton>
        <button className="btn btn-quiet text-xs" onClick={onCancel}>cancel</button>
      </div>
    </div>
  );
}
