8dce58c5f9
Shared module:
- /static/shared/sidebar.css ← unified CSS (#pgz-sb, .pgz-collapsed, mobile overlay, tooltip)
- /static/shared/sidebar.js ← auto-mounting JS shell + PGZSidebar API
* Auto-renders #pgz-sb na <body> start (data-inline=1 to opt out)
* NAV_EXTERNAL: Prijava, Aplikacija, Administracija, CRM, ERP, KPI, Audit, Public portal
* Toggle (≡) -> localStorage 'sidebarCollapsed' (perzistira preko SVIH stranica)
* Mobile <768px: ≡ burger + ✕ close, body backdrop
* Loads /api/auth/me u footer (avatar/username/uloga); ⎋ logout briše JWT i ide na /login
* data-active="<key>" highlight aktivnog portala
Page integration:
- sport2.html ← inline NAV_EXTERNAL u buildNav() + "Portali" separator (zadrži postojeći sidebar)
- app.html ← inline NAV_EXTERNAL u buildNav() (zadrži role-based interni nav, dopuni Portalima)
- admin.html ← Portali stavke u <aside class="sidebar"> (matching .nav-item style)
- erp.html ← Portali stavke u <aside class="sidebar"> (matching .nav-item style)
- crm.html ← include shared sidebar.css + sidebar.js data-active="crm"
- audit.html ← include shared sidebar.css + sidebar.js data-active="audit"
- kpi.html ← include shared sidebar.css + sidebar.js data-active="kpi"
- login.html ← include shared sidebar.css + sidebar.js data-active="login"
Backups: _backups/{*.cc3_pre_unified_sidebar.*}
Live verified: 8 pages serve HTTP 200; sidebar.css/js HTTP 200; portal markers per page OK.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1365 lines
72 KiB
HTML
1365 lines
72 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="hr">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>PGŽ Sport — CRM (Članarine • Liječnički • Obrasci)</title>
|
||
<style>
|
||
:root {
|
||
--pgz-blue:#1a73e8; --pgz-blue2:#1e3a8a;
|
||
--bg:#0f1115; --bg2:#171a21; --bg3:#1f242d;
|
||
--rim:#293040; --t1:#e6e8ef; --t2:#9aa3b6; --t3:#6b748b;
|
||
--ok:#22c55e; --warn:#f59e0b; --err:#ef4444; --info:#3b82f6;
|
||
}
|
||
* { box-sizing: border-box; }
|
||
body { margin:0; font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
|
||
background: var(--bg); color: var(--t1); font-size: 14px; }
|
||
.topbar {
|
||
height: 54px; background: linear-gradient(90deg, var(--pgz-blue2), var(--pgz-blue));
|
||
display: flex; align-items: center; padding: 0 18px; gap: 16px;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.4);
|
||
}
|
||
.topbar .logo { font-weight: 700; font-size: 16px; }
|
||
.topbar .sep { color: rgba(255,255,255,0.5); }
|
||
.topbar .title { font-size: 14px; opacity: 0.95; }
|
||
.topbar .right { margin-left: auto; display: flex; gap: 10px; align-items: center; font-size: 12px; }
|
||
.topbar a { color: #fff; text-decoration: none; opacity: 0.8; padding: 6px 10px; border-radius: 4px; }
|
||
.topbar a:hover { opacity: 1; background: rgba(255,255,255,0.1); }
|
||
|
||
.tabs { display: flex; background: var(--bg2); border-bottom: 1px solid var(--rim); padding: 0 18px; }
|
||
.tab { padding: 14px 20px; cursor: pointer; color: var(--t2); border-bottom: 2px solid transparent;
|
||
font-weight: 500; user-select: none; }
|
||
.tab:hover { color: var(--t1); }
|
||
.tab.active { color: var(--pgz-blue); border-bottom-color: var(--pgz-blue); background: var(--bg3); }
|
||
.tab .count { background: var(--bg3); color: var(--t2); padding: 2px 8px; border-radius: 10px;
|
||
font-size: 11px; margin-left: 6px; }
|
||
.tab.active .count { background: var(--pgz-blue); color: #fff; }
|
||
|
||
.container { padding: 18px; }
|
||
|
||
.toolbar { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 14px; align-items: center; }
|
||
.toolbar input, .toolbar select {
|
||
background: var(--bg2); border: 1px solid var(--rim); color: var(--t1);
|
||
padding: 7px 11px; border-radius: 5px; font-size: 13px; min-width: 140px;
|
||
}
|
||
.toolbar input:focus, .toolbar select:focus { outline: none; border-color: var(--pgz-blue); }
|
||
.toolbar .grow { flex: 1; }
|
||
|
||
.btn { background: var(--bg3); color: var(--t1); border: 1px solid var(--rim);
|
||
padding: 7px 13px; border-radius: 5px; cursor: pointer; font-size: 13px;
|
||
font-family: inherit; }
|
||
.btn:hover { background: var(--bg2); border-color: var(--pgz-blue); }
|
||
.btn.primary { background: linear-gradient(135deg, var(--pgz-blue), var(--pgz-blue2)); border-color: var(--pgz-blue); color:#fff; }
|
||
.btn.primary:hover { filter: brightness(1.1); }
|
||
.btn.danger { color: var(--err); border-color: var(--err); }
|
||
.btn.sm { padding: 4px 8px; font-size: 12px; }
|
||
|
||
.kpi-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||
gap: 12px; margin-bottom: 14px; }
|
||
.kpi { background: var(--bg2); border: 1px solid var(--rim); padding: 12px 14px; border-radius: 8px; }
|
||
.kpi.g { border-left: 3px solid var(--ok); }
|
||
.kpi.r { border-left: 3px solid var(--err); }
|
||
.kpi.a { border-left: 3px solid var(--warn); }
|
||
.kpi.b { border-left: 3px solid var(--pgz-blue); }
|
||
.kpi-l { font-size: 11px; color: var(--t2); text-transform: uppercase; letter-spacing: 0.5px; }
|
||
.kpi-v { font-size: 22px; font-weight: 700; margin-top: 4px; }
|
||
.kpi-s { font-size: 11px; color: var(--t3); margin-top: 2px; }
|
||
|
||
.card { background: var(--bg2); border: 1px solid var(--rim); border-radius: 8px;
|
||
margin-bottom: 14px; overflow: hidden; }
|
||
.card-h { padding: 12px 16px; border-bottom: 1px solid var(--rim); display: flex; align-items: center;
|
||
justify-content: space-between; background: var(--bg3); }
|
||
.card-t { font-weight: 600; font-size: 14px; }
|
||
.card-b { padding: 14px 16px; }
|
||
|
||
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||
table th, table td { padding: 9px 12px; text-align: left; border-bottom: 1px solid var(--rim); }
|
||
table th { background: var(--bg3); color: var(--t2); font-weight: 600; font-size: 11px;
|
||
text-transform: uppercase; letter-spacing: 0.4px; }
|
||
table tr:hover td { background: rgba(26, 115, 232, 0.05); }
|
||
|
||
.tag { display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 11px; font-weight: 600; }
|
||
.tag.gr { background: rgba(34,197,94,0.2); color: var(--ok); }
|
||
.tag.am { background: rgba(245,158,11,0.2); color: var(--warn); }
|
||
.tag.rd { background: rgba(239,68,68,0.2); color: var(--err); }
|
||
.tag.bl { background: rgba(26,115,232,0.2); color: var(--pgz-blue); }
|
||
.tag.gy { background: rgba(154,163,182,0.2); color: var(--t2); }
|
||
|
||
.empty { text-align: center; padding: 40px; color: var(--t3); }
|
||
.loading { text-align: center; padding: 30px; color: var(--t2); }
|
||
|
||
.modal-bg { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.7);
|
||
display: none; justify-content: center; align-items: flex-start; padding-top: 5vh; z-index: 1000; }
|
||
.modal-bg.open { display: flex; }
|
||
.modal { background: var(--bg2); border: 1px solid var(--rim); border-radius: 8px;
|
||
width: 92%; max-width: 720px; max-height: 90vh; overflow-y: auto; }
|
||
.modal-h { padding: 14px 18px; border-bottom: 1px solid var(--rim); display: flex;
|
||
justify-content: space-between; align-items: center; background: var(--bg3); }
|
||
.modal-t { font-weight: 600; font-size: 15px; }
|
||
.modal-x { cursor: pointer; color: var(--t2); font-size: 22px; line-height: 1; padding: 0 4px; }
|
||
.modal-x:hover { color: var(--err); }
|
||
.modal-b { padding: 18px; }
|
||
|
||
.field { margin-bottom: 12px; }
|
||
.field label { display: block; font-size: 12px; color: var(--t2); margin-bottom: 4px;
|
||
text-transform: uppercase; letter-spacing: 0.3px; }
|
||
.field label.req::after { content: " *"; color: var(--err); }
|
||
.field input, .field select, .field textarea {
|
||
width: 100%; background: var(--bg); border: 1px solid var(--rim); color: var(--t1);
|
||
padding: 8px 12px; border-radius: 5px; font-size: 13px; font-family: inherit;
|
||
}
|
||
.field input:focus, .field select:focus, .field textarea:focus { outline: none; border-color: var(--pgz-blue); }
|
||
.field textarea { min-height: 70px; resize: vertical; }
|
||
.field .help { font-size: 11px; color: var(--t3); margin-top: 3px; }
|
||
|
||
.payment-card { background: var(--bg); border: 1px solid var(--rim); border-radius: 6px;
|
||
padding: 14px; margin-top: 12px; }
|
||
.payment-row { display: flex; justify-content: space-between; padding: 6px 0; border-bottom: 1px dashed var(--rim); }
|
||
.payment-row:last-child { border-bottom: none; }
|
||
.payment-row .l { color: var(--t2); font-size: 12px; }
|
||
.payment-row .v { font-weight: 600; font-family: 'SF Mono', Consolas, monospace; }
|
||
.payment-row .v.big { font-size: 18px; color: var(--pgz-blue); }
|
||
|
||
.qr-box { display: flex; gap: 16px; align-items: center; margin: 14px 0; }
|
||
.qr-box img { width: 160px; height: 160px; background: #fff; padding: 8px; border-radius: 6px; }
|
||
.qr-box .qr-info { flex: 1; }
|
||
|
||
.signature-box { background: var(--bg); border: 1px solid var(--rim); border-radius: 6px;
|
||
padding: 14px; margin-top: 14px; font-family: 'SF Mono', Consolas, monospace;
|
||
font-size: 11px; word-break: break-all; }
|
||
.signature-box .sha { color: var(--ok); }
|
||
|
||
.toast { position: fixed; bottom: 20px; right: 20px; background: var(--bg3); border: 1px solid var(--rim);
|
||
padding: 10px 16px; border-radius: 6px; font-size: 13px; z-index: 2000;
|
||
border-left: 3px solid var(--ok); transform: translateX(120%); transition: transform 0.3s; }
|
||
.toast.show { transform: translateX(0); }
|
||
.toast.err { border-left-color: var(--err); }
|
||
</style>
|
||
<link rel="stylesheet" href="/sport/static/shared/sidebar.css">
|
||
<script src="/sport/static/shared/sidebar.js" defer data-active="crm"></script>
|
||
</head>
|
||
<body>
|
||
|
||
<div class="topbar">
|
||
<div class="logo">⬢ PGŽ SPORT</div>
|
||
<div class="sep">·</div>
|
||
<div class="title">CRM — Članarine • Liječnički • Obrasci</div>
|
||
<div class="right">
|
||
<span style="opacity:.7">Round 3 / CC5</span>
|
||
<a href="/sport/static/sport2.html">← portal</a>
|
||
<a href="/sport/static/app.html">app →</a>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="tabs">
|
||
<div class="tab active" data-tab="clanovi" onclick="setTab('clanovi')">👤 Članovi <span class="count" id="cnt-clanovi">…</span></div>
|
||
<div class="tab" data-tab="clanarine" onclick="setTab('clanarine')">€ Članarine <span class="count" id="cnt-clanarine">…</span></div>
|
||
<div class="tab" data-tab="lijecnicki" onclick="setTab('lijecnicki')">⚕ Liječnički pregledi <span class="count" id="cnt-lijecnicki">…</span></div>
|
||
<div class="tab" data-tab="obrasci" onclick="setTab('obrasci')">📝 Obrasci <span class="count" id="cnt-obrasci">…</span></div>
|
||
<div style="margin-left:auto;display:flex;align-items:center;gap:8px;padding:0 14px">
|
||
<span style="font-size:11px;color:var(--t3)">ROLA:</span>
|
||
<select id="g-role" onchange="setRole(this.value)" style="background:var(--bg3);border:1px solid var(--rim);color:var(--t1);padding:4px 8px;border-radius:4px;font-size:12px">
|
||
<option value="pgz_admin">pgz_admin (full)</option>
|
||
<option value="klub_admin">klub_admin (sve osim OIB)</option>
|
||
<option value="savez_admin">savez_admin (samo napomena)</option>
|
||
<option value="klub_trener">klub_trener (sport polja)</option>
|
||
<option value="sportas">sportas (kontakt + slika)</option>
|
||
<option value="viewer">viewer (read-only)</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="container">
|
||
<div id="page-clanovi" class="page"></div>
|
||
<div id="page-clanarine" class="page" style="display:none"></div>
|
||
<div id="page-lijecnicki" class="page" style="display:none"></div>
|
||
<div id="page-obrasci" class="page" style="display:none"></div>
|
||
</div>
|
||
|
||
<div id="modal-bg" class="modal-bg" onclick="if(event.target===this)closeModal()">
|
||
<div class="modal" id="modal"></div>
|
||
</div>
|
||
|
||
<div id="toast" class="toast"></div>
|
||
|
||
<script>
|
||
// ────────────────────────────────────────────────────
|
||
// Helpers
|
||
// ────────────────────────────────────────────────────
|
||
const API = '/sport/api/crm';
|
||
const $ = (s, root=document) => root.querySelector(s);
|
||
const $$ = (s, root=document) => Array.from(root.querySelectorAll(s));
|
||
const esc = s => String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||
const fmtEur = v => (v == null) ? '—' : Number(v).toLocaleString('hr-HR', {minimumFractionDigits:2, maximumFractionDigits:2}) + ' €';
|
||
const fmt = v => (v == null) ? '—' : Number(v).toLocaleString('hr-HR');
|
||
const fmtDate = d => !d ? '—' : new Date(d).toLocaleDateString('hr-HR');
|
||
|
||
async function api(path, opts={}) {
|
||
const o = Object.assign({headers: {'Content-Type':'application/json'}}, opts);
|
||
if (o.body && typeof o.body !== 'string') o.body = JSON.stringify(o.body);
|
||
const r = await fetch(API + path, o);
|
||
if (!r.ok) {
|
||
const msg = await r.text().catch(()=>r.statusText);
|
||
throw new Error(`HTTP ${r.status}: ${msg.substring(0,200)}`);
|
||
}
|
||
return r.json();
|
||
}
|
||
|
||
function toast(msg, isErr=false) {
|
||
const t = $('#toast');
|
||
t.textContent = msg;
|
||
t.classList.toggle('err', isErr);
|
||
t.classList.add('show');
|
||
setTimeout(() => t.classList.remove('show'), 3500);
|
||
}
|
||
|
||
function openModal(html) {
|
||
$('#modal').innerHTML = html;
|
||
$('#modal-bg').classList.add('open');
|
||
}
|
||
function closeModal() {
|
||
$('#modal-bg').classList.remove('open');
|
||
$('#modal').innerHTML = '';
|
||
}
|
||
|
||
// Globalna rola (postavlja se preko dropdowna u topbaru)
|
||
let CURRENT_ROLE = localStorage.getItem('crm-role') || 'pgz_admin';
|
||
|
||
function setRole(r) {
|
||
CURRENT_ROLE = r;
|
||
localStorage.setItem('crm-role', r);
|
||
toast('Rola postavljena: ' + r);
|
||
// ako je otvoren panel, refreshaj edit dozvole
|
||
if (window._OPEN_PANEL_CID) loadClanPanel(window._OPEN_PANEL_CID);
|
||
}
|
||
|
||
// Wrapper za API koji dodaje X-Role
|
||
async function apiR(path, opts={}) {
|
||
const o = Object.assign({headers: {'Content-Type':'application/json', 'X-Role': CURRENT_ROLE}}, opts);
|
||
if (o.body && typeof o.body !== 'string') o.body = JSON.stringify(o.body);
|
||
const r = await fetch(API + path, o);
|
||
if (!r.ok) {
|
||
const msg = await r.text().catch(()=>r.statusText);
|
||
throw new Error(`HTTP ${r.status}: ${msg.substring(0,200)}`);
|
||
}
|
||
return r.json();
|
||
}
|
||
|
||
function setTab(name) {
|
||
$$('.tab').forEach(t => t.classList.toggle('active', t.dataset.tab === name));
|
||
$$('.page').forEach(p => p.style.display = (p.id === 'page-' + name) ? 'block' : 'none');
|
||
if (name === 'clanovi') loadClanovi();
|
||
if (name === 'clanarine') loadClanarine();
|
||
if (name === 'lijecnicki') loadLijecnicki();
|
||
if (name === 'obrasci') loadObrasci();
|
||
}
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// MODUL 1 — ČLANARINE (M7)
|
||
// ════════════════════════════════════════════════════
|
||
|
||
async function loadClanarine() {
|
||
const root = $('#page-clanarine');
|
||
root.innerHTML = '<div class="loading">Učitavanje članarina…</div>';
|
||
let data;
|
||
try {
|
||
data = await api('/clanarine?limit=200');
|
||
} catch (e) { root.innerHTML = `<div class="empty">Greška: ${esc(e.message)}</div>`; return; }
|
||
$('#cnt-clanarine').textContent = data.count;
|
||
const s = data.summary || {};
|
||
const kpi = `
|
||
<div class="kpi-grid">
|
||
<div class="kpi b"><div class="kpi-l">Ukupno zaduženja</div><div class="kpi-v">${fmt(s.total)}</div></div>
|
||
<div class="kpi g"><div class="kpi-l">Naplaćeno</div><div class="kpi-v">${fmtEur(s.total_placen)}</div></div>
|
||
<div class="kpi r"><div class="kpi-l">Dug</div><div class="kpi-v">${fmtEur(s.total_dug)}</div></div>
|
||
<div class="kpi a"><div class="kpi-l">Nepodmireno</div><div class="kpi-v">${fmt(s.n_nepodmireno)}</div></div>
|
||
</div>`;
|
||
const tools = `
|
||
<div class="toolbar">
|
||
<select id="cl-status" onchange="loadClanarineFiltered()">
|
||
<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>
|
||
<input id="cl-godina" type="number" placeholder="Godina" min="2020" max="2030" onchange="loadClanarineFiltered()">
|
||
<input id="cl-klub" type="number" placeholder="Klub ID" onchange="loadClanarineFiltered()">
|
||
<div class="grow"></div>
|
||
<button class="btn primary" onclick="bulkNotify()">📧 Notify dužnike</button>
|
||
<button class="btn" onclick="newClanarinaModal()">+ Novo zaduženje</button>
|
||
</div>`;
|
||
const rows = (data.rows || []).map(r => `
|
||
<tr>
|
||
<td><b>${esc(r.clan)}</b><div style="font-size:11px;color:var(--t3)">${esc(r.klub || '')}</div></td>
|
||
<td>${esc(r.godina)}</td>
|
||
<td>${esc(r.razdoblje || '')}</td>
|
||
<td>${fmtEur(r.iznos_propisan)}</td>
|
||
<td>${fmtEur(r.iznos_placen)}</td>
|
||
<td><b style="color:${r.dug>0?'var(--err)':'var(--ok)'}">${fmtEur(r.dug)}</b></td>
|
||
<td><span class="tag ${statusTag(r.status)}">${esc(r.status)}</span></td>
|
||
<td>
|
||
<button class="btn sm" onclick="openPayment(${r.id})" title="Pregled plaćanja">💳</button>
|
||
<button class="btn sm" onclick="openUplata(${r.id})" title="Registriraj uplatu">+€</button>
|
||
<a class="btn sm" href="${API}/clanarine/${r.id}/uplatnica.pdf" target="_blank" title="HUB-3 PDF">📄</a>
|
||
</td>
|
||
</tr>`).join('');
|
||
|
||
root.innerHTML = kpi + tools + `
|
||
<div class="card">
|
||
<div class="card-h"><div class="card-t">Lista članarina (${data.count})</div></div>
|
||
<table>
|
||
<thead><tr><th>Sportaš/Klub</th><th>God.</th><th>Razdoblje</th><th>Propisan</th><th>Plaćeno</th><th>Dug</th><th>Status</th><th></th></tr></thead>
|
||
<tbody>${rows || '<tr><td colspan="8" class="empty">Nema zapisa.</td></tr>'}</tbody>
|
||
</table>
|
||
</div>`;
|
||
}
|
||
|
||
function statusTag(s) {
|
||
return ({nepodmireno:'rd', djelomicno:'am', podmireno:'gr', storno:'gy'})[s] || 'gy';
|
||
}
|
||
|
||
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 tbody = $('#page-clanarine table tbody');
|
||
tbody.innerHTML = (data.rows || []).map(r => `
|
||
<tr>
|
||
<td><b>${esc(r.clan)}</b><div style="font-size:11px;color:var(--t3)">${esc(r.klub || '')}</div></td>
|
||
<td>${esc(r.godina)}</td>
|
||
<td>${esc(r.razdoblje || '')}</td>
|
||
<td>${fmtEur(r.iznos_propisan)}</td>
|
||
<td>${fmtEur(r.iznos_placen)}</td>
|
||
<td><b style="color:${r.dug>0?'var(--err)':'var(--ok)'}">${fmtEur(r.dug)}</b></td>
|
||
<td><span class="tag ${statusTag(r.status)}">${esc(r.status)}</span></td>
|
||
<td>
|
||
<button class="btn sm" onclick="openPayment(${r.id})">💳</button>
|
||
<button class="btn sm" onclick="openUplata(${r.id})">+€</button>
|
||
<a class="btn sm" href="${API}/clanarine/${r.id}/uplatnica.pdf" target="_blank">📄</a>
|
||
</td>
|
||
</tr>`).join('') || '<tr><td colspan="8" class="empty">Nema zapisa.</td></tr>';
|
||
}
|
||
|
||
async function openPayment(id) {
|
||
let info;
|
||
try { info = await api('/clanarine/' + id + '/payment-info'); }
|
||
catch (e) { return toast('Greška: ' + e.message, true); }
|
||
openModal(`
|
||
<div class="modal-h">
|
||
<div class="modal-t">💳 Podaci za plaćanje #${id}</div>
|
||
<div class="modal-x" onclick="closeModal()">×</div>
|
||
</div>
|
||
<div class="modal-b">
|
||
<div class="qr-box">
|
||
<img src="${API}/clanarine/${id}/qr.png" alt="EPC QR">
|
||
<div class="qr-info">
|
||
<p style="margin:0 0 8px;color:var(--t2);font-size:12px">Skenirajte QR mobilnom bankom (Zaba / PBZ / Erste / OTP / RBA) — popunit će sve podatke za uplatu.</p>
|
||
<a class="btn primary" href="${API}/clanarine/${id}/uplatnica.pdf" target="_blank">📄 HUB-3 PDF (uplatnica)</a>
|
||
</div>
|
||
</div>
|
||
<div class="payment-card">
|
||
<div class="payment-row"><div class="l">Iznos za uplatu</div><div class="v big">${fmtEur(info.iznos_eur)}</div></div>
|
||
<div class="payment-row"><div class="l">Primatelj</div><div class="v">${esc(info.primatelj)}</div></div>
|
||
<div class="payment-row"><div class="l">IBAN</div><div class="v">${esc(info.iban)}</div></div>
|
||
<div class="payment-row"><div class="l">Model</div><div class="v">${esc(info.model)}</div></div>
|
||
<div class="payment-row"><div class="l">Poziv na broj</div><div class="v">${esc(info.poziv_na_broj)}</div></div>
|
||
<div class="payment-row"><div class="l">Opis</div><div class="v">${esc(info.opis)}</div></div>
|
||
</div>
|
||
<details style="margin-top:14px">
|
||
<summary style="cursor:pointer;color:var(--t2);font-size:12px">EPC QR payload (BCD/002 SCT)</summary>
|
||
<pre style="background:var(--bg);padding:10px;border-radius:5px;font-size:11px;overflow:auto;margin-top:6px">${esc(info.epc_payload)}</pre>
|
||
</details>
|
||
</div>`);
|
||
}
|
||
|
||
function openUplata(id) {
|
||
openModal(`
|
||
<div class="modal-h">
|
||
<div class="modal-t">+€ Registriraj uplatu (članarina #${id})</div>
|
||
<div class="modal-x" onclick="closeModal()">×</div>
|
||
</div>
|
||
<div class="modal-b">
|
||
<form onsubmit="submitUplata(event, ${id})">
|
||
<div class="field"><label class="req">Iznos uplate (EUR)</label>
|
||
<input name="iznos" type="number" step="0.01" min="0.01" required></div>
|
||
<div class="field"><label>Datum uplate</label>
|
||
<input name="datum_uplate" type="date" value="${new Date().toISOString().slice(0,10)}"></div>
|
||
<div class="field"><label>Način uplate</label>
|
||
<select name="nacin_uplate">
|
||
<option value="transakcijski">Transakcijski račun</option>
|
||
<option value="gotovina">Gotovina</option>
|
||
<option value="kartica">Kartica</option>
|
||
</select></div>
|
||
<div class="field"><label>Referenca / broj naloga</label>
|
||
<input name="referenca" type="text"></div>
|
||
<div style="text-align:right;margin-top:14px">
|
||
<button type="button" class="btn" onclick="closeModal()">Odustani</button>
|
||
<button type="submit" class="btn primary">💾 Spremi uplatu</button>
|
||
</div>
|
||
</form>
|
||
</div>`);
|
||
}
|
||
|
||
async function submitUplata(e, id) {
|
||
e.preventDefault();
|
||
const f = e.target;
|
||
const body = {
|
||
iznos: parseFloat(f.iznos.value),
|
||
datum_uplate: f.datum_uplate.value || null,
|
||
nacin_uplate: f.nacin_uplate.value,
|
||
referenca: f.referenca.value || null,
|
||
};
|
||
try {
|
||
const r = await api('/clanarine/' + id + '/uplata', {method:'POST', body});
|
||
closeModal();
|
||
toast(`Uplata ${fmtEur(body.iznos)} registrirana. Status: ${r.status}`);
|
||
loadClanarine();
|
||
} catch (err) { toast('Greška: ' + err.message, true); }
|
||
}
|
||
|
||
function newClanarinaModal() {
|
||
openModal(`
|
||
<div class="modal-h">
|
||
<div class="modal-t">+ Novo zaduženje članarine</div>
|
||
<div class="modal-x" onclick="closeModal()">×</div>
|
||
</div>
|
||
<div class="modal-b">
|
||
<form onsubmit="submitNewClanarina(event)">
|
||
<div class="field"><label class="req">Član ID</label>
|
||
<input name="clan_id" type="number" required></div>
|
||
<div class="field"><label>Klub ID (auto ako se ne unese)</label>
|
||
<input name="klub_id" type="number"></div>
|
||
<div class="field"><label class="req">Godina</label>
|
||
<input name="godina" type="number" required value="${new Date().getFullYear()}"></div>
|
||
<div class="field"><label>Razdoblje</label>
|
||
<input name="razdoblje" type="text" value="godišnja"></div>
|
||
<div class="field"><label class="req">Iznos propisan (EUR)</label>
|
||
<input name="iznos_propisan" type="number" step="0.01" required></div>
|
||
<div class="field"><label>Iznos plaćen (ako odmah)</label>
|
||
<input name="iznos_placen" type="number" step="0.01" value="0"></div>
|
||
<div class="field"><label>Napomena</label>
|
||
<textarea name="napomena"></textarea></div>
|
||
<div style="text-align:right">
|
||
<button type="button" class="btn" onclick="closeModal()">Odustani</button>
|
||
<button type="submit" class="btn primary">💾 Kreiraj</button>
|
||
</div>
|
||
</form>
|
||
</div>`);
|
||
}
|
||
|
||
async function submitNewClanarina(e) {
|
||
e.preventDefault();
|
||
const f = e.target;
|
||
const body = {
|
||
clan_id: parseInt(f.clan_id.value),
|
||
klub_id: f.klub_id.value ? parseInt(f.klub_id.value) : null,
|
||
godina: parseInt(f.godina.value),
|
||
razdoblje: f.razdoblje.value,
|
||
iznos_propisan: parseFloat(f.iznos_propisan.value),
|
||
iznos_placen: parseFloat(f.iznos_placen.value || 0),
|
||
napomena: f.napomena.value || null,
|
||
};
|
||
try {
|
||
await api('/clanarine', {method:'POST', body});
|
||
closeModal();
|
||
toast('Članarina kreirana.');
|
||
loadClanarine();
|
||
} catch (err) { toast('Greška: ' + err.message, true); }
|
||
}
|
||
|
||
async function bulkNotify() {
|
||
if (!confirm('Pošalji notifikaciju svim dužnicima?')) return;
|
||
try {
|
||
const r = await api('/clanarine/notify-bulk', {method:'POST', body: {}});
|
||
toast(`Postavljeno ${r.queued} primatelja u red. (Mock — SMTP nije konfiguriran.)`);
|
||
} catch (err) { toast('Greška: ' + err.message, true); }
|
||
}
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// MODUL 2 — LIJEČNIČKI PREGLEDI (M8)
|
||
// ════════════════════════════════════════════════════
|
||
|
||
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'); }
|
||
catch (e) { root.innerHTML = `<div class="empty">Greška: ${esc(e.message)}</div>`; return; }
|
||
$('#cnt-lijecnicki').textContent = data.count;
|
||
const s = data.summary || {};
|
||
const kpi = `
|
||
<div class="kpi-grid">
|
||
<div class="kpi b"><div class="kpi-l">Ukupno pregleda</div><div class="kpi-v">${fmt(s.total)}</div></div>
|
||
<div class="kpi g"><div class="kpi-l">Važeći</div><div class="kpi-v">${fmt(s.vazeci)}</div></div>
|
||
<div class="kpi a"><div class="kpi-l">Uskoro istek (30d)</div><div class="kpi-v">${fmt(s.uskoro)}</div></div>
|
||
<div class="kpi r"><div class="kpi-l">Istekli</div><div class="kpi-v">${fmt(s.istekli)}</div></div>
|
||
</div>`;
|
||
const tools = `
|
||
<div class="toolbar">
|
||
<select id="lj-status" onchange="loadLijecnickiFiltered()">
|
||
<option value="">Svi statusi</option>
|
||
<option value="vazeci">Važeći</option>
|
||
<option value="uskoro">Uskoro istek</option>
|
||
<option value="istekao">Istekao</option>
|
||
</select>
|
||
<input id="lj-klub" type="number" placeholder="Klub ID" onchange="loadLijecnickiFiltered()">
|
||
<div class="grow"></div>
|
||
<button class="btn" onclick="loadZZJZ()">🏥 ZZJZ PGŽ termini</button>
|
||
<button class="btn" onclick="newLijecnickiModal()">+ Novi pregled</button>
|
||
</div>`;
|
||
const rows = (data.rows || []).map(r => `
|
||
<tr>
|
||
<td><b>${esc(r.clan)}</b><div style="font-size:11px;color:var(--t3)">${esc(r.klub || '')}</div></td>
|
||
<td>${fmtDate(r.datum_pregleda)}</td>
|
||
<td>${fmtDate(r.vrijedi_do)}</td>
|
||
<td><span class="tag ${({vazeci:'gr', uskoro:'am', istekao:'rd'})[r.status_calc]||'gy'}">
|
||
${r.status_calc}${r.dana_do_isteka != null ? ' ('+r.dana_do_isteka+'d)' : ''}</span></td>
|
||
<td>${esc(r.ustanova || '')}</td>
|
||
<td>${esc(r.lijecnik || '')}</td>
|
||
<td>${r.placeno ? '<span class="tag gr">DA</span>' : '<span class="tag rd">NE</span>'}</td>
|
||
<td>
|
||
<button class="btn sm" onclick="openZakaziModal(${r.id}, '${esc(r.clan)}')" title="Zakaži termin">📅</button>
|
||
<button class="btn sm" onclick="openLijecnickiDetalji(${r.id})" title="Detalji">👁</button>
|
||
</td>
|
||
</tr>`).join('');
|
||
root.innerHTML = kpi + tools + `
|
||
<div class="card">
|
||
<div class="card-h"><div class="card-t">Lista pregleda (${data.count})</div></div>
|
||
<table>
|
||
<thead><tr><th>Sportaš/Klub</th><th>Datum pregleda</th><th>Vrijedi do</th><th>Status</th><th>Ustanova</th><th>Liječnik</th><th>Plaćeno</th><th></th></tr></thead>
|
||
<tbody>${rows || '<tr><td colspan="8" class="empty">Nema zapisa.</td></tr>'}</tbody>
|
||
</table>
|
||
</div>`;
|
||
}
|
||
|
||
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 tbody = $('#page-lijecnicki table tbody');
|
||
tbody.innerHTML = (data.rows || []).map(r => `
|
||
<tr>
|
||
<td><b>${esc(r.clan)}</b><div style="font-size:11px;color:var(--t3)">${esc(r.klub || '')}</div></td>
|
||
<td>${fmtDate(r.datum_pregleda)}</td>
|
||
<td>${fmtDate(r.vrijedi_do)}</td>
|
||
<td><span class="tag ${({vazeci:'gr', uskoro:'am', istekao:'rd'})[r.status_calc]||'gy'}">
|
||
${r.status_calc}${r.dana_do_isteka != null ? ' ('+r.dana_do_isteka+'d)' : ''}</span></td>
|
||
<td>${esc(r.ustanova || '')}</td>
|
||
<td>${esc(r.lijecnik || '')}</td>
|
||
<td>${r.placeno ? '<span class="tag gr">DA</span>' : '<span class="tag rd">NE</span>'}</td>
|
||
<td>
|
||
<button class="btn sm" onclick="openZakaziModal(${r.id}, '${esc(r.clan)}')">📅</button>
|
||
<button class="btn sm" onclick="openLijecnickiDetalji(${r.id})">👁</button>
|
||
</td>
|
||
</tr>`).join('') || '<tr><td colspan="8" class="empty">Nema zapisa.</td></tr>';
|
||
}
|
||
|
||
async function loadZZJZ() {
|
||
let info, termini;
|
||
try {
|
||
info = await api('/zzjz/info');
|
||
termini = await api('/zzjz/termini');
|
||
} catch (e) { return toast('Greška: ' + e.message, true); }
|
||
const booking = info.online_booking || {};
|
||
const bookingHtml = booking.available
|
||
? `<a class="btn primary" target="_blank" href="${esc(booking.url)}">🔗 Otvori online sustav (${esc(booking.kind)})</a>`
|
||
: `<div class="tag am">Online sustav nije pronađen — koristi e-mail kontakt</div>
|
||
<div style="margin-top:8px"><a class="btn primary" href="mailto:${esc(info.email)}">✉ E-mail: ${esc(info.email)}</a></div>`;
|
||
const termHtml = (termini.termini || []).slice(0, 30).map(t => `
|
||
<tr>
|
||
<td>${esc(t.datum)}</td><td>${esc(t.vrijeme)}</td>
|
||
<td>${esc(t.doktor)}</td>
|
||
<td>${t.available ? '<span class="tag gr">slobodno</span>' : '<span class="tag rd">zauzeto</span>'}</td>
|
||
<td>${fmtEur(t.iznos_eur)}</td>
|
||
</tr>`).join('');
|
||
openModal(`
|
||
<div class="modal-h">
|
||
<div class="modal-t">🏥 ZZJZ PGŽ — Sportska medicina</div>
|
||
<div class="modal-x" onclick="closeModal()">×</div>
|
||
</div>
|
||
<div class="modal-b">
|
||
<div class="payment-card">
|
||
<div class="payment-row"><div class="l">Naziv</div><div class="v">${esc(info.naziv)}</div></div>
|
||
<div class="payment-row"><div class="l">Adresa</div><div class="v">${esc(info.adresa)}</div></div>
|
||
<div class="payment-row"><div class="l">Telefon</div><div class="v">${esc(info.telefon)}</div></div>
|
||
<div class="payment-row"><div class="l">E-mail</div><div class="v">${esc(info.email)}</div></div>
|
||
<div class="payment-row"><div class="l">Web</div><div class="v"><a href="${esc(info.url_sportska_medicina)}" target="_blank" style="color:var(--pgz-blue)">${esc(info.url_sportska_medicina)}</a></div></div>
|
||
</div>
|
||
<div style="margin:14px 0">${bookingHtml}</div>
|
||
<div class="card-h" style="background:transparent;border:none;padding:8px 0">
|
||
<div class="card-t">Dostupni termini (mock — tjedan ${esc(termini.week_start)})</div>
|
||
<div style="font-size:11px;color:var(--t3)">${termini.available} slobodno / ${termini.count} ukupno</div>
|
||
</div>
|
||
<table>
|
||
<thead><tr><th>Datum</th><th>Vrijeme</th><th>Doktor</th><th>Status</th><th>Iznos</th></tr></thead>
|
||
<tbody>${termHtml || '<tr><td colspan="5" class="empty">Nema termina.</td></tr>'}</tbody>
|
||
</table>
|
||
</div>`);
|
||
}
|
||
|
||
function openZakaziModal(lid, clan) {
|
||
openModal(`
|
||
<div class="modal-h">
|
||
<div class="modal-t">📅 Zakaži pregled — ${esc(clan)}</div>
|
||
<div class="modal-x" onclick="closeModal()">×</div>
|
||
</div>
|
||
<div class="modal-b">
|
||
<p style="color:var(--t2);font-size:13px;margin-top:0">Sustav će zakazati termin u ZZJZ PGŽ. Ako online sustav nije dostupan, otvorit će mailto: link.</p>
|
||
<form onsubmit="submitZakazi(event, ${lid})">
|
||
<div class="field"><label class="req">Datum</label>
|
||
<input name="datum" type="date" required value="${new Date(Date.now()+7*86400000).toISOString().slice(0,10)}"></div>
|
||
<div class="field"><label>Vrijeme</label>
|
||
<input name="vrijeme" type="time" value="09:00"></div>
|
||
<div class="field"><label>Ustanova</label>
|
||
<input name="ustanova" type="text" value="ZZJZ PGŽ"></div>
|
||
<div class="field"><label>Napomena</label>
|
||
<textarea name="napomena"></textarea></div>
|
||
<div style="text-align:right">
|
||
<button type="button" class="btn" onclick="closeModal()">Odustani</button>
|
||
<button type="submit" class="btn primary">📅 Zakaži</button>
|
||
</div>
|
||
</form>
|
||
</div>`);
|
||
}
|
||
|
||
async function submitZakazi(e, lid) {
|
||
e.preventDefault();
|
||
const f = e.target;
|
||
const body = {
|
||
datum: f.datum.value, vrijeme: f.vrijeme.value,
|
||
ustanova: f.ustanova.value, napomena: f.napomena.value || null,
|
||
};
|
||
try {
|
||
const r = await api('/lijecnicki/' + lid + '/zakazi', {method:'POST', body});
|
||
closeModal();
|
||
toast('Termin zakazan: ' + r.zakazano_za);
|
||
if (r.booking && r.booking.available) {
|
||
window.open(r.booking.url, '_blank');
|
||
} else if (r.mailto) {
|
||
window.location.href = r.mailto;
|
||
}
|
||
loadLijecnicki();
|
||
} catch (err) { toast('Greška: ' + err.message, true); }
|
||
}
|
||
|
||
async function openLijecnickiDetalji(lid) {
|
||
let l;
|
||
try { l = await api('/lijecnicki/' + lid); }
|
||
catch (e) { return toast('Greška: ' + e.message, true); }
|
||
openModal(`
|
||
<div class="modal-h">
|
||
<div class="modal-t">⚕ Pregled #${l.id} — ${esc(l.clan)}</div>
|
||
<div class="modal-x" onclick="closeModal()">×</div>
|
||
</div>
|
||
<div class="modal-b">
|
||
<div class="payment-card">
|
||
<div class="payment-row"><div class="l">Sportaš</div><div class="v">${esc(l.clan)}</div></div>
|
||
<div class="payment-row"><div class="l">Klub</div><div class="v">${esc(l.klub || '')}</div></div>
|
||
<div class="payment-row"><div class="l">Datum pregleda</div><div class="v">${fmtDate(l.datum_pregleda)}</div></div>
|
||
<div class="payment-row"><div class="l">Vrijedi do</div><div class="v">${fmtDate(l.vrijedi_do)}</div></div>
|
||
<div class="payment-row"><div class="l">Status</div><div class="v"><span class="tag ${({vazeci:'gr',uskoro:'am',istekao:'rd'})[l.status_calc]||'gy'}">${l.status_calc} (${l.dana_do_isteka}d)</span></div></div>
|
||
<div class="payment-row"><div class="l">Vrsta</div><div class="v">${esc(l.vrsta_pregleda || '')}</div></div>
|
||
<div class="payment-row"><div class="l">Ustanova</div><div class="v">${esc(l.ustanova || '')}</div></div>
|
||
<div class="payment-row"><div class="l">Liječnik</div><div class="v">${esc(l.lijecnik || '')}</div></div>
|
||
<div class="payment-row"><div class="l">EKG / Krv / Spirometrija</div><div class="v">${l.ekg?'✓':'✗'} / ${l.krv?'✓':'✗'} / ${l.spirometrija?'✓':'✗'}</div></div>
|
||
<div class="payment-row"><div class="l">Spreman za natjecanje</div><div class="v">${l.spreman_za_natjecanje?'<span class="tag gr">DA</span>':'<span class="tag rd">NE</span>'}</div></div>
|
||
<div class="payment-row"><div class="l">Iznos / plaćeno</div><div class="v">${fmtEur(l.iznos)} ${l.placeno?'<span class="tag gr">DA</span>':'<span class="tag rd">NE</span>'}</div></div>
|
||
</div>
|
||
${l.komentar_lijecnika ? `<div style="margin-top:12px;padding:10px;background:var(--bg);border-left:3px solid var(--pgz-blue);border-radius:5px"><div style="font-size:11px;color:var(--t3);margin-bottom:4px">KOMENTAR LIJEČNIKA</div>${esc(l.komentar_lijecnika)}</div>` : ''}
|
||
${l.napomena ? `<div style="margin-top:8px;padding:10px;background:var(--bg);border-left:3px solid var(--warn);border-radius:5px"><div style="font-size:11px;color:var(--t3);margin-bottom:4px">NAPOMENA</div>${esc(l.napomena)}</div>` : ''}
|
||
<div style="text-align:right;margin-top:14px">
|
||
<button class="btn" onclick="openZakaziModal(${l.id}, '${esc(l.clan)}')">📅 Zakaži novi termin</button>
|
||
</div>
|
||
</div>`);
|
||
}
|
||
|
||
function newLijecnickiModal() {
|
||
openModal(`
|
||
<div class="modal-h">
|
||
<div class="modal-t">+ Novi liječnički pregled</div>
|
||
<div class="modal-x" onclick="closeModal()">×</div>
|
||
</div>
|
||
<div class="modal-b">
|
||
<form onsubmit="submitNewLijecnicki(event)">
|
||
<div class="field"><label class="req">Član ID</label>
|
||
<input name="clan_id" type="number" required></div>
|
||
<div class="field"><label class="req">Datum pregleda</label>
|
||
<input name="datum_pregleda" type="date" required value="${new Date().toISOString().slice(0,10)}"></div>
|
||
<div class="field"><label>Vrijedi do (auto +1 god)</label>
|
||
<input name="vrijedi_do" type="date"></div>
|
||
<div class="field"><label>Vrsta pregleda</label>
|
||
<select name="vrsta_pregleda">
|
||
<option value="temeljni">Temeljni</option>
|
||
<option value="kontrolni">Kontrolni</option>
|
||
<option value="izvanredni">Izvanredni</option>
|
||
</select></div>
|
||
<div class="field"><label>Ustanova</label>
|
||
<input name="ustanova" type="text" value="ZZJZ PGŽ"></div>
|
||
<div class="field"><label>Liječnik</label>
|
||
<input name="lijecnik" type="text"></div>
|
||
<div class="field"><label>Iznos (EUR)</label>
|
||
<input name="iznos" type="number" step="0.01" value="60"></div>
|
||
<div style="text-align:right">
|
||
<button type="button" class="btn" onclick="closeModal()">Odustani</button>
|
||
<button type="submit" class="btn primary">💾 Spremi pregled</button>
|
||
</div>
|
||
</form>
|
||
</div>`);
|
||
}
|
||
|
||
async function submitNewLijecnicki(e) {
|
||
e.preventDefault();
|
||
const f = e.target;
|
||
const body = {
|
||
clan_id: parseInt(f.clan_id.value),
|
||
datum_pregleda: f.datum_pregleda.value,
|
||
vrijedi_do: f.vrijedi_do.value || null,
|
||
vrsta_pregleda: f.vrsta_pregleda.value,
|
||
ustanova: f.ustanova.value,
|
||
lijecnik: f.lijecnik.value || null,
|
||
iznos: parseFloat(f.iznos.value || 0),
|
||
};
|
||
try {
|
||
await api('/lijecnicki', {method:'POST', body});
|
||
closeModal();
|
||
toast('Pregled spremljen.');
|
||
loadLijecnicki();
|
||
} catch (err) { toast('Greška: ' + err.message, true); }
|
||
}
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// MODUL 3 — OBRASCI (M9)
|
||
// ════════════════════════════════════════════════════
|
||
|
||
async function loadObrasci() {
|
||
const root = $('#page-obrasci');
|
||
root.innerHTML = '<div class="loading">Učitavanje obrazaca…</div>';
|
||
let templates, submissions;
|
||
try {
|
||
templates = await api('/forms');
|
||
submissions = await api('/forms/submissions?limit=50');
|
||
} catch (e) { root.innerHTML = `<div class="empty">Greška: ${esc(e.message)}</div>`; return; }
|
||
$('#cnt-obrasci').textContent = templates.count;
|
||
const ss = submissions.summary || {};
|
||
const kpi = `
|
||
<div class="kpi-grid">
|
||
<div class="kpi b"><div class="kpi-l">Templati</div><div class="kpi-v">${fmt(templates.count)}</div></div>
|
||
<div class="kpi g"><div class="kpi-l">Predani</div><div class="kpi-v">${fmt(ss.submitted)}</div></div>
|
||
<div class="kpi a"><div class="kpi-l">Draft</div><div class="kpi-v">${fmt(ss.draft)}</div></div>
|
||
<div class="kpi b"><div class="kpi-l">Odobreni</div><div class="kpi-v">${fmt(ss.approved)}</div></div>
|
||
</div>`;
|
||
const cards = (templates.forms || []).map(f => `
|
||
<div class="card" style="margin-bottom:10px">
|
||
<div class="card-b" style="display:flex;justify-content:space-between;align-items:center">
|
||
<div>
|
||
<div style="font-weight:600">${esc(f.naziv)}</div>
|
||
<div style="font-size:11px;color:var(--t3);margin-top:3px">${esc(f.code)} · ${esc(f.kategorija || '—')} · ${f.field_count} polja${f.opis ? ' · ' + esc(f.opis.substring(0,80)) : ''}</div>
|
||
</div>
|
||
<button class="btn primary" onclick="openFormFill('${esc(f.code)}')">📝 Otvori obrazac</button>
|
||
</div>
|
||
</div>`).join('');
|
||
const subRows = (submissions.rows || []).map(s => `
|
||
<tr>
|
||
<td><b>${esc(s.template_naziv || s.template_code)}</b><div style="font-size:11px;color:var(--t3)">${esc(s.reference_no || '')}</div></td>
|
||
<td>${esc(s.klub_naziv || '—')}</td>
|
||
<td>${fmtDate(s.created_at)}</td>
|
||
<td><span class="tag ${({draft:'gy',submitted:'am',approved:'gr',rejected:'rd'})[s.status]||'gy'}">${esc(s.status)}</span></td>
|
||
<td><code style="font-size:10px;color:var(--ok)">${esc((s.signature_sha256 || '').substring(0,12))}${s.signature_sha256?'…':''}</code></td>
|
||
<td>
|
||
<button class="btn sm" onclick="openSubmissionDetalji(${s.id})" title="Detalji">👁</button>
|
||
<a class="btn sm" href="${API}/forms/submissions/${s.id}/pdf" target="_blank" title="PDF">📄</a>
|
||
</td>
|
||
</tr>`).join('');
|
||
root.innerHTML = kpi + `
|
||
<div class="row" style="display:grid;grid-template-columns:1fr 1.4fr;gap:14px">
|
||
<div>
|
||
<div class="card-h" style="border-radius:8px 8px 0 0;background:var(--bg2);border:1px solid var(--rim);border-bottom:none">
|
||
<div class="card-t">📋 Dostupni obrasci (${templates.count})</div>
|
||
</div>
|
||
<div style="background:var(--bg2);border:1px solid var(--rim);border-top:none;border-radius:0 0 8px 8px;padding:12px;max-height:600px;overflow-y:auto">${cards}</div>
|
||
</div>
|
||
<div>
|
||
<div class="card">
|
||
<div class="card-h"><div class="card-t">Predani obrasci (${submissions.count})</div></div>
|
||
<table>
|
||
<thead><tr><th>Obrazac</th><th>Klub</th><th>Datum</th><th>Status</th><th>SHA-256</th><th></th></tr></thead>
|
||
<tbody>${subRows || '<tr><td colspan="6" class="empty">Nema predanih obrazaca.</td></tr>'}</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
async function openFormFill(code) {
|
||
let tpl, prefill;
|
||
try {
|
||
tpl = await api('/forms/' + code);
|
||
// prefill bez klub_id pretpostavlja prazan
|
||
prefill = await api(`/forms/${code}/prefill`);
|
||
} catch (e) { return toast('Greška: ' + e.message, true); }
|
||
const fields = (tpl.schema_json && tpl.schema_json.fields) || [];
|
||
const pre = prefill.prefill || {};
|
||
const fieldsHtml = fields.map(f => {
|
||
const v = pre[f.name] != null ? pre[f.name] : '';
|
||
const reqClass = f.required ? 'req' : '';
|
||
let inp = '';
|
||
if (f.type === 'textarea') {
|
||
inp = `<textarea name="${esc(f.name)}">${esc(v)}</textarea>`;
|
||
} else if (f.type === 'select' && Array.isArray(f.options)) {
|
||
inp = `<select name="${esc(f.name)}"><option value=""></option>${f.options.map(o => `<option ${o===v?'selected':''}>${esc(o)}</option>`).join('')}</select>`;
|
||
} else if (f.type === 'date') {
|
||
inp = `<input type="date" name="${esc(f.name)}" value="${esc(v)}">`;
|
||
} else if (f.type === 'number') {
|
||
inp = `<input type="number" name="${esc(f.name)}" value="${esc(v)}" ${f.required?'required':''}>`;
|
||
} else if (f.type === 'file') {
|
||
inp = `<input type="text" name="${esc(f.name)}" placeholder="(file upload — TODO)">`;
|
||
} else {
|
||
inp = `<input type="text" name="${esc(f.name)}" value="${esc(v)}" ${f.required?'required':''}>`;
|
||
}
|
||
return `<div class="field"><label class="${reqClass}">${esc(f.label || f.name)}</label>${inp}${f.help ? `<div class="help">${esc(f.help)}</div>` : ''}</div>`;
|
||
}).join('');
|
||
|
||
openModal(`
|
||
<div class="modal-h">
|
||
<div class="modal-t">📝 ${esc(tpl.naziv)}</div>
|
||
<div class="modal-x" onclick="closeModal()">×</div>
|
||
</div>
|
||
<div class="modal-b">
|
||
<p style="color:var(--t2);font-size:12px;margin-top:0">${esc(tpl.opis || '')} <br><span style="color:var(--t3)">Polja označena * su obavezna. Submit = digitalni potpis (sha256) + status "submitted".</span></p>
|
||
<form onsubmit="submitFormFill(event, '${esc(code)}')">
|
||
<div class="field"><label>Klub ID (opcionalno — za bolju autopopulaciju)</label>
|
||
<input id="fill-klub" type="number" placeholder="npr. 10" onchange="reloadPrefill('${esc(code)}', this.value)"></div>
|
||
${fieldsHtml}
|
||
<div class="field"><label>Vaše ime/prezime (digitalni potpis)</label>
|
||
<input name="__signer" type="text" placeholder="npr. Damir Radulić" required></div>
|
||
<div style="text-align:right;margin-top:14px">
|
||
<button type="button" class="btn" onclick="closeModal()">Odustani</button>
|
||
<button type="button" class="btn" onclick="saveFormDraft(event, '${esc(code)}', this)">💾 Spremi draft</button>
|
||
<button type="submit" class="btn primary">✍ Potpiši i predaj</button>
|
||
</div>
|
||
</form>
|
||
</div>`);
|
||
}
|
||
|
||
async function reloadPrefill(code, klubId) {
|
||
if (!klubId) return;
|
||
try {
|
||
const data = await api(`/forms/${code}/prefill?klub_id=${parseInt(klubId)}`);
|
||
Object.entries(data.prefill || {}).forEach(([k, v]) => {
|
||
const el = document.querySelector(`[name="${k}"]`);
|
||
if (el && !el.value) el.value = v;
|
||
});
|
||
toast(`Autopopulirano ${data.applied_fields.length} polja iz kluba ${klubId}`);
|
||
} catch (err) { toast('Prefill greška: ' + err.message, true); }
|
||
}
|
||
|
||
function _collectFormData(form) {
|
||
const data = {};
|
||
let signer = null;
|
||
let klubId = null;
|
||
Array.from(form.elements).forEach(el => {
|
||
if (!el.name) return;
|
||
if (el.name === '__signer') { signer = el.value; return; }
|
||
if (el.id === 'fill-klub') { klubId = el.value ? parseInt(el.value) : null; return; }
|
||
data[el.name] = el.value;
|
||
});
|
||
return {data, signer, klubId};
|
||
}
|
||
|
||
async function submitFormFill(e, code) {
|
||
e.preventDefault();
|
||
const {data, signer, klubId} = _collectFormData(e.target);
|
||
try {
|
||
// create draft
|
||
const draft = await api('/forms/submissions', {method:'POST', body: {
|
||
template_code: code, klub_id: klubId, data,
|
||
}});
|
||
// submit + sign
|
||
const signed = await api('/forms/submissions/' + draft.id + '/submit', {method:'POST', body: {
|
||
full_name: signer, confirm: true,
|
||
}});
|
||
closeModal();
|
||
toast('Obrazac potpisan i predan. SHA-256: ' + signed.signature_sha256.substring(0,12) + '…');
|
||
showSignatureConfirm(signed);
|
||
loadObrasci();
|
||
} catch (err) { toast('Greška: ' + err.message, true); }
|
||
}
|
||
|
||
async function saveFormDraft(e, code, btn) {
|
||
const form = btn.closest('form');
|
||
const {data, klubId} = _collectFormData(form);
|
||
try {
|
||
const draft = await api('/forms/submissions', {method:'POST', body: {
|
||
template_code: code, klub_id: klubId, data,
|
||
}});
|
||
closeModal();
|
||
toast('Spremljen draft #' + draft.id + ' (REF ' + draft.reference_no + ')');
|
||
loadObrasci();
|
||
} catch (err) { toast('Greška: ' + err.message, true); }
|
||
}
|
||
|
||
function showSignatureConfirm(signed) {
|
||
setTimeout(() => openModal(`
|
||
<div class="modal-h">
|
||
<div class="modal-t">✓ Obrazac digitalno potpisan</div>
|
||
<div class="modal-x" onclick="closeModal()">×</div>
|
||
</div>
|
||
<div class="modal-b">
|
||
<div class="payment-card">
|
||
<div class="payment-row"><div class="l">Submission ID</div><div class="v">#${signed.id}</div></div>
|
||
<div class="payment-row"><div class="l">Status</div><div class="v"><span class="tag am">${esc(signed.status)}</span></div></div>
|
||
<div class="payment-row"><div class="l">Potpisao</div><div class="v">${esc(signed.signed_by)}</div></div>
|
||
<div class="payment-row"><div class="l">Vrijeme</div><div class="v" style="font-size:11px">${esc(signed.signed_at)}</div></div>
|
||
</div>
|
||
<div class="signature-box">
|
||
<div style="color:var(--t2);margin-bottom:6px">DIGITALNI POTPIS — SHA-256</div>
|
||
<div class="sha">${esc(signed.signature_sha256)}</div>
|
||
</div>
|
||
<div style="text-align:right;margin-top:14px">
|
||
<a class="btn primary" href="${API}/forms/submissions/${signed.id}/pdf" target="_blank">📄 Preuzmi PDF</a>
|
||
</div>
|
||
</div>`), 200);
|
||
}
|
||
|
||
async function openSubmissionDetalji(sid) {
|
||
let s;
|
||
try { s = await api('/forms/submissions/' + sid); }
|
||
catch (e) { return toast('Greška: ' + e.message, true); }
|
||
const data = s.data || {};
|
||
const fields = (s.schema_json && s.schema_json.fields) || [];
|
||
const fieldsHtml = fields.filter(f => !f.name.startsWith('__')).map(f => {
|
||
const v = data[f.name];
|
||
if (v == null || v === '') return '';
|
||
return `<div class="payment-row"><div class="l">${esc(f.label || f.name)}</div><div class="v">${esc(v).substring(0,200)}</div></div>`;
|
||
}).join('');
|
||
const sig = data.__signature_sha256;
|
||
openModal(`
|
||
<div class="modal-h">
|
||
<div class="modal-t">📋 Submission #${s.id} — ${esc(s.template_naziv)}</div>
|
||
<div class="modal-x" onclick="closeModal()">×</div>
|
||
</div>
|
||
<div class="modal-b">
|
||
<div class="payment-card">
|
||
<div class="payment-row"><div class="l">Reference</div><div class="v">${esc(s.reference_no || '')}</div></div>
|
||
<div class="payment-row"><div class="l">Klub</div><div class="v">${esc(s.klub_naziv || '—')}</div></div>
|
||
<div class="payment-row"><div class="l">Status</div><div class="v"><span class="tag ${({draft:'gy',submitted:'am',approved:'gr',rejected:'rd'})[s.status]||'gy'}">${esc(s.status)}</span></div></div>
|
||
<div class="payment-row"><div class="l">Predano</div><div class="v">${fmtDate(s.submitted_at)}</div></div>
|
||
</div>
|
||
<div class="card-h" style="background:transparent;border:none;padding:8px 0;margin-top:14px"><div class="card-t">Sadržaj</div></div>
|
||
<div class="payment-card">${fieldsHtml || '<div style="color:var(--t3)">Prazno.</div>'}</div>
|
||
${sig ? `<div class="signature-box"><div style="color:var(--t2);margin-bottom:6px">DIGITALNI POTPIS — SHA-256</div><div class="sha">${esc(sig)}</div><div style="margin-top:6px;color:var(--t3)">Potpisao: ${esc(data.__signed_by||'')} • ${esc(data.__signed_at||'')}</div></div>` : '<div style="color:var(--err);margin-top:10px;font-size:12px">⚠ Nije digitalno potpisan</div>'}
|
||
<div style="text-align:right;margin-top:14px;display:flex;gap:8px;justify-content:flex-end">
|
||
${s.status === 'submitted' ? `
|
||
<button class="btn" onclick="approveSub(${s.id})">✓ Odobri</button>
|
||
<button class="btn danger" onclick="rejectSub(${s.id})">✗ Odbij</button>
|
||
` : ''}
|
||
<button class="btn" onclick="reSign(${s.id})">✍ Potpiši ponovno</button>
|
||
<a class="btn primary" href="${API}/forms/submissions/${s.id}/pdf" target="_blank">📄 PDF</a>
|
||
</div>
|
||
</div>`);
|
||
}
|
||
|
||
async function approveSub(sid) {
|
||
if (!confirm('Odobri submission #' + sid + '?')) return;
|
||
try {
|
||
await api('/forms/submissions/' + sid + '/approve', {method:'POST', body: {user_id: 1}});
|
||
closeModal(); toast('Submission #' + sid + ' odobren.'); loadObrasci();
|
||
} catch (e) { toast('Greška: ' + e.message, true); }
|
||
}
|
||
|
||
async function rejectSub(sid) {
|
||
const reason = prompt('Razlog odbijanja:');
|
||
if (!reason) return;
|
||
try {
|
||
await api('/forms/submissions/' + sid + '/reject', {method:'POST', body: {user_id: 1, reason}});
|
||
closeModal(); toast('Submission #' + sid + ' odbijen.'); loadObrasci();
|
||
} catch (e) { toast('Greška: ' + e.message, true); }
|
||
}
|
||
|
||
async function reSign(sid) {
|
||
const name = prompt('Vaše ime za potpis:');
|
||
if (!name) return;
|
||
try {
|
||
const r = await api('/forms/submissions/' + sid + '/sign', {method:'POST', body: {full_name: name, user_id: 1}});
|
||
closeModal(); toast('Potpisano. SHA-256: ' + r.signature_sha256.substring(0,12) + '…'); loadObrasci();
|
||
} catch (e) { toast('Greška: ' + e.message, true); }
|
||
}
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// MODUL 4 — ČLANOVI / DASHBOARD osobe (CRM Dashboard)
|
||
// ════════════════════════════════════════════════════
|
||
|
||
let CLANOVI_LAST_QUERY = '';
|
||
|
||
async function loadClanovi() {
|
||
const root = $('#page-clanovi');
|
||
root.innerHTML = `
|
||
<div class="toolbar">
|
||
<input id="cl-q" type="text" placeholder="Pretraži po imenu / OIB-u (min 2 slova)…" style="min-width:340px;flex:1" oninput="searchClanovi(this.value)">
|
||
<input id="cl-klub-filter" type="number" placeholder="Klub ID (filter)" onchange="searchClanovi($('#cl-q').value)">
|
||
<div class="grow"></div>
|
||
<span style="font-size:11px;color:var(--t3)">Klik na karticu → puni dashboard člana</span>
|
||
</div>
|
||
<div id="cl-results"><div class="loading">Upišite ime za pretragu…</div></div>
|
||
`;
|
||
// initial: load nekoliko poznatih ID-ova kao primjer
|
||
if (!CLANOVI_LAST_QUERY) {
|
||
document.getElementById('cl-q').value = 'Mateo';
|
||
searchClanovi('Mateo');
|
||
}
|
||
}
|
||
|
||
let _searchTimer;
|
||
function searchClanovi(q) {
|
||
clearTimeout(_searchTimer);
|
||
CLANOVI_LAST_QUERY = q;
|
||
if (!q || q.length < 2) {
|
||
$('#cl-results').innerHTML = '<div class="loading">Upišite ime za pretragu (min 2 slova)…</div>';
|
||
return;
|
||
}
|
||
_searchTimer = setTimeout(async () => {
|
||
const klub = $('#cl-klub-filter').value;
|
||
const params = new URLSearchParams({q, limit: 30});
|
||
if (klub) params.append('klub_id', klub);
|
||
try {
|
||
const data = await apiR('/clanovi/search?' + params);
|
||
$('#cnt-clanovi').textContent = data.count;
|
||
const cards = (data.rows || []).map(r => `
|
||
<div class="card" style="margin-bottom:8px;cursor:pointer" onclick="openClanPanel(${r.id})">
|
||
<div class="card-b" style="display:flex;align-items:center;gap:14px">
|
||
<div style="width:48px;height:48px;border-radius:50%;background:var(--bg3);overflow:hidden;display:flex;align-items:center;justify-content:center;flex-shrink:0;border:1px solid var(--rim)">
|
||
${r.slika_url ? `<img src="${esc(r.slika_url)}" style="width:100%;height:100%;object-fit:cover" onerror="this.style.display='none'">` : `<span style="font-size:18px;font-weight:600;color:var(--t2)">${esc((r.ime||'?')[0]+(r.prezime||'?')[0])}</span>`}
|
||
</div>
|
||
<div style="flex:1">
|
||
<div style="font-weight:600">${esc(r.ime)} ${esc(r.prezime)}</div>
|
||
<div style="font-size:11px;color:var(--t3)">${esc(r.klub || '—')} · ${esc(r.pozicija || '—')}${r.broj_dresa ? ' · #'+r.broj_dresa : ''}</div>
|
||
</div>
|
||
<div><span class="tag bl">#${r.id}</span></div>
|
||
</div>
|
||
</div>`).join('');
|
||
$('#cl-results').innerHTML = `
|
||
<div style="color:var(--t3);font-size:12px;margin-bottom:8px">${data.count} rezultat${data.count==1?'':'a'}</div>
|
||
${cards || '<div class="empty">Nema rezultata.</div>'}`;
|
||
} catch (e) { $('#cl-results').innerHTML = `<div class="empty">Greška: ${esc(e.message)}</div>`; }
|
||
}, 250);
|
||
}
|
||
|
||
window._OPEN_PANEL_CID = null;
|
||
|
||
async function openClanPanel(cid) {
|
||
window._OPEN_PANEL_CID = cid;
|
||
loadClanPanel(cid);
|
||
}
|
||
|
||
function closeClanPanel() {
|
||
window._OPEN_PANEL_CID = null;
|
||
closeModal();
|
||
}
|
||
|
||
async function loadClanPanel(cid) {
|
||
let d, perms;
|
||
try {
|
||
d = await apiR('/clanovi/' + cid + '/full');
|
||
perms = await apiR('/clanovi/permissions?role=' + encodeURIComponent(CURRENT_ROLE));
|
||
} catch (e) { return toast('Greška: ' + e.message, true); }
|
||
const c = d.clan, k = d.klub || {};
|
||
const editable = perms.editable; // 'ALL' ili lista polja
|
||
const canEdit = (field) => editable === 'ALL' || (Array.isArray(editable) && editable.includes(field));
|
||
const canUploadAvatar = canEdit('slika_url');
|
||
|
||
const av = c.slika_url_full || c.slika_url || '';
|
||
const initials = ((c.ime||'?')[0]+(c.prezime||'?')[0]).toUpperCase();
|
||
|
||
// 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);
|
||
return `
|
||
<div class="payment-row">
|
||
<div class="l">${esc(label)}${ed?'':' <span style="color:var(--t3);font-size:9px">🔒</span>'}</div>
|
||
<div class="v" style="display:flex;align-items:center;gap:6px">
|
||
<span id="fld-${key}-display" style="font-family:inherit">${esc(safe)}</span>
|
||
${ed ? `<button class="btn sm" onclick="editFieldInline('${key}', '${esc(label)}', ${JSON.stringify(val||'').replace(/"/g,'"')}, '${type}', ${cid})">✎</button>` : ''}
|
||
</div>
|
||
</div>`;
|
||
};
|
||
|
||
const kpiHtml = `
|
||
<div class="kpi-grid" style="margin-bottom:12px">
|
||
<div class="kpi b"><div class="kpi-l">Sezone</div><div class="kpi-v">${fmt(d.kpi.broj_sezona)}</div></div>
|
||
<div class="kpi g"><div class="kpi-l">Nastupi</div><div class="kpi-v">${fmt(d.kpi.nastupi_total)}</div></div>
|
||
<div class="kpi a"><div class="kpi-l">Pogoci</div><div class="kpi-v">${fmt(d.kpi.pogoci_total)}</div></div>
|
||
<div class="kpi r"><div class="kpi-l">Dug članarina</div><div class="kpi-v">${fmtEur(d.kpi.dug_clanarina_eur)}</div></div>
|
||
<div class="kpi ${d.kpi.lijecnicki_status==='vazeci'?'g':d.kpi.lijecnicki_status==='uskoro'?'a':'r'}"><div class="kpi-l">Liječnički</div><div class="kpi-v" style="font-size:14px">${esc(d.kpi.lijecnicki_status||'—')}</div><div class="kpi-s">${d.kpi.lijecnicki_dana_do_isteka != null ? d.kpi.lijecnicki_dana_do_isteka+' dana' : ''}</div></div>
|
||
</div>`;
|
||
|
||
const sectionPersonal = `
|
||
<div class="card-h" style="background:transparent;border:none;padding:6px 0;margin-top:6px"><div class="card-t">📋 Osobni podaci</div></div>
|
||
<div class="payment-card">
|
||
${f('ime','Ime',c.ime)}
|
||
${f('prezime','Prezime',c.prezime)}
|
||
${f('oib','OIB',c.oib)}
|
||
${f('datum_rodenja','Datum rođenja',c.datum_rodenja||c.datum_rodjenja,'date')}
|
||
${f('mjesto_rodenja','Mjesto rođenja',c.mjesto_rodenja||c.mjesto_rodjenja)}
|
||
${f('spol','Spol',c.spol)}
|
||
</div>`;
|
||
|
||
const sectionKontakt = `
|
||
<div class="card-h" style="background:transparent;border:none;padding:6px 0;margin-top:6px"><div class="card-t">📞 Kontakt</div></div>
|
||
<div class="payment-card">
|
||
${f('email','E-mail',c.email)}
|
||
${f('telefon','Telefon',c.telefon)}
|
||
${f('adresa','Adresa',c.adresa)}
|
||
${f('grad','Grad',c.grad)}
|
||
${f('postanski_broj','Pošt. broj',c.postanski_broj)}
|
||
</div>`;
|
||
|
||
const sectionSport = `
|
||
<div class="card-h" style="background:transparent;border:none;padding:6px 0;margin-top:6px"><div class="card-t">⚽ Sport</div></div>
|
||
<div class="payment-card">
|
||
${f('sport','Sport',c.sport)}
|
||
${f('kategorija','Kategorija',c.kategorija)}
|
||
${f('podkategorija','Podkategorija',c.podkategorija)}
|
||
${f('pozicija','Pozicija',c.pozicija)}
|
||
${f('dominantna_noga','Dominantna noga',c.dominantna_noga)}
|
||
${f('visina_cm','Visina (cm)',c.visina_cm,'number')}
|
||
${f('tezina_kg','Težina (kg)',c.tezina_kg,'number')}
|
||
${f('broj_dresa','Broj dresa',c.broj_dresa,'number')}
|
||
${f('uloga','Uloga',c.uloga)}
|
||
${f('uloga_detalj','Uloga (detalj)',c.uloga_detalj)}
|
||
</div>`;
|
||
|
||
const sectionStatus = `
|
||
<div class="card-h" style="background:transparent;border:none;padding:6px 0;margin-top:6px"><div class="card-t">📊 Status</div></div>
|
||
<div class="payment-card">
|
||
${f('aktivan','Aktivan',c.aktivan)}
|
||
${f('datum_pristupa','Datum pristupa',c.datum_pristupa,'date')}
|
||
${f('datum_napustanja','Datum napuštanja',c.datum_napustanja,'date')}
|
||
${f('kategoriziran','Kategoriziran',c.kategoriziran)}
|
||
${f('kategorija_hoo','HOO kategorija',c.kategorija_hoo,'number')}
|
||
${f('reprezentativac','Reprezentativac',c.reprezentativac)}
|
||
${f('reprezentacija_kategorija','Reprezentacija (kat.)',c.reprezentacija_kategorija)}
|
||
${f('stipendiran','Stipendiran',c.stipendiran)}
|
||
${f('stipendija_iznos','Stipendija (€)',c.stipendija_iznos,'number')}
|
||
${f('licenca_broj','Licenca broj',c.licenca_broj)}
|
||
${f('licenca_vrijedi_do','Licenca vrijedi do',c.licenca_vrijedi_do,'date')}
|
||
${f('radno_pravni_status','Radno-pravni status',c.radno_pravni_status)}
|
||
</div>`;
|
||
|
||
const sectionKlub = `
|
||
<div class="card-h" style="background:transparent;border:none;padding:6px 0;margin-top:6px"><div class="card-t">⬢ Klub</div></div>
|
||
<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.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 ? `
|
||
<div style="margin-top:8px"><b>Povijest klubova (${d.povijest_klubova.length}):</b></div>
|
||
<table style="margin-top:6px"><thead><tr><th>Klub</th><th>Od</th><th>Do</th><th># sezona</th></tr></thead>
|
||
<tbody>${d.povijest_klubova.map(p=>`<tr><td>${esc(p.klub_naziv)}</td><td>${esc(p.od)}</td><td>${esc(p.do_)}</td><td>${p.broj_sezona}</td></tr>`).join('')}</tbody>
|
||
</table>` : ''}
|
||
`;
|
||
|
||
const tabSezone = `
|
||
<table><thead><tr><th>Sezona</th><th>Klub</th><th>Natjecanje</th><th>Nast.</th><th>Pog.</th><th>Asist.</th><th>Žuti</th><th>Crv.</th><th>Min.</th></tr></thead>
|
||
<tbody>${(d.sezone||[]).map(s=>`<tr><td><b>${esc(s.sezona)}</b></td><td>${esc(s.klub_naziv||'—')}</td><td>${esc(s.natjecanje||'—')}</td><td>${fmt(s.nastupi)}</td><td>${fmt(s.pogoci)}</td><td>${fmt(s.asistencije)}</td><td>${fmt(s.zuti_kartoni)}</td><td>${fmt(s.crveni_kartoni)}</td><td>${fmt(s.minute_total)}</td></tr>`).join('') || '<tr><td colspan="9" class="empty">Nema podataka.</td></tr>'}</tbody>
|
||
</table>`;
|
||
|
||
const tabUtakmice = `
|
||
<table><thead><tr><th>Datum</th><th>Domaćin</th><th>Gost</th><th>Rezultat</th><th>Natj.</th><th>Pog.</th><th>Min.</th><th></th></tr></thead>
|
||
<tbody>${(d.utakmice_zadnje20||[]).map(u=>`<tr><td>${fmtDate(u.datum)}</td><td>${esc(u.domacin||'—')}</td><td>${esc(u.gost||'—')}</td><td><b>${esc(u.rezultat||'—')}</b></td><td>${esc(u.natjecanje||'—')}</td><td>${fmt(u.pogoci)}</td><td>${fmt(u.minute)}</td><td>${u.utakmica_url?`<a class="btn sm" href="${esc(u.utakmica_url)}" target="_blank">↗</a>`:''}</td></tr>`).join('') || '<tr><td colspan="8" class="empty">Nema utakmica.</td></tr>'}</tbody>
|
||
</table>`;
|
||
|
||
const tabLij = `
|
||
<table><thead><tr><th>Datum</th><th>Vrijedi do</th><th>Status</th><th>Vrsta</th><th>Ustanova</th><th>Liječnik</th><th>Plaćeno</th></tr></thead>
|
||
<tbody>${(d.lijecnicki||[]).map(l=>`<tr><td>${fmtDate(l.datum_pregleda)}</td><td>${fmtDate(l.vrijedi_do)}</td><td><span class="tag ${({vazeci:'gr',uskoro:'am',istekao:'rd'})[l.status_calc]||'gy'}">${esc(l.status_calc)} (${l.dana_do_isteka}d)</span></td><td>${esc(l.vrsta_pregleda||'—')}</td><td>${esc(l.ustanova||'—')}</td><td>${esc(l.lijecnik||'—')}</td><td>${l.placeno?'<span class="tag gr">DA</span>':'<span class="tag rd">NE</span>'}</td></tr>`).join('') || '<tr><td colspan="7" class="empty">Nema pregleda.</td></tr>'}</tbody>
|
||
</table>`;
|
||
|
||
const tabClanarine = `
|
||
<table><thead><tr><th>God.</th><th>Razdoblje</th><th>Propisan</th><th>Plaćeno</th><th>Dug</th><th>Status</th><th>Datum upl.</th><th></th></tr></thead>
|
||
<tbody>${(d.clanarine||[]).map(cl=>`<tr><td>${esc(cl.godina)}</td><td>${esc(cl.razdoblje||'—')}</td><td>${fmtEur(cl.iznos_propisan)}</td><td>${fmtEur(cl.iznos_placen)}</td><td><b style="color:${cl.dug>0?'var(--err)':'var(--ok)'}">${fmtEur(cl.dug)}</b></td><td><span class="tag ${statusTag(cl.status)}">${esc(cl.status)}</span></td><td>${fmtDate(cl.datum_uplate)}</td><td><a class="btn sm" href="${API}/clanarine/${cl.id}/uplatnica.pdf" target="_blank">📄</a></td></tr>`).join('') || '<tr><td colspan="8" class="empty">Nema članarina.</td></tr>'}</tbody>
|
||
</table>`;
|
||
|
||
const tabDokumenti = `
|
||
<table><thead><tr><th>God.</th><th>Naslov</th><th>Vrsta</th><th>Snippet</th><th></th></tr></thead>
|
||
<tbody>${(d.dokumenti||[]).map(dk=>`<tr><td>${esc(dk.godina)}</td><td><b>${esc(dk.title||'—')}</b></td><td>${esc(dk.vrsta||'—')}</td><td style="font-size:11px;color:var(--t3);max-width:280px">${esc((dk.snippet||'').substring(0,140))}</td><td>${dk.pdf_url?`<a class="btn sm" href="${esc(dk.pdf_url)}" target="_blank">📄</a>`:dk.url?`<a class="btn sm" href="${esc(dk.url)}" target="_blank">↗</a>`:''}</td></tr>`).join('') || '<tr><td colspan="5" class="empty">Nema dokumenata.</td></tr>'}</tbody>
|
||
</table>`;
|
||
|
||
const tabObrasci = `
|
||
<table><thead><tr><th>Obrazac</th><th>Ref.</th><th>Status</th><th>Predano</th><th></th></tr></thead>
|
||
<tbody>${(d.obrasci||[]).map(o=>`<tr><td><b>${esc(o.template_naziv||o.template_code)}</b></td><td><code style="font-size:10px">${esc(o.reference_no||'')}</code></td><td><span class="tag ${({draft:'gy',submitted:'am',approved:'gr',rejected:'rd'})[o.status]||'gy'}">${esc(o.status)}</span></td><td>${fmtDate(o.submitted_at||o.created_at)}</td><td><a class="btn sm" href="${API}/forms/submissions/${o.id}/pdf" target="_blank">📄</a></td></tr>`).join('') || '<tr><td colspan="5" class="empty">Nema obrazaca.</td></tr>'}</tbody>
|
||
</table>`;
|
||
|
||
const tabNagrade = (d.nagrade && d.nagrade.length) ? `
|
||
<table><thead><tr><th>Godina</th><th>Natjecanje</th><th>Razina</th><th>Disciplina</th><th>Plasman</th><th>Klub</th></tr></thead>
|
||
<tbody>${d.nagrade.map(n=>`<tr><td>${esc(n.godina)}</td><td>${esc(n.natjecanje||'—')}</td><td>${esc(n.razina_natjecanja||'—')}</td><td>${esc(n.disciplina||'—')}</td><td><b>${n.plasman||'—'}</b></td><td>${esc(n.klub_naziv||'—')}</td></tr>`).join('')}</tbody>
|
||
</table>` : '';
|
||
|
||
// Modal — širi nego standardno
|
||
$('#modal').style.maxWidth = '1100px';
|
||
openModal(`
|
||
<div class="modal-h">
|
||
<div class="modal-t">👤 ${esc(c.ime)} ${esc(c.prezime)} <span style="color:var(--t3);font-size:11px;font-weight:400">#${cid} · ${esc(CURRENT_ROLE)} (${editable === 'ALL' ? 'full edit' : Array.isArray(editable) ? editable.length+' edit polja' : 'no edit'})</span></div>
|
||
<div class="modal-x" onclick="closeClanPanel()">×</div>
|
||
</div>
|
||
<div class="modal-b">
|
||
<div style="display:grid;grid-template-columns:140px 1fr;gap:18px;margin-bottom:14px">
|
||
<div style="text-align:center">
|
||
<div id="avatar-display" style="width:140px;height:140px;border-radius:8px;background:var(--bg3);border:1px solid var(--rim);display:flex;align-items:center;justify-content:center;overflow:hidden">
|
||
${av ? `<img src="${esc(av)}?_=${Date.now()}" style="width:100%;height:100%;object-fit:cover">` : `<span style="font-size:48px;font-weight:700;color:var(--t2)">${esc(initials)}</span>`}
|
||
</div>
|
||
${canUploadAvatar ? `
|
||
<input type="file" id="avatar-file" accept="image/*" style="display:none" onchange="uploadAvatar(${cid})">
|
||
<button class="btn primary sm" style="margin-top:8px;width:100%" onclick="document.getElementById('avatar-file').click()">📷 Upload</button>
|
||
` : `<div style="font-size:10px;color:var(--t3);margin-top:6px">🔒 Bez dozvole</div>`}
|
||
</div>
|
||
<div>
|
||
${kpiHtml}
|
||
<div style="font-size:12px;color:var(--t3)">Dob: <b style="color:var(--t1)">${c.dob_calc != null ? c.dob_calc + ' god.' : '—'}</b> · Slika: <code style="font-size:10px">${esc(c.slika_url || '—').substring(0,60)}</code></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="cp-tabs" style="display:flex;gap:0;border-bottom:1px solid var(--rim);margin-bottom:12px;flex-wrap:wrap">
|
||
<div class="cp-tab active" onclick="cpTab('osobni')" data-cpt="osobni" style="padding:10px 14px;cursor:pointer;border-bottom:2px solid var(--pgz-blue);font-weight:600">Osobni</div>
|
||
<div class="cp-tab" onclick="cpTab('sezone')" data-cpt="sezone" style="padding:10px 14px;cursor:pointer;color:var(--t2)">Sezone (${d.sezone.length})</div>
|
||
<div class="cp-tab" onclick="cpTab('utakmice')" data-cpt="utakmice" style="padding:10px 14px;cursor:pointer;color:var(--t2)">Utakmice (${d.utakmice_zadnje20.length})</div>
|
||
<div class="cp-tab" onclick="cpTab('lij')" data-cpt="lij" style="padding:10px 14px;cursor:pointer;color:var(--t2)">Liječnički (${d.lijecnicki.length})</div>
|
||
<div class="cp-tab" onclick="cpTab('clanarine')" data-cpt="clanarine" style="padding:10px 14px;cursor:pointer;color:var(--t2)">Članarine (${d.clanarine.length})</div>
|
||
<div class="cp-tab" onclick="cpTab('dokumenti')" data-cpt="dokumenti" style="padding:10px 14px;cursor:pointer;color:var(--t2)">Dokumenti (${d.dokumenti.length})</div>
|
||
<div class="cp-tab" onclick="cpTab('obrasci')" data-cpt="obrasci" style="padding:10px 14px;cursor:pointer;color:var(--t2)">Obrasci (${d.obrasci.length})</div>
|
||
${tabNagrade ? `<div class="cp-tab" onclick="cpTab('nagrade')" data-cpt="nagrade" style="padding:10px 14px;cursor:pointer;color:var(--t2)">Nagrade (${d.nagrade.length})</div>` : ''}
|
||
</div>
|
||
|
||
<div id="cp-osobni" class="cp-page">
|
||
${sectionPersonal}
|
||
${sectionKontakt}
|
||
${sectionSport}
|
||
${sectionStatus}
|
||
${sectionKlub}
|
||
<div class="card-h" style="background:transparent;border:none;padding:6px 0;margin-top:6px"><div class="card-t">📝 Napomena</div></div>
|
||
<div class="payment-card">${f('napomena','Napomena',c.napomena)}</div>
|
||
<div class="card-h" style="background:transparent;border:none;padding:6px 0;margin-top:6px"><div class="card-t">📖 Biografija</div></div>
|
||
<div class="payment-card">${f('biografija','Biografija',c.biografija,'textarea')}</div>
|
||
</div>
|
||
<div id="cp-sezone" class="cp-page" style="display:none">${tabSezone}</div>
|
||
<div id="cp-utakmice" class="cp-page" style="display:none">${tabUtakmice}</div>
|
||
<div id="cp-lij" class="cp-page" style="display:none">${tabLij}</div>
|
||
<div id="cp-clanarine" class="cp-page" style="display:none">${tabClanarine}</div>
|
||
<div id="cp-dokumenti" class="cp-page" style="display:none">${tabDokumenti}</div>
|
||
<div id="cp-obrasci" class="cp-page" style="display:none">${tabObrasci}</div>
|
||
${tabNagrade ? `<div id="cp-nagrade" class="cp-page" style="display:none">${tabNagrade}</div>` : ''}
|
||
</div>
|
||
`);
|
||
}
|
||
|
||
function cpTab(name) {
|
||
$$('.cp-tab').forEach(t => {
|
||
const active = t.dataset.cpt === name;
|
||
t.style.borderBottom = active ? '2px solid var(--pgz-blue)' : '';
|
||
t.style.color = active ? 'var(--t1)' : 'var(--t2)';
|
||
t.style.fontWeight = active ? '600' : '500';
|
||
t.classList.toggle('active', active);
|
||
});
|
||
$$('.cp-page').forEach(p => p.style.display = (p.id === 'cp-' + name) ? 'block' : 'none');
|
||
}
|
||
|
||
function editFieldInline(key, label, currentVal, type, cid) {
|
||
const inputType = type === 'date' ? 'date' : type === 'number' ? 'number' : 'text';
|
||
const isTextarea = type === 'textarea';
|
||
const isBool = typeof currentVal === 'boolean';
|
||
let inputHtml;
|
||
if (isBool) {
|
||
inputHtml = `<select id="ef-input"><option value="true" ${currentVal===true?'selected':''}>true</option><option value="false" ${currentVal===false?'selected':''}>false</option></select>`;
|
||
} else if (isTextarea) {
|
||
inputHtml = `<textarea id="ef-input" style="width:100%;min-height:80px">${esc(currentVal||'')}</textarea>`;
|
||
} else {
|
||
inputHtml = `<input id="ef-input" type="${inputType}" value="${esc(currentVal||'')}" style="width:100%">`;
|
||
}
|
||
const promptHtml = `
|
||
<div class="modal-h">
|
||
<div class="modal-t">✎ ${esc(label)}</div>
|
||
<div class="modal-x" onclick="closeModal();loadClanPanel(${cid})">×</div>
|
||
</div>
|
||
<div class="modal-b">
|
||
<div class="field"><label>${esc(label)} (${esc(key)})</label>${inputHtml}</div>
|
||
<div style="text-align:right;margin-top:14px">
|
||
<button class="btn" onclick="closeModal();loadClanPanel(${cid})">Odustani</button>
|
||
<button class="btn primary" onclick="saveField('${key}', ${cid}, ${isBool}, ${isTextarea ? 'false' : type==='number'?'true':'false'})">💾 Spremi</button>
|
||
</div>
|
||
</div>`;
|
||
$('#modal').style.maxWidth = '500px';
|
||
openModal(promptHtml);
|
||
setTimeout(() => $('#ef-input')?.focus(), 50);
|
||
}
|
||
|
||
async function saveField(key, cid, isBool, isNumber) {
|
||
const el = $('#ef-input');
|
||
let val = el.value;
|
||
if (isBool) val = val === 'true';
|
||
else if (isNumber) val = val === '' ? null : Number(val);
|
||
else if (val === '') val = null;
|
||
try {
|
||
const r = await apiR('/clanovi/' + cid, {method:'PUT', body: {[key]: val}});
|
||
if (r.rejected_fields && r.rejected_fields.includes(key)) {
|
||
toast(`Polje "${key}" odbijeno za rolu ${CURRENT_ROLE}`, true);
|
||
} else {
|
||
toast(`✓ Polje ${key} spremljeno (rola ${CURRENT_ROLE})`);
|
||
}
|
||
closeModal();
|
||
$('#modal').style.maxWidth = '1100px';
|
||
loadClanPanel(cid);
|
||
} catch (e) { toast('Greška: ' + e.message, true); }
|
||
}
|
||
|
||
async function uploadAvatar(cid) {
|
||
const inp = $('#avatar-file');
|
||
if (!inp.files || !inp.files[0]) return;
|
||
const fd = new FormData();
|
||
fd.append('file', inp.files[0]);
|
||
try {
|
||
const r = await fetch(API + '/clanovi/' + cid + '/avatar', {
|
||
method: 'POST',
|
||
headers: {'X-Role': CURRENT_ROLE},
|
||
body: fd,
|
||
});
|
||
if (!r.ok) throw new Error(`HTTP ${r.status}: ${await r.text()}`);
|
||
const d = await r.json();
|
||
toast(`✓ Avatar uploaded: ${d.size_bytes} bytes`);
|
||
loadClanPanel(cid);
|
||
} catch (e) { toast('Greška upload-a: ' + e.message, true); }
|
||
}
|
||
|
||
// ────────────────────────────────────────────────────
|
||
// init
|
||
// ────────────────────────────────────────────────────
|
||
// postavi role iz localStorage u dropdown
|
||
const _roleSel = document.getElementById('g-role');
|
||
if (_roleSel) _roleSel.value = CURRENT_ROLE;
|
||
|
||
loadClanovi();
|
||
// preload counts za sve tabove
|
||
(async () => {
|
||
try {
|
||
const cl = await api('/clanarine?limit=1');
|
||
$('#cnt-clanarine').textContent = cl.summary?.total ?? '?';
|
||
const lj = await api('/lijecnicki?limit=1');
|
||
$('#cnt-lijecnicki').textContent = lj.summary?.total ?? '?';
|
||
const fm = await api('/forms');
|
||
$('#cnt-obrasci').textContent = fm.count;
|
||
} catch (e) {}
|
||
})();
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|