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:
+238
-49
@@ -512,8 +512,10 @@ const NAV_BY_ROLE = {
|
||||
{id:'erp', ic:'\u{1F4BC}', label:'ERP', href:'/erp/full'},
|
||||
{id:'crm', ic:'\u{1F4DD}', label:'CRM', href:'/crm/v2'},
|
||||
{id:'dokumenti', ic:'\u{1F4D6}', label:'Dokumenti'},
|
||||
{id:'racuni', ic:'\u{1F9FE}', label:'Računi (OCR)'},
|
||||
{id:'racuni', ic:'\u{1F9FE}', label:'Računi (OCR)', href:'/erp/full?tab=uploads'},
|
||||
{id:'putni', ic:'\u{2708}', label:'Putni nalozi', href:'/erp/full?tab=putni'},
|
||||
{id:'kalendar', ic:'\u{1F4C5}', label:'Kalendar'},
|
||||
{id:'notif', ic:'\u{1F514}', label:'Notifikacije'},
|
||||
{id:'audit', ic:'\u{1F50D}', label:'Audit log'},
|
||||
{id:'forenzika', ic:'⚠', label:'Forenzika', badge:11},
|
||||
],
|
||||
@@ -524,8 +526,10 @@ const NAV_BY_ROLE = {
|
||||
{id:'sportasi', ic:'\u{1F464}', label:'Naši sportaši'},
|
||||
{id:'zahtjevi', ic:'\u{1F4D1}', label:'Zahtjevi PGŽ', badge:3},
|
||||
{id:'kalendar', ic:'\u{1F4C5}', label:'Kalendar'},
|
||||
{id:'notif', ic:'\u{1F514}', label:'Notifikacije'},
|
||||
{id:'lijecnicki',ic:'⚕', label:'Liječnički'},
|
||||
{id:'racuni', ic:'\u{1F9FE}', label:'Računi (OCR)'},
|
||||
{id:'racuni', ic:'\u{1F9FE}', label:'Računi (OCR)', href:'/erp/full?tab=uploads'},
|
||||
{id:'putni', ic:'\u{2708}', label:'Putni nalozi', href:'/erp/full?tab=putni'},
|
||||
],
|
||||
klub: [
|
||||
{id:'profil', ic:'\u{1F464}', label:'Moj profil'},
|
||||
@@ -535,8 +539,10 @@ const NAV_BY_ROLE = {
|
||||
{id:'lijecnicki',ic:'⚕', label:'Liječnički'},
|
||||
{id:'dokumenti', ic:'\u{1F4C4}', label:'Dokumenti'},
|
||||
{id:'kalendar', ic:'\u{1F4C5}', label:'Kalendar'},
|
||||
{id:'notif', ic:'\u{1F514}', label:'Notifikacije'},
|
||||
{id:'manifestacije', ic:'\u{1F4C5}', label:'Manifestacije'},
|
||||
{id:'racuni', ic:'\u{1F9FE}', label:'Računi (OCR)'},
|
||||
{id:'racuni', ic:'\u{1F9FE}', label:'Računi (OCR)', href:'/erp/full?tab=uploads'},
|
||||
{id:'putni', ic:'\u{2708}', label:'Putni nalozi', href:'/erp/full?tab=putni'},
|
||||
],
|
||||
sportas: [
|
||||
{id:'profil', ic:'\u{1F464}', label:'Moj profil'},
|
||||
@@ -546,6 +552,7 @@ const NAV_BY_ROLE = {
|
||||
{id:'dokumenti', ic:'\u{1F4C4}', label:'Moji dokumenti'},
|
||||
{id:'obrasci', ic:'\u{1F4DD}', label:'Obrasci', badge:1},
|
||||
{id:'kalendar', ic:'\u{1F4C5}', label:'Kalendar'},
|
||||
{id:'notif', ic:'\u{1F514}', label:'Notifikacije'},
|
||||
{id:'manifestacije', ic:'\u{1F4C5}', label:'Manifestacije'},
|
||||
],
|
||||
};
|
||||
@@ -754,13 +761,16 @@ function setRole(r){
|
||||
//=========== NAV ===========
|
||||
function buildNav(){
|
||||
const items = NAV_BY_ROLE[_state.role] || [];
|
||||
$('#nav').innerHTML = items.map(n =>
|
||||
`<div class="nav-i ${n.id===_state.section?'active':''}" data-id="${n.id}" data-label="${esc(n.label)}" onclick="navTo('${n.id}')">
|
||||
$('#nav').innerHTML = items.map((n, idx) => {
|
||||
const click = n.href
|
||||
? `onclick="window.location.href='${n.href}'"`
|
||||
: `onclick="navTo('${n.id}')"`;
|
||||
return `<div class="nav-i ${n.id===_state.section?'active':''}" data-id="${n.id}" data-label="${esc(n.label)}" ${click}>
|
||||
<span class="ic">${n.ic}</span>
|
||||
<span class="lbl">${esc(n.label)}</span>
|
||||
${n.badge?`<span class="badge">${n.badge}</span>`:''}
|
||||
</div>`
|
||||
).join('');
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
window.addEventListener('hashchange', () => {
|
||||
const h = (location.hash||'').replace(/^#/,'');
|
||||
@@ -802,9 +812,11 @@ const TITLES = {
|
||||
klubovi:['Klubovi','Sportski klubovi PGŽ'],
|
||||
sportasi:['Sportaši','Registrirani članovi'],
|
||||
financije:['Financije','Sufinanciranje sporta'],
|
||||
racuni:['Računi (OCR)','OCR upload + obrada'],
|
||||
racuni:['Računi (OCR)','OCR upload + obrada (ERP Full)'],
|
||||
putni:['Putni nalozi','Otvara ERP Full → Putni nalozi'],
|
||||
crm:['CRM','Članarine + liječnički'],
|
||||
kalendar:['Kalendar','Liječnički termini, manifestacije, eventi'],
|
||||
notif:['Notifikacije','Centar obavijesti — InApp poruke'],
|
||||
audit:['Audit log','Sve aktivnosti sustava'],
|
||||
forenzika:['Forenzika','Sumnjive transakcije / PEP'],
|
||||
},
|
||||
@@ -815,8 +827,10 @@ const TITLES = {
|
||||
sportasi:['Naši sportaši','Registrirani sportaši saveza'],
|
||||
zahtjevi:['Zahtjevi PGŽ','Sufinanciranje aktivnosti'],
|
||||
kalendar:['Kalendar','Manifestacije saveza'],
|
||||
notif:['Notifikacije','Centar obavijesti — InApp poruke'],
|
||||
lijecnicki:['Liječnički','Pregledi članova saveza'],
|
||||
racuni:['Računi','Računi saveza'],
|
||||
racuni:['Računi','Računi saveza (ERP Full)'],
|
||||
putni:['Putni nalozi','ERP Full → Putni nalozi'],
|
||||
},
|
||||
klub: {
|
||||
profil:['Moj profil','Osobni podaci'],
|
||||
@@ -826,8 +840,10 @@ const TITLES = {
|
||||
lijecnicki:['Liječnički','Pregledi članova'],
|
||||
dokumenti:['Dokumenti','Dokumenti kluba'],
|
||||
kalendar:['Kalendar','Liječnički termini + manifestacije'],
|
||||
notif:['Notifikacije','Centar obavijesti — InApp poruke'],
|
||||
manifestacije:['Manifestacije','Nadolazeće aktivnosti'],
|
||||
racuni:['Računi','Troškovi kluba'],
|
||||
racuni:['Računi','Troškovi kluba (ERP Full)'],
|
||||
putni:['Putni nalozi','ERP Full → Putni nalozi'],
|
||||
},
|
||||
sportas: {
|
||||
profil:['Moj profil','Osobni podaci'],
|
||||
@@ -837,6 +853,7 @@ const TITLES = {
|
||||
dokumenti:['Moji dokumenti','Suglasnosti, ugovori'],
|
||||
obrasci:['Obrasci','Za potpis'],
|
||||
kalendar:['Kalendar','Moji termini i događaji'],
|
||||
notif:['Notifikacije','Centar obavijesti — InApp poruke'],
|
||||
manifestacije:['Manifestacije','Moje aktivnosti'],
|
||||
},
|
||||
};
|
||||
@@ -1164,7 +1181,7 @@ SECTIONS['pgz:dashboard'] = async () => {
|
||||
<div style="display:grid;gap:8px">
|
||||
<button class="btn primary" onclick="setRole('pgz');navTo('korisnici')">+ Dodaj korisnika</button>
|
||||
<button class="btn" onclick="navTo('forenzika')">⚠ Pregled forenzike</button>
|
||||
<button class="btn" onclick="navTo('racuni')">🧾 Skeniraj račun (OCR)</button>
|
||||
<button class="btn" onclick="window.location.href='/erp/full?tab=uploads'">🧾 Skeniraj račun (OCR)</button>
|
||||
<button class="btn" onclick="navTo('audit')">🔍 Audit log</button>
|
||||
<button class="btn gold" onclick="window.open('/sport/','_blank')">🌐 Public portal</button>
|
||||
</div>
|
||||
@@ -1309,22 +1326,22 @@ SECTIONS['pgz:financije'] = async () => {
|
||||
</div>`;
|
||||
};
|
||||
|
||||
SECTIONS['pgz:racuni'] = () => `
|
||||
<div class="card">
|
||||
<div class="card-h"><div class="card-t">🧾 OCR upload (drag & drop)</div></div>
|
||||
<div style="border:2px dashed var(--rim2);border-radius:8px;padding:40px;text-align:center;background:var(--bg3);cursor:pointer" onclick="alert('OCR upload — backend M5 (CC4)')">
|
||||
<div style="font-size:48px;margin-bottom:8px">📷</div>
|
||||
<div style="font-weight:700;color:var(--t0);margin-bottom:4px">Dovuci ovdje sliku ili PDF računa</div>
|
||||
<div style="font-size:11px;color:var(--t2)">ili klikni za odabir · cestarina, gorivo, hotel, dnevnice...</div>
|
||||
<button class="btn primary" style="margin-top:12px">📸 Snimi kamerom</button>
|
||||
</div>
|
||||
<div style="font-size:11px;color:var(--t4);margin-top:10px">Backend: Tesseract OCR + Ri.NET AI Engine ekstrakcija polja → invoices DB</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-h"><div class="card-t">📋 Nedavni računi</div></div>
|
||||
<table><thead><tr><th>Datum</th><th>Izdavatelj</th><th>OIB</th><th>Vrsta</th><th class="num">Iznos</th><th>Status</th></tr></thead>
|
||||
<tbody>${MOCK.invoices.map(r => `<tr><td>${esc(r.datum)}</td><td><b>${esc(r.izdavatelj)}</b></td><td>${esc(formatOib(r.oib))}</td><td><span class="tag ${r.tag}">${esc(r.vrsta)}</span></td><td class="num">${fmtEur(r.iznos)}</td><td>${esc(r.status)}</td></tr>`).join('')}</tbody></table>
|
||||
// Računi (OCR) je premješten u ERP Full → tab Uploads (consolidation 2026-05-05).
|
||||
// Legacy entry preusmjerava korisnika.
|
||||
SECTIONS['pgz:racuni'] = () => {
|
||||
setTimeout(() => { window.location.href = '/erp/full?tab=uploads'; }, 50);
|
||||
return `<div class="card"><div class="card-h"><div class="card-t">🧾 Računi (OCR) — premješteno u ERP Full</div></div>
|
||||
<p style="color:var(--t2);margin:8px 0">Otvaranje <b>ERP Full → Uploads (OCR)</b>…</p>
|
||||
<a class="btn primary" href="/erp/full?tab=uploads" style="text-decoration:none">📎 Otvori sada</a>
|
||||
</div>`;
|
||||
};
|
||||
SECTIONS['pgz:putni'] = () => {
|
||||
setTimeout(() => { window.location.href = '/erp/full?tab=putni'; }, 50);
|
||||
return `<div class="card"><div class="card-h"><div class="card-t">✈ Putni nalozi — premješteno u ERP Full</div></div>
|
||||
<p style="color:var(--t2);margin:8px 0">Otvaranje <b>ERP Full → Putni nalozi</b>…</p>
|
||||
<a class="btn primary" href="/erp/full?tab=putni" style="text-decoration:none">✈ Otvori sada</a>
|
||||
</div>`;
|
||||
};
|
||||
|
||||
SECTIONS['pgz:crm'] = () => `
|
||||
<div style="margin-bottom:12px">
|
||||
@@ -1367,24 +1384,192 @@ SECTIONS['pgz:audit'] = () => `
|
||||
|
||||
// =======================================================================
|
||||
// CC5 R5 — KALENDAR (liječnički termini + manifestacije + eventi)
|
||||
// Agent B 2026-05-05: Added CRUD on pgz_sport.kalendar_events
|
||||
// =======================================================================
|
||||
|
||||
// ─── KALENDAR CRUD helpers ─────────────────────────────────────────────
|
||||
window._kalState = window._kalState || { events: [], ym: null };
|
||||
|
||||
async function kalLoadEvents(ym){
|
||||
const [Y, M] = ym.split('-').map(Number);
|
||||
const from = `${Y}-${String(M).padStart(2,'0')}-01`;
|
||||
const nm = M===12 ? {y:Y+1,m:1} : {y:Y,m:M+1};
|
||||
const to = `${nm.y}-${String(nm.m).padStart(2,'0')}-01`;
|
||||
try {
|
||||
const d = await apiAuth(`/v2/kalendar/events?from=${from}&to=${to}`);
|
||||
return (d && d.rows) || [];
|
||||
} catch(e){ return []; }
|
||||
}
|
||||
|
||||
function _kalFmtLocal(iso){
|
||||
if(!iso) return '';
|
||||
const d = new Date(iso);
|
||||
if(isNaN(d.getTime())) return '';
|
||||
const pad = n => String(n).padStart(2,'0');
|
||||
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||||
}
|
||||
|
||||
function kalEventModalHtml(ev){
|
||||
ev = ev || {};
|
||||
const isEdit = !!ev.id;
|
||||
return `
|
||||
<div id="kalModalBg" style="position:fixed;inset:0;background:rgba(0,0,0,0.6);z-index:9999;display:flex;align-items:center;justify-content:center" onclick="if(event.target===this) kalCloseModal()">
|
||||
<div style="background:var(--bg2);border:1px solid var(--rim);border-radius:8px;width:min(560px,92vw);max-height:90vh;overflow:auto">
|
||||
<div style="padding:14px 16px;border-bottom:1px solid var(--rim);display:flex;justify-content:space-between;align-items:center">
|
||||
<div style="font-weight:600;font-size:15px">${isEdit?'Uredi termin #'+ev.id:'Novi termin'}</div>
|
||||
<button class="btn sm" onclick="kalCloseModal()">✕</button>
|
||||
</div>
|
||||
<div style="padding:14px 16px;display:flex;flex-direction:column;gap:10px">
|
||||
<label style="display:flex;flex-direction:column;gap:4px;font-size:11px;color:var(--t3);text-transform:uppercase;font-weight:600">
|
||||
Naziv *
|
||||
<input id="kalF_title" value="${esc(ev.title||'')}" style="background:var(--bg1);border:1px solid var(--rim);color:var(--t1);padding:8px;border-radius:4px;font-size:13px">
|
||||
</label>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
|
||||
<label style="display:flex;flex-direction:column;gap:4px;font-size:11px;color:var(--t3);text-transform:uppercase;font-weight:600">
|
||||
Početak *
|
||||
<input id="kalF_start" type="datetime-local" value="${_kalFmtLocal(ev.start_at)}" style="background:var(--bg1);border:1px solid var(--rim);color:var(--t1);padding:8px;border-radius:4px;font-size:13px">
|
||||
</label>
|
||||
<label style="display:flex;flex-direction:column;gap:4px;font-size:11px;color:var(--t3);text-transform:uppercase;font-weight:600">
|
||||
Kraj
|
||||
<input id="kalF_end" type="datetime-local" value="${_kalFmtLocal(ev.end_at)}" style="background:var(--bg1);border:1px solid var(--rim);color:var(--t1);padding:8px;border-radius:4px;font-size:13px">
|
||||
</label>
|
||||
</div>
|
||||
<label style="display:flex;flex-direction:column;gap:4px;font-size:11px;color:var(--t3);text-transform:uppercase;font-weight:600">
|
||||
Lokacija
|
||||
<input id="kalF_loc" value="${esc(ev.location||'')}" style="background:var(--bg1);border:1px solid var(--rim);color:var(--t1);padding:8px;border-radius:4px;font-size:13px">
|
||||
</label>
|
||||
<label style="display:flex;flex-direction:column;gap:4px;font-size:11px;color:var(--t3);text-transform:uppercase;font-weight:600">
|
||||
Opis
|
||||
<textarea id="kalF_desc" rows="3" style="background:var(--bg1);border:1px solid var(--rim);color:var(--t1);padding:8px;border-radius:4px;font-size:13px;resize:vertical">${esc(ev.description||'')}</textarea>
|
||||
</label>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
|
||||
<label style="display:flex;flex-direction:column;gap:4px;font-size:11px;color:var(--t3);text-transform:uppercase;font-weight:600">
|
||||
Tip
|
||||
<select id="kalF_type" style="background:var(--bg1);border:1px solid var(--rim);color:var(--t1);padding:8px;border-radius:4px;font-size:13px">
|
||||
${['event','meeting','manif','training','medical','other'].map(t => `<option value="${t}" ${ev.event_type===t?'selected':''}>${t}</option>`).join('')}
|
||||
</select>
|
||||
</label>
|
||||
<label style="display:flex;flex-direction:column;gap:4px;font-size:11px;color:var(--t3);text-transform:uppercase;font-weight:600">
|
||||
Boja
|
||||
<select id="kalF_color" style="background:var(--bg1);border:1px solid var(--rim);color:var(--t1);padding:8px;border-radius:4px;font-size:13px">
|
||||
<option value="b" ${(!ev.color||ev.color==='b')?'selected':''}>plava</option>
|
||||
<option value="g" ${ev.color==='g'?'selected':''}>zelena</option>
|
||||
<option value="a" ${ev.color==='a'?'selected':''}>amber</option>
|
||||
<option value="r" ${ev.color==='r'?'selected':''}>crvena</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding:12px 16px;border-top:1px solid var(--rim);display:flex;justify-content:space-between;gap:8px">
|
||||
<div>
|
||||
${isEdit ? `<button class="btn sm" style="background:rgba(220,38,38,0.18);border-color:rgba(220,38,38,0.5);color:#fca5a5" onclick="kalDelete(${ev.id})">🗑 Obriši</button>` : ''}
|
||||
</div>
|
||||
<div style="display:flex;gap:8px">
|
||||
<button class="btn sm" onclick="kalCloseModal()">Odustani</button>
|
||||
<button class="btn primary sm" onclick="kalSave(${ev.id||'null'})">${isEdit?'Spremi':'Kreiraj'}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function kalOpenModal(ev){
|
||||
const wrap = document.createElement('div');
|
||||
wrap.id = 'kalModalWrap';
|
||||
wrap.innerHTML = kalEventModalHtml(ev);
|
||||
document.body.appendChild(wrap);
|
||||
setTimeout(() => { try { document.getElementById('kalF_title').focus(); } catch(e){} }, 50);
|
||||
}
|
||||
|
||||
function kalCloseModal(){
|
||||
const w = document.getElementById('kalModalWrap');
|
||||
if(w) w.remove();
|
||||
}
|
||||
|
||||
async function kalRefresh(){
|
||||
const ym = window._kalState.ym || (new Date().getFullYear()+'-'+String(new Date().getMonth()+1).padStart(2,'0'));
|
||||
$('#content').innerHTML = '<div class=loading>Učitavanje...</div>';
|
||||
$('#content').innerHTML = await renderKalendar({ym});
|
||||
}
|
||||
|
||||
async function kalSave(eid){
|
||||
const title = (document.getElementById('kalF_title').value||'').trim();
|
||||
const start = document.getElementById('kalF_start').value;
|
||||
const end = document.getElementById('kalF_end').value;
|
||||
const loc = document.getElementById('kalF_loc').value;
|
||||
const desc = document.getElementById('kalF_desc').value;
|
||||
const typ = document.getElementById('kalF_type').value;
|
||||
const col = document.getElementById('kalF_color').value;
|
||||
if(!title){ alert('Naziv je obavezan.'); return; }
|
||||
if(!start){ alert('Početak je obavezan.'); return; }
|
||||
const toIso = (s) => s ? new Date(s).toISOString() : null;
|
||||
const body = {
|
||||
title,
|
||||
start_at: toIso(start),
|
||||
end_at: end ? toIso(end) : null,
|
||||
location: loc || null,
|
||||
description: desc || null,
|
||||
event_type: typ,
|
||||
color: col,
|
||||
};
|
||||
let res;
|
||||
if(eid && eid !== 'null'){
|
||||
res = await apiAuth('/v2/kalendar/events/'+eid, {method:'PUT', body: JSON.stringify(body)});
|
||||
} else {
|
||||
res = await apiAuth('/v2/kalendar/events', {method:'POST', body: JSON.stringify(body)});
|
||||
}
|
||||
if(!res || res.__error || res.__unauthorized){
|
||||
alert('Greška pri spremanju (status '+(res && res.status || '?')+').');
|
||||
return;
|
||||
}
|
||||
kalCloseModal();
|
||||
await kalRefresh();
|
||||
}
|
||||
|
||||
async function kalEdit(eid){
|
||||
const r = await apiAuth('/v2/kalendar/events/'+eid);
|
||||
if(!r || r.__error){ alert('Ne mogu dohvatiti termin.'); return; }
|
||||
kalOpenModal(r);
|
||||
}
|
||||
|
||||
async function kalDelete(eid){
|
||||
if(!confirm('Obrisati termin #'+eid+'?')) return;
|
||||
const r = await apiAuth('/v2/kalendar/events/'+eid, {method:'DELETE'});
|
||||
if(!r || r.__error){ alert('Greška pri brisanju.'); return; }
|
||||
kalCloseModal();
|
||||
await kalRefresh();
|
||||
}
|
||||
|
||||
async function renderKalendar(opts){
|
||||
opts = opts || {};
|
||||
const today = new Date();
|
||||
const ym = opts.ym || (today.getFullYear() + '-' + String(today.getMonth()+1).padStart(2,'0'));
|
||||
window._kalState.ym = ym;
|
||||
const [Y, M] = ym.split('-').map(Number);
|
||||
const first = new Date(Y, M-1, 1);
|
||||
const last = new Date(Y, M, 0);
|
||||
|
||||
// Učitaj sve liječničke koji ističu unutar +180 dana, manifestacije iz API-ja, i mock eventove
|
||||
let lij = [], manif = [], notif = [];
|
||||
// Učitaj liječničke + manifestacije + notifikacije + kalendar_events
|
||||
let lij = [], manif = [], notif = [], kalEvents = [];
|
||||
try { const d = await fetch('/sport/api/crm/lijecnicki/uskoro-isticu?days=180&include_expired=false').then(r=>r.json()); lij = d.rows || []; } catch(e){}
|
||||
try { const d = await fetch('/sport/api/manifestacije').then(r=>r.json()); manif = d.rows || d || []; } catch(e){}
|
||||
try { const d = await fetch('/sport/api/crm/notifications?limit=50').then(r=>r.json()); notif = d.rows || []; } catch(e){}
|
||||
kalEvents = await kalLoadEvents(ym);
|
||||
window._kalState.events = kalEvents;
|
||||
|
||||
const events = [];
|
||||
lij.forEach(l => events.push({date: l.vrijedi_do, type:'lij', title:`⚕ Pregled ističe: ${l.clan}`, klub:l.klub, color:'a'}));
|
||||
manif.forEach(m => { if (m.datum) events.push({date: m.datum, type:'manif', title:`📅 ${m.naziv || m.title || 'Manifestacija'}`, klub:m.lokacija || m.grad, color:'b'}); });
|
||||
// CRUD events from kalendar_events table
|
||||
kalEvents.forEach(k => events.push({
|
||||
id: k.id,
|
||||
date: (k.start_at||'').substring(0,10),
|
||||
type: k.event_type || 'event',
|
||||
title: k.title,
|
||||
klub: k.location || '',
|
||||
color: k.color || 'b',
|
||||
crud: true,
|
||||
}));
|
||||
// Eventi: ZZJZ termini mock — sljedećih 7 dana po radnim danima
|
||||
for(let d=0; d<14; d++){
|
||||
const dt = new Date(); dt.setDate(dt.getDate()+d);
|
||||
@@ -1423,16 +1608,27 @@ async function renderKalendar(opts){
|
||||
const k = `${Y}-${String(M).padStart(2,'0')}-${String(d).padStart(2,'0')}`;
|
||||
const ev = byDate[k] || [];
|
||||
const isToday = (k === today.toISOString().slice(0,10));
|
||||
const evHtml = ev.slice(0,3).map(e => `<div style="font-size:10px;background:rgba(${e.color==='a'?'245,158,11':e.color==='b'?'26,115,232':'34,197,94'},0.18);padding:2px 4px;border-radius:3px;margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(e.title)}${e.klub?' — '+esc(e.klub):''}">${esc(e.title.substring(0,28))}</div>`).join('');
|
||||
const evHtml = ev.slice(0,3).map(e => {
|
||||
const click = e.crud && e.id ? `onclick="event.stopPropagation();kalEdit(${e.id})"` : '';
|
||||
const cur = e.crud ? 'cursor:pointer;' : '';
|
||||
return `<div ${click} style="${cur}font-size:10px;background:rgba(${e.color==='a'?'245,158,11':e.color==='b'?'26,115,232':'34,197,94'},0.18);padding:2px 4px;border-radius:3px;margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(e.title)}${e.klub?' — '+esc(e.klub):''}${e.crud?' (klikni za uredi)':''}">${esc(e.title.substring(0,28))}</div>`;
|
||||
}).join('');
|
||||
const more = ev.length > 3 ? `<div style="font-size:9px;color:var(--t3);margin-top:2px">+${ev.length-3} više</div>` : '';
|
||||
grid += `<div style="background:${isToday?'rgba(26,115,232,0.15)':'var(--bg2)'};border:1px solid ${isToday?'var(--pgz-blue)':'var(--rim)'};border-radius:6px;padding:6px;min-height:90px;${ev.length?'cursor:pointer':''}" ><div style="font-weight:600;font-size:13px;color:${isToday?'var(--pgz-blue)':'var(--t1)'}">${d}</div>${evHtml}${more}</div>`;
|
||||
// Click on empty cell → create event for that date at 09:00
|
||||
const cellClick = `onclick="kalOpenModal({start_at:'${k}T09:00:00'})"`;
|
||||
grid += `<div ${cellClick} style="background:${isToday?'rgba(26,115,232,0.15)':'var(--bg2)'};border:1px solid ${isToday?'var(--pgz-blue)':'var(--rim)'};border-radius:6px;padding:6px;min-height:90px;cursor:pointer" title="Klik za novi termin"><div style="font-weight:600;font-size:13px;color:${isToday?'var(--pgz-blue)':'var(--t1)'}">${d}</div>${evHtml}${more}</div>`;
|
||||
}
|
||||
grid += '</div>';
|
||||
|
||||
// Lista nadolazećih (top 10)
|
||||
const upcoming = events.filter(e => e.date && e.date >= today.toISOString().slice(0,10))
|
||||
.sort((a,b) => a.date.localeCompare(b.date)).slice(0, 10);
|
||||
const upcomingHtml = upcoming.map(e => `<tr><td>${esc(e.date)}</td><td>${esc(e.title)}</td><td>${esc(e.klub||'—')}</td><td><span class="tag ${e.color==='a'?'am':e.color==='b'?'bl':'gr'}">${e.type}</span></td></tr>`).join('');
|
||||
const upcomingHtml = upcoming.map(e => {
|
||||
const actions = e.crud && e.id
|
||||
? `<button class="btn sm" onclick="kalEdit(${e.id})">Uredi</button> <button class="btn sm" style="color:#fca5a5" onclick="kalDelete(${e.id})">🗑</button>`
|
||||
: '<span style="color:var(--t3);font-size:11px">—</span>';
|
||||
return `<tr><td>${esc(e.date)}</td><td>${esc(e.title)}</td><td>${esc(e.klub||'—')}</td><td><span class="tag ${e.color==='a'?'am':e.color==='b'?'bl':'gr'}">${e.type}</span></td><td>${actions}</td></tr>`;
|
||||
}).join('');
|
||||
|
||||
return `
|
||||
<div class="kpi-grid" style="margin-bottom:12px">
|
||||
@@ -1448,16 +1644,19 @@ async function renderKalendar(opts){
|
||||
<button class="btn sm" onclick="$('#content').innerHTML='<div class=loading>Učitavanje...</div>';renderKalendar({ym:'${prevYm}'}).then(h=>$('#content').innerHTML=h)">←</button>
|
||||
<input type="month" value="${ym}" onchange="$('#content').innerHTML='<div class=loading>...</div>';renderKalendar({ym:this.value}).then(h=>$('#content').innerHTML=h)" style="background:var(--bg2);border:1px solid var(--rim);color:var(--t1);padding:4px 8px;border-radius:4px">
|
||||
<button class="btn sm" onclick="$('#content').innerHTML='<div class=loading>Učitavanje...</div>';renderKalendar({ym:'${nextYm}'}).then(h=>$('#content').innerHTML=h)">→</button>
|
||||
<button class="btn primary sm" onclick="fetch('/sport/api/crm/lijecnicki/notify-scan',{method:'POST',headers:{'Content-Type':'application/json'},body:'{}'}).then(r=>r.json()).then(d=>alert('Skenirano: '+d.created+' notifikacija kreirano'))">🔔 Scan isteke → notifikacije</button>
|
||||
<button class="btn primary sm" onclick="kalOpenModal({})">+ Novi termin</button>
|
||||
<button class="btn sm" onclick="fetch('/sport/api/crm/lijecnicki/notify-scan',{method:'POST',headers:{'Content-Type':'application/json'},body:'{}'}).then(r=>r.json()).then(d=>alert('Skenirano: '+d.created+' notifikacija kreirano'))">🔔 Scan isteke → notifikacije</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding:14px">${grid}</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-h"><div class="card-t">📋 Nadolazeći eventi (10)</div></div>
|
||||
<div class="card-h"><div class="card-t">📋 Nadolazeći eventi (10)</div>
|
||||
<div class="card-actions"><button class="btn primary sm" onclick="kalOpenModal({})">+ Novi termin</button></div>
|
||||
</div>
|
||||
<table>
|
||||
<thead><tr><th>Datum</th><th>Naziv</th><th>Lokacija/Klub</th><th>Tip</th></tr></thead>
|
||||
<tbody>${upcomingHtml || '<tr><td colspan="4" class="empty">Nema nadolazećih eventa.</td></tr>'}</tbody>
|
||||
<thead><tr><th>Datum</th><th>Naziv</th><th>Lokacija/Klub</th><th>Tip</th><th>Akcije</th></tr></thead>
|
||||
<tbody>${upcomingHtml || '<tr><td colspan="5" class="empty">Nema nadolazećih eventa.</td></tr>'}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card">
|
||||
@@ -1620,19 +1819,7 @@ SECTIONS['savez:zahtjevi'] = () => `
|
||||
</div>`).join('')}
|
||||
</div>`;
|
||||
|
||||
SECTIONS['savez:kalendar'] = () => `
|
||||
<div class="card"><div class="card-h"><div class="card-t">📅 Kalendar manifestacija — Svibanj 2026</div></div>
|
||||
<div class="cal-grid">
|
||||
${'PON UTO SRI ČET PET SUB NED'.split(' ').map(h => `<div class="cal-h">${h}</div>`).join('')}
|
||||
${[...Array(31)].map((_,i) => {
|
||||
const day = i+1;
|
||||
const ev = [4,11,18,25,9,16,30].includes(day);
|
||||
const today = day===5;
|
||||
return `<div class="cal-d ${today?'t':''} ${ev?'has-event':''}"><b>${day}</b>${ev?`<div style="font-size:9px;color:var(--pgz-gold);margin-top:2px">Trening / utakmica</div>`:''}</div>`;
|
||||
}).join('')}
|
||||
</div>
|
||||
<div style="margin-top:14px;font-size:11px;color:var(--t2)">● Trening kamp Platak (4–6.5) · ● Liga PGŽ atletika (11.5) · ● Open senior (18.5) · ● Memorijalna utrka (25.5)</div>
|
||||
</div>`;
|
||||
// (Stari savez:kalendar mock obrisan — sada koristi renderKalendar s CRUD-om iz pgz_sport.kalendar_events)
|
||||
|
||||
SECTIONS['savez:lijecnicki'] = () => `
|
||||
<div class="card"><div class="card-h"><div class="card-t">⚕ Liječnički pregledi članova saveza</div><div class="card-actions"><button class="btn primary">📅 Bulk ZZJZ rezervacija</button></div></div>
|
||||
@@ -1646,6 +1833,7 @@ SECTIONS['savez:lijecnicki'] = () => `
|
||||
</div>`;
|
||||
|
||||
SECTIONS['savez:racuni'] = SECTIONS['pgz:racuni'];
|
||||
SECTIONS['savez:putni'] = SECTIONS['pgz:putni'];
|
||||
|
||||
// =======================================================================
|
||||
// KLUB ADMIN — Dashboard + sub-pages
|
||||
@@ -1685,7 +1873,7 @@ SECTIONS['klub:dashboard'] = () => {
|
||||
<div class="card-h"><div class="card-t">⚡ Brze akcije</div></div>
|
||||
<div style="display:grid;gap:8px">
|
||||
<button class="btn primary" onclick="navTo('clanovi')">+ Dodaj člana</button>
|
||||
<button class="btn gold" onclick="navTo('racuni')">🧾 Skeniraj račun (OCR)</button>
|
||||
<button class="btn gold" onclick="window.location.href='/erp/full?tab=uploads'">🧾 Skeniraj račun (OCR)</button>
|
||||
<button class="btn" onclick="navTo('clanarine')">€ Članarine + HUB-3</button>
|
||||
<button class="btn" onclick="navTo('lijecnicki')">⚕ Liječnički bulk ZZJZ</button>
|
||||
<button class="btn" onclick="alert('Obrazac sufinanciranja — M9')">📑 Predaj zahtjev PGŽ</button>
|
||||
@@ -1765,6 +1953,7 @@ SECTIONS['klub:manifestacije'] = () => `
|
||||
</div>`;
|
||||
|
||||
SECTIONS['klub:racuni'] = SECTIONS['pgz:racuni'];
|
||||
SECTIONS['klub:putni'] = SECTIONS['pgz:putni'];
|
||||
|
||||
// =======================================================================
|
||||
// SPORTAŠ — Dashboard + sub-pages
|
||||
|
||||
@@ -103,6 +103,21 @@ table tr.sel td { background:rgba(26,115,232,.15); }
|
||||
.chip.high { background:#3a2e1a; color:#fbbf24; }
|
||||
.chip.normal { background:#1e293b; color:#94a3b8; }
|
||||
.chip.low { background:#1f1f1f; color:#71717a; }
|
||||
.chip.nepodmireno { background:#3a1e1e; color:#f87171; }
|
||||
.chip.djelomicno { background:#3a2e1a; color:#fbbf24; }
|
||||
.chip.podmireno { background:#1a3a2a; color:#4ade80; }
|
||||
.chip.storno { background:#1f1f1f; color:#71717a; }
|
||||
.chip.draft { background:#1e293b; color:#94a3b8; }
|
||||
.chip.submitted { background:#1e3a5f; color:#60a5fa; }
|
||||
.chip.approved { background:#1a3a2a; color:#4ade80; }
|
||||
.chip.rejected { background:#3a1e1e; color:#f87171; }
|
||||
.chip.spreman { background:#1a3a2a; color:#4ade80; }
|
||||
.chip.nije-spreman{ background:#3a1e1e; color:#f87171; }
|
||||
.tpl-row { padding:9px 12px; border-bottom:1px solid var(--rim); cursor:pointer; }
|
||||
.tpl-row:hover { background:var(--bg3); }
|
||||
.tpl-row.sel { background:rgba(26,115,232,.18); border-left:3px solid var(--pgz-blue); padding-left:9px; }
|
||||
.tpl-row .tpl-n { font-weight:600; font-size:12px; color:var(--t1); }
|
||||
.tpl-row .tpl-c { font-size:10px; color:var(--t3); margin-top:2px; text-transform:uppercase; letter-spacing:.4px; }
|
||||
|
||||
/* Kanban */
|
||||
.kanban { display:grid; grid-template-columns:repeat(6,1fr); gap:10px; min-height:60vh; }
|
||||
@@ -190,6 +205,9 @@ footer { height:36px; background:var(--bg2); border-top:1px solid var(--rim);
|
||||
<div class="tab" data-tab="opportunities">Opportunities <span class="count" id="cnt-opps">·</span></div>
|
||||
<div class="tab" data-tab="activities">Activities <span class="count" id="cnt-activities">·</span></div>
|
||||
<div class="tab" data-tab="cases">Cases <span class="count" id="cnt-cases">·</span></div>
|
||||
<div class="tab" data-tab="clanarine">Članarine <span class="count" id="cnt-clanarine">·</span></div>
|
||||
<div class="tab" data-tab="lijecnicki">Liječnički <span class="count" id="cnt-lijecnicki">·</span></div>
|
||||
<div class="tab" data-tab="obrasci">Obrasci <span class="count" id="cnt-obrasci">·</span></div>
|
||||
</div>
|
||||
|
||||
<div class="main">
|
||||
@@ -364,6 +382,89 @@ footer { height:36px; background:var(--bg2); border-top:1px solid var(--rim);
|
||||
</div></div>
|
||||
</div>
|
||||
|
||||
<!-- ────── ČLANARINE ────── -->
|
||||
<div class="tab-c" id="tc-clanarine">
|
||||
<div class="toolbar">
|
||||
<input id="cln-klub" type="number" placeholder="Klub ID" style="max-width:120px">
|
||||
<input id="cln-clan" type="number" placeholder="Član ID" style="max-width:120px">
|
||||
<input id="cln-godina" type="number" placeholder="Godina" style="max-width:120px">
|
||||
<select id="cln-status">
|
||||
<option value="">— Svi statusi —</option>
|
||||
<option value="nepodmireno">nepodmireno</option>
|
||||
<option value="djelomicno">djelomično</option>
|
||||
<option value="podmireno">podmireno</option>
|
||||
<option value="storno">storno</option>
|
||||
</select>
|
||||
<button class="btn" onclick="loadClanarine()">Filtriraj</button>
|
||||
<span class="grow"></span>
|
||||
<button class="btn primary" onclick="openClanarinaModal()">+ Nova članarina</button>
|
||||
</div>
|
||||
<div class="card"><div class="card-b" style="padding:0">
|
||||
<table id="t-clanarine"><thead><tr>
|
||||
<th>Član</th><th>Klub</th><th>Godina</th><th>Razdoblje</th>
|
||||
<th>Propisan</th><th>Plaćen</th><th>Datum uplate</th><th>Status</th><th></th>
|
||||
</tr></thead><tbody></tbody></table>
|
||||
</div></div>
|
||||
</div>
|
||||
|
||||
<!-- ────── LIJEČNIČKI ────── -->
|
||||
<div class="tab-c" id="tc-lijecnicki">
|
||||
<div class="toolbar">
|
||||
<input id="lij-klub" type="number" placeholder="Klub ID" style="max-width:120px">
|
||||
<input id="lij-clan" type="number" placeholder="Član ID" style="max-width:120px">
|
||||
<select id="lij-expiring">
|
||||
<option value="">— Svi —</option>
|
||||
<option value="true">Ističu (≤30d) ili istekli</option>
|
||||
</select>
|
||||
<button class="btn" onclick="loadLijecnicki()">Filtriraj</button>
|
||||
<span class="grow"></span>
|
||||
<button class="btn primary" onclick="openLijecnickiModal()">+ Novi pregled</button>
|
||||
</div>
|
||||
<div class="card"><div class="card-b" style="padding:0">
|
||||
<table id="t-lijecnicki"><thead><tr>
|
||||
<th>Član</th><th>Klub</th><th>Datum</th><th>Vrsta</th>
|
||||
<th>Vrijedi do</th><th>Liječnik</th><th>Spreman</th><th>Plaćeno</th><th></th>
|
||||
</tr></thead><tbody></tbody></table>
|
||||
</div></div>
|
||||
</div>
|
||||
|
||||
<!-- ────── OBRASCI ────── -->
|
||||
<div class="tab-c" id="tc-obrasci">
|
||||
<div style="display:grid; grid-template-columns:300px 1fr; gap:12px; align-items:start;">
|
||||
<!-- Templates list (left) -->
|
||||
<div class="card">
|
||||
<div class="card-h"><div class="card-t">Predlošci</div>
|
||||
<button class="btn sm" onclick="loadObrasciTemplates()">↻</button>
|
||||
</div>
|
||||
<div class="card-b" style="padding:0; max-height:70vh; overflow:auto;" id="obr-tpl-list">
|
||||
<div class="empty">Učitavanje…</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Submissions / detail (right) -->
|
||||
<div>
|
||||
<div class="toolbar">
|
||||
<strong id="obr-right-title">Podnešeni obrasci</strong>
|
||||
<span class="grow"></span>
|
||||
<select id="obr-status">
|
||||
<option value="">— Svi statusi —</option>
|
||||
<option value="draft">draft</option>
|
||||
<option value="submitted">submitted</option>
|
||||
<option value="approved">approved</option>
|
||||
<option value="rejected">rejected</option>
|
||||
</select>
|
||||
<input id="obr-klub" type="number" placeholder="Klub ID" style="max-width:120px">
|
||||
<button class="btn" onclick="loadObrasciSubmissions()">Filtriraj</button>
|
||||
</div>
|
||||
<div class="card"><div class="card-b" style="padding:0" id="obr-right-body">
|
||||
<table id="t-obr-sub"><thead><tr>
|
||||
<th>ID</th><th>Predložak</th><th>Klub</th><th>Član</th>
|
||||
<th>Status</th><th>Submitted</th><th>Approved</th><th></th>
|
||||
</tr></thead><tbody></tbody></table>
|
||||
</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /main -->
|
||||
|
||||
<footer>
|
||||
@@ -439,6 +540,9 @@ function switchTab(name) {
|
||||
if (name==='opportunities') loadOpps();
|
||||
if (name==='activities') loadActivities();
|
||||
if (name==='cases') loadCases();
|
||||
if (name==='clanarine') loadClanarine();
|
||||
if (name==='lijecnicki') loadLijecnicki();
|
||||
if (name==='obrasci') { loadObrasciTemplates(); loadObrasciSubmissions(); }
|
||||
}
|
||||
|
||||
// ────── /me ──────
|
||||
|
||||
+194
-14
@@ -1,9 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
PGŽ Sport — Dokumenti UI v1.0
|
||||
PGŽ Sport — Dokumenti UI v1.1
|
||||
dradulic@outlook.com / damir@rinet.one — 2026-05-05
|
||||
Library svih dokumenata: godišnjaci, publikacije, pravilnici, javni pozivi.
|
||||
Tabovi + filteri + drill-down modal s RAG citatima + XLSX export + globalna pretraga.
|
||||
Library svih dokumenata: godišnjaci, publikacije, pravilnici, javni pozivi,
|
||||
+ unified "Svi dokumenti" view (tip × izdavatelj) preko /api/v2/dokumenti/unified.
|
||||
Tabovi + filteri + drill-down modal s RAG citatima + inline PDF iframe + XLSX export.
|
||||
-->
|
||||
<html lang="hr">
|
||||
<head>
|
||||
@@ -150,9 +151,16 @@
|
||||
.modal-bg.open { display:flex; }
|
||||
.modal {
|
||||
background:var(--bg1); border:1px solid #2a3144; border-radius:12px;
|
||||
width:100%; max-width:880px; padding:0; overflow:hidden;
|
||||
width:100%; max-width:1100px; padding:0; overflow:hidden;
|
||||
box-shadow:0 12px 60px rgba(0,0,0,0.6);
|
||||
}
|
||||
.pdf-frame {
|
||||
width:100%; height:72vh; border:0; background:#0a0c12; border-radius:6px;
|
||||
}
|
||||
.pdf-fallback {
|
||||
padding:30px; text-align:center; color:var(--muted);
|
||||
background:var(--bg2); border-radius:6px;
|
||||
}
|
||||
.modal-h {
|
||||
padding:18px 22px; border-bottom:1px solid var(--line); display:flex;
|
||||
align-items:flex-start; justify-content:space-between; gap:12px;
|
||||
@@ -239,6 +247,7 @@
|
||||
</div>
|
||||
<div class="modal-tabs">
|
||||
<button class="modal-tab active" data-mtab="info" onclick="setModalTab('info')">ℹ️ Info</button>
|
||||
<button class="modal-tab" data-mtab="pdf" onclick="setModalTab('pdf')">📄 PDF</button>
|
||||
<button class="modal-tab" data-mtab="search" onclick="setModalTab('search')">🔎 Pretraži sadržaj</button>
|
||||
<button class="modal-tab" data-mtab="citati" onclick="setModalTab('citati')">📎 Citati</button>
|
||||
</div>
|
||||
@@ -254,7 +263,9 @@
|
||||
// Tab definitions (vrste mapping)
|
||||
// ─────────────────────────────────────────
|
||||
const TABS = [
|
||||
{ id:'godisnjaci', label:'📅 Godišnjaci ZSPGZ',
|
||||
{ id:'svi', label:'🗂️ Svi dokumenti',
|
||||
unified:true, view:'cards' },
|
||||
{ id:'godisnjaci', label:'📅 Godišnjaci ZSPGZ',
|
||||
vrste:['godisnjak'], view:'cards-year' },
|
||||
{ id:'publikacije', label:'📰 Publikacije',
|
||||
vrste:['program','plan','strategija','izvjestaj','raspodjela','erasmus'], view:'cards' },
|
||||
@@ -269,6 +280,28 @@ const TABS = [
|
||||
view:'cards' },
|
||||
];
|
||||
|
||||
// Unified library — derived classifications from the API
|
||||
const TIP_OPTIONS = [
|
||||
{ v:'', l:'Svi tipovi' },
|
||||
{ v:'godisnjak', l:'Godišnjak' },
|
||||
{ v:'publikacija', l:'Publikacija' },
|
||||
{ v:'pravilnik', l:'Pravilnik / Statut / Zakon' },
|
||||
{ v:'javni-poziv', l:'Javni poziv' },
|
||||
{ v:'natjecaj', l:'Natjecaj' },
|
||||
{ v:'ostalo', l:'Ostalo' },
|
||||
];
|
||||
const IZDAVATELJ_OPTIONS = [
|
||||
{ v:'', l:'Svi izdavatelji' },
|
||||
{ v:'ZSPGZ', l:'Zajednica sportova PGŽ' },
|
||||
{ v:'PGŽ', l:'PGŽ (Županija)' },
|
||||
{ v:'RSS', l:'Riječki sportski savez' },
|
||||
{ v:'HOO', l:'Hrvatski olimpijski odbor' },
|
||||
{ v:'JLS', l:'Grad / Općina (JLS)' },
|
||||
{ v:'savez', l:'Nacionalni sportski savez' },
|
||||
{ v:'klub', l:'Klub' },
|
||||
{ v:'ostalo',l:'Ostalo' },
|
||||
];
|
||||
|
||||
const VRSTE_HIDDEN = ['corpus','corpus_v2','corpus_v3','novost_savez','audit','godisnjak_facts'];
|
||||
const API = '/sport/api/v2'; // hosted under /sport/ via reverse proxy
|
||||
// Fallback: when running directly on :8095, /sport prefix is stripped — try both
|
||||
@@ -277,13 +310,16 @@ const API_DIRECT = '/api/v2';
|
||||
// ─────────────────────────────────────────
|
||||
// State
|
||||
// ─────────────────────────────────────────
|
||||
let activeTab = 'godisnjaci';
|
||||
let activeTab = 'svi';
|
||||
let allDocs = []; // all loaded docs (cached after first fetch)
|
||||
let unifiedDocs = []; // separately cached unified-view docs (with tip/izdavatelj)
|
||||
let unifiedFacets = null; // counts {by_tip, by_izdavatelj}
|
||||
let displayDocs = []; // currently filtered/visible
|
||||
let lastSearchHits = null; // for export of search results
|
||||
let currentDocId = null;
|
||||
let currentDocFull = null;
|
||||
const filterState = {}; // per-tab filter state {tab: {godinaMin, godinaMax, vrste:Set, q}}
|
||||
const filterState = {}; // per-tab filter state
|
||||
const unifiedState = { tip:'', izdavatelj:'', q:'', godinaMin:null, godinaMax:null };
|
||||
|
||||
// ─────────────────────────────────────────
|
||||
// Utilities
|
||||
@@ -332,12 +368,108 @@ function setTab(id){
|
||||
$$('.tab').forEach(b => b.classList.toggle('active', b.dataset.tab===id));
|
||||
if(id==='search'){
|
||||
renderSearchView();
|
||||
} else if(id==='svi'){
|
||||
renderUnifiedFilters();
|
||||
loadUnified();
|
||||
} else {
|
||||
renderFilters();
|
||||
applyFilters();
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────
|
||||
// Unified mode (Svi dokumenti)
|
||||
// ─────────────────────────────────────────
|
||||
function renderUnifiedFilters(){
|
||||
const tipSel = TIP_OPTIONS.map(o =>
|
||||
`<option value="${esc(o.v)}" ${unifiedState.tip===o.v?'selected':''}>${esc(o.l)}</option>`).join('');
|
||||
const izdSel = IZDAVATELJ_OPTIONS.map(o =>
|
||||
`<option value="${esc(o.v)}" ${unifiedState.izdavatelj===o.v?'selected':''}>${esc(o.l)}</option>`).join('');
|
||||
$('#filters-wrap').innerHTML = `
|
||||
<div class="filters">
|
||||
<div class="filter-grp">
|
||||
<label>Tip:</label>
|
||||
<select id="uTip" onchange="onUnifiedFilterChange()">${tipSel}</select>
|
||||
</div>
|
||||
<div class="filter-grp">
|
||||
<label>Izdavatelj:</label>
|
||||
<select id="uIzd" onchange="onUnifiedFilterChange()">${izdSel}</select>
|
||||
</div>
|
||||
<div class="filter-grp">
|
||||
<label>Godina:</label>
|
||||
<input type="number" id="uYmin" value="${unifiedState.godinaMin||''}" placeholder="od" min="1990" max="2030" onchange="onUnifiedFilterChange()" style="width:80px">
|
||||
<span>–</span>
|
||||
<input type="number" id="uYmax" value="${unifiedState.godinaMax||''}" placeholder="do" min="1990" max="2030" onchange="onUnifiedFilterChange()" style="width:80px">
|
||||
</div>
|
||||
<div class="filter-grp">
|
||||
<label>Pretraga:</label>
|
||||
<input type="text" id="uQ" value="${esc(unifiedState.q)}" placeholder="naziv / opis / organizacija..." oninput="onUnifiedQDebounce()">
|
||||
</div>
|
||||
<div style="margin-left:auto;color:var(--muted);font-size:0.85rem;" id="filterCount">…</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
let _uQTimer = null;
|
||||
function onUnifiedQDebounce(){
|
||||
clearTimeout(_uQTimer);
|
||||
_uQTimer = setTimeout(() => {
|
||||
unifiedState.q = $('#uQ').value || '';
|
||||
loadUnified();
|
||||
}, 350);
|
||||
}
|
||||
function onUnifiedFilterChange(){
|
||||
unifiedState.tip = $('#uTip').value || '';
|
||||
unifiedState.izdavatelj = $('#uIzd').value || '';
|
||||
unifiedState.godinaMin = parseInt($('#uYmin').value) || null;
|
||||
unifiedState.godinaMax = parseInt($('#uYmax').value) || null;
|
||||
unifiedState.q = $('#uQ').value || '';
|
||||
loadUnified();
|
||||
}
|
||||
|
||||
async function loadUnified(){
|
||||
$('#content').innerHTML = '<div class="loading">Učitavam dokumente…</div>';
|
||||
const params = new URLSearchParams();
|
||||
if(unifiedState.tip) params.set('tip', unifiedState.tip);
|
||||
if(unifiedState.izdavatelj) params.set('izdavatelj', unifiedState.izdavatelj);
|
||||
if(unifiedState.q) params.set('q', unifiedState.q);
|
||||
if(unifiedState.godinaMin) params.set('godina_min', unifiedState.godinaMin);
|
||||
if(unifiedState.godinaMax) params.set('godina_max', unifiedState.godinaMax);
|
||||
params.set('limit', 500);
|
||||
try {
|
||||
const r = await api('/dokumenti/unified?' + params.toString());
|
||||
const j = await r.json();
|
||||
unifiedDocs = j.dokumenti || [];
|
||||
displayDocs = unifiedDocs;
|
||||
$('#filterCount') && ($('#filterCount').textContent = `${unifiedDocs.length} dokumenata`);
|
||||
renderUnifiedContent(unifiedDocs);
|
||||
} catch(e){
|
||||
$('#content').innerHTML = `<div class="empty">⚠️ Greška: ${esc(e.message)}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderUnifiedContent(docs){
|
||||
if(docs.length === 0){
|
||||
$('#content').innerHTML = `<div class="empty">Nema dokumenata za odabrane filtere.</div>`;
|
||||
return;
|
||||
}
|
||||
const html = docs.map(d => unifiedCardHTML(d)).join('');
|
||||
$('#content').innerHTML = `<div class="grid">${html}</div>`;
|
||||
}
|
||||
|
||||
function unifiedCardHTML(d){
|
||||
const meta = [];
|
||||
if(d.izdavatelj) meta.push(`<span class="chip" style="background:rgba(0,48,135,0.5);color:#aac8ff">${esc(d.izdavatelj)}</span>`);
|
||||
if(d.organizacija) meta.push(`<span style="color:var(--muted)">🏛️ ${esc(d.organizacija.slice(0,28))}${d.organizacija.length>28?'…':''}</span>`);
|
||||
if(d.izdano_datum) meta.push(`<span style="color:var(--muted)">📅 ${fmtDate(d.izdano_datum)}</span>`);
|
||||
else if(d.godina) meta.push(`<span style="color:var(--muted)">📅 ${d.godina}</span>`);
|
||||
return `
|
||||
<div class="card" onclick="openModal(${d.id})">
|
||||
<span class="cardvrsta">${esc(d.tip||d.vrsta||'—')}</span>
|
||||
<div class="cardtitle">${esc(d.title || '(bez naslova)')}</div>
|
||||
<div class="cardmeta" style="flex-wrap:wrap;gap:6px;">${meta.join('')}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────
|
||||
// Filters UI
|
||||
// ─────────────────────────────────────────
|
||||
@@ -507,24 +639,42 @@ function cardHTML(d, yearStyle){
|
||||
// Data load
|
||||
// ─────────────────────────────────────────
|
||||
async function loadAll(){
|
||||
// Load both legacy list (for the 4 vrsta-tabs) and unified facets (for "Svi" badge).
|
||||
$('#content').innerHTML = '<div class="loading">Učitavam dokumente…</div>';
|
||||
try {
|
||||
const r = await api('/dokumenti?limit=1000');
|
||||
const j = await r.json();
|
||||
const [r1, r2] = await Promise.all([
|
||||
api('/dokumenti?limit=1000'),
|
||||
api('/dokumenti/facets'),
|
||||
]);
|
||||
const j = await r1.json();
|
||||
allDocs = (j.dokumenti || []).filter(d => !VRSTE_HIDDEN.includes(d.vrsta));
|
||||
try { unifiedFacets = await r2.json(); } catch(_){ unifiedFacets = null; }
|
||||
updateTabCounts();
|
||||
renderFilters();
|
||||
applyFilters();
|
||||
if(activeTab === 'svi'){
|
||||
renderUnifiedFilters();
|
||||
loadUnified();
|
||||
} else {
|
||||
renderFilters();
|
||||
applyFilters();
|
||||
}
|
||||
} catch(e){
|
||||
$('#content').innerHTML = `<div class="empty">⚠️ Greška pri učitavanju: ${esc(e.message)}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function updateTabCounts(){
|
||||
// "svi" tab uses unified facets total; the rest count over allDocs.
|
||||
for(const t of TABS){
|
||||
const old = activeTab; activeTab = t.id;
|
||||
const c = tabDocs().length;
|
||||
activeTab = old;
|
||||
let c = 0;
|
||||
if(t.id === 'svi'){
|
||||
if(unifiedFacets && unifiedFacets.by_tip){
|
||||
c = unifiedFacets.by_tip.reduce((s,x) => s + (x.broj||0), 0);
|
||||
} else { c = '—'; }
|
||||
} else {
|
||||
const old = activeTab; activeTab = t.id;
|
||||
c = tabDocs().length;
|
||||
activeTab = old;
|
||||
}
|
||||
const el = document.getElementById('cnt-'+t.id);
|
||||
if(el) el.textContent = c;
|
||||
}
|
||||
@@ -658,6 +808,31 @@ function setModalTab(t){
|
||||
${d.izvor_url ? `<div class="info-row"><div class="lab">Izvor URL</div><div class="val"><a href="${esc(d.izvor_url)}" target="_blank">${esc(d.izvor_url)}</a></div></div>` : ''}
|
||||
<div class="info-row"><div class="lab">RAG chunks</div><div class="val">${currentDocFull.chunks_count || 0}</div></div>
|
||||
`;
|
||||
} else if(t === 'pdf'){
|
||||
// Inline PDF viewer — try server stream, fall back to external pdf_url, then "open in new tab".
|
||||
const localPdf = `${API}/dokumenti/${d.id}/pdf`;
|
||||
const fallbackPdf = `${API_DIRECT}/dokumenti/${d.id}/pdf`;
|
||||
const externalPdf = d.pdf_url || d.izvor_url || null;
|
||||
$('#mBody').innerHTML = `
|
||||
<div style="margin-bottom:8px;display:flex;gap:8px;flex-wrap:wrap;">
|
||||
<a class="btn gold sm" target="_blank" rel="noopener" href="${esc(localPdf)}">⤴ Otvori u novom tabu</a>
|
||||
${externalPdf ? `<a class="btn secondary sm" target="_blank" rel="noopener" href="${esc(externalPdf)}">🔗 Originalni izvor</a>` : ''}
|
||||
</div>
|
||||
<iframe class="pdf-frame" id="pdfFrame" src="${esc(localPdf)}"
|
||||
onerror="this.src='${esc(fallbackPdf)}'"></iframe>
|
||||
<div id="pdfFallback" style="display:none;" class="pdf-fallback">
|
||||
PDF nije dostupan u inline pregledniku.
|
||||
${externalPdf ? `Pokušaj <a href="${esc(externalPdf)}" target="_blank">originalni izvor</a>.` : ''}
|
||||
</div>
|
||||
`;
|
||||
// Detect 404 on iframe load — best-effort timeout
|
||||
setTimeout(()=>{
|
||||
try {
|
||||
const fr = $('#pdfFrame');
|
||||
if(!fr || !fr.contentWindow) return;
|
||||
// Same-origin? May read; cross-origin we can't introspect — leave as-is.
|
||||
} catch(_){}
|
||||
}, 1500);
|
||||
} else if(t === 'search'){
|
||||
$('#mBody').innerHTML = `
|
||||
<div class="inline-search">
|
||||
@@ -721,10 +896,15 @@ function exportXLSX(){
|
||||
return;
|
||||
}
|
||||
const isSearch = activeTab === 'search';
|
||||
const isUnified = activeTab === 'svi';
|
||||
const rows = isSearch ? (lastSearchHits || []).map(h => ({
|
||||
id:h.id, title:h.title, vrsta:h.vrsta, godina:h.godina,
|
||||
izdano_datum:h.izdano_datum, izvor_url:h.izvor_url,
|
||||
excerpt:(h.excerpt||'').replace(/\s+/g,' ').slice(0,300)
|
||||
})) : isUnified ? displayDocs.map(d => ({
|
||||
id:d.id, title:d.title, tip:d.tip, izdavatelj:d.izdavatelj,
|
||||
vrsta:d.vrsta, godina:d.godina, organizacija:d.organizacija,
|
||||
izdano_datum:d.izdano_datum, izvor_url:d.izvor_url, chars:d.chars
|
||||
})) : displayDocs.map(d => ({
|
||||
id:d.id, title:d.title, vrsta:d.vrsta, godina:d.godina,
|
||||
organizacija:d.organizacija, izdano_datum:d.izdano_datum,
|
||||
|
||||
+79
-5
@@ -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>
|
||||
|
||||
+64
-15
@@ -69,19 +69,45 @@
|
||||
const $ = (s, root) => (root||document).querySelector(s);
|
||||
const esc = s => String(s==null?'':s).replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));
|
||||
|
||||
// Tracks the currently-loaded user (null = guest). Set by setUserDisplay().
|
||||
let _currentUser = null;
|
||||
|
||||
function readToken(){
|
||||
try { return localStorage.getItem('jwt') || localStorage.getItem('access_token') || ''; }
|
||||
catch(e){ return ''; }
|
||||
}
|
||||
function logout(){
|
||||
if(!confirm('Odjava iz aplikacije?')) return;
|
||||
try {
|
||||
localStorage.removeItem('jwt');
|
||||
localStorage.removeItem('access_token');
|
||||
localStorage.removeItem('app-role');
|
||||
return localStorage.getItem('pgz_access')
|
||||
|| sessionStorage.getItem('pgz_access')
|
||||
|| localStorage.getItem('jwt')
|
||||
|| localStorage.getItem('access_token')
|
||||
|| '';
|
||||
} catch(e){ return ''; }
|
||||
}
|
||||
async function logout(){
|
||||
if(!confirm('Odjava iz aplikacije?')) return;
|
||||
// Revoke server-side (matches app.html logout flow, commit e07292b)
|
||||
try {
|
||||
const tok = readToken();
|
||||
if(tok){
|
||||
await fetch('/sport/api/auth/logout', {
|
||||
method:'POST',
|
||||
headers:{'Authorization':'Bearer '+tok}
|
||||
}).catch(()=>{});
|
||||
}
|
||||
} catch(e){}
|
||||
// Clear ALL session keys
|
||||
['pgz_access','pgz_refresh','pgz_user','app-role','jwt','access_token','refresh_token','pgz_session_id'].forEach(k => {
|
||||
try { localStorage.removeItem(k); sessionStorage.removeItem(k); } catch(e){}
|
||||
});
|
||||
location.href = '/sport/login';
|
||||
}
|
||||
function gotoLogin(){
|
||||
// Preserve return URL so we land back here after sign-in
|
||||
try {
|
||||
const ret = encodeURIComponent(location.pathname + location.search + location.hash);
|
||||
location.href = '/sport/login?next=' + ret;
|
||||
} catch(e){
|
||||
location.href = '/sport/login';
|
||||
}
|
||||
}
|
||||
function initials(n){
|
||||
if(!n) return '?';
|
||||
const p = String(n).trim().split(/\s+/);
|
||||
@@ -141,19 +167,23 @@
|
||||
<div class="pgz-sb-mx" onclick="PGZSidebar.closeMobile()" title="Zatvori">✕</div>
|
||||
</div>
|
||||
<nav class="pgz-sb-nav" id="pgz-sb-nav">${renderSections(activeKey, user)}</nav>
|
||||
<div class="pgz-sb-foot" id="pgz-sb-foot" onclick="PGZSidebar.toggleUserMenu(event)">
|
||||
<div class="av" id="pgz-sb-av">PG</div>
|
||||
<div class="pgz-sb-foot" id="pgz-sb-foot"
|
||||
role="button" tabindex="0"
|
||||
title="Klikni za prijavu / odjavu"
|
||||
onclick="PGZSidebar.handleFootClick(event)"
|
||||
onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();PGZSidebar.handleFootClick(event);}">
|
||||
<div class="av" id="pgz-sb-av">?</div>
|
||||
<div class="ui">
|
||||
<div class="un" id="pgz-sb-un">Gost</div>
|
||||
<div class="ur" id="pgz-sb-ur">Demo</div>
|
||||
<div class="ur" id="pgz-sb-ur">Klikni za prijavu</div>
|
||||
</div>
|
||||
<div class="caret">▾</div>
|
||||
<div class="caret" id="pgz-sb-caret" title="Otvori izbornik" onclick="event.stopPropagation();PGZSidebar.toggleUserMenu(event)">▾</div>
|
||||
<div class="pgz-user-menu" id="pgz-user-menu" onclick="event.stopPropagation()">
|
||||
<a href="/app#profil"><span>👤</span><span>Moj profil</span></a>
|
||||
<a href="/app#postavke"><span>⚙</span><span>Postavke</span></a>
|
||||
<a href="/static/sport2.html"><span>🌐</span><span>Public portal</span></a>
|
||||
<div class="sep"></div>
|
||||
<a href="/login" id="pgz-menu-login"><span>🔑</span><span>Prijava</span></a>
|
||||
<a href="/sport/login" id="pgz-menu-login"><span>🔑</span><span>Prijava</span></a>
|
||||
<a class="danger" id="pgz-menu-logout" onclick="PGZSidebar.logout()" style="display:none"><span>⎋</span><span>Odjava</span></a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -171,13 +201,16 @@
|
||||
}
|
||||
|
||||
function setUserDisplay(me){
|
||||
_currentUser = me || null;
|
||||
const un = $('#pgz-sb-un'), ur = $('#pgz-sb-ur'), av = $('#pgz-sb-av');
|
||||
const foot = document.getElementById('pgz-sb-foot');
|
||||
const loginLink = document.getElementById('pgz-menu-login');
|
||||
const logoutLink = document.getElementById('pgz-menu-logout');
|
||||
if(!me){
|
||||
if(un) un.textContent = 'Gost';
|
||||
if(ur) ur.textContent = 'Klikni za prijavu';
|
||||
if(av){ av.textContent = '?'; av.innerHTML = av.innerHTML; }
|
||||
if(foot) foot.setAttribute('title', 'Klikni za prijavu');
|
||||
if(loginLink) loginLink.style.display = 'flex';
|
||||
if(logoutLink) logoutLink.style.display = 'none';
|
||||
return;
|
||||
@@ -186,11 +219,12 @@
|
||||
const role = me.user_type || '';
|
||||
const avSrc = me.avatar_url || me.google_picture;
|
||||
if(un) un.textContent = name;
|
||||
if(ur) ur.textContent = role;
|
||||
if(ur) ur.textContent = role || 'Korisnik';
|
||||
if(av){
|
||||
if(avSrc) av.innerHTML = `<img src="${esc(avSrc)}" alt="">`;
|
||||
else av.textContent = initials(name);
|
||||
}
|
||||
if(foot) foot.setAttribute('title', 'Klikni za odjavu (' + (me.email || name) + ')');
|
||||
if(loginLink) loginLink.style.display = 'none';
|
||||
if(logoutLink) logoutLink.style.display = 'flex';
|
||||
}
|
||||
@@ -286,7 +320,22 @@
|
||||
setTimeout(() => document.addEventListener('click', closer, true), 50);
|
||||
}
|
||||
},
|
||||
logout
|
||||
/* Single-click footer handler:
|
||||
* - Guest (no user) → /sport/login
|
||||
* - Logged in → logout() (revokes JWT, clears storage, redirects to login)
|
||||
* The caret (▾) opens the full user menu (profil/postavke/portal/logout).
|
||||
*/
|
||||
handleFootClick(ev){
|
||||
ev && ev.stopPropagation();
|
||||
if(_currentUser){
|
||||
return logout();
|
||||
}
|
||||
return gotoLogin();
|
||||
},
|
||||
gotoLogin,
|
||||
logout,
|
||||
// exposed for debugging / tests
|
||||
_state: () => ({ user: _currentUser })
|
||||
};
|
||||
window.PGZSidebar = PGZSidebar;
|
||||
|
||||
|
||||
+57
-13
@@ -1956,10 +1956,12 @@ async function openSportas(id){
|
||||
.forEach(k => { if((d[k]===null||d[k]===undefined||d[k]==='') && p[k]!==null && p[k]!==undefined && p[k]!=='') d[k]=p[k]; });
|
||||
}
|
||||
const stats = d.stats || {};
|
||||
const sezone = d.clan_sezona || [];
|
||||
const utakmice = d.utakmice || [];
|
||||
const sezone = (d.clan_sezona && d.clan_sezona.length) ? d.clan_sezona : ((dV2 && dV2.hns_seasons) || []);
|
||||
const utakmice = (d.utakmice && d.utakmice.length) ? d.utakmice : ((dV2 && dV2.hns_matches) || []);
|
||||
const nagrade = d.nagrade || [];
|
||||
const godisnjaci = d.godisnjak_godine || d.godisnjaci || [];
|
||||
// SUB6 2026-05-05: M2M kategorije (clan_kategorije) iz /v2/clan/{id}/full
|
||||
const kategorije = (dV2 && dV2.kategorije) || [];
|
||||
const initials = (((d.ime||'?')[0]||'?')+((d.prezime||'?')[0]||'?')).toUpperCase();
|
||||
const photo = d.slika_url ? '<img src="'+esc(d.slika_url)+'" alt="" onerror="this.style.display=\'none\';if(this.parentElement)this.parentElement.innerHTML=\'<div class=\\\'no\\\'>'+initials+'</div>\'">' : '<div class="no">'+initials+'</div>';
|
||||
const dob = d.datum_rodjenja || d.datum_rodenja;
|
||||
@@ -2020,6 +2022,7 @@ async function openSportas(id){
|
||||
<div class="tabs">
|
||||
<div class="tab active" onclick="switchPlayerTab(this,'p-sez')">🏆 HNS Karijera (${sezone.length})</div>
|
||||
<div class="tab" onclick="switchPlayerTab(this,'p-utak')">📅 Posljednje utakmice (${utakmice.length})</div>
|
||||
<div class="tab" onclick="switchPlayerTab(this,'p-kat')">🏷️ Kategorije (${kategorije.length})</div>
|
||||
<div class="tab" onclick="switchPlayerTab(this,'p-bio')">Bio</div>
|
||||
<div class="tab" onclick="switchPlayerTab(this,'p-god')">Godišnjaci (${godisnjaci.length})</div>
|
||||
<div class="tab" onclick="switchPlayerTab(this,'p-nag')">Nagrade (${nagrade.length})</div>
|
||||
@@ -2035,11 +2038,11 @@ async function openSportas(id){
|
||||
<td>${esc(s.natjecanje||'')}</td>
|
||||
<td>${esc(s.klub_naziv||'')}</td>
|
||||
<td class="num">${fmtNum(s.nastupi)}</td>
|
||||
<td class="num"><b style="color:var(--pgz-gold)">${fmtNum(s.pogoci)}</b></td>
|
||||
<td class="num"><b style="color:var(--pgz-gold)">${fmtNum(s.pogoci ?? s.golovi)}</b></td>
|
||||
<td class="num">${fmtNum(s.asistencije)}</td>
|
||||
<td class="num">${fmtNum(s.zuti_kartoni)}</td>
|
||||
<td class="num">${fmtNum(s.crveni_kartoni)}</td>
|
||||
<td>${s.natjecanje_url?'<a href="'+esc(s.natjecanje_url)+'" target="_blank">↗</a>':''}</td>
|
||||
<td class="num">${fmtNum(s.zuti_kartoni ?? s.zuti)}</td>
|
||||
<td class="num">${fmtNum(s.crveni_kartoni ?? s.crveni)}</td>
|
||||
<td>${(s.natjecanje_url||s.source_url)?'<a href="'+esc(s.natjecanje_url||s.source_url)+'" target="_blank">↗</a>':''}</td>
|
||||
</tr>`).join('')}
|
||||
</tbody>
|
||||
</table></div>` : '<div class="empty">Nema sezonskih podataka</div>'}
|
||||
@@ -2048,22 +2051,45 @@ async function openSportas(id){
|
||||
<div id="p-utak" class="ptab" style="display:none">
|
||||
<div class="pp-section-h">📅 Posljednje utakmice <span class="cnt">${utakmice.length} zabilježeno</span></div>
|
||||
${utakmice.length ? `<div style="overflow-x:auto;max-height:500px;overflow-y:auto"><table>
|
||||
<thead><tr><th>Datum</th><th>Natjecanje</th><th colspan="3">Susret</th><th class="num">Gol.</th><th class="num">Min.</th><th></th></tr></thead>
|
||||
<tbody>${utakmice.map(u => `
|
||||
<thead><tr><th>Datum</th><th>Natjecanje</th><th colspan="3">Susret</th><th>Pozicija</th><th class="num">Gol.</th><th class="num">Min.</th><th></th></tr></thead>
|
||||
<tbody>${utakmice.map(u => {
|
||||
const dom = u.klub_dom || u.domacin || '';
|
||||
const gost = u.klub_gost || u.gost || '';
|
||||
const golovi = (u.pogodaka != null ? u.pogodaka : u.golovi);
|
||||
const minute = (u.minute != null) ? u.minute : ((u.minute_od != null && u.minute_do != null) ? (u.minute_do - u.minute_od) : null);
|
||||
return `
|
||||
<tr class="no-click">
|
||||
<td>${fmtDate(u.datum)}</td>
|
||||
<td>${esc(u.natjecanje||'')}</td>
|
||||
<td>${u.klub_dom_logo?'<img src="'+esc(u.klub_dom_logo)+'" class="utlogo" onerror="this.style.display=\'none\'">':''}${esc(u.klub_dom||'')}</td>
|
||||
<td>${u.klub_dom_logo?'<img src="'+esc(u.klub_dom_logo)+'" class="utlogo" onerror="this.style.display=\'none\'">':''}${esc(dom)}</td>
|
||||
<td><b>${esc(u.rezultat||'-')}</b></td>
|
||||
<td>${u.klub_gost_logo?'<img src="'+esc(u.klub_gost_logo)+'" class="utlogo" onerror="this.style.display=\'none\'">':''}${esc(u.klub_gost||'')}</td>
|
||||
<td class="num"><b style="color:var(--pgz-gold)">${fmtNum(u.pogodaka)}</b></td>
|
||||
<td class="num">${fmtNum(u.minute)}</td>
|
||||
<td>${u.klub_gost_logo?'<img src="'+esc(u.klub_gost_logo)+'" class="utlogo" onerror="this.style.display=\'none\'">':''}${esc(gost)}</td>
|
||||
<td>${esc(u.pozicija||'—')}</td>
|
||||
<td class="num"><b style="color:var(--pgz-gold)">${fmtNum(golovi)}</b></td>
|
||||
<td class="num">${fmtNum(minute)}</td>
|
||||
<td>${u.source_url?'<a href="'+esc(u.source_url)+'" target="_blank">↗</a>':''}</td>
|
||||
</tr>`).join('')}
|
||||
</tr>`;
|
||||
}).join('')}
|
||||
</tbody>
|
||||
</table></div>` : '<div class="empty">Nema podataka o utakmicama</div>'}
|
||||
</div>
|
||||
|
||||
<div id="p-kat" class="ptab" style="display:none">
|
||||
<div class="pp-section-h">🏷️ Kategorije <span class="cnt">${kategorije.length} ${kategorije.length===1?'zapis':(kategorije.length<5&&kategorije.length>1?'zapisa':'zapisa')}</span></div>
|
||||
${kategorije.length ? `<div style="overflow-x:auto;max-height:500px;overflow-y:auto"><table>
|
||||
<thead><tr><th>Sezona</th><th>Kategorija</th><th>Klub</th><th>Izvor</th><th></th></tr></thead>
|
||||
<tbody>${kategorije.map(k => `
|
||||
<tr class="no-click">
|
||||
<td><b>${esc(k.sezona||'—')}</b></td>
|
||||
<td><span class="tag b">${esc(k.kategorija||'—')}</span></td>
|
||||
<td>${k.klub_id ? '<a class="link-chip" onclick="closePanel();setTimeout(()=>openKlub('+k.klub_id+'),250)">'+esc(k.klub_naziv||('Klub #'+k.klub_id))+'</a>' : (k.klub_naziv?esc(k.klub_naziv):'—')}</td>
|
||||
<td>${esc(k.source||'—')}</td>
|
||||
<td>${k.source_url?'<a href="'+esc(k.source_url)+'" target="_blank" rel="noopener">↗</a>':''}</td>
|
||||
</tr>`).join('')}
|
||||
</tbody>
|
||||
</table></div>` : '<div class="empty">Nema M2M kategorija (clan_kategorije)</div>'}
|
||||
</div>
|
||||
|
||||
<div id="p-bio" class="ptab" style="display:none">
|
||||
<div class="kv">
|
||||
<div class="k">OIB</div><div class="v">${d.oib?(canSeeFullOib({klub_id:d.klub_id,savez_id:d.savez_id})?'<a class="link-chip" onclick="openOIB("'+esc(d.oib)+'")">'+esc(d.oib)+'</a>':maskOib(d.oib)):'—'}</div>
|
||||
@@ -3343,6 +3369,24 @@ function init(){
|
||||
navTo('dashboard');
|
||||
}
|
||||
window.addEventListener('DOMContentLoaded', init);
|
||||
|
||||
// PRIORITY FILTERS (CRISIS V5) — default: prikazujemo samo klubove koji imaju podatke
|
||||
window._klub_only_priority = true; // by default: only klubovi koji primaju novac ili su u godisnjaku ili imaju HNS roster
|
||||
window._sportas_only_priority = true; // by default: only iz priority klubova
|
||||
window._sportas_only_hns = false; // dodatni filter: samo s HNS profilom
|
||||
|
||||
window.toggleKlubPriority = function(){
|
||||
window._klub_only_priority = !window._klub_only_priority;
|
||||
if(typeof loadKlubovi === 'function') loadKlubovi();
|
||||
};
|
||||
window.toggleSportasPriority = function(){
|
||||
window._sportas_only_priority = !window._sportas_only_priority;
|
||||
if(typeof loadSportasi === 'function') loadSportasi();
|
||||
};
|
||||
window.toggleSportasHNS = function(){
|
||||
window._sportas_only_hns = !window._sportas_only_hns;
|
||||
if(typeof loadSportasi === 'function') loadSportasi();
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user