Files
pgz-sport/static/crm.html
T
damir 4fc8327789 R7+ orchestrator + CC3 logo home: combined patches
Orchestrator-side:
- routers/img_proxy_router.py: 4xx/5xx → 1x1 transparent PNG (eliminates cascade <img onerror>)
- static/sport2.html: removed standalone three.min.js (3d-force-graph bundles), bumped to 1.73.4

CC3 (before limit hit):
- Logo home link applied to ALL HTML pages (admin.html, admin_users.html, audit.html, crm.html, erp.html, kpi.html, login.html)
- Backups in _backups/*.cc3_pre_logo.$ts

CC4 R3 (before plan mode):
- _backups/r3_cc4/ocr.py.pre_S2.$ts

Audit screenshots (80 pages) committed to _audit/audit_20260505_023639/shots/
2026-05-05 08:20:07 +02:00

1856 lines
99 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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="/static/shared/sidebar.css">
<script src="/static/shared/sidebar.js" defer data-active="clanarine"></script>
<style>body{padding-top:0}</style>
</head>
<body>
<div class="topbar">
<a href="/" class="logo" style="text-decoration:none;color:inherit;cursor:pointer" title="Početna">⬢ PGŽ SPORT</a>
<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="/static/sport2.html">← portal</a>
<a href="/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 class="tab" data-tab="stats" onclick="setTab('stats')">📊 Statistika</div>
<div class="tab" data-tab="notifs" onclick="setTab('notifs')">🔔 Notifikacije <span class="count" id="cnt-notifs"></span></div>
<div class="tab" data-tab="emailtpl" onclick="setTab('emailtpl')">📨 E-mail templates</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 id="page-stats" class="page" style="display:none"></div>
<div id="page-notifs" class="page" style="display:none"></div>
<div id="page-emailtpl" 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 => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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');
function getJwt() {
return localStorage.getItem('pgz_access') || sessionStorage.getItem('pgz_access') || localStorage.getItem('jwt') || localStorage.getItem('access_token') || null;
}
async function api(path, opts={}) {
const headers = Object.assign({'Content-Type':'application/json'}, (opts.headers || {}));
const jwt = getJwt();
if (jwt && !headers['Authorization']) headers['Authorization'] = 'Bearer ' + jwt;
const o = Object.assign({}, opts, {headers});
if (o.body && typeof o.body !== 'string') o.body = JSON.stringify(o.body);
const r = await fetch(API + path, o);
if (r.status === 401) {
// ako POST/PUT pukne s 401, nudi quick-login
if (opts.method && opts.method !== 'GET') {
if (confirm('Session istekla / nije logiran. Login s damir@pgz.hr?')) {
await quickLogin();
return api(path, opts); // retry once
}
}
}
if (!r.ok) {
const msg = await r.text().catch(()=>r.statusText);
throw new Error(`HTTP ${r.status}: ${msg.substring(0,200)}`);
}
return r.json();
}
async function quickLogin() {
try {
const r = await fetch('/sport/api/auth/login', {
method:'POST',
headers:{'Content-Type':'application/json'},
body: JSON.stringify({email:'damir@pgz.hr', password:'PGZ2026!'}),
});
if (!r.ok) throw new Error('login failed: ' + r.status);
const d = await r.json();
if (d.access_token) {
localStorage.setItem('jwt', d.access_token);
toast('✓ Login OK (damir@pgz.hr)');
return d.access_token;
}
} catch (e) { toast('Login greška: ' + e.message, true); }
}
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();
if (name === 'stats') loadStats();
if (name === 'notifs') loadNotifs();
if (name === 'emailtpl') loadEmailTpl();
}
// ════════════════════════════════════════════════════
// 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" onclick="selectAllUnpaid()">☑ Sve nepladene</button>
<button class="btn primary" onclick="bulkNotifySelected()">📧 Pošalji opomenu</button>
<button class="btn" onclick="bulkUplatniceSelected()">📄 Generiraj uplatnice (lista)</button>
<button class="btn" onclick="bulkUplatniceZipSelected()">🗜 Batch ZIP (PDF-ovi)</button>
<button class="btn" onclick="newClanarinaModal()">+ Novo zaduženje</button>
</div>
<div id="cl-bulkbar" style="display:none;background:var(--bg3);border:1px solid var(--pgz-blue);border-radius:6px;padding:8px 14px;margin-bottom:10px;align-items:center;gap:14px">
<span><b id="cl-selcount">0</b> odabrano</span>
<span style="color:var(--t3)">·</span>
<span>Ukupno dug: <b id="cl-seldug">0,00 €</b></span>
<div style="margin-left:auto;display:flex;gap:6px">
<button class="btn sm" onclick="clearSelection()">Poništi</button>
</div>
</div>`;
const rowHtml = r => `
<tr data-id="${r.id}" data-dug="${r.dug||0}" data-paid="${r.dug<=0?1:0}">
<td><input type="checkbox" class="cl-cb" onchange="updateBulkBar()"></td>
<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>`;
const rows = (data.rows || []).map(rowHtml).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 style="width:30px"><input type="checkbox" id="cl-cb-all" onchange="toggleAllCheckboxes(this.checked)"></th><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="9" class="empty">Nema zapisa.</td></tr>'}</tbody>
</table>
</div>`;
}
function toggleAllCheckboxes(checked) {
$$('#page-clanarine .cl-cb').forEach(cb => cb.checked = checked);
updateBulkBar();
}
function selectAllUnpaid() {
let n = 0;
$$('#page-clanarine tr[data-id]').forEach(tr => {
const cb = tr.querySelector('.cl-cb');
if (tr.dataset.paid === '0') { cb.checked = true; n++; } else cb.checked = false;
});
updateBulkBar();
toast(`Odabrano ${n} nepladenih.`);
}
function clearSelection() {
$$('#page-clanarine .cl-cb').forEach(cb => cb.checked = false);
const all = $('#cl-cb-all'); if (all) all.checked = false;
updateBulkBar();
}
function getSelectedClanarine() {
const rows = $$('#page-clanarine tr[data-id]').filter(tr => tr.querySelector('.cl-cb')?.checked);
return rows.map(tr => ({id: parseInt(tr.dataset.id), dug: parseFloat(tr.dataset.dug || 0)}));
}
function updateBulkBar() {
const sel = getSelectedClanarine();
const bar = $('#cl-bulkbar');
if (!bar) return;
if (sel.length === 0) { bar.style.display = 'none'; return; }
bar.style.display = 'flex';
$('#cl-selcount').textContent = sel.length;
$('#cl-seldug').textContent = fmtEur(sel.reduce((a,b) => a+b.dug, 0));
}
async function bulkNotifySelected() {
const sel = getSelectedClanarine();
if (sel.length === 0) {
if (!confirm('Nije odabrano ništa. Pošalji opomene SVIM dužnicima?')) return;
return doBulkNotify({});
}
if (!confirm(`Pošalji opomenu za ${sel.length} odabrane članarine?`)) return;
return doBulkNotify({ids: sel.map(s => s.id)});
}
async function doBulkNotify(body) {
try {
const r = await api('/clanarine/bulk/notify', {method:'POST', body});
toast(`✓ Matched ${r.matched}, queued ${r.queued_inapp} InApp + ${r.queued_email} Email.`);
} catch (e) { toast('Greška: ' + e.message, true); }
}
async function bulkUplatniceZipSelected() {
const sel = getSelectedClanarine();
const body = sel.length ? {ids: sel.map(s => s.id), only_unpaid: false} : {};
if (!sel.length && !confirm('Ništa nije odabrano — generirati ZIP za SVE dužnike?')) return;
toast(`Generiranje ZIP-a (${sel.length || 'svi'})... može potrajati`);
try {
const headers = {'Content-Type':'application/json'};
const jwt = getJwt();
if (jwt) headers['Authorization'] = 'Bearer ' + jwt;
const r = await fetch(API + '/clanarine/bulk/uplatnice.zip', {
method: 'POST',
headers,
body: JSON.stringify(body),
});
if (!r.ok) {
const t = await r.text();
throw new Error(`HTTP ${r.status}: ${t.substring(0,200)}`);
}
const blob = await r.blob();
const cnt = r.headers.get('X-Batch-Count') || '?';
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `hub3-batch-${new Date().toISOString().slice(0,10)}-${cnt}.zip`;
document.body.appendChild(a); a.click(); a.remove();
URL.revokeObjectURL(url);
toast(`✓ ZIP preuzeto (${cnt} PDF-ova, ${(blob.size/1024).toFixed(0)} KB)`);
} catch (e) { toast('Greška: ' + e.message, true); }
}
async function bulkUplatniceSelected() {
const sel = getSelectedClanarine();
const body = sel.length ? {ids: sel.map(s => s.id)} : {};
try {
const r = await api('/clanarine/bulk/uplatnice', {method:'POST', body});
if (r.count === 0) { toast('Nema uplatnica.'); return; }
openModal(`
<div class="modal-h"><div class="modal-t">📄 Generirane uplatnice (${r.count}) — ukupno ${fmtEur(r.total_dug_eur)}</div><div class="modal-x" onclick="closeModal()">×</div></div>
<div class="modal-b">
<p style="color:var(--t2);font-size:12px">Klikom na PDF/QR otvarate uplatnicu u novom tabu. Svaka se generira on-demand.</p>
<table><thead><tr><th>Klub</th><th>Sportaš</th><th>God.</th><th>Iznos</th><th>Akcije</th></tr></thead>
<tbody>${r.uplatnice.map(u => `<tr><td>${esc(u.klub||'—')}</td><td>${esc(u.clan)}</td><td>${esc(u.godina)}</td><td><b>${fmtEur(u.iznos_eur)}</b></td><td><a class="btn sm" href="${esc(u.pdf_url)}" target="_blank">📄 PDF</a> <a class="btn sm" href="${esc(u.qr_url)}" target="_blank">📱 QR</a></td></tr>`).join('')}</tbody></table>
</div>`);
} catch (e) { toast('Greška: ' + e.message, true); }
}
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 data-id="${r.id}" data-dug="${r.dug||0}" data-paid="${r.dug<=0?1:0}">
<td><input type="checkbox" class="cl-cb" onchange="updateBulkBar()"></td>
<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="9" class="empty">Nema zapisa.</td></tr>';
clearSelection();
}
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>
<a class="btn primary" onclick="exportClanoviXlsx()">📥 Export XLSX</a>
<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,'&quot;')}, '${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 headers = {'X-Role': CURRENT_ROLE};
const jwt = getJwt();
if (jwt) headers['Authorization'] = 'Bearer ' + jwt;
const r = await fetch(API + '/clanovi/' + cid + '/avatar', {
method: 'POST',
headers,
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); }
}
function exportClanoviXlsx() {
const klub = $('#cl-klub-filter')?.value;
const q = $('#cl-q')?.value;
const params = new URLSearchParams();
if (klub) params.append('klub_id', klub);
if (q) params.append('q', q);
const url = API + '/clanovi/export.xlsx' + (params.toString() ? '?' + params : '');
window.open(url, '_blank');
toast('XLSX export pokrenut…');
}
// ════════════════════════════════════════════════════
// MODUL 5 — STATISTIKA (R5 #5)
// ════════════════════════════════════════════════════
async function loadStats() {
const root = $('#page-stats');
root.innerHTML = '<div class="loading">Učitavanje statistike…</div>';
let d;
try { d = await api('/stats'); } catch (e) { root.innerHTML = `<div class="empty">Greška: ${esc(e.message)}</div>`; return; }
const c = d.clanovi, cl = d.clanarine, lj = d.lijecnicki;
const aktPct = c.total ? Math.round(c.aktivni / c.total * 100) : 0;
const podPct = cl.total ? Math.round(cl.n_podmireno / cl.total * 100) : 0;
// Trend uplata bar chart (jednostavan SVG)
const tr = d.trend_uplata_12m || [];
const maxIz = Math.max(...tr.map(t => parseFloat(t.iznos_total || 0)), 1);
const barW = 50, gap = 8;
const barsHtml = tr.map((t, i) => {
const h = Math.round(parseFloat(t.iznos_total || 0) / maxIz * 160);
const x = i * (barW + gap) + 30;
return `<g><rect x="${x}" y="${180 - h}" width="${barW}" height="${h}" fill="url(#g1)" rx="3"/>
<text x="${x + barW/2}" y="195" text-anchor="middle" font-size="9" fill="#9aa3b6">${esc(t.mjesec.substring(2))}</text>
<text x="${x + barW/2}" y="${178 - h}" text-anchor="middle" font-size="9" fill="#e6e8ef">${fmt(Math.round(t.iznos_total))}</text></g>`;
}).join('');
const spolHtml = (d.po_spolu || []).map(s => `<tr><td>${esc(s.spol||'?')}</td><td><b>${fmt(s.n)}</b></td></tr>`).join('');
const katHtml = (d.po_kategoriji || []).map(k => `<tr><td>${esc(k.kategorija)}</td><td><b>${fmt(k.n)}</b></td></tr>`).join('');
const noviHtml = (d.najnovije_uplate || []).map(u => `<tr><td>${fmtDate(u.datum_uplate)}</td><td>${esc(u.clan)}</td><td>${esc(u.klub||'—')}</td><td>${esc(u.godina)}</td><td><b>${fmtEur(u.iznos_placen)}</b></td></tr>`).join('');
root.innerHTML = `
<div class="kpi-grid">
<div class="kpi g"><div class="kpi-l">Aktivni članovi</div><div class="kpi-v">${fmt(c.aktivni)}</div><div class="kpi-s">${aktPct}% od ${fmt(c.total)}</div></div>
<div class="kpi r"><div class="kpi-l">Neaktivni</div><div class="kpi-v">${fmt(c.neaktivni)}</div></div>
<div class="kpi a"><div class="kpi-l">Reprezentativci</div><div class="kpi-v">${fmt(c.reprezentativci)}</div></div>
<div class="kpi b"><div class="kpi-l">Kategorizirani</div><div class="kpi-v">${fmt(c.kategorizirani)}</div></div>
<div class="kpi"><div class="kpi-l">Stipendirani</div><div class="kpi-v">${fmt(c.stipendirani)}</div></div>
</div>
<div class="kpi-grid">
<div class="kpi g"><div class="kpi-l">Članarine podmirene</div><div class="kpi-v">${fmt(cl.n_podmireno)}</div><div class="kpi-s">${podPct}% od ${fmt(cl.total)}</div></div>
<div class="kpi a"><div class="kpi-l">Djelomično</div><div class="kpi-v">${fmt(cl.n_djelomicno)}</div></div>
<div class="kpi r"><div class="kpi-l">Nepodmireno</div><div class="kpi-v">${fmt(cl.n_nepodmireno)}</div></div>
<div class="kpi b"><div class="kpi-l">Plaćeno (€)</div><div class="kpi-v">${fmtEur(cl.placen)}</div></div>
<div class="kpi r"><div class="kpi-l">Dug (€)</div><div class="kpi-v">${fmtEur(cl.dug)}</div></div>
</div>
<div class="kpi-grid">
<div class="kpi g"><div class="kpi-l">Liječnički važeći</div><div class="kpi-v">${fmt(lj.vazeci)}</div></div>
<div class="kpi a"><div class="kpi-l">Uskoro istek</div><div class="kpi-v">${fmt(lj.uskoro)}</div></div>
<div class="kpi r"><div class="kpi-l">Istekli</div><div class="kpi-v">${fmt(lj.istekli)}</div></div>
</div>
<div class="card">
<div class="card-h"><div class="card-t">📈 Trend uplata članarina (zadnjih 12 mjeseci)</div></div>
<div style="padding:14px;text-align:center">
${tr.length ? `<svg viewBox="0 0 ${tr.length*(barW+gap)+60} 210" style="width:100%;max-width:900px">
<defs><linearGradient id="g1" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#1a73e8"/><stop offset="100%" stop-color="#1e3a8a"/></linearGradient></defs>
${barsHtml}
</svg>` : '<div class="empty">Nema podataka o uplatama u zadnjih 12 mjeseci.</div>'}
</div>
</div>
<div class="row" style="display:grid;grid-template-columns:1fr 1fr 1.4fr;gap:14px">
<div class="card">
<div class="card-h"><div class="card-t">👥 Po spolu</div></div>
<table><thead><tr><th>Spol</th><th>Broj</th></tr></thead><tbody>${spolHtml || '<tr><td colspan="2" class="empty">—</td></tr>'}</tbody></table>
</div>
<div class="card">
<div class="card-h"><div class="card-t">🏷 Top kategorije</div></div>
<table><thead><tr><th>Kategorija</th><th>Broj</th></tr></thead><tbody>${katHtml || '<tr><td colspan="2" class="empty">—</td></tr>'}</tbody></table>
</div>
<div class="card">
<div class="card-h"><div class="card-t">💸 Najnovije uplate</div></div>
<table><thead><tr><th>Datum</th><th>Sportaš</th><th>Klub</th><th>God.</th><th>Iznos</th></tr></thead><tbody>${noviHtml || '<tr><td colspan="5" class="empty">—</td></tr>'}</tbody></table>
</div>
</div>`;
}
// ════════════════════════════════════════════════════
// MODUL 6 — NOTIFIKACIJE (R5 #6)
// ════════════════════════════════════════════════════
async function loadNotifs() {
const root = $('#page-notifs');
root.innerHTML = '<div class="loading">Učitavanje notifikacija…</div>';
let d;
try { d = await api('/notifications?limit=200'); }
catch (e) { root.innerHTML = `<div class="empty">Greška: ${esc(e.message)}</div>`; return; }
$('#cnt-notifs').textContent = d.summary?.unread_inapp ?? d.count;
const tools = `
<div class="toolbar">
<button class="btn primary" onclick="scanLijecnicki()">🔄 Scan liječničke → kreiraj notifikacije (30/15/7 dana)</button>
<button class="btn" onclick="markAllReadUI()">✓ Označi sve pročitano</button>
<div class="grow"></div>
<select id="nf-channel" onchange="loadNotifs()" style="min-width:120px">
<option value="">Svi kanali</option>
<option value="inapp">InApp</option>
<option value="email">Email</option>
</select>
<select id="nf-status" onchange="loadNotifs()" style="min-width:120px">
<option value="">Svi statusi</option>
<option value="pending">Pending</option>
<option value="sent">Sent</option>
</select>
</div>`;
const kpi = `
<div class="kpi-grid">
<div class="kpi b"><div class="kpi-l">Ukupno</div><div class="kpi-v">${fmt(d.summary?.total)}</div></div>
<div class="kpi a"><div class="kpi-l">Pending</div><div class="kpi-v">${fmt(d.summary?.pending)}</div></div>
<div class="kpi g"><div class="kpi-l">Sent</div><div class="kpi-v">${fmt(d.summary?.sent)}</div></div>
<div class="kpi r"><div class="kpi-l">InApp neprocitano</div><div class="kpi-v">${fmt(d.summary?.unread_inapp)}</div></div>
</div>`;
const list = (d.rows || []).map(n => `
<div class="card" style="margin-bottom:8px;border-left:3px solid ${n.read_at ? 'var(--t3)' : (n.subject.includes('ISTEKAO') || n.subject.includes('Opomena') ? 'var(--err)' : 'var(--warn)')}">
<div class="card-b" style="padding:12px">
<div style="display:flex;justify-content:space-between;align-items:start;gap:10px">
<div style="flex:1">
<div style="font-weight:600">${esc(n.subject)}</div>
<div style="font-size:11px;color:var(--t3);margin-top:3px">${esc(fmtDate(n.scheduled_at))} · <span class="tag ${n.channel==='inapp'?'bl':'gr'}">${esc(n.channel)}</span> · <span class="tag ${n.status==='pending'?'am':'gr'}">${esc(n.status)}</span>${n.read_at ? ' · pročitano '+esc(fmtDate(n.read_at)) : ''}</div>
<div style="font-size:12px;color:var(--t2);margin-top:6px;white-space:pre-wrap">${esc((n.body||'').substring(0,260))}${(n.body||'').length>260?'…':''}</div>
${n.meta?.zakazi_url ? `<a class="btn sm primary" style="margin-top:6px;display:inline-block" href="${esc(n.meta.zakazi_url)}" target="_blank">📅 Zakaži ZZJZ</a>` : ''}
${n.meta?.uplatnica_url ? `<a class="btn sm primary" style="margin-top:6px;display:inline-block" href="${esc(n.meta.uplatnica_url)}" target="_blank">📄 Uplatnica</a>` : ''}
</div>
${!n.read_at ? `<button class="btn sm" onclick="markRead(${n.id})">✓</button>` : ''}
</div>
</div>
</div>`).join('');
root.innerHTML = kpi + tools + (list || '<div class="empty">Nema notifikacija. Pokreni "Scan liječničke" za generiranje.</div>');
// restore filter selection
const ch = localStorage.getItem('nf-channel'); if (ch && $('#nf-channel')) $('#nf-channel').value = ch;
const st = localStorage.getItem('nf-status'); if (st && $('#nf-status')) $('#nf-status').value = st;
}
async function scanLijecnicki() {
if (!confirm('Skenirati sve liječničke i kreirati notifikacije za istečene + 30/15/7 dana?')) return;
try {
const r = await api('/lijecnicki/notify-scan', {method:'POST', body: {}});
toast(`✓ Kreirano ${r.created} notifikacija (thresholds: ${r.thresholds_dana.join('/')} dana)`);
loadNotifs();
} catch (e) { toast('Greška: ' + e.message, true); }
}
async function markRead(nid) {
try {
await api('/notifications/' + nid + '/read', {method:'POST'});
toast('✓ Označeno pročitano');
loadNotifs();
} catch (e) { toast('Greška: ' + e.message, true); }
}
async function markAllReadUI() {
if (!confirm('Označiti sve InApp notifikacije kao pročitane?')) return;
try {
const r = await api('/notifications/mark-all-read', {method:'POST', body: {channel: 'inapp'}});
toast(`✓ Označeno ${r.marked_read} kao pročitano`);
loadNotifs();
} catch (e) { toast('Greška: ' + e.message, true); }
}
// ════════════════════════════════════════════════════
// MODUL 7 — E-MAIL TEMPLATES (R6 #3)
// ════════════════════════════════════════════════════
async function loadEmailTpl() {
const root = $('#page-emailtpl');
root.innerHTML = '<div class="loading">Učitavanje templata…</div>';
let d;
try { d = await api('/email-templates?active_only=false'); }
catch (e) { root.innerHTML = `<div class="empty">Greška: ${esc(e.message)}</div>`; return; }
const list = (d.templates || []).map(t => `
<div class="card" style="margin-bottom:8px">
<div class="card-b">
<div style="display:flex;justify-content:space-between;gap:10px;align-items:start">
<div style="flex:1">
<div style="font-weight:600">${esc(t.naziv)} <span class="tag ${t.active?'gr':'gy'}">${t.active?'aktivan':'neaktivan'}</span></div>
<div style="font-size:11px;color:var(--t3);margin-top:3px"><code>${esc(t.code)}</code> · ${esc(t.kategorija || '—')} · vars: ${(t.variables||[]).length}</div>
<div style="font-size:12px;color:var(--t2);margin-top:6px"><b>Subject:</b> ${esc(t.subject_tpl)}</div>
<details style="margin-top:6px">
<summary style="cursor:pointer;font-size:11px;color:var(--pgz-blue)">Body preview</summary>
<pre style="background:var(--bg);padding:8px;border-radius:5px;font-size:11px;white-space:pre-wrap;margin-top:4px;color:var(--t1)">${esc(t.body_tpl.substring(0,500))}${t.body_tpl.length>500?'…':''}</pre>
</details>
</div>
<div style="display:flex;flex-direction:column;gap:4px">
<button class="btn sm" onclick='openTplPreview(${JSON.stringify(t.code)})' title="Render s test podacima">▶ Preview</button>
<button class="btn sm" onclick='openTplEdit(${JSON.stringify(t.code)})'>✎ Uredi</button>
</div>
</div>
</div>
</div>`).join('');
root.innerHTML = `
<div class="toolbar">
<span style="font-size:12px;color:var(--t2)">${d.count} template-a — koriste se za <b>opomene članarina</b>, <b>liječničke podsjetnike</b>, <b>obrasce na potpis</b>. Varijable: <code style="font-size:11px">{{key}}</code></span>
<div class="grow"></div>
<button class="btn primary" onclick="openTplCreate()">+ Novi template</button>
</div>
${list || '<div class="empty">Nema templata.</div>'}`;
}
const _DEFAULT_TPL_VARS = {
clanarina_opomena: {ime:'Mateo', prezime:'Hrvatin', godina:2026, klub:'RK ZAMET', iznos_dug:'720,00', razdoblje:'godišnja', uplatnica_url:'/api/crm/clanarine/218/uplatnica.pdf'},
lijecnicki_podsjetnik: {ime:'Mateo', prezime:'Hrvatin', klub:'RK ZAMET', status_msg:'ističe za 7 dana', vrijedi_do:'2026-05-12', dana:7, ustanova:'ZZJZ PGŽ', zakazi_url:'/api/crm/lijecnicki/100/zakazi'},
obrazac_potpis: {ime:'Damir', prezime:'Radulić', naziv_obrasca:'Sufinanciranje 2026', reference_no:'SUFINANC-2026-A0BCF45D', klub:'RK ZAMET', status:'draft'},
};
async function openTplPreview(code) {
const vars = _DEFAULT_TPL_VARS[code] || {};
let r;
try { r = await api(`/email-templates/${code}/render`, {method:'POST', body: {variables: vars}}); }
catch (e) { return toast('Greška: ' + e.message, true); }
$('#modal').style.maxWidth = '720px';
openModal(`
<div class="modal-h"><div class="modal-t">▶ Preview: ${esc(r.naziv || code)}</div><div class="modal-x" onclick="closeModal()">×</div></div>
<div class="modal-b">
<div style="background:var(--bg);padding:12px;border-radius:6px;border:1px solid var(--rim);margin-bottom:10px">
<div style="font-size:11px;color:var(--t3)">SUBJECT</div>
<div style="font-weight:600;margin-top:4px">${esc(r.subject)}</div>
</div>
<div style="background:var(--bg);padding:12px;border-radius:6px;border:1px solid var(--rim)">
<div style="font-size:11px;color:var(--t3)">BODY</div>
<pre style="margin-top:6px;white-space:pre-wrap;color:var(--t1);font-family:inherit;font-size:13px">${esc(r.body)}</pre>
</div>
<div style="margin-top:10px;font-size:11px;color:var(--t3)">Varijable: <code>${esc(JSON.stringify(vars))}</code></div>
<div style="text-align:right;margin-top:14px">
<button class="btn" onclick="closeModal()">Zatvori</button>
<button class="btn primary" onclick='sendTplMock(${JSON.stringify(code)})'>📤 Pošalji test (upiše u notifs)</button>
</div>
</div>`);
}
async function sendTplMock(code) {
const to = prompt('To (e-mail za mock send):', 'test@rinet.one');
if (!to) return;
const vars = _DEFAULT_TPL_VARS[code] || {};
try {
const r = await api(`/email-templates/${code}/send`, {method:'POST', body: {to, user_id: 1, variables: vars}});
closeModal();
toast(`✓ Poslano (mock): ${r.queued.length} notifikacije`);
loadNotifs();
} catch (e) { toast('Greška: ' + e.message, true); }
}
async function openTplEdit(code) {
let t;
try { t = await api(`/email-templates/${code}`); }
catch (e) { return toast('Greška: ' + e.message, true); }
$('#modal').style.maxWidth = '720px';
openModal(`
<div class="modal-h"><div class="modal-t">✎ Uredi: ${esc(t.naziv)}</div><div class="modal-x" onclick="closeModal()">×</div></div>
<div class="modal-b">
<form onsubmit="saveTpl(event, ${JSON.stringify(code)})">
<div class="field"><label>Naziv</label><input name="naziv" value="${esc(t.naziv)}" required></div>
<div class="field"><label>Kategorija</label><input name="kategorija" value="${esc(t.kategorija||'')}"></div>
<div class="field"><label class="req">Subject template</label><input name="subject_tpl" value="${esc(t.subject_tpl)}" required></div>
<div class="field"><label class="req">Body template ({{var}} sintaksa)</label><textarea name="body_tpl" style="min-height:200px;font-family:Consolas,monospace;font-size:12px" required>${esc(t.body_tpl)}</textarea></div>
<div class="field"><label>Aktivan</label><select name="active"><option value="true" ${t.active?'selected':''}>true</option><option value="false" ${!t.active?'selected':''}>false</option></select></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</button>
</div>
</form>
</div>`);
}
async function saveTpl(e, code) {
e.preventDefault();
const f = e.target;
const body = {
naziv: f.naziv.value,
kategorija: f.kategorija.value || null,
subject_tpl: f.subject_tpl.value,
body_tpl: f.body_tpl.value,
active: f.active.value === 'true',
};
try {
await api(`/email-templates/${code}`, {method:'PUT', body});
closeModal();
toast('✓ Template spremljen.');
loadEmailTpl();
} catch (err) { toast('Greška: ' + err.message, true); }
}
function openTplCreate() {
$('#modal').style.maxWidth = '720px';
openModal(`
<div class="modal-h"><div class="modal-t">+ Novi e-mail template</div><div class="modal-x" onclick="closeModal()">×</div></div>
<div class="modal-b">
<form onsubmit="createTpl(event)">
<div class="field"><label class="req">Code (jedinstveni, lower_snake)</label><input name="code" required pattern="[a-z0-9_]+" placeholder="npr. clan_dobrodoslica"></div>
<div class="field"><label class="req">Naziv</label><input name="naziv" required></div>
<div class="field"><label>Kategorija</label><input name="kategorija" placeholder="clanarine | lijecnicki | obrasci | ostalo"></div>
<div class="field"><label class="req">Subject template</label><input name="subject_tpl" required value="Predmet — {{ime}} {{prezime}}"></div>
<div class="field"><label class="req">Body template</label><textarea name="body_tpl" style="min-height:200px;font-family:Consolas,monospace;font-size:12px" required>Poštovani {{ime}} {{prezime}},
(sadržaj)
S poštovanjem,
PGŽ Sport ERP/CRM</textarea></div>
<div style="text-align:right;margin-top:14px">
<button type="button" class="btn" onclick="closeModal()">Odustani</button>
<button type="submit" class="btn primary">💾 Kreiraj</button>
</div>
</form>
</div>`);
}
async function createTpl(e) {
e.preventDefault();
const f = e.target;
const body = {
code: f.code.value,
naziv: f.naziv.value,
kategorija: f.kategorija.value || null,
subject_tpl: f.subject_tpl.value,
body_tpl: f.body_tpl.value,
};
try {
await api('/email-templates', {method:'POST', body});
closeModal();
toast('✓ Template kreiran.');
loadEmailTpl();
} catch (err) { toast('Greška: ' + err.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;
const nf = await api('/notifications?limit=1');
$('#cnt-notifs').textContent = nf.summary?.unread_inapp ?? 0;
} catch (e) {}
})();
</script>
</body>
</html>