Task 1: OCR u ERP/CRM — /api/ocr/upload + tab Računi (OCR)

- routers/ocr_router.py: POST /api/ocr/upload (Tesseract+pdf2image, regex field extraction)
- pgz_sport_api.py: mount ocr_router with try/except guard
- static/erp_full.html: nova tab "📷 OCR" + panel
- static/crm_v2.html: OCR upload modal/tab

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Damir Radulić
2026-05-05 18:28:22 +02:00
parent 8127e2ef22
commit f488623920
4 changed files with 615 additions and 1 deletions
+80
View File
@@ -484,6 +484,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>
@@ -1681,6 +1708,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();