- POST /api/admin/skills/{slug}/sync — read SKILL.md from disk, insert/update DB
- DELETE /api/admin/skills/{slug} — remove skill from DB (keeps disk files)
- UI: Sync/Re-sync and Delete buttons per skill in the skills list
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1460 lines
60 KiB
HTML
1460 lines
60 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="he" dir="rtl">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>עוזר משפטי — ניהול תיקים</title>
|
||
<style>
|
||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||
background: #f5f5f5;
|
||
color: #1a1a2e;
|
||
direction: rtl;
|
||
min-height: 100vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
/* Header */
|
||
header {
|
||
background: #1a1a2e;
|
||
color: #ffffff;
|
||
padding: 14px 32px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
header h1 { font-size: 1.25em; font-weight: 600; cursor: pointer; }
|
||
header .sep { width: 2px; height: 20px; background: rgba(255,255,255,0.2); border-radius: 1px; }
|
||
header .subtitle { font-size: 0.85em; opacity: 0.6; }
|
||
header nav { margin-right: auto; display: flex; gap: 8px; }
|
||
header nav a {
|
||
color: rgba(255,255,255,0.7); text-decoration: none; font-size: 0.82em;
|
||
padding: 4px 12px; border-radius: 4px; transition: all 0.15s;
|
||
}
|
||
header nav a:hover, header nav a.active { color: #fff; background: rgba(255,255,255,0.1); }
|
||
|
||
/* Main */
|
||
.main { flex: 1; max-width: 1200px; width: 100%; margin: 0 auto; padding: 28px 24px; }
|
||
.page { display: none; }
|
||
.page.active { display: block; }
|
||
|
||
/* Cards */
|
||
.card {
|
||
background: #ffffff; border-radius: 10px;
|
||
box-shadow: 0 1px 4px rgba(0,0,0,0.06), 0 0 1px rgba(0,0,0,0.08);
|
||
overflow: hidden; margin-bottom: 16px;
|
||
}
|
||
.card-header {
|
||
padding: 14px 20px; font-size: 0.9em; font-weight: 600; color: #1a1a2e;
|
||
border-bottom: 1px solid #eee; display: flex; align-items: center; gap: 8px;
|
||
}
|
||
.card-body { padding: 20px; }
|
||
|
||
/* Buttons */
|
||
.btn {
|
||
padding: 8px 20px; border: none; border-radius: 6px; font-size: 0.88em;
|
||
font-family: inherit; cursor: pointer; transition: all 0.15s; font-weight: 500;
|
||
}
|
||
.btn-primary { background: #e94560; color: #fff; }
|
||
.btn-primary:hover { background: #d6304a; }
|
||
.btn-secondary { background: #eee; color: #1a1a2e; }
|
||
.btn-secondary:hover { background: #ddd; }
|
||
.btn-outline { background: transparent; color: #e94560; border: 1px solid #e94560; }
|
||
.btn-outline:hover { background: #fff0f3; }
|
||
.btn-sm { padding: 4px 12px; font-size: 0.78em; }
|
||
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
||
.btn-success { background: #27ae60; color: #fff; }
|
||
|
||
/* Form Elements */
|
||
.form-row { display: flex; gap: 12px; flex-wrap: wrap; align-items: end; margin-bottom: 14px; }
|
||
.form-group { flex: 1; min-width: 160px; }
|
||
.form-group label { display: block; font-size: 0.78em; color: #888; margin-bottom: 4px; font-weight: 500; }
|
||
.form-group select, .form-group input[type="text"], .form-group input[type="date"], .form-group textarea {
|
||
width: 100%; padding: 8px 10px; border: 1px solid #ddd; border-radius: 6px;
|
||
font-size: 0.88em; font-family: inherit; direction: rtl; background: #fff; transition: border-color 0.15s;
|
||
}
|
||
.form-group textarea { resize: vertical; min-height: 60px; }
|
||
.form-group select:focus, .form-group input:focus, .form-group textarea:focus { outline: none; border-color: #e94560; }
|
||
|
||
/* ── Case List ─────────────────────────────────────────── */
|
||
.case-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 16px; }
|
||
.case-card {
|
||
background: #fff; border-radius: 10px; padding: 20px; cursor: pointer;
|
||
box-shadow: 0 1px 4px rgba(0,0,0,0.06); transition: box-shadow 0.2s, transform 0.15s;
|
||
border-right: 4px solid #e94560;
|
||
}
|
||
.case-card:hover { box-shadow: 0 4px 16px rgba(0,0,0,0.1); transform: translateY(-2px); }
|
||
.case-card .case-number { font-size: 1.1em; font-weight: 700; color: #e94560; }
|
||
.case-card .case-title { font-size: 0.9em; color: #555; margin: 6px 0; line-height: 1.4; }
|
||
.case-card .case-meta { font-size: 0.78em; color: #999; display: flex; gap: 16px; flex-wrap: wrap; }
|
||
.case-card .badge {
|
||
display: inline-block; padding: 2px 8px; border-radius: 4px;
|
||
font-size: 0.72em; font-weight: 600; background: #f0f0f0; color: #666;
|
||
}
|
||
.case-card .badge.new { background: #e3f2fd; color: #1976d2; }
|
||
.case-card .badge.in_progress { background: #fff3e0; color: #f57c00; }
|
||
.case-card .badge.drafted { background: #e8f5e9; color: #388e3c; }
|
||
|
||
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||
.page-header h2 { font-size: 1.2em; font-weight: 600; }
|
||
|
||
/* ── Wizard ────────────────────────────────────────────── */
|
||
.wizard-steps {
|
||
display: flex; gap: 0; margin-bottom: 24px;
|
||
background: #fff; border-radius: 10px; overflow: hidden;
|
||
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
|
||
}
|
||
.wizard-step {
|
||
flex: 1; padding: 12px 16px; text-align: center; font-size: 0.82em; color: #999;
|
||
border-bottom: 3px solid transparent; transition: all 0.2s;
|
||
}
|
||
.wizard-step.active { color: #e94560; border-bottom-color: #e94560; font-weight: 600; }
|
||
.wizard-step.done { color: #27ae60; border-bottom-color: #27ae60; }
|
||
.wizard-panel { display: none; }
|
||
.wizard-panel.active { display: block; }
|
||
.wizard-nav { display: flex; justify-content: space-between; margin-top: 20px; }
|
||
|
||
/* Dynamic list (appellants/respondents) */
|
||
.dynamic-list { margin-bottom: 12px; }
|
||
.dynamic-list .item { display: flex; gap: 8px; align-items: center; margin-bottom: 6px; }
|
||
.dynamic-list .item input { flex: 1; }
|
||
.dynamic-list .remove-btn {
|
||
background: none; border: none; color: #e94560; cursor: pointer; font-size: 1.1em; padding: 4px;
|
||
}
|
||
|
||
/* ── Case View ─────────────────────────────────────────── */
|
||
.case-header-bar {
|
||
background: #fff; border-radius: 10px; padding: 20px; margin-bottom: 16px;
|
||
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
|
||
display: flex; justify-content: space-between; align-items: start; gap: 20px;
|
||
}
|
||
.case-header-bar .info h2 { font-size: 1.15em; margin-bottom: 6px; }
|
||
.case-header-bar .info .meta { font-size: 0.82em; color: #888; display: flex; gap: 16px; flex-wrap: wrap; }
|
||
.case-header-bar .links { display: flex; gap: 8px; flex-shrink: 0; }
|
||
.case-header-bar .links a {
|
||
font-size: 0.78em; padding: 4px 10px; border-radius: 4px;
|
||
text-decoration: none; border: 1px solid #ddd; color: #555; transition: all 0.15s;
|
||
}
|
||
.case-header-bar .links a:hover { border-color: #e94560; color: #e94560; }
|
||
|
||
.doc-group { margin-bottom: 16px; }
|
||
.doc-group-header {
|
||
font-size: 0.85em; font-weight: 600; color: #555; padding: 8px 0;
|
||
border-bottom: 1px solid #eee; margin-bottom: 8px;
|
||
display: flex; justify-content: space-between;
|
||
}
|
||
.doc-group-header .count { font-weight: 400; color: #999; }
|
||
.doc-item {
|
||
display: flex; align-items: center; gap: 10px; padding: 8px 12px;
|
||
border-radius: 6px; transition: background 0.15s; font-size: 0.85em;
|
||
}
|
||
.doc-item:hover { background: #f8f8f8; }
|
||
.doc-item .doc-icon { color: #999; }
|
||
.doc-item .doc-name { flex: 1; }
|
||
.doc-item .doc-status { font-size: 0.75em; color: #999; }
|
||
|
||
/* Upload zone (reusable) */
|
||
.upload-zone {
|
||
border: 2px dashed #ccc; border-radius: 10px; padding: 40px 24px;
|
||
text-align: center; cursor: pointer; transition: border-color 0.2s, background 0.2s; background: #fafafa;
|
||
}
|
||
.upload-zone:hover, .upload-zone.dragover { border-color: #e94560; background: #fff5f7; }
|
||
.upload-zone h3 { font-size: 0.95em; font-weight: 500; color: #555; margin-bottom: 4px; }
|
||
.upload-zone p { font-size: 0.78em; color: #aaa; }
|
||
.upload-zone input[type="file"] { display: none; }
|
||
|
||
/* Pending upload items in case view */
|
||
.pending-upload {
|
||
border: 1px solid #eee; border-radius: 8px; padding: 14px; margin-bottom: 10px; background: #fafafa;
|
||
}
|
||
.pending-upload .file-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
|
||
.pending-upload .file-name { font-weight: 600; font-size: 0.88em; word-break: break-all; }
|
||
.pending-upload .tag-row { display: flex; gap: 10px; align-items: end; flex-wrap: wrap; }
|
||
.pending-upload .tag-row .form-group { min-width: 120px; }
|
||
|
||
/* Processing tasks */
|
||
.task-item {
|
||
display: flex; align-items: center; gap: 12px; padding: 12px;
|
||
border: 1px solid #eee; border-radius: 8px; margin-bottom: 8px; background: #fafafa;
|
||
}
|
||
.task-item .spinner {
|
||
width: 20px; height: 20px; border: 2.5px solid #eee; border-top-color: #e94560;
|
||
border-radius: 50%; animation: spin 0.8s linear infinite; flex-shrink: 0;
|
||
}
|
||
.task-item.done .spinner { border-color: #27ae60; border-top-color: #27ae60; animation: none; }
|
||
.task-item.failed .spinner { border-color: #e94560; border-top-color: #e94560; animation: none; }
|
||
@keyframes spin { to { transform: rotate(360deg); } }
|
||
.task-info { flex: 1; min-width: 0; }
|
||
.task-info .task-name { font-size: 0.85em; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||
.task-info .task-status { font-size: 0.75em; color: #999; margin-top: 2px; }
|
||
|
||
/* Creation progress in wizard */
|
||
.creation-steps { list-style: none; }
|
||
.creation-steps li { padding: 8px 0; font-size: 0.88em; display: flex; align-items: center; gap: 8px; }
|
||
.creation-steps li .step-icon { width: 20px; text-align: center; }
|
||
.creation-steps li.pending .step-icon { color: #ccc; }
|
||
.creation-steps li.running .step-icon { color: #e94560; }
|
||
.creation-steps li.done .step-icon { color: #27ae60; }
|
||
.creation-steps li.error .step-icon { color: #e94560; }
|
||
|
||
/* Status Bar */
|
||
.status-bar {
|
||
background: #1a1a2e; color: rgba(255,255,255,0.5); font-size: 0.75em;
|
||
padding: 8px 32px; display: flex; gap: 24px; align-items: center;
|
||
}
|
||
.status-bar .stat { display: flex; align-items: center; gap: 6px; }
|
||
.status-bar .stat-value { color: rgba(255,255,255,0.85); font-weight: 600; }
|
||
|
||
/* Toast */
|
||
.toast {
|
||
position: fixed; bottom: 60px; left: 50%; transform: translateX(-50%) translateY(10px);
|
||
background: #1a1a2e; color: white; padding: 10px 24px; border-radius: 8px;
|
||
font-size: 0.85em; z-index: 1000; opacity: 0; transition: opacity 0.3s, transform 0.3s; pointer-events: none;
|
||
}
|
||
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
|
||
.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; }
|
||
|
||
/* ── Skills Management ───────────────────────────────── */
|
||
.skill-list { display: flex; flex-direction: column; gap: 10px; }
|
||
.skill-item {
|
||
display: flex; align-items: center; gap: 14px; padding: 14px 18px;
|
||
border: 1px solid #eee; border-radius: 8px; background: #fafafa;
|
||
transition: background 0.15s;
|
||
}
|
||
.skill-item:hover { background: #f0f0f0; }
|
||
.skill-item .skill-icon { font-size: 1.5em; flex-shrink: 0; }
|
||
.skill-item .skill-info { flex: 1; min-width: 0; }
|
||
.skill-item .skill-name { font-size: 0.95em; font-weight: 600; }
|
||
.skill-item .skill-meta { font-size: 0.75em; color: #999; margin-top: 3px; display: flex; gap: 14px; flex-wrap: wrap; }
|
||
.skill-item .skill-badges { display: flex; gap: 6px; flex-shrink: 0; }
|
||
.skill-item .badge-ok { background: #e8f5e9; color: #388e3c; padding: 2px 8px; border-radius: 4px; font-size: 0.72em; font-weight: 600; }
|
||
.skill-item .badge-warn { background: #fff3e0; color: #f57c00; padding: 2px 8px; border-radius: 4px; font-size: 0.72em; font-weight: 600; }
|
||
.skill-install-result {
|
||
margin-top: 16px; padding: 16px; border-radius: 8px;
|
||
background: #e8f5e9; border: 1px solid #c8e6c9; font-size: 0.88em;
|
||
}
|
||
.skill-install-result.error { background: #ffebee; border-color: #ffcdd2; }
|
||
|
||
@media (max-width: 800px) {
|
||
.main { padding: 16px; }
|
||
header { padding: 14px 16px; }
|
||
.status-bar { padding: 8px 16px; flex-wrap: wrap; gap: 12px; }
|
||
.case-grid { grid-template-columns: 1fr; }
|
||
.wizard-steps { flex-wrap: wrap; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<header>
|
||
<h1 onclick="navigate('/')">עוזר משפטי</h1>
|
||
<div class="sep"></div>
|
||
<span class="subtitle" id="pageSubtitle">ניהול תיקים</span>
|
||
<nav>
|
||
<a href="#/" id="navHome">תיקים</a>
|
||
<a href="#/new" id="navNew">+ תיק חדש</a>
|
||
<a href="#/upload" id="navUpload">העלאה</a>
|
||
<a href="#/skills" id="navSkills">Skills</a>
|
||
</nav>
|
||
</header>
|
||
|
||
<div class="main">
|
||
<!-- ══ Page: Case List ══ -->
|
||
<div class="page" id="page-home">
|
||
<div class="page-header">
|
||
<h2>תיקי ערר</h2>
|
||
<button class="btn btn-primary" onclick="navigate('/new')">+ תיק חדש</button>
|
||
</div>
|
||
<div class="case-grid" id="caseGrid">
|
||
<div class="empty">טוען תיקים...</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ══ Page: New Case Wizard ══ -->
|
||
<div class="page" id="page-new">
|
||
<div class="wizard-steps">
|
||
<div class="wizard-step active" data-step="1">1. פרטי תיק</div>
|
||
<div class="wizard-step" data-step="2">2. צדדים</div>
|
||
<div class="wizard-step" data-step="3">3. לוח זמנים</div>
|
||
<div class="wizard-step" data-step="4">4. סיכום ויצירה</div>
|
||
</div>
|
||
|
||
<!-- Step 1: Case Details -->
|
||
<div class="wizard-panel active" id="wiz-step-1">
|
||
<div class="card"><div class="card-body">
|
||
<div class="form-row">
|
||
<div class="form-group" style="max-width:180px">
|
||
<label>מספר תיק *</label>
|
||
<input type="text" id="wiz-case-number" placeholder="1130-25" dir="ltr">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>כותרת *</label>
|
||
<input type="text" id="wiz-title" placeholder="ערר על אישור תכנית להוספת קומה">
|
||
</div>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>סוג ערר</label>
|
||
<select id="wiz-committee-type">
|
||
<option value="רישוי">רישוי ובניה</option>
|
||
<option value="היטל השבחה">היטל השבחה</option>
|
||
<option value="פיצויים">פיצויים (ס' 197)</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>כתובת נכס</label>
|
||
<input type="text" id="wiz-address" placeholder="רח' אבינדב 23, קריית יערים">
|
||
</div>
|
||
<div class="form-group" style="max-width:180px">
|
||
<label>מספר היתר/תכנית</label>
|
||
<input type="text" id="wiz-permit" placeholder="152-1257682" dir="ltr">
|
||
</div>
|
||
</div>
|
||
</div></div>
|
||
<div class="wizard-nav">
|
||
<span></span>
|
||
<button class="btn btn-primary" onclick="wizNext()">הבא ←</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Step 2: Parties -->
|
||
<div class="wizard-panel" id="wiz-step-2">
|
||
<div class="card"><div class="card-header">עוררים</div><div class="card-body">
|
||
<div class="dynamic-list" id="appellantsList">
|
||
<div class="item"><input type="text" placeholder="שם עורר"><button class="remove-btn" onclick="removeItem(this)">×</button></div>
|
||
</div>
|
||
<button class="btn btn-sm btn-secondary" onclick="addItem('appellantsList')">+ הוסף עורר</button>
|
||
</div></div>
|
||
<div class="card"><div class="card-header">משיבים</div><div class="card-body">
|
||
<div class="dynamic-list" id="respondentsList">
|
||
<div class="item"><input type="text" placeholder="שם משיב"><button class="remove-btn" onclick="removeItem(this)">×</button></div>
|
||
</div>
|
||
<button class="btn btn-sm btn-secondary" onclick="addItem('respondentsList')">+ הוסף משיב</button>
|
||
</div></div>
|
||
<div class="wizard-nav">
|
||
<button class="btn btn-secondary" onclick="wizPrev()">→ הקודם</button>
|
||
<button class="btn btn-primary" onclick="wizNext()">הבא ←</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Step 3: Schedule -->
|
||
<div class="wizard-panel" id="wiz-step-3">
|
||
<div class="card"><div class="card-body">
|
||
<div class="form-row">
|
||
<div class="form-group" style="max-width:200px">
|
||
<label>תאריך דיון</label>
|
||
<input type="date" id="wiz-hearing-date">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>תוצאה צפויה</label>
|
||
<select id="wiz-outcome">
|
||
<option value="">לא הוגדר</option>
|
||
<option value="rejection">דחייה</option>
|
||
<option value="partial_acceptance">קבלה חלקית</option>
|
||
<option value="full_acceptance">קבלה מלאה</option>
|
||
<option value="betterment_levy">היטל השבחה</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>הערות</label>
|
||
<textarea id="wiz-notes" rows="3" placeholder="הערות חופשיות..."></textarea>
|
||
</div>
|
||
</div></div>
|
||
<div class="wizard-nav">
|
||
<button class="btn btn-secondary" onclick="wizPrev()">→ הקודם</button>
|
||
<button class="btn btn-primary" onclick="wizNext()">הבא ←</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Step 4: Review & Create -->
|
||
<div class="wizard-panel" id="wiz-step-4">
|
||
<div class="card"><div class="card-header">סיכום</div><div class="card-body">
|
||
<div id="wizSummary"></div>
|
||
</div></div>
|
||
<div class="card"><div class="card-header">יצירה אוטומטית</div><div class="card-body">
|
||
<ul class="creation-steps" id="creationSteps">
|
||
<li class="pending" id="cs-db"><span class="step-icon">◯</span> יצירת תיק במסד הנתונים</li>
|
||
<li class="pending" id="cs-gitea"><span class="step-icon">◯</span> יצירת Repository ב-Gitea</li>
|
||
<li class="pending" id="cs-paperclip"><span class="step-icon">◯</span> יצירת פרויקט ב-Paperclip</li>
|
||
</ul>
|
||
<div style="margin-top:16px">
|
||
<button class="btn btn-success" id="createCaseBtn" onclick="createCase()">צור תיק</button>
|
||
</div>
|
||
</div></div>
|
||
<div class="wizard-nav">
|
||
<button class="btn btn-secondary" onclick="wizPrev()">→ הקודם</button>
|
||
<span></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ══ Page: Case View ══ -->
|
||
<div class="page" id="page-case">
|
||
<div class="case-header-bar" id="caseHeaderBar">
|
||
<div class="info">
|
||
<h2 id="caseViewTitle">טוען...</h2>
|
||
<div class="meta" id="caseViewMeta"></div>
|
||
</div>
|
||
<div class="links" id="caseViewLinks"></div>
|
||
</div>
|
||
|
||
<!-- Upload to case -->
|
||
<div class="card">
|
||
<div class="card-header">העלאת מסמכים</div>
|
||
<div class="card-body">
|
||
<div class="upload-zone" id="caseDropZone">
|
||
<h3>גרור קבצים לכאן או לחץ לבחירה</h3>
|
||
<p>PDF, DOCX, RTF, TXT — עד 50MB</p>
|
||
<input type="file" id="caseFileInput" multiple accept=".pdf,.docx,.rtf,.txt,.md">
|
||
</div>
|
||
<div id="casePendingUploads" style="margin-top: 12px"></div>
|
||
<div id="caseTasks" style="margin-top: 12px"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Documents list -->
|
||
<div class="card">
|
||
<div class="card-header">מסמכים בתיק</div>
|
||
<div class="card-body" id="caseDocsList">
|
||
<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: Skills Management ══ -->
|
||
<div class="page" id="page-skills">
|
||
<div class="page-header">
|
||
<h2>Paperclip Skills</h2>
|
||
</div>
|
||
|
||
<!-- Install Skill -->
|
||
<div class="card">
|
||
<div class="card-header">התקנה / שדרוג Skill</div>
|
||
<div class="card-body">
|
||
<div class="upload-zone" id="skillDropZone">
|
||
<div style="font-size:3em;color:#ccc;margin-bottom:16px">🔌</div>
|
||
<h3>גרור קובץ ZIP של Skill או לחץ לבחירה</h3>
|
||
<p>ZIP עם SKILL.md, scripts/, references/ — לפי מבנה Anthropic</p>
|
||
<input type="file" id="skillFileInput" accept=".zip">
|
||
</div>
|
||
<div id="skillInstallResult"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Installed Skills -->
|
||
<div class="card">
|
||
<div class="card-header">Skills מותקנים</div>
|
||
<div class="card-body">
|
||
<div class="skill-list" id="skillList">
|
||
<div class="empty">טוען...</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Restart Paperclip -->
|
||
<div class="card">
|
||
<div class="card-header">Paperclip Server</div>
|
||
<div class="card-body" style="display:flex;align-items:center;gap:16px">
|
||
<button class="btn btn-secondary" id="restartPaperclipBtn" onclick="restartPaperclip()">Restart Paperclip</button>
|
||
<span id="restartStatus" style="font-size:0.85em;color:#888"></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ══ Page: Legacy Upload ══ -->
|
||
<div class="page" id="page-upload">
|
||
<div class="card"><div class="card-body">
|
||
<div class="upload-zone" id="legacyDropZone">
|
||
<div style="font-size:3em;color:#ccc;margin-bottom:16px">📄</div>
|
||
<h3>גרור קבצים לכאן או לחץ לבחירה</h3>
|
||
<p>PDF, DOCX, RTF, TXT — עד 50MB</p>
|
||
<input type="file" id="legacyFileInput" multiple accept=".pdf,.docx,.rtf,.txt,.md">
|
||
</div>
|
||
</div></div>
|
||
<div class="card" id="legacyPendingCard" style="display:none">
|
||
<div class="card-header">קבצים ממתינים לסיווג</div>
|
||
<div class="card-body" id="legacyPendingList"></div>
|
||
</div>
|
||
<div class="card" id="legacyTasksCard" style="display:none">
|
||
<div class="card-header">עיבוד</div>
|
||
<div class="card-body" id="legacyTasksList"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Status Bar -->
|
||
<div class="status-bar">
|
||
<div class="stat">תיקים: <span class="stat-value" id="statCases">—</span></div>
|
||
<div class="stat">מסמכים: <span class="stat-value" id="statDocs">—</span></div>
|
||
<div class="stat">קטעים: <span class="stat-value" id="statChunks">—</span></div>
|
||
</div>
|
||
|
||
<div class="toast" id="toast"></div>
|
||
|
||
<script>
|
||
const API = '/api';
|
||
let currentCaseNumber = '';
|
||
|
||
// ── Router ───────────────────────────────────────────────
|
||
function navigate(path) {
|
||
window.location.hash = path;
|
||
}
|
||
|
||
function handleRoute() {
|
||
const hash = window.location.hash || '#/';
|
||
const pages = document.querySelectorAll('.page');
|
||
pages.forEach(p => p.classList.remove('active'));
|
||
|
||
// Nav links
|
||
document.querySelectorAll('header nav a').forEach(a => a.classList.remove('active'));
|
||
|
||
let subtitle = 'ניהול תיקים';
|
||
|
||
if (hash === '#/' || hash === '#') {
|
||
document.getElementById('page-home').classList.add('active');
|
||
document.getElementById('navHome').classList.add('active');
|
||
loadCaseList();
|
||
} else if (hash === '#/new') {
|
||
document.getElementById('page-new').classList.add('active');
|
||
document.getElementById('navNew').classList.add('active');
|
||
subtitle = 'תיק חדש';
|
||
resetWizard();
|
||
} else if (hash.startsWith('#/case/')) {
|
||
const caseNum = hash.replace('#/case/', '');
|
||
document.getElementById('page-case').classList.add('active');
|
||
subtitle = 'ערר ' + caseNum;
|
||
currentCaseNumber = caseNum;
|
||
loadCaseView(caseNum);
|
||
} else if (hash === '#/skills') {
|
||
document.getElementById('page-skills').classList.add('active');
|
||
document.getElementById('navSkills').classList.add('active');
|
||
subtitle = 'Paperclip Skills';
|
||
loadSkillList();
|
||
setupSkillUpload();
|
||
} else if (hash === '#/upload') {
|
||
document.getElementById('page-upload').classList.add('active');
|
||
document.getElementById('navUpload').classList.add('active');
|
||
subtitle = 'העלאת מסמכים';
|
||
loadLegacyPending();
|
||
}
|
||
|
||
document.getElementById('pageSubtitle').textContent = subtitle;
|
||
}
|
||
|
||
window.addEventListener('hashchange', handleRoute);
|
||
window.addEventListener('load', () => { handleRoute(); loadStatus(); });
|
||
|
||
// ── Case List ────────────────────────────────────────────
|
||
async function loadCaseList() {
|
||
const grid = document.getElementById('caseGrid');
|
||
try {
|
||
const res = await fetch(API + '/cases?detail=true');
|
||
const cases = await res.json();
|
||
if (!cases.length) {
|
||
grid.innerHTML = '<div class="empty">אין תיקים עדיין.<br>לחץ "+ תיק חדש" כדי להתחיל.</div>';
|
||
return;
|
||
}
|
||
const STATUS_LABELS = {
|
||
new: 'חדש', in_progress: 'בתהליך', documents_ready: 'מסמכים מוכנים',
|
||
outcome_set: 'תוצאה נקבעה', direction_approved: 'כיוון אושר',
|
||
drafting: 'בכתיבה', drafted: 'טיוטה', qa_review: 'QA', reviewed: 'נבדק', final: 'סופי',
|
||
};
|
||
grid.innerHTML = cases.map(c => `
|
||
<div class="case-card" onclick="navigate('/case/${esc(c.case_number)}')">
|
||
<div style="display:flex;justify-content:space-between;align-items:center">
|
||
<span class="case-number">ערר ${esc(c.case_number)}</span>
|
||
<span class="badge ${c.status}">${STATUS_LABELS[c.status] || c.status}</span>
|
||
</div>
|
||
<div class="case-title">${esc(c.title)}</div>
|
||
<div class="case-meta">
|
||
${c.document_count !== undefined ? `<span>${c.document_count} מסמכים</span>` : ''}
|
||
${c.committee_type ? `<span>${esc(c.committee_type)}</span>` : ''}
|
||
${c.hearing_date ? `<span>דיון: ${esc(c.hearing_date)}</span>` : ''}
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
} catch (e) {
|
||
grid.innerHTML = '<div class="empty">שגיאה בטעינת תיקים</div>';
|
||
}
|
||
}
|
||
|
||
// ── Wizard ────────────────────────────────────────────────
|
||
let wizStep = 1;
|
||
|
||
function resetWizard() {
|
||
wizStep = 1;
|
||
updateWizardUI();
|
||
// Reset creation steps
|
||
document.querySelectorAll('.creation-steps li').forEach(li => {
|
||
li.className = 'pending';
|
||
li.querySelector('.step-icon').innerHTML = '◯';
|
||
});
|
||
document.getElementById('createCaseBtn').disabled = false;
|
||
}
|
||
|
||
function updateWizardUI() {
|
||
document.querySelectorAll('.wizard-step').forEach(s => {
|
||
const step = parseInt(s.dataset.step);
|
||
s.classList.toggle('active', step === wizStep);
|
||
s.classList.toggle('done', step < wizStep);
|
||
});
|
||
document.querySelectorAll('.wizard-panel').forEach((p, i) => {
|
||
p.classList.toggle('active', i + 1 === wizStep);
|
||
});
|
||
if (wizStep === 4) buildSummary();
|
||
}
|
||
|
||
function wizNext() {
|
||
if (wizStep === 1) {
|
||
if (!document.getElementById('wiz-case-number').value.trim() || !document.getElementById('wiz-title').value.trim()) {
|
||
return toast('יש למלא מספר תיק וכותרת', 'error');
|
||
}
|
||
}
|
||
if (wizStep < 4) { wizStep++; updateWizardUI(); }
|
||
}
|
||
|
||
function wizPrev() {
|
||
if (wizStep > 1) { wizStep--; updateWizardUI(); }
|
||
}
|
||
|
||
function addItem(listId) {
|
||
const list = document.getElementById(listId);
|
||
const div = document.createElement('div');
|
||
div.className = 'item';
|
||
div.innerHTML = `<input type="text" placeholder="שם"><button class="remove-btn" onclick="removeItem(this)">×</button>`;
|
||
list.appendChild(div);
|
||
div.querySelector('input').focus();
|
||
}
|
||
|
||
function removeItem(btn) {
|
||
const list = btn.closest('.dynamic-list');
|
||
btn.closest('.item').remove();
|
||
if (!list.children.length) addItem(list.id);
|
||
}
|
||
|
||
function getListValues(listId) {
|
||
return Array.from(document.querySelectorAll(`#${listId} .item input`))
|
||
.map(i => i.value.trim()).filter(Boolean);
|
||
}
|
||
|
||
function buildSummary() {
|
||
const data = getWizardData();
|
||
const OUTCOME_LABELS = { rejection: 'דחייה', partial_acceptance: 'קבלה חלקית', full_acceptance: 'קבלה מלאה', betterment_levy: 'היטל השבחה' };
|
||
document.getElementById('wizSummary').innerHTML = `
|
||
<table style="width:100%;font-size:0.88em;border-collapse:collapse">
|
||
<tr><td style="padding:6px;color:#888;width:120px">מספר תיק</td><td style="padding:6px;font-weight:600">${esc(data.case_number)}</td></tr>
|
||
<tr><td style="padding:6px;color:#888">כותרת</td><td style="padding:6px">${esc(data.title)}</td></tr>
|
||
<tr><td style="padding:6px;color:#888">סוג</td><td style="padding:6px">${esc(data.committee_type)}</td></tr>
|
||
<tr><td style="padding:6px;color:#888">כתובת</td><td style="padding:6px">${esc(data.property_address || '—')}</td></tr>
|
||
<tr><td style="padding:6px;color:#888">עוררים</td><td style="padding:6px">${data.appellants.length ? data.appellants.map(esc).join(', ') : '—'}</td></tr>
|
||
<tr><td style="padding:6px;color:#888">משיבים</td><td style="padding:6px">${data.respondents.length ? data.respondents.map(esc).join(', ') : '—'}</td></tr>
|
||
<tr><td style="padding:6px;color:#888">תוצאה צפויה</td><td style="padding:6px">${OUTCOME_LABELS[data.expected_outcome] || '—'}</td></tr>
|
||
</table>
|
||
`;
|
||
}
|
||
|
||
function getWizardData() {
|
||
return {
|
||
case_number: document.getElementById('wiz-case-number').value.trim(),
|
||
title: document.getElementById('wiz-title').value.trim(),
|
||
committee_type: document.getElementById('wiz-committee-type').value,
|
||
property_address: document.getElementById('wiz-address').value.trim(),
|
||
permit_number: document.getElementById('wiz-permit').value.trim(),
|
||
appellants: getListValues('appellantsList'),
|
||
respondents: getListValues('respondentsList'),
|
||
hearing_date: document.getElementById('wiz-hearing-date').value,
|
||
expected_outcome: document.getElementById('wiz-outcome').value,
|
||
notes: document.getElementById('wiz-notes').value.trim(),
|
||
};
|
||
}
|
||
|
||
async function createCase() {
|
||
const data = getWizardData();
|
||
const btn = document.getElementById('createCaseBtn');
|
||
btn.disabled = true;
|
||
btn.textContent = 'יוצר...';
|
||
|
||
const setStep = (id, state, icon) => {
|
||
const li = document.getElementById(id);
|
||
li.className = state;
|
||
li.querySelector('.step-icon').innerHTML = icon;
|
||
};
|
||
|
||
try {
|
||
// Step 1: Create in DB
|
||
setStep('cs-db', 'running', '⚙');
|
||
const dbRes = await fetch(API + '/cases/create', {
|
||
method: 'POST', headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify(data),
|
||
});
|
||
if (!dbRes.ok) { const e = await dbRes.json(); throw new Error(e.detail || 'DB error'); }
|
||
setStep('cs-db', 'done', '✓');
|
||
|
||
// Step 2: Gitea repo
|
||
setStep('cs-gitea', 'running', '⚙');
|
||
try {
|
||
const giteaRes = await fetch(API + '/integrations/gitea/create-repo', {
|
||
method: 'POST', headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({ case_number: data.case_number, title: data.title }),
|
||
});
|
||
if (giteaRes.ok) {
|
||
setStep('cs-gitea', 'done', '✓');
|
||
} else {
|
||
setStep('cs-gitea', 'error', '✗');
|
||
}
|
||
} catch (e) {
|
||
setStep('cs-gitea', 'error', '✗');
|
||
}
|
||
|
||
// Step 3: Paperclip project
|
||
setStep('cs-paperclip', 'running', '⚙');
|
||
try {
|
||
const pcRes = await fetch(API + '/integrations/paperclip/create-project', {
|
||
method: 'POST', headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({
|
||
case_number: data.case_number, title: data.title,
|
||
appeal_type: data.committee_type,
|
||
description: `${data.appellants.join(', ')} נ' ${data.respondents.join(', ')}`,
|
||
}),
|
||
});
|
||
if (pcRes.ok) {
|
||
setStep('cs-paperclip', 'done', '✓');
|
||
} else {
|
||
setStep('cs-paperclip', 'error', '✗');
|
||
}
|
||
} catch (e) {
|
||
setStep('cs-paperclip', 'error', '✗');
|
||
}
|
||
|
||
toast('התיק נוצר בהצלחה!', 'success');
|
||
setTimeout(() => navigate('/case/' + data.case_number), 1000);
|
||
|
||
} catch (err) {
|
||
setStep('cs-db', 'error', '✗');
|
||
toast(err.message, 'error');
|
||
btn.disabled = false;
|
||
btn.textContent = 'צור תיק';
|
||
}
|
||
}
|
||
|
||
// ── Case View ────────────────────────────────────────────
|
||
const DOC_TYPE_LABELS = {
|
||
appeal: 'כתבי ערר', response: 'כתבי תשובה', protocol: 'פרוטוקולים',
|
||
plan: 'תכניות', decision: 'החלטות', court_decision: 'פסקי דין',
|
||
permit: 'היתרים', appraisal: 'שומות', exhibit: 'נספחים',
|
||
objection: 'התנגדויות', reference: 'מסמכי עזר', auto: 'לא מסווג',
|
||
};
|
||
|
||
async function loadCaseView(caseNumber) {
|
||
try {
|
||
const res = await fetch(API + '/cases/' + encodeURIComponent(caseNumber) + '/details');
|
||
if (!res.ok) throw new Error('Case not found');
|
||
const data = await res.json();
|
||
|
||
document.getElementById('caseViewTitle').textContent = `ערר ${caseNumber} — ${data.title || ''}`;
|
||
|
||
const STATUS_LABELS = {
|
||
new: 'חדש', in_progress: 'בתהליך', documents_ready: 'מסמכים מוכנים',
|
||
drafted: 'טיוטה', final: 'סופי',
|
||
};
|
||
const meta = [];
|
||
meta.push(`<span class="badge ${data.status}">${STATUS_LABELS[data.status] || data.status}</span>`);
|
||
if (data.committee_type) meta.push(data.committee_type);
|
||
if (data.property_address) meta.push(data.property_address);
|
||
if (data.appellants?.length) meta.push('עוררים: ' + data.appellants.join(', '));
|
||
document.getElementById('caseViewMeta').innerHTML = meta.map(m => `<span>${m}</span>`).join('');
|
||
|
||
// Links
|
||
const links = [];
|
||
links.push(`<a href="https://gitea.nautilus.marcusgroup.org/cases/${caseNumber}" target="_blank">Gitea</a>`);
|
||
links.push(`<a href="#" onclick="openPaperclip('${esc(caseNumber)}');return false">Paperclip</a>`);
|
||
document.getElementById('caseViewLinks').innerHTML = links.join('');
|
||
|
||
// Documents grouped by type
|
||
const docs = data.documents || [];
|
||
if (!docs.length) {
|
||
document.getElementById('caseDocsList').innerHTML = '<div class="empty">אין מסמכים עדיין — העלה קבצים למעלה</div>';
|
||
} else {
|
||
const groups = {};
|
||
docs.forEach(d => {
|
||
const t = d.doc_type || 'reference';
|
||
if (!groups[t]) groups[t] = [];
|
||
groups[t].push(d);
|
||
});
|
||
const order = ['appeal','response','protocol','plan','decision','court_decision','permit','appraisal','exhibit','objection','reference'];
|
||
let html = '';
|
||
for (const type of order) {
|
||
if (!groups[type]) continue;
|
||
html += `<div class="doc-group">
|
||
<div class="doc-group-header">
|
||
<span>${DOC_TYPE_LABELS[type] || type}</span>
|
||
<span class="count">${groups[type].length}</span>
|
||
</div>`;
|
||
for (const doc of groups[type]) {
|
||
const statusIcon = doc.extraction_status === 'completed' ? '✓' : '◯';
|
||
html += `<div class="doc-item">
|
||
<span class="doc-icon">📄</span>
|
||
<span class="doc-name">${esc(doc.title)}</span>
|
||
<span class="doc-status">${statusIcon}</span>
|
||
</div>`;
|
||
}
|
||
html += '</div>';
|
||
}
|
||
document.getElementById('caseDocsList').innerHTML = html;
|
||
}
|
||
} catch (e) {
|
||
document.getElementById('caseViewTitle').textContent = `תיק ${caseNumber} לא נמצא`;
|
||
document.getElementById('caseViewMeta').innerHTML = '';
|
||
document.getElementById('caseViewLinks').innerHTML = '';
|
||
document.getElementById('caseDocsList').innerHTML = `
|
||
<div class="empty">
|
||
התיק לא קיים עדיין במערכת.<br>
|
||
<button class="btn btn-primary" style="margin-top:12px" onclick="navigate('/new')">צור תיק חדש</button>
|
||
</div>`;
|
||
return;
|
||
}
|
||
|
||
setupCaseUpload(caseNumber);
|
||
loadExports(caseNumber);
|
||
setupExportUpload(caseNumber);
|
||
}
|
||
|
||
async function openPaperclip(caseNumber) {
|
||
// Try to find the Paperclip project URL
|
||
try {
|
||
const res = await fetch(API + '/cases/' + encodeURIComponent(caseNumber) + '/details');
|
||
const data = await res.json();
|
||
// Determine company prefix from committee_type
|
||
const prefix = (data.committee_type === 'היטל השבחה' || data.committee_type === 'פיצויים') ? 'CMPA' : 'CMP';
|
||
window.open(`https://pc.nautilus.marcusgroup.org/${prefix}/projects`, '_blank');
|
||
} catch (e) {
|
||
window.open('https://pc.nautilus.marcusgroup.org', '_blank');
|
||
}
|
||
}
|
||
|
||
// ── Case Document Upload ─────────────────────────────────
|
||
let casePendingFiles = [];
|
||
|
||
function setupCaseUpload(caseNumber) {
|
||
const dropZone = document.getElementById('caseDropZone');
|
||
const fileInput = document.getElementById('caseFileInput');
|
||
|
||
// Remove old listeners by cloning
|
||
const newDrop = dropZone.cloneNode(true);
|
||
dropZone.parentNode.replaceChild(newDrop, dropZone);
|
||
const newInput = newDrop.querySelector('input[type="file"]');
|
||
|
||
newDrop.addEventListener('click', () => newInput.click());
|
||
newDrop.addEventListener('dragover', e => { e.preventDefault(); newDrop.classList.add('dragover'); });
|
||
newDrop.addEventListener('dragleave', () => newDrop.classList.remove('dragover'));
|
||
newDrop.addEventListener('drop', e => {
|
||
e.preventDefault();
|
||
newDrop.classList.remove('dragover');
|
||
addFilesToCase(e.dataTransfer.files, caseNumber);
|
||
});
|
||
newInput.addEventListener('change', () => {
|
||
if (newInput.files.length) addFilesToCase(newInput.files, caseNumber);
|
||
newInput.value = '';
|
||
});
|
||
|
||
casePendingFiles = [];
|
||
renderCasePending(caseNumber);
|
||
}
|
||
|
||
function addFilesToCase(files, caseNumber) {
|
||
for (const file of files) {
|
||
casePendingFiles.push({ file, doc_type: 'auto', party_name: '' });
|
||
}
|
||
renderCasePending(caseNumber);
|
||
}
|
||
|
||
function renderCasePending(caseNumber) {
|
||
const container = document.getElementById('casePendingUploads');
|
||
if (!casePendingFiles.length) { container.innerHTML = ''; return; }
|
||
|
||
container.innerHTML = casePendingFiles.map((pf, i) => `
|
||
<div class="pending-upload" data-idx="${i}">
|
||
<div class="file-row">
|
||
<span class="file-name">${esc(pf.file.name)}</span>
|
||
<span style="font-size:0.78em;color:#999">${formatSize(pf.file.size)}</span>
|
||
</div>
|
||
<div class="tag-row">
|
||
<div class="form-group" style="min-width:140px">
|
||
<label>סוג מסמך</label>
|
||
<select onchange="casePendingFiles[${i}].doc_type=this.value">
|
||
<option value="auto">אוטומטי</option>
|
||
<option value="appeal">כתב ערר</option>
|
||
<option value="response">תשובה</option>
|
||
<option value="protocol">פרוטוקול</option>
|
||
<option value="plan">תכנית</option>
|
||
<option value="decision">החלטה</option>
|
||
<option value="court_decision">פסק דין</option>
|
||
<option value="permit">היתר</option>
|
||
<option value="appraisal">שומה</option>
|
||
<option value="exhibit">נספח</option>
|
||
<option value="objection">התנגדות</option>
|
||
<option value="reference">מסמך עזר</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group" style="min-width:140px">
|
||
<label>שם צד (אופציונלי)</label>
|
||
<input type="text" placeholder="לדוגמה: ועדת הראל" onchange="casePendingFiles[${i}].party_name=this.value">
|
||
</div>
|
||
<button class="btn btn-sm btn-outline" onclick="removeCasePending(${i},'${esc(caseNumber)}')">×</button>
|
||
</div>
|
||
</div>
|
||
`).join('') + `
|
||
<div style="margin-top:12px">
|
||
<button class="btn btn-primary" onclick="uploadAllCaseDocs('${esc(caseNumber)}')">שייך לתיק (${casePendingFiles.length} קבצים)</button>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function removeCasePending(idx, caseNumber) {
|
||
casePendingFiles.splice(idx, 1);
|
||
renderCasePending(caseNumber);
|
||
}
|
||
|
||
async function uploadAllCaseDocs(caseNumber) {
|
||
const tasksContainer = document.getElementById('caseTasks');
|
||
for (const pf of casePendingFiles) {
|
||
const formData = new FormData();
|
||
formData.append('file', pf.file);
|
||
formData.append('doc_type', pf.doc_type);
|
||
formData.append('party_name', pf.party_name);
|
||
|
||
try {
|
||
const res = await fetch(API + '/cases/' + encodeURIComponent(caseNumber) + '/documents/upload-tagged', {
|
||
method: 'POST', body: formData,
|
||
});
|
||
if (!res.ok) {
|
||
const err = await res.json();
|
||
toast(err.detail || 'שגיאה', 'error');
|
||
continue;
|
||
}
|
||
const data = await res.json();
|
||
trackCaseTask(data.task_id, data.filename, tasksContainer, caseNumber);
|
||
} catch (e) {
|
||
toast('שגיאת רשת', 'error');
|
||
}
|
||
}
|
||
casePendingFiles = [];
|
||
document.getElementById('casePendingUploads').innerHTML = '';
|
||
}
|
||
|
||
function trackCaseTask(taskId, displayName, container, caseNumber) {
|
||
const STEP_LABELS = { extracting: 'מחלץ טקסט', chunking: 'מפצל לקטעים', embedding: 'יוצר embeddings' };
|
||
const div = document.createElement('div');
|
||
div.className = 'task-item';
|
||
div.innerHTML = `
|
||
<div class="spinner"></div>
|
||
<div class="task-info">
|
||
<div class="task-name">${esc(displayName)}</div>
|
||
<div class="task-status">בתור...</div>
|
||
</div>`;
|
||
container.prepend(div);
|
||
|
||
const es = new EventSource(API + '/progress/' + taskId);
|
||
es.onmessage = e => {
|
||
const data = JSON.parse(e.data);
|
||
const statusEl = div.querySelector('.task-status');
|
||
let label = { queued: 'בתור...', processing: 'מעבד...', completed: 'הושלם', failed: 'נכשל' }[data.status] || data.status;
|
||
if (data.step) label += ' — ' + (STEP_LABELS[data.step] || data.step);
|
||
statusEl.textContent = label;
|
||
if (data.status === 'completed') {
|
||
div.classList.add('done');
|
||
es.close();
|
||
toast('הועלה: ' + displayName, 'success');
|
||
loadCaseView(caseNumber);
|
||
loadStatus();
|
||
} else if (data.status === 'failed') {
|
||
div.classList.add('failed');
|
||
es.close();
|
||
statusEl.textContent = 'נכשל: ' + (data.error || 'שגיאה');
|
||
}
|
||
};
|
||
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 ? '✅' : '📄'}</span>
|
||
<div class="export-info">
|
||
<div class="export-name">${esc(f.filename)}</div>
|
||
<div class="export-meta">${dateStr} · ${formatSize(f.size)}${isFinal ? ' · <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 = [];
|
||
|
||
function setupLegacyUpload() {
|
||
const dropZone = document.getElementById('legacyDropZone');
|
||
const fileInput = document.getElementById('legacyFileInput');
|
||
dropZone.addEventListener('click', () => fileInput.click());
|
||
dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('dragover'); });
|
||
dropZone.addEventListener('dragleave', () => dropZone.classList.remove('dragover'));
|
||
dropZone.addEventListener('drop', e => {
|
||
e.preventDefault(); dropZone.classList.remove('dragover');
|
||
handleLegacyFiles(e.dataTransfer.files);
|
||
});
|
||
fileInput.addEventListener('change', () => {
|
||
if (fileInput.files.length) handleLegacyFiles(fileInput.files);
|
||
fileInput.value = '';
|
||
});
|
||
}
|
||
|
||
async function handleLegacyFiles(files) {
|
||
for (const file of files) {
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
try {
|
||
const res = await fetch(API + '/upload', { method: 'POST', body: formData });
|
||
if (res.ok) toast('הקובץ הועלה', 'success');
|
||
} catch (e) { toast('שגיאה', 'error'); }
|
||
}
|
||
loadLegacyPending();
|
||
}
|
||
|
||
async function loadLegacyPending() {
|
||
const res = await fetch(API + '/uploads');
|
||
const files = await res.json();
|
||
const card = document.getElementById('legacyPendingCard');
|
||
const list = document.getElementById('legacyPendingList');
|
||
|
||
if (!files.length) { card.style.display = 'none'; return; }
|
||
card.style.display = 'block';
|
||
|
||
try {
|
||
const casesRes = await fetch(API + '/cases');
|
||
legacyCases = await casesRes.json();
|
||
} catch (e) { legacyCases = []; }
|
||
|
||
list.innerHTML = files.map(f => `
|
||
<div class="pending-upload" data-filename="${esc(f.filename)}">
|
||
<div class="file-row">
|
||
<span class="file-name">${esc(f.filename.replace(/^\d+_/, ''))}</span>
|
||
<span style="font-size:0.78em;color:#999">${formatSize(f.size)}</span>
|
||
</div>
|
||
<div class="tag-row">
|
||
<div class="form-group">
|
||
<label>סיווג</label>
|
||
<select class="legacy-category" onchange="toggleLegacyFields(this)">
|
||
<option value="">בחר...</option>
|
||
<option value="case">מסמך תיק</option>
|
||
<option value="training">החלטה לאימון</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group legacy-case-field" style="display:none">
|
||
<label>תיק</label>
|
||
<select class="legacy-case-select">
|
||
<option value="">בחר תיק...</option>
|
||
${legacyCases.map(c => `<option value="${esc(c.case_number)}">${esc(c.case_number)} — ${esc(c.title)}</option>`).join('')}
|
||
</select>
|
||
</div>
|
||
<div class="form-group legacy-case-field" style="display:none">
|
||
<label>סוג מסמך</label>
|
||
<select class="legacy-doctype-select">
|
||
<option value="appeal">כתב ערר</option>
|
||
<option value="response">תשובה</option>
|
||
<option value="decision">החלטה</option>
|
||
<option value="exhibit">נספח</option>
|
||
<option value="reference">מסמך עזר</option>
|
||
</select>
|
||
</div>
|
||
<button class="btn btn-sm btn-primary" onclick="processLegacyFile('${esc(f.filename)}')" disabled>עבד</button>
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
function toggleLegacyFields(select) {
|
||
const container = select.closest('.pending-upload');
|
||
const show = select.value === 'case';
|
||
container.querySelectorAll('.legacy-case-field').forEach(el => el.style.display = show ? 'block' : 'none');
|
||
container.querySelector('.btn-primary').disabled = !select.value;
|
||
}
|
||
|
||
async function processLegacyFile(filename) {
|
||
const container = document.querySelector(`.pending-upload[data-filename="${filename}"]`);
|
||
const category = container.querySelector('.legacy-category').value;
|
||
const body = { filename, category, title: '' };
|
||
if (category === 'case') {
|
||
body.case_number = container.querySelector('.legacy-case-select').value;
|
||
body.doc_type = container.querySelector('.legacy-doctype-select').value;
|
||
if (!body.case_number) return toast('יש לבחור תיק', 'error');
|
||
}
|
||
|
||
const res = await fetch(API + '/classify', {
|
||
method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(body),
|
||
});
|
||
if (!res.ok) { toast('שגיאה', 'error'); return; }
|
||
const data = await res.json();
|
||
|
||
const tasksCard = document.getElementById('legacyTasksCard');
|
||
const tasksList = document.getElementById('legacyTasksList');
|
||
tasksCard.style.display = 'block';
|
||
trackCaseTask(data.task_id, filename.replace(/^\d+_/, ''), tasksList, body.case_number || '');
|
||
setTimeout(loadLegacyPending, 500);
|
||
}
|
||
|
||
// ── Status Bar ───────────────────────────────────────────
|
||
async function loadStatus() {
|
||
try {
|
||
const res = await fetch(API + '/processing-status');
|
||
const data = await res.json();
|
||
document.getElementById('statCases').textContent = data.cases ?? '—';
|
||
document.getElementById('statDocs').textContent = data.documents ?? '—';
|
||
document.getElementById('statChunks').textContent = data.chunks ?? '—';
|
||
} catch (e) {}
|
||
}
|
||
|
||
// ── Helpers ──────────────────────────────────────────────
|
||
function esc(s) {
|
||
if (!s) return '';
|
||
const d = document.createElement('div');
|
||
d.textContent = String(s);
|
||
return d.innerHTML;
|
||
}
|
||
function formatSize(bytes) {
|
||
if (bytes < 1024) return bytes + ' B';
|
||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
||
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
||
}
|
||
function toast(msg, type = '') {
|
||
const el = document.getElementById('toast');
|
||
el.textContent = msg;
|
||
el.className = 'toast show ' + type;
|
||
setTimeout(() => el.className = 'toast', 3000);
|
||
}
|
||
|
||
// ── Skills Management ───────────────────────────────────
|
||
async function loadSkillList() {
|
||
const list = document.getElementById('skillList');
|
||
try {
|
||
const res = await fetch(API + '/admin/skills');
|
||
const skills = await res.json();
|
||
if (!skills.length) {
|
||
list.innerHTML = '<div class="empty">אין skills מותקנים</div>';
|
||
return;
|
||
}
|
||
list.innerHTML = skills.map(s => {
|
||
const fileCount = s.file_inventory ? s.file_inventory.length : 0;
|
||
const updatedStr = s.updated_at ? new Date(s.updated_at).toLocaleDateString('he-IL') : '—';
|
||
const inDb = s.db_markdown_chars > 0;
|
||
const onDisk = s.disk_exists;
|
||
// Action buttons
|
||
const actions = [];
|
||
if (onDisk && !inDb) {
|
||
actions.push(`<button class="btn btn-sm btn-primary" onclick="syncSkill('${esc(s.slug)}')">Sync to DB</button>`);
|
||
} else if (onDisk && inDb) {
|
||
actions.push(`<button class="btn btn-sm btn-secondary" onclick="syncSkill('${esc(s.slug)}')">Re-sync</button>`);
|
||
}
|
||
if (inDb) {
|
||
actions.push(`<button class="btn btn-sm btn-outline" style="color:#e94560;border-color:#e94560" onclick="deleteSkill('${esc(s.slug)}')">Delete from DB</button>`);
|
||
}
|
||
return `
|
||
<div class="skill-item">
|
||
<span class="skill-icon">🔌</span>
|
||
<div class="skill-info">
|
||
<div class="skill-name">${esc(s.name)}</div>
|
||
<div class="skill-meta">
|
||
<span>${fileCount} files</span>
|
||
<span>${s.db_markdown_chars.toLocaleString()} chars</span>
|
||
<span>Updated: ${updatedStr}</span>
|
||
${s.disk_skill_md_bytes ? `<span>Disk: ${formatSize(s.disk_skill_md_bytes)}</span>` : ''}
|
||
</div>
|
||
</div>
|
||
<div class="skill-badges">
|
||
${inDb ? '<span class="badge-ok">DB</span>' : '<span class="badge-warn">No DB</span>'}
|
||
${onDisk ? '<span class="badge-ok">Disk</span>' : '<span class="badge-warn">No Disk</span>'}
|
||
</div>
|
||
<div style="display:flex;gap:6px;flex-shrink:0">${actions.join('')}</div>
|
||
</div>`;
|
||
}).join('');
|
||
} catch (e) {
|
||
list.innerHTML = '<div class="empty">שגיאה בטעינת skills</div>';
|
||
}
|
||
}
|
||
|
||
function setupSkillUpload() {
|
||
const dropZone = document.getElementById('skillDropZone');
|
||
const fileInput = document.getElementById('skillFileInput');
|
||
|
||
const newDrop = dropZone.cloneNode(true);
|
||
dropZone.parentNode.replaceChild(newDrop, dropZone);
|
||
const newInput = newDrop.querySelector('input[type="file"]');
|
||
|
||
newDrop.addEventListener('click', () => newInput.click());
|
||
newDrop.addEventListener('dragover', e => { e.preventDefault(); newDrop.classList.add('dragover'); });
|
||
newDrop.addEventListener('dragleave', () => newDrop.classList.remove('dragover'));
|
||
newDrop.addEventListener('drop', e => {
|
||
e.preventDefault();
|
||
newDrop.classList.remove('dragover');
|
||
if (e.dataTransfer.files.length) installSkill(e.dataTransfer.files[0]);
|
||
});
|
||
newInput.addEventListener('change', () => {
|
||
if (newInput.files.length) installSkill(newInput.files[0]);
|
||
newInput.value = '';
|
||
});
|
||
}
|
||
|
||
async function installSkill(file) {
|
||
if (!file.name.toLowerCase().endsWith('.zip')) {
|
||
return toast('Only ZIP files are supported', 'error');
|
||
}
|
||
|
||
const resultDiv = document.getElementById('skillInstallResult');
|
||
resultDiv.innerHTML = '<div class="skill-install-result">Installing...</div>';
|
||
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
|
||
try {
|
||
const res = await fetch(API + '/admin/skills/install', {
|
||
method: 'POST', body: formData,
|
||
});
|
||
const data = await res.json();
|
||
if (!res.ok) {
|
||
resultDiv.innerHTML = `<div class="skill-install-result error">${esc(data.detail || 'Installation failed')}</div>`;
|
||
toast('Installation failed', 'error');
|
||
return;
|
||
}
|
||
|
||
const actionLabel = data.action === 'updated' ? 'Updated' : 'Installed';
|
||
resultDiv.innerHTML = `
|
||
<div class="skill-install-result">
|
||
<b>${actionLabel}: ${esc(data.slug)}</b><br>
|
||
${data.files_extracted} files extracted · ${data.markdown_chars.toLocaleString()} chars<br><br>
|
||
<button class="btn btn-primary" onclick="confirmRestart()">Restart Paperclip</button>
|
||
<span style="font-size:0.82em;color:#888;margin-right:12px">Restart required to apply changes</span>
|
||
</div>`;
|
||
toast(actionLabel + ': ' + data.slug, 'success');
|
||
loadSkillList();
|
||
} catch (e) {
|
||
resultDiv.innerHTML = `<div class="skill-install-result error">Network error</div>`;
|
||
toast('Network error', 'error');
|
||
}
|
||
}
|
||
|
||
async function syncSkill(slug) {
|
||
try {
|
||
const res = await fetch(API + '/admin/skills/' + encodeURIComponent(slug) + '/sync', { method: 'POST' });
|
||
const data = await res.json();
|
||
if (!res.ok) {
|
||
toast(data.detail || 'Sync failed', 'error');
|
||
return;
|
||
}
|
||
toast(`${data.action}: ${slug} (${data.markdown_chars.toLocaleString()} chars, ${data.file_inventory.length} files)`, 'success');
|
||
loadSkillList();
|
||
} catch (e) {
|
||
toast('Network error', 'error');
|
||
}
|
||
}
|
||
|
||
async function deleteSkill(slug) {
|
||
if (!confirm(`Delete "${slug}" from DB? Files on disk will NOT be removed.`)) return;
|
||
try {
|
||
const res = await fetch(API + '/admin/skills/' + encodeURIComponent(slug), { method: 'DELETE' });
|
||
const data = await res.json();
|
||
if (!res.ok) {
|
||
toast(data.detail || 'Delete failed', 'error');
|
||
return;
|
||
}
|
||
toast(`Deleted: ${slug}`, 'success');
|
||
loadSkillList();
|
||
} catch (e) {
|
||
toast('Network error', 'error');
|
||
}
|
||
}
|
||
|
||
function confirmRestart() {
|
||
if (!confirm('Restart Paperclip?')) return;
|
||
restartPaperclip();
|
||
}
|
||
|
||
async function restartPaperclip() {
|
||
const btn = document.getElementById('restartPaperclipBtn');
|
||
const status = document.getElementById('restartStatus');
|
||
btn.disabled = true;
|
||
btn.textContent = 'Restarting...';
|
||
status.textContent = '';
|
||
|
||
try {
|
||
const res = await fetch(API + '/admin/paperclip/restart', { method: 'POST' });
|
||
const data = await res.json();
|
||
if (!res.ok) {
|
||
status.textContent = 'Error: ' + (data.detail || 'failed');
|
||
status.style.color = '#e94560';
|
||
toast('Restart failed', 'error');
|
||
} else {
|
||
status.textContent = 'Restarted successfully';
|
||
status.style.color = '#27ae60';
|
||
toast('Paperclip restarted', 'success');
|
||
}
|
||
} catch (e) {
|
||
status.textContent = 'Network error';
|
||
status.style.color = '#e94560';
|
||
toast('Network error', 'error');
|
||
} finally {
|
||
btn.disabled = false;
|
||
btn.textContent = 'Restart Paperclip';
|
||
}
|
||
}
|
||
|
||
// Init legacy upload listeners
|
||
setupLegacyUpload();
|
||
</script>
|
||
</body>
|
||
</html>
|