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
+238 -49
View File
@@ -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 (46.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