Servis: pregled troškova, auto-cena delova, modalni prozor za potvrdu

- Detalji naloga prikazuju cenu usluge, ugrađene delove, ukupno i za naplatu kao zasebne stavke
- Otpremnica uključuje stavku ugrađenih delova u obračun
- Biranje artikla u formi za delove automatski popunjava cenu po komadu
- Zamenjen confirm() sa prilagođenim modalnim prozorom za sve potvrde
This commit is contained in:
2026-06-20 00:40:29 +02:00
parent 86cbace213
commit 5f017fd7ed
4 changed files with 135 additions and 11 deletions
+37
View File
@@ -46,6 +46,9 @@ type PodaciDetaljiNaloga struct {
ServisniDelovi []model.ServisniDeoSaArtiklom
Artikli []model.ArtikalSaKategorijom
Sacuvano bool
UkupnoDelovi float64
UkupnoSve float64
PreostaloSve float64
}
// Servis renderuje listu servisnih naloga sa opcionom pretragom i filterom statusa
@@ -355,6 +358,23 @@ func (h *Handler) DetaljiNaloga(w http.ResponseWriter, r *http.Request) {
slog.Error("greška pri učitavanju artikala", "error", err)
}
var ukupnoDelovi float64
for _, d := range delovi {
ukupnoDelovi += d.Ukupno()
}
var ukupnoSve, preostaloSve float64
if nalog.CenaKonacna != nil {
ukupnoSve = *nalog.CenaKonacna + ukupnoDelovi
avans := 0.0
if nalog.Avans != nil {
avans = *nalog.Avans
}
preostaloSve = ukupnoSve - avans
if preostaloSve < 0 {
preostaloSve = 0
}
}
ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "servis"
ps.NaslovStranice = "Detalji naloga"
@@ -366,6 +386,9 @@ func (h *Handler) DetaljiNaloga(w http.ResponseWriter, r *http.Request) {
ServisniDelovi: delovi,
Artikli: artikli,
Sacuvano: r.URL.Query().Get("sacuvano") == "1",
UkupnoDelovi: ukupnoDelovi,
UkupnoSve: ukupnoSve,
PreostaloSve: preostaloSve,
}
h.renderujTemplate(w, "servis_detalji", podaci)
@@ -638,6 +661,7 @@ type PodaciOtpremnice struct {
Nalog model.ServisniNalog
ServisniDelovi []model.ServisniDeoSaArtiklom
UkupnoDelovi float64
PreostaloSve float64
Klijent *model.Klijent
KlijentNaziv string
TehnicarNaziv string
@@ -700,11 +724,24 @@ func (h *Handler) StampaOtpremnice(w http.ResponseWriter, r *http.Request) {
for _, d := range delovi {
ukupnoDelovi += d.Ukupno()
}
var preostaloSve float64
if nalog.CenaKonacna != nil {
ukupnoSve := *nalog.CenaKonacna + ukupnoDelovi
avans := 0.0
if nalog.Avans != nil {
avans = *nalog.Avans
}
preostaloSve = ukupnoSve - avans
if preostaloSve < 0 {
preostaloSve = 0
}
}
h.renderujStandalone(w, "servis_otpremnica", PodaciOtpremnice{
Nalog: *nalog,
ServisniDelovi: delovi,
UkupnoDelovi: ukupnoDelovi,
PreostaloSve: preostaloSve,
Klijent: klijent,
KlijentNaziv: klijentNaziv,
TehnicarNaziv: tehnicarNaziv,
+39 -6
View File
@@ -161,22 +161,38 @@
</div>
</div>
<div>
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:4px;">Konačna cena</div>
<div style="font-size:20px;font-weight:600;color:var(--sb-akcent);">
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:4px;">Cena usluge</div>
<div style="font-size:16px;font-weight:500;color:var(--tekst-glavni);">
{{if .Nalog.CenaKonacna}}{{.Nalog.CenaKonacnaStr}} din{{else}}—{{end}}
</div>
</div>
{{if gt .UkupnoDelovi 0.0}}
<div>
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:4px;">Ugrađeni delovi</div>
<div style="font-size:16px;font-weight:500;color:var(--tekst-glavni);">
{{printf "%.2f" .UkupnoDelovi}} din
</div>
</div>
{{end}}
{{if .Nalog.CenaKonacna}}
<div>
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:4px;">Ukupno</div>
<div style="font-size:20px;font-weight:600;color:var(--sb-akcent);">
{{printf "%.2f" .UkupnoSve}} din
</div>
</div>
{{end}}
<div>
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:4px;">Avans</div>
<div style="font-size:16px;font-weight:500;color:var(--tekst-glavni);">
{{if .Nalog.Avans}}{{.Nalog.AvansStr}} din{{else}}—{{end}}
</div>
</div>
{{if .Nalog.PreostaloZaNaplatu}}
{{if .Nalog.CenaKonacna}}
<div>
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:4px;">Preostalo za naplatu</div>
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:4px;">Za naplatu</div>
<div style="font-size:20px;font-weight:600;color:#16a34a;">
{{.Nalog.PreostaloZaNaplatuStr}} din
{{printf "%.2f" .PreostaloSve}} din
</div>
</div>
{{end}}
@@ -199,7 +215,7 @@
<select name="artikal_id" style="width:100%;" required>
<option value="">— odaberi —</option>
{{range .Artikli}}
<option value="{{.ID}}">{{.Naziv}} ({{.Kolicina}} kom)</option>
<option value="{{.ID}}" data-cena="{{.ProdajnaCena}}">{{.Naziv}} ({{.Kolicina}} kom)</option>
{{end}}
</select>
</div>
@@ -279,4 +295,21 @@
</div>
</div>
<script>
(function() {
var sel = document.querySelector('select[name="artikal_id"]');
var cenaInput = document.querySelector('input[name="cena_komada"]');
if (!sel || !cenaInput) return;
sel.addEventListener('change', function() {
var opt = sel.options[sel.selectedIndex];
var cena = parseFloat(opt.dataset.cena);
if (!isNaN(cena) && cena > 0) {
cenaInput.value = cena.toFixed(2);
} else {
cenaInput.value = '0';
}
});
})();
</script>
{{end}}
@@ -199,6 +199,12 @@
<td>Cena usluge</td>
<td class="desno" style="width:160px;">{{.Nalog.CenaKonacnaStr}} din</td>
</tr>
{{if gt .UkupnoDelovi 0.0}}
<tr>
<td>Ugrađeni delovi i materijal</td>
<td class="desno">{{printf "%.2f" .UkupnoDelovi}} din</td>
</tr>
{{end}}
{{if .Nalog.Avans}}
<tr>
<td>Avans (plaćeno)</td>
@@ -207,12 +213,10 @@
{{end}}
</tbody>
</table>
{{if .Nalog.PreostaloZaNaplatu}}
<div class="naplata-blok">
<div class="naplata-labela">Za naplatu pri preuzimanju:</div>
<div class="naplata-iznos">{{.Nalog.PreostaloZaNaplatuStr}} din</div>
<div class="naplata-iznos">{{printf "%.2f" .PreostaloSve}} din</div>
</div>
{{end}}
</div>
{{end}}
+52 -2
View File
@@ -226,8 +226,51 @@
{{block "dodatni-js" .}}{{end}}
<!-- modal za potvrdu akcije -->
<div id="potvrda-modal" style="display:none;position:fixed;inset:0;z-index:9999;align-items:center;justify-content:center;">
<div id="potvrda-pozadina" style="position:absolute;inset:0;background:rgba(0,0,0,0.55);backdrop-filter:blur(3px);"></div>
<div style="position:relative;background:var(--kartica-pozadina);border:0.5px solid var(--ivica);border-radius:12px;padding:28px 28px 22px;max-width:380px;width:90%;box-shadow:0 20px 60px rgba(0,0,0,0.35);">
<div style="font-size:15px;font-weight:500;color:var(--tekst-glavni);margin-bottom:20px;line-height:1.5;" id="potvrda-poruka"></div>
<div style="display:flex;gap:10px;justify-content:flex-end;">
<button id="potvrda-odustani" class="btn-sekundarno">Odustani</button>
<button id="potvrda-potvrdi" class="btn-opasno">Potvrdi</button>
</div>
</div>
</div>
<!-- CSRF i potvrda: inicijalizacija na učitavanju i posle htmx swap-a -->
<script>
(function() {
var modal = document.getElementById('potvrda-modal');
var poruka = document.getElementById('potvrda-poruka');
var btnPotvrdi = document.getElementById('potvrda-potvrdi');
var btnOdustani = document.getElementById('potvrda-odustani');
var pozadina = document.getElementById('potvrda-pozadina');
var _resolveFn = null;
function prikaziModal(tekst) {
return new Promise(function(resolve) {
poruka.textContent = tekst;
modal.style.display = 'flex';
_resolveFn = resolve;
});
}
function zatvoriModal(rezultat) {
modal.style.display = 'none';
if (_resolveFn) { _resolveFn(rezultat); _resolveFn = null; }
}
btnPotvrdi.addEventListener('click', function() { zatvoriModal(true); });
btnOdustani.addEventListener('click', function() { zatvoriModal(false); });
pozadina.addEventListener('click', function() { zatvoriModal(false); });
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && modal.style.display === 'flex') zatvoriModal(false);
});
window._ntechPotvrdi = prikaziModal;
})();
function ntechInicijalizuj() {
var m = document.querySelector('meta[name="csrf-token"]');
if (m && m.content) {
@@ -242,11 +285,18 @@
if (el._potvrda) return;
el._potvrda = true;
el.addEventListener('click', function(e) {
if (!confirm(el.getAttribute('data-potvrda'))) e.preventDefault();
e.preventDefault();
window._ntechPotvrdi(el.getAttribute('data-potvrda')).then(function(ok) {
if (!ok) return;
// dugme unutar forme — submit forme
var forma = el.closest('form');
if (forma) { forma.submit(); return; }
// link
if (el.href) { window.location.href = el.href; }
});
});
});
}
// htmx:afterSettle listener se dodaje samo jednom — ne sme da se gomila po swap-ovima
if (!window._ntechCsrfDodato) {
window._ntechCsrfDodato = true;
document.addEventListener('htmx:afterSettle', ntechInicijalizuj);