Add settings page for tag-to-company mappings and auto-create Paperclip projects
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m22s

When a case is created, a Paperclip project is now automatically created in
the correct company based on the appeal_subtype tag. Tag-to-company mappings
are managed via a new Settings page that pulls companies from Paperclip DB.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-14 06:24:23 +00:00
parent 140a2e442d
commit 2faae002e7
4 changed files with 320 additions and 13 deletions

View File

@@ -1881,6 +1881,7 @@ kbd {
<a href="#/compose" id="navCompose">כתיבה</a>
<a href="#/skills" id="navSkills">Skills</a>
<a href="#/diagnostics" id="navDiagnostics">מצב מערכת</a>
<a href="#/settings" id="navSettings">הגדרות</a>
<button class="theme-toggle" id="themeToggle" onclick="toggleTheme()" title="החלף ערכת צבעים (Shift+D)">
<span id="themeIcon">🌙</span>
</button>
@@ -2405,6 +2406,45 @@ kbd {
<div class="empty">טוען...</div>
</div>
</div>
<!-- ══ Page: Settings ══ -->
<div class="page" id="page-settings">
<div class="page-header">
<h2>הגדרות</h2>
</div>
<!-- Tag → Company Mappings -->
<div class="card">
<div class="card-header">שיוך תגי תיקים לחברות Paperclip</div>
<div class="card-body">
<p style="color:#888;font-size:0.85em;margin-bottom:16px">כל תג ערר (סוג תיק) משויך לחברה ב-Paperclip. כשנפתח תיק חדש, הפרויקט נוצר אוטומטית בחברה המתאימה לפי התג.</p>
<!-- Add new mapping -->
<div style="display:flex;gap:12px;align-items:flex-end;flex-wrap:wrap;margin-bottom:24px;padding:16px;background:var(--bg-secondary,#f8f9fa);border-radius:8px">
<div style="flex:1;min-width:160px">
<label style="display:block;font-size:0.8em;color:#888;margin-bottom:4px">תג ערר</label>
<input type="text" id="settingsNewTag" placeholder="לדוגמה: building_permit" style="width:100%;padding:8px;border:1px solid var(--border,#ddd);border-radius:6px;background:var(--bg-primary,#fff);color:var(--text-primary)">
</div>
<div style="flex:1;min-width:160px">
<label style="display:block;font-size:0.8em;color:#888;margin-bottom:4px">תיאור בעברית</label>
<input type="text" id="settingsNewTagLabel" placeholder="לדוגמה: רישוי ובנייה" style="width:100%;padding:8px;border:1px solid var(--border,#ddd);border-radius:6px;background:var(--bg-primary,#fff);color:var(--text-primary)">
</div>
<div style="flex:1.5;min-width:200px">
<label style="display:block;font-size:0.8em;color:#888;margin-bottom:4px">חברה ב-Paperclip</label>
<select id="settingsCompanySelect" style="width:100%;padding:8px;border:1px solid var(--border,#ddd);border-radius:6px;background:var(--bg-primary,#fff);color:var(--text-primary)">
<option value="">טוען חברות...</option>
</select>
</div>
<button class="btn btn-primary" onclick="addTagMapping()" style="white-space:nowrap">הוסף שיוך</button>
</div>
<!-- Existing mappings table -->
<div id="tagMappingsTable">
<div class="empty">טוען...</div>
</div>
</div>
</div>
</div>
</div>
<!-- Modal for pattern examples -->
@@ -2514,6 +2554,11 @@ function handleRoute() {
document.getElementById('navCompose').classList.add('active');
subtitle = 'כתיבת החלטה';
initComposePage();
} else if (hash === '#/settings') {
document.getElementById('page-settings').classList.add('active');
document.getElementById('navSettings').classList.add('active');
subtitle = 'הגדרות';
loadSettingsPage();
}
document.getElementById('pageSubtitle').textContent = subtitle;
@@ -4974,6 +5019,163 @@ async function loadCorpusList() {
container.innerHTML = `<div class="empty">שגיאה בטעינה: ${esc(e.message)}</div>`;
}
}
// ── Settings Page ─────────────────────────────────────────────────
let _settingsCompanies = [];
async function loadSettingsPage() {
await Promise.all([loadPaperclipCompanies(), loadTagMappings()]);
}
async function loadPaperclipCompanies() {
const sel = document.getElementById('settingsCompanySelect');
try {
const res = await fetch(`${API}/settings/paperclip-companies`);
if (!res.ok) throw new Error(await res.text());
_settingsCompanies = await res.json();
// Build options safely via DOM
sel.textContent = '';
const defaultOpt = document.createElement('option');
defaultOpt.value = '';
defaultOpt.textContent = '— בחר חברה —';
sel.appendChild(defaultOpt);
for (const c of _settingsCompanies) {
const opt = document.createElement('option');
opt.value = c.id;
opt.dataset.name = c.name;
opt.textContent = c.name + (c.identifier ? ` (${c.identifier})` : '');
sel.appendChild(opt);
}
} catch (e) {
sel.textContent = '';
const opt = document.createElement('option');
opt.value = '';
opt.textContent = 'שגיאה: ' + e.message;
sel.appendChild(opt);
}
}
async function loadTagMappings() {
const container = document.getElementById('tagMappingsTable');
try {
const res = await fetch(`${API}/settings/tag-mappings`);
if (!res.ok) throw new Error(await res.text());
const mappings = await res.json();
if (!mappings.length) {
container.textContent = '';
const empty = document.createElement('div');
empty.className = 'empty';
empty.textContent = 'אין שיוכים מוגדרים עדיין';
container.appendChild(empty);
return;
}
// Group by company
const byCompany = {};
for (const m of mappings) {
const key = m.company_id;
if (!byCompany[key]) byCompany[key] = { company_name: m.company_name || m.company_id, tags: [] };
byCompany[key].tags.push(m);
}
const table = document.createElement('table');
table.style.cssText = 'width:100%;border-collapse:collapse;font-size:0.9em';
const thead = table.createTHead();
const headerRow = thead.insertRow();
headerRow.style.borderBottom = '2px solid var(--border,#ddd)';
for (const label of ['Company', 'Tag', 'Label', '']) {
const th = document.createElement('th');
th.style.cssText = 'text-align:right;padding:8px';
if (label === '') th.style.width = '60px';
th.textContent = label;
headerRow.appendChild(th);
}
const tbody = table.createTBody();
for (const [companyId, group] of Object.entries(byCompany)) {
for (let i = 0; i < group.tags.length; i++) {
const m = group.tags[i];
const tr = tbody.insertRow();
tr.style.borderBottom = '1px solid var(--border,#eee)';
if (i === 0) {
const tdCompany = tr.insertCell();
tdCompany.style.cssText = 'padding:8px;font-weight:600;vertical-align:top';
tdCompany.rowSpan = group.tags.length;
tdCompany.textContent = group.company_name;
}
const tdTag = tr.insertCell();
tdTag.style.padding = '8px';
const code = document.createElement('code');
code.style.cssText = 'background:var(--bg-secondary,#f0f0f0);padding:2px 6px;border-radius:4px';
code.textContent = m.tag;
tdTag.appendChild(code);
const tdLabel = tr.insertCell();
tdLabel.style.padding = '8px';
tdLabel.textContent = m.tag_label || '—';
const tdAction = tr.insertCell();
tdAction.style.padding = '8px';
const btn = document.createElement('button');
btn.className = 'btn-icon btn-icon-danger';
btn.title = 'הסר שיוך';
btn.textContent = '✕';
btn.addEventListener('click', () => deleteTagMapping(m.id));
tdAction.appendChild(btn);
}
}
container.textContent = '';
container.appendChild(table);
} catch (e) {
container.textContent = '';
const empty = document.createElement('div');
empty.className = 'empty';
empty.textContent = 'שגיאה: ' + e.message;
container.appendChild(empty);
}
}
async function addTagMapping() {
const tag = document.getElementById('settingsNewTag').value.trim();
const tagLabel = document.getElementById('settingsNewTagLabel').value.trim();
const sel = document.getElementById('settingsCompanySelect');
const companyId = sel.value;
const companyName = sel.selectedOptions[0]?.dataset?.name || '';
if (!tag) { showToast('יש להזין תג', 'error'); return; }
if (!companyId) { showToast('יש לבחור חברה', 'error'); return; }
try {
const res = await fetch(`${API}/settings/tag-mappings`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tag, tag_label: tagLabel, company_id: companyId, company_name: companyName }),
});
if (!res.ok) throw new Error(await res.text());
showToast('שיוך נוסף בהצלחה');
document.getElementById('settingsNewTag').value = '';
document.getElementById('settingsNewTagLabel').value = '';
sel.value = '';
await loadTagMappings();
} catch (e) {
showToast('שגיאה: ' + e.message, 'error');
}
}
async function deleteTagMapping(id) {
if (!confirm('להסיר שיוך זה?')) return;
try {
const res = await fetch(`${API}/settings/tag-mappings/${id}`, { method: 'DELETE' });
if (!res.ok) throw new Error(await res.text());
showToast('שיוך הוסר');
await loadTagMappings();
} catch (e) {
showToast('שגיאה: ' + e.message, 'error');
}
}
</script>
</body>
</html>