RUSH 4-sub: filteri Klubovi/Sportaši + manifestacije card view + CRM v2 redesign
RUSH-1 Klubovi: list_klubovi() LEFT JOIN v_klubovi_financiranje (prima_pgz/rss/grad_rijeka, u_godisnjaku, ukupno_potpora). financiran=true sad OR od 3 davatelja (drop legacy klubovi.pgz_sufinanciran s 1312 false-positive). Sort sort=potpora&order=desc. UI: gold ukupno_potpora + tooltip + sortable kolona. Defaults priority view (financirani+godišnjak ON, hns_roster OFF). Test: priority=604, +hns=36, all=1641, financiran=15 sorted ZAMET 80208€. RUSH-2 Sportaši: SELECT widened (slika_url, reprezentativac, kategoriziran, broj_dresa). avatarUrl() helper s 3 forme (apsolutni / lokalni /sport/uploads/avatars / initials fallback) + 32px circular avatar lijevo od imena. Test: priority=3712, no-priority=6086, +hns=1439, 1990-2000=645. RUSH-3 Manifestacije: bugfix razina filter HTTP 500 (ambiguous column nakon LEFT JOIN savezi → m.razina/mjesto/organizator). 3 dropdowna iz meta (26 mjesta / 8 razina / 50 organizatora), view toggle 🃏 Kartice / 📋 Tablica (localStorage), 🔗 link ikona u card+table, source_url → Google fallback. Test: default=3, mjesto=Lošinj=2, razina=Tradicionalna=3, organizator=AK Kvarner=1. RUSH-4 CRM v2: tab strip rewrite (10 taba u spec redu Članarine|Liječnički|Obrasci|E-mail|Accounts|Contacts|Leads|Opps|Activities|Cases, sticky+scrollable+gold underline). Pipeline → Opps tab. Novi e-mail templates tab (5 endpointa, 3 seed templates, +Novi modal). Card layout (.cgrid/.ccard) za Accounts/Contacts/Leads/Opps. Export dropdown 📥 ▾ CSV/XLSX(SheetJS CDN)/PDF na svaki tab. Test: /crm_v2 200, 10/10 tab labela, 10 Export dropdowna + 31 exportTab() handlera. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+456
-88
@@ -10,6 +10,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>PGŽ Sport — CRM v2 (Salesforce-Lite)</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--pgz-blue:#1a73e8; --pgz-blue2:#1e3a8a; --pgz-gold:#fbbf24;
|
||||
@@ -234,7 +235,7 @@ footer { height:36px; background:var(--bg2); border-top:1px solid var(--rim);
|
||||
<div class="topbar">
|
||||
<span class="logo">PGŽ SPORT</span>
|
||||
<span class="sep">›</span>
|
||||
<span class="title">CRM v2 — Salesforce-Lite (Pipeline)</span>
|
||||
<span class="title">CRM v2 — Salesforce-Lite</span>
|
||||
<div class="right">
|
||||
<span id="me">…</span>
|
||||
<a href="/sport/platform">Platform</a>
|
||||
@@ -245,40 +246,21 @@ footer { height:36px; background:var(--bg2); border-top:1px solid var(--rim);
|
||||
</div>
|
||||
|
||||
<div class="tabs">
|
||||
<div class="tab active" data-tab="pipeline">Pipeline</div>
|
||||
<div class="tab active" data-tab="clanarine">Članarine <span class="count" id="cnt-clanarine">·</span></div>
|
||||
<div class="tab" data-tab="lijecnicki">Liječnički <span class="count" id="cnt-lijecnicki">·</span></div>
|
||||
<div class="tab" data-tab="obrasci">Obrasci <span class="count" id="cnt-obrasci">·</span></div>
|
||||
<div class="tab" data-tab="emailtpl">E-mail templates <span class="count" id="cnt-emailtpl">·</span></div>
|
||||
<div class="tab" data-tab="accounts">Accounts <span class="count" id="cnt-accounts">·</span></div>
|
||||
<div class="tab" data-tab="contacts">Contacts <span class="count" id="cnt-contacts">·</span></div>
|
||||
<div class="tab" data-tab="leads">Leads <span class="count" id="cnt-leads">·</span></div>
|
||||
<div class="tab" data-tab="opportunities">Opportunities <span class="count" id="cnt-opps">·</span></div>
|
||||
<div class="tab" data-tab="activities">Activities <span class="count" id="cnt-activities">·</span></div>
|
||||
<div class="tab" data-tab="cases">Cases <span class="count" id="cnt-cases">·</span></div>
|
||||
<div class="tab" data-tab="clanarine">Članarine <span class="count" id="cnt-clanarine">·</span></div>
|
||||
<div class="tab" data-tab="lijecnicki">Liječnički <span class="count" id="cnt-lijecnicki">·</span></div>
|
||||
<div class="tab" data-tab="obrasci">Obrasci <span class="count" id="cnt-obrasci">·</span></div>
|
||||
</div>
|
||||
|
||||
<div class="main">
|
||||
|
||||
<!-- ────── PIPELINE ────── -->
|
||||
<div class="tab-c on" id="tc-pipeline">
|
||||
<div class="kpi-grid">
|
||||
<div class="kpi b"><div class="kpi-l">Open opps</div><div class="kpi-v" id="k-opps">·</div><div class="kpi-s" id="k-opps-eur">·</div></div>
|
||||
<div class="kpi gold"><div class="kpi-l">Weighted total</div><div class="kpi-v" id="k-weighted">·</div><div class="kpi-s">prosjek vjerojatnosti</div></div>
|
||||
<div class="kpi g"><div class="kpi-l">Won this quarter</div><div class="kpi-v" id="k-won">·</div><div class="kpi-s" id="k-won-eur">·</div></div>
|
||||
<div class="kpi a"><div class="kpi-l">Leads (new+contacted)</div><div class="kpi-v" id="k-leads">·</div><div class="kpi-s">qualified: <span id="k-leads-q">·</span></div></div>
|
||||
<div class="kpi r"><div class="kpi-l">Overdue activities</div><div class="kpi-v" id="k-overdue">·</div><div class="kpi-s">upcoming: <span id="k-upcoming">·</span></div></div>
|
||||
<div class="kpi"><div class="kpi-l">Open cases</div><div class="kpi-v" id="k-cases">·</div><div class="kpi-s" id="k-cases-urgent">·</div></div>
|
||||
</div>
|
||||
|
||||
<div class="toolbar">
|
||||
<strong>Pipeline kanban</strong>
|
||||
<span class="grow"></span>
|
||||
<button class="btn primary sm" onclick="openOppModal()">+ Nova prilika</button>
|
||||
<button class="btn sm" onclick="loadPipeline()">↻ Refresh</button>
|
||||
</div>
|
||||
|
||||
<div class="kanban" id="kanban"></div>
|
||||
</div>
|
||||
<!-- ────── PIPELINE (legacy tab removed — KPIs + kanban now live in Opportunities tab) ────── -->
|
||||
|
||||
<!-- ────── ACCOUNTS ────── -->
|
||||
<div class="tab-c" id="tc-accounts">
|
||||
@@ -294,9 +276,17 @@ footer { height:36px; background:var(--bg2); border-top:1px solid var(--rim);
|
||||
</select>
|
||||
<button class="btn" onclick="loadAccounts()">Pretraži</button>
|
||||
<span class="grow"></span>
|
||||
<div class="exp"><button class="exp-btn" onclick="toggleExp('exp-accounts')">📥 Export ▾</button>
|
||||
<div class="exp-menu" id="exp-accounts">
|
||||
<button onclick="exportTab('accounts','csv')">CSV</button>
|
||||
<button onclick="exportTab('accounts','xlsx')">XLSX</button>
|
||||
<button onclick="exportTab('accounts','pdf')">PDF</button>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn primary" onclick="openAccountModal()">+ Novi account</button>
|
||||
</div>
|
||||
<div class="card"><div class="card-b" style="padding:0">
|
||||
<div id="acc-cards" class="cgrid"></div>
|
||||
<div class="card" style="display:none"><div class="card-b" style="padding:0">
|
||||
<table id="t-accounts"><thead><tr>
|
||||
<th>Naziv</th><th>Tip</th><th>Grad</th><th>OIB</th><th>Email</th>
|
||||
<th>Kontakti</th><th>Opps</th><th>Owner</th><th></th>
|
||||
@@ -311,9 +301,17 @@ footer { height:36px; background:var(--bg2); border-top:1px solid var(--rim);
|
||||
<input id="con-acc" type="number" placeholder="Account ID (filter)" style="max-width:170px">
|
||||
<button class="btn" onclick="loadContacts()">Pretraži</button>
|
||||
<span class="grow"></span>
|
||||
<div class="exp"><button class="exp-btn" onclick="toggleExp('exp-contacts')">📥 Export ▾</button>
|
||||
<div class="exp-menu" id="exp-contacts">
|
||||
<button onclick="exportTab('contacts','csv')">CSV</button>
|
||||
<button onclick="exportTab('contacts','xlsx')">XLSX</button>
|
||||
<button onclick="exportTab('contacts','pdf')">PDF</button>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn primary" onclick="openContactModal()">+ Novi kontakt</button>
|
||||
</div>
|
||||
<div class="card"><div class="card-b" style="padding:0">
|
||||
<div id="con-cards" class="cgrid"></div>
|
||||
<div class="card" style="display:none"><div class="card-b" style="padding:0">
|
||||
<table id="t-contacts"><thead><tr>
|
||||
<th>Ime</th><th>Prezime</th><th>Account</th><th>Funkcija</th>
|
||||
<th>Email</th><th>Telefon</th><th></th>
|
||||
@@ -335,9 +333,17 @@ footer { height:36px; background:var(--bg2); border-top:1px solid var(--rim);
|
||||
</select>
|
||||
<button class="btn" onclick="loadLeads()">Pretraži</button>
|
||||
<span class="grow"></span>
|
||||
<div class="exp"><button class="exp-btn" onclick="toggleExp('exp-leads')">📥 Export ▾</button>
|
||||
<div class="exp-menu" id="exp-leads">
|
||||
<button onclick="exportTab('leads','csv')">CSV</button>
|
||||
<button onclick="exportTab('leads','xlsx')">XLSX</button>
|
||||
<button onclick="exportTab('leads','pdf')">PDF</button>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn primary" onclick="openLeadModal()">+ Novi lead</button>
|
||||
</div>
|
||||
<div class="card"><div class="card-b" style="padding:0">
|
||||
<div id="lead-cards" class="cgrid"></div>
|
||||
<div class="card" style="display:none"><div class="card-b" style="padding:0">
|
||||
<table id="t-leads"><thead><tr>
|
||||
<th>Ime</th><th>Prezime</th><th>Organizacija</th><th>Email</th>
|
||||
<th>Izvor</th><th>Status</th><th></th>
|
||||
@@ -360,9 +366,34 @@ footer { height:36px; background:var(--bg2); border-top:1px solid var(--rim);
|
||||
</select>
|
||||
<button class="btn" onclick="loadOpps()">Pretraži</button>
|
||||
<span class="grow"></span>
|
||||
<div class="exp"><button class="exp-btn" onclick="toggleExp('exp-opps')">📥 Export ▾</button>
|
||||
<div class="exp-menu" id="exp-opps">
|
||||
<button onclick="exportTab('opps','csv')">CSV</button>
|
||||
<button onclick="exportTab('opps','xlsx')">XLSX</button>
|
||||
<button onclick="exportTab('opps','pdf')">PDF</button>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn primary" onclick="openOppModal()">+ Nova prilika</button>
|
||||
</div>
|
||||
<div class="card"><div class="card-b" style="padding:0">
|
||||
|
||||
<!-- KPIs + Pipeline kanban (migrirano iz Pipeline taba) -->
|
||||
<div class="kpi-grid">
|
||||
<div class="kpi b"><div class="kpi-l">Open opps</div><div class="kpi-v" id="k-opps">·</div><div class="kpi-s" id="k-opps-eur">·</div></div>
|
||||
<div class="kpi gold"><div class="kpi-l">Weighted total</div><div class="kpi-v" id="k-weighted">·</div><div class="kpi-s">prosjek vjerojatnosti</div></div>
|
||||
<div class="kpi g"><div class="kpi-l">Won this quarter</div><div class="kpi-v" id="k-won">·</div><div class="kpi-s" id="k-won-eur">·</div></div>
|
||||
<div class="kpi a"><div class="kpi-l">Leads (new+contacted)</div><div class="kpi-v" id="k-leads">·</div><div class="kpi-s">qualified: <span id="k-leads-q">·</span></div></div>
|
||||
<div class="kpi r"><div class="kpi-l">Overdue activities</div><div class="kpi-v" id="k-overdue">·</div><div class="kpi-s">upcoming: <span id="k-upcoming">·</span></div></div>
|
||||
<div class="kpi"><div class="kpi-l">Open cases</div><div class="kpi-v" id="k-cases">·</div><div class="kpi-s" id="k-cases-urgent">·</div></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-h"><div class="card-t">Pipeline kanban</div>
|
||||
<button class="btn sm" onclick="loadPipeline()">↻ Refresh</button>
|
||||
</div>
|
||||
<div class="card-b"><div class="kanban" id="kanban"></div></div>
|
||||
</div>
|
||||
|
||||
<div id="opp-cards" class="cgrid"></div>
|
||||
<div class="card" style="display:none"><div class="card-b" style="padding:0">
|
||||
<table id="t-opps"><thead><tr>
|
||||
<th>Naziv</th><th>Account</th><th>Tip</th><th>Faza</th>
|
||||
<th>EUR</th><th>%</th><th>Close</th><th></th>
|
||||
@@ -388,6 +419,13 @@ footer { height:36px; background:var(--bg2); border-top:1px solid var(--rim);
|
||||
</select>
|
||||
<button class="btn" onclick="loadActivities()">Filtriraj</button>
|
||||
<span class="grow"></span>
|
||||
<div class="exp"><button class="exp-btn" onclick="toggleExp('exp-activities')">📥 Export ▾</button>
|
||||
<div class="exp-menu" id="exp-activities">
|
||||
<button onclick="exportTab('activities','csv')">CSV</button>
|
||||
<button onclick="exportTab('activities','xlsx')">XLSX</button>
|
||||
<button onclick="exportTab('activities','pdf')">PDF</button>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn primary" onclick="openActivityModal()">+ Nova aktivnost</button>
|
||||
</div>
|
||||
<div class="card"><div class="card-b" style="padding:0">
|
||||
@@ -419,6 +457,13 @@ footer { height:36px; background:var(--bg2); border-top:1px solid var(--rim);
|
||||
</select>
|
||||
<button class="btn" onclick="loadCases()">Pretraži</button>
|
||||
<span class="grow"></span>
|
||||
<div class="exp"><button class="exp-btn" onclick="toggleExp('exp-cases')">📥 Export ▾</button>
|
||||
<div class="exp-menu" id="exp-cases">
|
||||
<button onclick="exportTab('cases','csv')">CSV</button>
|
||||
<button onclick="exportTab('cases','xlsx')">XLSX</button>
|
||||
<button onclick="exportTab('cases','pdf')">PDF</button>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn primary" onclick="openCaseModal()">+ Novi case</button>
|
||||
</div>
|
||||
<div class="card"><div class="card-b" style="padding:0">
|
||||
@@ -430,7 +475,7 @@ footer { height:36px; background:var(--bg2); border-top:1px solid var(--rim);
|
||||
</div>
|
||||
|
||||
<!-- ────── ČLANARINE ────── -->
|
||||
<div class="tab-c" id="tc-clanarine">
|
||||
<div class="tab-c on" id="tc-clanarine">
|
||||
<div class="toolbar">
|
||||
<input id="cln-klub" type="number" placeholder="Klub ID" style="max-width:120px">
|
||||
<input id="cln-clan" type="number" placeholder="Član ID" style="max-width:120px">
|
||||
@@ -444,6 +489,13 @@ footer { height:36px; background:var(--bg2); border-top:1px solid var(--rim);
|
||||
</select>
|
||||
<button class="btn" onclick="loadClanarine()">Filtriraj</button>
|
||||
<span class="grow"></span>
|
||||
<div class="exp"><button class="exp-btn" onclick="toggleExp('exp-clanarine')">📥 Export ▾</button>
|
||||
<div class="exp-menu" id="exp-clanarine">
|
||||
<button onclick="exportTab('clanarine','csv')">CSV</button>
|
||||
<button onclick="exportTab('clanarine','xlsx')">XLSX</button>
|
||||
<button onclick="exportTab('clanarine','pdf')">PDF</button>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn primary" onclick="openClanarinaModal()">+ Nova članarina</button>
|
||||
</div>
|
||||
<div class="card"><div class="card-b" style="padding:0">
|
||||
@@ -465,6 +517,14 @@ footer { height:36px; background:var(--bg2); border-top:1px solid var(--rim);
|
||||
</select>
|
||||
<button class="btn" onclick="loadLijecnicki()">Filtriraj</button>
|
||||
<span class="grow"></span>
|
||||
<div class="exp"><button class="exp-btn" onclick="toggleExp('exp-lijecnicki')">📥 Export ▾</button>
|
||||
<div class="exp-menu" id="exp-lijecnicki">
|
||||
<button onclick="exportTab('lijecnicki','csv')">CSV</button>
|
||||
<button onclick="exportTab('lijecnicki','xlsx')">XLSX</button>
|
||||
<button onclick="exportTab('lijecnicki','pdf')">PDF</button>
|
||||
</div>
|
||||
</div>
|
||||
<button id="lij-srv-export-btn" class="export-btn" type="button" title="Server-side export svih redaka iz baze">Export ▾ (svi)</button>
|
||||
<button class="btn primary" onclick="openLijecnickiModal()">+ Novi pregled</button>
|
||||
</div>
|
||||
<div class="card"><div class="card-b" style="padding:0">
|
||||
@@ -501,6 +561,13 @@ footer { height:36px; background:var(--bg2); border-top:1px solid var(--rim);
|
||||
</select>
|
||||
<input id="obr-klub" type="number" placeholder="Klub ID" style="max-width:120px">
|
||||
<button class="btn" onclick="loadObrasciSubmissions()">Filtriraj</button>
|
||||
<div class="exp"><button class="exp-btn" onclick="toggleExp('exp-obrasci')">📥 Export ▾</button>
|
||||
<div class="exp-menu" id="exp-obrasci">
|
||||
<button onclick="exportTab('obrasci','csv')">CSV</button>
|
||||
<button onclick="exportTab('obrasci','xlsx')">XLSX</button>
|
||||
<button onclick="exportTab('obrasci','pdf')">PDF</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card"><div class="card-b" style="padding:0" id="obr-right-body">
|
||||
<table id="t-obr-sub"><thead><tr>
|
||||
@@ -512,6 +579,31 @@ footer { height:36px; background:var(--bg2); border-top:1px solid var(--rim);
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ────── E-MAIL TEMPLATES (RUSH-4 / 2026-05-05) ────── -->
|
||||
<div class="tab-c" id="tc-emailtpl">
|
||||
<div class="toolbar">
|
||||
<input id="etpl-q" placeholder="Pretraži (kod / naziv)…">
|
||||
<select id="etpl-cat">
|
||||
<option value="">— Sve kategorije —</option>
|
||||
<option value="clanarine">clanarine</option>
|
||||
<option value="lijecnicki">lijecnicki</option>
|
||||
<option value="obrasci">obrasci</option>
|
||||
<option value="opci">opci</option>
|
||||
</select>
|
||||
<button class="btn" onclick="loadEmailTpls()">Pretraži</button>
|
||||
<span class="grow"></span>
|
||||
<div class="exp"><button class="exp-btn" onclick="toggleExp('exp-emailtpl')">📥 Export ▾</button>
|
||||
<div class="exp-menu" id="exp-emailtpl">
|
||||
<button onclick="exportTab('emailtpl','csv')">CSV</button>
|
||||
<button onclick="exportTab('emailtpl','xlsx')">XLSX</button>
|
||||
<button onclick="exportTab('emailtpl','pdf')">PDF</button>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn primary" onclick="openEmailTplModal()">+ Novi predložak</button>
|
||||
</div>
|
||||
<div id="etpl-grid" class="cgrid"></div>
|
||||
</div>
|
||||
|
||||
</div><!-- /main -->
|
||||
|
||||
<footer>
|
||||
@@ -610,17 +702,90 @@ const esc = s => String(s==null?'':s).replace(/[&<>"']/g, c=>({'&':'&','<':'
|
||||
document.querySelectorAll('.tab').forEach(t => t.addEventListener('click', () => switchTab(t.dataset.tab)));
|
||||
function switchTab(name) {
|
||||
document.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', t.dataset.tab===name));
|
||||
document.querySelectorAll('.tab-c').forEach(c => c.classList.toggle('on', c.id==='tc-'+name || (name==='opportunities' && c.id==='tc-opps')));
|
||||
if (name==='pipeline') loadPipeline();
|
||||
document.querySelectorAll('.tab-c').forEach(c => c.classList.toggle('on', c.id==='tc-'+name || (name==='opportunities' && c.id==='tc-opps') || (name==='emailtpl' && c.id==='tc-emailtpl')));
|
||||
// Always refresh KPIs/pipeline counts in background
|
||||
if (name==='accounts') loadAccounts();
|
||||
if (name==='contacts') loadContacts();
|
||||
if (name==='leads') loadLeads();
|
||||
if (name==='opportunities') loadOpps();
|
||||
if (name==='opportunities') { loadOpps(); loadPipeline(); }
|
||||
if (name==='activities') loadActivities();
|
||||
if (name==='cases') loadCases();
|
||||
if (name==='clanarine') loadClanarine();
|
||||
if (name==='lijecnicki') loadLijecnicki();
|
||||
if (name==='obrasci') { loadObrasciTemplates(); loadObrasciSubmissions(); }
|
||||
if (name==='emailtpl') loadEmailTpls();
|
||||
}
|
||||
|
||||
// ────── Export dropdown helpers ──────
|
||||
function toggleExp(id) {
|
||||
const m = document.getElementById(id); if (!m) return;
|
||||
document.querySelectorAll('.exp-menu').forEach(x => { if (x.id !== id) x.classList.remove('on'); });
|
||||
m.classList.toggle('on');
|
||||
}
|
||||
document.addEventListener('click', e => {
|
||||
if (!e.target.closest('.exp')) document.querySelectorAll('.exp-menu').forEach(x => x.classList.remove('on'));
|
||||
});
|
||||
|
||||
// Tabular export: pull rows from current cards/tables for a given tab
|
||||
const EXPORT_HEADERS = {
|
||||
accounts: ['Naziv','Tip','Grad','OIB','Email','Telefon','Kontakti','Opps','Owner'],
|
||||
contacts: ['Ime','Prezime','Account','Funkcija','Email','Telefon'],
|
||||
leads: ['Ime','Prezime','Organizacija','Email','Telefon','Izvor','Status'],
|
||||
opps: ['Naziv','Account','Tip','Faza','EUR','Vjerojatnost %','Close'],
|
||||
activities: ['Tip','Subject','Account','Kontakt','Due','Status'],
|
||||
cases: ['Subject','Account','Status','Priority','Stvoren'],
|
||||
clanarine: ['Član','Klub','Godina','Razdoblje','Propisan','Plaćen','Datum uplate','Status'],
|
||||
lijecnicki: ['Član','Klub','Datum','Vrsta','Vrijedi do','Liječnik','Spreman','Plaćeno'],
|
||||
obrasci: ['ID','Predložak','Klub','Član','Status','Submitted','Approved'],
|
||||
emailtpl: ['Code','Naziv','Kategorija','Subject','Active'],
|
||||
};
|
||||
const EXPORT_CACHE = {}; // tab → array of row arrays
|
||||
|
||||
function setExportRows(tab, rows) { EXPORT_CACHE[tab] = rows; }
|
||||
|
||||
function exportTab(tab, fmt) {
|
||||
document.querySelectorAll('.exp-menu').forEach(x => x.classList.remove('on'));
|
||||
const headers = EXPORT_HEADERS[tab] || [];
|
||||
const rows = EXPORT_CACHE[tab] || [];
|
||||
if (!rows.length) { toast('Nema redaka za export', 'err'); return; }
|
||||
const fname = 'crm_' + tab + '_' + new Date().toISOString().slice(0,10);
|
||||
if (fmt === 'csv') {
|
||||
const csv = [headers, ...rows].map(r => r.map(c => {
|
||||
const s = (c==null?'':String(c)).replace(/"/g,'""');
|
||||
return /[",\n;]/.test(s) ? '"'+s+'"' : s;
|
||||
}).join(';')).join('\r\n');
|
||||
const blob = new Blob([''+csv], {type:'text/csv;charset=utf-8'});
|
||||
const a = document.createElement('a'); a.href = URL.createObjectURL(blob);
|
||||
a.download = fname + '.csv'; a.click();
|
||||
setTimeout(()=>URL.revokeObjectURL(a.href), 1000);
|
||||
toast('CSV: '+rows.length+' redaka');
|
||||
} else if (fmt === 'xlsx') {
|
||||
if (typeof XLSX === 'undefined') { toast('SheetJS nije učitan', 'err'); return; }
|
||||
const ws = XLSX.utils.aoa_to_sheet([headers, ...rows]);
|
||||
const wb = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(wb, ws, tab);
|
||||
XLSX.writeFile(wb, fname + '.xlsx');
|
||||
toast('XLSX: '+rows.length+' redaka');
|
||||
} else if (fmt === 'pdf') {
|
||||
// Print-friendly window: use a fresh document with table only
|
||||
const w = window.open('', '_blank', 'width=900,height=700');
|
||||
const html = `<!doctype html><html><head><title>${fname}</title>
|
||||
<style>
|
||||
body{font-family:Arial,sans-serif;font-size:11px;margin:18px;color:#000}
|
||||
h2{font-size:15px;margin:0 0 8px}
|
||||
table{width:100%;border-collapse:collapse;font-size:10px}
|
||||
th{background:#eee;text-align:left;padding:5px 7px;border:1px solid #999;text-transform:uppercase;font-size:9px;letter-spacing:.4px}
|
||||
td{padding:4px 7px;border:1px solid #ccc}
|
||||
tr:nth-child(even) td{background:#fafafa}
|
||||
</style></head><body>
|
||||
<h2>PGŽ Sport CRM — ${tab.toUpperCase()} <small style="font-weight:400;color:#666">(${new Date().toLocaleString('hr-HR')})</small></h2>
|
||||
<table><thead><tr>${headers.map(h=>'<th>'+h+'</th>').join('')}</tr></thead>
|
||||
<tbody>${rows.map(r=>'<tr>'+r.map(c=>'<td>'+(c==null?'':String(c).replace(/&/g,'&').replace(/</g,'<'))+'</td>').join('')+'</tr>').join('')}</tbody></table>
|
||||
</body></html>`;
|
||||
w.document.write(html); w.document.close();
|
||||
setTimeout(() => { try { w.focus(); w.print(); } catch(e){} }, 350);
|
||||
toast('PDF print dialog otvoren');
|
||||
}
|
||||
}
|
||||
|
||||
// ────── /me ──────
|
||||
@@ -734,21 +899,43 @@ async function loadAccounts() {
|
||||
if (t) qs.set('type', t);
|
||||
try {
|
||||
const data = await api('/accounts?'+qs.toString());
|
||||
const items = data.items||[];
|
||||
// Card grid (primary)
|
||||
const grid = document.getElementById('acc-cards');
|
||||
if (grid) {
|
||||
grid.innerHTML = items.map(a => `
|
||||
<div class="ccard" onclick="editAccount(${a.id})">
|
||||
<div class="ccard-actions">
|
||||
<button class="btn sm danger" onclick="event.stopPropagation();delAccount(${a.id},'${esc(a.naziv).replace(/'/g,"\\'")}')">×</button>
|
||||
</div>
|
||||
<div class="ccard-h">${esc(a.naziv)}</div>
|
||||
<div class="ccard-sub"><span class="chip ${a.type}">${esc(a.type)}</span> ${esc(a.grad||'')}</div>
|
||||
<div class="ccard-row"><span>OIB</span><strong>${esc(a.oib||'—')}</strong></div>
|
||||
<div class="ccard-row"><span>Email</span><strong>${esc(a.email||'—')}</strong></div>
|
||||
<div class="ccard-row"><span>Kontakti / Opps</span><strong>${a.contacts_n||0} / ${a.opps_n||0}</strong></div>
|
||||
<div class="ccard-row"><span>Owner</span><strong>${esc(a.owner_email||'—')}</strong></div>
|
||||
</div>
|
||||
`).join('') || '<div class="empty" style="grid-column:1/-1">Nema accounta — dodajte prvi.</div>';
|
||||
}
|
||||
// Hidden table (compat for legacy code/exports)
|
||||
const tb = document.querySelector('#t-accounts tbody');
|
||||
tb.innerHTML = (data.items||[]).map(a => `
|
||||
<tr onclick="editAccount(${a.id})">
|
||||
<td><strong>${esc(a.naziv)}</strong></td>
|
||||
<td>${esc(a.type)}</td>
|
||||
<td>${esc(a.grad||'—')}</td>
|
||||
<td>${esc(a.oib||'—')}</td>
|
||||
<td>${esc(a.email||'—')}</td>
|
||||
<td>${a.contacts_n||0}</td>
|
||||
<td>${a.opps_n||0}</td>
|
||||
<td>${esc(a.owner_email||'—')}</td>
|
||||
<td><button class="btn sm" onclick="event.stopPropagation();delAccount(${a.id},'${esc(a.naziv).replace(/'/g,"\\'")}')">×</button></td>
|
||||
</tr>
|
||||
`).join('') || '<tr><td colspan="9" class="empty">Nema accounta — dodajte prvi.</td></tr>';
|
||||
document.getElementById('cnt-accounts').textContent = (data.items||[]).length;
|
||||
if (tb) {
|
||||
tb.innerHTML = items.map(a => `
|
||||
<tr onclick="editAccount(${a.id})">
|
||||
<td><strong>${esc(a.naziv)}</strong></td>
|
||||
<td>${esc(a.type)}</td>
|
||||
<td>${esc(a.grad||'—')}</td>
|
||||
<td>${esc(a.oib||'—')}</td>
|
||||
<td>${esc(a.email||'—')}</td>
|
||||
<td>${a.contacts_n||0}</td>
|
||||
<td>${a.opps_n||0}</td>
|
||||
<td>${esc(a.owner_email||'—')}</td>
|
||||
<td><button class="btn sm" onclick="event.stopPropagation();delAccount(${a.id},'${esc(a.naziv).replace(/'/g,"\\'")}')">×</button></td>
|
||||
</tr>
|
||||
`).join('') || '<tr><td colspan="9" class="empty">Nema accounta.</td></tr>';
|
||||
}
|
||||
setExportRows('accounts', items.map(a => [a.naziv, a.type, a.grad||'', a.oib||'', a.email||'', a.telefon||'', a.contacts_n||0, a.opps_n||0, a.owner_email||'']));
|
||||
document.getElementById('cnt-accounts').textContent = items.length;
|
||||
} catch (e) { toast('Accounts err: '+e.message, 'err'); }
|
||||
}
|
||||
|
||||
@@ -823,19 +1010,33 @@ async function loadContacts() {
|
||||
if (aid) qs.set('account_id', aid);
|
||||
try {
|
||||
const data = await api('/contacts?'+qs.toString());
|
||||
const items = data.items||[];
|
||||
const grid = document.getElementById('con-cards');
|
||||
if (grid) {
|
||||
grid.innerHTML = items.map(c => `
|
||||
<div class="ccard" onclick="editContact(${c.id})">
|
||||
<div class="ccard-actions">
|
||||
<button class="btn sm danger" onclick="event.stopPropagation();delContact(${c.id})">×</button>
|
||||
</div>
|
||||
<div class="ccard-h">${esc(c.ime)} ${esc(c.prezime)}</div>
|
||||
<div class="ccard-sub">${esc(c.funkcija||'—')} · ${esc(c.account_naziv||'—')}</div>
|
||||
<div class="ccard-row"><span>Email</span><strong>${esc(c.email||'—')}</strong></div>
|
||||
<div class="ccard-row"><span>Telefon</span><strong>${esc(c.telefon||c.mobitel||'—')}</strong></div>
|
||||
</div>
|
||||
`).join('') || '<div class="empty" style="grid-column:1/-1">Nema kontakata.</div>';
|
||||
}
|
||||
const tb = document.querySelector('#t-contacts tbody');
|
||||
tb.innerHTML = (data.items||[]).map(c => `
|
||||
<tr onclick="editContact(${c.id})">
|
||||
<td><strong>${esc(c.ime)}</strong></td>
|
||||
<td>${esc(c.prezime)}</td>
|
||||
<td>${esc(c.account_naziv||'—')}</td>
|
||||
<td>${esc(c.funkcija||'—')}</td>
|
||||
<td>${esc(c.email||'—')}</td>
|
||||
<td>${esc(c.telefon||c.mobitel||'—')}</td>
|
||||
<td><button class="btn sm" onclick="event.stopPropagation();delContact(${c.id})">×</button></td>
|
||||
</tr>
|
||||
`).join('') || '<tr><td colspan="7" class="empty">Nema kontakata.</td></tr>';
|
||||
document.getElementById('cnt-contacts').textContent = (data.items||[]).length;
|
||||
if (tb) {
|
||||
tb.innerHTML = items.map(c => `
|
||||
<tr onclick="editContact(${c.id})">
|
||||
<td><strong>${esc(c.ime)}</strong></td><td>${esc(c.prezime)}</td>
|
||||
<td>${esc(c.account_naziv||'—')}</td><td>${esc(c.funkcija||'—')}</td>
|
||||
<td>${esc(c.email||'—')}</td><td>${esc(c.telefon||c.mobitel||'—')}</td>
|
||||
<td><button class="btn sm" onclick="event.stopPropagation();delContact(${c.id})">×</button></td>
|
||||
</tr>`).join('');
|
||||
}
|
||||
setExportRows('contacts', items.map(c => [c.ime||'', c.prezime||'', c.account_naziv||'', c.funkcija||'', c.email||'', c.telefon||c.mobitel||'']));
|
||||
document.getElementById('cnt-contacts').textContent = items.length;
|
||||
} catch (e) { toast(e.message, 'err'); }
|
||||
}
|
||||
function contactFormHTML(c={}) {
|
||||
@@ -903,22 +1104,35 @@ async function loadLeads() {
|
||||
if (s) qs.set('status', s);
|
||||
try {
|
||||
const data = await api('/leads?'+qs.toString());
|
||||
const items = data.items||[];
|
||||
const grid = document.getElementById('lead-cards');
|
||||
if (grid) {
|
||||
grid.innerHTML = items.map(l => `
|
||||
<div class="ccard" onclick="editLead(${l.id})">
|
||||
<div class="ccard-actions">
|
||||
${l.status!=='converted' ? `<button class="btn sm gold" onclick="event.stopPropagation();convertLead(${l.id})">→</button>` : ''}
|
||||
<button class="btn sm danger" onclick="event.stopPropagation();delLead(${l.id})">×</button>
|
||||
</div>
|
||||
<div class="ccard-h">${esc(l.ime||'')} ${esc(l.prezime||'')}</div>
|
||||
<div class="ccard-sub"><span class="chip ${l.status}">${l.status}</span> ${esc(l.organizacija||'—')}</div>
|
||||
<div class="ccard-row"><span>Email</span><strong>${esc(l.email||'—')}</strong></div>
|
||||
<div class="ccard-row"><span>Telefon</span><strong>${esc(l.telefon||'—')}</strong></div>
|
||||
<div class="ccard-row"><span>Izvor</span><strong>${esc(l.izvor||'—')}</strong></div>
|
||||
</div>
|
||||
`).join('') || '<div class="empty" style="grid-column:1/-1">Nema leadova.</div>';
|
||||
}
|
||||
const tb = document.querySelector('#t-leads tbody');
|
||||
tb.innerHTML = (data.items||[]).map(l => `
|
||||
<tr onclick="editLead(${l.id})">
|
||||
<td>${esc(l.ime||'—')}</td>
|
||||
<td>${esc(l.prezime||'—')}</td>
|
||||
<td>${esc(l.organizacija||'—')}</td>
|
||||
<td>${esc(l.email||'—')}</td>
|
||||
<td>${esc(l.izvor||'—')}</td>
|
||||
<td><span class="chip ${l.status}">${l.status}</span></td>
|
||||
<td>
|
||||
${l.status!=='converted' ? `<button class="btn sm gold" onclick="event.stopPropagation();convertLead(${l.id})">→ Konvertiraj</button>` : ''}
|
||||
<button class="btn sm" onclick="event.stopPropagation();delLead(${l.id})">×</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('') || '<tr><td colspan="7" class="empty">Nema leadova.</td></tr>';
|
||||
document.getElementById('cnt-leads').textContent = (data.items||[]).length;
|
||||
if (tb) {
|
||||
tb.innerHTML = items.map(l => `
|
||||
<tr onclick="editLead(${l.id})">
|
||||
<td>${esc(l.ime||'—')}</td><td>${esc(l.prezime||'—')}</td>
|
||||
<td>${esc(l.organizacija||'—')}</td><td>${esc(l.email||'—')}</td>
|
||||
<td>${esc(l.izvor||'—')}</td><td><span class="chip ${l.status}">${l.status}</span></td>
|
||||
<td><button class="btn sm" onclick="event.stopPropagation();delLead(${l.id})">×</button></td>
|
||||
</tr>`).join('');
|
||||
}
|
||||
setExportRows('leads', items.map(l => [l.ime||'', l.prezime||'', l.organizacija||'', l.email||'', l.telefon||'', l.izvor||'', l.status||'']));
|
||||
document.getElementById('cnt-leads').textContent = items.length;
|
||||
} catch (e) { toast(e.message, 'err'); }
|
||||
}
|
||||
function leadFormHTML(l={}) {
|
||||
@@ -1029,20 +1243,36 @@ async function loadOpps() {
|
||||
if (s) qs.set('stage', s);
|
||||
try {
|
||||
const data = await api('/opportunities?'+qs.toString());
|
||||
const items = data.items||[];
|
||||
const grid = document.getElementById('opp-cards');
|
||||
if (grid) {
|
||||
grid.innerHTML = items.map(o => `
|
||||
<div class="ccard" onclick="editOpp(${o.id})">
|
||||
<div class="ccard-actions">
|
||||
<button class="btn sm danger" onclick="event.stopPropagation();delOpp(${o.id})">×</button>
|
||||
</div>
|
||||
<div class="ccard-h">${esc(o.naziv)}</div>
|
||||
<div class="ccard-sub">${esc(o.account_naziv||'—')} · <span class="chip">${esc(o.stage)}</span></div>
|
||||
<div class="ccard-row"><span>Iznos</span><strong style="color:var(--pgz-gold)">${fmtEur(o.amount_eur)}</strong></div>
|
||||
<div class="ccard-row"><span>Vjerojatnost</span><strong>${o.probability||0}%</strong></div>
|
||||
<div class="ccard-row"><span>Close</span><strong>${fmtDate(o.close_date)}</strong></div>
|
||||
<div class="ccard-row"><span>Tip</span><strong>${esc(o.type||'—')}</strong></div>
|
||||
</div>
|
||||
`).join('') || '<div class="empty" style="grid-column:1/-1">Nema prilika.</div>';
|
||||
}
|
||||
const tb = document.querySelector('#t-opps tbody');
|
||||
tb.innerHTML = (data.items||[]).map(o => `
|
||||
<tr onclick="editOpp(${o.id})">
|
||||
<td><strong>${esc(o.naziv)}</strong></td>
|
||||
<td>${esc(o.account_naziv||'—')}</td>
|
||||
<td>${esc(o.type||'—')}</td>
|
||||
<td><span class="chip">${esc(o.stage)}</span></td>
|
||||
<td>${fmtEur(o.amount_eur)}</td>
|
||||
<td>${o.probability||0}%</td>
|
||||
<td>${fmtDate(o.close_date)}</td>
|
||||
<td><button class="btn sm" onclick="event.stopPropagation();delOpp(${o.id})">×</button></td>
|
||||
</tr>
|
||||
`).join('') || '<tr><td colspan="8" class="empty">Nema prilika.</td></tr>';
|
||||
document.getElementById('cnt-opps').textContent = (data.items||[]).length;
|
||||
if (tb) {
|
||||
tb.innerHTML = items.map(o => `
|
||||
<tr onclick="editOpp(${o.id})">
|
||||
<td><strong>${esc(o.naziv)}</strong></td><td>${esc(o.account_naziv||'—')}</td>
|
||||
<td>${esc(o.type||'—')}</td><td><span class="chip">${esc(o.stage)}</span></td>
|
||||
<td>${fmtEur(o.amount_eur)}</td><td>${o.probability||0}%</td>
|
||||
<td>${fmtDate(o.close_date)}</td>
|
||||
<td><button class="btn sm" onclick="event.stopPropagation();delOpp(${o.id})">×</button></td>
|
||||
</tr>`).join('');
|
||||
}
|
||||
setExportRows('opps', items.map(o => [o.naziv||'', o.account_naziv||'', o.type||'', o.stage||'', o.amount_eur||0, o.probability||0, fmtDate(o.close_date)]));
|
||||
document.getElementById('cnt-opps').textContent = items.length;
|
||||
} catch (e) { toast(e.message, 'err'); }
|
||||
}
|
||||
function oppFormHTML(o={}) {
|
||||
@@ -1131,6 +1361,7 @@ async function loadActivities() {
|
||||
</td>
|
||||
</tr>
|
||||
`).join('') || '<tr><td colspan="7" class="empty">Nema aktivnosti.</td></tr>';
|
||||
setExportRows('activities', (data.items||[]).map(a => [a.type||'', a.subject||'', a.account_naziv||'', a.contact_naziv||'', fmtDT(a.due_at), a.completed_at?'done':'open']));
|
||||
document.getElementById('cnt-activities').textContent = (data.items||[]).length;
|
||||
} catch (e) { toast(e.message, 'err'); }
|
||||
}
|
||||
@@ -1217,6 +1448,7 @@ async function loadCases() {
|
||||
<td><button class="btn sm" onclick="event.stopPropagation();delCase(${c.id})">×</button></td>
|
||||
</tr>
|
||||
`).join('') || '<tr><td colspan="6" class="empty">Nema caseova.</td></tr>';
|
||||
setExportRows('cases', (data.items||[]).map(c => [c.subject||'', c.account_naziv||'', c.status||'', c.priority||'', fmtDT(c.created_at)]));
|
||||
document.getElementById('cnt-cases').textContent = (data.items||[]).length;
|
||||
} catch (e) { toast(e.message, 'err'); }
|
||||
}
|
||||
@@ -1323,6 +1555,7 @@ async function loadClanarine() {
|
||||
<td>${isAdminUser() ? `<button class="btn sm" onclick="event.stopPropagation();delClanarina(${c.id})">×</button>` : ''}</td>
|
||||
</tr>
|
||||
`).join('') || '<tr><td colspan="9" class="empty">Nema članarina za odabrane filtere.</td></tr>';
|
||||
setExportRows('clanarine', (data.items||[]).map(c => [c.clan_naziv||'', c.klub_naziv||'', c.godina||'', c.razdoblje||'', c.iznos_propisan||0, c.iznos_placen||0, fmtDate(c.datum_uplate), c.status||'']));
|
||||
document.getElementById('cnt-clanarine').textContent = data.count ?? (data.items||[]).length;
|
||||
} catch (e) { toast('Članarine err: '+e.message, 'err'); }
|
||||
}
|
||||
@@ -1424,6 +1657,7 @@ async function loadLijecnicki() {
|
||||
<td>${isAdminUser() ? `<button class="btn sm" onclick="event.stopPropagation();delLijecnicki(${l.id})">×</button>` : ''}</td>
|
||||
</tr>`;
|
||||
}).join('') || '<tr><td colspan="9" class="empty">Nema liječničkih pregleda.</td></tr>';
|
||||
setExportRows('lijecnicki', (data.items||[]).map(l => [l.clan_naziv||'', l.klub_naziv||'', fmtDate(l.datum_pregleda), l.vrsta_pregleda||'', fmtDate(l.vrijedi_do), l.lijecnik||'', l.spreman_za_natjecanje?'DA':'NE', l.placeno?'DA':'NE']));
|
||||
document.getElementById('cnt-lijecnicki').textContent = data.count ?? (data.items||[]).length;
|
||||
} catch (e) { toast('Liječnički err: '+e.message, 'err'); }
|
||||
}
|
||||
@@ -1642,6 +1876,7 @@ async function loadObrasciSubmissions() {
|
||||
` : ''}</td>
|
||||
</tr>
|
||||
`).join('') || '<tr><td colspan="8" class="empty">Nema podnesenih obrazaca.</td></tr>';
|
||||
setExportRows('obrasci', (data.items||[]).map(s => [s.id, s.template_naziv||s.template_code||'', s.klub_naziv||'', s.clan_naziv||'', s.status||'', fmtDT(s.submitted_at), fmtDT(s.approved_at)]));
|
||||
} catch (e) { toast('Submissions err: '+e.message, 'err'); }
|
||||
}
|
||||
|
||||
@@ -1712,6 +1947,120 @@ async function subStatus(id, status) {
|
||||
} catch (e) { toast('Greška: '+e.message, 'err'); }
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
// RUSH-4 — E-mail templates (CRM v2 GUI redesign, 2026-05-05)
|
||||
// dradulic@outlook.com / damir@rinet.one
|
||||
// Endpoint: /api/v2/crm/email-templates (CRUD)
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
let EMAIL_TPLS = [];
|
||||
|
||||
async function loadEmailTpls() {
|
||||
const q = (document.getElementById('etpl-q')?.value || '').trim().toLowerCase();
|
||||
const cat = document.getElementById('etpl-cat')?.value || '';
|
||||
const qs = new URLSearchParams();
|
||||
qs.set('active_only', 'false');
|
||||
if (cat) qs.set('kategorija', cat);
|
||||
try {
|
||||
const data = await api('/email-templates?'+qs.toString());
|
||||
EMAIL_TPLS = (data.items||[]).filter(t => {
|
||||
if (!q) return true;
|
||||
return (t.code||'').toLowerCase().includes(q)
|
||||
|| (t.naziv||'').toLowerCase().includes(q);
|
||||
});
|
||||
const grid = document.getElementById('etpl-grid');
|
||||
grid.innerHTML = EMAIL_TPLS.map(t => `
|
||||
<div class="tcard" onclick="editEmailTpl(${t.id})">
|
||||
<div class="tcard-code">${esc(t.code)}</div>
|
||||
<div class="tcard-naziv">${esc(t.naziv)} ${t.active===false?'<span class="chip closed" style="margin-left:6px">neaktivan</span>':''}</div>
|
||||
<div class="tcard-cat">${esc(t.kategorija||'—')}</div>
|
||||
<div class="tcard-snip"><strong>Subject:</strong> ${esc((t.subject_tpl||'').slice(0,90))}${(t.subject_tpl||'').length>90?'…':''}<br>${esc((t.body_tpl||'').replace(/\s+/g,' ').slice(0,140))}${(t.body_tpl||'').length>140?'…':''}</div>
|
||||
</div>
|
||||
`).join('') || '<div class="empty" style="grid-column:1/-1">Nema predložaka.</div>';
|
||||
setExportRows('emailtpl', EMAIL_TPLS.map(t => [t.code||'', t.naziv||'', t.kategorija||'', t.subject_tpl||'', t.active?'true':'false']));
|
||||
document.getElementById('cnt-emailtpl').textContent = EMAIL_TPLS.length;
|
||||
} catch (e) { toast('Email tpl err: '+e.message, 'err'); }
|
||||
}
|
||||
|
||||
function emailTplFormHTML(t={}) {
|
||||
return `
|
||||
<div class="fld-row">
|
||||
<div class="fld"><label>Code*</label><input id="ef-code" value="${esc(t.code||'')}" placeholder="npr. clanarina_opomena"></div>
|
||||
<div class="fld"><label>Kategorija</label>
|
||||
<select id="ef-cat">
|
||||
<option value="">—</option>
|
||||
${['clanarine','lijecnicki','obrasci','opci'].map(c=>`<option value="${c}" ${t.kategorija===c?'selected':''}>${c}</option>`).join('')}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fld"><label>Naziv*</label><input id="ef-naziv" value="${esc(t.naziv||'')}"></div>
|
||||
<div class="fld"><label>Subject (predmet)*</label><input id="ef-subj" value="${esc(t.subject_tpl||'')}" placeholder="npr. Opomena za {{godina}}"></div>
|
||||
<div class="fld"><label>Body (HTML / tekst)* — placeholderi {{...}}</label>
|
||||
<textarea id="ef-body" style="min-height:160px;font-family:var(--mono)">${esc(t.body_tpl||'')}</textarea>
|
||||
</div>
|
||||
<div class="fld"><label>Variables (JSON, npr. {"naziv_kluba":"string"})</label>
|
||||
<textarea id="ef-vars" style="min-height:60px;font-family:var(--mono)">${esc(t.variables ? JSON.stringify(t.variables, null, 2) : '')}</textarea>
|
||||
</div>
|
||||
<div class="fld"><label><input id="ef-act" type="checkbox" ${t.active===false?'':'checked'}> Aktivan</label></div>
|
||||
`;
|
||||
}
|
||||
|
||||
function readEmailTplForm() {
|
||||
let vars = null;
|
||||
const raw = document.getElementById('ef-vars').value.trim();
|
||||
if (raw) {
|
||||
try { vars = JSON.parse(raw); }
|
||||
catch(e) { toast('Variables JSON nije validan', 'err'); throw e; }
|
||||
}
|
||||
return {
|
||||
code: document.getElementById('ef-code').value.trim(),
|
||||
naziv: document.getElementById('ef-naziv').value.trim(),
|
||||
kategorija: document.getElementById('ef-cat').value || null,
|
||||
subject_tpl: document.getElementById('ef-subj').value,
|
||||
body_tpl: document.getElementById('ef-body').value,
|
||||
variables: vars,
|
||||
active: document.getElementById('ef-act').checked,
|
||||
};
|
||||
}
|
||||
|
||||
function openEmailTplModal(t) {
|
||||
const isEdit = !!(t && t.id);
|
||||
showModal(isEdit?'Uredi predložak':'Novi e-mail predložak',
|
||||
emailTplFormHTML(t||{active:true}),
|
||||
async () => {
|
||||
let body; try { body = readEmailTplForm(); } catch(e) { return; }
|
||||
if (!body.code || !body.naziv || !body.subject_tpl || !body.body_tpl) {
|
||||
toast('Code, naziv, subject i body su obavezni', 'err'); return;
|
||||
}
|
||||
try {
|
||||
if (isEdit) await api('/email-templates/'+t.id, {method:'PUT', body:JSON.stringify(body)});
|
||||
else await api('/email-templates', {method:'POST', body:JSON.stringify(body)});
|
||||
toast('Spremljeno'); closeModal(); loadEmailTpls();
|
||||
} catch (e) { toast('Greška: '+e.message, 'err'); }
|
||||
});
|
||||
}
|
||||
|
||||
async function editEmailTpl(id) {
|
||||
try { const t = await api('/email-templates/'+id);
|
||||
// Add delete button to footer
|
||||
openEmailTplModal(t);
|
||||
setTimeout(() => {
|
||||
const foot = document.getElementById('m-foot');
|
||||
if (foot && !foot.querySelector('.btn.danger')) {
|
||||
const del = document.createElement('button');
|
||||
del.className = 'btn danger'; del.textContent = 'Obriši';
|
||||
del.onclick = () => delEmailTpl(id, t.naziv);
|
||||
foot.insertBefore(del, foot.firstChild);
|
||||
}
|
||||
}, 0);
|
||||
} catch (e) { toast(e.message, 'err'); }
|
||||
}
|
||||
|
||||
async function delEmailTpl(id, naziv) {
|
||||
if (!confirm('Obrisati predložak "'+naziv+'"?')) return;
|
||||
try { await api('/email-templates/'+id, {method:'DELETE'}); toast('Obrisano'); closeModal(); loadEmailTpls(); }
|
||||
catch (e) { toast('Greška: '+e.message, 'err'); }
|
||||
}
|
||||
|
||||
// ────── Modal helpers ──────
|
||||
function showModal(title, bodyHTML, onSave) {
|
||||
document.getElementById('m-title').textContent = title;
|
||||
@@ -1731,7 +2080,26 @@ document.getElementById('modal').addEventListener('click', e => {
|
||||
// ────── Init ──────
|
||||
loadMe();
|
||||
ensureMe();
|
||||
loadPipeline();
|
||||
loadPipeline(); // populates KPI counters + kanban (kanban only visible in Opportunities tab now)
|
||||
loadClanarine(); // default active tab
|
||||
|
||||
// ── Universal Export ▾ — server-side fallback for tabs that load full
|
||||
// record sets from REST (lijecnicki/obrasci). The existing exportTab()
|
||||
// flow above keeps working for client-side cached tabs (accounts,
|
||||
// contacts, leads, opps). attachExportDropdown is a no-op when
|
||||
// export_dropdown.js fails to load.
|
||||
document.addEventListener('DOMContentLoaded', function(){
|
||||
if (!window.attachExportDropdown) return;
|
||||
const lij = document.getElementById('lij-srv-export-btn');
|
||||
if (lij) window.attachExportDropdown(lij, function(){
|
||||
const klub = document.getElementById('lij-klub'); const clan = document.getElementById('lij-clan');
|
||||
const qp = new URLSearchParams(); qp.set('limit','2000');
|
||||
if (klub && klub.value) qp.set('klub_id', klub.value);
|
||||
if (clan && clan.value) qp.set('clan_id', clan.value);
|
||||
return '/sport/api/v2/lijecnicki?'+qp.toString();
|
||||
}, 'lijecnicki');
|
||||
});
|
||||
</script>
|
||||
<script src="/static/js/export_dropdown.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user