Files
pgz-sport/static/js/export_dropdown.js
T
Damir Radulić 9b0ed43b92 RUSH 4-sub: filteri Klubovi/Sportaši + manifestacije card view + CRM v2 redesign
RUSH-1 Klubovi: list_klubovi() LEFT JOIN v_klubovi_financiranje (prima_pgz/rss/grad_rijeka, u_godisnjaku, ukupno_potpora). financiran=true sad OR od 3 davatelja (drop legacy klubovi.pgz_sufinanciran s 1312 false-positive). Sort sort=potpora&order=desc. UI: gold ukupno_potpora + tooltip + sortable kolona. Defaults priority view (financirani+godišnjak ON, hns_roster OFF). Test: priority=604, +hns=36, all=1641, financiran=15 sorted ZAMET 80208€.

RUSH-2 Sportaši: SELECT widened (slika_url, reprezentativac, kategoriziran, broj_dresa). avatarUrl() helper s 3 forme (apsolutni / lokalni /sport/uploads/avatars / initials fallback) + 32px circular avatar lijevo od imena. Test: priority=3712, no-priority=6086, +hns=1439, 1990-2000=645.

RUSH-3 Manifestacije: bugfix razina filter HTTP 500 (ambiguous column nakon LEFT JOIN savezi → m.razina/mjesto/organizator). 3 dropdowna iz meta (26 mjesta / 8 razina / 50 organizatora), view toggle 🃏 Kartice / 📋 Tablica (localStorage), 🔗 link ikona u card+table, source_url → Google fallback. Test: default=3, mjesto=Lošinj=2, razina=Tradicionalna=3, organizator=AK Kvarner=1.

RUSH-4 CRM v2: tab strip rewrite (10 taba u spec redu Članarine|Liječnički|Obrasci|E-mail|Accounts|Contacts|Leads|Opps|Activities|Cases, sticky+scrollable+gold underline). Pipeline → Opps tab. Novi e-mail templates tab (5 endpointa, 3 seed templates, +Novi modal). Card layout (.cgrid/.ccard) za Accounts/Contacts/Leads/Opps. Export dropdown 📥 ▾ CSV/XLSX(SheetJS CDN)/PDF na svaki tab. Test: /crm_v2 200, 10/10 tab labela, 10 Export dropdowna + 31 exportTab() handlera.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 18:33:20 +02:00

182 lines
7.7 KiB
JavaScript

