Merge grane feature/storno-modal-i-fiskalni-tab: storno modal, about:blank ispravka, dnevni pazar modul, štampaj kopiju u prodaji
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"ntech/internal/config"
|
||||
ntechsqlite "ntech/internal/db/sqlite"
|
||||
"ntech/internal/fiskal"
|
||||
"ntech/internal/middleware"
|
||||
@@ -56,6 +57,12 @@ func (h *Handler) FiskalniPazar(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
podaci := PodaciFiskalniPazar{PodaciStranice: ps, Danas: time.Now().Format("2006-01-02")}
|
||||
|
||||
if !h.modulUkljucen(r.Context(), config.ModulFiskalizacija) {
|
||||
podaci.Greska = "Fiskalizacija nije uključena u profilu firme — Podešavanja → Opšte → Pravni i poreski status."
|
||||
h.renderujTemplate(w, "fiskal_pazar", podaci)
|
||||
return
|
||||
}
|
||||
|
||||
klijent := h.fiskalKlijent()
|
||||
if klijent == nil {
|
||||
podaci.Greska = "Fiskalizacija nije podešena — unesi URL PFR servera u Podešavanja → Fiskalizacija."
|
||||
@@ -82,6 +89,11 @@ func (h *Handler) ZakljuciFiskalniDan(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := h.zahtevajDozvolu(w, r, "fiskal.zakljucenje"); !ok {
|
||||
return
|
||||
}
|
||||
if !h.modulUkljucen(r.Context(), config.ModulFiskalizacija) {
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Fiskalizacija nije uključena u profilu firme.")
|
||||
http.Redirect(w, r, "/fiskal/pazar", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
klijent := h.fiskalKlijent()
|
||||
if klijent == nil {
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Fiskalizacija nije podešena.")
|
||||
@@ -110,6 +122,11 @@ func (h *Handler) FiskalniIzvestaj(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if !h.modulUkljucen(r.Context(), config.ModulFiskalizacija) {
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Fiskalizacija nije uključena u profilu firme.")
|
||||
http.Redirect(w, r, "/fiskal/pazar", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
klijent := h.fiskalKlijent()
|
||||
if klijent == nil {
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Fiskalizacija nije podešena.")
|
||||
|
||||
@@ -226,6 +226,10 @@ document.addEventListener('alpine:init', () => {
|
||||
this.artikliOpcije = window._ntechArtikli || []
|
||||
this.klijenti = window._ntechKlijenti || []
|
||||
this.pdvObveznik = window._ntechPdvObveznik === true
|
||||
// ako firma ne fiskalizuje, ne otvaramo ni prazan tab u pripremiFiskalniTab() —
|
||||
// stranica detalja nikad neće imati FiskalniRacun da ga popuni, pa bi ostao
|
||||
// trajno na about:blank (v. docs/Greške.md)
|
||||
this.prikaziRacun = window._ntechFiskalizacija === true
|
||||
this.isMobile = window.matchMedia('(max-width: 768px)').matches
|
||||
window.matchMedia('(max-width: 768px)').addEventListener('change', e => {
|
||||
this.isMobile = e.matches
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
{{/* Knjigovodstvo — grupa: KIR/KPR/Obračun (modul „pdv") + Nivelacije (dozvola izmene cena) */}}
|
||||
{{$pdvVid := and (index .Moduli "pdv") (index .Dozvole "pdv.pregled")}}
|
||||
{{$kpoVid := and (index .Moduli "kpo") (index .Dozvole "kpo.pregled")}}
|
||||
{{$pazarVid := index .Dozvole "fiskal.pazar"}}
|
||||
{{$pazarVid := and (index .Moduli "fiskalizacija") (index .Dozvole "fiskal.pazar")}}
|
||||
{{$knjVid := or $pdvVid $kpoVid $pazarVid (index .Dozvole "artikal.izmeni")}}
|
||||
{{if $knjVid}}
|
||||
{{$knjAktivan := or (eq .Stranica "nivelacije") (eq .Stranica "kpo") (eq .Stranica "pdv-kir") (eq .Stranica "pdv-kpr") (eq .Stranica "pdv-obracun") (eq .Stranica "fiskal-pazar")}}
|
||||
|
||||
@@ -52,29 +52,6 @@
|
||||
</span>
|
||||
{{end}}
|
||||
</div>
|
||||
<a
|
||||
href="/prodaja/{{.Nalog.ID}}/stampa"
|
||||
target="_blank"
|
||||
class="btn-sekundarno"
|
||||
style="gap: 6px">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true">
|
||||
<polyline points="6 9 6 2 18 2 18 9" />
|
||||
<path
|
||||
d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2" />
|
||||
<rect x="6" y="14" width="12" height="8" />
|
||||
</svg>
|
||||
Štampaj
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
style="
|
||||
@@ -252,23 +229,66 @@
|
||||
Storno vraća sve artikle na stanje u magacinu. Nalog ostaje u evidenciji sa oznakom storniran.
|
||||
</div>
|
||||
</div>
|
||||
<form method="POST" action="/prodaja/storno/{{.Nalog.ID}}" style="display:flex;flex-direction:column;gap:8px;align-items:flex-end;">
|
||||
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
|
||||
<input type="text" name="razlog" placeholder="Razlog storniranja (opciono)" style="width:240px;">
|
||||
{{if .PotrebnaIdentKupca}}
|
||||
<input type="text" name="poreski_broj_kupca" placeholder="PIB ili JMBG kupca (obavezno za refundaciju)" required style="width:240px;">
|
||||
{{end}}
|
||||
<button type="submit" class="btn-upozorenje"
|
||||
data-potvrda="Stornirati nalog {{.Nalog.BrojNaloga}}?">
|
||||
Storniraj nalog
|
||||
</button>
|
||||
</form>
|
||||
<button type="button" class="btn-upozorenje" onclick="ntechPotvrdiPaOtvoriStorno(this, 'Stornirati nalog {{.Nalog.BrojNaloga}}?')">
|
||||
Storniraj nalog
|
||||
</button>
|
||||
<dialog class="premesti-modal" onclick="if(event.target===this)this.close()">
|
||||
<form method="dialog" class="premesti-zaglavlje">
|
||||
<h3>Storniranje naloga {{.Nalog.BrojNaloga}}</h3>
|
||||
<button type="submit" class="premesti-zatvori" aria-label="Zatvori">×</button>
|
||||
</form>
|
||||
<div style="display:flex;flex-direction:column;gap:10px;padding:16px;">
|
||||
<form method="POST" action="/prodaja/storno/{{.Nalog.ID}}" data-refresh="#sadrzaj" style="display:flex;flex-direction:column;gap:10px;">
|
||||
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
|
||||
<div>
|
||||
<label class="polje-labela">Razlog storniranja (opciono)</label>
|
||||
<input type="text" name="razlog" placeholder="Razlog storniranja" style="width:100%;padding:8px 12px;border:0.5px solid var(--ivica);border-radius:8px;font-size:13px;background:var(--pozadina);color:var(--tekst-glavni);outline:none;">
|
||||
</div>
|
||||
{{if .PotrebnaIdentKupca}}
|
||||
<div>
|
||||
<label class="polje-labela">PIB ili JMBG kupca</label>
|
||||
<input type="text" id="storno-pib" name="poreski_broj_kupca" placeholder="Obavezno za fiskalni refund" required
|
||||
style="width:100%;padding:8px 12px;border:1px solid #dc2626;border-radius:8px;font-size:13px;background:var(--pozadina);color:var(--tekst-glavni);outline:none;">
|
||||
</div>
|
||||
{{end}}
|
||||
<button type="submit" id="btn-storno-potvrdi" class="btn-upozorenje" style="width:100%;{{if .PotrebnaIdentKupca}}opacity:0.4;cursor:not-allowed;{{end}}" {{if .PotrebnaIdentKupca}}disabled{{end}}>
|
||||
Potvrdi storno
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
</div>
|
||||
<script>
|
||||
(function(){
|
||||
// isti obrazac kao u Servisu (servis_detalji.html) — v. komentar tamo zašto
|
||||
// se NE koristi ntechOtvoriModal (auto-submit na zatvaranje dialoga).
|
||||
window.ntechPotvrdiPaOtvoriStorno = function (btn, poruka) {
|
||||
window._ntechPotvrdi(poruka).then(function (ok) {
|
||||
if (!ok) return;
|
||||
var d = btn.nextElementSibling;
|
||||
if (d && d.tagName === 'DIALOG') d.showModal();
|
||||
});
|
||||
};
|
||||
var pib = document.getElementById('storno-pib');
|
||||
var btnPotvrdi = document.getElementById('btn-storno-potvrdi');
|
||||
if (pib && btnPotvrdi) {
|
||||
pib.addEventListener('input', function () {
|
||||
var ok = pib.value.trim() !== '';
|
||||
btnPotvrdi.disabled = !ok;
|
||||
btnPotvrdi.style.opacity = ok ? '1' : '0.4';
|
||||
btnPotvrdi.style.cursor = ok ? 'pointer' : 'not-allowed';
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
{{if .FiskalniRacun}}
|
||||
<div class="fiskalni-racun" style="padding-top:14px;border-top:0.5px solid var(--ivica);">
|
||||
<h3 style="margin:0 0 8px 0;font-size:16px;">Fiskalni račun</h3>
|
||||
<div style="margin-bottom:8px;display:flex;justify-content:space-between;align-items:center;">
|
||||
<h3 style="margin:0;font-size:16px;">Fiskalni račun</h3>
|
||||
<a href="/prodaja/{{.Nalog.ID}}/fiskalni-racun" target="_blank" class="btn-sekundarno" style="font-size:12px;padding:4px 12px;">Štampaj kopiju</a>
|
||||
</div>
|
||||
<div style="display:flex;gap:16px;flex-wrap:wrap;align-items:flex-start;">
|
||||
<div style="flex:1;min-width:200px;">
|
||||
<p style="margin:0 0 4px 0;"><strong>PFR broj:</strong> {{.FiskalniRacun.PfrBroj}}</p>
|
||||
@@ -296,6 +316,18 @@
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
{{else}}
|
||||
<script>
|
||||
(function(){
|
||||
// fiskalizacija je pokušana (racun=1 tražen pri naplati) ali računa nema —
|
||||
// PFR nedostupan ili slično (v. FiskalGreska ispod). Prazan tab je već otvoren
|
||||
// pre redirekta — zatvori ga umesto da ostane trajno na about:blank.
|
||||
if (new URLSearchParams(location.search).get('racun') === '1') {
|
||||
var t = window.open('', 'ntech-fiskalni-tab');
|
||||
if (t) t.close();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
{{if .FiskalGreska}}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
var _ntechArtikli = {{.ArtikliJSON}};
|
||||
var _ntechKlijenti = {{.KlijentiJSON}};
|
||||
var _ntechPdvObveznik = {{if index .Moduli "pdv"}}true{{else}}false{{end}};
|
||||
var _ntechFiskalizacija = {{if index .Moduli "fiskalizacija"}}true{{else}}false{{end}};
|
||||
</script>
|
||||
|
||||
<div style="width: 100%" x-data="prodajaForma">
|
||||
|
||||
@@ -214,13 +214,27 @@
|
||||
f.addEventListener('submit', function(){
|
||||
var cb = f.querySelector('#prikazi-racun');
|
||||
if (!cb || !cb.checked) return;
|
||||
// otvori prazan imenovani tab SINHRONO unutar submit-a (očuvan user
|
||||
// gesture) — isti obrazac kao pripremiFiskalniTab u Prodaji (ntech.js).
|
||||
// Naplata ovde ide preko AJAX-a (data-refresh), pa se popunjava tek
|
||||
// kad MutationObserver javi da je swap gotov — a to je već van user
|
||||
// gesture-a, pa direktan window.open() u tom trenutku brauzer tretira
|
||||
// kao blokiran popup i otvara about:blank (v. docs/Greške.md).
|
||||
var tab = window.open('', 'ntech-fiskalni-tab');
|
||||
var sadrzaj = document.getElementById('sadrzaj');
|
||||
if (!sadrzaj) return;
|
||||
if (!sadrzaj) {
|
||||
if (tab) tab.close();
|
||||
return;
|
||||
}
|
||||
var observer = new MutationObserver(function(){
|
||||
observer.disconnect();
|
||||
// jedan prozor — server u istu stranicu ubacuje i povraćaj viška
|
||||
// avansa (ako postoji), da brauzer ne blokira drugi popup
|
||||
window.open('/servis/{{.Nalog.ID}}/fiskalni-racun', '_blank');
|
||||
if (tab && !tab.closed) {
|
||||
tab.location.href = '/servis/{{.Nalog.ID}}/fiskalni-racun';
|
||||
} else {
|
||||
window.open('/servis/{{.Nalog.ID}}/fiskalni-racun', '_blank');
|
||||
}
|
||||
});
|
||||
observer.observe(sadrzaj.parentElement, {childList: true});
|
||||
});
|
||||
@@ -1124,19 +1138,63 @@
|
||||
Storno vraća ugrađene delove na stanje u magacinu. Nalog ostaje u evidenciji sa oznakom storniran.
|
||||
</div>
|
||||
</div>
|
||||
<form method="POST" action="/servis/storno/{{.Nalog.ID}}" style="display:flex;flex-direction:column;gap:8px;align-items:flex-end;">
|
||||
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
|
||||
<input type="text" name="razlog" placeholder="Razlog storniranja (opciono)" style="width:240px;">
|
||||
{{if .PotrebnaIdentKupca}}
|
||||
<input type="text" name="poreski_broj_kupca" placeholder="PIB ili JMBG kupca (obavezno za refundaciju)" required style="width:240px;">
|
||||
{{end}}
|
||||
<button type="submit" class="btn-upozorenje"
|
||||
data-potvrda="Stornirati nalog {{.Nalog.BrojNaloga}}?">
|
||||
Storniraj nalog
|
||||
</button>
|
||||
</form>
|
||||
<button type="button" class="btn-upozorenje" onclick="ntechPotvrdiPaOtvoriStorno(this, 'Stornirati nalog {{.Nalog.BrojNaloga}}?')">
|
||||
Storniraj nalog
|
||||
</button>
|
||||
<dialog class="premesti-modal" onclick="if(event.target===this)this.close()">
|
||||
<form method="dialog" class="premesti-zaglavlje">
|
||||
<h3>Storniranje naloga {{.Nalog.BrojNaloga}}</h3>
|
||||
<button type="submit" class="premesti-zatvori" aria-label="Zatvori">×</button>
|
||||
</form>
|
||||
<div style="display:flex;flex-direction:column;gap:10px;padding:16px;">
|
||||
<form method="POST" action="/servis/storno/{{.Nalog.ID}}" data-refresh="#sadrzaj" style="display:flex;flex-direction:column;gap:10px;">
|
||||
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
|
||||
<div>
|
||||
<label class="polje-labela">Razlog storniranja (opciono)</label>
|
||||
<input type="text" name="razlog" placeholder="Razlog storniranja" style="width:100%;padding:8px 12px;border:0.5px solid var(--ivica);border-radius:8px;font-size:13px;background:var(--pozadina);color:var(--tekst-glavni);outline:none;">
|
||||
</div>
|
||||
{{if .PotrebnaIdentKupca}}
|
||||
<div>
|
||||
<label class="polje-labela">PIB ili JMBG kupca</label>
|
||||
<input type="text" id="storno-pib" name="poreski_broj_kupca" placeholder="Obavezno za fiskalni refund" required
|
||||
style="width:100%;padding:8px 12px;border:1px solid #dc2626;border-radius:8px;font-size:13px;background:var(--pozadina);color:var(--tekst-glavni);outline:none;">
|
||||
</div>
|
||||
{{end}}
|
||||
<button type="submit" id="btn-storno-potvrdi" class="btn-upozorenje" style="width:100%;{{if .PotrebnaIdentKupca}}opacity:0.4;cursor:not-allowed;{{end}}" {{if .PotrebnaIdentKupca}}disabled{{end}}>
|
||||
Potvrdi storno
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function(){
|
||||
// storno: potvrda namere PRE otvaranja modala (dvostepeni tok), pa tek onda
|
||||
// polje za PIB/JMBG (obavezno samo ako je nalog fiskalizovan — v. .PotrebnaIdentKupca).
|
||||
// NE koristi ntechOtvoriModal ovde — ta funkcija snima početno stanje forme i
|
||||
// auto-submituje na zatvaranje dialoga (za popup polja bez dugmeta Sačuvaj), što
|
||||
// bi za storno značilo da se nalog stornira i pri slučajnom zatvaranju bez klika
|
||||
// na „Potvrdi storno".
|
||||
window.ntechPotvrdiPaOtvoriStorno = function (btn, poruka) {
|
||||
window._ntechPotvrdi(poruka).then(function (ok) {
|
||||
if (!ok) return;
|
||||
var d = btn.nextElementSibling;
|
||||
if (d && d.tagName === 'DIALOG') d.showModal();
|
||||
});
|
||||
};
|
||||
var pib = document.getElementById('storno-pib');
|
||||
var btnPotvrdi = document.getElementById('btn-storno-potvrdi');
|
||||
if (pib && btnPotvrdi) {
|
||||
pib.addEventListener('input', function () {
|
||||
var ok = pib.value.trim() !== '';
|
||||
btnPotvrdi.disabled = !ok;
|
||||
btnPotvrdi.style.opacity = ok ? '1' : '0.4';
|
||||
btnPotvrdi.style.cursor = ok ? 'pointer' : 'not-allowed';
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
<!-- zona za brisanje -->
|
||||
|
||||
Reference in New Issue
Block a user