Servis detalji: glatko osvežavanje bez treperenja i skoka

Sve forme na detaljima (dodavanje/brisanje rada i dela, promena statusa) sada
osvežavaju ceo #glavni-sadrzaj preko data-refresh umesto punog reloda: status
badge se ažurira odmah, nema skoka na vrh ni treptaja pozadine. AJAX grana dodaje
bez-anim reflow (CSS #glavni-sadrzaj.bez-anim) i re-izvršava inline skripte da se
combobox pretrage ponovo povežu nakon zamene.
This commit is contained in:
2026-06-22 00:42:59 +02:00
parent 1637f65f95
commit 4a9cdb3631
3 changed files with 103 additions and 13 deletions
+8
View File
@@ -1100,6 +1100,14 @@ select {
#dobavljaci-rezultati.bez-anim .tabela tbody tr, #dobavljaci-rezultati.bez-anim .tabela tbody tr,
#dobavljaci-rezultati.bez-anim .animiraj { animation: none; } #dobavljaci-rezultati.bez-anim .animiraj { animation: none; }
/* osvežavanje dela stranice preko data-refresh (npr. detalji naloga) ne sme da
re-animira kartice — sprečava treptaj pri dodavanju/brisanju i promeni statusa */
#glavni-sadrzaj.bez-anim .tabela tbody tr,
#glavni-sadrzaj.bez-anim .animiraj,
.bez-anim.kartica,
.bez-anim .tabela tbody tr,
.bez-anim .animiraj { animation: none; }
/* korisnikova preferencija animacije: body[data-animacija] nadjačava podrazumevano. /* korisnikova preferencija animacije: body[data-animacija] nadjačava podrazumevano.
Kad korisnik odabere stil, animiraju se i redovi tabela i mobilne kartice. */ Kad korisnik odabere stil, animiraju se i redovi tabela i mobilne kartice. */
[data-animacija="bez"] .animiraj, [data-animacija="bez"] .animiraj,
+81 -11
View File
@@ -45,7 +45,7 @@
</span> </span>
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;"> <div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">
{{template "status-badge-detalji" .Nalog.Status}} {{template "status-badge-detalji" .Nalog.Status}}
<form method="post" action="/servis/{{.Nalog.ID}}/status" style="display:flex;align-items:center;gap:6px;" data-full-reload> <form method="post" action="/servis/{{.Nalog.ID}}/status" style="display:flex;align-items:center;gap:6px;" data-refresh="#glavni-sadrzaj">
<input type="hidden" name="_csrf" value="{{.CsrfToken}}"> <input type="hidden" name="_csrf" value="{{.CsrfToken}}">
<select name="status" style="font-size:13px;padding:4px 8px;border-radius:6px;border:0.5px solid var(--ivica);background:var(--kartica-pozadina);color:var(--tekst-glavni);cursor:pointer;"> <select name="status" style="font-size:13px;padding:4px 8px;border-radius:6px;border:0.5px solid var(--ivica);background:var(--kartica-pozadina);color:var(--tekst-glavni);cursor:pointer;">
{{range .SviStatusi}} {{range .SviStatusi}}
@@ -168,7 +168,7 @@
<!-- forma za dodavanje rada iz cenovnika usluga --> <!-- forma za dodavanje rada iz cenovnika usluga -->
{{if index .Dozvole "servis.izmeni"}} {{if index .Dozvole "servis.izmeni"}}
<form method="POST" action="/servis/{{.Nalog.ID}}/radovi" style="margin-bottom:16px;" data-refresh="#radovi-spisak"> <form method="POST" action="/servis/{{.Nalog.ID}}/radovi" style="margin-bottom:16px;" data-refresh="#glavni-sadrzaj">
<input type="hidden" name="_csrf" value="{{.CsrfToken}}"> <input type="hidden" name="_csrf" value="{{.CsrfToken}}">
<div style="display:grid;grid-template-columns:1fr 80px 120px auto;gap:10px;align-items:flex-end;"> <div style="display:grid;grid-template-columns:1fr 80px 120px auto;gap:10px;align-items:flex-end;">
<div> <div>
@@ -217,7 +217,7 @@
<td style="padding:9px 10px;text-align:right;font-size:14px;font-weight:500;color:var(--tekst-glavni);">{{dinari .Ukupno}} din</td> <td style="padding:9px 10px;text-align:right;font-size:14px;font-weight:500;color:var(--tekst-glavni);">{{dinari .Ukupno}} din</td>
<td style="padding:9px 10px;text-align:center;"> <td style="padding:9px 10px;text-align:center;">
{{if index $.Dozvole "servis.izmeni"}} {{if index $.Dozvole "servis.izmeni"}}
<form method="POST" action="/servis/{{$.Nalog.ID}}/radovi/{{.ID}}/obrisi" style="display:inline;" data-refresh="#radovi-spisak"> <form method="POST" action="/servis/{{$.Nalog.ID}}/radovi/{{.ID}}/obrisi" style="display:inline;" data-refresh="#glavni-sadrzaj">
<input type="hidden" name="_csrf" value="{{$.CsrfToken}}"> <input type="hidden" name="_csrf" value="{{$.CsrfToken}}">
<button type="submit" <button type="submit"
data-potvrda="Ukloniti rad '{{.Naziv}}'?" data-potvrda="Ukloniti rad '{{.Naziv}}'?"
@@ -247,21 +247,27 @@
<!-- forma za dodavanje dela --> <!-- forma za dodavanje dela -->
{{if index .Dozvole "servis.izmeni"}} {{if index .Dozvole "servis.izmeni"}}
<form method="POST" action="/servis/{{.Nalog.ID}}/delovi" style="margin-bottom:16px;" data-refresh="#delovi-spisak"> <form method="POST" action="/servis/{{.Nalog.ID}}/delovi" style="margin-bottom:16px;" data-refresh="#glavni-sadrzaj">
<input type="hidden" name="_csrf" value="{{.CsrfToken}}"> <input type="hidden" name="_csrf" value="{{.CsrfToken}}">
<div style="display:grid;grid-template-columns:1fr 80px 120px auto;gap:10px;align-items:flex-end;"> <div style="margin-bottom:10px;">
<label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">Kategorija</label>
<select id="deo-kategorija" style="width:100%;"></select>
</div>
<div style="display:grid;grid-template-columns:1fr 70px 80px 120px auto;gap:10px;align-items:flex-end;">
<div> <div>
<label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">Artikal</label> <label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">Artikal</label>
<input type="text" id="deo-artikal-pretraga" list="artikli-datalist" autocomplete="off" <input type="text" id="deo-artikal-pretraga" list="artikli-datalist" autocomplete="off"
placeholder="Pretraži artikal..." style="width:100%;"> placeholder="Pretraži artikal..." style="width:100%;">
<datalist id="artikli-datalist"> <datalist id="artikli-datalist"></datalist>
{{range .Artikli}}<option value="{{.Naziv}} ({{.Kolicina}} kom)" data-id="{{.ID}}" data-cena="{{.ProdajnaCena}}">{{end}}
</datalist>
<input type="hidden" name="artikal_id" id="deo-artikal"> <input type="hidden" name="artikal_id" id="deo-artikal">
</div> </div>
<div>
<label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">U magacinu</label>
<div id="deo-umagacinu" style="font-size:14px;font-weight:500;color:var(--tekst-glavni);text-align:center;padding:8px 0;"></div>
</div>
<div> <div>
<label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">Kol.</label> <label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">Kol.</label>
<input type="number" name="kolicina" min="1" value="1" style="width:100%;" required> <input type="number" name="kolicina" id="deo-kol" min="1" value="1" style="width:100%;" required>
</div> </div>
<div> <div>
<label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">Cena/kom (din)</label> <label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">Cena/kom (din)</label>
@@ -296,7 +302,7 @@
<td style="padding:9px 10px;text-align:right;font-size:14px;font-weight:500;color:var(--tekst-glavni);">{{dinari .Ukupno}} din</td> <td style="padding:9px 10px;text-align:right;font-size:14px;font-weight:500;color:var(--tekst-glavni);">{{dinari .Ukupno}} din</td>
<td style="padding:9px 10px;text-align:center;"> <td style="padding:9px 10px;text-align:center;">
{{if index $.Dozvole "servis.izmeni"}} {{if index $.Dozvole "servis.izmeni"}}
<form method="POST" action="/servis/{{$.Nalog.ID}}/delovi/{{.ID}}/obrisi" style="display:inline;" data-refresh="#delovi-spisak"> <form method="POST" action="/servis/{{$.Nalog.ID}}/delovi/{{.ID}}/obrisi" style="display:inline;" data-refresh="#glavni-sadrzaj">
<input type="hidden" name="_csrf" value="{{$.CsrfToken}}"> <input type="hidden" name="_csrf" value="{{$.CsrfToken}}">
<button type="submit" <button type="submit"
data-potvrda="Ukloniti deo '{{.ArtikalNaziv}}'?" data-potvrda="Ukloniti deo '{{.ArtikalNaziv}}'?"
@@ -368,6 +374,70 @@ function vežiCombobox(pretragaId, datalistId, hiddenId, cenaId) {
pretraga.addEventListener('change', uskladi); pretraga.addEventListener('change', uskladi);
} }
vežiCombobox('rad-usluga-pretraga', 'usluge-datalist', 'rad-usluga', 'rad-cena'); vežiCombobox('rad-usluga-pretraga', 'usluge-datalist', 'rad-usluga', 'rad-cena');
vežiCombobox('deo-artikal-pretraga', 'artikli-datalist', 'deo-artikal', 'deo-cena');
// Delovi: kategorija filter + ograničenje količine na magacinsko stanje.
// Datalist se gradi iz JSON-a po izabranoj kategoriji; pri izboru artikla
// postavljamo cenu, max količine = stanje, i prikaz dostupne količine.
(function () {
var ARTIKLI = {{json .Artikli}} || [];
var kategSel = document.getElementById('deo-kategorija');
var pretraga = document.getElementById('deo-artikal-pretraga');
var datalist = document.getElementById('artikli-datalist');
var hidden = document.getElementById('deo-artikal');
var cena = document.getElementById('deo-cena');
var umagacinu = document.getElementById('deo-umagacinu');
if (!pretraga || !datalist) return;
function labela(a) { return a.Naziv + ' (' + a.Kolicina + ' kom)'; }
if (kategSel) {
var kategs = [];
ARTIKLI.forEach(function (a) {
var k = a.KategorijaNaziv || '';
if (k && kategs.indexOf(k) < 0) kategs.push(k);
});
kategs.sort();
kategSel.innerHTML = '<option value="">Sve kategorije</option>' +
kategs.map(function (k) { return '<option value="' + k + '">' + k + '</option>'; }).join('');
}
function popuniDatalist() {
var izabrana = kategSel ? kategSel.value : '';
datalist.innerHTML = ARTIKLI
.filter(function (a) { return !izabrana || (a.KategorijaNaziv || '') === izabrana; })
.map(function (a) {
return '<option value="' + labela(a) + '" data-id="' + a.ID +
'" data-cena="' + a.ProdajnaCena + '" data-stanje="' + a.Kolicina + '">';
}).join('');
}
popuniDatalist();
if (kategSel) kategSel.addEventListener('change', function () {
popuniDatalist();
pretraga.value = '';
hidden.value = '';
if (umagacinu) umagacinu.textContent = '—';
});
function uskladi() {
var opt = null, opts = datalist.options;
for (var i = 0; i < opts.length; i++) {
if (opts[i].value === pretraga.value) { opt = opts[i]; break; }
}
if (opt) {
hidden.value = opt.dataset.id || '';
if (cena) {
var c = parseFloat(opt.dataset.cena);
cena.value = (!isNaN(c) && c > 0) ? c.toFixed(2) : '0';
}
if (umagacinu) umagacinu.textContent = (parseInt(opt.dataset.stanje, 10) || 0);
} else {
hidden.value = '';
if (umagacinu) umagacinu.textContent = '—';
}
}
pretraga.addEventListener('input', uskladi);
pretraga.addEventListener('change', uskladi);
})();
</script> </script>
{{end}} {{end}}
+14 -2
View File
@@ -393,8 +393,20 @@
var doc = new DOMParser().parseFromString(html, 'text/html'); var doc = new DOMParser().parseFromString(html, 'text/html');
var novi = doc.querySelector(ciljSel); var novi = doc.querySelector(ciljSel);
var stari = document.querySelector(ciljSel); var stari = document.querySelector(ciljSel);
if (novi && stari) stari.replaceWith(novi); if (novi && stari) {
f.reset(); // bez-anim spречava treptaj re-animacije kartica pri zameni
novi.classList.add('bez-anim');
stari.replaceWith(novi);
void novi.offsetHeight;
novi.classList.remove('bez-anim');
// replaceWith ne pokreće <script> iz fragmenta — re-izvrši ih
// ručno (npr. da se combobox/pretrage ponovo povežu)
novi.querySelectorAll('script').forEach(function (old) {
var s = document.createElement('script');
if (old.src) s.src = old.src; else s.textContent = old.textContent;
old.parentNode.replaceChild(s, old);
});
}
if (btn) btn.disabled = false; if (btn) btn.disabled = false;
}) })
.catch(function () { location.href = res.url; }); .catch(function () { location.href = res.url; });