CRISIS FIX: login flow + mobile responsive + token expiry handling
ROOT CAUSE ISOLATED:
Backend POST /api/auth/login, GET/PUT /api/auth/me, POST avatar, POST /logout
all return 200 OK (verified curl). Damirov problem is browser-side:
stale localStorage tokens that don't match current backend → 401 cascade
→ avatar upload appears as 'failed: 401' → profile changes 'lost'.
FIXES:
1. apiAuth() in app.html now:
- Pre-checks JWT exp claim before request
- On 401 response: clears localStorage (pgz_access/refresh/user) +
redirects to /login?reason=unauthorized
- On JWT expired: redirects to /login?reason=expired
2. login.html displays toast for ?reason=expired/unauthorized
3. Mobile responsive CSS (max-width: 768px):
- app.html: hamburger menu, sidebar slide-in, full-width drill-down panel
- sport2.html: KPI grid 2-col, klubovi 1-col, tables horizontal scroll
- Both: viewport meta + media queries + touch-friendly buttons
4. Mobile menu toggle button + backdrop overlay added
VERIFIED E2E (curl):
- POST /auth/login → 200 + JWT
- GET /auth/me → 200 + telefon persisted
- PUT /auth/me → 200, DB row updated
- POST /auth/me/avatar → 200, file saved + avatar_url returned
- POST /auth/logout → 200, token revoked (next /me returns 401)
This commit is contained in:
+5
-4
@@ -159,6 +159,7 @@ td.num { font-family: 'JetBrains Mono', monospace; text-align: right; }
|
||||
</style>
|
||||
<link rel="stylesheet" href="/static/shared/sidebar.css">
|
||||
<script src="/static/shared/sidebar.js" defer data-active="korisnici"></script>
|
||||
<script src="/static/oib_format.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app">
|
||||
@@ -433,7 +434,7 @@ async function loadDashboard() {
|
||||
`).join('');
|
||||
}
|
||||
|
||||
$('#metaInfo').textContent = `Tenant: ${d.tenant.display_name} · OIB: ${d.tenant.oib || '—'} · ${new Date().toLocaleString('hr-HR')}`;
|
||||
$('#metaInfo').textContent = `Tenant: ${d.tenant.display_name} · OIB: ${d.tenant.oib ? formatOib(d.tenant.oib) : '—'} · ${new Date().toLocaleString('hr-HR')}`;
|
||||
}
|
||||
|
||||
async function loadERP() {
|
||||
@@ -473,7 +474,7 @@ async function loadCRM(q='') {
|
||||
const d = await fetchJSON(url);
|
||||
if (d && d.klubovi) {
|
||||
$('#klubTable tbody').innerHTML = d.klubovi.map(k => `
|
||||
<tr><td><strong>${k.naziv}</strong></td><td>${k.oib || '—'}</td>
|
||||
<tr><td><strong>${k.naziv}</strong></td><td>${k.oib ? formatOib(k.oib,{klub_id:k.id,savez_id:k.savez_id}) : '—'}</td>
|
||||
<td>${k.sport || '—'}</td><td>${k.grad || '—'}</td>
|
||||
<td>${k.email || '—'}</td><td class="num">${fmt(k.clanovi)}</td>
|
||||
<td class="num">${fmt(k.invoices_count)}</td></tr>
|
||||
@@ -487,7 +488,7 @@ async function loadOsobe(q='') {
|
||||
if (d && d.osobe) {
|
||||
$('#osobeTable tbody').innerHTML = d.osobe.map(o => `
|
||||
<tr><td>${o.ime}</td><td><strong>${o.prezime}</strong></td>
|
||||
<td>${o.oib || '—'}</td><td>${o.klub_naziv || '—'}</td>
|
||||
<td>${o.oib ? formatOib(o.oib,{klub_id:o.klub_id}) : '—'}</td><td>${o.klub_naziv || '—'}</td>
|
||||
<td>${o.pozicija || '—'}</td><td>${o.email || '—'}</td>
|
||||
<td>${o.aktivan ? badge('Aktivan', 'green') : badge('Neaktivan', 'gray')}</td></tr>
|
||||
`).join('');
|
||||
@@ -500,7 +501,7 @@ async function loadTenants() {
|
||||
$('#tenantsGrid').innerHTML = d.tenants.map(t => `
|
||||
<div class="tenant-card">
|
||||
<div class="name">${t.display_name}</div>
|
||||
<div class="slug">@${t.slug} · ${t.type} · ${t.oib || 'no OIB'}</div>
|
||||
<div class="slug">@${t.slug} · ${t.type} · ${t.oib ? formatOib(t.oib) : 'no OIB'}</div>
|
||||
<div class="stats">
|
||||
<div class="stat"><strong>${fmt(t.klubovi_count || 0)}</strong>klubovi</div>
|
||||
<div class="stat"><strong>${statusBadge(t.status).match(/>([^<]+)</)[1]}</strong>status</div>
|
||||
|
||||
@@ -141,6 +141,7 @@ td.actions-col .btn { padding: 4px 8px; font-size: 11px; }
|
||||
.sidebar { display: none; }
|
||||
}
|
||||
</style>
|
||||
<script src="/static/oib_format.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app" id="appShell">
|
||||
@@ -653,7 +654,7 @@ async function loadTenants() {
|
||||
<tr><td>${t.id}</td><td><code>${escapeHtml(t.slug)}</code></td>
|
||||
<td><strong>${escapeHtml(t.display_name)}</strong></td>
|
||||
<td><span class="badge cyan">${escapeHtml(t.type||'—')}</span></td>
|
||||
<td>${escapeHtml(t.oib||'—')}</td>
|
||||
<td>${t.oib?escapeHtml(formatOib(t.oib)):'—'}</td>
|
||||
<td><span class="badge ${t.status==='active'?'green':'gray'}">${escapeHtml(t.status||'—')}</span></td></tr>
|
||||
`).join('') || '<tr><td colspan="6" class="empty">—</td></tr>';
|
||||
$('#savezi2Tbody').innerHTML = (d.savezi || []).map(s => `
|
||||
@@ -663,7 +664,7 @@ async function loadTenants() {
|
||||
$('#klubCount').textContent = `${(d.klubovi||[]).length} prikazano`;
|
||||
$('#klubovi2Tbody').innerHTML = (d.klubovi || []).slice(0, 200).map(k => `
|
||||
<tr><td>${k.id}</td><td>${escapeHtml(k.naziv)}</td><td>${escapeHtml(k.sport||'—')}</td>
|
||||
<td>${escapeHtml(k.grad||'—')}</td><td>${escapeHtml(k.oib||'—')}</td><td>${k.savez_id||'—'}</td></tr>
|
||||
<td>${escapeHtml(k.grad||'—')}</td><td>${k.oib?escapeHtml(formatOib(k.oib,{klub_id:k.id,savez_id:k.savez_id})):'—'}</td><td>${k.savez_id||'—'}</td></tr>
|
||||
`).join('') || '<tr><td colspan="6" class="empty">—</td></tr>';
|
||||
}
|
||||
|
||||
|
||||
+72
-3
@@ -256,9 +256,63 @@ table tbody tr:hover{background:var(--bg3)}
|
||||
.main,.sb.collapsed ~ .main{margin-left:0}
|
||||
.role-switch{display:none}
|
||||
}
|
||||
|
||||
/* === MOBILE RESPONSIVE (CRISIS FIX) === */
|
||||
@media (max-width: 768px) {
|
||||
body { font-size: 14px; }
|
||||
.app { display: block !important; }
|
||||
.sb {
|
||||
position: fixed; left: -260px; top: 0; width: 260px; height: 100vh;
|
||||
z-index: 1000; transition: left 0.3s ease;
|
||||
}
|
||||
.sb.mobile-open { left: 0; }
|
||||
.main { margin-left: 0 !important; padding: 12px !important; }
|
||||
.topbar { padding: 8px 12px !important; }
|
||||
.topbar #user-tenant { display: none; }
|
||||
#user-name { font-size: 12px !important; }
|
||||
.role-badge { font-size: 9px !important; padding: 2px 6px !important; }
|
||||
|
||||
/* Mobile menu hamburger */
|
||||
.mobile-menu-btn {
|
||||
display: inline-flex !important; padding: 8px; cursor: pointer;
|
||||
background: var(--bg2); border: 1px solid var(--rim); border-radius: 4px;
|
||||
font-size: 18px; color: var(--t1); margin-right: 8px;
|
||||
}
|
||||
|
||||
/* Drill-down panel full-width on mobile */
|
||||
#dpanel { width: 100vw !important; max-width: 100vw !important; right: -100vw !important; }
|
||||
#dpanel.open { right: 0 !important; }
|
||||
|
||||
/* Profile responsive */
|
||||
.profile-page { padding: 8px !important; }
|
||||
.kv { grid-template-columns: 1fr !important; }
|
||||
|
||||
/* Tables horizontal scroll */
|
||||
table { font-size: 12px !important; }
|
||||
.table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
|
||||
|
||||
/* KPI grid */
|
||||
.kpi-grid { grid-template-columns: 1fr 1fr !important; gap: 8px !important; }
|
||||
|
||||
/* Buttons full-width on mobile in forms */
|
||||
form .btn { width: 100%; margin-top: 8px; }
|
||||
}
|
||||
|
||||
@media (min-width: 769px) {
|
||||
.mobile-menu-btn { display: none !important; }
|
||||
}
|
||||
|
||||
/* Sidebar overlay backdrop on mobile when open */
|
||||
.sb-backdrop {
|
||||
display: none; position: fixed; inset: 0; background: rgba(0,0,0,.5); z-index: 999;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.sb-backdrop.show { display: block; }
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="/static/shared/sidebar.css">
|
||||
<script src="/static/shared/sidebar.js" defer data-active="profil"></script>
|
||||
<script src="/static/oib_format.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -490,7 +544,7 @@ async function showDetail(kind, id, title){
|
||||
else {
|
||||
body = `
|
||||
<h2 style="font-size:18px;color:var(--t0);margin-bottom:6px">${esc(d.naziv||'—')}</h2>
|
||||
<div style="font-size:11px;color:var(--t2);margin-bottom:14px">${esc(d.skraceni_naziv||'')} · ${esc(d.oib||'')}</div>
|
||||
<div style="font-size:11px;color:var(--t2);margin-bottom:14px">${esc(d.skraceni_naziv||'')} · ${esc(d.oib?formatOib(d.oib,{savez_id:d.id}):'')}</div>
|
||||
<div class="kv">
|
||||
<div class="k">Predsjednik</div><div class="v">${esc(d.predsjednik||'—')}</div>
|
||||
<div class="k">Tajnik</div><div class="v">${esc(d.tajnik||'—')}</div>
|
||||
@@ -511,7 +565,7 @@ async function showDetail(kind, id, title){
|
||||
<h2 style="font-size:18px;color:var(--t0);margin-bottom:6px">${esc(d.naziv||'—')}</h2>
|
||||
<div style="font-size:11px;color:var(--t2);margin-bottom:14px">${esc(d.savez||'')} · ${esc(d.grad||'')}</div>
|
||||
<div class="kv">
|
||||
<div class="k">OIB</div><div class="v">${esc(d.oib||'—')}</div>
|
||||
<div class="k">OIB</div><div class="v">${d.oib?esc(formatOib(d.oib,{klub_id:d.id,savez_id:d.savez_id})):'—'}</div>
|
||||
<div class="k">Predsjednik</div><div class="v">${esc(d.predsjednik||'—')}</div>
|
||||
<div class="k">Adresa</div><div class="v">${esc(d.adresa||'—')}</div>
|
||||
<div class="k">Email</div><div class="v">${esc(d.email||'—')}</div>
|
||||
@@ -1158,7 +1212,7 @@ SECTIONS['pgz:racuni'] = () => `
|
||||
<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(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>
|
||||
<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>
|
||||
</div>`;
|
||||
|
||||
SECTIONS['pgz:crm'] = () => `
|
||||
@@ -1923,6 +1977,21 @@ async function profileDeleteAccount() {
|
||||
alert('Greška: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Mobile sidebar toggle (CRISIS FIX)
|
||||
function toggleMobileSidebar(){
|
||||
const sb = document.getElementById('sb');
|
||||
if(!sb) return;
|
||||
sb.classList.toggle('mobile-open');
|
||||
let backdrop = document.querySelector('.sb-backdrop');
|
||||
if(!backdrop){
|
||||
backdrop = document.createElement('div');
|
||||
backdrop.className = 'sb-backdrop';
|
||||
backdrop.onclick = () => toggleMobileSidebar();
|
||||
document.body.appendChild(backdrop);
|
||||
}
|
||||
backdrop.classList.toggle('show');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+7
-2
@@ -138,6 +138,7 @@ table tr:hover td { background: rgba(26, 115, 232, 0.05); }
|
||||
<link rel="stylesheet" href="/static/shared/sidebar.css">
|
||||
<script src="/static/shared/sidebar.js" defer data-active="clanarine"></script>
|
||||
<style>body{padding-top:0}</style>
|
||||
<script src="/static/oib_format.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -1235,7 +1236,11 @@ async function loadClanPanel(cid) {
|
||||
// helper za render polja s edit/no-edit
|
||||
const f = (key, label, val, type='text') => {
|
||||
const ed = canEdit(key);
|
||||
const safe = val == null || val === '' ? '—' : String(val);
|
||||
let safe = val == null || val === '' ? '—' : String(val);
|
||||
// role-based PII rendering for OIB
|
||||
if (key === 'oib' && safe !== '—') {
|
||||
safe = formatOib(safe, {klub_id: c.klub_id, savez_id: c.savez_id});
|
||||
}
|
||||
return `
|
||||
<div class="payment-row">
|
||||
<div class="l">${esc(label)}${ed?'':' <span style="color:var(--t3);font-size:9px">🔒</span>'}</div>
|
||||
@@ -1313,7 +1318,7 @@ async function loadClanPanel(cid) {
|
||||
<div class="payment-card">
|
||||
<div class="payment-row"><div class="l">Trenutni klub</div><div class="v">${esc(k.naziv || '—')}</div></div>
|
||||
${k.savez_naziv ? `<div class="payment-row"><div class="l">Savez</div><div class="v">${esc(k.savez_naziv)}</div></div>` : ''}
|
||||
${k.oib ? `<div class="payment-row"><div class="l">OIB kluba</div><div class="v">${esc(k.oib)}</div></div>` : ''}
|
||||
${k.oib ? `<div class="payment-row"><div class="l">OIB kluba</div><div class="v">${esc(formatOib(k.oib,{klub_id:k.id,savez_id:k.savez_id}))}</div></div>` : ''}
|
||||
${k.iban ? `<div class="payment-row"><div class="l">IBAN</div><div class="v">${esc(k.iban)}</div></div>` : ''}
|
||||
</div>
|
||||
${d.povijest_klubova && d.povijest_klubova.length ? `
|
||||
|
||||
+4
-3
@@ -82,6 +82,7 @@ tr.clickable:hover { background:var(--bg-3); box-shadow:inset 3px 0 0 var(--acce
|
||||
</style>
|
||||
<link rel="stylesheet" href="/static/shared/sidebar.css">
|
||||
<script src="/static/shared/sidebar.js" defer data-active="racuni"></script>
|
||||
<script src="/static/oib_format.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app">
|
||||
@@ -606,7 +607,7 @@ async function loadInvoices() {
|
||||
<td onclick="openInvoice(${i.id})">${i.invoice_kind||'—'}</td>
|
||||
<td onclick="openInvoice(${i.id})">${i.invoice_no||'—'}</td>
|
||||
<td onclick="openInvoice(${i.id})">${i.vendor_name||'—'}</td>
|
||||
<td onclick="openInvoice(${i.id})" style="font-family:'JetBrains Mono'">${i.vendor_oib||'—'}</td>
|
||||
<td onclick="openInvoice(${i.id})" style="font-family:'JetBrains Mono'">${i.vendor_oib?formatOib(i.vendor_oib,{klub_id:i.klub_id}):'—'}</td>
|
||||
<td onclick="openInvoice(${i.id})">${i.klub_naziv||'—'}</td>
|
||||
<td class="num" onclick="openInvoice(${i.id})">${fmtEur(i.amount_gross)}</td>
|
||||
<td onclick="openInvoice(${i.id})">${sBadge(i.payment_status)}</td>
|
||||
@@ -752,7 +753,7 @@ async function openInvoice(id) {
|
||||
// KV polja
|
||||
$('#inv_kv').innerHTML = `
|
||||
<div>Izdavatelj</div><div>${escHtml(i.vendor_name||'—')}</div>
|
||||
<div>OIB izdavatelja</div><div>${escHtml(i.vendor_oib||'—')}</div>
|
||||
<div>OIB izdavatelja</div><div>${i.vendor_oib?escHtml(formatOib(i.vendor_oib,{klub_id:i.klub_id})):'—'}</div>
|
||||
<div>Broj računa</div><div>${escHtml(i.invoice_no||'—')}</div>
|
||||
<div>Datum</div><div>${fmtDate(i.invoice_date)}</div>
|
||||
<div>Klub</div><div>${escHtml(i.klub_naziv||'—')}</div>
|
||||
@@ -914,7 +915,7 @@ async function openPutni(id) {
|
||||
$('#pn_invoices_table tbody').innerHTML = invs.length ? invs.map(i => `
|
||||
<tr class="clickable" onclick="closeModal('pnModal'); setTimeout(()=>openInvoice(${i.id}), 100)">
|
||||
<td>${i.id}</td><td>${escHtml(i.invoice_kind||'—')}</td><td>${escHtml(i.vendor_name||'—')}</td>
|
||||
<td style="font-family:'JetBrains Mono'">${escHtml(i.vendor_oib||'—')}</td>
|
||||
<td style="font-family:'JetBrains Mono'">${i.vendor_oib?escHtml(formatOib(i.vendor_oib,{klub_id:i.klub_id})):'—'}</td>
|
||||
<td>${fmtDate(i.invoice_date)}</td>
|
||||
<td class="num">${fmtEur(i.amount_gross)}</td>
|
||||
<td>${sBadge(i.payment_status)}</td>
|
||||
|
||||
+2
-1
@@ -471,6 +471,7 @@ table.dt tr:hover td { background:rgba(0,212,255,.025) }
|
||||
.nav-links { display:none }
|
||||
}
|
||||
</style>
|
||||
<script src="/static/oib_format.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -1050,7 +1051,7 @@ async function loadClubs(q='', city='') {
|
||||
<td style="font-weight:700;color:var(--t0)">${r.naziv||r.naziv_pravne_osobe||'–'}</td>
|
||||
<td><span class="chip city">${r.grad||'–'}</span></td>
|
||||
<td style="color:var(--t4);font-size:10px">${r.tip_udruge||r.tip_subjekta||'–'}</td>
|
||||
<td class="mono" style="font-size:9px;color:var(--t4)">${r.oib||'–'}</td>
|
||||
<td class="mono" style="font-size:9px;color:var(--t4)">${r.oib?formatOib(r.oib,{klub_id:r.id,savez_id:r.savez_id}):'–'}</td>
|
||||
<td class="mono" style="font-size:9px;color:var(--t4)">${r.reg_broj||'–'}</td>
|
||||
</tr>`).join('');
|
||||
} catch(e) { $('clubs-tb').innerHTML=`<tr><td colspan="5" style="color:var(--red);padding:12px">Greška: ${e.message}</td></tr>` }
|
||||
|
||||
@@ -560,5 +560,27 @@ $('#cookieMore').addEventListener('click', e => { e.preventDefault(); $('#privac
|
||||
$('#email').focus();
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script>
|
||||
// Auto-detect why user landed on login (session expired/unauthorized)
|
||||
(function(){
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const reason = params.get('reason');
|
||||
if (reason === 'expired') setTimeout(() => {
|
||||
const div = document.createElement('div');
|
||||
div.style.cssText = 'position:fixed;top:20px;left:50%;transform:translateX(-50%);background:#c0392b;color:#fff;padding:12px 20px;border-radius:6px;z-index:9999;font-size:14px';
|
||||
div.textContent = 'Sesija je istekla. Molim prijavi se ponovno.';
|
||||
document.body.appendChild(div);
|
||||
setTimeout(() => div.remove(), 5000);
|
||||
}, 100);
|
||||
if (reason === 'unauthorized') setTimeout(() => {
|
||||
const div = document.createElement('div');
|
||||
div.style.cssText = 'position:fixed;top:20px;left:50%;transform:translateX(-50%);background:#e67e22;color:#fff;padding:12px 20px;border-radius:6px;z-index:9999;font-size:14px';
|
||||
div.textContent = 'Sesija je nevažeća. Prijavi se opet.';
|
||||
document.body.appendChild(div);
|
||||
setTimeout(() => div.remove(), 5000);
|
||||
}, 100);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -230,6 +230,7 @@ table.dt tr:hover td{background:rgba(0,48,135,.1);cursor:pointer}
|
||||
.sh-stats{grid-template-columns:repeat(3,1fr)}
|
||||
}
|
||||
</style>
|
||||
<script src="/static/oib_format.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -711,7 +712,7 @@ async function openSavezDetail(id){
|
||||
<div class="srow"><label>Sport</label><span>${d.sport||'–'}</span></div>
|
||||
<div class="srow"><label>Predsjednik</label><span style="color:var(--cyan)">${d.predsjednik||'–'}</span></div>
|
||||
<div class="srow"><label>Tajnik</label><span style="color:${d.tajnik?'var(--t1)':'var(--red)'}">${d.tajnik||'NULL'}</span></div>
|
||||
<div class="srow"><label>OIB</label><span class="mn">${d.oib||'–'}</span></div>
|
||||
<div class="srow"><label>OIB</label><span class="mn">${d.oib?formatOib(d.oib,{savez_id:d.id}):'–'}</span></div>
|
||||
<div class="srow"><label>Email</label><span>${d.email||'–'}</span></div>
|
||||
</div>
|
||||
<div class="tabs" id="sv-tabs">
|
||||
@@ -815,7 +816,7 @@ async function openKlubDetail(id){
|
||||
<div class="srow"><label>Predsjednik</label><span style="color:var(--cyan)">${k.predsjednik||'–'}</span></div>
|
||||
<div class="srow"><label>Tajnik</label><span style="color:${k.tajnik?'var(--t1)':'var(--red)'}">${k.tajnik||'NULL'}</span></div>
|
||||
<div class="srow"><label>Savez</label><span>${k.savez_naziv||k.savez||'–'}</span></div>
|
||||
<div class="srow"><label>OIB</label><span class="mn">${k.oib||'–'}</span></div>
|
||||
<div class="srow"><label>OIB</label><span class="mn">${k.oib?formatOib(k.oib,{klub_id:k.id,savez_id:k.savez_id}):'–'}</span></div>
|
||||
<div class="srow"><label>Sjedište</label><span style="font-size:10px">${k.sjediste||'–'}</span></div>
|
||||
<div class="srow"><label>Razina</label><span>${k.razina||'–'}</span></div>
|
||||
</div>
|
||||
@@ -909,7 +910,7 @@ async function openSportasProfil(id){
|
||||
</div>
|
||||
<div class="tab-c on" id="sp-t-bio">
|
||||
<div style="background:var(--bg3);border-radius:var(--r);padding:10px">
|
||||
<div class="srow"><label>OIB</label><span class="mn">${c.oib?'••'+c.oib.slice(-3):'–'}</span></div>
|
||||
<div class="srow"><label>OIB</label><span class="mn">${c.oib?formatOib(c.oib,{klub_id:c.klub_id,savez_id:c.savez_id}):'–'}</span></div>
|
||||
<div class="srow"><label>Datum rodjenja</label><span>${c.datum_rodenja||c.datum_rodjenja||'–'}</span></div>
|
||||
<div class="srow"><label>Spol</label><span>${c.spol||'–'}</span></div>
|
||||
<div class="srow"><label>Visina/Težina</label><span>${c.visina_cm||'–'} cm / ${c.tezina_kg||'–'} kg</span></div>
|
||||
@@ -1025,7 +1026,7 @@ async function loadClanarine(){
|
||||
return `<tr onclick="openSportasProfil(${r.clan_id})" style="cursor:pointer">
|
||||
<td style="display:flex;align-items:center;gap:7px">
|
||||
<div style="width:28px;height:28px;border-radius:4px;background:var(--bg3);overflow:hidden;display:flex;align-items:center;justify-content:center;font-size:12px">${r.slika_url?`<img src="${r.slika_url}" style="width:100%;height:100%;object-fit:cover" onerror="this.style.display='none'">`:r.spol==='Ž'?'👩':'👤'}</div>
|
||||
<div><div style="font-weight:600">${r.ime||''} ${r.prezime||''}</div><div style="font-size:8px;color:var(--t4);font-family:var(--mono)">${r.oib||'–'}</div></div>
|
||||
<div><div style="font-weight:600">${r.ime||''} ${r.prezime||''}</div><div style="font-size:8px;color:var(--t4);font-family:var(--mono)">${r.oib?formatOib(r.oib,{klub_id:r.klub_id,savez_id:r.savez_id}):'–'}</div></div>
|
||||
</td>
|
||||
<td style="font-size:10px;color:var(--t2)">${['','I','II','III','IV','V','VI'][r.hoo_kategorija||0]||'–'}</td>
|
||||
<td class="mn">${r.godina||'–'}</td>
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="hr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>PGŽ Sport · Politika privatnosti</title>
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='6' fill='%2306080d'/><text x='16' y='23' text-anchor='middle' font-size='18' font-family='monospace' fill='%2300f0ff'>P</text></svg>">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg: #06080d;
|
||||
--bg-2: #0d1117;
|
||||
--bg-3: #161b22;
|
||||
--border: #1f2937;
|
||||
--text: #e6edf3;
|
||||
--text-2: #8b949e;
|
||||
--text-3: #6e7681;
|
||||
--accent: #00f0ff;
|
||||
--accent-2: #00b8d4;
|
||||
--green: #56d364;
|
||||
--yellow: #d29922;
|
||||
}
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html, body { font-family: 'Inter', system-ui, sans-serif; background: var(--bg); color: var(--text); min-height: 100vh; font-size: 14px; line-height: 1.65; }
|
||||
.wrap { max-width: 880px; margin: 0 auto; padding: 56px 28px 96px; }
|
||||
header { border-bottom: 1px solid var(--border); padding-bottom: 24px; margin-bottom: 32px; }
|
||||
h1 { font-size: 28px; font-weight: 700; letter-spacing: -0.5px; margin-bottom: 6px; }
|
||||
.kicker { color: var(--accent); font-family: 'JetBrains Mono', monospace; font-size: 11px; letter-spacing: 2px; text-transform: uppercase; margin-bottom: 12px; }
|
||||
.meta { color: var(--text-2); font-size: 12px; font-family: 'JetBrains Mono', monospace; }
|
||||
h2 { font-size: 18px; font-weight: 600; margin: 36px 0 12px; color: var(--text); border-left: 3px solid var(--accent); padding-left: 12px; }
|
||||
h3 { font-size: 14px; font-weight: 600; margin: 20px 0 8px; color: var(--text); }
|
||||
p, li { color: var(--text-2); margin-bottom: 10px; }
|
||||
strong { color: var(--text); font-weight: 600; }
|
||||
ul { padding-left: 22px; margin-bottom: 12px; }
|
||||
a { color: var(--accent); text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
.box { background: var(--bg-2); border: 1px solid var(--border); border-radius: 8px; padding: 18px 22px; margin: 16px 0; }
|
||||
.box.warn { border-color: var(--yellow); }
|
||||
.box.ok { border-color: var(--green); }
|
||||
table { width: 100%; border-collapse: collapse; margin: 12px 0 24px; font-size: 13px; }
|
||||
th, td { text-align: left; padding: 10px 12px; border-bottom: 1px solid var(--border); }
|
||||
th { color: var(--accent); font-weight: 600; font-size: 11px; letter-spacing: 1px; text-transform: uppercase; }
|
||||
td { color: var(--text-2); }
|
||||
td strong { color: var(--text); }
|
||||
code { font-family: 'JetBrains Mono', monospace; font-size: 12px; background: var(--bg-3); padding: 1px 6px; border-radius: 3px; color: var(--accent); }
|
||||
.footer-back { margin-top: 48px; padding-top: 24px; border-top: 1px solid var(--border); display: flex; justify-content: space-between; flex-wrap: wrap; gap: 12px; font-size: 12px; color: var(--text-3); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<header>
|
||||
<div class="kicker">PGŽ Sport ERP/CRM · v1 · 2026</div>
|
||||
<h1>Politika privatnosti i zaštite osobnih podataka</h1>
|
||||
<div class="meta">Verzija dokumenta: <strong>v1</strong> · Stupa na snagu: 2026-05-04 · Posljednja izmjena: 2026-05-05</div>
|
||||
</header>
|
||||
|
||||
<div class="box">
|
||||
<p><strong>Voditelj obrade:</strong> Primorsko-goranska županija — Odjel za sport, Slogin kula 2/IV, Rijeka</p>
|
||||
<p><strong>Kontakt za GDPR:</strong> <a href="mailto:gdpr@pgz.hr">gdpr@pgz.hr</a></p>
|
||||
<p><strong>Službenik za zaštitu podataka (DPO):</strong> Damir Radulić — <a href="mailto:damir@rinet.one">damir@rinet.one</a></p>
|
||||
</div>
|
||||
|
||||
<h2>1. Koje podatke prikupljamo</h2>
|
||||
<p>Platforma PGŽ Sport prikuplja i obrađuje sljedeće kategorije osobnih podataka, sukladno Općoj uredbi o zaštiti podataka (GDPR — Uredba (EU) 2016/679) i Zakonu o provedbi Opće uredbe o zaštiti podataka (NN 42/18):</p>
|
||||
<ul>
|
||||
<li><strong>Identifikacijski podaci:</strong> ime, prezime, OIB, datum rođenja, spol</li>
|
||||
<li><strong>Kontakt podaci:</strong> e-pošta, broj telefona, adresa kluba/saveza</li>
|
||||
<li><strong>Funkcijski podaci:</strong> uloga (predsjednik, tajnik, član, trener), klub/savez, kategorija</li>
|
||||
<li><strong>Tehnički podaci:</strong> IP adresa prilikom prijave, identifikator sesije, podaci o uređaju (User-Agent), vrijeme prijave</li>
|
||||
<li><strong>Sigurnosni podaci:</strong> lozinka (hash), 2FA tajna (kriptirana), revocirani tokeni</li>
|
||||
<li><strong>Sportski podaci:</strong> licence, kategorizacija, liječnički pregledi, članarine, transferi</li>
|
||||
</ul>
|
||||
|
||||
<h2>2. Pravna osnova obrade (čl. 6 GDPR)</h2>
|
||||
<table>
|
||||
<thead><tr><th>Kategorija obrade</th><th>Pravna osnova</th><th>Članak</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><strong>Prijava, sigurnost sesije, audit log</strong></td><td>Legitimni interes voditelja obrade</td><td>čl. 6(1)(f)</td></tr>
|
||||
<tr><td><strong>Vođenje registra sportskih klubova</strong></td><td>Pravna obveza (Zakon o sportu, NN 141/22)</td><td>čl. 6(1)(c)</td></tr>
|
||||
<tr><td><strong>Obrada zahtjeva za sufinanciranje</strong></td><td>Izvršavanje zadaće u javnom interesu</td><td>čl. 6(1)(e)</td></tr>
|
||||
<tr><td><strong>Analitički kolačići</strong></td><td>Privola (opt-in)</td><td>čl. 6(1)(a)</td></tr>
|
||||
<tr><td><strong>Marketinške komunikacije</strong></td><td>Privola (opt-in)</td><td>čl. 6(1)(a)</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>3. Vaša prava (čl. 15–22 GDPR)</h2>
|
||||
|
||||
<h3>Članak 15 — Pravo na pristup</h3>
|
||||
<p>Imate pravo dobiti potvrdu obrađuju li se Vaši osobni podaci te pristup tim podacima. Implementirano kroz: <code>GET /api/users/me/gdpr-export</code> (vraća JSON s kompletnim profilom, sesijama, audit logom, povijesti privola, vezama na klub/savez).</p>
|
||||
|
||||
<h3>Članak 16 — Pravo na ispravak</h3>
|
||||
<p>Imate pravo zatražiti ispravak netočnih podataka. Implementirano kroz: <code>PUT /api/auth/me</code> (ime, prezime, OIB, telefon, jezik) i sučelje "Moj profil".</p>
|
||||
|
||||
<h3>Članak 17 — Pravo na brisanje ("pravo na zaborav")</h3>
|
||||
<p>Imate pravo zatražiti brisanje Vaših osobnih podataka kada osnova za obradu prestane. Implementirano kroz: <code>POST /api/users/me/gdpr-erase</code> ili <code>POST /api/gdpr/erase</code>. Zahtjev se obrađuje u roku od 30 dana. Nakon odobrenja, identifikacijski podaci se anonimiziraju (e-pošta postaje <code>erased-{id}@anonymous.gdpr</code>, ime postaje "Erased", OIB i telefon se brišu).</p>
|
||||
<div class="box warn">
|
||||
<p><strong>Napomena:</strong> Pojedini podaci moraju ostati zbog pravne obveze (npr. revizijski trag financijskih transakcija — Zakon o računovodstvu, 11 godina). U tom slučaju podaci se pseudonimiziraju, ali ne brišu u potpunosti.</p>
|
||||
</div>
|
||||
|
||||
<h3>Članak 18 — Pravo na ograničenje obrade</h3>
|
||||
<p>Imate pravo zatražiti privremeno ograničenje obrade dok se ne riješi spor o točnosti podataka. Kontaktirajte <a href="mailto:gdpr@pgz.hr">gdpr@pgz.hr</a>.</p>
|
||||
|
||||
<h3>Članak 20 — Pravo na prenosivost podataka</h3>
|
||||
<p>Imate pravo dobiti svoje podatke u strukturiranom, uobičajeno korištenom i strojno čitljivom formatu (JSON). Implementirano kroz: <code>GET /api/users/me/gdpr-export</code>.</p>
|
||||
|
||||
<h3>Članak 21 — Pravo na prigovor</h3>
|
||||
<p>Imate pravo prigovoriti obradi temeljenoj na legitimnom interesu. Kontaktirajte <a href="mailto:gdpr@pgz.hr">gdpr@pgz.hr</a>.</p>
|
||||
|
||||
<h3>Članak 7(3) — Povlačenje privole</h3>
|
||||
<p>Privola za neobvezne kolačiće (analitika, marketing) može se povući u bilo kojem trenutku, jednako jednostavno kao što je dana. Implementirano kroz: <code>POST /api/users/me/withdraw-consent</code> ili <code>DELETE /api/users/me/gdpr-consent</code>.</p>
|
||||
|
||||
<h2>4. Kolačići</h2>
|
||||
<table>
|
||||
<thead><tr><th>Tip</th><th>Svrha</th><th>Trajanje</th><th>Pravna osnova</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><strong>Nužni</strong></td><td>Sesija, CSRF, sigurnost prijave</td><td>Sesija</td><td>Legitimni interes</td></tr>
|
||||
<tr><td><strong>Funkcionalni</strong></td><td>Postavke jezika, tema, sidebar stanje</td><td>30 dana</td><td>Privola</td></tr>
|
||||
<tr><td><strong>Analitički</strong></td><td>Anonimne statistike korištenja</td><td>365 dana</td><td>Privola</td></tr>
|
||||
<tr><td><strong>Marketinški</strong></td><td>Trenutno se ne koriste</td><td>—</td><td>Privola</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>5. Razdoblja čuvanja</h2>
|
||||
<ul>
|
||||
<li><strong>Audit log</strong> (prijave, izmjene): 5 godina</li>
|
||||
<li><strong>Sesijski tokeni:</strong> max 90 dana, a po odjavi se opozivaju</li>
|
||||
<li><strong>Korisnički profili:</strong> dok je račun aktivan + 1 godina nakon deaktivacije</li>
|
||||
<li><strong>Financijski podaci:</strong> 11 godina (Zakon o računovodstvu, čl. 8)</li>
|
||||
<li><strong>Podaci o članovima klubova:</strong> dok je član registriran u klubu + 5 godina</li>
|
||||
</ul>
|
||||
|
||||
<h2>6. Sigurnosne mjere</h2>
|
||||
<ul>
|
||||
<li>HTTPS (TLS 1.3) za sav promet</li>
|
||||
<li>Lozinke pohranjene kao Argon2/bcrypt hash</li>
|
||||
<li>Dvofaktorska autentikacija (TOTP) dostupna svim korisnicima</li>
|
||||
<li>Audit log svih akcija sa IP adresom i User-Agentom</li>
|
||||
<li>OIB se prikazuje samo administratorima; za ostale korisnike se maskira (<code>•••XXX••</code>)</li>
|
||||
<li>Pristup po načelu najmanjih ovlasti (RBAC) — uloge: super_admin, pgz_admin, savez_admin, klub_admin, klub_user, klub_clan, viewer</li>
|
||||
</ul>
|
||||
|
||||
<h2>7. Dijeljenje podataka s trećim stranama</h2>
|
||||
<p>Vaši podaci se <strong>ne prodaju</strong> i <strong>ne ustupaju</strong> trećim stranama u marketinške svrhe. Podaci se mogu razmjenjivati isključivo s:</p>
|
||||
<ul>
|
||||
<li>Hrvatskim sportskim savezom — kada je to pravna obveza za registraciju kluba/člana</li>
|
||||
<li>Ministarstvom turizma i sporta — pri prijavi za sufinanciranje</li>
|
||||
<li>Nadležnim tijelima (sud, policija) — na temelju pravomoćnog naloga</li>
|
||||
</ul>
|
||||
|
||||
<h2>8. Pritužbe</h2>
|
||||
<p>Pritužbu na obradu osobnih podataka možete podnijeti:</p>
|
||||
<ul>
|
||||
<li>Voditelju obrade: <a href="mailto:gdpr@pgz.hr">gdpr@pgz.hr</a></li>
|
||||
<li>Službeniku za zaštitu podataka: <a href="mailto:damir@rinet.one">damir@rinet.one</a></li>
|
||||
<li>Agenciji za zaštitu osobnih podataka (AZOP), Selska cesta 136, Zagreb — <a href="https://azop.hr" target="_blank" rel="noopener">azop.hr</a></li>
|
||||
</ul>
|
||||
|
||||
<div class="box ok">
|
||||
<p><strong>Strojno čitljiva verzija ove politike:</strong> dostupna na <code>GET /api/gdpr/policy</code> u JSON formatu (verzija, URL, popis prava, kontakti).</p>
|
||||
</div>
|
||||
|
||||
<div class="footer-back">
|
||||
<span>© 2026 Primorsko-goranska županija · Odjel za sport</span>
|
||||
<span><a href="/sport/static/login.html">← Povratak na prijavu</a></span>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
+38
-6
@@ -223,9 +223,41 @@ a.tag:hover,.tag[onclick]:hover{transform:translateY(-1px);filter:brightness(1.1
|
||||
.main{margin-left:0}
|
||||
.pp-stats{grid-template-columns:repeat(3,1fr)}
|
||||
}
|
||||
|
||||
/* === MOBILE RESPONSIVE (CRISIS FIX) === */
|
||||
@media (max-width: 768px) {
|
||||
body { font-size: 13px; }
|
||||
.header { flex-direction: column !important; gap: 8px !important; padding: 10px !important; }
|
||||
.header h1 { font-size: 16px !important; }
|
||||
.nav-tabs { overflow-x: auto; white-space: nowrap; -webkit-overflow-scrolling: touch; }
|
||||
.nav-tabs .tab { display: inline-block !important; }
|
||||
|
||||
.card { padding: 10px !important; }
|
||||
.kpi-grid { grid-template-columns: 1fr 1fr !important; gap: 6px !important; }
|
||||
.kpi-v { font-size: 18px !important; }
|
||||
|
||||
.klubovi-grid, .grid-2, .grid-3, .grid-4 {
|
||||
grid-template-columns: 1fr !important;
|
||||
}
|
||||
|
||||
/* Tables → horizontal scroll */
|
||||
table { font-size: 11px !important; min-width: 480px; }
|
||||
.table-container, .card { overflow-x: auto; }
|
||||
|
||||
/* Drill-down panel full-width */
|
||||
#panel { width: 100vw !important; max-width: 100vw !important; right: -100vw !important; }
|
||||
#panel.open { right: 0 !important; }
|
||||
|
||||
/* Buttons */
|
||||
.btn { padding: 8px 12px !important; font-size: 13px !important; }
|
||||
|
||||
/* Center mobile content */
|
||||
.container, main { padding: 8px !important; max-width: 100% !important; margin: 0 !important; }
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="/static/shared/sidebar.css">
|
||||
<script src="/static/shared/sidebar.js" defer data-active="dashboard"></script>
|
||||
<script src="/static/oib_format.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -1193,7 +1225,7 @@ async function openSavez(id){
|
||||
<div class="card">
|
||||
<div class="card-h"><div class="card-t">📋 Osnovne informacije</div></div>
|
||||
<div class="kv">
|
||||
<div class="k">OIB</div><div class="v">${txt(s.oib)}</div>
|
||||
<div class="k">OIB</div><div class="v">${s.oib?formatOib(s.oib,{savez_id:s.id}):'—'}</div>
|
||||
<div class="k">Adresa</div><div class="v">${txt(s.adresa)}</div>
|
||||
<div class="k">Predsjednik</div><div class="v">${txt(s.predsjednik)}</div>
|
||||
<div class="k">Tajnik</div><div class="v">${txt(s.tajnik)}</div>
|
||||
@@ -1359,7 +1391,7 @@ async function openKlub(id){
|
||||
<div id="k-info" class="ktab">
|
||||
<div class="kv">
|
||||
<div class="k">Naziv</div><div class="v">${esc(k.naziv||'')}</div>
|
||||
<div class="k">OIB</div><div class="v">${txt(k.oib)}</div>
|
||||
<div class="k">OIB</div><div class="v">${k.oib?formatOib(k.oib,{klub_id:k.id,savez_id:k.savez_id}):'—'}</div>
|
||||
<div class="k">Sport</div><div class="v">${txt(k.sport)}</div>
|
||||
<div class="k">Razina</div><div class="v">${txt(k.razina)}</div>
|
||||
<div class="k">Savez</div><div class="v">${txt(k.savez_naziv)}</div>
|
||||
@@ -1699,7 +1731,7 @@ async function openSportas(id){
|
||||
|
||||
<div id="p-bio" class="ptab" style="display:none">
|
||||
<div class="kv">
|
||||
<div class="k">OIB</div><div class="v">${d.oib?'<a class="link-chip" onclick="openOIB("'+esc(d.oib)+'")">'+esc(d.oib)+'</a>':'—'}</div>
|
||||
<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>
|
||||
<div class="k">Datum rođenja</div><div class="v">${fmtDate(dob)}</div>
|
||||
<div class="k">Mjesto rođenja</div><div class="v">${txt(d.mjesto_rodjenja||d.mjesto_rodenja)}</div>
|
||||
<div class="k">Spol</div><div class="v">${txt(d.spol)}</div>
|
||||
@@ -1990,7 +2022,7 @@ function openObjekt(id){
|
||||
<div class="k">Adresa</div><div class="v">${txt(o.adresa)}</div>
|
||||
<div class="k">Grad</div><div class="v">${txt(o.grad)}</div>
|
||||
<div class="k">Upravitelj</div><div class="v">${txt(o.upravitelj)}</div>
|
||||
<div class="k">OIB</div><div class="v">${txt(o.upravitelj_oib)}</div>
|
||||
<div class="k">OIB</div><div class="v">${o.upravitelj_oib?formatOib(o.upravitelj_oib):'—'}</div>
|
||||
<div class="k">Kapacitet</div><div class="v">${o.kapacitet?fmtNum(o.kapacitet)+' mjesta':'—'}</div>
|
||||
<div class="k">Veličina</div><div class="v">${txt(o.veličina)}</div>
|
||||
<div class="k">Sportovi</div><div class="v">${(o.sportovi||[]).map(s=>'<span class="tag b">'+esc(s)+'</span>').join(' ')||'—'}</div>
|
||||
@@ -2477,7 +2509,7 @@ function openMrezaNode(n){
|
||||
<div class="k">ID</div><div class="v" style="font-family:var(--mono);font-size:11px">${esc(n.id)}</div>
|
||||
<div class="k">Tip</div><div class="v">${esc(n.type)}</div>
|
||||
<div class="k">Naziv</div><div class="v">${esc(n.label)}</div>
|
||||
${m.oib?'<div class="k">OIB</div><div class="v" style="font-family:var(--mono)">'+esc(m.oib)+'</div>':''}
|
||||
${m.oib?'<div class="k">OIB</div><div class="v" style="font-family:var(--mono)">'+esc(formatOib(m.oib,{klub_id:m.klub_id,savez_id:m.savez_id}))+'</div>':''}
|
||||
${m.city?'<div class="k">Grad</div><div class="v">'+esc(m.city)+'</div>':''}
|
||||
${m.buyer_contracts!=null?'<div class="k">Ugovori kao kupac</div><div class="v">'+m.buyer_contracts+'</div>':''}
|
||||
${m.buyer_value!=null?'<div class="k">Vrijednost (kupac)</div><div class="v">'+fmtEurFull(m.buyer_value)+'</div>':''}
|
||||
@@ -2942,7 +2974,7 @@ async function runForensicScan(){
|
||||
<div class="alert-card ${cls}">
|
||||
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:12px">
|
||||
<div style="flex:1;min-width:0">
|
||||
<div class="at">${esc(p.name)} <span class="tag b">id ${p.id}</span> ${p.oib?'<a class="tag" onclick="openOIB("'+esc(p.oib)+'")" style="cursor:pointer">OIB '+esc(p.oib)+'</a>':''}</div>
|
||||
<div class="at">${esc(p.name)} <span class="tag b">id ${p.id}</span> ${p.oib?(canSeeFullOib({klub_id:p.klub_id,savez_id:p.savez_id})?'<a class="tag" onclick="openOIB("'+esc(p.oib)+'")" style="cursor:pointer">OIB '+esc(p.oib)+'</a>':'<span class="tag">OIB '+esc(maskOib(p.oib))+'</span>'):''}</div>
|
||||
<div class="ad">${p.function?esc(p.function):''}${p.party?' · '+esc(p.party):''}${p.county?' · '+esc(p.county):''}</div>
|
||||
<div style="margin-top:6px;font-size:11px;color:var(--t2)">
|
||||
🔗 ${(p.links||[]).length} povezanih entiteta
|
||||
|
||||
@@ -185,6 +185,7 @@ table tbody tr.no-click:hover{background:transparent}
|
||||
.pp-stats{grid-template-columns:repeat(3,1fr)}
|
||||
}
|
||||
</style>
|
||||
<script src="/static/oib_format.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -580,7 +581,7 @@ async function openSavez(id){
|
||||
<div class="card">
|
||||
<div class="card-h"><div class="card-t">📋 Osnovne informacije</div></div>
|
||||
<div class="kv">
|
||||
<div class="k">OIB</div><div class="v">${txt(s.oib)}</div>
|
||||
<div class="k">OIB</div><div class="v">${s.oib?formatOib(s.oib,{savez_id:s.id}):'—'}</div>
|
||||
<div class="k">Adresa</div><div class="v">${txt(s.adresa)}</div>
|
||||
<div class="k">Predsjednik</div><div class="v">${txt(s.predsjednik)}</div>
|
||||
<div class="k">Tajnik</div><div class="v">${txt(s.tajnik)}</div>
|
||||
@@ -742,7 +743,7 @@ async function openKlub(id){
|
||||
<div id="k-info" class="ktab">
|
||||
<div class="kv">
|
||||
<div class="k">Naziv</div><div class="v">${esc(k.naziv||'')}</div>
|
||||
<div class="k">OIB</div><div class="v">${txt(k.oib)}</div>
|
||||
<div class="k">OIB</div><div class="v">${k.oib?formatOib(k.oib,{klub_id:k.id,savez_id:k.savez_id}):'—'}</div>
|
||||
<div class="k">Sport</div><div class="v">${txt(k.sport)}</div>
|
||||
<div class="k">Razina</div><div class="v">${txt(k.razina)}</div>
|
||||
<div class="k">Savez</div><div class="v">${txt(k.savez_naziv)}</div>
|
||||
@@ -992,7 +993,7 @@ async function openSportas(id){
|
||||
|
||||
<div id="p-bio" class="ptab" style="display:none">
|
||||
<div class="kv">
|
||||
<div class="k">OIB</div><div class="v">${txt(d.oib)}</div>
|
||||
<div class="k">OIB</div><div class="v">${d.oib?formatOib(d.oib,{klub_id:d.klub_id,savez_id:d.savez_id}):'—'}</div>
|
||||
<div class="k">Datum rođenja</div><div class="v">${fmtDate(dob)}</div>
|
||||
<div class="k">Mjesto rođenja</div><div class="v">${txt(d.mjesto_rodjenja||d.mjesto_rodenja)}</div>
|
||||
<div class="k">Spol</div><div class="v">${txt(d.spol)}</div>
|
||||
@@ -1253,7 +1254,7 @@ function openObjekt(id){
|
||||
<div class="k">Adresa</div><div class="v">${txt(o.adresa)}</div>
|
||||
<div class="k">Grad</div><div class="v">${txt(o.grad)}</div>
|
||||
<div class="k">Upravitelj</div><div class="v">${txt(o.upravitelj)}</div>
|
||||
<div class="k">OIB</div><div class="v">${txt(o.upravitelj_oib)}</div>
|
||||
<div class="k">OIB</div><div class="v">${o.upravitelj_oib?formatOib(o.upravitelj_oib):'—'}</div>
|
||||
<div class="k">Kapacitet</div><div class="v">${o.kapacitet?fmtNum(o.kapacitet)+' mjesta':'—'}</div>
|
||||
<div class="k">Veličina</div><div class="v">${txt(o.veličina)}</div>
|
||||
<div class="k">Sportovi</div><div class="v">${(o.sportovi||[]).map(s=>'<span class="tag b">'+esc(s)+'</span>').join(' ')||'—'}</div>
|
||||
|
||||
@@ -160,6 +160,7 @@ input:focus, select:focus { outline: none; border-color: var(--accent); }
|
||||
@keyframes spin { to { transform: translate(-50%, -50%) rotate(360deg); } }
|
||||
.show { display: block !important; }
|
||||
</style>
|
||||
<script src="/static/oib_format.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -395,7 +396,7 @@ async function handleNodeClick(n) {
|
||||
function renderKlubDetail(d) {
|
||||
const k = d.klub || {};
|
||||
let html = `
|
||||
<div class="field"><span>OIB</span><b>${k.oib || '—'}</b></div>
|
||||
<div class="field"><span>OIB</span><b>${k.oib?formatOib(k.oib,{klub_id:k.id,savez_id:k.savez_id}):'—'}</b></div>
|
||||
<div class="field"><span>Sport</span><b>${k.sport || '—'}</b></div>
|
||||
<div class="field"><span>Grad</span><b>${k.grad || '—'}</b></div>
|
||||
<div class="field"><span>Adresa</span><b>${k.adresa || '—'}</b></div>
|
||||
|
||||
@@ -44,6 +44,7 @@ button{cursor:pointer;font-weight:600}
|
||||
.loader{display:inline-block;width:14px;height:14px;border:2px solid #00f0ff;border-top-color:transparent;border-radius:50%;animation:sp 0.8s linear infinite;vertical-align:middle;margin-right:6px}
|
||||
@keyframes sp{to{transform:rotate(360deg)}}
|
||||
</style>
|
||||
<script src="/static/oib_format.js" defer></script>
|
||||
</head><body>
|
||||
|
||||
<div id="g"></div>
|
||||
@@ -223,7 +224,7 @@ async function openDetail(node) {
|
||||
let html = `<h2>🏆 ${escape(k.naziv || node.name)}</h2>`;
|
||||
html += `<div class="kv"><span>Sport</span><b>${escape(k.sport || '—')}</b></div>`;
|
||||
html += `<div class="kv"><span>Grad</span><b>${escape(k.grad || '—')}</b></div>`;
|
||||
html += `<div class="kv"><span>OIB</span><b>${escape(k.oib || '—')}</b></div>`;
|
||||
html += `<div class="kv"><span>OIB</span><b>${escape(k.oib?formatOib(k.oib,{klub_id:k.id,savez_id:k.savez_id}):'—')}</b></div>`;
|
||||
html += `<div class="kv"><span>Predsjednik</span><b>${escape(k.predsjednik || '—')}</b></div>`;
|
||||
html += `<div class="kv"><span>Tajnik</span><b>${escape(k.tajnik || '—')}</b></div>`;
|
||||
html += `<div class="kv"><span>Email</span><b>${escape(k.email || '—')}</b></div>`;
|
||||
@@ -257,7 +258,7 @@ async function openDetail(node) {
|
||||
const s = await r.json();
|
||||
let html = `<h2>🏛 ${escape(s.naziv || node.name)}</h2>`;
|
||||
html += `<div class="kv"><span>Sport</span><b>${escape(s.sport || '—')}</b></div>`;
|
||||
html += `<div class="kv"><span>OIB</span><b>${escape(s.oib || '—')}</b></div>`;
|
||||
html += `<div class="kv"><span>OIB</span><b>${escape(s.oib?formatOib(s.oib,{savez_id:s.id}):'—')}</b></div>`;
|
||||
html += `<div class="kv"><span>Predsjednik</span><b>${escape(s.predsjednik || '—')}</b></div>`;
|
||||
html += `<div class="kv"><span>Tajnik</span><b>${escape(s.tajnik || '—')}</b></div>`;
|
||||
html += `<div class="kv"><span>Godina osnutka</span><b>${escape(s.godina_osnutka || '—')}</b></div>`;
|
||||
|
||||
Reference in New Issue
Block a user