CRM v2 — Salesforce-Lite
Platform ERP CRM Odjava
Članarine ·
Liječnički ·
Obrasci ·
E-mail templates ·
Accounts ·
Contacts ·
Leads ·
Opportunities ·
Activities ·
Cases ·
Open opps
·
·
Weighted total
·
prosjek vjerojatnosti
Won this quarter
·
·
Leads (new+contacted)
·
qualified: ·
Overdue activities
·
upcoming: ·
Open cases
·
·
Pipeline kanban
TipSubjectAccountKontakt DueStatus
SubjectAccountStatus PriorityStvoren
ČlanKlubGodinaRazdoblje PropisanPlaćenDatum uplateStatus
ČlanKlubDatumVrsta Vrijedi doLiječnikSpremanPlaćeno
Predlošci
Učitavanje…
Podnešeni obrasci
IDPredložakKlubČlan StatusSubmittedApproved

PGŽ Sport CRM — ${tab.toUpperCase()} (${new Date().toLocaleString('hr-HR')})

${headers.map(h=>'').join('')}${rows.map(r=>''+r.map(c=>'').join('')}
'+h+'
'+(c==null?'':String(c).replace(/&/g,'&').replace(/').join('')+'
`; 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/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 => `
${STAGE_LABEL[b.stage]}
${b.count} · ${fmtEur(b.amount_total)}
${(b.items||[]).map(o => `
${esc(o.naziv)}
${esc(o.account_naziv||'—')}
${fmtEur(o.amount_eur)} ${o.probability||0}% · ${fmtDate(o.close_date)}
`).join('')}
`).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 items = data.items||[]; // Card grid (primary) const grid = document.getElementById('acc-cards'); if (grid) { grid.innerHTML = items.map(a => `
${esc(a.naziv)}
${esc(a.type)} ${esc(a.grad||'')}
OIB${esc(a.oib||'—')}
Email${esc(a.email||'—')}
Kontakti / Opps${a.contacts_n||0} / ${a.opps_n||0}
Owner${esc(a.owner_email||'—')}
`).join('') || '
Nema accounta — dodajte prvi.
'; } // Hidden table (compat for legacy code/exports) const tb = document.querySelector('#t-accounts tbody'); if (tb) { tb.innerHTML = items.map(a => ` ${esc(a.naziv)} ${esc(a.type)} ${esc(a.grad||'—')} ${esc(a.oib||'—')} ${esc(a.email||'—')} ${a.contacts_n||0} ${a.opps_n||0} ${esc(a.owner_email||'—')} `).join('') || 'Nema accounta.'; } 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'); } } function accountFormHTML(a={}) { return `
`; } 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 items = data.items||[]; const grid = document.getElementById('con-cards'); if (grid) { grid.innerHTML = items.map(c => `
${esc(c.ime)} ${esc(c.prezime)}
${esc(c.funkcija||'—')} · ${esc(c.account_naziv||'—')}
Email${esc(c.email||'—')}
Telefon${esc(c.telefon||c.mobitel||'—')}
`).join('') || '
Nema kontakata.
'; } const tb = document.querySelector('#t-contacts tbody'); if (tb) { tb.innerHTML = items.map(c => ` ${esc(c.ime)}${esc(c.prezime)} ${esc(c.account_naziv||'—')}${esc(c.funkcija||'—')} ${esc(c.email||'—')}${esc(c.telefon||c.mobitel||'—')} `).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={}) { return `
`; } 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 items = data.items||[]; const grid = document.getElementById('lead-cards'); if (grid) { grid.innerHTML = items.map(l => `
${l.status!=='converted' ? `` : ''}
${esc(l.ime||'')} ${esc(l.prezime||'')}
${l.status} ${esc(l.organizacija||'—')}
Email${esc(l.email||'—')}
Telefon${esc(l.telefon||'—')}
Izvor${esc(l.izvor||'—')}
`).join('') || '
Nema leadova.
'; } const tb = document.querySelector('#t-leads tbody'); if (tb) { tb.innerHTML = items.map(l => ` ${esc(l.ime||'—')}${esc(l.prezime||'—')} ${esc(l.organizacija||'—')}${esc(l.email||'—')} ${esc(l.izvor||'—')}${l.status} `).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={}) { return `
`; } 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 = `
`; 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 items = data.items||[]; const grid = document.getElementById('opp-cards'); if (grid) { grid.innerHTML = items.map(o => `
${esc(o.naziv)}
${esc(o.account_naziv||'—')} · ${esc(o.stage)}
Iznos${fmtEur(o.amount_eur)}
Vjerojatnost${o.probability||0}%
Close${fmtDate(o.close_date)}
Tip${esc(o.type||'—')}
`).join('') || '
Nema prilika.
'; } const tb = document.querySelector('#t-opps tbody'); if (tb) { tb.innerHTML = items.map(o => ` ${esc(o.naziv)}${esc(o.account_naziv||'—')} ${esc(o.type||'—')}${esc(o.stage)} ${fmtEur(o.amount_eur)}${o.probability||0}% ${fmtDate(o.close_date)} `).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={}) { return `
`; } 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 => ` ${esc(a.type)} ${esc(a.subject)} ${esc(a.account_naziv||'—')} ${esc(a.contact_naziv||'—')} ${fmtDT(a.due_at)} ${a.completed_at ? 'done' : 'open'} ${!a.completed_at ? `` : ''} `).join('') || 'Nema aktivnosti.'; 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'); } } function activityFormHTML(a={}) { return `
`; } 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 => ` ${esc(c.subject)} ${esc(c.account_naziv||'—')} ${c.status} ${c.priority} ${fmtDT(c.created_at)} `).join('') || 'Nema caseova.'; 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'); } } function caseFormHTML(k={}) { return `
`; } 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 => ` ${esc(c.clan_naziv||'—')} #${c.clan_id||''} ${esc(c.klub_naziv||'—')} ${c.godina} ${esc(c.razdoblje||'—')} ${fmtEur(c.iznos_propisan)} ${fmtEur(c.iznos_placen)} ${fmtDate(c.datum_uplate)} ${c.status} ${isAdminUser() ? `` : ''} `).join('') || 'Nema članarina za odabrane filtere.'; 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'); } } function clanarinaFormHTML(c={}) { return `
`; } 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 ` ${esc(l.clan_naziv||'—')} #${l.clan_id||''} ${esc(l.klub_naziv||'—')} ${fmtDate(l.datum_pregleda)} ${esc(l.vrsta_pregleda||'—')} ${fmtDate(l.vrijedi_do)} ${expired?'istekao':''} ${esc(l.lijecnik||'—')} ${l.spreman_za_natjecanje ? 'DA' : 'NE'} ${l.placeno ? 'DA' : 'NE'} ${isAdminUser() ? `` : ''} `; }).join('') || 'Nema liječničkih pregleda.'; 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'); } } function lijecnickiFormHTML(l={}) { return `
`; } 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 = '
Nema predložaka.
'; return; } list.innerHTML = OBR_TEMPLATES.map(t => `
${esc(t.naziv)}
${esc(t.kategorija||'—')} · ${esc(t.code)}
`).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 `
`; } if (type === 'select' && Array.isArray(f.options)) { return `
`; } const it = (type==='number'?'number': type==='date'?'date': type==='email'?'email':'text'); return `
`; }).join(''); } else { inner = `
`; } return `
${esc(t.naziv)} · ${esc(t.kategorija||'—')}
${esc(t.opis||'')}
${inner}
`; } 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 => ` #${s.id} ${esc(s.template_naziv||s.template_code||'—')} ${esc(s.klub_naziv||'—')} ${esc(s.clan_naziv||'—')} ${s.status} ${fmtDT(s.submitted_at)} ${fmtDT(s.approved_at)} ${admin && (s.status==='submitted'||s.status==='draft') ? ` ` : ''} `).join('') || 'Nema podnesenih obrazaca.'; 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'); } } 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 = `
${esc(s.template_naziv||'—')} (${esc(s.template_code||'')})
Klub: ${esc(s.klub_naziv||'—')} · Član: ${esc(s.clan_naziv||'—')}
Submitter: ${esc(s.submitter_email||'—')} · Status: ${s.status}
submitted: ${fmtDT(s.submitted_at)} · reviewed: ${fmtDT(s.reviewed_at)} · approved: ${fmtDT(s.approved_at)} ${s.rejected_reason ? '
rejected_reason: '+esc(s.rejected_reason) : ''}
`; document.getElementById('m-title').textContent = 'Obrazac #'+s.id; document.getElementById('m-body').innerHTML = body; const foot = document.getElementById('m-foot'); foot.innerHTML = `` + (admin && (s.status==='submitted'||s.status==='draft') ? ` ` : '') + ((s.status==='draft' && (CURRENT_USER && (CURRENT_USER.user_id===s.user_id || admin))) ? `` : ''); 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 = ` `; 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'); } } // ══════════════════════════════════════════════════════════════════ // 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 => `
${esc(t.code)}
${esc(t.naziv)} ${t.active===false?'neaktivan':''}
${esc(t.kategorija||'—')}
Subject: ${esc((t.subject_tpl||'').slice(0,90))}${(t.subject_tpl||'').length>90?'…':''}
${esc((t.body_tpl||'').replace(/\s+/g,' ').slice(0,140))}${(t.body_tpl||'').length>140?'…':''}
`).join('') || '
Nema predložaka.
'; 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 `
`; } 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; document.getElementById('m-body').innerHTML = bodyHTML; // Restore the standard footer (it may have been replaced by detail views) document.getElementById('m-foot').innerHTML = '' + ''; 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(); }); // ────── OCR (lightweight /api/ocr) ────── const OCR_API = '/sport/api/ocr'; function ocrOpen(){ document.getElementById('ocr-modal').style.display = 'flex'; } function ocrClose(){ document.getElementById('ocr-modal').style.display = 'none'; } async function ocrCrmHealth(){ const out = document.getElementById('ocr-crm-health'); if(out) out.textContent = '...checking'; try { const r = await fetch(OCR_API + '/health'); const j = await r.json(); if(out){ out.textContent = 'tesseract: ' + (j.tesseract_available ? 'OK' : 'NO') + ' · pdf2image: ' + (j.pdf2image_available ? 'OK' : 'NO'); } } catch(e){ if(out) out.textContent = 'health err: ' + (e && e.message || e); } } async function ocrCrmUpload(){ const f = document.getElementById('ocr-crm-file').files[0]; const stat = document.getElementById('ocr-crm-status'); const fields = document.getElementById('ocr-crm-fields'); const txt = document.getElementById('ocr-crm-text'); if(!f){ if(stat) stat.textContent = 'odaberi datoteku'; return; } if(stat) stat.textContent = 'uploading…'; const fd = new FormData(); fd.append('file', f); try { const r = await fetch(OCR_API + '/upload', { method: 'POST', body: fd }); const j = await r.json(); if(!r.ok){ if(stat) stat.textContent = 'err ' + r.status; return; } const ex = j.extracted || {}; fields.innerHTML = '' + '' + '' + '' + '' + '' + '' + '' + '' + '
vendor'+(ex.vendor||'—')+'
OIB'+(ex.oib||'—')+'
invoice_no'+(ex.invoice_no||'—')+'
date'+(ex.date||'—')+'
amount'+(ex.amount==null?'—':ex.amount)+'
ocr_status'+(j.ocr_status||'—')+'
confidence'+(j.ocr_confidence==null?'—':j.ocr_confidence)+'
file'+((j.file_name||'?')+' · '+(j.file_size||0)+' B')+'
'; txt.textContent = j.ocr_text || '— (prazno / OCR nije izvršen) —'; if(stat) stat.textContent = 'done · id=' + (j.id == null ? 'n/a' : j.id); } catch(e){ if(stat) stat.textContent = 'err: ' + (e && e.message || e); } } // ────── Init ────── loadMe(); ensureMe(); 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'); });