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:
@@ -46,6 +46,9 @@ type PodaciDetaljiNaloga struct {
|
|||||||
ServisniDelovi []model.ServisniDeoSaArtiklom
|
ServisniDelovi []model.ServisniDeoSaArtiklom
|
||||||
Artikli []model.ArtikalSaKategorijom
|
Artikli []model.ArtikalSaKategorijom
|
||||||
Sacuvano bool
|
Sacuvano bool
|
||||||
|
UkupnoDelovi float64
|
||||||
|
UkupnoSve float64
|
||||||
|
PreostaloSve float64
|
||||||
}
|
}
|
||||||
|
|
||||||
// Servis renderuje listu servisnih naloga sa opcionom pretragom i filterom statusa
|
// 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)
|
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 := h.popuniPodaciStranice(r, podesavanja)
|
||||||
ps.Stranica = "servis"
|
ps.Stranica = "servis"
|
||||||
ps.NaslovStranice = "Detalji naloga"
|
ps.NaslovStranice = "Detalji naloga"
|
||||||
@@ -366,6 +386,9 @@ func (h *Handler) DetaljiNaloga(w http.ResponseWriter, r *http.Request) {
|
|||||||
ServisniDelovi: delovi,
|
ServisniDelovi: delovi,
|
||||||
Artikli: artikli,
|
Artikli: artikli,
|
||||||
Sacuvano: r.URL.Query().Get("sacuvano") == "1",
|
Sacuvano: r.URL.Query().Get("sacuvano") == "1",
|
||||||
|
UkupnoDelovi: ukupnoDelovi,
|
||||||
|
UkupnoSve: ukupnoSve,
|
||||||
|
PreostaloSve: preostaloSve,
|
||||||
}
|
}
|
||||||
|
|
||||||
h.renderujTemplate(w, "servis_detalji", podaci)
|
h.renderujTemplate(w, "servis_detalji", podaci)
|
||||||
@@ -638,6 +661,7 @@ type PodaciOtpremnice struct {
|
|||||||
Nalog model.ServisniNalog
|
Nalog model.ServisniNalog
|
||||||
ServisniDelovi []model.ServisniDeoSaArtiklom
|
ServisniDelovi []model.ServisniDeoSaArtiklom
|
||||||
UkupnoDelovi float64
|
UkupnoDelovi float64
|
||||||
|
PreostaloSve float64
|
||||||
Klijent *model.Klijent
|
Klijent *model.Klijent
|
||||||
KlijentNaziv string
|
KlijentNaziv string
|
||||||
TehnicarNaziv string
|
TehnicarNaziv string
|
||||||
@@ -700,11 +724,24 @@ func (h *Handler) StampaOtpremnice(w http.ResponseWriter, r *http.Request) {
|
|||||||
for _, d := range delovi {
|
for _, d := range delovi {
|
||||||
ukupnoDelovi += d.Ukupno()
|
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{
|
h.renderujStandalone(w, "servis_otpremnica", PodaciOtpremnice{
|
||||||
Nalog: *nalog,
|
Nalog: *nalog,
|
||||||
ServisniDelovi: delovi,
|
ServisniDelovi: delovi,
|
||||||
UkupnoDelovi: ukupnoDelovi,
|
UkupnoDelovi: ukupnoDelovi,
|
||||||
|
PreostaloSve: preostaloSve,
|
||||||
Klijent: klijent,
|
Klijent: klijent,
|
||||||
KlijentNaziv: klijentNaziv,
|
KlijentNaziv: klijentNaziv,
|
||||||
TehnicarNaziv: tehnicarNaziv,
|
TehnicarNaziv: tehnicarNaziv,
|
||||||
|
|||||||
@@ -161,22 +161,38 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:4px;">Konačna cena</div>
|
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:4px;">Cena usluge</div>
|
||||||
<div style="font-size:20px;font-weight:600;color:var(--sb-akcent);">
|
<div style="font-size:16px;font-weight:500;color:var(--tekst-glavni);">
|
||||||
{{if .Nalog.CenaKonacna}}{{.Nalog.CenaKonacnaStr}} din{{else}}—{{end}}
|
{{if .Nalog.CenaKonacna}}{{.Nalog.CenaKonacnaStr}} din{{else}}—{{end}}
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:4px;">Avans</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);">
|
<div style="font-size:16px;font-weight:500;color:var(--tekst-glavni);">
|
||||||
{{if .Nalog.Avans}}{{.Nalog.AvansStr}} din{{else}}—{{end}}
|
{{if .Nalog.Avans}}{{.Nalog.AvansStr}} din{{else}}—{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{if .Nalog.PreostaloZaNaplatu}}
|
{{if .Nalog.CenaKonacna}}
|
||||||
<div>
|
<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;">
|
<div style="font-size:20px;font-weight:600;color:#16a34a;">
|
||||||
{{.Nalog.PreostaloZaNaplatuStr}} din
|
{{printf "%.2f" .PreostaloSve}} din
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -199,7 +215,7 @@
|
|||||||
<select name="artikal_id" style="width:100%;" required>
|
<select name="artikal_id" style="width:100%;" required>
|
||||||
<option value="">— odaberi —</option>
|
<option value="">— odaberi —</option>
|
||||||
{{range .Artikli}}
|
{{range .Artikli}}
|
||||||
<option value="{{.ID}}">{{.Naziv}} ({{.Kolicina}} kom)</option>
|
<option value="{{.ID}}" data-cena="{{.ProdajnaCena}}">{{.Naziv}} ({{.Kolicina}} kom)</option>
|
||||||
{{end}}
|
{{end}}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -279,4 +295,21 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
</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}}
|
{{end}}
|
||||||
|
|||||||
@@ -199,6 +199,12 @@
|
|||||||
<td>Cena usluge</td>
|
<td>Cena usluge</td>
|
||||||
<td class="desno" style="width:160px;">{{.Nalog.CenaKonacnaStr}} din</td>
|
<td class="desno" style="width:160px;">{{.Nalog.CenaKonacnaStr}} din</td>
|
||||||
</tr>
|
</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}}
|
{{if .Nalog.Avans}}
|
||||||
<tr>
|
<tr>
|
||||||
<td>Avans (plaćeno)</td>
|
<td>Avans (plaćeno)</td>
|
||||||
@@ -207,12 +213,10 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{{if .Nalog.PreostaloZaNaplatu}}
|
|
||||||
<div class="naplata-blok">
|
<div class="naplata-blok">
|
||||||
<div class="naplata-labela">Za naplatu pri preuzimanju:</div>
|
<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>
|
</div>
|
||||||
{{end}}
|
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
|||||||
@@ -226,8 +226,51 @@
|
|||||||
|
|
||||||
{{block "dodatni-js" .}}{{end}}
|
{{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 -->
|
<!-- CSRF i potvrda: inicijalizacija na učitavanju i posle htmx swap-a -->
|
||||||
<script>
|
<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() {
|
function ntechInicijalizuj() {
|
||||||
var m = document.querySelector('meta[name="csrf-token"]');
|
var m = document.querySelector('meta[name="csrf-token"]');
|
||||||
if (m && m.content) {
|
if (m && m.content) {
|
||||||
@@ -242,11 +285,18 @@
|
|||||||
if (el._potvrda) return;
|
if (el._potvrda) return;
|
||||||
el._potvrda = true;
|
el._potvrda = true;
|
||||||
el.addEventListener('click', function(e) {
|
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) {
|
if (!window._ntechCsrfDodato) {
|
||||||
window._ntechCsrfDodato = true;
|
window._ntechCsrfDodato = true;
|
||||||
document.addEventListener('htmx:afterSettle', ntechInicijalizuj);
|
document.addEventListener('htmx:afterSettle', ntechInicijalizuj);
|
||||||
|
|||||||
Reference in New Issue
Block a user