CC5 R5 UI: Kalendar + Stats + Notifs + bulk akcije + XLSX export
app.html — Kalendar sekcija (NOVO za sve role): - Mjesečni grid (pon-ned), klik na dan = prikaz svih eventa - KPI: liječnički isteci, manifestacije, neprocitano InApp, ukupno eventa - Eventi: liječnički termini + manifestacije iz API + ZZJZ termin slot mock - Navigacija prev/next + month picker - "Scan isteke → notifikacije" gumb (one-click backend POST) - Lista nadolazećih (10) + lista InApp neprocitanih s mark-read crm.html — 2 nova taba: - 📊 Statistika: aktivni vs neaktivni, reprezentativci, kategorizirani, članarine summary, liječnički status, SVG bar chart trend uplata 12 mj, podjela po spolu/kategoriji, top 10 najnovijih uplata - 🔔 Notifikacije: lista InApp+Email s filterima (channel/status), gumb za scan liječničkih (kreira 30/15/7 + expired bucket), mark-read pojedinačno i bulk, deep-link na /lijecnicki/{id}/zakazi i /clanarine/{id}/uplatnica.pdf iz meta polja Bulk akcije za clanarine (R5 #3): - Checkbox po retku + master + "Sve nepladene" gumb - Bulk bar pokazuje selected count + total dug - "Pošalji opomenu" → POST /bulk/notify (sa specifičnim ids ili sve dužnike) - "Generiraj uplatnice" → POST /bulk/uplatnice → modal s linkovima na PDF/QR XLSX export (R5 #4): - "📥 Export XLSX" gumb na Članovi tab → otvara /clanovi/export.xlsx s trenutnim filterima (klub_id, q)
This commit is contained in:
+264
-8
@@ -156,6 +156,8 @@ table tr:hover td { background: rgba(26, 115, 232, 0.05); }
|
||||
<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 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">
|
||||
@@ -174,6 +176,8 @@ table tr:hover td { background: rgba(26, 115, 232, 0.05); }
|
||||
<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>
|
||||
|
||||
<div id="modal-bg" class="modal-bg" onclick="if(event.target===this)closeModal()">
|
||||
@@ -252,6 +256,8 @@ function setTab(name) {
|
||||
if (name === 'clanarine') loadClanarine();
|
||||
if (name === 'lijecnicki') loadLijecnicki();
|
||||
if (name === 'obrasci') loadObrasci();
|
||||
if (name === 'stats') loadStats();
|
||||
if (name === 'notifs') loadNotifs();
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════
|
||||
@@ -286,11 +292,22 @@ async function loadClanarine() {
|
||||
<input id="cl-godina" type="number" placeholder="Godina" min="2020" max="2030" onchange="loadClanarineFiltered()">
|
||||
<input id="cl-klub" type="number" placeholder="Klub ID" onchange="loadClanarineFiltered()">
|
||||
<div class="grow"></div>
|
||||
<button class="btn primary" onclick="bulkNotify()">📧 Notify dužnike</button>
|
||||
<button class="btn" onclick="selectAllUnpaid()">☑ Sve nepladene</button>
|
||||
<button class="btn primary" onclick="bulkNotifySelected()">📧 Pošalji opomenu</button>
|
||||
<button class="btn" onclick="bulkUplatniceSelected()">📄 Generiraj uplatnice</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 rows = (data.rows || []).map(r => `
|
||||
<tr>
|
||||
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>
|
||||
@@ -303,18 +320,83 @@ async function loadClanarine() {
|
||||
<button class="btn sm" onclick="openUplata(${r.id})" title="Registriraj uplatu">+€</button>
|
||||
<a class="btn sm" href="${API}/clanarine/${r.id}/uplatnica.pdf" target="_blank" title="HUB-3 PDF">📄</a>
|
||||
</td>
|
||||
</tr>`).join('');
|
||||
</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>Sportaš/Klub</th><th>God.</th><th>Razdoblje</th><th>Propisan</th><th>Plaćeno</th><th>Dug</th><th>Status</th><th></th></tr></thead>
|
||||
<tbody>${rows || '<tr><td colspan="8" class="empty">Nema zapisa.</td></tr>'}</tbody>
|
||||
<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 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';
|
||||
}
|
||||
@@ -330,7 +412,8 @@ async function loadClanarineFiltered() {
|
||||
const data = await api('/clanarine?' + params);
|
||||
const tbody = $('#page-clanarine table tbody');
|
||||
tbody.innerHTML = (data.rows || []).map(r => `
|
||||
<tr>
|
||||
<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>
|
||||
@@ -343,7 +426,8 @@ async function loadClanarineFiltered() {
|
||||
<button class="btn sm" onclick="openUplata(${r.id})">+€</button>
|
||||
<a class="btn sm" href="${API}/clanarine/${r.id}/uplatnica.pdf" target="_blank">📄</a>
|
||||
</td>
|
||||
</tr>`).join('') || '<tr><td colspan="8" class="empty">Nema zapisa.</td></tr>';
|
||||
</tr>`).join('') || '<tr><td colspan="9" class="empty">Nema zapisa.</td></tr>';
|
||||
clearSelection();
|
||||
}
|
||||
|
||||
async function openPayment(id) {
|
||||
@@ -1007,6 +1091,7 @@ async function loadClanovi() {
|
||||
<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>
|
||||
@@ -1339,6 +1424,175 @@ async function uploadAvatar(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); }
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────
|
||||
// init
|
||||
// ────────────────────────────────────────────────────
|
||||
@@ -1356,6 +1610,8 @@ loadClanovi();
|
||||
$('#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>
|
||||
|
||||
Reference in New Issue
Block a user