← 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 => ({ '&':'&','<':'<','>':'>','"':'"',"'":'''}[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>