feat: /api/v2/analiza/* endpoints - sport analytics backend
This commit is contained in:
+181
-12
@@ -139,6 +139,7 @@ table tr:hover td { background: rgba(26, 115, 232, 0.05); }
|
||||
<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>
|
||||
<script src="/static/shared/sortable.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -174,6 +175,25 @@ table tr:hover td { background: rgba(26, 115, 232, 0.05); }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ════════════ Tenant scope picker (added 2026-05-09) ════════════ -->
|
||||
<div id="scope-bar" style="display:flex;align-items:center;gap:10px;padding:8px 14px;border-bottom:1px solid var(--rim);background:var(--bg2);font-size:12px;flex-wrap:wrap">
|
||||
<span style="color:var(--t3);font-size:11px;text-transform:uppercase;letter-spacing:0.6px">Kontekst</span>
|
||||
<label style="display:flex;align-items:center;gap:6px">
|
||||
<span style="color:var(--t2)">Klub:</span>
|
||||
<select id="scope-klub" onchange="onScopeKlubChange(this.value)" style="background:var(--bg3);border:1px solid var(--rim);color:var(--t1);padding:4px 8px;border-radius:4px;font-size:12px;min-width:240px">
|
||||
<option value="">Učitavanje…</option>
|
||||
</select>
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:6px" id="scope-clan-wrap">
|
||||
<span style="color:var(--t2)">Sportaš:</span>
|
||||
<select id="scope-clan" onchange="onScopeClanChange(this.value)" style="background:var(--bg3);border:1px solid var(--rim);color:var(--t1);padding:4px 8px;border-radius:4px;font-size:12px;min-width:220px" disabled>
|
||||
<option value="">— svi —</option>
|
||||
</select>
|
||||
</label>
|
||||
<span id="scope-summary" style="color:var(--t3);font-size:11px;margin-left:auto"></span>
|
||||
<button id="scope-clear" onclick="clearScope()" style="background:transparent;border:1px solid var(--rim);color:var(--t2);padding:3px 10px;border-radius:4px;font-size:11px;cursor:pointer" title="Resetiraj filtere">⟲ resetiraj</button>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div id="page-clanovi" class="page"></div>
|
||||
<div id="page-clanarine" class="page" style="display:none"></div>
|
||||
@@ -263,6 +283,150 @@ function closeModal() {
|
||||
$('#modal').innerHTML = '';
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────
|
||||
// Tenant-aware scope state (added 2026-05-09)
|
||||
// ────────────────────────────────────────────────────
|
||||
//
|
||||
// SCOPE_KLUB_ID — null = "Svi klubovi" (PGŽ users only); else int klub id
|
||||
// SCOPE_CLAN_ID — null = svi sportaši u klubu; else int clan id
|
||||
// SCOPE_USER — JWT payload of logged-in user (or null if anon)
|
||||
// SCOPE_KLUBOVI — list of klubs the current user may pick from
|
||||
//
|
||||
// Backend forces klub_id for klub_user/klub_admin regardless of UI state, so
|
||||
// these globals are purely UX scaffolding.
|
||||
let SCOPE_KLUB_ID = (() => { const v = localStorage.getItem('crm-scope-klub'); return v ? parseInt(v,10) : null; })();
|
||||
let SCOPE_CLAN_ID = (() => { const v = localStorage.getItem('crm-scope-clan'); return v ? parseInt(v,10) : null; })();
|
||||
let SCOPE_USER = null;
|
||||
let SCOPE_KLUBOVI = [];
|
||||
|
||||
function _decodeJwt(t) {
|
||||
try { return JSON.parse(atob(t.split('.')[1].replace(/-/g,'+').replace(/_/g,'/'))); }
|
||||
catch (e) { return null; }
|
||||
}
|
||||
|
||||
function scopeQs(extraParams) {
|
||||
// Append klub_id / clan_id to a URLSearchParams or plain object.
|
||||
const p = (extraParams instanceof URLSearchParams) ? extraParams : new URLSearchParams(extraParams || {});
|
||||
if (SCOPE_KLUB_ID && !p.has('klub_id')) p.set('klub_id', String(SCOPE_KLUB_ID));
|
||||
if (SCOPE_CLAN_ID && !p.has('clan_id')) p.set('clan_id', String(SCOPE_CLAN_ID));
|
||||
return p;
|
||||
}
|
||||
|
||||
async function initScope() {
|
||||
const tok = getJwt();
|
||||
SCOPE_USER = tok ? _decodeJwt(tok) : null;
|
||||
const role = SCOPE_USER?.role || '';
|
||||
const isPgz = ['super_admin','pgz_admin','pgz_user','pgz_finance','pgz_zzjz'].includes(role);
|
||||
// Fetch tenant-scoped klubovi list (backend filters for klub users automatically).
|
||||
let klubs = [];
|
||||
try {
|
||||
const r = await fetch('/sport/api/v2/klubovi?limit=2000', {
|
||||
headers: tok ? {'Authorization':'Bearer ' + tok} : {},
|
||||
});
|
||||
if (r.ok) { const d = await r.json(); klubs = d.rows || []; }
|
||||
} catch (e) { console.warn('initScope klubovi fetch failed', e); }
|
||||
SCOPE_KLUBOVI = klubs;
|
||||
const sel = $('#scope-klub');
|
||||
if (sel) {
|
||||
const opts = [];
|
||||
if (isPgz || !SCOPE_USER) {
|
||||
opts.push('<option value="">— Svi klubovi —</option>');
|
||||
}
|
||||
for (const k of klubs) {
|
||||
const sel2 = (SCOPE_KLUB_ID === k.id) ? ' selected' : '';
|
||||
opts.push(`<option value="${k.id}"${sel2}>${esc(k.naziv)}</option>`);
|
||||
}
|
||||
sel.innerHTML = opts.join('');
|
||||
// For klub users: pre-select their (only) klub if not chosen yet.
|
||||
if (!isPgz && klubs.length === 1 && !SCOPE_KLUB_ID) {
|
||||
SCOPE_KLUB_ID = klubs[0].id;
|
||||
localStorage.setItem('crm-scope-klub', String(SCOPE_KLUB_ID));
|
||||
sel.value = String(SCOPE_KLUB_ID);
|
||||
}
|
||||
if (!isPgz) sel.disabled = (klubs.length <= 1);
|
||||
}
|
||||
await populateClanPicker();
|
||||
refreshScopeSummary();
|
||||
}
|
||||
|
||||
async function populateClanPicker() {
|
||||
const sel = $('#scope-clan');
|
||||
if (!sel) return;
|
||||
if (!SCOPE_KLUB_ID) {
|
||||
sel.innerHTML = '<option value="">— odaberi klub prvo —</option>';
|
||||
sel.disabled = true;
|
||||
return;
|
||||
}
|
||||
sel.disabled = false;
|
||||
sel.innerHTML = '<option value="">Učitavanje…</option>';
|
||||
try {
|
||||
const tok = getJwt();
|
||||
const r = await fetch(`/sport/api/v2/klubovi/${SCOPE_KLUB_ID}/clanovi?limit=500`, {
|
||||
headers: tok ? {'Authorization':'Bearer ' + tok} : {},
|
||||
});
|
||||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||||
const d = await r.json();
|
||||
const arr = d.sportasi || d.rows || d.clanovi || [];
|
||||
const opts = ['<option value="">— svi sportaši —</option>'];
|
||||
for (const c of arr) {
|
||||
const lbl = `${c.ime || ''} ${c.prezime || ''}`.trim() || `#${c.id}`;
|
||||
const sel2 = (SCOPE_CLAN_ID === c.id) ? ' selected' : '';
|
||||
opts.push(`<option value="${c.id}"${sel2}>${esc(lbl)}</option>`);
|
||||
}
|
||||
sel.innerHTML = opts.join('');
|
||||
} catch (e) {
|
||||
sel.innerHTML = '<option value="">— greška u dohvatu —</option>';
|
||||
}
|
||||
}
|
||||
|
||||
function refreshScopeSummary() {
|
||||
const el = $('#scope-summary');
|
||||
if (!el) return;
|
||||
const k = SCOPE_KLUBOVI.find(x => x.id === SCOPE_KLUB_ID);
|
||||
const role = SCOPE_USER?.role || 'anon';
|
||||
const klubTxt = k ? `klub: ${k.naziv}` : 'svi klubovi';
|
||||
const clanTxt = SCOPE_CLAN_ID ? ` · sportaš: #${SCOPE_CLAN_ID}` : '';
|
||||
el.textContent = `${role} · ${klubTxt}${clanTxt}`;
|
||||
}
|
||||
|
||||
function onScopeKlubChange(v) {
|
||||
SCOPE_KLUB_ID = v ? parseInt(v, 10) : null;
|
||||
SCOPE_CLAN_ID = null;
|
||||
if (SCOPE_KLUB_ID) localStorage.setItem('crm-scope-klub', String(SCOPE_KLUB_ID));
|
||||
else localStorage.removeItem('crm-scope-klub');
|
||||
localStorage.removeItem('crm-scope-clan');
|
||||
populateClanPicker().then(refreshScopeSummary);
|
||||
reloadCurrentTab();
|
||||
}
|
||||
|
||||
function onScopeClanChange(v) {
|
||||
SCOPE_CLAN_ID = v ? parseInt(v, 10) : null;
|
||||
if (SCOPE_CLAN_ID) localStorage.setItem('crm-scope-clan', String(SCOPE_CLAN_ID));
|
||||
else localStorage.removeItem('crm-scope-clan');
|
||||
refreshScopeSummary();
|
||||
reloadCurrentTab();
|
||||
}
|
||||
|
||||
function clearScope() {
|
||||
// PGŽ users → reset both. Klub users → reset only sportaš (klub is forced).
|
||||
const role = SCOPE_USER?.role || '';
|
||||
const isPgz = ['super_admin','pgz_admin','pgz_user','pgz_finance','pgz_zzjz'].includes(role);
|
||||
if (isPgz || !SCOPE_USER) {
|
||||
SCOPE_KLUB_ID = null;
|
||||
localStorage.removeItem('crm-scope-klub');
|
||||
const sel = $('#scope-klub'); if (sel) sel.value = '';
|
||||
}
|
||||
SCOPE_CLAN_ID = null;
|
||||
localStorage.removeItem('crm-scope-clan');
|
||||
populateClanPicker().then(refreshScopeSummary);
|
||||
reloadCurrentTab();
|
||||
}
|
||||
|
||||
function reloadCurrentTab() {
|
||||
const active = document.querySelector('.tab.active');
|
||||
if (active && active.dataset.tab) setTab(active.dataset.tab);
|
||||
}
|
||||
|
||||
// Globalna rola (postavlja se preko dropdowna u topbaru)
|
||||
let CURRENT_ROLE = localStorage.getItem('crm-role') || 'pgz_admin';
|
||||
|
||||
@@ -307,7 +471,8 @@ async function loadClanarine() {
|
||||
root.innerHTML = '<div class="loading">Učitavanje članarina…</div>';
|
||||
let data;
|
||||
try {
|
||||
data = await api('/clanarine?limit=200');
|
||||
const qs = scopeQs({limit: 200});
|
||||
data = await api('/clanarine?' + qs.toString());
|
||||
} catch (e) { root.innerHTML = `<div class="empty">Greška: ${esc(e.message)}</div>`; return; }
|
||||
$('#cnt-clanarine').textContent = data.count;
|
||||
const s = data.summary || {};
|
||||
@@ -474,11 +639,12 @@ async function loadClanarineFiltered() {
|
||||
const status = $('#cl-status').value;
|
||||
const godina = $('#cl-godina').value;
|
||||
const klub = $('#cl-klub').value;
|
||||
const params = new URLSearchParams({limit: 200});
|
||||
if (status) params.append('status', status);
|
||||
if (godina) params.append('godina', godina);
|
||||
if (klub) params.append('klub_id', klub);
|
||||
const data = await api('/clanarine?' + params);
|
||||
const params = scopeQs({limit: 200});
|
||||
if (status) params.set('status', status);
|
||||
if (godina) params.set('godina', godina);
|
||||
// Inline klub filter overrides scope picker (back-compat with old toolbar input)
|
||||
if (klub) params.set('klub_id', klub);
|
||||
const data = await api('/clanarine?' + params.toString());
|
||||
const tbody = $('#page-clanarine table tbody');
|
||||
tbody.innerHTML = (data.rows || []).map(r => `
|
||||
<tr data-id="${r.id}" data-dug="${r.dug||0}" data-paid="${r.dug<=0?1:0}">
|
||||
@@ -642,7 +808,7 @@ async function loadLijecnicki() {
|
||||
const root = $('#page-lijecnicki');
|
||||
root.innerHTML = '<div class="loading">Učitavanje pregleda…</div>';
|
||||
let data;
|
||||
try { data = await api('/lijecnicki?limit=200'); }
|
||||
try { data = await api('/lijecnicki?' + scopeQs({limit: 200}).toString()); }
|
||||
catch (e) { root.innerHTML = `<div class="empty">Greška: ${esc(e.message)}</div>`; return; }
|
||||
$('#cnt-lijecnicki').textContent = data.count;
|
||||
const s = data.summary || {};
|
||||
@@ -694,10 +860,10 @@ async function loadLijecnicki() {
|
||||
async function loadLijecnickiFiltered() {
|
||||
const status = $('#lj-status').value;
|
||||
const klub = $('#lj-klub').value;
|
||||
const params = new URLSearchParams({limit: 200});
|
||||
if (status) params.append('status', status);
|
||||
if (klub) params.append('klub_id', klub);
|
||||
const data = await api('/lijecnicki?' + params);
|
||||
const params = scopeQs({limit: 200});
|
||||
if (status) params.set('status', status);
|
||||
if (klub) params.set('klub_id', klub);
|
||||
const data = await api('/lijecnicki?' + params.toString());
|
||||
const tbody = $('#page-lijecnicki table tbody');
|
||||
tbody.innerHTML = (data.rows || []).map(r => `
|
||||
<tr>
|
||||
@@ -1840,7 +2006,9 @@ async function createTpl(e) {
|
||||
const _roleSel = document.getElementById('g-role');
|
||||
if (_roleSel) _roleSel.value = CURRENT_ROLE;
|
||||
|
||||
loadClanovi();
|
||||
// init scope picker (klubovi + sportaši) prije prvog tab loada — ako klub user
|
||||
// nema linkanih klubova, backend će ionako vratiti 403 pa UI pokazuje prazno.
|
||||
initScope().then(() => loadClanovi()).catch(() => loadClanovi());
|
||||
// preload counts za sve tabove
|
||||
(async () => {
|
||||
try {
|
||||
@@ -1856,5 +2024,6 @@ loadClanovi();
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script src="/static/_ai_widget.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user