/* Private camp reporting. No scholarship endpoint, account role, or data is used. */
async function campReportRequest(endpoint) {
  const response = await fetch(`/admin/camp-interest-api/${endpoint}`, { credentials: 'same-origin', cache: 'no-store', headers: { Accept: 'application/json' } });
  const result = await response.json().catch(() => ({}));
  if (!response.ok || !result.success) {
    const error = new Error(response.status === 401 || response.status === 403
      ? 'Sign in with an approved chapter leadership email, then reload this page.'
      : 'Camp interest results could not be loaded. Please try again.');
    error.accessDenied = response.status === 401 || response.status === 403;
    throw error;
  }
  return result;
}

function campReportDownload(filename, content, contentType) {
  const url = URL.createObjectURL(new Blob([content], { type: contentType }));
  const link = document.createElement('a');
  link.href = url;
  link.download = filename;
  document.body.appendChild(link);
  link.click();
  link.remove();
  setTimeout(() => URL.revokeObjectURL(url), 1000);
}

function campReportCSVCell(value) {
  // Formula prefixes are neutralized even though summary values are normally numeric.
  const text = String(value ?? '');
  return `"${(/^[=+\-@\t\r]/.test(text) ? `'${text}` : text).replaceAll('"', '""')}"`;
}

function campReportTimestamp(value) {
  const date = new Date(value);
  return Number.isNaN(date.getTime()) ? 'Time unavailable' : new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'short' }).format(date);
}

function campReceiptKey(value) {
  return typeof value === 'string' ? value.replace(/[\s\u2010-\u2015-]/g, '').toUpperCase() : '';
}

function campFamilyLabel(family) {
  return family.parentName || [family.parentFirstName, family.parentLastName].filter(Boolean).join(' ') || family.parentEmail;
}

function campReceiptMatches(response, query) {
  const normalized = campReceiptKey(query);
  return !!normalized && campReceiptKey(response.receiptNumber).includes(normalized);
}

function campPhoneMatches(response, query) {
  if (!/^[+\d().\s-]+$/.test(query)) return false;
  const digits = query.replace(/\D/g, '');
  return digits.length >= 3 && String(response.parentPhone || '').replace(/\D/g, '').includes(digits);
}

function campSubmissionMatches(response, query) {
  if (!query) return true;
  if (campReceiptMatches(response, query) || campPhoneMatches(response, query)) return true;
  const text = [response.parentFirstName, response.parentLastName, response.parentName, response.parentEmail, response.parentPhone, response.receiptNumber].filter(Boolean).join(' ').toLocaleLowerCase();
  return query.toLocaleLowerCase().trim().split(/\s+/).every((term) => text.includes(term) || campReceiptMatches(response, term) || campPhoneMatches(response, term));
}

function CampFamilyCard({ family, year, searchFilter, historyOpen, onHistoryToggle }) {
  const previousResponses = Array.isArray(family.previousResponses) ? family.previousResponses : [];
  const currentMatch = !!searchFilter && campSubmissionMatches(family, searchFilter);
  const historicalMatch = !!searchFilter && previousResponses.some((previous) => campSubmissionMatches(previous, searchFilter));
  return <article className={`wk-camp-report-card wk-camp-family-card${currentMatch || historicalMatch ? ' wk-camp-family-match' : ''}`}>
    <h3>{campFamilyLabel(family)}</h3>
    <p className="wk-camp-family-receipt">Receipt: <strong className="wk-camp-receipt-code">{family.receiptNumber || 'Not available'}</strong>{currentMatch && <span className="wk-camp-receipt-match">{campReceiptMatches(family, searchFilter) ? 'Matched receipt' : 'Matched response'}</span>}</p>
    <p><a href={`mailto:${family.parentEmail}`}>{family.parentEmail}</a></p>
    <p>Phone: {family.parentPhone || 'Not provided'}</p>
    <p>Latest response received <time dateTime={family.receivedAt}>{campReportTimestamp(family.receivedAt)}</time></p>
    {historicalMatch && <p className="wk-camp-receipt-history-notice">Your search matches an earlier response. Only the latest family response counts.</p>}
    {Number.isInteger(family.revisionCount) && family.revisionCount > 1 && <p>Updated response · {family.revisionCount} responses received for this family email. Only this latest response counts.</p>}
    <p>Camp planning updates: <strong>{family.updatesOptIn ? 'Opted in' : 'Not opted in'}</strong></p>
    <ul>{family.campers.map((child, index) => <li key={index}>Potential camper {index + 1}: completed {child.grade}th grade by summer {year} · {child.interest === 'strong' ? 'Strong interest' : 'Possible interest'}</li>)}</ul>
    {previousResponses.length > 0 && <details className="wk-camp-family-history" open={historyOpen} onToggle={(event) => onHistoryToggle(family.id, event.currentTarget.open)}>
      <summary>Earlier responses ({previousResponses.length})</summary>
      <p>Earlier answers are retained for context and excluded from camper totals.</p>
      <ol>{previousResponses.map((previous) => {
        const matched = !!searchFilter && campSubmissionMatches(previous, searchFilter);
        return <li key={previous.id} className={matched ? 'wk-camp-earlier-receipt-match' : undefined}>
          <h4>Response {previous.revision} · <time dateTime={previous.receivedAt}>{campReportTimestamp(previous.receivedAt)}</time></h4>
          <p className="wk-camp-family-receipt">Receipt: <strong className="wk-camp-receipt-code">{previous.receiptNumber || 'Not available'}</strong>{matched && <span className="wk-camp-receipt-match">{campReceiptMatches(previous, searchFilter) ? 'Matched receipt' : 'Matched earlier response'}</span>}</p>
          <p>{campFamilyLabel(previous)} · <a href={`mailto:${previous.parentEmail}`}>{previous.parentEmail}</a></p>
          <p>Phone: {previous.parentPhone || 'Not provided'}</p>
          <p>Camp planning updates: {previous.updatesOptIn ? 'Opted in' : 'Not opted in'}</p>
          <ul>{previous.campers.map((child, index) => <li key={index}>Potential camper {index + 1}: completed {child.grade}th grade by summer {year} · {child.interest === 'strong' ? 'Strong interest' : 'Possible interest'}</li>)}</ul>
        </li>;
      })}</ol>
    </details>}
  </article>;
}

function CampFamilyDeleteDialog({ family, year, busy, error, refreshRequired, confirmed, onConfirm, onCancel, onDelete }) {
  const dialog = React.useRef(null);
  const alert = React.useRef(null);
  React.useEffect(() => {
    const element = dialog.current;
    element.showModal();
    return () => element.close();
  }, []);
  React.useEffect(() => { if (error) alert.current?.focus({ preventScroll: true }); }, [error]);
  return <dialog ref={dialog} className="wk-camp-delete-dialog" aria-labelledby="camp-delete-title" aria-describedby="camp-delete-scope" onCancel={(event) => { event.preventDefault(); if (!busy) onCancel(); }}>
    <h2 id="camp-delete-title">Delete this family’s camp responses?</h2>
    <p className="wk-camp-delete-family"><strong>{campFamilyLabel(family)}</strong><br />{family.parentEmail}</p>
    <p id="camp-delete-scope">This permanently deletes this family’s {year} camp interest and updates signup responses, saved contact information, and all earlier versions for this email. Their potential campers will be removed from the planning totals. This cannot be undone.</p>
    <label className="wk-camp-interest-checkbox"><input type="checkbox" checked={confirmed} disabled={busy || refreshRequired} onChange={(event) => onConfirm(event.target.checked)} />I understand this deletes all {year} camp interest responses for this family email.</label>
    {error && <p className="wk-camp-delete-error" role="alert" tabIndex={-1} ref={alert}>{error}</p>}
    {busy && <p role="status">Deleting family responses…</p>}
    <div className="wk-camp-delete-actions">
      <button className="wk-camp-interest-secondary" type="button" autoFocus disabled={busy} onClick={onCancel}>Cancel</button>
      <button className="wk-camp-interest-secondary wk-camp-delete-button" type="button" disabled={!confirmed || busy || refreshRequired} onClick={onDelete}>{busy ? 'Deleting responses…' : 'Delete responses'}</button>
    </div>
  </dialog>;
}

function CampInterestAdmin() {
  const [summary, setSummary] = React.useState(null);
  const [busy, setBusy] = React.useState(true);
  const [error, setError] = React.useState('');
  const [submissions, setSubmissions] = React.useState(null);
  const [submissionsAsOf, setSubmissionsAsOf] = React.useState('');
  const [familiesOpen, setFamiliesOpen] = React.useState(false);
  const [familiesBusy, setFamiliesBusy] = React.useState(false);
  const [familiesError, setFamiliesError] = React.useState('');
  const [searchQuery, setSearchQuery] = React.useState('');
  const [searchFilter, setSearchFilter] = React.useState('');
  const [updatesFilter, setUpdatesFilter] = React.useState('all');
  const [historyOpen, setHistoryOpen] = React.useState({});
  const [detailsOpen, setDetailsOpen] = React.useState({});
  const [detailsFocus, setDetailsFocus] = React.useState('');
  const [sort, setSort] = React.useState({ column: 'last', direction: 'ascending' });
  const [deleteTarget, setDeleteTarget] = React.useState(null);
  const [deleteConfirmed, setDeleteConfirmed] = React.useState(false);
  const [deleteBusy, setDeleteBusy] = React.useState(false);
  const [deleteError, setDeleteError] = React.useState('');
  const [deleteRefreshRequired, setDeleteRefreshRequired] = React.useState(false);
  const [actionNotice, setActionNotice] = React.useState('');
  const errorRef = React.useRef(null);
  const mounted = React.useRef(true);
  const requestVersion = React.useRef(0);
  const deletionInFlight = React.useRef(false);
  const loadSummary = React.useCallback(async (keepFamiliesOpen = false) => {
    if (deletionInFlight.current) return;
    const version = ++requestVersion.current;
    setBusy(true);
    setError('');
    setSummary(null);
    setSubmissions(null);
    setSubmissionsAsOf('');
    setFamiliesOpen(keepFamiliesOpen);
    setFamiliesBusy(keepFamiliesOpen);
    setFamiliesError('');
    setSearchQuery('');
    setSearchFilter('');
    setUpdatesFilter('all');
    setHistoryOpen({});
    setDetailsOpen({});
    setDetailsFocus('');
    setDeleteTarget(null);
    setDeleteConfirmed(false);
    setDeleteError('');
    setDeleteRefreshRequired(false);
    const requests = [campReportRequest('summary')];
    if (keepFamiliesOpen) requests.push(campReportRequest('submissions'));
    const [summaryResult, familyResult] = await Promise.allSettled(requests);
    if (!mounted.current || requestVersion.current !== version) return;
    if (summaryResult.status === 'fulfilled') setSummary(summaryResult.value);
    else setError(summaryResult.reason.message);
    if (familyResult?.status === 'fulfilled' && !(summaryResult.status === 'rejected' && summaryResult.reason.accessDenied)) {
      setSubmissions(familyResult.value.submissions);
      setSubmissionsAsOf(familyResult.value.asOf);
    } else if (familyResult?.status === 'rejected') {
      setFamiliesError(familyResult.reason.message);
      if (familyResult.reason.accessDenied) { setSummary(null); setError(familyResult.reason.message); }
    }
    setBusy(false);
    setFamiliesBusy(false);
  }, []);
  React.useEffect(() => {
    mounted.current = true;
    loadSummary();
    const restored = (event) => { if (event.persisted) window.location.reload(); };
    window.addEventListener('pageshow', restored);
    return () => { mounted.current = false; requestVersion.current++; window.removeEventListener('pageshow', restored); };
  }, [loadSummary]);
  React.useEffect(() => { if (error) errorRef.current?.focus({ preventScroll: true }); }, [error]);
  React.useEffect(() => { if (detailsFocus) document.getElementById(`camp-family-details-${detailsFocus}`)?.focus(); }, [detailsFocus]);

  const viewFamilies = async () => {
    if (deletionInFlight.current) return;
    if (familiesOpen) {
      requestVersion.current++;
      setFamiliesOpen(false);
      setSubmissions(null);
      setSubmissionsAsOf('');
      setFamiliesError('');
      setSearchQuery('');
      setSearchFilter('');
      setUpdatesFilter('all');
      setHistoryOpen({});
      setDetailsOpen({});
      setDetailsFocus('');
      return;
    }
    const version = requestVersion.current;
    setFamiliesOpen(true);
    setFamiliesBusy(true);
    setFamiliesError('');
    try {
      const result = await campReportRequest('submissions');
      if (!mounted.current || requestVersion.current !== version) return;
      setSubmissions(result.submissions);
      setSubmissionsAsOf(result.asOf);
    } catch (failure) {
      if (!mounted.current || requestVersion.current !== version) return;
      setSubmissions(null);
      setFamiliesError(failure.message);
      if (failure.accessDenied) { setSummary(null); setError(failure.message); setSearchQuery(''); setSearchFilter(''); setUpdatesFilter('all'); setHistoryOpen({}); setDetailsOpen({}); setDetailsFocus(''); }
    } finally { if (mounted.current && requestVersion.current === version) setFamiliesBusy(false); }
  };
  const downloadSummary = () => {
    if (!summary) return;
    const rows = [
      ['Camp year', summary.year], ['As of', summary.asOf],
      ['Households', summary.households], ['Potential campers', summary.totalCampers],
      ['Strong interest', summary.strong], ['Possible interest', summary.possible], [],
      ['Grade completed by summer ' + summary.year, 'Strong interest', 'Possible interest', 'Total'],
      ...summary.grades.map((row) => [row.grade, row.strong, row.possible, row.total]), [],
      ['Interest estimates only. No registrations or reserved places.'],
      ['Only the most recent response for each parent/guardian email counts.'],
    ];
    campReportDownload(`camp-interest-${summary.year}-summary.csv`, '\uFEFF' + rows.map((row) => row.map(campReportCSVCell).join(',')).join('\r\n') + '\r\n', 'text/csv;charset=utf-8');
  };
  const copyLink = async () => {
    try {
      await navigator.clipboard.writeText(new URL('/admin/camp-interest', window.location.origin).href);
      setActionNotice('Results link copied. Only approved chapter leadership can open it.');
    } catch { setActionNotice('The link could not be copied. Share this page’s address with approved chapter leadership.'); }
  };
  const year = summary?.year || window.WKYHB.camp?.year || 2028;
  const filteredSubmissions = (submissions?.filter((family) => (updatesFilter === 'all' || (updatesFilter === 'updates' ? family.updatesOptIn : !family.updatesOptIn)) && (!searchFilter || campSubmissionMatches(family, searchFilter) || family.previousResponses?.some((previous) => campSubmissionMatches(previous, searchFilter)))) || []).sort((left, right) => {
    const value = (family) => sort.column === 'first' ? family.parentFirstName || (!family.parentLastName ? family.parentName : '') || '' : sort.column === 'last' ? family.parentLastName || '' : sort.column === 'email' ? family.parentEmail : family.receivedAt;
    const compare = (a, b) => String(a || '').localeCompare(String(b || ''), undefined, { sensitivity: 'base', numeric: true });
    const primary = compare(value(left), value(right));
    const tie = compare(left.parentLastName, right.parentLastName) || compare(left.parentFirstName || left.parentName, right.parentFirstName || right.parentName) || compare(left.parentEmail, right.parentEmail);
    return (primary || tie) * (sort.direction === 'ascending' ? 1 : -1);
  });
  const clearSearch = () => {
    setSearchQuery('');
    setSearchFilter('');
    setHistoryOpen({});
    setDetailsOpen({});
    setDetailsFocus('');
    document.getElementById('camp-family-query')?.focus();
  };
  const searchFamilies = (event) => {
    event.preventDefault();
    const query = searchQuery.trim();
    setSearchFilter(query);
    const historicalFamilies = (submissions || []).filter((family) => query && family.previousResponses?.some((previous) => campSubmissionMatches(previous, query)));
    setHistoryOpen((current) => {
      const next = { ...current };
      historicalFamilies.forEach((family) => { next[family.id] = true; });
      return next;
    });
    setDetailsOpen((current) => {
      const next = { ...current };
      historicalFamilies.forEach((family) => { next[family.id] = true; });
      if (query) (submissions || []).forEach((family) => { if (campReceiptMatches(family, query)) next[family.id] = true; });
      return next;
    });
  };
  const sortFamilies = (column) => setSort((current) => ({ column, direction: current.column === column && current.direction === 'ascending' ? 'descending' : 'ascending' }));
  const toggleDetails = (family) => {
    const opening = !detailsOpen[family.id];
    setDetailsOpen((current) => ({ ...current, [family.id]: opening }));
    setDetailsFocus(opening ? family.id : '');
  };
  const sortHeader = (column, label) => <th scope="col" aria-sort={sort.column === column ? sort.direction : 'none'}><button type="button" className="wk-camp-family-sort" onClick={() => sortFamilies(column)}>{label}<span aria-hidden="true">{sort.column === column ? sort.direction === 'ascending' ? ' ↑' : ' ↓' : ' ↕'}</span></button></th>;
  const startDelete = (family) => {
    setDeleteTarget(family);
    setDeleteConfirmed(false);
    setDeleteError('');
    setDeleteRefreshRequired(false);
  };
  const cancelDelete = () => {
    if (deletionInFlight.current) return;
    setDeleteTarget(null);
    setDeleteConfirmed(false);
    setDeleteError('');
    setDeleteRefreshRequired(false);
  };
  const deleteFamily = async () => {
    if (!deleteTarget || !deleteConfirmed || deleteRefreshRequired || deletionInFlight.current) return;
    const target = deleteTarget;
    const version = requestVersion.current;
    deletionInFlight.current = true;
    setDeleteBusy(true);
    setDeleteError('');
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), 30000);
    try {
      const response = await fetch(`/admin/camp-interest-api/submissions/${encodeURIComponent(target.id)}`, { method: 'DELETE', credentials: 'same-origin', cache: 'no-store', signal: controller.signal, headers: { Accept: 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify({ year, parentEmail: target.parentEmail, confirmation: 'delete-family' }) });
      if (!mounted.current || requestVersion.current !== version) return;
      if (response.status === 401 || response.status === 403) {
        requestVersion.current++;
        setSummary(null); setSubmissions(null); setSubmissionsAsOf(''); setFamiliesOpen(false); setFamiliesError('');
        setSearchQuery(''); setSearchFilter(''); setUpdatesFilter('all'); setHistoryOpen({}); setDetailsOpen({}); setDetailsFocus(''); setDeleteTarget(null); setDeleteConfirmed(false);
        setError('Sign in with an approved chapter leadership email, then reload this page.');
        return;
      }
      const result = await response.json().catch(() => null);
      if (!mounted.current || requestVersion.current !== version) return;
      if (response.ok && !result?.success) {
        setDeleteRefreshRequired(true);
        setDeleteError('Deletion could not be confirmed. Cancel and refresh results before trying again.');
        return;
      }
      if (!response.ok) {
        if (response.status === 409) { setDeleteRefreshRequired(true); setDeleteError('A newer response has arrived for this family. Cancel and refresh results before deleting.'); }
        else if (response.status === 404) { setDeleteRefreshRequired(true); setDeleteError('These family responses are no longer available. Cancel and refresh results.'); }
        else setDeleteError('Family responses could not be deleted. Please try again.');
        return;
      }
      deletionInFlight.current = false;
      setDeleteBusy(false);
      setDeleteTarget(null);
      setActionNotice('Family responses deleted.');
      await loadSummary(true);
    } catch {
      if (mounted.current && requestVersion.current === version) {
        setDeleteRefreshRequired(true);
        setDeleteError('Deletion could not be confirmed. Cancel and refresh results before trying again.');
      }
    } finally {
      clearTimeout(timeout);
      deletionInFlight.current = false;
      if (mounted.current) setDeleteBusy(false);
    }
  };
  return <section className="wk-camp-report wk-container" aria-labelledby="camp-report-title">
    <div className="wk-leadership-return"><a href="/admin/leadership">← Leadership hub</a><a href="/cdn-cgi/access/logout" onClick={() => { setSummary(null); setSubmissions(null); }}>Sign out</a></div>
    <header className="wk-camp-report-header">
      <p className="wk-camp-kicker">Chapter leadership</p>
      <h1 id="camp-report-title">{year} Camp Interest</h1>
      <p>Private planning results for chapter officers, board members, and camp directors.</p>
      <p>These are expressions of interest. Dates, location, and cost are still undecided; counts do not represent registrations or reserved places.</p>
      <p>Only the most recent response for each parent/guardian email counts toward these totals.</p>
    </header>
    {busy && <p role="status">Loading camp interest results…</p>}
    {error && <div className="wk-camp-interest-notice" role="alert" ref={errorRef} tabIndex={-1}><p>{error}</p><p><a href="/leadership">Leadership login</a></p><button className="wk-camp-interest-secondary" type="button" onClick={() => loadSummary()}>Reload results</button></div>}
    {summary && <React.Fragment>
      <p className="wk-camp-report-asof">As of <time dateTime={summary.asOf}>{campReportTimestamp(summary.asOf)}</time></p>
      <dl className="wk-camp-report-stats">
        {[['Households', summary.households], ['Potential campers', summary.totalCampers], ['Strong interest', summary.strong], ['Possible interest', summary.possible]].map(([label, count]) => <div key={label}><dt>{label}</dt><dd>{count}</dd></div>)}
      </dl>
      <div className="wk-camp-report-card">
        <h2>Potential campers by grade</h2>
        <p>Grade each child will have completed by summer {year}.</p>
        <div className="wk-camp-report-table-wrap"><table className="wk-camp-report-table"><caption className="wk-camp-interest-sr">Camper interest by grade completed in summer {year}</caption>
          <thead><tr><th scope="col">Grade</th><th scope="col">Strong interest</th><th scope="col">Possible interest</th><th scope="col">Total</th></tr></thead>
          <tbody>{summary.grades.map((row) => <tr key={row.grade}><th scope="row">{row.grade}th</th><td>{row.strong}</td><td>{row.possible}</td><td>{row.total}</td></tr>)}</tbody>
          <tfoot><tr><th scope="row">Total</th><td>{summary.strong}</td><td>{summary.possible}</td><td>{summary.totalCampers}</td></tr></tfoot>
        </table></div>
      </div>
      <div className="wk-camp-report-actions">
        <button className="wk-camp-interest-secondary" type="button" onClick={downloadSummary}>Download summary CSV</button>
        <button className="wk-camp-interest-secondary" type="button" onClick={() => window.print()}>Print / save as PDF</button>
        <button className="wk-camp-interest-secondary" type="button" onClick={copyLink}>Copy results link</button>
        <button className="wk-camp-interest-text-button" type="button" disabled={deleteBusy} onClick={() => loadSummary()}>Refresh results</button>
        <p className="wk-camp-interest-help">Exports and printouts contain counts only. Share them within chapter leadership.</p>
        {actionNotice && <p role="status">{actionNotice}</p>}
      </div>
      <section className="wk-camp-family-list" aria-label="Family submissions">
        <button className="wk-camp-interest-secondary" type="button" disabled={familiesBusy || deleteBusy} onClick={viewFamilies} aria-expanded={familiesOpen} aria-controls="camp-family-submissions">{familiesOpen ? 'Hide family submissions' : 'View family submissions'}</button>
        {familiesOpen && <div id="camp-family-submissions">
          <h2>Family submissions</h2>
          <p>Showing each family’s most recent response. Contact details are for chapter leadership only. Updates consent is shown separately for each household.</p>
          {familiesBusy && <p role="status">Loading family submissions…</p>}
          {familiesError && <p role="alert">{familiesError}</p>}
          {submissions && <React.Fragment>
            <p className="wk-camp-report-asof">As of <time dateTime={submissionsAsOf}>{campReportTimestamp(submissionsAsOf)}</time></p>
            <form className="wk-camp-receipt-lookup" role="search" aria-label="Search family submissions" onSubmit={searchFamilies}>
              <div className="wk-camp-interest-field">
                <label htmlFor="camp-family-query">Search family submissions</label>
                <input id="camp-family-query" type="search" autoComplete="off" spellCheck={false} maxLength={200} placeholder="Name, email, phone, or receipt" value={searchQuery} onChange={(event) => setSearchQuery(event.target.value)} aria-describedby="camp-family-search-help" />
                <p className="wk-camp-interest-help" id="camp-family-search-help">Search names, email addresses, phone numbers, or receipts in current and earlier responses. Use part of a name or email, or several words together. Receipt searches ignore capitalization, spaces, and hyphens; phone searches also match unformatted numbers.</p>
              </div>
              <div className="wk-camp-receipt-lookup-actions">
                <button className="wk-camp-interest-secondary" type="submit" disabled={!searchQuery.trim()}>Search families</button>
                <button className="wk-camp-interest-text-button" type="button" disabled={!searchQuery && !searchFilter} onClick={clearSearch}>Clear search</button>
              </div>
            </form>
            <div className="wk-camp-interest-field wk-camp-family-updates-filter">
              <label htmlFor="camp-family-updates">Updates signup</label>
              <select id="camp-family-updates" value={updatesFilter} onChange={(event) => setUpdatesFilter(event.target.value)} aria-describedby="camp-family-updates-help"><option value="all">All families</option><option value="updates">Signed up for updates</option><option value="interest">Interest only</option></select>
              <p id="camp-family-updates-help" className="wk-camp-interest-help">This filter uses each family’s latest updates preference and does not change the planning totals.</p>
            </div>
            {(searchFilter || updatesFilter !== 'all') && <p role="status">{filteredSubmissions.length ? `Showing ${filteredSubmissions.length} matching ${filteredSubmissions.length === 1 ? 'family' : 'families'}.` : searchFilter ? 'No family responses match your search.' : 'No family responses match this updates filter.'}</p>}
            {!submissions.length && <p>No family responses have been received yet.</p>}
            {filteredSubmissions.length > 0 && <React.Fragment>
              <p className="wk-camp-interest-help wk-camp-family-table-help" id="camp-family-table-help">Select a name, email, or received column to sort. On smaller screens, scroll the table sideways. View details for receipts, camper grades, and earlier responses. Names from the earlier form are shown as full names because separate first and last names were not collected.</p>
              <div className="wk-camp-report-table-wrap wk-camp-family-table-wrap" role="region" aria-label="Family submissions table" tabIndex={0} aria-describedby="camp-family-table-help">
                <table className="wk-camp-report-table wk-camp-family-table">
                  <caption className="wk-camp-interest-sr">Latest family submissions</caption>
                  <thead><tr>{sortHeader('first', 'First name')}{sortHeader('last', 'Last name')}{sortHeader('email', 'Email')}<th scope="col">Phone</th><th scope="col">Potential campers</th>{sortHeader('received', 'Received')}<th scope="col">Updates</th><th scope="col">Actions</th></tr></thead>
                  <tbody>{filteredSubmissions.map((family) => {
                    const label = campFamilyLabel(family);
                    const legacyName = !family.parentFirstName && !family.parentLastName;
                    return <tr key={family.id}>
                      <td>{legacyName ? family.parentName || '—' : family.parentFirstName || '—'}{legacyName && family.parentName && <span className="wk-camp-family-legacy-name">Full name (earlier form)</span>}</td>
                      <td>{family.parentLastName || '—'}</td>
                      <td><a href={`mailto:${family.parentEmail}`}>{family.parentEmail}</a></td>
                      <td>{family.parentPhone || '—'}</td>
                      <td>{family.campers.length}</td>
                      <td><time dateTime={family.receivedAt}>{campReportTimestamp(family.receivedAt)}</time></td>
                      <td>{family.updatesOptIn ? 'Opted in' : 'Not opted in'}</td>
                      <td><div className="wk-camp-family-row-actions">
                        <button type="button" className="wk-camp-interest-secondary" aria-expanded={!!detailsOpen[family.id]} aria-controls={`camp-family-details-${family.id}`} aria-label={`${detailsOpen[family.id] ? 'Hide' : 'View'} details for ${label}`} onClick={() => toggleDetails(family)}>{detailsOpen[family.id] ? 'Hide details' : 'View details'}</button>
                        <button type="button" className="wk-camp-interest-text-button" aria-label={`Delete responses for ${label}`} onClick={() => startDelete(family)}>Delete responses</button>
                      </div></td>
                    </tr>;
                  })}</tbody>
                </table>
              </div>
              {filteredSubmissions.map((family) => <div key={family.id} id={`camp-family-details-${family.id}`} role="region" tabIndex={-1} hidden={!detailsOpen[family.id]} aria-label={`Response details for ${campFamilyLabel(family)}`}>{detailsOpen[family.id] && <CampFamilyCard family={family} year={year} searchFilter={searchFilter} historyOpen={!!historyOpen[family.id]} onHistoryToggle={(id, value) => setHistoryOpen((current) => current[id] === value ? current : { ...current, [id]: value })} />}</div>)}
            </React.Fragment>}
          </React.Fragment>}
        </div>}
        {deleteTarget && <CampFamilyDeleteDialog family={deleteTarget} year={year} busy={deleteBusy} error={deleteError} refreshRequired={deleteRefreshRequired} confirmed={deleteConfirmed} onConfirm={setDeleteConfirmed} onCancel={cancelDelete} onDelete={deleteFamily} />}
      </section>
    </React.Fragment>}
  </section>;
}

window.CampInterestAdmin = CampInterestAdmin;
