Prodaja: modal naplate sa fiskalnim računom (kao u Servisu)

- dugme Naplati otvara modal (primljeno/kusur, način plaćanja, prikaz računa)
- primljeni iznos se prosleđuje u fiskalni zahtev radi tačnog Gotovina/Povraćaj
- nova ruta i handler za prikaz/štampu fiskalnog računa prodaje
- fiskalni tab se otvara preko imenovanog prozora (bez blokiranja popup-a)
- Fisk mock: refund se sada računa iz stvarnih plaćanja, ne fiksno 0
This commit is contained in:
2026-07-01 21:03:47 +02:00
parent 1df83866df
commit fd762a6527
7 changed files with 204 additions and 57 deletions
+10 -2
View File
@@ -413,6 +413,14 @@ def _build_invoice_response(req, request_id):
"messages": "Success", "messages": "Success",
} }
# povraćaj — ako je primljeno više nego što je duženo (npr. kupac dao krupniju
# novčanicu), razlika se ispisuje na računu; kod refundacije nema povraćaja
payments = inv_req.get("payment", [{"type": "Cash", "amount": total_amount}])
total_paid = round(sum(float(p.get("amount", 0)) for p in payments), 2)
refund = round(total_paid - total_amount, 2) if transaction_type != "Refund" else 0
if refund < 0:
refund = 0
# Puni podaci za snimanje i generisanje računa # Puni podaci za snimanje i generisanje računa
full_data = { full_data = {
**odgovor, **odgovor,
@@ -420,7 +428,7 @@ def _build_invoice_response(req, request_id):
"invoiceType": invoice_type, "invoiceType": invoice_type,
"transactionType": transaction_type, "transactionType": transaction_type,
"items": items, "items": items,
"payments": inv_req.get("payment", [{"type": "Cash", "amount": total_amount}]), "payments": payments,
"cashier": inv_req.get("cashier", "Kasir"), "cashier": inv_req.get("cashier", "Kasir"),
"buyerId": inv_req.get("buyerId", ""), "buyerId": inv_req.get("buyerId", ""),
"referentDocumentNumber": inv_req.get("referentDocumentNumber", ""), "referentDocumentNumber": inv_req.get("referentDocumentNumber", ""),
@@ -430,7 +438,7 @@ def _build_invoice_response(req, request_id):
"store": firma["locationName"], "store": firma["locationName"],
"address": firma["address"], "address": firma["address"],
"district": firma["district"], "district": firma["district"],
"refund": 0, "refund": refund,
# za avansni konačni # za avansni konačni
"advancePaid": req.get("advancePaid", 0), "advancePaid": req.get("advancePaid", 0),
"advanceTax": req.get("advanceTax", 0), "advanceTax": req.get("advanceTax", 0),
+1
View File
@@ -420,6 +420,7 @@ func main() {
r.With(doz("prodaja.storno")).Post("/prodaja/storno/{id}", h.StornoProdaje) r.With(doz("prodaja.storno")).Post("/prodaja/storno/{id}", h.StornoProdaje)
r.With(doz("prodaja.dodaj")).Post("/prodaja/{id}/fiskalizuj", h.RetryFiskalizacijaProdaje) r.With(doz("prodaja.dodaj")).Post("/prodaja/{id}/fiskalizuj", h.RetryFiskalizacijaProdaje)
r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "prodaja.pregled")).Get("/prodaja/{id}/stampa", h.StampaProdaje) r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "prodaja.pregled")).Get("/prodaja/{id}/stampa", h.StampaProdaje)
r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "prodaja.pregled")).Get("/prodaja/{id}/fiskalni-racun", h.StampaFiskalnogProdaje)
r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "prodaja.pregled")).Get("/prodaja/{id}", h.DetaljiProdaje) r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "prodaja.pregled")).Get("/prodaja/{id}", h.DetaljiProdaje)
// podsetnici // podsetnici
+13 -3
View File
@@ -51,11 +51,14 @@ func TipPlacanja(nacin string) string {
// NapraviZahtev konvertuje prodajni nalog i stavke u Teron InvoiceRequest. // NapraviZahtev konvertuje prodajni nalog i stavke u Teron InvoiceRequest.
// VAŽNO: sve cene se konvertuju iz neto u bruto pre slanja (BrutoCena). // VAŽNO: sve cene se konvertuju iz neto u bruto pre slanja (BrutoCena).
// pib/jmbg se koriste za buyerId identifikaciju kupca. // pib/jmbg se koriste za buyerId identifikaciju kupca. primljeno je iznos koji je
// kupac stvarno predao (za obračun povraćaja na fiskalnom računu) — ako je 0 ili
// manji od ukupnog iznosa, šalje se tačan iznos duga (bez povraćaja).
func NapraviZahtev( func NapraviZahtev(
nalog model.ProdajniNalog, nalog model.ProdajniNalog,
stavke []model.StavkaProdajeSaArtiklom, stavke []model.StavkaProdajeSaArtiklom,
pib, jmbg, kasir string, pib, jmbg, kasir string,
primljeno float64,
) InvoiceRequest { ) InvoiceRequest {
items := make([]InvoiceItem, 0, len(stavke)) items := make([]InvoiceItem, 0, len(stavke))
ukupanIznos := 0.0 ukupanIznos := 0.0
@@ -90,6 +93,13 @@ func NapraviZahtev(
// Ukupno za plaćanje je uvek suma bruto stavki (neto iz naloga bi bilo pogrešno) // Ukupno za plaćanje je uvek suma bruto stavki (neto iz naloga bi bilo pogrešno)
ukupnoPlacanje := ukupanIznos ukupnoPlacanje := ukupanIznos
// ako je kupac dao više od duga, na računu se iskazuje stvarno primljen iznos
// (fiskalni uređaj sam izračunava povraćaj kao razliku)
iznosPlacanja := ukupnoPlacanje
if primljeno > ukupnoPlacanje {
iznosPlacanja = primljeno
}
nacinPlacanja := nalog.NacinPlacanja nacinPlacanja := nalog.NacinPlacanja
if nacinPlacanja == "" { if nacinPlacanja == "" {
nacinPlacanja = "Gotovina" nacinPlacanja = "Gotovina"
@@ -101,7 +111,7 @@ func NapraviZahtev(
TransactionType: "Sale", TransactionType: "Sale",
Payment: []PaymentItem{ Payment: []PaymentItem{
{ {
Amount: ukupnoPlacanje, Amount: iznosPlacanja,
PaymentType: TipPlacanja(nacinPlacanja), PaymentType: TipPlacanja(nacinPlacanja),
}, },
}, },
@@ -127,7 +137,7 @@ func NapraviRefundZahtev(
stavke []model.StavkaProdajeSaArtiklom, stavke []model.StavkaProdajeSaArtiklom,
pib, jmbg, kasir, referentBroj string, pib, jmbg, kasir, referentBroj string,
) InvoiceRequest { ) InvoiceRequest {
zahtev := NapraviZahtev(nalog, stavke, pib, jmbg, kasir) zahtev := NapraviZahtev(nalog, stavke, pib, jmbg, kasir, 0)
zahtev.InvoiceRequest.TransactionType = "Refund" zahtev.InvoiceRequest.TransactionType = "Refund"
zahtev.InvoiceRequest.ReferentDocumentNumber = referentBroj zahtev.InvoiceRequest.ReferentDocumentNumber = referentBroj
return zahtev return zahtev
+70 -5
View File
@@ -325,9 +325,14 @@ func (h *Handler) SacuvajProdaju(w http.ResponseWriter, r *http.Request) {
} }
// Fiskalizacija — ako je modul uključen (best-effort: prodaja ostaje validna i bez fiskalizacije) // Fiskalizacija — ako je modul uključen (best-effort: prodaja ostaje validna i bez fiskalizacije)
racunKreiran := false
if h.modulUkljucen(r.Context(), config.ModulFiskalizacija) { if h.modulUkljucen(r.Context(), config.ModulFiskalizacija) {
if klijent := h.fiskalKlijent(); klijent != nil { if klijent := h.fiskalKlijent(); klijent != nil {
h.fiskalizujProdaju(r.Context(), id, klijent) primljeno, _ := strconv.ParseFloat(strings.TrimSpace(r.FormValue("primljeno")), 64)
h.fiskalizujProdaju(r.Context(), id, klijent, primljeno)
if fr, _ := h.FiskalRepo.DohvatiPoProdaji(r.Context(), id); fr != nil {
racunKreiran = true
}
} }
} }
@@ -347,7 +352,11 @@ func (h *Handler) SacuvajProdaju(w http.ResponseWriter, r *http.Request) {
} }
} }
http.Redirect(w, r, "/prodaja/"+strconv.FormatInt(id, 10)+"?sacuvano=1", http.StatusSeeOther) redirectURL := "/prodaja/" + strconv.FormatInt(id, 10) + "?sacuvano=1"
if r.FormValue("prikazi_racun") == "1" && racunKreiran {
redirectURL += "&racun=1"
}
http.Redirect(w, r, redirectURL, http.StatusSeeOther)
} }
// DetaljiProdaje prikazuje pregled jednog prodajnog naloga sa svim stavkama // DetaljiProdaje prikazuje pregled jednog prodajnog naloga sa svim stavkama
@@ -470,6 +479,60 @@ func (h *Handler) StampaProdaje(w http.ResponseWriter, r *http.Request) {
h.renderujStandalone(w, "prodaja_stampa", podaci) h.renderujStandalone(w, "prodaja_stampa", podaci)
} }
// StampaFiskalnogProdaje renderuje journal izdatog fiskalnog računa za prodaju (za štampu/uvid)
func (h *Handler) StampaFiskalnogProdaje(w http.ResponseWriter, r *http.Request) {
id, err := parseID(chi.URLParam(r, "id"))
if err != nil {
http.Error(w, "Neispravan ID", http.StatusBadRequest)
return
}
fr, err := h.FiskalRepo.DohvatiPoProdaji(r.Context(), id)
if err != nil || fr == nil {
http.Error(w, "Fiskalni račun nije pronađen", http.StatusNotFound)
return
}
nalog, _ := h.ProdajaRepo.DohvatiID(r.Context(), id)
journal := ""
if fr.SiroviOdgovor != "" {
var raw map[string]any
if json.Unmarshal([]byte(fr.SiroviOdgovor), &raw) == nil {
if j, ok := raw["journal"].(string); ok {
journal = j
}
}
}
if nalog != nil {
journal += "\n\n--- NTech prodaja: " + nalog.BrojNaloga + " ---\n"
}
// Podeli journal na deo pre i posle QR placeholder-a
const qrPlaceholder = "{{{{QR-KOD}}}}"
pre, post, hasQR := strings.Cut(journal, qrPlaceholder)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprint(w, `<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Fiskalni račun</title>
<style>
*{box-sizing:border-box;}
body{font-family:monospace;font-size:12px;padding:20px;max-width:max-content;margin:0 auto;}
pre{white-space:pre;margin:0;padding:0;font-family:inherit;font-size:inherit;display:block;}
@media print{body{font-size:11px;padding:10px;}}
</style></head><body>`)
fmt.Fprint(w, `<pre>`)
fmt.Fprint(w, pre)
fmt.Fprint(w, `</pre>`)
if hasQR && fr.QRKod != "" {
fmt.Fprintf(w, `<div style="margin:10px 0;"><img src="data:image/png;base64,%s" style="display:block;margin:0 auto;width:72mm;height:72mm;"></div>`, fr.QRKod)
}
fmt.Fprint(w, `<pre>`)
fmt.Fprint(w, post)
fmt.Fprint(w, `</pre>`)
fmt.Fprint(w, `<p style="margin-top:16px;"><button onclick="window.print()" style="padding:8px 16px;">Štampaj</button></p></body></html>`)
}
// ObrisiProdaju prima POST zahtev i stornira nalog umesto fizičkog brisanja. // ObrisiProdaju prima POST zahtev i stornira nalog umesto fizičkog brisanja.
// Jednom izdat račun ne sme da nestane — umesto brisanja koristi se storno. // Jednom izdat račun ne sme da nestane — umesto brisanja koristi se storno.
func (h *Handler) ObrisiProdaju(w http.ResponseWriter, r *http.Request) { func (h *Handler) ObrisiProdaju(w http.ResponseWriter, r *http.Request) {
@@ -733,7 +796,9 @@ func (h *Handler) stornirajProdaju(ctx context.Context, id int64, razlog string,
// fiskalizujProdaju šalje fiskalni zahtev za prodajni nalog. Best-effort: greške // fiskalizujProdaju šalje fiskalni zahtev za prodajni nalog. Best-effort: greške
// se loguju, ne zaustavljaju tok pozivaoca (kreiranje prodaje ili ručni retry). // se loguju, ne zaustavljaju tok pozivaoca (kreiranje prodaje ili ručni retry).
func (h *Handler) fiskalizujProdaju(ctx context.Context, prodajaID int64, klijent *fiskal.Klijent) { // primljeno je iznos koji je kupac predao (za tačan povraćaj na računu) — 0 ako
// nije poznat (npr. ručni retry), tada se šalje tačan dug bez povraćaja.
func (h *Handler) fiskalizujProdaju(ctx context.Context, prodajaID int64, klijent *fiskal.Klijent, primljeno float64) {
nalog, err := h.ProdajaRepo.DohvatiID(ctx, prodajaID) nalog, err := h.ProdajaRepo.DohvatiID(ctx, prodajaID)
if err != nil { if err != nil {
slog.Error("fiskalizujProdaju: nije pronađen nalog", "prodaja_id", prodajaID, "error", err) slog.Error("fiskalizujProdaju: nije pronađen nalog", "prodaja_id", prodajaID, "error", err)
@@ -757,7 +822,7 @@ func (h *Handler) fiskalizujProdaju(ctx context.Context, prodajaID int64, klijen
} }
} }
zahtev := fiskal.NapraviZahtev(*nalog, stavke, pib, jmbg, kasir) zahtev := fiskal.NapraviZahtev(*nalog, stavke, pib, jmbg, kasir, primljeno)
odgovor, errFisk := klijent.IzdajRacun(ctx, zahtev) odgovor, errFisk := klijent.IzdajRacun(ctx, zahtev)
if errFisk != nil { if errFisk != nil {
@@ -822,7 +887,7 @@ func (h *Handler) RetryFiskalizacijaProdaje(w http.ResponseWriter, r *http.Reque
return return
} }
h.fiskalizujProdaju(r.Context(), id, klijent) h.fiskalizujProdaju(r.Context(), id, klijent, 0)
if fr, _ := h.FiskalRepo.DohvatiPoProdaji(r.Context(), id); fr != nil { if fr, _ := h.FiskalRepo.DohvatiPoProdaji(r.Context(), id); fr != nil {
middleware.SetFlash(w, r, h.DB, "uspeh", "Fiskalni račun izdat.") middleware.SetFlash(w, r, h.DB, "uspeh", "Fiskalni račun izdat.")
+22
View File
@@ -138,6 +138,10 @@ document.addEventListener('alpine:init', () => {
Alpine.data('prodajaForma', () => ({ Alpine.data('prodajaForma', () => ({
stavke: [{artikal_id: '', kolicina: 1, cena: 0, cena_sa_pdv: 0, pdv_stopa: 20, popust: 0, kategorija_naziv: '', _naziv: '', _pretraga: '', _prikaziListu: false, _limit: 30}], stavke: [{artikal_id: '', kolicina: 1, cena: 0, cena_sa_pdv: 0, pdv_stopa: 20, popust: 0, kategorija_naziv: '', _naziv: '', _pretraga: '', _prikaziListu: false, _limit: 30}],
zbirnoHover: false, zbirnoHover: false,
nacinPlacanja: 'gotovina',
primljenoIznos: '',
prikaziRacun: true,
_fiskalniTab: null,
artikliOpcije: [], artikliOpcije: [],
pretragaArtikal: '', pretragaArtikal: '',
pdvObveznik: true, pdvObveznik: true,
@@ -357,6 +361,24 @@ document.addEventListener('alpine:init', () => {
this.stavke = this.stavke.filter(s => s.artikal_id) this.stavke = this.stavke.filter(s => s.artikal_id)
this.$nextTick(() => e.target.submit()) this.$nextTick(() => e.target.submit())
}, },
otvoriModalNaplate() {
this.primljenoIznos = ''
this.$refs.dijalogNaplate.showModal()
},
kusurIznos() {
return (parseFloat(this.primljenoIznos) || 0) - parseFloat(this.ukupnoSvega())
},
naplataValidna() {
return this.primljenoIznos !== '' && (parseFloat(this.primljenoIznos) || 0) >= parseFloat(this.ukupnoSvega())
},
// pripremiFiskalniTab otvara prazan tab SINHRONO unutar klika (očuvan user gesture),
// da bi kasnije, posle redirekta na stranicu detalja prodaje, mogao da se imenom
// popuni URL-om fiskalnog računa bez blokiranja popup-blokera
pripremiFiskalniTab() {
if (this.prikaziRacun) {
this._fiskalniTab = window.open('', 'ntech-fiskalni-tab')
}
},
ukupnoStavke(s) { ukupnoStavke(s) {
const cena = parseFloat(s.cena) || 0 const cena = parseFloat(s.cena) || 0
const popust = parseFloat(s.popust) || 0 const popust = parseFloat(s.popust) || 0
@@ -287,6 +287,15 @@
{{end}} {{end}}
</div> </div>
</div> </div>
<script>
(function(){
// ako je kroz modal naplate u „Nova prodaja" zatražen prikaz — otvoren je prazan
// imenovani tab pre redirekta ovde; sada ga popunjavamo (ne blokira popup-blocker)
if (new URLSearchParams(location.search).get('racun') === '1') {
window.open('/prodaja/{{.Nalog.ID}}/fiskalni-racun', 'ntech-fiskalni-tab')
}
})();
</script>
{{end}} {{end}}
{{if .FiskalGreska}} {{if .FiskalGreska}}
+79 -47
View File
@@ -46,54 +46,44 @@
<span style="font-size: 16px; font-weight: 500; color: var(--tekst-glavni)">Nova prodaja</span> <span style="font-size: 16px; font-weight: 500; color: var(--tekst-glavni)">Nova prodaja</span>
</div> </div>
<div style="display: flex; flex-direction: column; gap: 14px"> <div style="display: flex; flex-direction: column; gap: 14px">
<div class="forma-grid-2" style="display:grid;grid-template-columns:1fr 1fr;gap:12px;"> <div>
<div> <div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 4px; flex-wrap: wrap; gap: 6px">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 4px; flex-wrap: wrap; gap: 6px"> <label class="polje-labela" style="margin-bottom: 0">Klijent</label>
<label class="polje-labela" style="margin-bottom: 0">Klijent</label> <div style="display: flex; gap: 6px">
<div style="display: flex; gap: 6px"> <label class="tip-klijenta-opcija" :class="klasaTipKlijenta('sve')" @click="postaviTipKlijenta('sve')">
<label class="tip-klijenta-opcija" :class="klasaTipKlijenta('sve')" @click="postaviTipKlijenta('sve')"> <input type="radio" name="tip-klijenta-filter" value="sve" :checked="jeTipKlijenta('sve')" style="margin: 0" /> Sva lica
<input type="radio" name="tip-klijenta-filter" value="sve" :checked="jeTipKlijenta('sve')" style="margin: 0" /> Sva lica </label>
</label> <label class="tip-klijenta-opcija" :class="klasaTipKlijenta('pravno')" @click="postaviTipKlijenta('pravno')">
<label class="tip-klijenta-opcija" :class="klasaTipKlijenta('pravno')" @click="postaviTipKlijenta('pravno')"> <input type="radio" name="tip-klijenta-filter" value="pravno" :checked="jeTipKlijenta('pravno')" style="margin: 0" /> Pravna lica
<input type="radio" name="tip-klijenta-filter" value="pravno" :checked="jeTipKlijenta('pravno')" style="margin: 0" /> Pravna lica </label>
</label> <label class="tip-klijenta-opcija" :class="klasaTipKlijenta('fizicko')" @click="postaviTipKlijenta('fizicko')">
<label class="tip-klijenta-opcija" :class="klasaTipKlijenta('fizicko')" @click="postaviTipKlijenta('fizicko')"> <input type="radio" name="tip-klijenta-filter" value="fizicko" :checked="jeTipKlijenta('fizicko')" style="margin: 0" /> Fizička lica
<input type="radio" name="tip-klijenta-filter" value="fizicko" :checked="jeTipKlijenta('fizicko')" style="margin: 0" /> Fizička lica </label>
</label>
</div>
</div>
<input type="hidden" name="klijent_id" :value="izabraniKlijentId || ''" />
<template x-if="izabraniKlijent">
<div style="display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; padding: 8px 10px; background: var(--pozadina); border: 0.5px solid var(--ivica); border-radius: 8px">
<div>
<div style="font-size: 13px; font-weight: 500; color: var(--tekst-glavni)" x-text="izabraniKlijent.naziv"></div>
<template x-for="p in poljaIzabranogKlijenta(izabraniKlijent)" :key="p.labela">
<div style="font-size: 11px; color: var(--tekst-sporedni)"><span x-text="p.labela"></span>: <span x-text="p.vrednost"></span></div>
</template>
</div>
<button type="button" class="btn-sekundarno" @click="ocistiKlijenta()" style="padding: 3px 10px; font-size: 12px">Promeni</button>
</div>
</template>
<div x-show="!izabraniKlijent" style="position: relative" @click.outside="prikaziListuKlijenata = false">
<input type="text" x-model="pretragaKlijent" autocomplete="off" @focus="pretragaKlijentPromenjena()" @input="pretragaKlijentPromenjena()" placeholder="Pretraži klijenta ili ostavi prazno..." style="width: 100%" />
<div x-show="prikaziListuKlijenata && filtriraniKlijenti.length" x-cloak @scroll="ucitajViseKlijenata($event)" style="position: absolute; z-index: 30; left: 0; right: 0; margin-top: 4px; background: var(--pozadina); border: 0.5px solid var(--ivica); border-radius: 10px; max-height: 220px; overflow: auto; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.14)">
<template x-for="k in filtriraniKlijenti" :key="k.id">
<div @click="izaberiKlijenta(k)" class="klijent-stavka" style="padding: 8px 12px; cursor: pointer; border-bottom: 0.5px solid var(--ivica)">
<div style="font-size: 13px; font-weight: 500; color: var(--tekst-glavni)" x-text="k.naziv"></div>
<div style="font-size: 11px; color: var(--tekst-sporedni)" x-text="detaljKlijenta(k)"></div>
</div>
</template>
<div x-show="imaJosKlijenata" style="padding: 6px 12px; text-align: center; font-size: 11px; color: var(--tekst-sporedni)">Skrolujte za još rezultata...</div>
</div>
</div> </div>
</div> </div>
<div style="display: flex; flex-direction: column; justify-content: flex-end"> <input type="hidden" name="klijent_id" :value="izabraniKlijentId || ''" />
<label class="polje-labela">Način plaćanja</label> <template x-if="izabraniKlijent">
<select name="nacin_placanja" style="width:100%;"> <div style="display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; padding: 8px 10px; background: var(--pozadina); border: 0.5px solid var(--ivica); border-radius: 8px">
<option value="gotovina">Gotovina</option> <div>
<option value="kartica">Platna kartica</option> <div style="font-size: 13px; font-weight: 500; color: var(--tekst-glavni)" x-text="izabraniKlijent.naziv"></div>
<option value="prenos">Bankarski prenos</option> <template x-for="p in poljaIzabranogKlijenta(izabraniKlijent)" :key="p.labela">
</select> <div style="font-size: 11px; color: var(--tekst-sporedni)"><span x-text="p.labela"></span>: <span x-text="p.vrednost"></span></div>
</template>
</div>
<button type="button" class="btn-sekundarno" @click="ocistiKlijenta()" style="padding: 3px 10px; font-size: 12px">Promeni</button>
</div>
</template>
<div x-show="!izabraniKlijent" style="position: relative" @click.outside="prikaziListuKlijenata = false">
<input type="text" x-model="pretragaKlijent" autocomplete="off" @focus="pretragaKlijentPromenjena()" @input="pretragaKlijentPromenjena()" placeholder="Pretraži klijenta ili ostavi prazno..." style="width: 100%" />
<div x-show="prikaziListuKlijenata && filtriraniKlijenti.length" x-cloak @scroll="ucitajViseKlijenata($event)" style="position: absolute; z-index: 30; left: 0; right: 0; margin-top: 4px; background: var(--pozadina); border: 0.5px solid var(--ivica); border-radius: 10px; max-height: 220px; overflow: auto; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.14)">
<template x-for="k in filtriraniKlijenti" :key="k.id">
<div @click="izaberiKlijenta(k)" class="klijent-stavka" style="padding: 8px 12px; cursor: pointer; border-bottom: 0.5px solid var(--ivica)">
<div style="font-size: 13px; font-weight: 500; color: var(--tekst-glavni)" x-text="k.naziv"></div>
<div style="font-size: 11px; color: var(--tekst-sporedni)" x-text="detaljKlijenta(k)"></div>
</div>
</template>
<div x-show="imaJosKlijenata" style="padding: 6px 12px; text-align: center; font-size: 11px; color: var(--tekst-sporedni)">Skrolujte za još rezultata...</div>
</div>
</div> </div>
</div> </div>
<div> <div>
@@ -302,10 +292,52 @@
<!-- dugmad forme --> <!-- dugmad forme -->
<div style="display: flex; justify-content: flex-end; gap: 10px"> <div style="display: flex; justify-content: flex-end; gap: 10px">
<a hx-boost="true" hx-target="#glavni-sadrzaj" hx-select="#glavni-sadrzaj" hx-swap="outerHTML" href="/prodaja" class="btn-sekundarno">Odustani</a> <a hx-boost="true" hx-target="#glavni-sadrzaj" hx-select="#glavni-sadrzaj" hx-swap="outerHTML" href="/prodaja" class="btn-sekundarno">Odustani</a>
<button type="submit" class="btn-primarno" :disabled="imaPrekoracenja() || !imaPopunjenuStavku()" :style="(imaPrekoracenja() || !imaPopunjenuStavku()) ? 'opacity:0.4;cursor:not-allowed;' : ''"> <button type="button" @click="otvoriModalNaplate()" class="btn-primarno" :disabled="imaPrekoracenja() || !imaPopunjenuStavku()" :style="(imaPrekoracenja() || !imaPopunjenuStavku()) ? 'opacity:0.4;cursor:not-allowed;' : ''">
Naplati Naplati
</button> </button>
</div> </div>
<!-- modal naplate — unos primljenog iznosa/kusura i način plaćanja pre konačnog slanja prodaje -->
<dialog class="premesti-modal" x-ref="dijalogNaplate" onclick="if(event.target===this)this.close()">
<div class="premesti-zaglavlje">
<h3>Naplata</h3>
<button type="button" class="premesti-zatvori" @click="$refs.dijalogNaplate.close()" aria-label="Zatvori">&times;</button>
</div>
<div style="display:flex;flex-direction:column;gap:10px;padding:16px;">
<div style="display:flex;justify-content:space-between;align-items:center;">
<span style="font-size:16px;font-weight:600;color:var(--tekst-glavni);">Za naplatu</span>
<span style="font-size:18px;font-weight:600;color:var(--tekst-glavni);" x-text="fmtDin(ukupnoSvega()) + ' din'"></span>
</div>
<div style="margin-top:6px;padding-top:10px;border-top:0.5px solid var(--ivica);">
<label class="polje-labela">Primljeno (din)</label>
<input type="number" name="primljeno" min="0" step="0.01" x-model="primljenoIznos" placeholder="Unesite primljeni iznos"
style="width:100%;padding:8px 12px;background:var(--pozadina);border:0.5px solid var(--ivica);border-radius:8px;color:var(--tekst-glavni);font-size:14px;box-sizing:border-box;">
</div>
<div x-show="primljenoIznos !== '' && kusurIznos() >= 0" x-cloak
style="display:flex;justify-content:space-between;align-items:center;padding:8px 12px;background:color-mix(in srgb,var(--uspeh) 10%,transparent);border:0.5px solid var(--uspeh);border-radius:8px;">
<span style="color:var(--uspeh);font-weight:500;">Kusur (vratiti)</span>
<span style="font-size:16px;font-weight:600;color:var(--uspeh);" x-text="fmtDin(kusurIznos()) + ' din'"></span>
</div>
<div>
<label class="polje-labela">Način plaćanja</label>
<select name="nacin_placanja" x-model="nacinPlacanja"
style="width:100%;padding:8px 12px;background:var(--pozadina);border:0.5px solid var(--ivica);border-radius:8px;color:var(--tekst-glavni);font-size:13px;">
<option value="gotovina">Gotovina</option>
<option value="kartica">Platna kartica</option>
<option value="prenos">Bankarski prenos</option>
</select>
</div>
<label style="display:flex;align-items:center;gap:10px;padding:10px 12px;background:var(--pozadina);border:0.5px solid var(--ivica);border-radius:8px;cursor:pointer;">
<input type="checkbox" name="prikazi_racun" value="1" x-model="prikaziRacun"
style="width:16px;height:16px;accent-color:var(--sb-akcent);cursor:pointer;flex-shrink:0;">
<span style="font-size:13px;color:var(--tekst-glavni);">Prikaži fiskalni račun</span>
</label>
<button type="submit" class="btn-primarno" style="width:100%;margin-top:4px;" :disabled="!naplataValidna()"
:style="!naplataValidna() ? 'opacity:0.4;cursor:not-allowed;' : ''" @click="pripremiFiskalniTab()">
Naplati
</button>
</div>
</dialog>
</form> </form>
</div> </div>
{{end}} {{end}}