Add exports panel: versioned drafts, download, upload revisions, mark final

Export DOCX now saves to data/exports/{case_number}/ with auto-versioning
(טיוטה-v1, v2...). The case view UI shows all drafts with download buttons,
allows uploading revised versions (עריכה-v1...), and marking a version as
final (copies to training corpus for style learning).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-08 12:10:02 +00:00
parent b2f60d51f4
commit 5a8d5cac0a
4 changed files with 293 additions and 4 deletions

View File

@@ -217,6 +217,28 @@ header nav a:hover, header nav a.active { color: #fff; background: rgba(255,255,
.toast.error { background: #e94560; }
.toast.success { background: #27ae60; }
/* ── Exports / Drafts ─────────────────────────────────── */
.export-item {
display: flex; align-items: center; gap: 12px; padding: 10px 14px;
border: 1px solid #eee; border-radius: 8px; margin-bottom: 8px; background: #fafafa;
transition: background 0.15s;
}
.export-item:hover { background: #f0f0f0; }
.export-item .export-icon { font-size: 1.3em; flex-shrink: 0; }
.export-item .export-info { flex: 1; min-width: 0; }
.export-item .export-name { font-size: 0.88em; font-weight: 600; }
.export-item .export-meta { font-size: 0.75em; color: #999; margin-top: 2px; }
.export-item .export-actions { display: flex; gap: 6px; flex-shrink: 0; }
.export-item.final { border-color: #27ae60; background: #f0faf3; }
.export-item.final .export-icon { color: #27ae60; }
.export-upload-zone {
border: 2px dashed #ccc; border-radius: 8px; padding: 16px;
text-align: center; cursor: pointer; transition: border-color 0.2s, background 0.2s;
background: #fafafa; font-size: 0.85em; color: #888; margin-top: 8px;
}
.export-upload-zone:hover { border-color: #e94560; background: #fff5f7; }
.export-upload-zone input[type="file"] { display: none; }
.empty { text-align: center; color: #bbb; padding: 40px 20px; font-size: 0.88em; line-height: 1.6; }
@media (max-width: 800px) {
@@ -403,6 +425,21 @@ header nav a:hover, header nav a.active { color: #fff; background: rgba(255,255,
<div class="empty">אין מסמכים</div>
</div>
</div>
<!-- Exports / Drafts -->
<div class="card">
<div class="card-header">
<span>טיוטות וגרסאות</span>
<button class="btn btn-sm btn-primary" id="exportDocxBtn" style="margin-right:auto" onclick="triggerExport()">ייצא טיוטה חדשה</button>
</div>
<div class="card-body">
<div id="exportsList"><div class="empty">אין טיוטות עדיין</div></div>
<div class="export-upload-zone" id="exportUploadZone">
העלאת גרסה מעודכנת (DOCX)
<input type="file" id="exportFileInput" accept=".docx">
</div>
</div>
</div>
</div>
<!-- ══ Page: Legacy Upload ══ -->
@@ -753,6 +790,8 @@ async function loadCaseView(caseNumber) {
}
setupCaseUpload(caseNumber);
loadExports(caseNumber);
setupExportUpload(caseNumber);
}
async function openPaperclip(caseNumber) {
@@ -912,6 +951,121 @@ function trackCaseTask(taskId, displayName, container, caseNumber) {
es.onerror = () => es.close();
}
// ── Exports / Drafts ────────────────────────────────────
async function loadExports(caseNumber) {
const container = document.getElementById('exportsList');
try {
const res = await fetch(API + '/cases/' + encodeURIComponent(caseNumber) + '/exports');
const files = await res.json();
if (!files.length) {
container.innerHTML = '<div class="empty">אין טיוטות עדיין — לחץ "ייצא טיוטה חדשה" כדי ליצור</div>';
return;
}
container.innerHTML = files.map(f => {
const date = new Date(f.created_at * 1000);
const dateStr = date.toLocaleDateString('he-IL') + ' ' + date.toLocaleTimeString('he-IL', {hour:'2-digit',minute:'2-digit'});
const isFinal = f.is_final;
return `
<div class="export-item ${isFinal ? 'final' : ''}">
<span class="export-icon">${isFinal ? '&#9989;' : '&#128196;'}</span>
<div class="export-info">
<div class="export-name">${esc(f.filename)}</div>
<div class="export-meta">${dateStr} &middot; ${formatSize(f.size)}${isFinal ? ' &middot; <b>גרסה סופית</b>' : ''}</div>
</div>
<div class="export-actions">
<a class="btn btn-sm btn-secondary" href="${API}/cases/${encodeURIComponent(caseNumber)}/exports/${encodeURIComponent(f.filename)}/download" download="${esc(f.filename)}">הורד</a>
${!isFinal ? `<button class="btn btn-sm btn-success" onclick="markFinal('${esc(caseNumber)}','${esc(f.filename)}')">סמן סופי</button>` : ''}
</div>
</div>`;
}).join('');
} catch (e) {
container.innerHTML = '<div class="empty">שגיאה בטעינת טיוטות</div>';
}
}
async function triggerExport() {
if (!currentCaseNumber) return;
const btn = document.getElementById('exportDocxBtn');
btn.disabled = true;
btn.textContent = 'מייצא...';
try {
const res = await fetch(API + '/cases/' + encodeURIComponent(currentCaseNumber) + '/export-docx', { method: 'POST' });
if (!res.ok) {
const err = await res.json();
toast(err.detail || err.message || 'שגיאה בייצוא', 'error');
return;
}
const data = await res.json();
toast('טיוטה יוצאה: ' + (data.path || '').split('/').pop(), 'success');
loadExports(currentCaseNumber);
} catch (e) {
toast('שגיאת רשת', 'error');
} finally {
btn.disabled = false;
btn.textContent = 'ייצא טיוטה חדשה';
}
}
async function markFinal(caseNumber, filename) {
if (!confirm('לסמן את הגרסה כסופית? הקובץ יועתק גם לקורפוס האימון.')) return;
try {
const res = await fetch(API + '/cases/' + encodeURIComponent(caseNumber) + '/exports/' + encodeURIComponent(filename) + '/mark-final', { method: 'POST' });
if (!res.ok) {
const err = await res.json();
toast(err.detail || 'שגיאה', 'error');
return;
}
toast('הגרסה סומנה כסופית', 'success');
loadExports(caseNumber);
loadCaseView(caseNumber);
} catch (e) {
toast('שגיאת רשת', 'error');
}
}
function setupExportUpload(caseNumber) {
const zone = document.getElementById('exportUploadZone');
const fileInput = document.getElementById('exportFileInput');
const newZone = zone.cloneNode(true);
zone.parentNode.replaceChild(newZone, zone);
const newInput = newZone.querySelector('input[type="file"]');
newZone.addEventListener('click', () => newInput.click());
newZone.addEventListener('dragover', e => { e.preventDefault(); newZone.style.borderColor = '#e94560'; });
newZone.addEventListener('dragleave', () => { newZone.style.borderColor = '#ccc'; });
newZone.addEventListener('drop', e => {
e.preventDefault();
newZone.style.borderColor = '#ccc';
if (e.dataTransfer.files.length) uploadExportFile(e.dataTransfer.files[0], caseNumber);
});
newInput.addEventListener('change', () => {
if (newInput.files.length) uploadExportFile(newInput.files[0], caseNumber);
newInput.value = '';
});
}
async function uploadExportFile(file, caseNumber) {
const formData = new FormData();
formData.append('file', file);
try {
const res = await fetch(API + '/cases/' + encodeURIComponent(caseNumber) + '/exports/upload', {
method: 'POST', body: formData,
});
if (!res.ok) {
const err = await res.json();
toast(err.detail || 'שגיאה', 'error');
return;
}
const data = await res.json();
toast('גרסה הועלתה: ' + data.filename, 'success');
loadExports(caseNumber);
} catch (e) {
toast('שגיאת רשת', 'error');
}
}
// ── Legacy Upload Page ───────────────────────────────────
// (Simplified version of original upload functionality)
let legacyCases = [];