← back to Abrams Discovery Viewer

public/index.html

210 lines

<!doctype html>
<html lang="en">
<head>
    <link rel="icon" type="image/svg+xml" href="/favicon.svg">

<meta charset="utf-8">
<title>Abrams v. Pabo — Discovery Response Tool</title>
<style>
  * { box-sizing: border-box; }
  body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; margin: 0; height: 100vh; display: flex; flex-direction: column; background: #1a1a1a; color: #e8e8e8; }
  header { padding: 8px 14px; background: #0f0f0f; border-bottom: 1px solid #333; display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
  header h1 { font-size: 14px; margin: 0; color: #DDAB1E; font-weight: 700; }
  header .case { color: #888; font-size: 12px; }
  header select, header button { background: #2a2a2a; color: #e8e8e8; border: 1px solid #444; border-radius: 4px; padding: 4px 10px; font-size: 12px; cursor: pointer; }
  header button:hover { background: #3a3a3a; }
  header .stat { color: #4ade80; font-size: 12px; margin-left: auto; }
  main { flex: 1; display: flex; min-height: 0; }
  #pdf-pane { width: 50%; min-width: 320px; border-right: 1px solid #333; background: #0a0a0a; position: relative; }
  #pdf-pane iframe { width: 100%; height: 100%; border: 0; background: #fff; }
  #form-pane { flex: 1; overflow-y: auto; padding: 12px; }
  .row { background: #222; border: 1px solid #333; border-radius: 6px; padding: 12px; margin-bottom: 10px; }
  .row.complete { border-color: #4ade80; }
  .row.draft { border-color: #fbbf24; }
  .row .head { display: flex; gap: 10px; align-items: baseline; margin-bottom: 6px; }
  .row .item-no { color: #DDAB1E; font-weight: 700; font-size: 14px; min-width: 50px; }
  .row .src { background: #333; color: #aaa; padding: 1px 6px; border-radius: 3px; font-size: 10px; text-transform: uppercase; }
  .row .q { color: #ccc; font-size: 12px; line-height: 1.4; margin-bottom: 8px; }
  .row textarea { width: 100%; background: #1a1a1a; color: #e8e8e8; border: 1px solid #444; border-radius: 4px; padding: 8px; font-family: inherit; font-size: 13px; line-height: 1.4; resize: vertical; min-height: 60px; }
  .row textarea:focus { outline: none; border-color: #DDAB1E; }
  .row .meta { display: flex; gap: 10px; margin-top: 6px; align-items: center; }
  .row .meta input, .row .meta select { background: #1a1a1a; color: #e8e8e8; border: 1px solid #444; border-radius: 4px; padding: 4px 8px; font-size: 12px; }
  .row .meta input { flex: 1; }
  .row .saved { color: #4ade80; font-size: 11px; margin-left: auto; opacity: 0; transition: opacity 0.2s; }
  .row .saved.show { opacity: 1; }
  .section-header { color: #DDAB1E; font-size: 13px; font-weight: 700; margin: 14px 0 6px; padding-top: 6px; border-top: 1px solid #333; text-transform: uppercase; letter-spacing: 0.05em; }
  .filter-bar { display: flex; gap: 6px; margin-bottom: 10px; flex-wrap: wrap; }
  .filter-bar button { background: #2a2a2a; color: #aaa; border: 1px solid #444; padding: 4px 10px; border-radius: 4px; font-size: 11px; cursor: pointer; }
  .filter-bar button.active { background: #DDAB1E; color: #000; border-color: #DDAB1E; }
</style>
</head>
<body>
<header>
  <h1>Abrams v. Pabo</h1>
  <span class="case">Case 25CUB00758 · Kern County · Discovery Set One</span>
  <select id="view-select">
    <option value="abrams-fin">Steven — Form Interrogatories</option>
    <option value="abrams-rpd">Steven — Request for Production</option>
    <option value="dultz-fin">Natalia — Form Interrogatories</option>
    <option value="dultz-rpd">Natalia — Request for Production</option>
  </select>
  <button onclick="window.open('/api/csv','_blank')">Export CSV</button>
  <span class="stat" id="progress"></span>
</header>
<main>
  <div id="pdf-pane">
    <iframe id="pdf-frame" src="/pdfs/abrams-fin.pdf#view=FitH"></iframe>
  </div>
  <div id="form-pane">
    <div class="filter-bar">
      <button class="active" data-filter="all">All</button>
      <button data-filter="empty">Unanswered</button>
      <button data-filter="draft">Draft</button>
      <button data-filter="complete">Complete</button>
    </div>
    <div id="rows"></div>
  </div>
</main>
<script>
let bank = [];
let saved = {};
let currentView = 'abrams-fin';
let currentFilter = 'all';

async function init() {
  bank = await (await fetch('/api/bank')).json();
  saved = await (await fetch('/api/responses')).json();
  document.getElementById('view-select').addEventListener('change', e => {
    currentView = e.target.value;
    document.getElementById('pdf-frame').src = '/pdfs/' + currentView + '.pdf#view=FitH';
    render();
  });
  document.querySelectorAll('.filter-bar button').forEach(b => {
    b.addEventListener('click', () => {
      document.querySelectorAll('.filter-bar button').forEach(x => x.classList.remove('active'));
      b.classList.add('active');
      currentFilter = b.dataset.filter;
      render();
    });
  });
  render();
}

function rowsForView() {
  const [pCode, source] = currentView.split('-');
  return bank.filter(r => {
    const codeOk = (pCode === 'abrams' && r.plaintiff.startsWith('Steven')) || (pCode === 'dultz' && r.plaintiff.startsWith('Natalia'));
    const srcOk = (source === 'fin' && r.source === 'FIN') || (source === 'rpd' && r.source === 'RPD');
    return codeOk && srcOk;
  });
}

function passesFilter(r) {
  const v = saved[r.id] || {};
  const status = v.status || (v.response ? 'draft' : 'empty');
  if (currentFilter === 'all') return true;
  if (currentFilter === 'empty') return !v.response;
  if (currentFilter === 'draft') return v.response && status !== 'complete';
  if (currentFilter === 'complete') return status === 'complete';
  return true;
}

function render() {
  const rows = rowsForView();
  const container = document.getElementById('rows');
  let html = '';
  let lastSection = '';
  let answered = 0;
  rows.forEach(r => {
    const v = saved[r.id] || {};
    if (v.response) answered++;
    if (!passesFilter(r)) return;
    const sec = r.source === 'FIN' ? r.item.split('.')[0] + '.0' : 'Document Requests';
    if (sec !== lastSection) {
      html += `<div class="section-header">${escape(sectionLabel(r.source, sec))}</div>`;
      lastSection = sec;
    }
    const status = v.status || (v.response ? 'draft' : 'empty');
    const cls = status === 'complete' ? ' complete' : (v.response ? ' draft' : '');
    html += `
      <div class="row${cls}" data-id="${r.id}">
        <div class="head">
          <span class="item-no">${escape(r.item)}</span>
          <span class="src">${escape(r.source)}</span>
        </div>
        <div class="q">${escape(r.question)}</div>
        <textarea data-field="response" placeholder="Type your response...">${escape(v.response || '')}</textarea>
        <div class="meta">
          <input data-field="documents" placeholder="Documents to attach (file names, Bates #s)" value="${escape(v.documents || '')}">
          <select data-field="status">
            <option value="">— status —</option>
            <option value="draft" ${status==='draft'?'selected':''}>Draft</option>
            <option value="complete" ${status==='complete'?'selected':''}>Complete</option>
            <option value="objection" ${status==='objection'?'selected':''}>Objection</option>
            <option value="na" ${status==='na'?'selected':''}>N/A</option>
          </select>
          <input data-field="notes" placeholder="Notes / objections" value="${escape(v.notes || '')}">
          <span class="saved">saved</span>
        </div>
      </div>`;
  });
  container.innerHTML = html;
  document.getElementById('progress').textContent = `${answered} / ${rows.length} answered`;
  wireSave();
}

function sectionLabel(src, sec) {
  if (src === 'RPD') return 'Document Requests';
  const map = {
    '1.0':'1.0 Identity of Persons Answering','2.0':'2.0 General Background','4.0':'4.0 Insurance','6.0':'6.0 Physical/Mental/Emotional Injuries','7.0':'7.0 Property Damage','8.0':'8.0 Loss of Income','9.0':'9.0 Other Damages','10.0':'10.0 Medical History','11.0':'11.0 Other & Previous Claims','12.0':'12.0 Investigation — General','13.0':'13.0 Investigation — Surveillance','14.0':'14.0 Statutory/Regulatory Violations','15.0':'15.0 Denials and Affirmative Defenses','17.0':'17.0 Responses to RFAs','20.0':'20.0 How the Incident Occurred — Motor Vehicle'
  };
  return map[sec] || sec;
}

function escape(s) {
  return String(s||'').replace(/[&<>"']/g, c => ({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
}

let saveTimers = {};
function wireSave() {
  document.querySelectorAll('.row').forEach(rowEl => {
    const id = rowEl.dataset.id;
    rowEl.querySelectorAll('[data-field]').forEach(input => {
      input.addEventListener('input', () => scheduleSave(id, rowEl));
      input.addEventListener('change', () => scheduleSave(id, rowEl));
    });
  });
}

function scheduleSave(id, rowEl) {
  clearTimeout(saveTimers[id]);
  saveTimers[id] = setTimeout(() => doSave(id, rowEl), 400);
}

async function doSave(id, rowEl) {
  const payload = { id };
  rowEl.querySelectorAll('[data-field]').forEach(input => {
    payload[input.dataset.field] = input.value;
  });
  const r = await fetch('/api/responses', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify(payload) });
  const j = await r.json();
  if (j.ok) {
    saved[id] = j.row;
    const status = j.row.status || (j.row.response ? 'draft' : 'empty');
    rowEl.classList.remove('complete', 'draft');
    if (status === 'complete') rowEl.classList.add('complete');
    else if (j.row.response) rowEl.classList.add('draft');
    const sv = rowEl.querySelector('.saved');
    if (sv) { sv.classList.add('show'); setTimeout(() => sv.classList.remove('show'), 800); }
    // Update progress count
    const rows = rowsForView();
    let a = 0; rows.forEach(r => { if ((saved[r.id]||{}).response) a++; });
    document.getElementById('progress').textContent = `${a} / ${rows.length} answered`;
  }
}

init();
</script>
</body>
</html>