Servis: custom dropdown za usluge i artikle (naziv+cena), live preračun na javnoj stranici

This commit is contained in:
2026-06-25 02:45:30 +02:00
parent 47ae5e9347
commit d9eeec4c6a
2 changed files with 163 additions and 36 deletions
+123 -28
View File
@@ -11,6 +11,35 @@
.detalji-kartica:nth-child(5) { animation-delay: 0.36s; }
/* bez animacija na detaljima — smetaju pri dodavanju/brisanju stavki */
#sadrzaj .kartica.animiraj { animation: none; }
/* custom dropdown za usluge i artikle */
.cust-sel { position: relative; }
.cust-sel-btn {
display: flex; align-items: center; justify-content: space-between; gap: 8px;
height: 38px; padding: 0 10px; box-sizing: border-box; cursor: pointer;
border: 0.5px solid var(--ivica) !important; border-radius: 8px; font-size: 14px;
background: var(--kartica) !important; color: var(--tekst-glavni) !important;
user-select: none;
}
.cust-sel-btn:focus { border-color: #3b82f6 !important; outline: none; }
.cust-sel-tekst { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cust-sel-chevron { font-size: 10px; color: var(--tekst-sporedni); flex-shrink: 0; transition: transform 0.15s; }
.cust-sel.otvoren .cust-sel-chevron { transform: rotate(180deg); }
.cust-sel-lista {
display: none; position: absolute; top: calc(100% + 3px); left: 0; right: 0; z-index: 500;
background: var(--kartica); border: 0.5px solid var(--ivica); border-radius: 10px;
max-height: 260px; overflow-y: auto; box-shadow: 0 8px 30px rgba(0,0,0,0.4);
}
.cust-sel.otvoren .cust-sel-lista { display: block; }
.cso {
display: flex; align-items: center; justify-content: space-between; gap: 12px;
padding: 8px 12px; cursor: pointer; border-bottom: 0.5px solid var(--ivica); font-size: 13px;
}
.cso:last-child { border-bottom: none; }
.cso:hover, .cso.izabran { background: rgba(255,255,255,0.06); }
.cso-naziv { color: var(--tekst-glavni); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cso-cena { color: #f59e0b; font-weight: 600; white-space: nowrap; flex-shrink: 0; }
.cso-prazno { color: var(--tekst-sporedni); }
.kartica.cust-aktivan { position: relative; z-index: 100; }
</style>
{{end}}
@@ -467,10 +496,17 @@
<div class="detalji-forma-rad">
<div>
<label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">Usluga</label>
<select name="usluga_id" id="rad-usluga" style="width:100%;" required>
<option value="">— odaberi uslugu —</option>
{{range .Usluge}}{{if not (index $.KorisceneUsluge .ID)}}<option value="{{.ID}}" data-cena="{{.Cena}}">{{.Naziv}} — {{dinari .Cena}} din</option>{{end}}{{end}}
</select>
<div class="cust-sel" data-sel="rad-usluga">
<div class="cust-sel-btn" tabindex="0">
<span class="cust-sel-tekst cso-prazno">— odaberi uslugu —</span>
<span class="cust-sel-chevron"></span>
</div>
<div class="cust-sel-lista">
<div class="cso" data-val="" data-cena="0"><span class="cso-prazno">— odaberi uslugu —</span></div>
{{range .Usluge}}{{if not (index $.KorisceneUsluge .ID)}}<div class="cso" data-val="{{.ID}}" data-cena="{{.Cena}}"><span class="cso-naziv">{{.Naziv}}</span><span class="cso-cena">{{dinari .Cena}} din</span></div>{{end}}{{end}}
</div>
</div>
<input type="hidden" name="usluga_id" id="rad-usluga">
</div>
<div>
<label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">Kol.</label>
@@ -581,9 +617,14 @@
<div class="detalji-forma-deo">
<div>
<label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">Artikal</label>
<select name="artikal_id" id="deo-artikal" style="width:100%;" required>
<option value="">— odaberi artikal —</option>
</select>
<div class="cust-sel" id="deo-artikal-cust" data-sel="deo-artikal">
<div class="cust-sel-btn" tabindex="0">
<span class="cust-sel-tekst cso-prazno">— odaberi artikal —</span>
<span class="cust-sel-chevron"></span>
</div>
<div class="cust-sel-lista"></div>
</div>
<input type="hidden" name="artikal_id" id="deo-artikal">
</div>
<div>
<label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">U magacinu</label>
@@ -692,27 +733,30 @@
</div>
</div>
<script>(function(){
window._dinFmt=function(v){var s=Math.round((v||0)*100);var dec=String(s%100).padStart(2,'0');var cel=String(Math.floor(s/100));var r='';for(var i=0;i<cel.length;i++){if(i>0&&(cel.length-i)%3===0)r+='.';r+=cel[i];}return r+','+dec;}
var k=document.getElementById('deo-kategorija');
var a=document.getElementById('deo-artikal');
var u=document.getElementById('deo-umagacinu');
var l=document.getElementById('deo-kol');
var c=document.getElementById('deo-cena');
var st=0;
function osv(){if(!l||!a)return;var o=a.options[a.selectedIndex];var s=o?parseInt(o.getAttribute('data-stanje'),10):0;st=isNaN(s)?0:s;if(u)u.textContent=a.value?st:'—';if(l)l.classList.toggle('prekoraceno',a.value&&st===0)}
function osv(){if(!l||!a)return;var s=parseInt(a.getAttribute('data-stanje'),10);var st=isNaN(s)?0:s;if(u)u.textContent=a.value?st:'\u2014';if(l)l.classList.toggle('prekoraceno',!!a.value&&st===0)}
function pop(){
if(!a)return;
var cust=document.getElementById('deo-artikal-cust');
if(!cust)return;
var lista=cust.querySelector('.cust-sel-lista');
var ka=k?k.value:'';
a.innerHTML='<option value="">— odaberi artikal —</option>';
var aa=window._ntechArtikli||[];
aa.forEach(function(x){if(!ka||(x.KategorijaNaziv||'')===ka)a.innerHTML+='<option value="'+x.ID+'" data-cena="'+x.ProdajnaCena+'" data-stanje="'+(x.Kolicina||0)+'">'+x.Naziv+' — '+parseFloat(x.ProdajnaCena||0).toFixed(2)+' din</option>'})
var html='<div class="cso" data-val="" data-cena="0" data-stanje="0"><span class="cso-prazno">\u2014 odaberi artikal \u2014</span></div>';
aa.forEach(function(x){if(!ka||(x.KategorijaNaziv||'')===ka)html+='<div class="cso" data-val="'+x.ID+'" data-cena="'+x.ProdajnaCena+'" data-stanje="'+(x.Kolicina||0)+'"><span class="cso-naziv">'+x.Naziv+'</span><span class="cso-cena">'+window._dinFmt(x.ProdajnaCena)+' din</span></div>';});
if(lista)lista.innerHTML=html;
}
if(k){var aa2=window._ntechArtikli||[];var kgs=[];aa2.forEach(function(x){var n=x.KategorijaNaziv||'';if(n&&kgs.indexOf(n)===-1)kgs.push(n)})
k.innerHTML='<option value=""> sve kategorije </option>'+kgs.map(function(x){return'<option value="'+x+'">'+x+'</option>'}).join('');k.onchange=pop}
k.innerHTML='<option value="">\u2014 sve kategorije \u2014</option>'+kgs.map(function(x){return'<option value="'+x+'">'+x+'</option>'}).join('');k.onchange=pop}
pop();
if(a)a.onchange=function(){var o=a.options[a.selectedIndex];if(!o)return;var v=parseFloat(o.getAttribute('data-cena'));if(c)c.value=(!isNaN(v)&&v>0)?v.toFixed(2):'0';osv()}
if(a)a.addEventListener('change',function(){var v=parseFloat(a.getAttribute('data-cena'));if(c)c.value=(!isNaN(v)&&v>0)?v.toFixed(2):'0';osv()});
if(l)l.oninput=osv;
var fd=document.querySelector('form[action$=\"/delovi\"]');
if(fd)fd.addEventListener('reset',function(){if(u){u.textContent='';u.classList.remove('prekoraceno')}st=0;if(l)l.classList.remove('prekoraceno')})
if(fd)fd.addEventListener('reset',function(){if(u){u.textContent='\u2014';u.classList.remove('prekoraceno')}if(l)l.classList.remove('prekoraceno')})
})();</script>
</div>
{{end}}
@@ -811,15 +855,63 @@
</div>
<script>
// Radovi: pri izboru usluge iz <select>-a popuni predloženu cenu (data-cena).
// Delegacija na document — kartica radova se osvežava AJAX-om (data-refresh),
// pa listener vezan za sam <select> ne bi preživeo zamenu elementa.
// Custom dropdown: toggle, izbor opcije, zatvaranje klikom van.
function _custZatvoriSve() {
document.querySelectorAll('.cust-sel.otvoren').forEach(function (el) {
el.classList.remove('otvoren');
var k = el.closest('.kartica');
if (k) k.classList.remove('cust-aktivan');
});
}
document.addEventListener('click', function (e) {
var btn = e.target.closest('.cust-sel-btn');
if (btn) {
var cust = btn.closest('.cust-sel');
var bio = cust.classList.contains('otvoren');
_custZatvoriSve();
if (!bio) {
cust.classList.add('otvoren');
var k = cust.closest('.kartica');
if (k) k.classList.add('cust-aktivan');
}
return;
}
var opt = e.target.closest('.cso');
if (opt) {
var cust = opt.closest('.cust-sel');
var selId = cust.getAttribute('data-sel');
var inp = selId ? document.getElementById(selId) : null;
var val = opt.getAttribute('data-val') || '';
var btnEl = cust.querySelector('.cust-sel-btn');
var btnTekst = btnEl ? btnEl.querySelector('.cust-sel-tekst') : null;
var naziv = opt.querySelector('.cso-naziv');
var prazan = opt.querySelector('.cso-prazno');
if (btnTekst) {
btnTekst.textContent = naziv ? naziv.textContent : (prazan ? prazan.textContent : '');
btnTekst.className = 'cust-sel-tekst' + (val ? '' : ' cso-prazno');
}
cust.querySelectorAll('.cso').forEach(function (o) { o.classList.remove('izabran'); });
if (val) opt.classList.add('izabran');
if (inp) {
inp.value = val;
inp.setAttribute('data-cena', opt.getAttribute('data-cena') || '0');
inp.setAttribute('data-stanje', opt.getAttribute('data-stanje') || '0');
inp.dispatchEvent(new Event('change', { bubbles: true }));
}
cust.classList.remove('otvoren');
var k2 = cust.closest('.kartica');
if (k2) k2.classList.remove('cust-aktivan');
return;
}
_custZatvoriSve();
});
// Radovi: pri izboru usluge (custom dropdown → hidden input) popuni predloženu cenu.
document.addEventListener('change', function (e) {
if (!e.target || e.target.id !== 'rad-usluga') return;
var cena = document.getElementById('rad-cena');
if (!cena) return;
var opt = e.target.options[e.target.selectedIndex];
var c = parseFloat(opt.getAttribute('data-cena'));
var c = parseFloat(e.target.getAttribute('data-cena'));
cena.value = (!isNaN(c) && c > 0) ? c.toFixed(2) : '0';
});
@@ -862,12 +954,16 @@ document.addEventListener('change', function (e) {
function popuniArtikle() {
ARTIKLI = window._ntechArtikli || [];
var izabrana = kategSel ? kategSel.value : '';
artikalSel.innerHTML = '<option value="">— odaberi artikal —</option>' +
var cust = document.getElementById('deo-artikal-cust');
var lista = cust ? cust.querySelector('.cust-sel-lista') : null;
var html = '<div class="cso" data-val="" data-cena="0" data-stanje="0"><span class="cso-prazno">— odaberi artikal —</span></div>';
ARTIKLI.filter(function (a) { return !izabrana || (a.KategorijaNaziv || '') === izabrana; })
.map(function (a) {
return '<option value="' + a.ID + '" data-cena="' + a.ProdajnaCena +
'" data-stanje="' + a.Kolicina + '">' + a.Naziv + ' — ' + parseFloat(a.ProdajnaCena||0).toFixed(2) + ' din</option>';
}).join('');
.forEach(function (a) {
html += '<div class="cso" data-val="' + a.ID + '" data-cena="' + a.ProdajnaCena +
'" data-stanje="' + (a.Kolicina || 0) + '"><span class="cso-naziv">' + a.Naziv +
'</span><span class="cso-cena">' + window._dinFmt(a.ProdajnaCena) + ' din</span></div>';
});
if (lista) lista.innerHTML = html;
if (umagacinu) umagacinu.textContent = '—';
}
window._ntechPopuniArtikle = popuniArtikle;
@@ -875,10 +971,9 @@ document.addEventListener('change', function (e) {
if (kategSel) kategSel.addEventListener('change', popuniArtikle);
artikalSel.addEventListener('change', function () {
var opt = artikalSel.options[artikalSel.selectedIndex];
var c = parseFloat(opt.getAttribute('data-cena'));
var c = parseFloat(artikalSel.getAttribute('data-cena'));
if (cena) cena.value = (!isNaN(c) && c > 0) ? c.toFixed(2) : '0';
var st = parseInt(opt.getAttribute('data-stanje'), 10);
var st = parseInt(artikalSel.getAttribute('data-stanje'), 10);
stanjeIzabrano = isNaN(st) ? 0 : st;
if (umagacinu) umagacinu.textContent = artikalSel.value ? stanjeIzabrano : '—';
osveziPrekoracenje();
@@ -243,7 +243,7 @@
{{if .ImaPredlog}}
<div class="cena-blok" style="margin-top:8px;padding:10px 14px;display:flex;justify-content:space-between;align-items:center;background:#0f172a;border:1px solid #f59e0b;border-radius:8px;">
<span class="cena-labela" style="margin:0;color:#fbbf24;">Sa predlogom</span>
<span class="cena-vrednost" style="font-size:16px;font-weight:700;color:#fbbf24;">{{dinari .UkupnoSaPredlogom}} din</span>
<span class="cena-vrednost" id="sa-predlogom-zbir" data-base="{{printf "%.2f" .UkupnoSve}}" style="font-size:16px;font-weight:700;color:#fbbf24;">{{dinari .UkupnoSaPredlogom}} din</span>
</div>
{{end}}
</div>
@@ -267,7 +267,7 @@
{{range .PredlozeniRadovi}}
<label style="display:flex;align-items:center;justify-content:space-between;gap:10px;padding:8px 0;border-bottom:0.5px solid #1e293b;cursor:pointer;">
<span style="display:flex;align-items:center;gap:10px;flex:1;">
<input type="checkbox" name="rad_id" value="{{.ID}}" checked
<input type="checkbox" name="rad_id" value="{{.ID}}" checked data-iznos="{{printf "%.2f" .Ukupno}}"
style="width:18px;height:18px;accent-color:#f59e0b;flex-shrink:0;">
<span style="font-size:13px;color:#e2e8f0;">{{.Naziv}}{{if gt .Kolicina 1.0}} &times; {{printf "%.0f" .Kolicina}}{{end}}</span>
</span>
@@ -281,7 +281,7 @@
{{range .PredlozeniDelovi}}
<label style="display:flex;align-items:center;justify-content:space-between;gap:10px;padding:8px 0;border-bottom:0.5px solid #1e293b;cursor:pointer;">
<span style="display:flex;align-items:center;gap:10px;flex:1;">
<input type="checkbox" name="artikal_id" value="{{.ArtikalID}}" checked
<input type="checkbox" name="artikal_id" value="{{.ArtikalID}}" checked data-iznos="{{printf "%.2f" .Ukupno}}"
style="width:18px;height:18px;accent-color:#f59e0b;flex-shrink:0;">
<span style="font-size:13px;color:#e2e8f0;">{{.ArtikalNaziv}}{{if gt .Kolicina 1}} &times; {{.Kolicina}}{{end}}</span>
</span>
@@ -292,7 +292,7 @@
<div class="cena-blok predlog-ukupno" style="margin-top:12px;display:flex;justify-content:space-between;align-items:center;">
<span class="cena-labela" style="margin:0;">Predlog ukupno</span>
<span class="cena-vrednost">{{dinari .UkupnoPredlog}} din</span>
<span class="cena-vrednost" id="predlog-zbir">{{dinari .UkupnoPredlog}} din</span>
</div>
<textarea name="komentar" placeholder="Poruka serviseru (opciono)..." rows="2"
@@ -320,5 +320,37 @@
{{end}}
</div>
<script>
(function () {
var zbir = document.getElementById('predlog-zbir');
if (!zbir) return;
var forma = zbir.closest('form');
if (!forma) return;
function fmt(v) {
var s = Math.round(v * 100);
var dec = String(s % 100).padStart(2, '0');
var cel = String(Math.floor(s / 100));
var r = '';
for (var i = 0; i < cel.length; i++) {
if (i > 0 && (cel.length - i) % 3 === 0) r += '.';
r += cel[i];
}
return r + ',' + dec + ' din';
}
var saPredlogom = document.getElementById('sa-predlogom-zbir');
var bazaUkupno = saPredlogom ? parseFloat(saPredlogom.getAttribute('data-base')) || 0 : 0;
function preracunaj() {
var suma = 0;
forma.querySelectorAll('input[type="checkbox"][data-iznos]').forEach(function (cb) {
if (cb.checked) suma += parseFloat(cb.getAttribute('data-iznos')) || 0;
});
zbir.textContent = fmt(suma);
if (saPredlogom) saPredlogom.textContent = fmt(bazaUkupno + suma);
}
forma.addEventListener('change', function (e) {
if (e.target && e.target.type === 'checkbox') preracunaj();
});
})();
</script>
</body>
</html>