M12.2 UI: enrichment diff modal + apply button (sport2.html)

- enrichEntity() now renders {current, proposed} as a diff table with a
  checkbox per field (defaults to checked).
- 'Označi sve' / 'Poništi sve' / '💾 Spremi izmjene' buttons.
- enrichApply() POSTs selected fields to /v2/enrich/{kind}/{id}/apply
  with the cached source list, then refreshes the entity panel and
  re-runs preview so the now-saved values are visible inline.
- Toast '✓ Spremljeno N polja u bazu' confirms the write.
- '✓ Obogaćeno YYYY-MM-DD' badge surfaces metadata.enriched_at.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
CC6 Worker
2026-05-05 00:17:52 +02:00
parent fbbe953de3
commit cef4d2575b
+107 -15
View File
@@ -357,34 +357,78 @@ async function apiPost(path, body){
}
}
// Cache the latest preview so /apply can pass back the same sources
window._enrichPreviews = window._enrichPreviews || {};
async function enrichEntity(kind, id){
const targetId = 'enrich-out-'+kind+'-'+id;
const target = document.getElementById(targetId);
if(target) target.innerHTML = '<div class="loading">⏳ Obogaćivanje u tijeku — pretraživanje izvora…</div>';
const r = await apiPost('/v2/enrich/'+kind+'/'+id);
if(!r){ if(target) target.innerHTML = '<div class="empty">Greška pri obogaćivanju</div>'; return; }
window._enrichPreviews[kind+':'+id] = r;
const cov = r.coverage||0;
const covCls = cov>=70?'high':(cov>=40?'mid':'low');
const proposed = r.proposed || {};
const current = r.current || {};
const propKeys = Object.keys(proposed);
const lastEnr = r.last_enriched_at
? `<span class="tb-s" title="${esc(r.last_enriched_at)}">✓ Obogaćeno ${esc(String(r.last_enriched_at).slice(0,10))}</span>`
: '';
let diffHtml = '';
if(propKeys.length){
const rows = propKeys.map(k => {
const cv = current[k]; const pv = proposed[k];
const cvHtml = cv
? '<span style="color:var(--t1)">'+esc(String(cv).slice(0,200))+'</span>'
: '<span class="tag rd">prazno</span>';
const pvHtml = '<span style="color:var(--ok)">'+esc(String(pv).slice(0,400))+'</span>';
return `<tr>
<td style="vertical-align:top;padding:6px 8px"><label style="display:flex;gap:6px;align-items:center;cursor:pointer">
<input type="checkbox" data-field="${esc(k)}" checked style="width:16px;height:16px"> <b style="font-family:monospace;font-size:12px">${esc(k)}</b>
</label></td>
<td style="vertical-align:top;padding:6px 8px;font-size:12px;max-width:240px">${cvHtml}</td>
<td style="vertical-align:top;padding:6px 8px;font-size:12px">${pvHtml}</td>
</tr>`;
}).join('');
diffHtml = `
<div style="margin:10px 0;border:1px solid var(--ln);border-radius:6px;overflow:hidden">
<div style="padding:8px 10px;background:var(--bg3);font-size:11px;color:var(--t4);text-transform:uppercase;letter-spacing:.5px">📋 Predložene izmjene (uncheck za preskočiti)</div>
<table style="width:100%;border-collapse:collapse;font-size:12px">
<thead><tr style="background:var(--bg2)"><th style="text-align:left;padding:6px 8px;width:160px">Polje</th><th style="text-align:left;padding:6px 8px;width:240px">Trenutno</th><th style="text-align:left;padding:6px 8px">Predloženo</th></tr></thead>
<tbody id="enrich-diff-${kind}-${id}">${rows}</tbody>
</table>
<div style="padding:8px 10px;background:var(--bg2);display:flex;gap:8px;justify-content:flex-end">
<button class="btn" onclick="enrichSelectAll('${kind}',${id},true)">Označi sve</button>
<button class="btn" onclick="enrichSelectAll('${kind}',${id},false)">Poništi sve</button>
<button class="btn primary" onclick="enrichApply('${kind}',${id})">💾 Spremi izmjene</button>
</div>
</div>`;
} else {
diffHtml = '<div class="empty" style="padding:14px">Nema novih predloženih dopuna iz vanjskih izvora.</div>';
}
const sourcesHtml = (r.sources||[]).map(s => `
<div style="padding:8px 10px;background:var(--bg3);border-left:3px solid var(--pgz-gold);border-radius:4px;margin-bottom:6px">
<div style="font-size:11px;color:var(--t4);text-transform:uppercase;letter-spacing:.5px">${esc(s.source||'')}</div>
${s.title ? '<div style="font-weight:700;color:var(--t0);font-size:13px">'+esc(s.title)+'</div>' : ''}
${s.extract ? '<div style="font-size:11.5px;color:var(--t1);line-height:1.5">'+esc(String(s.extract).slice(0,300))+'…</div>' : ''}
${s.url ? '<div style="margin-top:4px"><a href="'+esc(s.url)+'" target="_blank" style="font-size:11px">↗ '+esc(String(s.url).slice(0,90))+'</a></div>' : ''}
</div>`).join('');
const html = `
<div style="display:flex;gap:8px;align-items:center;margin-bottom:10px">
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:10px">
<span class="tag gr">🟢 OBOGAĆENO</span>
<span class="score ${covCls}">Coverage ${cov}%</span>
<span class="tb-s">${r.filled_fields}/${r.total_fields} polja popunjeno</span>
${lastEnr}
</div>
${r.live_snippet && r.live_snippet.title ? `
<div style="padding:10px;background:var(--bg3);border-left:3px solid var(--pgz-gold);border-radius:5px;margin-bottom:10px">
<div style="font-size:11px;color:var(--t4);text-transform:uppercase;letter-spacing:.5px;margin-bottom:4px">📡 Live snippet</div>
<div style="font-weight:700;color:var(--t0);font-size:13px;margin-bottom:4px">${esc(r.live_snippet.title)}</div>
${r.live_snippet.description ? '<div style="font-size:11.5px;color:var(--t1);line-height:1.5">'+esc(r.live_snippet.description)+'</div>' : ''}
<div style="margin-top:6px"><a href="${esc(r.live_snippet.url)}" target="_blank">↗ ${esc(r.live_snippet.url.slice(0,80))}</a></div>
</div>
` : ''}
${r.missing_fields && r.missing_fields.length ? `
<div style="margin-bottom:10px">
<div style="font-size:11px;color:var(--t2);margin-bottom:4px">Nedostaje:</div>
<div>${r.missing_fields.map(f=>'<span class="tag rd">'+esc(f)+'</span>').join('')}</div>
</div>
` : ''}
${diffHtml}
${sourcesHtml ? `<div style="margin:10px 0">
<div style="font-size:11px;color:var(--t4);text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px">🔗 Izvori</div>
${sourcesHtml}
</div>` : ''}
<div>
<div style="font-size:11px;color:var(--t2);margin-bottom:6px">🔍 Istraži dalje:</div>
<div style="display:flex;flex-wrap:wrap;gap:6px">
@@ -395,6 +439,54 @@ async function enrichEntity(kind, id){
if(target) target.innerHTML = html;
}
function enrichSelectAll(kind, id, on){
const tbody = document.getElementById('enrich-diff-'+kind+'-'+id);
if(!tbody) return;
tbody.querySelectorAll('input[type=checkbox]').forEach(cb => { cb.checked = !!on; });
}
async function enrichApply(kind, id){
const target = document.getElementById('enrich-out-'+kind+'-'+id);
const tbody = document.getElementById('enrich-diff-'+kind+'-'+id);
const preview = (window._enrichPreviews||{})[kind+':'+id];
if(!preview){ alert('Prvo pokreni "▶ Pokreni"'); return; }
const proposed = preview.proposed || {};
const fields = {};
if(tbody){
tbody.querySelectorAll('input[type=checkbox]:checked').forEach(cb => {
const f = cb.getAttribute('data-field');
if(f && proposed[f] !== undefined) fields[f] = proposed[f];
});
} else {
Object.assign(fields, proposed);
}
if(!Object.keys(fields).length){ alert('Označi barem jedno polje za primjenu.'); return; }
if(target) target.innerHTML = '<div class="loading">⏳ Spremam u bazu…</div>';
try{
const r = await fetch(API+'/v2/enrich/'+kind+'/'+id+'/apply', {
method:'POST',
headers:{'Content-Type':'application/json'},
body: JSON.stringify({fields, sources: preview.sources || []}),
});
const data = await r.json();
if(!r.ok){ throw new Error(data.detail || ('HTTP '+r.status)); }
// Refresh the entire detail panel so the new values render
if(kind === 'klub' && typeof openKlub === 'function') await openKlub(id);
else if(kind === 'savez' && typeof openSavez === 'function') await openSavez(id);
else if(kind === 'sportas' && typeof openSportas === 'function') await openSportas(id);
setTimeout(() => enrichEntity(kind, id), 350);
const cnt = Object.keys(data.applied||{}).length;
const t = document.createElement('div');
t.style.cssText = 'position:fixed;bottom:20px;right:20px;background:var(--ok,#1ec773);color:#0b1a16;padding:10px 16px;border-radius:6px;font-weight:700;z-index:9999;box-shadow:0 4px 16px rgba(0,0,0,.4)';
t.textContent = '✓ Spremljeno '+cnt+' polja u bazu';
document.body.appendChild(t);
setTimeout(()=>t.remove(), 3500);
}catch(e){
console.error(e);
if(target) target.innerHTML = '<div class="empty" style="color:var(--bad,#ff6b6b)">Greška pri spremanju: '+esc(e.message||String(e))+'</div>';
}
}
function enrichBlock(kind, id){
return `
<div class="card" id="enrich-card-${kind}-${id}">