Merge agent1-ocr: OCR u ERP/CRM
This commit is contained in:
@@ -623,6 +623,33 @@ footer { height:36px; background:var(--bg2); border-top:1px solid var(--rim);
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ━━━ OCR floating button + modal ━━━ -->
|
||||
<button id="ocr-fab" onclick="ocrOpen()"
|
||||
style="position:fixed;right:18px;bottom:18px;z-index:60;
|
||||
background:#1f6feb;color:#fff;border:none;border-radius:24px;
|
||||
padding:10px 16px;font-size:13px;cursor:pointer;
|
||||
box-shadow:0 6px 18px rgba(0,0,0,0.4)">
|
||||
📷 OCR Upload
|
||||
</button>
|
||||
|
||||
<div id="ocr-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.55);z-index:80;align-items:center;justify-content:center">
|
||||
<div style="background:#0f1620;color:#dbe2ee;border:1px solid #25334a;border-radius:10px;width:min(720px,94vw);max-height:90vh;overflow:auto;padding:14px">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid #25334a;padding-bottom:8px;margin-bottom:10px">
|
||||
<h3 style="margin:0;font-size:14px">📷 OCR Upload (PDF / JPG / PNG)</h3>
|
||||
<button onclick="ocrClose()" style="background:none;border:none;color:#dbe2ee;font-size:18px;cursor:pointer">×</button>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
|
||||
<input type="file" id="ocr-crm-file" accept="application/pdf,image/jpeg,image/jpg,image/png">
|
||||
<button class="btn primary" onclick="ocrCrmUpload()">Upload</button>
|
||||
<button class="btn" onclick="ocrCrmHealth()">Health</button>
|
||||
<span id="ocr-crm-status" style="font-size:11px;color:#8aa0bd"></span>
|
||||
</div>
|
||||
<div id="ocr-crm-health" style="font-size:11px;color:#8aa0bd;margin-top:6px"></div>
|
||||
<div id="ocr-crm-fields" style="margin-top:10px;font-size:12px"></div>
|
||||
<pre id="ocr-crm-text" style="margin-top:10px;max-height:300px;overflow:auto;background:#0a1018;padding:10px;border-radius:6px;font-size:11px;white-space:pre-wrap">— prazno —</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="toast"></div>
|
||||
|
||||
<script>
|
||||
@@ -2077,6 +2104,59 @@ document.getElementById('modal').addEventListener('click', e => {
|
||||
if (e.target.id === 'modal') closeModal();
|
||||
});
|
||||
|
||||
// ────── OCR (lightweight /api/ocr) ──────
|
||||
const OCR_API = '/sport/api/ocr';
|
||||
|
||||
function ocrOpen(){ document.getElementById('ocr-modal').style.display = 'flex'; }
|
||||
function ocrClose(){ document.getElementById('ocr-modal').style.display = 'none'; }
|
||||
|
||||
async function ocrCrmHealth(){
|
||||
const out = document.getElementById('ocr-crm-health');
|
||||
if(out) out.textContent = '...checking';
|
||||
try {
|
||||
const r = await fetch(OCR_API + '/health');
|
||||
const j = await r.json();
|
||||
if(out){
|
||||
out.textContent = 'tesseract: ' + (j.tesseract_available ? 'OK' : 'NO') +
|
||||
' · pdf2image: ' + (j.pdf2image_available ? 'OK' : 'NO');
|
||||
}
|
||||
} catch(e){
|
||||
if(out) out.textContent = 'health err: ' + (e && e.message || e);
|
||||
}
|
||||
}
|
||||
|
||||
async function ocrCrmUpload(){
|
||||
const f = document.getElementById('ocr-crm-file').files[0];
|
||||
const stat = document.getElementById('ocr-crm-status');
|
||||
const fields = document.getElementById('ocr-crm-fields');
|
||||
const txt = document.getElementById('ocr-crm-text');
|
||||
if(!f){ if(stat) stat.textContent = 'odaberi datoteku'; return; }
|
||||
if(stat) stat.textContent = 'uploading…';
|
||||
const fd = new FormData();
|
||||
fd.append('file', f);
|
||||
try {
|
||||
const r = await fetch(OCR_API + '/upload', { method: 'POST', body: fd });
|
||||
const j = await r.json();
|
||||
if(!r.ok){ if(stat) stat.textContent = 'err ' + r.status; return; }
|
||||
const ex = j.extracted || {};
|
||||
fields.innerHTML =
|
||||
'<table style="width:100%;font-size:12px">'
|
||||
+ '<tr><th style="text-align:left;width:140px">vendor</th><td>'+(ex.vendor||'—')+'</td></tr>'
|
||||
+ '<tr><th style="text-align:left">OIB</th><td>'+(ex.oib||'—')+'</td></tr>'
|
||||
+ '<tr><th style="text-align:left">invoice_no</th><td>'+(ex.invoice_no||'—')+'</td></tr>'
|
||||
+ '<tr><th style="text-align:left">date</th><td>'+(ex.date||'—')+'</td></tr>'
|
||||
+ '<tr><th style="text-align:left">amount</th><td>'+(ex.amount==null?'—':ex.amount)+'</td></tr>'
|
||||
+ '<tr><th style="text-align:left">ocr_status</th><td>'+(j.ocr_status||'—')+'</td></tr>'
|
||||
+ '<tr><th style="text-align:left">confidence</th><td>'+(j.ocr_confidence==null?'—':j.ocr_confidence)+'</td></tr>'
|
||||
+ '<tr><th style="text-align:left">file</th><td>'+((j.file_name||'?')+' · '+(j.file_size||0)+' B')+'</td></tr>'
|
||||
+ '</table>';
|
||||
txt.textContent = j.ocr_text || '— (prazno / OCR nije izvršen) —';
|
||||
if(stat) stat.textContent = 'done · id=' + (j.id == null ? 'n/a' : j.id);
|
||||
} catch(e){
|
||||
if(stat) stat.textContent = 'err: ' + (e && e.message || e);
|
||||
}
|
||||
}
|
||||
|
||||
// ────── Init ──────
|
||||
loadMe();
|
||||
ensureMe();
|
||||
|
||||
+125
-1
@@ -118,6 +118,7 @@ table tbody tr:hover{background:var(--bg3)}
|
||||
<button class="tab" data-panel="partneri">🤝 Partneri</button>
|
||||
<button class="tab" data-panel="racuni">🧾 Računi</button>
|
||||
<button class="tab" data-panel="uploads">📎 Uploads (OCR)</button>
|
||||
<button class="tab" data-panel="ocr">📷 OCR</button>
|
||||
<button class="tab" data-panel="putni">✈ Putni nalozi</button>
|
||||
<button class="tab" data-panel="payments">💰 Plaćanja</button>
|
||||
<button class="tab" data-panel="pdv">% PDV</button>
|
||||
@@ -245,6 +246,54 @@ table tbody tr:hover{background:var(--bg3)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ OCR (Računi) — lightweight /api/ocr/upload ============ -->
|
||||
<section class="panel" id="panel-ocr">
|
||||
<div class="card">
|
||||
<div class="card-h">
|
||||
<div class="card-t">📷 OCR — Računi (Tesseract + regex extrakcija)</div>
|
||||
<div style="display:flex;gap:6px">
|
||||
<button class="btn" onclick="ocrHealth()">🩺 Health</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding:10px;border:2px dashed var(--rim2);border-radius:8px;background:var(--bg3);margin-bottom:10px">
|
||||
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
|
||||
<input type="file" id="ocr-file" accept="application/pdf,image/jpeg,image/jpg,image/png">
|
||||
<button class="btn primary" onclick="ocrUpload()">⬆ Upload</button>
|
||||
<span id="ocr-status" style="color:var(--t2);font-size:11px"></span>
|
||||
</div>
|
||||
<div id="ocr-health" style="margin-top:6px;font-size:11px;color:var(--t1)"></div>
|
||||
</div>
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
|
||||
<div class="card">
|
||||
<div class="card-h"><div class="card-t">Ekstrahirana polja</div></div>
|
||||
<div class="tbl-wrap" style="padding:6px">
|
||||
<table id="ocr-fields"><tbody>
|
||||
<tr><th style="text-align:left;width:140px">vendor</th><td id="ocr-vendor">—</td></tr>
|
||||
<tr><th style="text-align:left">OIB</th><td id="ocr-oib">—</td></tr>
|
||||
<tr><th style="text-align:left">invoice_no</th><td id="ocr-invno">—</td></tr>
|
||||
<tr><th style="text-align:left">date</th><td id="ocr-date">—</td></tr>
|
||||
<tr><th style="text-align:left">amount</th><td id="ocr-amount">—</td></tr>
|
||||
<tr><th style="text-align:left">ocr_status</th><td id="ocr-ostatus">—</td></tr>
|
||||
<tr><th style="text-align:left">confidence</th><td id="ocr-conf">—</td></tr>
|
||||
<tr><th style="text-align:left">file</th><td id="ocr-file-info">—</td></tr>
|
||||
</tbody></table>
|
||||
</div>
|
||||
<div style="padding:10px;display:flex;gap:8px">
|
||||
<!-- TODO: stvarna integracija sa pgz_sport.racuni_ulazni (real save) -->
|
||||
<button class="btn primary" onclick="ocrSaveRacun()">💾 Spremi u racuni_ulazni</button>
|
||||
<button class="btn" onclick="ocrReset()">↺ Reset</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-h"><div class="card-t">Prepoznati tekst (OCR)</div></div>
|
||||
<pre id="ocr-text" style="white-space:pre-wrap;max-height:420px;overflow:auto;padding:10px;font-size:11px;background:var(--bg2);color:var(--t1);margin:0">— prazno —</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ PUTNI NALOZI / EXPENSE REPORTS ============ -->
|
||||
<section class="panel" id="panel-putni">
|
||||
<div class="card">
|
||||
@@ -1187,6 +1236,80 @@ function exportPdf(report, godina){
|
||||
window.open(API+'/export/pdf/'+report+'?godina='+godina, '_blank');
|
||||
}
|
||||
|
||||
// ===== OCR (lightweight /api/ocr) =====
|
||||
const OCR_API = '/sport/api/ocr';
|
||||
let _ocrLast = null;
|
||||
|
||||
function _ocrSet(id, val){
|
||||
const el = document.getElementById(id);
|
||||
if(el) el.textContent = (val === null || val === undefined || val === '') ? '—' : String(val);
|
||||
}
|
||||
|
||||
async function ocrHealth(){
|
||||
const out = document.getElementById('ocr-health');
|
||||
if(out) out.textContent = '...checking';
|
||||
try {
|
||||
const r = await fetch(OCR_API + '/health');
|
||||
const j = await r.json();
|
||||
if(out){
|
||||
out.textContent = 'tesseract: ' + (j.tesseract_available ? 'OK' : 'NO') +
|
||||
' · pdf2image: ' + (j.pdf2image_available ? 'OK' : 'NO') +
|
||||
' · upload_dir: ' + (j.upload_dir || '?');
|
||||
}
|
||||
} catch(e){
|
||||
if(out) out.textContent = 'health err: ' + (e && e.message || e);
|
||||
}
|
||||
}
|
||||
|
||||
async function ocrUpload(){
|
||||
const f = document.getElementById('ocr-file').files[0];
|
||||
const stat = document.getElementById('ocr-status');
|
||||
if(!f){ if(stat) stat.textContent = 'odaberi datoteku'; return; }
|
||||
if(stat) stat.textContent = 'uploading…';
|
||||
const fd = new FormData();
|
||||
fd.append('file', f);
|
||||
try {
|
||||
const r = await fetch(OCR_API + '/upload', { method: 'POST', body: fd });
|
||||
const j = await r.json();
|
||||
if(!r.ok){
|
||||
if(stat) stat.textContent = 'err ' + r.status + ': ' + (j && j.detail || '');
|
||||
return;
|
||||
}
|
||||
_ocrLast = j;
|
||||
const ex = j.extracted || {};
|
||||
_ocrSet('ocr-vendor', ex.vendor);
|
||||
_ocrSet('ocr-oib', ex.oib);
|
||||
_ocrSet('ocr-invno', ex.invoice_no);
|
||||
_ocrSet('ocr-date', ex.date);
|
||||
_ocrSet('ocr-amount', ex.amount);
|
||||
_ocrSet('ocr-ostatus', j.ocr_status);
|
||||
_ocrSet('ocr-conf', j.ocr_confidence);
|
||||
_ocrSet('ocr-file-info', (j.file_name || '?') + ' · ' + (j.file_size||0) + ' B · ' + (j.mime||'?'));
|
||||
const txt = document.getElementById('ocr-text');
|
||||
if(txt) txt.textContent = j.ocr_text || '— (prazno / OCR nije izvršen) —';
|
||||
if(stat) stat.textContent = 'done · id=' + (j.id == null ? 'n/a' : j.id);
|
||||
} catch(e){
|
||||
if(stat) stat.textContent = 'err: ' + (e && e.message || e);
|
||||
}
|
||||
}
|
||||
|
||||
function ocrReset(){
|
||||
['ocr-vendor','ocr-oib','ocr-invno','ocr-date','ocr-amount','ocr-ostatus','ocr-conf','ocr-file-info'].forEach(id => _ocrSet(id, null));
|
||||
const txt = document.getElementById('ocr-text');
|
||||
if(txt) txt.textContent = '— prazno —';
|
||||
const stat = document.getElementById('ocr-status');
|
||||
if(stat) stat.textContent = '';
|
||||
_ocrLast = null;
|
||||
}
|
||||
|
||||
function ocrSaveRacun(){
|
||||
// TODO: stvarna integracija sa pgz_sport.racuni_ulazni (real save) — wire later
|
||||
if(!_ocrLast){ alert('Nema OCR podatka. Prvo uploadaj račun.'); return; }
|
||||
alert('TODO: spremi u racuni_ulazni\nfile_path: ' + (_ocrLast.file_path || '?') +
|
||||
'\nvendor: ' + ((_ocrLast.extracted||{}).vendor || '?') +
|
||||
'\namount: ' + ((_ocrLast.extracted||{}).amount || '?'));
|
||||
}
|
||||
|
||||
// Lazy loaders per panel
|
||||
const loaders = {
|
||||
dnevnik: loadDnevnik,
|
||||
@@ -1200,7 +1323,8 @@ const loaders = {
|
||||
place: () => { loadZap(); loadPlace(); },
|
||||
proracun: loadProracun,
|
||||
izvjestaji: loadIzvjestaj,
|
||||
kontni: loadKontniPlan
|
||||
kontni: loadKontniPlan,
|
||||
ocr: ocrHealth
|
||||
};
|
||||
|
||||
// Switch programmatically (used by deep links: ?tab=uploads / #tab=putni)
|
||||
|
||||
Reference in New Issue
Block a user