Prodaja: filteri liste (period, storno) i pretraga klijenta u novoj prodaji
- Lista prodaje: filter po periodu (od/do), prečica 'Ovaj mesec', filter samo stornirano - Nova prodaja: pretraga klijenta sa filterom tipa (sva/pravna/fizička lica) umesto običnog dropdown-a, sa dogruzavanjem rezultata pri skrolu - Ispravljen nedostajući CSRF token u formi nove prodaje (POST je uvek padao na 403) - Ispravljeno vizuelno poravnanje polja Način plaćanja
This commit is contained in:
@@ -141,8 +141,69 @@ document.addEventListener('alpine:init', () => {
|
||||
pretragaArtikal: '',
|
||||
pdvObveznik: true,
|
||||
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() {
|
||||
this.artikliOpcije = window._ntechArtikli || []
|
||||
this.klijenti = window._ntechKlijenti || []
|
||||
this.pdvObveznik = window._ntechPdvObveznik === true
|
||||
this.isMobile = window.matchMedia('(max-width: 768px)').matches
|
||||
window.matchMedia('(max-width: 768px)').addEventListener('change', e => {
|
||||
|
||||
@@ -6,14 +6,30 @@
|
||||
<div class="poruka-uspeh poruka-animacija">Prodajni nalog je uspešno obrisan.</div>
|
||||
{{end}}
|
||||
|
||||
<!-- zaglavlje sa pretragom i dugmetom -->
|
||||
<div style="display: flex; align-items: center; gap: 10px; flex-wrap: wrap">
|
||||
<form method="GET" action="/prodaja" style="display: flex; gap: 8px; flex: 1; min-width: 200px">
|
||||
<input type="text" name="pretraga" value="{{.Pretraga}}" placeholder="Pretraži po broju naloga..." style="flex: 1" />
|
||||
<button type="submit" class="btn-sekundarno" style="white-space: nowrap">Pretraži</button>
|
||||
{{if .Pretraga}}
|
||||
<!-- zaglavlje sa pretragom, filterima i dugmetom -->
|
||||
<div style="display: flex; align-items: flex-end; gap: 10px; flex-wrap: wrap">
|
||||
<form method="GET" action="/prodaja" style="display: flex; gap: 10px; align-items: flex-end; flex-wrap: wrap; flex: 1">
|
||||
<div style="flex: 1; min-width: 180px">
|
||||
<label class="polje-labela">Pretraga</label>
|
||||
<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>
|
||||
{{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>
|
||||
<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>
|
||||
@@ -57,7 +73,7 @@
|
||||
{{else}}
|
||||
<tr>
|
||||
<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>
|
||||
{{end}}
|
||||
</td>
|
||||
@@ -88,7 +104,7 @@
|
||||
</div>
|
||||
{{else}}
|
||||
<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>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
@@ -273,7 +273,7 @@
|
||||
<p style="margin:0 0 4px 0;color:var(--tekst-sekundarno);"><strong>Brojač:</strong> {{.FiskalniRacun.Brojac}}</p>
|
||||
{{if .FiskalniRacun.UrlVerifikacija}}
|
||||
<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>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
@@ -5,11 +5,21 @@
|
||||
.forma-kartica:nth-child(2) { animation-delay: 0.12s; }
|
||||
@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; }
|
||||
.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>
|
||||
{{end}} {{define "sadrzaj"}}
|
||||
|
||||
<script>
|
||||
var _ntechArtikli = {{.ArtikliJSON}};
|
||||
var _ntechKlijenti = {{.KlijentiJSON}};
|
||||
var _ntechPdvObveznik = {{if index .Moduli "pdv"}}true{{else}}false{{end}};
|
||||
</script>
|
||||
|
||||
@@ -22,27 +32,60 @@
|
||||
</a>
|
||||
|
||||
<form method="POST" action="/prodaja/nova">
|
||||
<input type="hidden" name="_csrf" value="{{.CsrfToken}}" />
|
||||
{{if .Greska}}
|
||||
<div class="poruka-greska greska-animacija">{{.Greska}}</div>
|
||||
{{end}}
|
||||
|
||||
<!-- 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);">
|
||||
<span style="font-size: 16px; font-weight: 500; color: var(--tekst-glavni)">Nova prodaja</span>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 14px">
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
|
||||
<div>
|
||||
<label class="polje-labela">Klijent</label>
|
||||
<select name="klijent_id" style="width: 100%">
|
||||
<option value="">— bez klijenta —</option>
|
||||
{{range .Klijenti}}
|
||||
<option value="{{.ID}}">{{.PunoIme}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
<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>
|
||||
<div style="display: flex; gap: 6px">
|
||||
<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
|
||||
</label>
|
||||
<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 style="display: flex; flex-direction: column; justify-content: flex-end">
|
||||
<label class="polje-labela">Način plaćanja</label>
|
||||
<select name="nacin_placanja" style="width:100%;">
|
||||
<option value="gotovina">Gotovina</option>
|
||||
|
||||
Reference in New Issue
Block a user