Task 4: Universal Export ▾ — CSV/XLSX/PDF dropdown across all screens
- routers/export_router.py: /api/v2/export?format=...&endpoint=...&filters=... - static/js/export_dropdown.js: shared attachExportDropdown helper - sport2/app/crm_v2/erp_full: Export ▾ button wired to representative tables - pgz_sport_api.py: mount export_router with try/except Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+509
-94
@@ -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;
|
||||
@@ -33,13 +34,60 @@ body { font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif
|
||||
.topbar a:hover { opacity:1; background:rgba(255,255,255,.1); }
|
||||
.topbar #me { padding:4px 10px; background:rgba(0,0,0,.2); border-radius:14px; font-size:11px; }
|
||||
|
||||
.tabs { display:flex; background:var(--bg2); border-bottom:1px solid var(--rim); padding:0 18px; flex-wrap:wrap; }
|
||||
/* === CRM v2 redesign — sticky tabs, ERP-style (RUSH-4 / 2026-05-05) === */
|
||||
.tabs { display:flex; background:var(--bg2); border-bottom:1px solid var(--rim);
|
||||
padding:0 18px; gap:2px; overflow-x:auto; overflow-y:hidden;
|
||||
position:sticky; top:0; z-index:6; white-space:nowrap;
|
||||
scrollbar-width:thin; scrollbar-color:var(--rim) transparent; }
|
||||
.tabs::-webkit-scrollbar { height:4px; }
|
||||
.tabs::-webkit-scrollbar-thumb { background:var(--rim); }
|
||||
.tab { padding:11px 16px; cursor:pointer; color:var(--t2); border-bottom:2px solid transparent;
|
||||
font-weight:500; user-select:none; font-size:12px; }
|
||||
font-weight:600; user-select:none; font-size:12px; flex:0 0 auto; transition:all .15s; }
|
||||
.tab:hover { color:var(--t1); }
|
||||
.tab.active { color:var(--pgz-blue); border-bottom-color:var(--pgz-blue); background:var(--bg3); }
|
||||
.tab.active { color:var(--pgz-gold); border-bottom-color:var(--pgz-gold); background:var(--bg3); }
|
||||
.tab .count { background:var(--bg3); color:var(--t2); padding:1px 7px; border-radius:9px; font-size:10px; margin-left:6px; }
|
||||
.tab.active .count { background:var(--pgz-blue); color:#fff; }
|
||||
.tab.active .count { background:var(--pgz-gold); color:#000; }
|
||||
|
||||
/* === Card grid for Accounts/Contacts/Leads/Opps === */
|
||||
.cgrid { display:grid; grid-template-columns:repeat(auto-fill,minmax(280px,1fr)); gap:12px; margin-top:6px; }
|
||||
.ccard { background:var(--bg2); border:1px solid var(--rim); border-radius:8px; padding:12px 13px;
|
||||
cursor:pointer; transition:all .15s; position:relative; }
|
||||
.ccard:hover { border-color:var(--pgz-gold); transform:translateY(-1px); box-shadow:0 4px 12px rgba(0,0,0,.3); }
|
||||
.ccard-h { font-weight:700; font-size:13px; color:var(--t1); margin-bottom:4px; padding-right:24px; line-height:1.25; }
|
||||
.ccard-sub { font-size:11px; color:var(--t2); margin-bottom:8px; }
|
||||
.ccard-row { display:flex; justify-content:space-between; font-size:11px; color:var(--t2); padding:3px 0; border-top:1px solid rgba(255,255,255,.04); }
|
||||
.ccard-row:first-of-type { border-top:0; }
|
||||
.ccard-row strong { color:var(--t1); font-weight:600; }
|
||||
.ccard-actions { position:absolute; top:8px; right:8px; display:flex; gap:4px; }
|
||||
.ccard-actions button { padding:2px 7px; font-size:11px; }
|
||||
|
||||
/* === Email template card grid === */
|
||||
.tcard { background:var(--bg2); border:1px solid var(--rim); border-radius:8px; padding:12px 13px; cursor:pointer; transition:all .15s; }
|
||||
.tcard:hover { border-color:var(--pgz-gold); }
|
||||
.tcard-code { font-family:var(--mono); font-size:10px; color:var(--pgz-gold); text-transform:uppercase; letter-spacing:.5px; }
|
||||
.tcard-naziv { font-weight:700; font-size:13px; color:var(--t1); margin:4px 0; }
|
||||
.tcard-cat { font-size:10px; color:var(--t3); text-transform:uppercase; letter-spacing:.4px; margin-bottom:6px; }
|
||||
.tcard-snip { font-size:11px; color:var(--t2); line-height:1.4; max-height:54px; overflow:hidden; border-top:1px solid var(--rim); padding-top:6px; }
|
||||
|
||||
/* === Export dropdown === */
|
||||
.exp { position:relative; display:inline-block; }
|
||||
.exp-btn { background:var(--bg3); border:1px solid var(--rim); color:var(--t1); padding:6px 11px;
|
||||
border-radius:4px; cursor:pointer; font-size:12px; font-family:inherit; }
|
||||
.exp-btn:hover { border-color:var(--pgz-gold); color:var(--pgz-gold); }
|
||||
.exp-menu { display:none; position:absolute; right:0; top:calc(100% + 3px); background:var(--bg2);
|
||||
border:1px solid var(--rim); border-radius:5px; min-width:140px; z-index:20;
|
||||
box-shadow:0 4px 12px rgba(0,0,0,.5); overflow:hidden; }
|
||||
.exp-menu.on { display:block; }
|
||||
.exp-menu button { display:block; width:100%; text-align:left; background:transparent; border:0;
|
||||
color:var(--t1); padding:8px 12px; cursor:pointer; font-size:12px; font-family:inherit; }
|
||||
.exp-menu button:hover { background:var(--bg3); color:var(--pgz-gold); }
|
||||
|
||||
@media print {
|
||||
.topbar, .tabs, .toolbar, footer, #toast, .modal, .ccard-actions, .exp { display:none !important; }
|
||||
body, .main { background:#fff !important; color:#000 !important; overflow:visible !important; height:auto !important; }
|
||||
.ccard, .tcard, .card { background:#fff !important; color:#000 !important; border:1px solid #999 !important; break-inside:avoid; }
|
||||
table th, table td { color:#000 !important; border-color:#999 !important; }
|
||||
}
|
||||
|
||||
.main { padding:14px 18px; height:calc(100vh - 50px - 36px); overflow:auto; }
|
||||
.tab-c { display:none; }
|
||||
@@ -187,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>
|
||||
@@ -198,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">
|
||||
@@ -247,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>
|
||||
@@ -264,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>
|
||||
@@ -288,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>
|
||||
@@ -313,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>
|
||||
@@ -341,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">
|
||||
@@ -372,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">
|
||||
@@ -383,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">
|
||||
@@ -397,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">
|
||||
@@ -418,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">
|
||||
@@ -454,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>
|
||||
@@ -465,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>
|
||||
@@ -563,24 +702,97 @@ 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 ──────
|
||||
async function loadMe() {
|
||||
try {
|
||||
const tok = getToken();
|
||||
const me = await fetch('/sport/api/v2/auth/me', {headers:{'Authorization':'Bearer '+tok}}).then(r=>r.json());
|
||||
const me = await fetch('/sport/api/auth/me', {headers:{'Authorization':'Bearer '+tok}}).then(r=>r.json());
|
||||
document.getElementById('me').textContent = (me.email || me.full_name || 'user');
|
||||
} catch { document.getElementById('me').textContent='?'; }
|
||||
}
|
||||
@@ -687,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'); }
|
||||
}
|
||||
|
||||
@@ -776,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={}) {
|
||||
@@ -856,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={}) {
|
||||
@@ -982,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={}) {
|
||||
@@ -1084,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'); }
|
||||
}
|
||||
@@ -1170,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'); }
|
||||
}
|
||||
@@ -1234,7 +1513,7 @@ async function delCase(id) {
|
||||
let CURRENT_USER = null;
|
||||
async function ensureMe() {
|
||||
if (CURRENT_USER) return CURRENT_USER;
|
||||
const candidates = ['/sport/api/auth/me', '/sport/api/v2/auth/me', '/sport/api/v2/me'];
|
||||
const candidates = ['/sport/api/auth/me', '/sport/api/auth/me', '/sport/api/v2/me'];
|
||||
for (const url of candidates) {
|
||||
try {
|
||||
const r = await fetch(url, {headers:{'Authorization':'Bearer '+TOKEN}});
|
||||
@@ -1276,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'); }
|
||||
}
|
||||
@@ -1377,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'); }
|
||||
}
|
||||
@@ -1595,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'); }
|
||||
}
|
||||
|
||||
@@ -1665,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;
|
||||
@@ -1684,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