Prodaja: rešen hover na footer-u, auto-dodavanje nove stavke, X dugme za brisanje (CSP fix), naplati dugme
This commit is contained in:
+30
-12
@@ -137,6 +137,7 @@ document.addEventListener('alpine:init', () => {
|
|||||||
// forma za prodaju
|
// forma za prodaju
|
||||||
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,
|
||||||
artikliOpcije: [],
|
artikliOpcije: [],
|
||||||
pretragaArtikal: '',
|
pretragaArtikal: '',
|
||||||
pdvObveznik: true,
|
pdvObveznik: true,
|
||||||
@@ -257,10 +258,19 @@ document.addEventListener('alpine:init', () => {
|
|||||||
poslednja._prikaziListu = true
|
poslednja._prikaziListu = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.stavke.push({artikal_id: '', kolicina: 1, cena: 0, cena_sa_pdv: 0, pdv_stopa: this.pdvObveznik ? 20 : 0, popust: 0, kategorija_naziv: '', _naziv: '', _pretraga: '', _prikaziListu: false, _limit: 30})
|
this.stavke.push(this.novaStavka())
|
||||||
|
},
|
||||||
|
novaStavka() {
|
||||||
|
return {artikal_id: '', kolicina: 1, cena: 0, cena_sa_pdv: 0, pdv_stopa: this.pdvObveznik ? 20 : 0, popust: 0, kategorija_naziv: '', _naziv: '', _pretraga: '', _prikaziListu: false, _limit: 30}
|
||||||
},
|
},
|
||||||
ukloniStavku(i) {
|
ukloniStavku(i) {
|
||||||
if (this.stavke.length > 1) this.stavke.splice(i, 1)
|
this.stavke.splice(i, 1)
|
||||||
|
// ne ostavljaj tabelu bez ijednog reda — korisnik uvek treba prazan red spreman za unos.
|
||||||
|
// $nextTick: ako se novi red doda u istom tiku kad je poslednji obrisan, Alpine ume da
|
||||||
|
// pogrešno recikliše DOM čvor istog x-for ključa (indeksa) i ostavi zastarele x-show/x-text.
|
||||||
|
if (this.stavke.length === 0) {
|
||||||
|
this.$nextTick(() => this.stavke.push(this.novaStavka()))
|
||||||
|
}
|
||||||
},
|
},
|
||||||
izaberiArtikal(stavka, a) {
|
izaberiArtikal(stavka, a) {
|
||||||
stavka.artikal_id = a.id
|
stavka.artikal_id = a.id
|
||||||
@@ -269,6 +279,11 @@ document.addEventListener('alpine:init', () => {
|
|||||||
stavka._prikaziListu = false
|
stavka._prikaziListu = false
|
||||||
stavka._limit = 30
|
stavka._limit = 30
|
||||||
this.popuniCenu(stavka)
|
this.popuniCenu(stavka)
|
||||||
|
// ako je ovo bila poslednja (i sad popunjena) stavka, odmah dodaj sledeći prazan red
|
||||||
|
// da korisnik ne mora ručno da klikne "+ Dodaj stavku" pri unosu više artikala zaredom
|
||||||
|
if (this.stavke[this.stavke.length - 1] === stavka) {
|
||||||
|
this.stavke.push(this.novaStavka())
|
||||||
|
}
|
||||||
},
|
},
|
||||||
promeniArtikal(stavka) {
|
promeniArtikal(stavka) {
|
||||||
stavka.artikal_id = ''
|
stavka.artikal_id = ''
|
||||||
@@ -292,7 +307,7 @@ document.addEventListener('alpine:init', () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
const prazna = this.stavke.find(s => !s.artikal_id)
|
const prazna = this.stavke.find(s => !s.artikal_id)
|
||||||
const stavka = prazna || this.stavke[this.stavke.push({artikal_id: '', kolicina: 1, cena: 0, cena_sa_pdv: 0, pdv_stopa: this.pdvObveznik ? 20 : 0, popust: 0, kategorija_naziv: '', _naziv: '', _pretraga: '', _prikaziListu: false, _limit: 30}) - 1]
|
const stavka = prazna || this.stavke[this.stavke.push(this.novaStavka()) - 1]
|
||||||
this.izaberiArtikal(stavka, a)
|
this.izaberiArtikal(stavka, a)
|
||||||
},
|
},
|
||||||
popuniCenu(stavka) {
|
popuniCenu(stavka) {
|
||||||
@@ -329,8 +344,18 @@ document.addEventListener('alpine:init', () => {
|
|||||||
imaPrekoracenja() {
|
imaPrekoracenja() {
|
||||||
return this.stavke.some((_, i) => this.prekoracenje(i))
|
return this.stavke.some((_, i) => this.prekoracenje(i))
|
||||||
},
|
},
|
||||||
imaPraznihStavki() {
|
imaPopunjenuStavku() {
|
||||||
return this.stavke.some(s => !s.artikal_id)
|
return this.stavke.some(s => s.artikal_id)
|
||||||
|
},
|
||||||
|
// stavkaImaVrednost — koristi se za prikaz dugmeta "Ukloni" (skriven na praznom redu bez vrednosti)
|
||||||
|
stavkaImaVrednost(stavka) {
|
||||||
|
return (Number(this.ukupnoStavke(stavka)) || 0) > 0
|
||||||
|
},
|
||||||
|
// posaljiProdaju uklanja prazne stavke (npr. automatski dodat red na kraju koji korisnik
|
||||||
|
// nije popunio) pre nego što se forma stvarno pošalje na server
|
||||||
|
posaljiProdaju(e) {
|
||||||
|
this.stavke = this.stavke.filter(s => s.artikal_id)
|
||||||
|
this.$nextTick(() => e.target.submit())
|
||||||
},
|
},
|
||||||
ukupnoStavke(s) {
|
ukupnoStavke(s) {
|
||||||
const cena = parseFloat(s.cena) || 0
|
const cena = parseFloat(s.cena) || 0
|
||||||
@@ -346,13 +371,6 @@ document.addEventListener('alpine:init', () => {
|
|||||||
return z + (parseFloat(s.kolicina) * cenaPosle || 0)
|
return z + (parseFloat(s.kolicina) * cenaPosle || 0)
|
||||||
}, 0).toFixed(2)
|
}, 0).toFixed(2)
|
||||||
},
|
},
|
||||||
// pdvIznosStavke vraća PDV sadržan u ceni po komadu (cena je bruto — PDV se izdvaja naniže)
|
|
||||||
pdvIznosStavke(s) {
|
|
||||||
const cena = parseFloat(s.cena) || 0
|
|
||||||
const stopa = parseFloat(s.pdv_stopa) || 0
|
|
||||||
if (!stopa) return 0
|
|
||||||
return cena - cena / (1 + stopa / 100)
|
|
||||||
},
|
|
||||||
osnovicaSvega() {
|
osnovicaSvega() {
|
||||||
return this.stavke.reduce((z, s) => {
|
return this.stavke.reduce((z, s) => {
|
||||||
const cena = parseFloat(s.cena) || 0
|
const cena = parseFloat(s.cena) || 0
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
Nazad na prodaju
|
Nazad na prodaju
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<form method="POST" action="/prodaja/nova">
|
<form method="POST" action="/prodaja/nova" @submit.prevent="posaljiProdaju($event)">
|
||||||
<input type="hidden" name="_csrf" value="{{.CsrfToken}}" />
|
<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>
|
||||||
@@ -104,7 +104,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- stavke -->
|
<!-- stavke -->
|
||||||
<div class="kartica forma-kartica animiraj" style="margin-bottom: 16px">
|
<!-- :style ovde vraća karticu na bazno stanje dok je miš nad zbirnim redom (Osnovica/PDV/Ukupno) —
|
||||||
|
inline stil pobeđuje .kartica:hover, jer čist CSS ne ume selektivno da isključi hover na detetu -->
|
||||||
|
<div class="kartica forma-kartica animiraj"
|
||||||
|
:style="'margin-bottom: 16px;' + (zbirnoHover ? 'box-shadow: var(--senka); border-color: var(--ivica); background: var(--kartica); transform: none;' : '')">
|
||||||
<div style="margin-bottom: 16px; padding-bottom: 14px; border-bottom: 0.5px solid var(--ivica); display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px;">
|
<div style="margin-bottom: 16px; padding-bottom: 14px; border-bottom: 0.5px solid var(--ivica); display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px;">
|
||||||
<span style="font-size: 16px; font-weight: 500; color: var(--tekst-glavni)">Stavke</span>
|
<span style="font-size: 16px; font-weight: 500; color: var(--tekst-glavni)">Stavke</span>
|
||||||
<button type="button" @click="dodajStavku()" class="btn-primarno" style="font-size: 13px; padding: 6px 14px">
|
<button type="button" @click="dodajStavku()" class="btn-primarno" style="font-size: 13px; padding: 6px 14px">
|
||||||
@@ -174,10 +177,7 @@
|
|||||||
<div x-show="prekoracenje(i)" style="font-size: 11px; color: #dc2626; margin-top: 1px; text-align: center; white-space: nowrap;">Prekoračena količina</div>
|
<div x-show="prekoracenje(i)" style="font-size: 11px; color: #dc2626; margin-top: 1px; text-align: center; white-space: nowrap;">Prekoračena količina</div>
|
||||||
</td>
|
</td>
|
||||||
<td style="padding: 8px 10px">
|
<td style="padding: 8px 10px">
|
||||||
<div style="display:flex;flex-direction:column;gap:2px;align-items:flex-end;">
|
<input type="number" :name="'cena_po_komadu[]'" x-model="stavka.cena" min="0" step="0.01" :disabled="isMobile" style="width: 100%; text-align: right" />
|
||||||
<input type="number" :name="'cena_po_komadu[]'" x-model="stavka.cena" min="0" step="0.01" :disabled="isMobile" style="width: 100%; text-align: right" />
|
|
||||||
<span x-show="pdvObveznik && stavka.pdv_stopa > 0" style="font-size:10px;color:var(--tekst-sporedni);" x-text="'PDV ' + stavka.pdv_stopa + '% uključen'"></span>
|
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
<td style="padding: 8px 10px">
|
<td style="padding: 8px 10px">
|
||||||
<input type="number" :name="'popust[]'" x-model="stavka.popust" min="0" max="100" step="0.01" :disabled="isMobile" style="width: 100%; text-align: right" />
|
<input type="number" :name="'popust[]'" x-model="stavka.popust" min="0" max="100" step="0.01" :disabled="isMobile" style="width: 100%; text-align: right" />
|
||||||
@@ -186,14 +186,14 @@
|
|||||||
<span x-text="fmtDin(ukupnoStavke(stavka)) + ' din'"></span>
|
<span x-text="fmtDin(ukupnoStavke(stavka)) + ' din'"></span>
|
||||||
</td>
|
</td>
|
||||||
<td style="padding: 8px 10px; text-align: center">
|
<td style="padding: 8px 10px; text-align: center">
|
||||||
<button type="button" @click="ukloniStavku(i)" x-show="stavke.length > 1"
|
<button type="button" @click="ukloniStavku(i)" x-show="stavkaImaVrednost(stavka)"
|
||||||
style="background: none; border: none; cursor: pointer; color: #dc2626; font-size: 18px; line-height: 1; padding: 2px 6px; border-radius: 4px; transition: background 0.15s;"
|
style="background: none; border: none; cursor: pointer; color: #dc2626; font-size: 18px; line-height: 1; padding: 2px 6px; border-radius: 4px; transition: background 0.15s;"
|
||||||
onmouseover="this.style.background = 'rgba(220,38,38,0.08)'" onmouseout="this.style.background = 'none'" title="Ukloni stavku">×</button>
|
onmouseover="this.style.background = 'rgba(220,38,38,0.08)'" onmouseout="this.style.background = 'none'" title="Ukloni stavku">×</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
</tbody>
|
</tbody>
|
||||||
<tfoot>
|
<tfoot @mouseenter="zbirnoHover = true" @mouseleave="zbirnoHover = false">
|
||||||
<tr x-show="pdvObveznik" x-cloak style="border-top: 0.5px solid var(--ivica)">
|
<tr x-show="pdvObveznik" x-cloak style="border-top: 0.5px solid var(--ivica)">
|
||||||
<td colspan="4" style="padding: 4px 10px; text-align: right; font-size: 12px; color: var(--tekst-sporedni);">Osnovica:</td>
|
<td colspan="4" style="padding: 4px 10px; text-align: right; font-size: 12px; color: var(--tekst-sporedni);">Osnovica:</td>
|
||||||
<td style="padding: 4px 10px; text-align: right; font-size: 12px; color: var(--tekst-sporedni);">
|
<td style="padding: 4px 10px; text-align: right; font-size: 12px; color: var(--tekst-sporedni);">
|
||||||
@@ -221,18 +221,15 @@
|
|||||||
|
|
||||||
<!-- mobilne kartice stavki -->
|
<!-- mobilne kartice stavki -->
|
||||||
<div class="stavke-kartice" style="display: none; flex-direction: column; gap: 10px">
|
<div class="stavke-kartice" style="display: none; flex-direction: column; gap: 10px">
|
||||||
<!-- pretraga na mobilnom -->
|
|
||||||
<input type="text" x-model="pretragaArtikal" @keydown.enter.prevent="dodajPoBarkodu()" placeholder="Pretraži artikle ili skeniraj barkod..." autocomplete="off" style="width:100%;">
|
|
||||||
<template x-for="(stavka, i) in stavke" :key="i">
|
<template x-for="(stavka, i) in stavke" :key="i">
|
||||||
<div style="border: 0.5px solid var(--ivica); border-radius: 8px; padding: 12px;">
|
<div style="border: 0.5px solid var(--ivica); border-radius: 8px; padding: 12px;">
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
||||||
<span style="font-size: 13px; font-weight: 500; color: var(--tekst-sporedni);" x-text="'Stavka ' + (i + 1)"></span>
|
<span style="font-size: 13px; font-weight: 500; color: var(--tekst-sporedni);" x-text="'Stavka ' + (i + 1)"></span>
|
||||||
<button type="button" @click="ukloniStavku(i)" x-show="stavke.length > 1"
|
<button type="button" @click="ukloniStavku(i)" x-show="stavkaImaVrednost(stavka)"
|
||||||
style="background: none; border: 0.5px solid #dc2626; color: #dc2626; cursor: pointer; font-size: 13px; padding: 2px 8px; border-radius: 4px;">Ukloni</button>
|
style="background: none; border: 0.5px solid #dc2626; color: #dc2626; cursor: pointer; font-size: 13px; padding: 2px 8px; border-radius: 4px;">Ukloni</button>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; flex-direction: column; gap: 10px">
|
<div style="display: flex; flex-direction: column; gap: 10px">
|
||||||
<input type="hidden" :name="'pdv_stopa[]'" :value="stavka.pdv_stopa">
|
<input type="hidden" :name="'pdv_stopa[]'" :value="stavka.pdv_stopa">
|
||||||
<div x-show="stavka.kategorija_naziv" style="font-size:11px;color:var(--tekst-sporedni);" x-text="stavka.kategorija_naziv"></div>
|
|
||||||
<div style="position: relative;">
|
<div style="position: relative;">
|
||||||
<label class="polje-labela">Artikal</label>
|
<label class="polje-labela">Artikal</label>
|
||||||
<input type="hidden" :name="'artikal_id[]'" :value="stavka.artikal_id" :disabled="!isMobile">
|
<input type="hidden" :name="'artikal_id[]'" :value="stavka.artikal_id" :disabled="!isMobile">
|
||||||
@@ -264,6 +261,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
<div x-show="stavka.kategorija_naziv" style="font-size:11px;color:var(--tekst-sporedni);margin-top:-4px;" x-text="stavka.kategorija_naziv"></div>
|
||||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
|
||||||
<div>
|
<div>
|
||||||
<label class="polje-labela">Količina</label>
|
<label class="polje-labela">Količina</label>
|
||||||
@@ -276,7 +274,6 @@
|
|||||||
<div>
|
<div>
|
||||||
<label class="polje-labela">Cena/kom (din){{if index .Moduli "pdv"}} sa PDV{{end}}</label>
|
<label class="polje-labela">Cena/kom (din){{if index .Moduli "pdv"}} sa PDV{{end}}</label>
|
||||||
<input type="number" :name="'cena_po_komadu[]'" x-model="stavka.cena" min="0" step="0.01" :disabled="!isMobile" style="width: 100%" />
|
<input type="number" :name="'cena_po_komadu[]'" x-model="stavka.cena" min="0" step="0.01" :disabled="!isMobile" style="width: 100%" />
|
||||||
<div x-show="pdvObveznik && stavka.pdv_stopa > 0" style="font-size:10px;color:var(--tekst-sporedni);margin-top:3px;" x-text="'PDV ' + stavka.pdv_stopa + '% uključen'"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
|
||||||
@@ -291,11 +288,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div x-show="pdvObveznik" x-cloak style="text-align: right; font-size: 12px; color: var(--tekst-sporedni); padding: 0 4px;">
|
<div @mouseenter="zbirnoHover = true" @mouseleave="zbirnoHover = false">
|
||||||
Osnovica: <span x-text="fmtDin(osnovicaSvega()) + ' din'"></span> · PDV: <span x-text="fmtDin(pdvSvega()) + ' din'"></span>
|
<div x-show="pdvObveznik" x-cloak style="text-align: right; font-size: 12px; color: var(--tekst-sporedni); padding: 0 4px;">
|
||||||
</div>
|
Osnovica: <span x-text="fmtDin(osnovicaSvega()) + ' din'"></span> · PDV: <span x-text="fmtDin(pdvSvega()) + ' din'"></span>
|
||||||
<div style="text-align: right; font-size: 15px; font-weight: 600; color: var(--tekst-glavni); padding: 8px 4px;">
|
</div>
|
||||||
<span x-text="pdvObveznik ? 'Ukupno za naplatu:' : 'Ukupno:'"></span> <span x-text="fmtDin(ukupnoSvega()) + ' din'"></span>
|
<div style="text-align: right; font-size: 15px; font-weight: 600; color: var(--tekst-glavni); padding: 8px 4px;">
|
||||||
|
<span x-text="pdvObveznik ? 'Ukupno za naplatu:' : 'Ukupno:'"></span> <span x-text="fmtDin(ukupnoSvega()) + ' din'"></span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -303,8 +302,8 @@
|
|||||||
<!-- 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() || imaPraznihStavki()" :style="(imaPrekoracenja() || imaPraznihStavki()) ? 'opacity:0.4;cursor:not-allowed;' : ''">
|
<button type="submit" class="btn-primarno" :disabled="imaPrekoracenja() || !imaPopunjenuStavku()" :style="(imaPrekoracenja() || !imaPopunjenuStavku()) ? 'opacity:0.4;cursor:not-allowed;' : ''">
|
||||||
Sačuvaj prodaju
|
Naplati
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
Reference in New Issue
Block a user