PDF link target=_blank + nginx timeouts + priority filteri (samo s podacima)

nginx (sport.rinet.one):
- proxy_read_timeout 60s → 300s
- proxy_send_timeout 300s
- proxy_buffering off (PDF stream)
- client_max_body_size 50M → 100M

Endpoints:
- /api/v2/klubovi/financirani: +with_data filter (samo s potporama/godišnjakom/HNS)
- /api/v2/sportasi/filtered: +samo_priority +samo_s_hns

Frontend:
- PDF link target=_blank rel=noopener
- window._klub_only_priority = true (default)
- window._sportas_only_priority = true (default)

DB View:
- pgz_sport.v_nogomet_priority (prima_potpore, u_godisnjaku, ima_hns_roster)
This commit is contained in:
2026-05-05 13:51:07 +02:00
parent c6a5ec62aa
commit f7b5114f58
289 changed files with 37204 additions and 363 deletions
+79 -5
View File
@@ -222,14 +222,24 @@ table tbody tr:hover{background:var(--bg3)}
<!-- ============ INVOICE UPLOADS (OCR) ============ -->
<section class="panel" id="panel-uploads">
<div class="card">
<div class="card-h"><div class="card-t">Invoice Uploads (OCR/AI extraction)</div></div>
<div class="card-h">
<div class="card-t">Računi (OCR) — Invoice Uploads / AI extraction</div>
<div style="display:flex;gap:6px">
<button class="btn primary" onclick="document.getElementById('up-file').click()">📎 Upload novi račun</button>
<input type="file" id="up-file" accept="application/pdf,image/*" style="display:none" onchange="uploadInvoiceFile(this.files[0])">
</div>
</div>
<div id="up-drop" style="border:2px dashed var(--rim2);border-radius:8px;padding:18px;text-align:center;color:var(--t2);margin-bottom:10px;background:var(--bg3)">
Dovuci PDF / JPG / PNG ovdje (max 25 MB) ili koristi gumb gore.
<div id="up-progress" style="margin-top:6px;font-size:11px;color:var(--t1)"></div>
</div>
<div class="toolbar">
<input id="up-q" placeholder="Datoteka / vendor / broj…">
<label>Status <select id="up-status"><option value="">— svi —</option><option value="pending">pending</option><option value="ocr_done">ocr_done</option><option value="approved">approved</option><option value="rejected">rejected</option></select></label>
<button class="btn" onclick="loadUploads()">Osvježi</button>
</div>
<div class="tbl-wrap">
<table id="up-tbl"><thead><tr><th>#</th><th>Datoteka</th><th class="num">Veličina</th><th>Vendor</th><th>OIB</th><th>Br. računa</th><th>Datum</th><th class="num">Brutto</th><th>OCR status</th><th class="num">Conf</th><th>Račun</th></tr></thead><tbody><tr><td colspan="11" style="color:var(--t2);text-align:center;padding:18px">Klikni "Osvježi"…</td></tr></tbody></table>
<table id="up-tbl"><thead><tr><th>#</th><th>Datoteka</th><th class="num">Veličina</th><th>Vendor</th><th>OIB</th><th>Br. računa</th><th>Datum</th><th class="num">Brutto</th><th>OCR status</th><th class="num">Conf</th><th>Račun</th><th>Akcije</th></tr></thead><tbody><tr><td colspan="12" style="color:var(--t2);text-align:center;padding:18px">Klikni "Osvježi"…</td></tr></tbody></table>
</div>
</div>
</section>
@@ -973,7 +983,7 @@ async function loadProracun(){
// ===== INVOICE UPLOADS =====
async function loadUploads(){
const tbody = document.querySelector('#up-tbl tbody');
tbody.innerHTML = `<tr><td colspan="11" style="color:var(--t2);text-align:center;padding:14px">Učitavam…</td></tr>`;
tbody.innerHTML = `<tr><td colspan="12" style="color:var(--t2);text-align:center;padding:14px">Učitavam…</td></tr>`;
try {
const q = (document.getElementById('up-q').value||'').trim();
const st = document.getElementById('up-status').value;
@@ -994,13 +1004,55 @@ async function loadUploads(){
<td><span class="badge ${r.ocr_status||''}">${r.ocr_status||'—'}</span></td>
<td class="num">${r.ocr_confidence!=null?fmt(r.ocr_confidence)+' %':''}</td>
<td>${r.invoice_id?('#'+r.invoice_id):'—'}</td>
<td><a class="btn sec" href="/uploads/${r.file_path||''}" target="_blank">Otvori</a></td>
</tr>`).join('')
: `<tr><td colspan="11" style="color:var(--t2);text-align:center;padding:14px">Nema uploadova.</td></tr>`;
: `<tr><td colspan="12" style="color:var(--t2);text-align:center;padding:14px">Nema uploadova.</td></tr>`;
} catch(e) {
tbody.innerHTML = `<tr><td colspan="11" style="color:var(--red)">Greška: ${e.message}</td></tr>`;
tbody.innerHTML = `<tr><td colspan="12" style="color:var(--red)">Greška: ${e.message}</td></tr>`;
}
}
// Upload new invoice file via multipart
async function uploadInvoiceFile(file){
if(!file) return;
const prog = document.getElementById('up-progress');
prog.textContent = 'Šaljem ' + file.name + ' (' + Math.round(file.size/1024) + ' KB)…';
try {
const fd = new FormData();
fd.append('file', file);
// Note: no Content-Type header — browser sets multipart boundary
const r = await fetch(API + '/invoice-uploads', {
method:'POST', body: fd, headers: AUTH()
});
if(!r.ok) throw new Error('HTTP ' + r.status + ' ' + (await r.text()));
const j = await r.json();
prog.textContent = '✓ Uploaded #' + j.id + ' (' + Math.round((j.file_size||0)/1024) + ' KB) — OCR pending.';
document.getElementById('up-file').value = '';
loadUploads();
} catch(e) {
prog.textContent = '✗ Greška: ' + e.message;
}
}
// Drag & drop on uploads card
document.addEventListener('DOMContentLoaded', () => {
const drop = document.getElementById('up-drop');
if(!drop) return;
['dragenter','dragover'].forEach(ev => drop.addEventListener(ev, e => {
e.preventDefault(); e.stopPropagation();
drop.style.background='var(--bg2)';
}));
['dragleave','drop'].forEach(ev => drop.addEventListener(ev, e => {
e.preventDefault(); e.stopPropagation();
drop.style.background='var(--bg3)';
}));
drop.addEventListener('drop', e => {
if(e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0]){
uploadInvoiceFile(e.dataTransfer.files[0]);
}
});
});
// ===== PUTNI NALOZI / EXPENSE REPORTS =====
async function loadExpenseReports(){
const tbody = document.querySelector('#pn-tbl tbody');
@@ -1148,10 +1200,32 @@ const loaders = {
kontni: loadKontniPlan
};
// Switch programmatically (used by deep links: ?tab=uploads / #tab=putni)
function activateTab(panelId){
const t = document.querySelector('.tab[data-panel="' + panelId + '"]');
if(!t) return false;
t.click();
return true;
}
// Initial
document.addEventListener('DOMContentLoaded', () => {
loadKontoCache();
loadPartnerCache();
// Deep-link support: ?tab=<panel> or #tab=<panel>
let target = null;
try {
const u = new URL(window.location.href);
target = u.searchParams.get('tab');
if(!target && u.hash){
const m = u.hash.match(/tab=([a-z]+)/i);
if(m) target = m[1];
}
} catch(e) {}
if(target && activateTab(target)){
// tab.click() already triggers loader
return;
}
loadDnevnik();
});
</script>