b72d037141
Bug: crm_v2.html, admin_users.html, ostali pozivali /api/v2/auth/me
koji ne postoji u backendu (postoji /api/auth/me bez v2).
401 redirect na /login?reason=unauthorized iako Damir prijavljen.
Fix:
- Frontend: replace /api/v2/auth/me → /api/auth/me u svim file-ovima
- Backend: dodan defensive alias @app.get('/api/v2/auth/me')
1738 lines
84 KiB
HTML
1738 lines
84 KiB
HTML
<!DOCTYPE html>
|
||
<!--
|
||
PGŽ Sport — CRM v2 (Salesforce-Lite) | v1.0.0 | 05.05.2026
|
||
Author: Damir Radulić <dradulic@outlook.com> / damir@rinet.one
|
||
Lokacija: /opt/pgz-sport/static/crm_v2.html
|
||
Svrha: Pipeline kanban + Accounts/Contacts/Leads/Opportunities/Activities/Cases
|
||
-->
|
||
<html lang="hr">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>PGŽ Sport — CRM v2 (Salesforce-Lite)</title>
|
||
<style>
|
||
:root {
|
||
--pgz-blue:#1a73e8; --pgz-blue2:#1e3a8a; --pgz-gold:#fbbf24;
|
||
--bg:#0f1115; --bg2:#171a21; --bg3:#1f242d;
|
||
--rim:#293040; --t1:#e6e8ef; --t2:#9aa3b6; --t3:#6b748b; --t4:#4b5269;
|
||
--ok:#22c55e; --warn:#f59e0b; --err:#ef4444; --info:#3b82f6;
|
||
--mono:'JetBrains Mono',Menlo,monospace;
|
||
}
|
||
* { box-sizing: border-box; }
|
||
html,body { margin:0; padding:0; }
|
||
body { font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
|
||
background:var(--bg); color:var(--t1); font-size:13px; height:100vh; overflow:hidden; }
|
||
.topbar { height:50px; background:linear-gradient(90deg,var(--pgz-blue2),var(--pgz-blue));
|
||
display:flex; align-items:center; padding:0 18px; gap:14px;
|
||
box-shadow:0 2px 8px rgba(0,0,0,.4); }
|
||
.topbar .logo { font-weight:700; font-size:15px; }
|
||
.topbar .sep { color:rgba(255,255,255,.5); }
|
||
.topbar .title { font-size:13px; opacity:.95; }
|
||
.topbar .right { margin-left:auto; display:flex; gap:10px; align-items:center; font-size:12px; }
|
||
.topbar a { color:#fff; text-decoration:none; opacity:.85; padding:5px 9px; border-radius:4px; }
|
||
.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; }
|
||
|
||
/* === 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:600; user-select:none; font-size:12px; flex:0 0 auto; transition:all .15s; }
|
||
.tab:hover { color:var(--t1); }
|
||
.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-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; }
|
||
.tab-c.on { display:block; }
|
||
|
||
.kpi-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(170px,1fr)); gap:10px; margin-bottom:14px; }
|
||
.kpi { background:var(--bg2); border:1px solid var(--rim); padding:11px 13px; border-radius:6px; }
|
||
.kpi.g { border-left:3px solid var(--ok); }
|
||
.kpi.r { border-left:3px solid var(--err); }
|
||
.kpi.a { border-left:3px solid var(--warn); }
|
||
.kpi.b { border-left:3px solid var(--pgz-blue); }
|
||
.kpi.gold { border-left:3px solid var(--pgz-gold); }
|
||
.kpi-l { font-size:10px; color:var(--t3); text-transform:uppercase; letter-spacing:.5px; }
|
||
.kpi-v { font-size:20px; font-weight:700; margin-top:3px; }
|
||
.kpi-s { font-size:10px; color:var(--t3); margin-top:2px; }
|
||
|
||
.toolbar { display:flex; gap:9px; flex-wrap:wrap; margin-bottom:12px; align-items:center; }
|
||
.toolbar input, .toolbar select {
|
||
background:var(--bg2); border:1px solid var(--rim); color:var(--t1);
|
||
padding:6px 10px; border-radius:4px; font-size:12px; min-width:130px; font-family:inherit;
|
||
}
|
||
.toolbar input:focus, .toolbar select:focus { outline:none; border-color:var(--pgz-blue); }
|
||
.toolbar .grow { flex:1; }
|
||
|
||
.btn { background:var(--bg3); color:var(--t1); border:1px solid var(--rim);
|
||
padding:6px 11px; border-radius:4px; cursor:pointer; font-size:12px; font-family:inherit; }
|
||
.btn:hover { background:var(--bg2); border-color:var(--pgz-blue); }
|
||
.btn.primary { background:linear-gradient(135deg,var(--pgz-blue),var(--pgz-blue2));
|
||
border-color:var(--pgz-blue); color:#fff; }
|
||
.btn.primary:hover { filter:brightness(1.1); }
|
||
.btn.danger { color:var(--err); border-color:var(--err); }
|
||
.btn.sm { padding:3px 7px; font-size:11px; }
|
||
.btn.gold { background:var(--pgz-gold); color:#000; border-color:var(--pgz-gold); }
|
||
|
||
.card { background:var(--bg2); border:1px solid var(--rim); border-radius:6px; margin-bottom:12px; overflow:hidden; }
|
||
.card-h { padding:10px 14px; border-bottom:1px solid var(--rim); display:flex;
|
||
align-items:center; justify-content:space-between; background:var(--bg3); }
|
||
.card-t { font-weight:600; font-size:12px; }
|
||
.card-b { padding:11px 14px; }
|
||
|
||
table { width:100%; border-collapse:collapse; font-size:12px; }
|
||
table th, table td { padding:8px 11px; text-align:left; border-bottom:1px solid var(--rim); }
|
||
table th { background:var(--bg3); color:var(--t2); font-weight:600; font-size:10px;
|
||
text-transform:uppercase; letter-spacing:.4px; cursor:default; }
|
||
table tr:hover td { background:rgba(26,115,232,.06); cursor:pointer; }
|
||
table tr.sel td { background:rgba(26,115,232,.15); }
|
||
|
||
.chip { display:inline-block; padding:2px 8px; border-radius:10px; font-size:10px;
|
||
font-weight:600; text-transform:uppercase; letter-spacing:.3px; }
|
||
.chip.new { background:#1e293b; color:#94a3b8; }
|
||
.chip.contacted { background:#1e3a5f; color:#60a5fa; }
|
||
.chip.qualified { background:#3a2e1a; color:#fbbf24; }
|
||
.chip.lost { background:#3a1e1e; color:#f87171; }
|
||
.chip.converted { background:#1a3a2a; color:#4ade80; }
|
||
.chip.open { background:#1e293b; color:#94a3b8; }
|
||
.chip.in_progress { background:#1e3a5f; color:#60a5fa; }
|
||
.chip.waiting { background:#3a2e1a; color:#fbbf24; }
|
||
.chip.resolved { background:#1a3a2a; color:#4ade80; }
|
||
.chip.closed { background:#1f1f1f; color:#71717a; }
|
||
.chip.urgent { background:#3a1e1e; color:#f87171; }
|
||
.chip.high { background:#3a2e1a; color:#fbbf24; }
|
||
.chip.normal { background:#1e293b; color:#94a3b8; }
|
||
.chip.low { background:#1f1f1f; color:#71717a; }
|
||
.chip.nepodmireno { background:#3a1e1e; color:#f87171; }
|
||
.chip.djelomicno { background:#3a2e1a; color:#fbbf24; }
|
||
.chip.podmireno { background:#1a3a2a; color:#4ade80; }
|
||
.chip.storno { background:#1f1f1f; color:#71717a; }
|
||
.chip.draft { background:#1e293b; color:#94a3b8; }
|
||
.chip.submitted { background:#1e3a5f; color:#60a5fa; }
|
||
.chip.approved { background:#1a3a2a; color:#4ade80; }
|
||
.chip.rejected { background:#3a1e1e; color:#f87171; }
|
||
.chip.spreman { background:#1a3a2a; color:#4ade80; }
|
||
.chip.nije-spreman{ background:#3a1e1e; color:#f87171; }
|
||
.tpl-row { padding:9px 12px; border-bottom:1px solid var(--rim); cursor:pointer; }
|
||
.tpl-row:hover { background:var(--bg3); }
|
||
.tpl-row.sel { background:rgba(26,115,232,.18); border-left:3px solid var(--pgz-blue); padding-left:9px; }
|
||
.tpl-row .tpl-n { font-weight:600; font-size:12px; color:var(--t1); }
|
||
.tpl-row .tpl-c { font-size:10px; color:var(--t3); margin-top:2px; text-transform:uppercase; letter-spacing:.4px; }
|
||
|
||
/* Kanban */
|
||
.kanban { display:grid; grid-template-columns:repeat(6,1fr); gap:10px; min-height:60vh; }
|
||
.kcol { background:var(--bg2); border:1px solid var(--rim); border-radius:6px;
|
||
padding:8px; display:flex; flex-direction:column; min-height:200px; }
|
||
.kcol.drag-over { border-color:var(--pgz-blue); background:rgba(26,115,232,.08); }
|
||
.kcol-h { display:flex; justify-content:space-between; align-items:center;
|
||
padding:5px 4px 9px; border-bottom:1px solid var(--rim); margin-bottom:8px; }
|
||
.kcol-t { font-size:10px; text-transform:uppercase; letter-spacing:.5px; color:var(--t2); font-weight:700; }
|
||
.kcol-s { font-size:10px; color:var(--t3); font-family:var(--mono); }
|
||
.kcol.prospecting .kcol-t { color:#94a3b8; }
|
||
.kcol.qualification .kcol-t { color:#60a5fa; }
|
||
.kcol.proposal .kcol-t { color:#fbbf24; }
|
||
.kcol.negotiation .kcol-t { color:#f97316; }
|
||
.kcol.closed_won .kcol-t { color:#4ade80; }
|
||
.kcol.closed_lost .kcol-t { color:#f87171; }
|
||
.kcards { flex:1; overflow-y:auto; display:flex; flex-direction:column; gap:6px; }
|
||
.kcard { background:var(--bg3); border:1px solid var(--rim); border-radius:5px;
|
||
padding:8px 9px; cursor:grab; font-size:11px; }
|
||
.kcard:hover { border-color:var(--pgz-blue); }
|
||
.kcard.dragging { opacity:.45; }
|
||
.kcard-t { font-weight:600; color:var(--t1); margin-bottom:3px; }
|
||
.kcard-a { color:var(--t2); font-size:10px; margin-bottom:5px; }
|
||
.kcard-r { display:flex; justify-content:space-between; font-size:10px; color:var(--t3); font-family:var(--mono); }
|
||
.kcard-eur { color:var(--pgz-gold); font-weight:700; }
|
||
|
||
/* Modal */
|
||
.modal { display:none; position:fixed; inset:0; background:rgba(0,0,0,.65); z-index:50;
|
||
align-items:flex-start; justify-content:center; padding-top:5vh; overflow:auto; }
|
||
.modal.on { display:flex; }
|
||
.mbox { background:var(--bg2); border:1px solid var(--rim); border-radius:8px;
|
||
width:560px; max-width:92vw; max-height:90vh; overflow:auto; }
|
||
.mbox.wide { width:780px; }
|
||
.mh { padding:12px 16px; border-bottom:1px solid var(--rim); display:flex;
|
||
justify-content:space-between; align-items:center; background:var(--bg3); }
|
||
.mh h3 { margin:0; font-size:14px; }
|
||
.mh .x { background:none; border:none; color:var(--t2); font-size:18px; cursor:pointer; }
|
||
.mh .x:hover { color:var(--t1); }
|
||
.mb { padding:14px 16px; }
|
||
.mf { padding:11px 16px; border-top:1px solid var(--rim); display:flex; gap:8px; justify-content:flex-end; background:var(--bg3); }
|
||
|
||
.fld { margin-bottom:10px; }
|
||
.fld label { display:block; font-size:11px; color:var(--t3); text-transform:uppercase;
|
||
letter-spacing:.4px; margin-bottom:3px; font-weight:600; }
|
||
.fld input, .fld select, .fld textarea {
|
||
width:100%; background:var(--bg); border:1px solid var(--rim); color:var(--t1);
|
||
padding:6px 9px; border-radius:4px; font-size:12px; font-family:inherit;
|
||
}
|
||
.fld textarea { min-height:60px; resize:vertical; }
|
||
.fld input:focus, .fld select:focus, .fld textarea:focus { outline:none; border-color:var(--pgz-blue); }
|
||
.fld-row { display:grid; grid-template-columns:1fr 1fr; gap:10px; }
|
||
|
||
.empty { padding:30px; text-align:center; color:var(--t3); font-size:12px; }
|
||
|
||
#toast { position:fixed; bottom:20px; right:20px; background:var(--bg3); border:1px solid var(--rim);
|
||
padding:10px 14px; border-radius:6px; z-index:99; display:none; font-size:12px; }
|
||
#toast.ok { border-color:var(--ok); }
|
||
#toast.err { border-color:var(--err); }
|
||
|
||
footer { height:36px; background:var(--bg2); border-top:1px solid var(--rim);
|
||
display:flex; align-items:center; padding:0 18px; font-size:10px;
|
||
color:var(--t3); font-family:var(--mono); justify-content:space-between; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div class="topbar">
|
||
<span class="logo">PGŽ SPORT</span>
|
||
<span class="sep">›</span>
|
||
<span class="title">CRM v2 — Salesforce-Lite (Pipeline)</span>
|
||
<div class="right">
|
||
<span id="me">…</span>
|
||
<a href="/sport/platform">Platform</a>
|
||
<a href="/sport/erp">ERP</a>
|
||
<a href="/sport/crm">CRM</a>
|
||
<a href="#" id="logout">Odjava</a>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="tabs">
|
||
<div class="tab active" data-tab="pipeline">Pipeline</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>
|
||
|
||
<!-- ────── ACCOUNTS ────── -->
|
||
<div class="tab-c" id="tc-accounts">
|
||
<div class="toolbar">
|
||
<input id="acc-q" placeholder="Pretraži (naziv/OIB/email/grad)…">
|
||
<select id="acc-type">
|
||
<option value="">— Svi tipovi —</option>
|
||
<option value="klub">klub</option>
|
||
<option value="savez">savez</option>
|
||
<option value="sponzor">sponzor</option>
|
||
<option value="drzava">drzava</option>
|
||
<option value="drugo">drugo</option>
|
||
</select>
|
||
<button class="btn" onclick="loadAccounts()">Pretraži</button>
|
||
<span class="grow"></span>
|
||
<button class="btn primary" onclick="openAccountModal()">+ Novi account</button>
|
||
</div>
|
||
<div class="card"><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>
|
||
</tr></thead><tbody></tbody></table>
|
||
</div></div>
|
||
</div>
|
||
|
||
<!-- ────── CONTACTS ────── -->
|
||
<div class="tab-c" id="tc-contacts">
|
||
<div class="toolbar">
|
||
<input id="con-q" placeholder="Pretraži (ime/email/funkcija)…">
|
||
<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>
|
||
<button class="btn primary" onclick="openContactModal()">+ Novi kontakt</button>
|
||
</div>
|
||
<div class="card"><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>
|
||
</tr></thead><tbody></tbody></table>
|
||
</div></div>
|
||
</div>
|
||
|
||
<!-- ────── LEADS ────── -->
|
||
<div class="tab-c" id="tc-leads">
|
||
<div class="toolbar">
|
||
<input id="lead-q" placeholder="Pretraži (ime/organizacija/email)…">
|
||
<select id="lead-status">
|
||
<option value="">— Svi statusi —</option>
|
||
<option value="new">new</option>
|
||
<option value="contacted">contacted</option>
|
||
<option value="qualified">qualified</option>
|
||
<option value="lost">lost</option>
|
||
<option value="converted">converted</option>
|
||
</select>
|
||
<button class="btn" onclick="loadLeads()">Pretraži</button>
|
||
<span class="grow"></span>
|
||
<button class="btn primary" onclick="openLeadModal()">+ Novi lead</button>
|
||
</div>
|
||
<div class="card"><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>
|
||
</tr></thead><tbody></tbody></table>
|
||
</div></div>
|
||
</div>
|
||
|
||
<!-- ────── OPPORTUNITIES ────── -->
|
||
<div class="tab-c" id="tc-opps">
|
||
<div class="toolbar">
|
||
<input id="opp-q" placeholder="Pretraži (naziv/account)…">
|
||
<select id="opp-stage">
|
||
<option value="">— Sve faze —</option>
|
||
<option value="prospecting">prospecting</option>
|
||
<option value="qualification">qualification</option>
|
||
<option value="proposal">proposal</option>
|
||
<option value="negotiation">negotiation</option>
|
||
<option value="closed_won">closed_won</option>
|
||
<option value="closed_lost">closed_lost</option>
|
||
</select>
|
||
<button class="btn" onclick="loadOpps()">Pretraži</button>
|
||
<span class="grow"></span>
|
||
<button class="btn primary" onclick="openOppModal()">+ Nova prilika</button>
|
||
</div>
|
||
<div class="card"><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>
|
||
</tr></thead><tbody></tbody></table>
|
||
</div></div>
|
||
</div>
|
||
|
||
<!-- ────── ACTIVITIES ────── -->
|
||
<div class="tab-c" id="tc-activities">
|
||
<div class="toolbar">
|
||
<select id="act-type">
|
||
<option value="">— Svi tipovi —</option>
|
||
<option value="call">call</option>
|
||
<option value="meeting">meeting</option>
|
||
<option value="email">email</option>
|
||
<option value="task">task</option>
|
||
<option value="note">note</option>
|
||
</select>
|
||
<select id="act-open">
|
||
<option value="">— Sve —</option>
|
||
<option value="true">Otvorene</option>
|
||
<option value="false">Zatvorene</option>
|
||
</select>
|
||
<button class="btn" onclick="loadActivities()">Filtriraj</button>
|
||
<span class="grow"></span>
|
||
<button class="btn primary" onclick="openActivityModal()">+ Nova aktivnost</button>
|
||
</div>
|
||
<div class="card"><div class="card-b" style="padding:0">
|
||
<table id="t-activities"><thead><tr>
|
||
<th>Tip</th><th>Subject</th><th>Account</th><th>Kontakt</th>
|
||
<th>Due</th><th>Status</th><th></th>
|
||
</tr></thead><tbody></tbody></table>
|
||
</div></div>
|
||
</div>
|
||
|
||
<!-- ────── CASES ────── -->
|
||
<div class="tab-c" id="tc-cases">
|
||
<div class="toolbar">
|
||
<input id="case-q" placeholder="Pretraži…">
|
||
<select id="case-status">
|
||
<option value="">— Svi statusi —</option>
|
||
<option value="open">open</option>
|
||
<option value="in_progress">in_progress</option>
|
||
<option value="waiting">waiting</option>
|
||
<option value="resolved">resolved</option>
|
||
<option value="closed">closed</option>
|
||
</select>
|
||
<select id="case-priority">
|
||
<option value="">— Sve prioriteti —</option>
|
||
<option value="urgent">urgent</option>
|
||
<option value="high">high</option>
|
||
<option value="normal">normal</option>
|
||
<option value="low">low</option>
|
||
</select>
|
||
<button class="btn" onclick="loadCases()">Pretraži</button>
|
||
<span class="grow"></span>
|
||
<button class="btn primary" onclick="openCaseModal()">+ Novi case</button>
|
||
</div>
|
||
<div class="card"><div class="card-b" style="padding:0">
|
||
<table id="t-cases"><thead><tr>
|
||
<th>Subject</th><th>Account</th><th>Status</th>
|
||
<th>Priority</th><th>Stvoren</th><th></th>
|
||
</tr></thead><tbody></tbody></table>
|
||
</div></div>
|
||
</div>
|
||
|
||
<!-- ────── ČLANARINE ────── -->
|
||
<div class="tab-c" 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">
|
||
<input id="cln-godina" type="number" placeholder="Godina" style="max-width:120px">
|
||
<select id="cln-status">
|
||
<option value="">— Svi statusi —</option>
|
||
<option value="nepodmireno">nepodmireno</option>
|
||
<option value="djelomicno">djelomično</option>
|
||
<option value="podmireno">podmireno</option>
|
||
<option value="storno">storno</option>
|
||
</select>
|
||
<button class="btn" onclick="loadClanarine()">Filtriraj</button>
|
||
<span class="grow"></span>
|
||
<button class="btn primary" onclick="openClanarinaModal()">+ Nova članarina</button>
|
||
</div>
|
||
<div class="card"><div class="card-b" style="padding:0">
|
||
<table id="t-clanarine"><thead><tr>
|
||
<th>Član</th><th>Klub</th><th>Godina</th><th>Razdoblje</th>
|
||
<th>Propisan</th><th>Plaćen</th><th>Datum uplate</th><th>Status</th><th></th>
|
||
</tr></thead><tbody></tbody></table>
|
||
</div></div>
|
||
</div>
|
||
|
||
<!-- ────── LIJEČNIČKI ────── -->
|
||
<div class="tab-c" id="tc-lijecnicki">
|
||
<div class="toolbar">
|
||
<input id="lij-klub" type="number" placeholder="Klub ID" style="max-width:120px">
|
||
<input id="lij-clan" type="number" placeholder="Član ID" style="max-width:120px">
|
||
<select id="lij-expiring">
|
||
<option value="">— Svi —</option>
|
||
<option value="true">Ističu (≤30d) ili istekli</option>
|
||
</select>
|
||
<button class="btn" onclick="loadLijecnicki()">Filtriraj</button>
|
||
<span class="grow"></span>
|
||
<button class="btn primary" onclick="openLijecnickiModal()">+ Novi pregled</button>
|
||
</div>
|
||
<div class="card"><div class="card-b" style="padding:0">
|
||
<table id="t-lijecnicki"><thead><tr>
|
||
<th>Član</th><th>Klub</th><th>Datum</th><th>Vrsta</th>
|
||
<th>Vrijedi do</th><th>Liječnik</th><th>Spreman</th><th>Plaćeno</th><th></th>
|
||
</tr></thead><tbody></tbody></table>
|
||
</div></div>
|
||
</div>
|
||
|
||
<!-- ────── OBRASCI ────── -->
|
||
<div class="tab-c" id="tc-obrasci">
|
||
<div style="display:grid; grid-template-columns:300px 1fr; gap:12px; align-items:start;">
|
||
<!-- Templates list (left) -->
|
||
<div class="card">
|
||
<div class="card-h"><div class="card-t">Predlošci</div>
|
||
<button class="btn sm" onclick="loadObrasciTemplates()">↻</button>
|
||
</div>
|
||
<div class="card-b" style="padding:0; max-height:70vh; overflow:auto;" id="obr-tpl-list">
|
||
<div class="empty">Učitavanje…</div>
|
||
</div>
|
||
</div>
|
||
<!-- Submissions / detail (right) -->
|
||
<div>
|
||
<div class="toolbar">
|
||
<strong id="obr-right-title">Podnešeni obrasci</strong>
|
||
<span class="grow"></span>
|
||
<select id="obr-status">
|
||
<option value="">— Svi statusi —</option>
|
||
<option value="draft">draft</option>
|
||
<option value="submitted">submitted</option>
|
||
<option value="approved">approved</option>
|
||
<option value="rejected">rejected</option>
|
||
</select>
|
||
<input id="obr-klub" type="number" placeholder="Klub ID" style="max-width:120px">
|
||
<button class="btn" onclick="loadObrasciSubmissions()">Filtriraj</button>
|
||
</div>
|
||
<div class="card"><div class="card-b" style="padding:0" id="obr-right-body">
|
||
<table id="t-obr-sub"><thead><tr>
|
||
<th>ID</th><th>Predložak</th><th>Klub</th><th>Član</th>
|
||
<th>Status</th><th>Submitted</th><th>Approved</th><th></th>
|
||
</tr></thead><tbody></tbody></table>
|
||
</div></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div><!-- /main -->
|
||
|
||
<footer>
|
||
<span>PGŽ Sport CRM v2 · Salesforce-Lite</span>
|
||
<span id="api-info">api.rinet.one/sport/api/v2/crm/*</span>
|
||
</footer>
|
||
|
||
<!-- Modal shell -->
|
||
<div class="modal" id="modal">
|
||
<div class="mbox" id="mbox">
|
||
<div class="mh"><h3 id="m-title">Modal</h3><button class="x" onclick="closeModal()">×</button></div>
|
||
<div class="mb" id="m-body"></div>
|
||
<div class="mf" id="m-foot">
|
||
<button class="btn" onclick="closeModal()">Odustani</button>
|
||
<button class="btn primary" id="m-save">Spremi</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="toast"></div>
|
||
|
||
<script>
|
||
// ━━━ AUTH: model after app.html (pgz_access primary, fallbacks for legacy keys) ━━━
|
||
function getToken(){
|
||
try {
|
||
return localStorage.getItem('pgz_access')
|
||
|| sessionStorage.getItem('pgz_access')
|
||
|| localStorage.getItem('jwt')
|
||
|| localStorage.getItem('access_token')
|
||
|| localStorage.getItem('token')
|
||
|| '';
|
||
} catch(e){ return ''; }
|
||
}
|
||
let TOKEN = getToken();
|
||
const API = '/sport/api/v2/crm';
|
||
const STAGE_LABEL = {
|
||
prospecting:'Prospecting', qualification:'Qualification', proposal:'Proposal',
|
||
negotiation:'Negotiation', closed_won:'Closed Won', closed_lost:'Closed Lost',
|
||
};
|
||
const STAGES = ['prospecting','qualification','proposal','negotiation','closed_won','closed_lost'];
|
||
|
||
// JWT expiry pre-check + redirect only when truly missing/expired
|
||
(function checkAuth(){
|
||
if(!TOKEN){
|
||
if(!window.__pgz_redirecting && window.__pgz_made_api_call){ window.__pgz_redirecting = true; location.href = '/login?next=' + encodeURIComponent(location.pathname); } else { console.warn('[CRM] no token — login optional'); }
|
||
return;
|
||
}
|
||
try {
|
||
const payload = JSON.parse(atob(TOKEN.split('.')[1]));
|
||
if(payload.exp && payload.exp * 1000 < Date.now()){
|
||
['pgz_access','pgz_refresh','pgz_user','jwt','access_token','token'].forEach(k => {
|
||
try{localStorage.removeItem(k); sessionStorage.removeItem(k);}catch(e){}
|
||
});
|
||
if(!window.__pgz_redirecting){ window.__pgz_redirecting = true; location.href = '/login?reason=expired'; }
|
||
}
|
||
} catch(e){ /* not parseable, let server respond */ }
|
||
})();
|
||
|
||
async function api(path, opts={}) {
|
||
TOKEN = getToken(); // refresh in case of token rotation
|
||
const headers = {'Authorization':'Bearer '+TOKEN, 'Content-Type':'application/json', ...(opts.headers||{})};
|
||
const res = await fetch(API+path, {...opts, headers});
|
||
if (res.status === 401) {
|
||
['pgz_access','pgz_refresh','pgz_user','jwt','access_token','token'].forEach(k => {
|
||
try{localStorage.removeItem(k); sessionStorage.removeItem(k);}catch(e){}
|
||
});
|
||
if(!window.__pgz_redirecting){ window.__pgz_redirecting = true; location.href='/login?reason=unauthorized'; }
|
||
throw new Error('401');
|
||
}
|
||
const txt = await res.text();
|
||
let data; try { data = JSON.parse(txt); } catch { data = txt; }
|
||
if (!res.ok) {
|
||
const msg = (data && data.detail) ? data.detail : ('HTTP '+res.status);
|
||
throw new Error(typeof msg==='string'?msg:JSON.stringify(msg));
|
||
}
|
||
return data;
|
||
}
|
||
|
||
function toast(msg, type='ok') {
|
||
const t = document.getElementById('toast');
|
||
t.textContent = msg; t.className = type;
|
||
t.style.display='block';
|
||
setTimeout(()=>t.style.display='none', 2800);
|
||
}
|
||
|
||
const fmtEur = n => (n==null||n==='') ? '—' : Number(n).toLocaleString('hr-HR',{maximumFractionDigits:0})+' €';
|
||
const fmtDate = s => s ? String(s).slice(0,10) : '—';
|
||
const fmtDT = s => {
|
||
if (!s) return '—';
|
||
const d = new Date(s);
|
||
return isNaN(d) ? String(s).slice(0,16) : d.toLocaleString('hr-HR',{year:'numeric',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'});
|
||
};
|
||
const esc = s => String(s==null?'':s).replace(/[&<>"']/g, c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||
|
||
// ────── Tabs ──────
|
||
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();
|
||
if (name==='accounts') loadAccounts();
|
||
if (name==='contacts') loadContacts();
|
||
if (name==='leads') loadLeads();
|
||
if (name==='opportunities') loadOpps();
|
||
if (name==='activities') loadActivities();
|
||
if (name==='cases') loadCases();
|
||
if (name==='clanarine') loadClanarine();
|
||
if (name==='lijecnicki') loadLijecnicki();
|
||
if (name==='obrasci') { loadObrasciTemplates(); loadObrasciSubmissions(); }
|
||
}
|
||
|
||
// ────── /me ──────
|
||
async function loadMe() {
|
||
try {
|
||
const tok = getToken();
|
||
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='?'; }
|
||
}
|
||
|
||
document.getElementById('logout').addEventListener('click', async (e) => {
|
||
e.preventDefault();
|
||
const tok = getToken();
|
||
try { await fetch('/sport/api/v2/auth/logout', {method:'POST', headers:{'Authorization':'Bearer '+tok}}); } catch {}
|
||
['pgz_access','pgz_refresh','pgz_user','app-role','jwt','access_token','refresh_token','pgz_session_id','token'].forEach(k => {
|
||
try{localStorage.removeItem(k); sessionStorage.removeItem(k);}catch(e){}
|
||
});
|
||
location.href='/login';
|
||
});
|
||
|
||
// ────── Pipeline & Dashboard ──────
|
||
async function loadPipeline() {
|
||
try {
|
||
const [pipe, dash] = await Promise.all([api('/pipeline'), api('/dashboard')]);
|
||
|
||
// KPIs
|
||
document.getElementById('k-opps').textContent = dash.opportunities?.open_opps ?? 0;
|
||
document.getElementById('k-opps-eur').textContent = fmtEur(dash.opportunities?.open_amount);
|
||
document.getElementById('k-weighted').textContent = fmtEur(dash.opportunities?.weighted_amount);
|
||
document.getElementById('k-won').textContent = dash.opportunities?.won_q ?? 0;
|
||
document.getElementById('k-won-eur').textContent = fmtEur(dash.opportunities?.won_q_amount);
|
||
const lbs = (dash.leads_by_status||[]).reduce((m,r)=>{m[r.status]=Number(r.n);return m;},{});
|
||
document.getElementById('k-leads').textContent = (lbs.new||0)+(lbs.contacted||0);
|
||
document.getElementById('k-leads-q').textContent = lbs.qualified||0;
|
||
document.getElementById('k-overdue').textContent = dash.activities?.overdue ?? 0;
|
||
document.getElementById('k-upcoming').textContent = dash.activities?.upcoming ?? 0;
|
||
const cbs = (dash.cases_by_status||[]).reduce((m,r)=>{m[r.status]=Number(r.n);return m;},{});
|
||
document.getElementById('k-cases').textContent = (cbs.open||0)+(cbs.in_progress||0)+(cbs.waiting||0);
|
||
document.getElementById('k-cases-urgent').textContent = 'closed: '+(cbs.closed||0)+' / resolved: '+(cbs.resolved||0);
|
||
|
||
// Counts in tabs
|
||
document.getElementById('cnt-accounts').textContent = dash.accounts_total ?? 0;
|
||
document.getElementById('cnt-contacts').textContent = dash.contacts_total ?? 0;
|
||
document.getElementById('cnt-leads').textContent = (dash.leads_by_status||[]).reduce((s,r)=>s+Number(r.n),0);
|
||
document.getElementById('cnt-opps').textContent = pipe.stages.reduce((s,b)=>s+b.count,0);
|
||
document.getElementById('cnt-activities').textContent = (dash.activities?.overdue||0)+(dash.activities?.upcoming||0)+(dash.activities?.done||0);
|
||
document.getElementById('cnt-cases').textContent = (dash.cases_by_status||[]).reduce((s,r)=>s+Number(r.n),0);
|
||
|
||
// Kanban
|
||
const kb = document.getElementById('kanban');
|
||
kb.innerHTML = pipe.stages.map(b => `
|
||
<div class="kcol ${b.stage}" data-stage="${b.stage}">
|
||
<div class="kcol-h">
|
||
<div class="kcol-t">${STAGE_LABEL[b.stage]}</div>
|
||
<div class="kcol-s">${b.count} · ${fmtEur(b.amount_total)}</div>
|
||
</div>
|
||
<div class="kcards">
|
||
${(b.items||[]).map(o => `
|
||
<div class="kcard" draggable="true" data-id="${o.id}" data-stage="${o.stage}" onclick="editOpp(${o.id})">
|
||
<div class="kcard-t">${esc(o.naziv)}</div>
|
||
<div class="kcard-a">${esc(o.account_naziv||'—')}</div>
|
||
<div class="kcard-r">
|
||
<span class="kcard-eur">${fmtEur(o.amount_eur)}</span>
|
||
<span>${o.probability||0}% · ${fmtDate(o.close_date)}</span>
|
||
</div>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
|
||
bindKanbanDnD();
|
||
} catch (e) { toast('Pipeline err: '+e.message, 'err'); }
|
||
}
|
||
|
||
function bindKanbanDnD() {
|
||
const cards = document.querySelectorAll('.kcard');
|
||
const cols = document.querySelectorAll('.kcol');
|
||
let dragId = null;
|
||
cards.forEach(c => {
|
||
c.addEventListener('dragstart', e => {
|
||
dragId = c.dataset.id;
|
||
c.classList.add('dragging');
|
||
e.dataTransfer.effectAllowed = 'move';
|
||
});
|
||
c.addEventListener('dragend', () => c.classList.remove('dragging'));
|
||
});
|
||
cols.forEach(col => {
|
||
col.addEventListener('dragover', e => { e.preventDefault(); col.classList.add('drag-over'); });
|
||
col.addEventListener('dragleave', () => col.classList.remove('drag-over'));
|
||
col.addEventListener('drop', async e => {
|
||
e.preventDefault(); col.classList.remove('drag-over');
|
||
const newStage = col.dataset.stage;
|
||
if (!dragId) return;
|
||
try {
|
||
await api('/opportunities/'+dragId+'/stage', {method:'PATCH', body:JSON.stringify({stage:newStage})});
|
||
toast('Faza promijenjena: '+STAGE_LABEL[newStage]);
|
||
loadPipeline();
|
||
} catch (er) { toast('Stage update err: '+er.message, 'err'); }
|
||
});
|
||
});
|
||
}
|
||
|
||
// ────── Accounts ──────
|
||
async function loadAccounts() {
|
||
const q = document.getElementById('acc-q').value.trim();
|
||
const t = document.getElementById('acc-type').value;
|
||
const qs = new URLSearchParams();
|
||
if (q) qs.set('q', q);
|
||
if (t) qs.set('type', t);
|
||
try {
|
||
const data = await api('/accounts?'+qs.toString());
|
||
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;
|
||
} catch (e) { toast('Accounts err: '+e.message, 'err'); }
|
||
}
|
||
|
||
function accountFormHTML(a={}) {
|
||
return `
|
||
<div class="fld"><label>Naziv*</label><input id="f-naziv" value="${esc(a.naziv||'')}"></div>
|
||
<div class="fld-row">
|
||
<div class="fld"><label>Tip</label>
|
||
<select id="f-type">
|
||
${['klub','savez','sponzor','drzava','drugo'].map(x=>`<option value="${x}" ${a.type===x?'selected':''}>${x}</option>`).join('')}
|
||
</select>
|
||
</div>
|
||
<div class="fld"><label>OIB</label><input id="f-oib" value="${esc(a.oib||'')}"></div>
|
||
</div>
|
||
<div class="fld-row">
|
||
<div class="fld"><label>Email</label><input id="f-email" value="${esc(a.email||'')}"></div>
|
||
<div class="fld"><label>Telefon</label><input id="f-telefon" value="${esc(a.telefon||'')}"></div>
|
||
</div>
|
||
<div class="fld-row">
|
||
<div class="fld"><label>Web</label><input id="f-web" value="${esc(a.web||'')}"></div>
|
||
<div class="fld"><label>Industry</label><input id="f-industry" value="${esc(a.industry||'')}" placeholder="sport / grana / ..."></div>
|
||
</div>
|
||
<div class="fld-row">
|
||
<div class="fld"><label>Adresa</label><input id="f-adresa" value="${esc(a.adresa||'')}"></div>
|
||
<div class="fld"><label>Grad</label><input id="f-grad" value="${esc(a.grad||'')}"></div>
|
||
</div>
|
||
<div class="fld"><label>Napomene</label><textarea id="f-napomene">${esc(a.napomene||'')}</textarea></div>
|
||
`;
|
||
}
|
||
function readAccountForm() {
|
||
return {
|
||
naziv: document.getElementById('f-naziv').value.trim(),
|
||
type: document.getElementById('f-type').value,
|
||
oib: document.getElementById('f-oib').value.trim() || null,
|
||
email: document.getElementById('f-email').value.trim() || null,
|
||
telefon: document.getElementById('f-telefon').value.trim() || null,
|
||
web: document.getElementById('f-web').value.trim() || null,
|
||
industry: document.getElementById('f-industry').value.trim() || null,
|
||
adresa: document.getElementById('f-adresa').value.trim() || null,
|
||
grad: document.getElementById('f-grad').value.trim() || null,
|
||
napomene: document.getElementById('f-napomene').value || null,
|
||
};
|
||
}
|
||
function openAccountModal(a) {
|
||
const isEdit = !!(a && a.id);
|
||
showModal(isEdit ? 'Uredi account' : 'Novi account', accountFormHTML(a||{}), async () => {
|
||
const body = readAccountForm();
|
||
if (!body.naziv) { toast('Naziv je obavezan', 'err'); return; }
|
||
try {
|
||
if (isEdit) await api('/accounts/'+a.id, {method:'PUT', body:JSON.stringify(body)});
|
||
else await api('/accounts', {method:'POST', body:JSON.stringify(body)});
|
||
toast('Spremljeno'); closeModal(); loadAccounts(); loadPipeline();
|
||
} catch (e) { toast('Greška: '+e.message, 'err'); }
|
||
});
|
||
}
|
||
async function editAccount(id) {
|
||
try { const a = await api('/accounts/'+id); openAccountModal(a); }
|
||
catch (e) { toast(e.message, 'err'); }
|
||
}
|
||
async function delAccount(id, naziv) {
|
||
if (!confirm('Obrisati account "'+naziv+'"? (kaskadno briše opps/cases/activities)')) return;
|
||
try { await api('/accounts/'+id, {method:'DELETE'}); toast('Obrisano'); loadAccounts(); loadPipeline(); }
|
||
catch (e) { toast(e.message, 'err'); }
|
||
}
|
||
|
||
// ────── Contacts ──────
|
||
async function loadContacts() {
|
||
const q = document.getElementById('con-q').value.trim();
|
||
const aid = document.getElementById('con-acc').value.trim();
|
||
const qs = new URLSearchParams();
|
||
if (q) qs.set('q', q);
|
||
if (aid) qs.set('account_id', aid);
|
||
try {
|
||
const data = await api('/contacts?'+qs.toString());
|
||
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;
|
||
} catch (e) { toast(e.message, 'err'); }
|
||
}
|
||
function contactFormHTML(c={}) {
|
||
return `
|
||
<div class="fld-row">
|
||
<div class="fld"><label>Ime*</label><input id="f-ime" value="${esc(c.ime||'')}"></div>
|
||
<div class="fld"><label>Prezime*</label><input id="f-prezime" value="${esc(c.prezime||'')}"></div>
|
||
</div>
|
||
<div class="fld-row">
|
||
<div class="fld"><label>Account ID</label><input id="f-account_id" type="number" value="${c.account_id||''}"></div>
|
||
<div class="fld"><label>Funkcija</label><input id="f-funkcija" value="${esc(c.funkcija||'')}" placeholder="predsjednik / tajnik / trener / ..."></div>
|
||
</div>
|
||
<div class="fld-row">
|
||
<div class="fld"><label>Email</label><input id="f-email" value="${esc(c.email||'')}"></div>
|
||
<div class="fld"><label>Telefon</label><input id="f-telefon" value="${esc(c.telefon||'')}"></div>
|
||
</div>
|
||
<div class="fld-row">
|
||
<div class="fld"><label>Mobitel</label><input id="f-mobitel" value="${esc(c.mobitel||'')}"></div>
|
||
<div class="fld"><label>Clan ID (opc.)</label><input id="f-clan_id" type="number" value="${c.clan_id||''}"></div>
|
||
</div>
|
||
<div class="fld"><label>Napomene</label><textarea id="f-napomene">${esc(c.napomene||'')}</textarea></div>
|
||
`;
|
||
}
|
||
function readContactForm() {
|
||
return {
|
||
ime: document.getElementById('f-ime').value.trim(),
|
||
prezime: document.getElementById('f-prezime').value.trim(),
|
||
account_id: parseInt(document.getElementById('f-account_id').value)||null,
|
||
clan_id: parseInt(document.getElementById('f-clan_id').value)||null,
|
||
funkcija: document.getElementById('f-funkcija').value.trim()||null,
|
||
email: document.getElementById('f-email').value.trim()||null,
|
||
telefon: document.getElementById('f-telefon').value.trim()||null,
|
||
mobitel: document.getElementById('f-mobitel').value.trim()||null,
|
||
napomene: document.getElementById('f-napomene').value||null,
|
||
};
|
||
}
|
||
function openContactModal(c) {
|
||
const isEdit = !!(c && c.id);
|
||
showModal(isEdit?'Uredi kontakt':'Novi kontakt', contactFormHTML(c||{}), async () => {
|
||
const body = readContactForm();
|
||
if (!body.ime || !body.prezime) { toast('Ime i prezime su obavezni', 'err'); return; }
|
||
try {
|
||
if (isEdit) await api('/contacts/'+c.id, {method:'PUT', body:JSON.stringify(body)});
|
||
else await api('/contacts', {method:'POST', body:JSON.stringify(body)});
|
||
toast('Spremljeno'); closeModal(); loadContacts();
|
||
} catch (e) { toast(e.message, 'err'); }
|
||
});
|
||
}
|
||
async function editContact(id) {
|
||
try { const c = await api('/contacts/'+id); openContactModal(c); }
|
||
catch (e) { toast(e.message, 'err'); }
|
||
}
|
||
async function delContact(id) {
|
||
if (!confirm('Obrisati kontakt?')) return;
|
||
try { await api('/contacts/'+id, {method:'DELETE'}); toast('Obrisano'); loadContacts(); }
|
||
catch (e) { toast(e.message, 'err'); }
|
||
}
|
||
|
||
// ────── Leads ──────
|
||
async function loadLeads() {
|
||
const q = document.getElementById('lead-q').value.trim();
|
||
const s = document.getElementById('lead-status').value;
|
||
const qs = new URLSearchParams();
|
||
if (q) qs.set('q', q);
|
||
if (s) qs.set('status', s);
|
||
try {
|
||
const data = await api('/leads?'+qs.toString());
|
||
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;
|
||
} catch (e) { toast(e.message, 'err'); }
|
||
}
|
||
function leadFormHTML(l={}) {
|
||
return `
|
||
<div class="fld-row">
|
||
<div class="fld"><label>Ime</label><input id="f-ime" value="${esc(l.ime||'')}"></div>
|
||
<div class="fld"><label>Prezime</label><input id="f-prezime" value="${esc(l.prezime||'')}"></div>
|
||
</div>
|
||
<div class="fld"><label>Organizacija</label><input id="f-org" value="${esc(l.organizacija||'')}"></div>
|
||
<div class="fld-row">
|
||
<div class="fld"><label>Email</label><input id="f-email" value="${esc(l.email||'')}"></div>
|
||
<div class="fld"><label>Telefon</label><input id="f-telefon" value="${esc(l.telefon||'')}"></div>
|
||
</div>
|
||
<div class="fld-row">
|
||
<div class="fld"><label>Izvor</label><input id="f-izvor" value="${esc(l.izvor||'')}" placeholder="web / sajam / preporuka / cold-call"></div>
|
||
<div class="fld"><label>Status</label>
|
||
<select id="f-status">
|
||
${['new','contacted','qualified','lost','converted'].map(x=>`<option value="${x}" ${l.status===x?'selected':''}>${x}</option>`).join('')}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="fld"><label>Napomene</label><textarea id="f-napomene">${esc(l.napomene||'')}</textarea></div>
|
||
`;
|
||
}
|
||
function readLeadForm() {
|
||
return {
|
||
ime: document.getElementById('f-ime').value.trim()||null,
|
||
prezime: document.getElementById('f-prezime').value.trim()||null,
|
||
organizacija: document.getElementById('f-org').value.trim()||null,
|
||
email: document.getElementById('f-email').value.trim()||null,
|
||
telefon: document.getElementById('f-telefon').value.trim()||null,
|
||
izvor: document.getElementById('f-izvor').value.trim()||null,
|
||
status: document.getElementById('f-status').value,
|
||
napomene: document.getElementById('f-napomene').value||null,
|
||
};
|
||
}
|
||
function openLeadModal(l) {
|
||
const isEdit = !!(l && l.id);
|
||
showModal(isEdit?'Uredi lead':'Novi lead', leadFormHTML(l||{status:'new'}), async () => {
|
||
const body = readLeadForm();
|
||
try {
|
||
if (isEdit) await api('/leads/'+l.id, {method:'PUT', body:JSON.stringify(body)});
|
||
else await api('/leads', {method:'POST', body:JSON.stringify(body)});
|
||
toast('Spremljeno'); closeModal(); loadLeads();
|
||
} catch (e) { toast(e.message, 'err'); }
|
||
});
|
||
}
|
||
async function editLead(id) {
|
||
try { const l = await api('/leads/'+id); openLeadModal(l); }
|
||
catch (e) { toast(e.message, 'err'); }
|
||
}
|
||
async function delLead(id) {
|
||
if (!confirm('Obrisati lead?')) return;
|
||
try { await api('/leads/'+id, {method:'DELETE'}); toast('Obrisano'); loadLeads(); }
|
||
catch (e) { toast(e.message, 'err'); }
|
||
}
|
||
async function convertLead(id) {
|
||
try {
|
||
const l = await api('/leads/'+id);
|
||
const body = `
|
||
<div class="fld"><label>Account naziv</label><input id="cv-acc" value="${esc(l.organizacija || ((l.ime||'')+' '+(l.prezime||'')).trim())}"></div>
|
||
<div class="fld"><label>Account tip</label>
|
||
<select id="cv-type">
|
||
${['klub','savez','sponzor','drzava','drugo'].map(x=>`<option value="${x}">${x}</option>`).join('')}
|
||
</select>
|
||
</div>
|
||
<div class="fld"><label><input type="checkbox" id="cv-opp"> Stvori i Opportunity</label></div>
|
||
<div id="cv-opp-fields" style="display:none">
|
||
<div class="fld"><label>Naziv prilike</label><input id="cv-opp-naziv" value="Lead ${l.id} → Opportunity"></div>
|
||
<div class="fld-row">
|
||
<div class="fld"><label>Iznos (EUR)</label><input id="cv-opp-amount" type="number" step="0.01"></div>
|
||
<div class="fld"><label>Vjerojatnost %</label><input id="cv-opp-prob" type="number" value="20"></div>
|
||
</div>
|
||
<div class="fld"><label>Close datum</label><input id="cv-opp-close" type="date"></div>
|
||
</div>
|
||
`;
|
||
showModal('Konvertiraj lead', body, async () => {
|
||
const payload = {
|
||
account: { naziv: document.getElementById('cv-acc').value, type: document.getElementById('cv-type').value },
|
||
};
|
||
if (document.getElementById('cv-opp').checked) {
|
||
payload.opportunity = {
|
||
naziv: document.getElementById('cv-opp-naziv').value,
|
||
amount_eur: parseFloat(document.getElementById('cv-opp-amount').value)||null,
|
||
probability: parseInt(document.getElementById('cv-opp-prob').value)||20,
|
||
close_date: document.getElementById('cv-opp-close').value || null,
|
||
};
|
||
}
|
||
try {
|
||
await api('/leads/'+id+'/convert', {method:'POST', body:JSON.stringify(payload)});
|
||
toast('Lead konvertiran'); closeModal(); loadLeads(); loadPipeline();
|
||
} catch (e) { toast(e.message, 'err'); }
|
||
});
|
||
setTimeout(() => {
|
||
document.getElementById('cv-opp').addEventListener('change', e => {
|
||
document.getElementById('cv-opp-fields').style.display = e.target.checked ? 'block' : 'none';
|
||
});
|
||
}, 0);
|
||
} catch (e) { toast(e.message, 'err'); }
|
||
}
|
||
|
||
// ────── Opportunities ──────
|
||
async function loadOpps() {
|
||
const q = document.getElementById('opp-q').value.trim();
|
||
const s = document.getElementById('opp-stage').value;
|
||
const qs = new URLSearchParams();
|
||
if (q) qs.set('q', q);
|
||
if (s) qs.set('stage', s);
|
||
try {
|
||
const data = await api('/opportunities?'+qs.toString());
|
||
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;
|
||
} catch (e) { toast(e.message, 'err'); }
|
||
}
|
||
function oppFormHTML(o={}) {
|
||
return `
|
||
<div class="fld"><label>Naziv*</label><input id="f-naziv" value="${esc(o.naziv||'')}"></div>
|
||
<div class="fld-row">
|
||
<div class="fld"><label>Account ID*</label><input id="f-account_id" type="number" value="${o.account_id||''}"></div>
|
||
<div class="fld"><label>Contact ID</label><input id="f-contact_id" type="number" value="${o.contact_id||''}"></div>
|
||
</div>
|
||
<div class="fld-row">
|
||
<div class="fld"><label>Tip</label>
|
||
<select id="f-type">
|
||
${['financiranje','sponzorstvo','grant','natjecanje','drugo'].map(x=>`<option value="${x}" ${o.type===x?'selected':''}>${x}</option>`).join('')}
|
||
</select>
|
||
</div>
|
||
<div class="fld"><label>Faza</label>
|
||
<select id="f-stage">
|
||
${STAGES.map(x=>`<option value="${x}" ${o.stage===x?'selected':''}>${x}</option>`).join('')}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="fld-row">
|
||
<div class="fld"><label>Iznos (EUR)</label><input id="f-amount" type="number" step="0.01" value="${o.amount_eur||''}"></div>
|
||
<div class="fld"><label>Vjerojatnost %</label><input id="f-prob" type="number" value="${o.probability??20}"></div>
|
||
</div>
|
||
<div class="fld"><label>Close datum</label><input id="f-close" type="date" value="${(o.close_date||'').slice(0,10)}"></div>
|
||
<div class="fld"><label>Napomene</label><textarea id="f-napomene">${esc(o.napomene||'')}</textarea></div>
|
||
`;
|
||
}
|
||
function readOppForm() {
|
||
return {
|
||
naziv: document.getElementById('f-naziv').value.trim(),
|
||
account_id: parseInt(document.getElementById('f-account_id').value),
|
||
contact_id: parseInt(document.getElementById('f-contact_id').value)||null,
|
||
type: document.getElementById('f-type').value,
|
||
stage: document.getElementById('f-stage').value,
|
||
amount_eur: parseFloat(document.getElementById('f-amount').value)||null,
|
||
probability: parseInt(document.getElementById('f-prob').value)||0,
|
||
close_date: document.getElementById('f-close').value||null,
|
||
napomene: document.getElementById('f-napomene').value||null,
|
||
};
|
||
}
|
||
function openOppModal(o) {
|
||
const isEdit = !!(o && o.id);
|
||
showModal(isEdit?'Uredi priliku':'Nova prilika', oppFormHTML(o||{stage:'prospecting',probability:20}), async () => {
|
||
const body = readOppForm();
|
||
if (!body.naziv || !body.account_id) { toast('Naziv i Account ID su obavezni', 'err'); return; }
|
||
try {
|
||
if (isEdit) await api('/opportunities/'+o.id, {method:'PUT', body:JSON.stringify(body)});
|
||
else await api('/opportunities', {method:'POST', body:JSON.stringify(body)});
|
||
toast('Spremljeno'); closeModal(); loadOpps(); loadPipeline();
|
||
} catch (e) { toast(e.message, 'err'); }
|
||
});
|
||
}
|
||
async function editOpp(id) {
|
||
try { const o = await api('/opportunities/'+id); openOppModal(o); }
|
||
catch (e) { toast(e.message, 'err'); }
|
||
}
|
||
async function delOpp(id) {
|
||
if (!confirm('Obrisati priliku?')) return;
|
||
try { await api('/opportunities/'+id, {method:'DELETE'}); toast('Obrisano'); loadOpps(); loadPipeline(); }
|
||
catch (e) { toast(e.message, 'err'); }
|
||
}
|
||
|
||
// ────── Activities ──────
|
||
async function loadActivities() {
|
||
const t = document.getElementById('act-type').value;
|
||
const o = document.getElementById('act-open').value;
|
||
const qs = new URLSearchParams();
|
||
if (t) qs.set('type', t);
|
||
if (o) qs.set('open_only', o);
|
||
try {
|
||
const data = await api('/activities?'+qs.toString());
|
||
const tb = document.querySelector('#t-activities tbody');
|
||
tb.innerHTML = (data.items||[]).map(a => `
|
||
<tr onclick="editActivity(${a.id})">
|
||
<td>${esc(a.type)}</td>
|
||
<td><strong>${esc(a.subject)}</strong></td>
|
||
<td>${esc(a.account_naziv||'—')}</td>
|
||
<td>${esc(a.contact_naziv||'—')}</td>
|
||
<td>${fmtDT(a.due_at)}</td>
|
||
<td>${a.completed_at ? '<span class="chip resolved">done</span>' : '<span class="chip new">open</span>'}</td>
|
||
<td>
|
||
${!a.completed_at ? `<button class="btn sm gold" onclick="event.stopPropagation();completeActivity(${a.id})">✓</button>` : ''}
|
||
<button class="btn sm" onclick="event.stopPropagation();delActivity(${a.id})">×</button>
|
||
</td>
|
||
</tr>
|
||
`).join('') || '<tr><td colspan="7" class="empty">Nema aktivnosti.</td></tr>';
|
||
document.getElementById('cnt-activities').textContent = (data.items||[]).length;
|
||
} catch (e) { toast(e.message, 'err'); }
|
||
}
|
||
function activityFormHTML(a={}) {
|
||
return `
|
||
<div class="fld-row">
|
||
<div class="fld"><label>Tip*</label>
|
||
<select id="f-type">
|
||
${['call','meeting','email','task','note'].map(x=>`<option value="${x}" ${a.type===x?'selected':''}>${x}</option>`).join('')}
|
||
</select>
|
||
</div>
|
||
<div class="fld"><label>Due (datum/vrijeme)</label><input id="f-due" type="datetime-local" value="${(a.due_at||'').slice(0,16)}"></div>
|
||
</div>
|
||
<div class="fld"><label>Subject*</label><input id="f-subject" value="${esc(a.subject||'')}"></div>
|
||
<div class="fld-row">
|
||
<div class="fld"><label>Account ID</label><input id="f-account_id" type="number" value="${a.account_id||''}"></div>
|
||
<div class="fld"><label>Contact ID</label><input id="f-contact_id" type="number" value="${a.contact_id||''}"></div>
|
||
</div>
|
||
<div class="fld-row">
|
||
<div class="fld"><label>Opportunity ID</label><input id="f-opp_id" type="number" value="${a.opportunity_id||''}"></div>
|
||
<div class="fld"><label>Lead ID</label><input id="f-lead_id" type="number" value="${a.lead_id||''}"></div>
|
||
</div>
|
||
<div class="fld"><label>Body / bilješke</label><textarea id="f-body">${esc(a.body||'')}</textarea></div>
|
||
`;
|
||
}
|
||
function readActivityForm() {
|
||
const due = document.getElementById('f-due').value;
|
||
return {
|
||
type: document.getElementById('f-type').value,
|
||
subject: document.getElementById('f-subject').value.trim(),
|
||
body: document.getElementById('f-body').value||null,
|
||
account_id: parseInt(document.getElementById('f-account_id').value)||null,
|
||
contact_id: parseInt(document.getElementById('f-contact_id').value)||null,
|
||
opportunity_id: parseInt(document.getElementById('f-opp_id').value)||null,
|
||
lead_id: parseInt(document.getElementById('f-lead_id').value)||null,
|
||
due_at: due ? new Date(due).toISOString() : null,
|
||
};
|
||
}
|
||
function openActivityModal(a) {
|
||
const isEdit = !!(a && a.id);
|
||
showModal(isEdit?'Uredi aktivnost':'Nova aktivnost', activityFormHTML(a||{type:'task'}), async () => {
|
||
const body = readActivityForm();
|
||
if (!body.subject) { toast('Subject je obavezan', 'err'); return; }
|
||
try {
|
||
if (isEdit) await api('/activities/'+a.id, {method:'PUT', body:JSON.stringify(body)});
|
||
else await api('/activities', {method:'POST', body:JSON.stringify(body)});
|
||
toast('Spremljeno'); closeModal(); loadActivities();
|
||
} catch (e) { toast(e.message, 'err'); }
|
||
});
|
||
}
|
||
async function editActivity(id) {
|
||
try { const a = await api('/activities/'+id); openActivityModal(a); }
|
||
catch (e) { toast(e.message, 'err'); }
|
||
}
|
||
async function completeActivity(id) {
|
||
try { await api('/activities/'+id+'/complete', {method:'PATCH'}); toast('Označeno gotovo'); loadActivities(); }
|
||
catch (e) { toast(e.message, 'err'); }
|
||
}
|
||
async function delActivity(id) {
|
||
if (!confirm('Obrisati aktivnost?')) return;
|
||
try { await api('/activities/'+id, {method:'DELETE'}); toast('Obrisano'); loadActivities(); }
|
||
catch (e) { toast(e.message, 'err'); }
|
||
}
|
||
|
||
// ────── Cases ──────
|
||
async function loadCases() {
|
||
const q = document.getElementById('case-q').value.trim();
|
||
const s = document.getElementById('case-status').value;
|
||
const p = document.getElementById('case-priority').value;
|
||
const qs = new URLSearchParams();
|
||
if (q) qs.set('q', q);
|
||
if (s) qs.set('status', s);
|
||
if (p) qs.set('priority', p);
|
||
try {
|
||
const data = await api('/cases?'+qs.toString());
|
||
const tb = document.querySelector('#t-cases tbody');
|
||
tb.innerHTML = (data.items||[]).map(c => `
|
||
<tr onclick="editCase(${c.id})">
|
||
<td><strong>${esc(c.subject)}</strong></td>
|
||
<td>${esc(c.account_naziv||'—')}</td>
|
||
<td><span class="chip ${c.status}">${c.status}</span></td>
|
||
<td><span class="chip ${c.priority}">${c.priority}</span></td>
|
||
<td>${fmtDT(c.created_at)}</td>
|
||
<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>';
|
||
document.getElementById('cnt-cases').textContent = (data.items||[]).length;
|
||
} catch (e) { toast(e.message, 'err'); }
|
||
}
|
||
function caseFormHTML(k={}) {
|
||
return `
|
||
<div class="fld"><label>Subject*</label><input id="f-subject" value="${esc(k.subject||'')}"></div>
|
||
<div class="fld-row">
|
||
<div class="fld"><label>Account ID</label><input id="f-account_id" type="number" value="${k.account_id||''}"></div>
|
||
<div class="fld"><label>Contact ID</label><input id="f-contact_id" type="number" value="${k.contact_id||''}"></div>
|
||
</div>
|
||
<div class="fld-row">
|
||
<div class="fld"><label>Status</label>
|
||
<select id="f-status">
|
||
${['open','in_progress','waiting','resolved','closed'].map(x=>`<option value="${x}" ${k.status===x?'selected':''}>${x}</option>`).join('')}
|
||
</select>
|
||
</div>
|
||
<div class="fld"><label>Prioritet</label>
|
||
<select id="f-priority">
|
||
${['low','normal','high','urgent'].map(x=>`<option value="${x}" ${k.priority===x?'selected':''}>${x}</option>`).join('')}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="fld"><label>Opis</label><textarea id="f-desc">${esc(k.description||'')}</textarea></div>
|
||
`;
|
||
}
|
||
function readCaseForm() {
|
||
return {
|
||
subject: document.getElementById('f-subject').value.trim(),
|
||
account_id: parseInt(document.getElementById('f-account_id').value)||null,
|
||
contact_id: parseInt(document.getElementById('f-contact_id').value)||null,
|
||
status: document.getElementById('f-status').value,
|
||
priority: document.getElementById('f-priority').value,
|
||
description: document.getElementById('f-desc').value||null,
|
||
};
|
||
}
|
||
function openCaseModal(k) {
|
||
const isEdit = !!(k && k.id);
|
||
showModal(isEdit?'Uredi case':'Novi case', caseFormHTML(k||{status:'open',priority:'normal'}), async () => {
|
||
const body = readCaseForm();
|
||
if (!body.subject) { toast('Subject je obavezan', 'err'); return; }
|
||
try {
|
||
if (isEdit) await api('/cases/'+k.id, {method:'PUT', body:JSON.stringify(body)});
|
||
else await api('/cases', {method:'POST', body:JSON.stringify(body)});
|
||
toast('Spremljeno'); closeModal(); loadCases();
|
||
} catch (e) { toast(e.message, 'err'); }
|
||
});
|
||
}
|
||
async function editCase(id) {
|
||
try { const k = await api('/cases/'+id); openCaseModal(k); }
|
||
catch (e) { toast(e.message, 'err'); }
|
||
}
|
||
async function delCase(id) {
|
||
if (!confirm('Obrisati case?')) return;
|
||
try { await api('/cases/'+id, {method:'DELETE'}); toast('Obrisano'); loadCases(); }
|
||
catch (e) { toast(e.message, 'err'); }
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// AGENT F — Članarine / Liječnički / Obrasci
|
||
// ══════════════════════════════════════════════════════════════════
|
||
|
||
let CURRENT_USER = null;
|
||
async function ensureMe() {
|
||
if (CURRENT_USER) return CURRENT_USER;
|
||
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}});
|
||
if (r.ok) { CURRENT_USER = await r.json(); break; }
|
||
} catch {}
|
||
}
|
||
return CURRENT_USER;
|
||
}
|
||
function isAdminUser() {
|
||
if (!CURRENT_USER) return false;
|
||
const t = CURRENT_USER.user_type || CURRENT_USER.role || (CURRENT_USER.user && CURRENT_USER.user.user_type) || '';
|
||
return t === 'super_admin' || t === 'pgz_admin';
|
||
}
|
||
|
||
// ────── Članarine ──────
|
||
async function loadClanarine() {
|
||
const klub = document.getElementById('cln-klub').value.trim();
|
||
const clan = document.getElementById('cln-clan').value.trim();
|
||
const god = document.getElementById('cln-godina').value.trim();
|
||
const st = document.getElementById('cln-status').value;
|
||
const qs = new URLSearchParams();
|
||
if (klub) qs.set('klub_id', klub);
|
||
if (clan) qs.set('clan_id', clan);
|
||
if (god) qs.set('godina', god);
|
||
if (st) qs.set('status', st);
|
||
try {
|
||
const data = await api('/clanarine?'+qs.toString());
|
||
const tb = document.querySelector('#t-clanarine tbody');
|
||
tb.innerHTML = (data.items||[]).map(c => `
|
||
<tr onclick="editClanarina(${c.id})">
|
||
<td><strong>${esc(c.clan_naziv||'—')}</strong> <span style="color:var(--t3)">#${c.clan_id||''}</span></td>
|
||
<td>${esc(c.klub_naziv||'—')}</td>
|
||
<td>${c.godina}</td>
|
||
<td>${esc(c.razdoblje||'—')}</td>
|
||
<td>${fmtEur(c.iznos_propisan)}</td>
|
||
<td>${fmtEur(c.iznos_placen)}</td>
|
||
<td>${fmtDate(c.datum_uplate)}</td>
|
||
<td><span class="chip ${c.status}">${c.status}</span></td>
|
||
<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>';
|
||
document.getElementById('cnt-clanarine').textContent = data.count ?? (data.items||[]).length;
|
||
} catch (e) { toast('Članarine err: '+e.message, 'err'); }
|
||
}
|
||
function clanarinaFormHTML(c={}) {
|
||
return `
|
||
<div class="fld-row">
|
||
<div class="fld"><label>Klub ID</label><input id="f-klub_id" type="number" value="${c.klub_id||''}"></div>
|
||
<div class="fld"><label>Član ID</label><input id="f-clan_id" type="number" value="${c.clan_id||''}"></div>
|
||
</div>
|
||
<div class="fld-row">
|
||
<div class="fld"><label>Godina*</label><input id="f-godina" type="number" value="${c.godina||(new Date()).getFullYear()}"></div>
|
||
<div class="fld"><label>Razdoblje</label><input id="f-razdoblje" value="${esc(c.razdoblje||'')}" placeholder="npr. cijela godina"></div>
|
||
</div>
|
||
<div class="fld-row">
|
||
<div class="fld"><label>Iznos propisan (EUR)*</label><input id="f-iznos_propisan" type="number" step="0.01" value="${c.iznos_propisan||''}"></div>
|
||
<div class="fld"><label>Iznos plaćen (EUR)</label><input id="f-iznos_placen" type="number" step="0.01" value="${c.iznos_placen||0}"></div>
|
||
</div>
|
||
<div class="fld-row">
|
||
<div class="fld"><label>Datum uplate</label><input id="f-datum_uplate" type="date" value="${(c.datum_uplate||'').slice(0,10)}"></div>
|
||
<div class="fld"><label>Način uplate</label><input id="f-nacin_uplate" value="${esc(c.nacin_uplate||'')}" placeholder="virman / kartica / gotovina"></div>
|
||
</div>
|
||
<div class="fld-row">
|
||
<div class="fld"><label>Referenca</label><input id="f-referenca" value="${esc(c.referenca||'')}"></div>
|
||
<div class="fld"><label>Račun broj</label><input id="f-racun_broj" value="${esc(c.racun_broj||'')}"></div>
|
||
</div>
|
||
<div class="fld"><label>Status</label>
|
||
<select id="f-status">
|
||
${['nepodmireno','djelomicno','podmireno','storno'].map(x=>`<option value="${x}" ${c.status===x?'selected':''}>${x}</option>`).join('')}
|
||
</select>
|
||
</div>
|
||
<div class="fld"><label>Napomena</label><textarea id="f-napomena">${esc(c.napomena||'')}</textarea></div>
|
||
`;
|
||
}
|
||
function readClanarinaForm() {
|
||
return {
|
||
klub_id: parseInt(document.getElementById('f-klub_id').value)||null,
|
||
clan_id: parseInt(document.getElementById('f-clan_id').value)||null,
|
||
godina: parseInt(document.getElementById('f-godina').value)||null,
|
||
razdoblje: document.getElementById('f-razdoblje').value.trim()||null,
|
||
iznos_propisan: parseFloat(document.getElementById('f-iznos_propisan').value)||0,
|
||
iznos_placen: parseFloat(document.getElementById('f-iznos_placen').value)||0,
|
||
datum_uplate: document.getElementById('f-datum_uplate').value||null,
|
||
nacin_uplate: document.getElementById('f-nacin_uplate').value.trim()||null,
|
||
referenca: document.getElementById('f-referenca').value.trim()||null,
|
||
racun_broj: document.getElementById('f-racun_broj').value.trim()||null,
|
||
status: document.getElementById('f-status').value,
|
||
napomena: document.getElementById('f-napomena').value||null,
|
||
};
|
||
}
|
||
function openClanarinaModal(c) {
|
||
const isEdit = !!(c && c.id);
|
||
showModal(isEdit?'Uredi članarinu':'Nova članarina',
|
||
clanarinaFormHTML(c||{godina:(new Date()).getFullYear(), status:'nepodmireno'}),
|
||
async () => {
|
||
const body = readClanarinaForm();
|
||
if (!body.godina || !body.iznos_propisan) { toast('Godina i iznos propisan su obavezni', 'err'); return; }
|
||
try {
|
||
if (isEdit) await api('/clanarine/'+c.id, {method:'PUT', body:JSON.stringify(body)});
|
||
else await api('/clanarine', {method:'POST', body:JSON.stringify(body)});
|
||
toast('Spremljeno'); closeModal(); loadClanarine();
|
||
} catch (e) { toast('Greška: '+e.message, 'err'); }
|
||
});
|
||
}
|
||
async function editClanarina(id) {
|
||
try { const c = await api('/clanarine/'+id); openClanarinaModal(c); }
|
||
catch (e) { toast(e.message, 'err'); }
|
||
}
|
||
async function delClanarina(id) {
|
||
if (!confirm('Obrisati članarinu #'+id+'?')) return;
|
||
try { await api('/clanarine/'+id, {method:'DELETE'}); toast('Obrisano'); loadClanarine(); }
|
||
catch (e) { toast(e.message, 'err'); }
|
||
}
|
||
|
||
// ────── Liječnički ──────
|
||
async function loadLijecnicki() {
|
||
const klub = document.getElementById('lij-klub').value.trim();
|
||
const clan = document.getElementById('lij-clan').value.trim();
|
||
const exp = document.getElementById('lij-expiring').value;
|
||
const qs = new URLSearchParams();
|
||
if (klub) qs.set('klub_id', klub);
|
||
if (clan) qs.set('clan_id', clan);
|
||
if (exp) qs.set('expiring', exp);
|
||
try {
|
||
const data = await api('/lijecnicki?'+qs.toString());
|
||
const tb = document.querySelector('#t-lijecnicki tbody');
|
||
const today = new Date().toISOString().slice(0,10);
|
||
tb.innerHTML = (data.items||[]).map(l => {
|
||
const expired = l.vrijedi_do && l.vrijedi_do < today;
|
||
return `
|
||
<tr onclick="editLijecnicki(${l.id})">
|
||
<td><strong>${esc(l.clan_naziv||'—')}</strong> <span style="color:var(--t3)">#${l.clan_id||''}</span></td>
|
||
<td>${esc(l.klub_naziv||'—')}</td>
|
||
<td>${fmtDate(l.datum_pregleda)}</td>
|
||
<td>${esc(l.vrsta_pregleda||'—')}</td>
|
||
<td>${fmtDate(l.vrijedi_do)} ${expired?'<span class="chip nepodmireno" style="margin-left:6px;">istekao</span>':''}</td>
|
||
<td>${esc(l.lijecnik||'—')}</td>
|
||
<td>${l.spreman_za_natjecanje ? '<span class="chip spreman">DA</span>' : '<span class="chip nije-spreman">NE</span>'}</td>
|
||
<td>${l.placeno ? '<span class="chip podmireno">DA</span>' : '<span class="chip nepodmireno">NE</span>'}</td>
|
||
<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>';
|
||
document.getElementById('cnt-lijecnicki').textContent = data.count ?? (data.items||[]).length;
|
||
} catch (e) { toast('Liječnički err: '+e.message, 'err'); }
|
||
}
|
||
function lijecnickiFormHTML(l={}) {
|
||
return `
|
||
<div class="fld-row">
|
||
<div class="fld"><label>Član ID*</label><input id="f-clan_id" type="number" value="${l.clan_id||''}"></div>
|
||
<div class="fld"><label>Klub ID</label><input id="f-klub_id" type="number" value="${l.klub_id||''}"></div>
|
||
</div>
|
||
<div class="fld-row">
|
||
<div class="fld"><label>Datum pregleda*</label><input id="f-datum_pregleda" type="date" value="${(l.datum_pregleda||'').slice(0,10)}"></div>
|
||
<div class="fld"><label>Vrijedi do</label><input id="f-vrijedi_do" type="date" value="${(l.vrijedi_do||'').slice(0,10)}"></div>
|
||
</div>
|
||
<div class="fld-row">
|
||
<div class="fld"><label>Vrsta pregleda</label>
|
||
<select id="f-vrsta_pregleda">
|
||
<option value="">—</option>
|
||
${['osnovni','prosireni','specijalisticki','kontrolni','povratak_nakon_ozljede'].map(x=>`<option value="${x}" ${l.vrsta_pregleda===x?'selected':''}>${x}</option>`).join('')}
|
||
</select>
|
||
</div>
|
||
<div class="fld"><label>Ustanova</label><input id="f-ustanova" value="${esc(l.ustanova||'')}"></div>
|
||
</div>
|
||
<div class="fld"><label>Liječnik</label><input id="f-lijecnik" value="${esc(l.lijecnik||'')}"></div>
|
||
<div class="fld-row">
|
||
<div class="fld"><label><input id="f-spreman" type="checkbox" ${l.spreman_za_natjecanje!==false?'checked':''}> Spreman za natjecanje</label></div>
|
||
<div class="fld"><label><input id="f-placeno" type="checkbox" ${l.placeno?'checked':''}> Plaćeno</label></div>
|
||
</div>
|
||
<div class="fld-row">
|
||
<div class="fld"><label><input id="f-ekg" type="checkbox" ${l.ekg?'checked':''}> EKG</label></div>
|
||
<div class="fld"><label><input id="f-krv" type="checkbox" ${l.krv?'checked':''}> Krvna slika</label></div>
|
||
<div class="fld"><label><input id="f-spirometrija" type="checkbox" ${l.spirometrija?'checked':''}> Spirometrija</label></div>
|
||
</div>
|
||
<div class="fld-row">
|
||
<div class="fld"><label>Iznos ukupno (EUR)</label><input id="f-iznos" type="number" step="0.01" value="${l.iznos||''}"></div>
|
||
<div class="fld"><label>Datum plaćanja</label><input id="f-datum_placanja" type="date" value="${(l.datum_placanja||'').slice(0,10)}"></div>
|
||
</div>
|
||
<div class="fld-row">
|
||
<div class="fld"><label>ZZJZ EUR</label><input id="f-iznos_zzjz" type="number" step="0.01" value="${l.iznos_zzjz||0}"></div>
|
||
<div class="fld"><label>Klub EUR</label><input id="f-iznos_klub" type="number" step="0.01" value="${l.iznos_klub||0}"></div>
|
||
<div class="fld"><label>Član EUR</label><input id="f-iznos_clan" type="number" step="0.01" value="${l.iznos_clan||0}"></div>
|
||
</div>
|
||
<div class="fld"><label>Nalaz</label><textarea id="f-nalaz">${esc(l.nalaz||'')}</textarea></div>
|
||
<div class="fld"><label>Komentar liječnika</label><textarea id="f-komentar_lijecnika">${esc(l.komentar_lijecnika||'')}</textarea></div>
|
||
<div class="fld"><label>Napomena</label><textarea id="f-napomena">${esc(l.napomena||'')}</textarea></div>
|
||
`;
|
||
}
|
||
function readLijecnickiForm() {
|
||
return {
|
||
clan_id: parseInt(document.getElementById('f-clan_id').value)||null,
|
||
klub_id: parseInt(document.getElementById('f-klub_id').value)||null,
|
||
datum_pregleda: document.getElementById('f-datum_pregleda').value||null,
|
||
vrijedi_do: document.getElementById('f-vrijedi_do').value||null,
|
||
vrsta_pregleda: document.getElementById('f-vrsta_pregleda').value||null,
|
||
ustanova: document.getElementById('f-ustanova').value.trim()||null,
|
||
lijecnik: document.getElementById('f-lijecnik').value.trim()||null,
|
||
spreman_za_natjecanje: document.getElementById('f-spreman').checked,
|
||
ekg: document.getElementById('f-ekg').checked,
|
||
krv: document.getElementById('f-krv').checked,
|
||
spirometrija: document.getElementById('f-spirometrija').checked,
|
||
nalaz: document.getElementById('f-nalaz').value||null,
|
||
komentar_lijecnika: document.getElementById('f-komentar_lijecnika').value||null,
|
||
iznos: parseFloat(document.getElementById('f-iznos').value)||null,
|
||
iznos_zzjz: parseFloat(document.getElementById('f-iznos_zzjz').value)||0,
|
||
iznos_klub: parseFloat(document.getElementById('f-iznos_klub').value)||0,
|
||
iznos_clan: parseFloat(document.getElementById('f-iznos_clan').value)||0,
|
||
datum_placanja: document.getElementById('f-datum_placanja').value||null,
|
||
placeno: document.getElementById('f-placeno').checked,
|
||
napomena: document.getElementById('f-napomena').value||null,
|
||
};
|
||
}
|
||
function openLijecnickiModal(l) {
|
||
const isEdit = !!(l && l.id);
|
||
showModal(isEdit?'Uredi liječnički pregled':'Novi liječnički pregled',
|
||
lijecnickiFormHTML(l||{spreman_za_natjecanje:true}),
|
||
async () => {
|
||
const body = readLijecnickiForm();
|
||
if (!body.clan_id || !body.datum_pregleda) { toast('Član i datum su obavezni', 'err'); return; }
|
||
try {
|
||
if (isEdit) await api('/lijecnicki/'+l.id, {method:'PUT', body:JSON.stringify(body)});
|
||
else await api('/lijecnicki', {method:'POST', body:JSON.stringify(body)});
|
||
toast('Spremljeno'); closeModal(); loadLijecnicki();
|
||
} catch (e) { toast('Greška: '+e.message, 'err'); }
|
||
});
|
||
}
|
||
async function editLijecnicki(id) {
|
||
try { const l = await api('/lijecnicki/'+id); openLijecnickiModal(l); }
|
||
catch (e) { toast(e.message, 'err'); }
|
||
}
|
||
async function delLijecnicki(id) {
|
||
if (!confirm('Obrisati pregled #'+id+'?')) return;
|
||
try { await api('/lijecnicki/'+id, {method:'DELETE'}); toast('Obrisano'); loadLijecnicki(); }
|
||
catch (e) { toast(e.message, 'err'); }
|
||
}
|
||
|
||
// ────── Obrasci ──────
|
||
let OBR_TEMPLATES = [];
|
||
let OBR_SELECTED_TPL = null;
|
||
|
||
async function loadObrasciTemplates() {
|
||
try {
|
||
const data = await api('/obrasci');
|
||
OBR_TEMPLATES = data.items||[];
|
||
const list = document.getElementById('obr-tpl-list');
|
||
if (!OBR_TEMPLATES.length) {
|
||
list.innerHTML = '<div class="empty">Nema predložaka.</div>';
|
||
return;
|
||
}
|
||
list.innerHTML = OBR_TEMPLATES.map(t => `
|
||
<div class="tpl-row" data-id="${t.id}" onclick="selectObrasciTpl(${t.id})">
|
||
<div class="tpl-n">${esc(t.naziv)}</div>
|
||
<div class="tpl-c">${esc(t.kategorija||'—')} · ${esc(t.code)}</div>
|
||
</div>
|
||
`).join('');
|
||
document.getElementById('cnt-obrasci').textContent = OBR_TEMPLATES.length;
|
||
} catch (e) { toast('Obrasci err: '+e.message, 'err'); }
|
||
}
|
||
|
||
function selectObrasciTpl(id) {
|
||
OBR_SELECTED_TPL = OBR_TEMPLATES.find(t => t.id === id);
|
||
document.querySelectorAll('#obr-tpl-list .tpl-row').forEach(r =>
|
||
r.classList.toggle('sel', parseInt(r.dataset.id) === id));
|
||
document.getElementById('obr-right-title').textContent =
|
||
'Predložak: ' + (OBR_SELECTED_TPL?.naziv || '');
|
||
openObrasciSubmitModal();
|
||
}
|
||
|
||
function obrasciSubmitFormHTML(t) {
|
||
const sch = (t.schema_json && t.schema_json.fields) || [];
|
||
let inner = '';
|
||
if (sch.length) {
|
||
inner = sch.map((f, i) => {
|
||
const lbl = esc(f.label || f.name || ('Polje '+(i+1)));
|
||
const req = f.required ? '*' : '';
|
||
const name = esc(f.name || ('f'+i));
|
||
const type = f.type || 'text';
|
||
if (type === 'textarea') {
|
||
return `<div class="fld"><label>${lbl}${req}</label><textarea data-fname="${name}"></textarea></div>`;
|
||
}
|
||
if (type === 'select' && Array.isArray(f.options)) {
|
||
return `<div class="fld"><label>${lbl}${req}</label><select data-fname="${name}">
|
||
${f.options.map(o => `<option value="${esc(o)}">${esc(o)}</option>`).join('')}
|
||
</select></div>`;
|
||
}
|
||
const it = (type==='number'?'number': type==='date'?'date': type==='email'?'email':'text');
|
||
return `<div class="fld"><label>${lbl}${req}</label><input data-fname="${name}" type="${it}"></div>`;
|
||
}).join('');
|
||
} else {
|
||
inner = `<div class="fld"><label>Podaci (JSON)</label><textarea data-fname="__json" placeholder='{"polje":"vrijednost"}'></textarea></div>`;
|
||
}
|
||
return `
|
||
<div style="background:var(--bg3); padding:8px 10px; border-radius:4px; margin-bottom:10px; font-size:11px; color:var(--t2);">
|
||
<strong>${esc(t.naziv)}</strong> · ${esc(t.kategorija||'—')}<br>
|
||
${esc(t.opis||'')}
|
||
</div>
|
||
<div class="fld-row">
|
||
<div class="fld"><label>Klub ID</label><input id="f-sub-klub" type="number"></div>
|
||
<div class="fld"><label>Član ID</label><input id="f-sub-clan" type="number"></div>
|
||
</div>
|
||
${inner}
|
||
<div class="fld"><label>Status</label>
|
||
<select id="f-sub-status">
|
||
<option value="draft">draft</option>
|
||
<option value="submitted" selected>submitted</option>
|
||
</select>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function openObrasciSubmitModal() {
|
||
const t = OBR_SELECTED_TPL; if (!t) return;
|
||
showModal('Podnesi: '+t.naziv, obrasciSubmitFormHTML(t), async () => {
|
||
const klub_id = parseInt(document.getElementById('f-sub-klub').value)||null;
|
||
const clan_id = parseInt(document.getElementById('f-sub-clan').value)||null;
|
||
const status = document.getElementById('f-sub-status').value;
|
||
const data = {};
|
||
document.querySelectorAll('#m-body [data-fname]').forEach(el => {
|
||
const k = el.dataset.fname;
|
||
if (k === '__json') {
|
||
try { Object.assign(data, JSON.parse(el.value||'{}')); }
|
||
catch(e) { toast('JSON nije validan', 'err'); throw e; }
|
||
} else {
|
||
data[k] = el.value;
|
||
}
|
||
});
|
||
try {
|
||
await api('/obrasci/submission', {method:'POST', body:JSON.stringify({
|
||
template_id: t.id, template_code: t.code, klub_id, clan_id, data, status
|
||
})});
|
||
toast('Obrazac podnesen'); closeModal(); loadObrasciSubmissions();
|
||
} catch (e) { toast('Greška: '+e.message, 'err'); }
|
||
});
|
||
}
|
||
|
||
async function loadObrasciSubmissions() {
|
||
const st = document.getElementById('obr-status').value;
|
||
const klb = document.getElementById('obr-klub').value.trim();
|
||
const qs = new URLSearchParams();
|
||
if (st) qs.set('status', st);
|
||
if (klb) qs.set('klub_id', klb);
|
||
try {
|
||
const data = await api('/obrasci/submission?'+qs.toString());
|
||
const tb = document.querySelector('#t-obr-sub tbody');
|
||
const admin = isAdminUser();
|
||
tb.innerHTML = (data.items||[]).map(s => `
|
||
<tr onclick="openObrasciSubDetail(${s.id})">
|
||
<td><strong>#${s.id}</strong></td>
|
||
<td>${esc(s.template_naziv||s.template_code||'—')}</td>
|
||
<td>${esc(s.klub_naziv||'—')}</td>
|
||
<td>${esc(s.clan_naziv||'—')}</td>
|
||
<td><span class="chip ${s.status}">${s.status}</span></td>
|
||
<td>${fmtDT(s.submitted_at)}</td>
|
||
<td>${fmtDT(s.approved_at)}</td>
|
||
<td>${admin && (s.status==='submitted'||s.status==='draft') ? `
|
||
<button class="btn sm gold" onclick="event.stopPropagation();subStatus(${s.id},'approved')">✓</button>
|
||
<button class="btn sm danger" onclick="event.stopPropagation();subStatus(${s.id},'rejected')">×</button>
|
||
` : ''}</td>
|
||
</tr>
|
||
`).join('') || '<tr><td colspan="8" class="empty">Nema podnesenih obrazaca.</td></tr>';
|
||
} catch (e) { toast('Submissions err: '+e.message, 'err'); }
|
||
}
|
||
|
||
async function openObrasciSubDetail(id) {
|
||
try {
|
||
const s = await api('/obrasci/submission/'+id);
|
||
const admin = isAdminUser();
|
||
const dataPretty = JSON.stringify(s.data||{}, null, 2);
|
||
const body = `
|
||
<div style="background:var(--bg3); padding:9px 11px; border-radius:4px; margin-bottom:10px; font-size:11px;">
|
||
<div><strong>${esc(s.template_naziv||'—')}</strong> <span style="color:var(--t3)">(${esc(s.template_code||'')})</span></div>
|
||
<div style="margin-top:3px; color:var(--t2);">
|
||
Klub: ${esc(s.klub_naziv||'—')} · Član: ${esc(s.clan_naziv||'—')}<br>
|
||
Submitter: ${esc(s.submitter_email||'—')} · Status: <span class="chip ${s.status}">${s.status}</span>
|
||
</div>
|
||
<div style="margin-top:4px; color:var(--t3); font-family:var(--mono); font-size:10px;">
|
||
submitted: ${fmtDT(s.submitted_at)} · reviewed: ${fmtDT(s.reviewed_at)} · approved: ${fmtDT(s.approved_at)}
|
||
${s.rejected_reason ? '<br>rejected_reason: '+esc(s.rejected_reason) : ''}
|
||
</div>
|
||
</div>
|
||
<div class="fld"><label>Podaci (JSON)</label>
|
||
<textarea readonly style="font-family:var(--mono); min-height:200px;">${esc(dataPretty)}</textarea>
|
||
</div>
|
||
`;
|
||
document.getElementById('m-title').textContent = 'Obrazac #'+s.id;
|
||
document.getElementById('m-body').innerHTML = body;
|
||
const foot = document.getElementById('m-foot');
|
||
foot.innerHTML = `<button class="btn" onclick="closeModal()">Zatvori</button>` +
|
||
(admin && (s.status==='submitted'||s.status==='draft') ? `
|
||
<button class="btn danger" onclick="subStatusFromModal(${s.id},'rejected')">Odbij</button>
|
||
<button class="btn primary" onclick="subStatusFromModal(${s.id},'approved')">Odobri</button>
|
||
` : '') +
|
||
((s.status==='draft' && (CURRENT_USER && (CURRENT_USER.user_id===s.user_id || admin))) ?
|
||
`<button class="btn gold" onclick="subStatusFromModal(${s.id},'submitted')">Pošalji</button>` : '');
|
||
document.getElementById('modal').classList.add('on');
|
||
} catch (e) { toast(e.message, 'err'); }
|
||
}
|
||
|
||
async function subStatusFromModal(id, status) {
|
||
let reason = null;
|
||
if (status === 'rejected') {
|
||
reason = prompt('Razlog odbijanja:'); if (reason === null) return;
|
||
}
|
||
try {
|
||
await api('/obrasci/submission/'+id+'/status', {
|
||
method:'PUT', body:JSON.stringify({status, rejected_reason: reason})
|
||
});
|
||
toast('Status: '+status);
|
||
closeModal();
|
||
// Restore footer
|
||
document.getElementById('m-foot').innerHTML = `
|
||
<button class="btn" onclick="closeModal()">Odustani</button>
|
||
<button class="btn primary" id="m-save">Spremi</button>`;
|
||
loadObrasciSubmissions();
|
||
} catch (e) { toast('Greška: '+e.message, 'err'); }
|
||
}
|
||
|
||
async function subStatus(id, status) {
|
||
let reason = null;
|
||
if (status === 'rejected') {
|
||
reason = prompt('Razlog odbijanja:'); if (reason === null) return;
|
||
}
|
||
try {
|
||
await api('/obrasci/submission/'+id+'/status', {
|
||
method:'PUT', body:JSON.stringify({status, rejected_reason: reason})
|
||
});
|
||
toast('Status: '+status); loadObrasciSubmissions();
|
||
} catch (e) { toast('Greška: '+e.message, 'err'); }
|
||
}
|
||
|
||
// ────── Modal helpers ──────
|
||
function showModal(title, bodyHTML, onSave) {
|
||
document.getElementById('m-title').textContent = title;
|
||
document.getElementById('m-body').innerHTML = bodyHTML;
|
||
// Restore the standard footer (it may have been replaced by detail views)
|
||
document.getElementById('m-foot').innerHTML =
|
||
'<button class="btn" onclick="closeModal()">Odustani</button>' +
|
||
'<button class="btn primary" id="m-save">Spremi</button>';
|
||
document.getElementById('m-save').onclick = onSave;
|
||
document.getElementById('modal').classList.add('on');
|
||
}
|
||
function closeModal() { document.getElementById('modal').classList.remove('on'); }
|
||
document.getElementById('modal').addEventListener('click', e => {
|
||
if (e.target.id === 'modal') closeModal();
|
||
});
|
||
|
||
// ────── Init ──────
|
||
loadMe();
|
||
ensureMe();
|
||
loadPipeline();
|
||
</script>
|
||
</body>
|
||
</html>
|