Merge feature/prodaja-pretraga-klijenta-i-filteri → develop

This commit is contained in:
2026-07-01 13:55:04 +02:00
9 changed files with 226 additions and 35 deletions
+9 -1
View File
@@ -219,9 +219,17 @@ type ServisRepository interface {
SacuvajNaplatu(ctx context.Context, id int64, nacinPlacanja string, naplaceno float64) error SacuvajNaplatu(ctx context.Context, id int64, nacinPlacanja string, naplaceno float64) error
} }
// ProdajaFilter definiše parametre za filtriranje liste prodajnih naloga
type ProdajaFilter struct {
Pretraga string
Od string // YYYY-MM-DD, prazno = bez donje granice
Do string // YYYY-MM-DD, prazno = bez gornje granice
SamoStornirano bool
}
// ProdajaRepository definiše operacije nad prodajnim nalozima // ProdajaRepository definiše operacije nad prodajnim nalozima
type ProdajaRepository interface { type ProdajaRepository interface {
Lista(ctx context.Context, pretraga string) ([]model.ProdajniNalogSaDetaljem, error) Lista(ctx context.Context, filter ProdajaFilter) ([]model.ProdajniNalogSaDetaljem, error)
DohvatiID(ctx context.Context, id int64) (*model.ProdajniNalog, error) DohvatiID(ctx context.Context, id int64) (*model.ProdajniNalog, error)
DohvatiStavke(ctx context.Context, nalogID int64) ([]model.StavkaProdajeSaArtiklom, error) DohvatiStavke(ctx context.Context, nalogID int64) ([]model.StavkaProdajeSaArtiklom, error)
Kreiraj(ctx context.Context, n *model.ProdajniNalog, stavke []model.StavkaProdaje, korisnikID *int64) (int64, error) Kreiraj(ctx context.Context, n *model.ProdajniNalog, stavke []model.StavkaProdaje, korisnikID *int64) (int64, error)
+17 -4
View File
@@ -37,8 +37,8 @@ func (r *ProdajaRepo) SledeciBroj(ctx context.Context) (string, error) {
return fmt.Sprintf("PR-%d-%04d", godina, sledeci), nil return fmt.Sprintf("PR-%d-%04d", godina, sledeci), nil
} }
// Lista vraća listu prodajnih naloga sa imenom klijenta, opcionom pretragom // Lista vraća listu prodajnih naloga sa imenom klijenta, po zadatom filteru
func (r *ProdajaRepo) Lista(ctx context.Context, pretraga string) ([]model.ProdajniNalogSaDetaljem, error) { func (r *ProdajaRepo) Lista(ctx context.Context, filter db.ProdajaFilter) ([]model.ProdajniNalogSaDetaljem, error) {
upit := ` upit := `
SELECT SELECT
pn.id, pn.klijent_id, pn.broj_naloga, pn.napomena, pn.ukupno, pn.id, pn.klijent_id, pn.broj_naloga, pn.napomena, pn.ukupno,
@@ -50,9 +50,22 @@ func (r *ProdajaRepo) Lista(ctx context.Context, pretraga string) ([]model.Proda
args := []any{} args := []any{}
if pretraga != "" { if filter.Pretraga != "" {
upit += " AND (pn.broj_naloga LIKE ? OR kp.naziv LIKE ? OR pn.napomena LIKE ?)" upit += " AND (pn.broj_naloga LIKE ? OR kp.naziv LIKE ? OR pn.napomena LIKE ?)"
args = append(args, "%"+pretraga+"%", "%"+pretraga+"%", "%"+pretraga+"%") args = append(args, "%"+filter.Pretraga+"%", "%"+filter.Pretraga+"%", "%"+filter.Pretraga+"%")
}
// pn.datum se čuva preko time.Time.String() (uključuje monotonic sufiks i naziv zone),
// pa ga SQLite-ova date() funkcija ne parsira ispravno — poredimo samo ISO datum na početku stringa
if filter.Od != "" {
upit += " AND substr(pn.datum, 1, 10) >= ?"
args = append(args, filter.Od)
}
if filter.Do != "" {
upit += " AND substr(pn.datum, 1, 10) <= ?"
args = append(args, filter.Do)
}
if filter.SamoStornirano {
upit += " AND pn.stornirano = 1"
} }
upit += " ORDER BY pn.datum DESC" upit += " ORDER BY pn.datum DESC"
+2 -1
View File
@@ -7,6 +7,7 @@ import (
"strings" "strings"
"time" "time"
appdb "ntech/internal/db"
"ntech/internal/db/sqlite" "ntech/internal/db/sqlite"
"ntech/internal/middleware" "ntech/internal/middleware"
"ntech/internal/model" "ntech/internal/model"
@@ -163,7 +164,7 @@ func (h *Handler) KpoBackfill(w http.ResponseWriter, r *http.Request) {
var kreirano, preskoceno int var kreirano, preskoceno int
// prodajni nalozi // prodajni nalozi
nalozi, err := h.ProdajaRepo.Lista(ctx, "") nalozi, err := h.ProdajaRepo.Lista(ctx, appdb.ProdajaFilter{})
if err != nil { if err != nil {
http.Error(w, "Greška pri učitavanju prodaje", http.StatusInternalServerError) http.Error(w, "Greška pri učitavanju prodaje", http.StatusInternalServerError)
return return
+2 -1
View File
@@ -8,6 +8,7 @@ import (
"strings" "strings"
"time" "time"
appdb "ntech/internal/db"
"ntech/internal/db/sqlite" "ntech/internal/db/sqlite"
"ntech/internal/middleware" "ntech/internal/middleware"
"ntech/internal/model" "ntech/internal/model"
@@ -222,7 +223,7 @@ func (h *Handler) KirBackfillProdaje(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
sada := time.Now() sada := time.Now()
nalozi, err := h.ProdajaRepo.Lista(ctx, "") nalozi, err := h.ProdajaRepo.Lista(ctx, appdb.ProdajaFilter{})
if err != nil { if err != nil {
http.Error(w, "Greška pri učitavanju prodaje", http.StatusInternalServerError) http.Error(w, "Greška pri učitavanju prodaje", http.StatusInternalServerError)
return return
+58 -10
View File
@@ -25,20 +25,26 @@ import (
// PodaciProdaje su podaci za stranicu sa listom prodajnih naloga // PodaciProdaje su podaci za stranicu sa listom prodajnih naloga
type PodaciProdaje struct { type PodaciProdaje struct {
model.PodaciStranice model.PodaciStranice
Nalozi []model.ProdajniNalogSaDetaljem Nalozi []model.ProdajniNalogSaDetaljem
Sacuvano bool Sacuvano bool
Obrisan bool Obrisan bool
Pretraga string Pretraga string
NemaFiskalnog map[int64]bool // ID-evi naloga bez izdatog fiskalnog računa Od string
Do string
OvajMesecOd string
OvajMesecDo string
SamoStornirano bool
NemaFiskalnog map[int64]bool // ID-evi naloga bez izdatog fiskalnog računa
} }
// PodaciFormeProdaje su podaci za formu unosa nove prodaje // PodaciFormeProdaje su podaci za formu unosa nove prodaje
type PodaciFormeProdaje struct { type PodaciFormeProdaje struct {
model.PodaciStranice model.PodaciStranice
Artikli []model.ArtikalSaKategorijom Artikli []model.ArtikalSaKategorijom
ArtikliJSON template.JS ArtikliJSON template.JS
Klijenti []model.Klijent Klijenti []model.Klijent
Greska string KlijentiJSON template.JS
Greska string
} }
// PodaciDetaljiProdaje su podaci za pregled jedne prodaje sa stavkama // PodaciDetaljiProdaje su podaci za pregled jedne prodaje sa stavkama
@@ -97,6 +103,29 @@ func artikalUJSONSaCenom(artikli []model.ArtikalSaKategorijom) template.JS {
return template.JS(b) return template.JS(b)
} }
// klijentiUJSON pretvara listu klijenata u template.JS vrednost za pretragu na strani klijenta (naziv, tip)
func klijentiUJSON(klijenti []model.Klijent) template.JS {
type stavka struct {
ID int64 `json:"id"`
Naziv string `json:"naziv"`
Tip string `json:"tip"`
Mesto string `json:"mesto"`
Telefon string `json:"telefon"`
}
lista := make([]stavka, 0, len(klijenti))
for _, k := range klijenti {
lista = append(lista, stavka{
ID: k.ID,
Naziv: k.PunoIme(),
Tip: k.Tip,
Mesto: k.Mesto,
Telefon: k.Telefon,
})
}
b, _ := json.Marshal(lista)
return template.JS(b)
}
// Prodaja renderuje listu svih prodajnih naloga // Prodaja renderuje listu svih prodajnih naloga
func (h *Handler) Prodaja(w http.ResponseWriter, r *http.Request) { func (h *Handler) Prodaja(w http.ResponseWriter, r *http.Request) {
podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB) podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
@@ -106,8 +135,16 @@ func (h *Handler) Prodaja(w http.ResponseWriter, r *http.Request) {
} }
pretraga := strings.TrimSpace(r.URL.Query().Get("pretraga")) pretraga := strings.TrimSpace(r.URL.Query().Get("pretraga"))
od := r.URL.Query().Get("od")
do := r.URL.Query().Get("do")
samoStornirano := r.URL.Query().Get("stornirano") == "1"
nalozi, err := h.ProdajaRepo.Lista(r.Context(), pretraga) nalozi, err := h.ProdajaRepo.Lista(r.Context(), appdb.ProdajaFilter{
Pretraga: pretraga,
Od: od,
Do: do,
SamoStornirano: samoStornirano,
})
if err != nil { if err != nil {
http.Error(w, "Greška pri učitavanju prodaje", http.StatusInternalServerError) http.Error(w, "Greška pri učitavanju prodaje", http.StatusInternalServerError)
return return
@@ -118,6 +155,10 @@ func (h *Handler) Prodaja(w http.ResponseWriter, r *http.Request) {
nemaFiskalnog, _ = h.FiskalRepo.ProdajeBezFiskalnog(r.Context()) nemaFiskalnog, _ = h.FiskalRepo.ProdajeBezFiskalnog(r.Context())
} }
sada := time.Now()
prviDanMeseca := time.Date(sada.Year(), sada.Month(), 1, 0, 0, 0, 0, sada.Location())
poslednjiDanMeseca := prviDanMeseca.AddDate(0, 1, -1)
ps := h.popuniPodaciStranice(r, podesavanja) ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "prodaja" ps.Stranica = "prodaja"
ps.NaslovStranice = "Prodaja" ps.NaslovStranice = "Prodaja"
@@ -127,6 +168,11 @@ func (h *Handler) Prodaja(w http.ResponseWriter, r *http.Request) {
Sacuvano: r.URL.Query().Get("sacuvano") == "1", Sacuvano: r.URL.Query().Get("sacuvano") == "1",
Obrisan: r.URL.Query().Get("obrisan") == "1", Obrisan: r.URL.Query().Get("obrisan") == "1",
Pretraga: pretraga, Pretraga: pretraga,
Od: od,
Do: do,
OvajMesecOd: prviDanMeseca.Format("2006-01-02"),
OvajMesecDo: poslednjiDanMeseca.Format("2006-01-02"),
SamoStornirano: samoStornirano,
NemaFiskalnog: nemaFiskalnog, NemaFiskalnog: nemaFiskalnog,
} }
@@ -161,6 +207,7 @@ func (h *Handler) NovaProdaja(w http.ResponseWriter, r *http.Request) {
Artikli: artikli, Artikli: artikli,
ArtikliJSON: artikalUJSONSaCenom(artikli), ArtikliJSON: artikalUJSONSaCenom(artikli),
Klijenti: klijenti, Klijenti: klijenti,
KlijentiJSON: klijentiUJSON(klijenti),
}) })
} }
@@ -189,6 +236,7 @@ func (h *Handler) SacuvajProdaju(w http.ResponseWriter, r *http.Request) {
Artikli: artikli, Artikli: artikli,
ArtikliJSON: artikalUJSONSaCenom(artikli), ArtikliJSON: artikalUJSONSaCenom(artikli),
Klijenti: klijenti, Klijenti: klijenti,
KlijentiJSON: klijentiUJSON(klijenti),
Greska: poruka, Greska: poruka,
}) })
} }
+61
View File
@@ -141,8 +141,69 @@ document.addEventListener('alpine:init', () => {
pretragaArtikal: '', pretragaArtikal: '',
pdvObveznik: true, pdvObveznik: true,
isMobile: false, isMobile: false,
klijenti: [],
tipKlijentaFilter: 'sve',
pretragaKlijent: '',
prikaziListuKlijenata: false,
izabraniKlijent: null,
izabraniKlijentId: 0,
limitKlijenata: 30,
jeTipKlijenta(t) {
return this.tipKlijentaFilter === t
},
klasaTipKlijenta(t) {
return this.jeTipKlijenta(t) ? 'tip-klijenta-opcija-akt' : ''
},
postaviTipKlijenta(t) {
this.tipKlijentaFilter = t
this.limitKlijenata = 30
},
pretragaKlijentPromenjena() {
this.prikaziListuKlijenata = true
this.limitKlijenata = 30
},
// svi klijenti koji odgovaraju filteru/pretrazi (bez ograničenja) — koristi se
// da lista zna da li ima još rezultata za dogruzavanje
get svihFiltriranihKlijenata() {
const q = this.pretragaKlijent.trim().toLowerCase()
return this.klijenti.filter(k => {
if (this.tipKlijentaFilter !== 'sve' && k.tip !== this.tipKlijentaFilter) return false
if (q === '') return true
return (k.naziv || '').toLowerCase().includes(q)
})
},
get filtriraniKlijenti() {
return this.svihFiltriranihKlijenata.slice(0, this.limitKlijenata)
},
get imaJosKlijenata() {
return this.limitKlijenata < this.svihFiltriranihKlijenata.length
},
ucitajViseKlijenata(e) {
const el = e.target
if (el.scrollTop + el.clientHeight >= el.scrollHeight - 40 && this.imaJosKlijenata) {
this.limitKlijenata += 30
}
},
izaberiKlijenta(k) {
this.izabraniKlijent = k
this.izabraniKlijentId = k.id
this.pretragaKlijent = ''
this.prikaziListuKlijenata = false
},
ocistiKlijenta() {
this.izabraniKlijent = null
this.izabraniKlijentId = 0
},
detaljKlijenta(k) {
if (!k) return ''
const d = []
if (k.mesto) d.push(k.mesto)
if (k.telefon) d.push(k.telefon)
return d.join(' · ')
},
init() { init() {
this.artikliOpcije = window._ntechArtikli || [] this.artikliOpcije = window._ntechArtikli || []
this.klijenti = window._ntechKlijenti || []
this.pdvObveznik = window._ntechPdvObveznik === true this.pdvObveznik = window._ntechPdvObveznik === true
this.isMobile = window.matchMedia('(max-width: 768px)').matches this.isMobile = window.matchMedia('(max-width: 768px)').matches
window.matchMedia('(max-width: 768px)').addEventListener('change', e => { window.matchMedia('(max-width: 768px)').addEventListener('change', e => {
+24 -8
View File
@@ -6,14 +6,30 @@
<div class="poruka-uspeh poruka-animacija">Prodajni nalog je uspešno obrisan.</div> <div class="poruka-uspeh poruka-animacija">Prodajni nalog je uspešno obrisan.</div>
{{end}} {{end}}
<!-- zaglavlje sa pretragom i dugmetom --> <!-- zaglavlje sa pretragom, filterima i dugmetom -->
<div style="display: flex; align-items: center; gap: 10px; flex-wrap: wrap"> <div style="display: flex; align-items: flex-end; gap: 10px; flex-wrap: wrap">
<form method="GET" action="/prodaja" style="display: flex; gap: 8px; flex: 1; min-width: 200px"> <form method="GET" action="/prodaja" style="display: flex; gap: 10px; align-items: flex-end; flex-wrap: wrap; flex: 1">
<input type="text" name="pretraga" value="{{.Pretraga}}" placeholder="Pretraži po broju naloga..." style="flex: 1" /> <div style="flex: 1; min-width: 180px">
<button type="submit" class="btn-sekundarno" style="white-space: nowrap">Pretraži</button> <label class="polje-labela">Pretraga</label>
{{if .Pretraga}} <input type="text" name="pretraga" value="{{.Pretraga}}" placeholder="Broj naloga, klijent, napomena..." style="width: 100%; box-sizing: border-box; padding: 8px 10px; border: 0.5px solid var(--ivica); border-radius: 8px; font-size: 13px; background: var(--pozadina); color: var(--tekst-glavni); outline: none" />
</div>
<div>
<label class="polje-labela">Od datuma</label>
<input type="date" name="od" value="{{.Od}}" style="padding: 8px 10px; border: 0.5px solid var(--ivica); border-radius: 8px; font-size: 13px; background: var(--pozadina); color: var(--tekst-glavni); outline: none" />
</div>
<div>
<label class="polje-labela">Do datuma</label>
<input type="date" name="do" value="{{.Do}}" style="padding: 8px 10px; border: 0.5px solid var(--ivica); border-radius: 8px; font-size: 13px; background: var(--pozadina); color: var(--tekst-glavni); outline: none" />
</div>
<label style="display: flex; align-items: center; gap: 6px; font-size: 13px; color: var(--tekst-glavni); padding-bottom: 9px; white-space: nowrap">
<input type="checkbox" name="stornirano" value="1" {{if .SamoStornirano}}checked{{end}} />
Samo stornirano
</label>
<button type="submit" class="btn-sekundarno" style="white-space: nowrap">Prikaži</button>
{{if or .Pretraga .Od .Do .SamoStornirano}}
<a hx-boost="true" hx-target="#glavni-sadrzaj" hx-select="#glavni-sadrzaj" hx-swap="outerHTML" href="/prodaja" class="btn-sekundarno" style="white-space: nowrap">✕ Resetuj</a> <a hx-boost="true" hx-target="#glavni-sadrzaj" hx-select="#glavni-sadrzaj" hx-swap="outerHTML" href="/prodaja" class="btn-sekundarno" style="white-space: nowrap">✕ Resetuj</a>
{{end}} {{end}}
<a hx-boost="true" hx-target="#glavni-sadrzaj" hx-select="#glavni-sadrzaj" hx-swap="outerHTML" href="/prodaja?{{if .Pretraga}}pretraga={{.Pretraga}}&{{end}}od={{.OvajMesecOd}}&do={{.OvajMesecDo}}{{if .SamoStornirano}}&stornirano=1{{end}}" class="btn-sekundarno" style="white-space: nowrap">Ovaj mesec</a>
</form> </form>
<a hx-boost="true" hx-target="#glavni-sadrzaj" hx-select="#glavni-sadrzaj" hx-swap="outerHTML" href="/prodaja/nova" class="btn-primarno">+ Nova prodaja</a> <a hx-boost="true" hx-target="#glavni-sadrzaj" hx-select="#glavni-sadrzaj" hx-swap="outerHTML" href="/prodaja/nova" class="btn-primarno">+ Nova prodaja</a>
</div> </div>
@@ -57,7 +73,7 @@
{{else}} {{else}}
<tr> <tr>
<td colspan="5" style="padding: 32px; text-align: center; font-size: 14px; color: var(--tekst-sporedni)"> <td colspan="5" style="padding: 32px; text-align: center; font-size: 14px; color: var(--tekst-sporedni)">
{{if $.Pretraga}} Nema naloga koji odgovaraju pretrazi. {{else}} Nema prodajnih naloga. {{if or $.Pretraga $.Od $.Do $.SamoStornirano}} Nema naloga koji odgovaraju filteru. {{else}} Nema prodajnih naloga.
<a hx-boost="true" hx-target="#glavni-sadrzaj" hx-select="#glavni-sadrzaj" hx-swap="outerHTML" href="/prodaja/nova" style="color: var(--sb-akcent)">Dodaj prvu prodaju.</a> <a hx-boost="true" hx-target="#glavni-sadrzaj" hx-select="#glavni-sadrzaj" hx-swap="outerHTML" href="/prodaja/nova" style="color: var(--sb-akcent)">Dodaj prvu prodaju.</a>
{{end}} {{end}}
</td> </td>
@@ -88,7 +104,7 @@
</div> </div>
{{else}} {{else}}
<div style="padding: 32px; text-align: center; font-size: 14px; color: var(--tekst-sporedni)"> <div style="padding: 32px; text-align: center; font-size: 14px; color: var(--tekst-sporedni)">
{{if $.Pretraga}} Nema naloga koji odgovaraju pretrazi. {{else}} Nema prodajnih naloga. {{if or $.Pretraga $.Od $.Do $.SamoStornirano}} Nema naloga koji odgovaraju filteru. {{else}} Nema prodajnih naloga.
<a hx-boost="true" hx-target="#glavni-sadrzaj" hx-select="#glavni-sadrzaj" hx-swap="outerHTML" href="/prodaja/nova" style="color: var(--sb-akcent)">Dodaj prvu prodaju.</a> <a hx-boost="true" hx-target="#glavni-sadrzaj" hx-select="#glavni-sadrzaj" hx-swap="outerHTML" href="/prodaja/nova" style="color: var(--sb-akcent)">Dodaj prvu prodaju.</a>
{{end}} {{end}}
</div> </div>
+1 -1
View File
@@ -273,7 +273,7 @@
<p style="margin:0 0 4px 0;color:var(--tekst-sekundarno);"><strong>Brojač:</strong> {{.FiskalniRacun.Brojac}}</p> <p style="margin:0 0 4px 0;color:var(--tekst-sekundarno);"><strong>Brojač:</strong> {{.FiskalniRacun.Brojac}}</p>
{{if .FiskalniRacun.UrlVerifikacija}} {{if .FiskalniRacun.UrlVerifikacija}}
<p style="margin:4px 0 0 0;font-size:13px;"> <p style="margin:4px 0 0 0;font-size:13px;">
<a href="{{.FiskalniRacun.UrlVerifikacija}}" target="_blank" rel="noopener noreferrer" class="spoljni-link">Provera na PURS-u →</a> <a href="{{.FiskalniRacun.UrlVerifikacija}}" target="_blank" rel="noopener noreferrer" style="color: var(--sb-akcent); font-weight: 500">Provera na PURS-u →</a>
</p> </p>
{{end}} {{end}}
</div> </div>
+52 -9
View File
@@ -5,11 +5,21 @@
.forma-kartica:nth-child(2) { animation-delay: 0.12s; } .forma-kartica:nth-child(2) { animation-delay: 0.12s; }
@media (max-width: 768px) { .stavke-tabela-wrapper { display: none; } .stavke-kartice { display: flex !important; } } @media (max-width: 768px) { .stavke-tabela-wrapper { display: none; } .stavke-kartice { display: flex !important; } }
.polje-labela { font-size: 12px; color: var(--tekst-sporedni); display: block; margin-bottom: 4px; } .polje-labela { font-size: 12px; color: var(--tekst-sporedni); display: block; margin-bottom: 4px; }
.tip-klijenta-opcija {
display: flex; align-items: center; gap: 5px;
padding: 3px 10px; border: 1px solid var(--ivica); border-radius: 20px;
cursor: pointer; font-size: 12px; color: var(--tekst-sporedni); user-select: none;
}
.tip-klijenta-opcija-akt { border-color: var(--akcentna); color: var(--tekst-glavni); background: var(--pozadina); }
.klijent-stavka:last-child { border-bottom: none; }
.klijent-stavka:hover { background: var(--pozadina); }
[x-cloak] { display: none !important; }
</style> </style>
{{end}} {{define "sadrzaj"}} {{end}} {{define "sadrzaj"}}
<script> <script>
var _ntechArtikli = {{.ArtikliJSON}}; var _ntechArtikli = {{.ArtikliJSON}};
var _ntechKlijenti = {{.KlijentiJSON}};
var _ntechPdvObveznik = {{if index .Moduli "pdv"}}true{{else}}false{{end}}; var _ntechPdvObveznik = {{if index .Moduli "pdv"}}true{{else}}false{{end}};
</script> </script>
@@ -22,27 +32,60 @@
</a> </a>
<form method="POST" action="/prodaja/nova"> <form method="POST" action="/prodaja/nova">
<input type="hidden" name="_csrf" value="{{.CsrfToken}}" />
{{if .Greska}} {{if .Greska}}
<div class="poruka-greska greska-animacija">{{.Greska}}</div> <div class="poruka-greska greska-animacija">{{.Greska}}</div>
{{end}} {{end}}
<!-- zaglavlje prodaje --> <!-- zaglavlje prodaje -->
<div class="kartica forma-kartica animiraj" style="margin-bottom: 16px"> <!-- position:relative + z-index: bez ovoga lista rezultata pretrage klijenata
(position:absolute unutra) crta se ISPOD sledeće kartice (Stavke) — kartica
bez pozicioniranja slika se u ranijoj fazi rasporeda bez obzira na z-index dece -->
<div class="kartica forma-kartica animiraj" style="margin-bottom: 16px; position: relative; z-index: 2">
<div style="margin-bottom: 16px; padding-bottom: 14px; border-bottom: 0.5px solid var(--ivica);"> <div style="margin-bottom: 16px; padding-bottom: 14px; border-bottom: 0.5px solid var(--ivica);">
<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 style="display:grid;grid-template-columns:1fr 1fr;gap:12px;"> <div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
<div> <div>
<label class="polje-labela">Klijent</label> <div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 4px; flex-wrap: wrap; gap: 6px">
<select name="klijent_id" style="width: 100%"> <label class="polje-labela" style="margin-bottom: 0">Klijent</label>
<option value="">— bez klijenta —</option> <div style="display: flex; gap: 6px">
{{range .Klijenti}} <label class="tip-klijenta-opcija" :class="klasaTipKlijenta('sve')" @click="postaviTipKlijenta('sve')">
<option value="{{.ID}}">{{.PunoIme}}</option> <input type="radio" name="tip-klijenta-filter" value="sve" :checked="jeTipKlijenta('sve')" style="margin: 0" /> Sva lica
{{end}} </label>
</select> <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
</label>
<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
</label>
</div>
</div>
<input type="hidden" name="klijent_id" :value="izabraniKlijentId || ''" />
<template x-if="izabraniKlijent">
<div style="display: flex; align-items: center; 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>
<div style="font-size: 11px; color: var(--tekst-sporedni)" x-text="detaljKlijenta(izabraniKlijent)"></div>
</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">
<label class="polje-labela">Način plaćanja</label> <label class="polje-labela">Način plaćanja</label>
<select name="nacin_placanja" style="width:100%;"> <select name="nacin_placanja" style="width:100%;">
<option value="gotovina">Gotovina</option> <option value="gotovina">Gotovina</option>