/* ═══════════════════════════════════════════════════════════════════════
* Fajl: static/js/export_dropdown.js | v1.0.0 | 05.05.2026
* Autor: Damir Radulić <dradulic@outlook.com> / damir@rinet.one
* Lokacija: /opt/pgz-sport/static/js/export_dropdown.js
* Svrha: Shared "Export ▾" dropdown — CSV / XLSX / PDF — za sve tablice u
* sport2.html, app.html, crm_v2.html, erp_full.html. Iza scene
* koristi /api/v2/export?format=...&endpoint=... s autoriziranim
* Bearer tokenom iz localStorage / sessionStorage.
* Public API:
* window.attachExportDropdown(buttonEl, endpointFn, filenameBase)
* - buttonEl: <button> element trigger
* - endpointFn: function returning the current endpoint+querystring
* each time it's called (so filters stay live)
* - filenameBase: short name for downloaded file (e.g. 'klubovi')
* ═══════════════════════════════════════════════════════════════════════ */
(function () {
'use strict';
if (window.attachExportDropdown) return; // idempotent
// ── inject minimal Palantir-ish CSS once ──────────────────────────────
var STYLE_ID = 'pgz-export-dropdown-style';
if (!document.getElementById(STYLE_ID)) {
var st = document.createElement('style');
st.id = STYLE_ID;
st.textContent = [
'.export-btn{background:var(--bg3,#1a1d24);border:1px solid var(--rim,#2a2e38);',
' color:var(--t1,#e0e3e9);padding:6px 11px;border-radius:5px;font-size:11px;',
' cursor:pointer;font-family:inherit;letter-spacing:.3px}',
'.export-btn:hover{border-color:var(--pgz-gold,#d4a849);color:var(--pgz-gold,#d4a849)}',
'.pgz-exp-wrap{position:relative;display:inline-block}',
'.pgz-exp-menu{position:absolute;right:0;top:calc(100% + 4px);min-width:130px;',
' background:var(--bg2,#15181f);border:1px solid var(--rim,#2a2e38);border-radius:6px;',
' box-shadow:0 6px 20px rgba(0,0,0,.45);padding:4px;z-index:9999;display:none;',
' font-family:ui-monospace,Menlo,Consolas,monospace}',
'.pgz-exp-menu.on{display:block}',
'.pgz-exp-menu button{display:block;width:100%;text-align:left;background:transparent;',
' border:0;color:var(--t1,#e0e3e9);padding:7px 11px;font-size:11px;cursor:pointer;',
' border-radius:4px;font-family:inherit;letter-spacing:.4px}',
'.pgz-exp-menu button:hover{background:var(--bg3,#1a1d24);color:var(--pgz-gold,#d4a849)}',
'.pgz-exp-menu .sep{border-top:1px solid var(--rim,#2a2e38);margin:3px 0}'
].join('\n');
document.head.appendChild(st);
}
// Close menus on outside click.
document.addEventListener('click', function (ev) {
var menus = document.querySelectorAll('.pgz-exp-menu.on');
menus.forEach(function (m) {
if (!m.contains(ev.target) && !(m.__trigger && m.__trigger.contains(ev.target))) {
m.classList.remove('on');
}
});
});
function _token() {
return (
localStorage.getItem('pgz_access') ||
sessionStorage.getItem('pgz_access') ||
localStorage.getItem('access_token') ||
sessionStorage.getItem('access_token') ||
''
);
}
function _resolveEndpoint(endpointFn) {
try {
var ep = (typeof endpointFn === 'function') ? endpointFn() : String(endpointFn || '');
if (!ep) return null;
// Normalize: must start with / so the server proxies to localhost.
if (!/^https?:\/\//i.test(ep) && !ep.startsWith('/')) ep = '/' + ep;
return ep;
} catch (e) {
console.error('[export] endpointFn threw', e);
return null;
}
}
function _trigger(format, endpointFn, filenameBase) {
var ep = _resolveEndpoint(endpointFn);
if (!ep) {
alert('Export: endpoint nije dostupan.');
return;
}
var url = '/api/v2/export?format=' + encodeURIComponent(format) +
'&endpoint=' + encodeURIComponent(ep) +
'&filename=' + encodeURIComponent(filenameBase || 'export');
var tok = _token();
if (format === 'pdf') {
// PDF mock = HTML page. Open in a new tab; the page has a Print button.
// If we have a token, push it as a hash so the user stays logged in
// when they re-fetch (server still validates from the original GET).
var w = window.open('', '_blank');
if (!w) { alert('Pop-up blocked — dopusti pop-up za export.'); return; }
// Use fetch with auth, then write to the new window.
fetch(url, { headers: tok ? { 'Authorization': 'Bearer ' + tok } : {} })
.then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.text(); })
.then(function (html) { w.document.open(); w.document.write(html); w.document.close(); })
.catch(function (e) {
w.document.body.innerText = 'Export greška: ' + e.message;
});
return;
}
// csv / xlsx — fetch as blob, force download via hidden <a>.
fetch(url, { headers: tok ? { 'Authorization': 'Bearer ' + tok } : {} })
.then(function (r) {
if (!r.ok) throw new Error('HTTP ' + r.status);
var dispo = r.headers.get('Content-Disposition') || '';
var m = dispo.match(/filename="?([^"]+)"?/);
var fname = m ? m[1] : (filenameBase || 'export') + '.' + format;
return r.blob().then(function (b) { return { blob: b, fname: fname }; });
})
.then(function (o) {
var burl = URL.createObjectURL(o.blob);
var a = document.createElement('a');
a.href = burl;
a.download = o.fname;
document.body.appendChild(a);
a.click();
setTimeout(function () {
document.body.removeChild(a);
URL.revokeObjectURL(burl);
}, 200);
})
.catch(function (e) {
alert('Export greška: ' + e.message);
console.error('[export]', e);
});
}
function attachExportDropdown(btn, endpointFn, filenameBase) {
if (!btn) return;
if (btn.__pgzExpAttached) return;
btn.__pgzExpAttached = true;
// Wrap in a positioned container so the menu floats correctly.
var wrap;
if (btn.parentElement && btn.parentElement.classList.contains('pgz-exp-wrap')) {
wrap = btn.parentElement;
} else {
wrap = document.createElement('span');
wrap.className = 'pgz-exp-wrap';
btn.parentNode.insertBefore(wrap, btn);
wrap.appendChild(btn);
}
if (!btn.classList.contains('export-btn')) btn.classList.add('export-btn');
if (!/▾|▼/.test(btn.textContent)) btn.textContent = (btn.textContent || 'Export') + ' ▾';
var menu = document.createElement('div');
menu.className = 'pgz-exp-menu';
menu.innerHTML = [
'<button data-fmt="csv">CSV (HR Excel)</button>',
'<button data-fmt="xlsx">XLSX</button>',
'<div class="sep"></div>',
'<button data-fmt="pdf">PDF (print)</button>'
].join('');
wrap.appendChild(menu);
menu.__trigger = btn;
btn.addEventListener('click', function (ev) {
ev.stopPropagation();
// Close other open menus first.
document.querySelectorAll('.pgz-exp-menu.on').forEach(function (m) {
if (m !== menu) m.classList.remove('on');
});
menu.classList.toggle('on');
});
menu.addEventListener('click', function (ev) {
var t = ev.target.closest('button[data-fmt]');
if (!t) return;
ev.stopPropagation();
menu.classList.remove('on');
_trigger(t.getAttribute('data-fmt'), endpointFn, filenameBase);
});
}
window.attachExportDropdown = attachExportDropdown;
})();