feat(pdv): šifarnik PDV stopa — handler, rute i UI (Faza 1)
Handleri (prikaz, dodaj, izmeni, arhiviraj/vrati) sa validacijom i flash porukama; rute pod /admin/podesavanja/pdv-stope (dozvole podesavanja.*); stranica pdv_stope registrovana u kes.go i dodata u meni Podešavanja. Šifarnik je opšti (bez RequireModul) jer ga koristi i kalkulacija.
This commit is contained in:
+15
-7
@@ -8,9 +8,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"mime"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -87,12 +87,14 @@ func main() {
|
|||||||
|
|
||||||
db, err := sqlite.OtvoriDB(putanjaBaze)
|
db, err := sqlite.OtvoriDB(putanjaBaze)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Greška pri otvaranju baze", "error", err); os.Exit(1)
|
slog.Error("Greška pri otvaranju baze", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
|
|
||||||
if err := sqlite.PokreniMigracije(db, migrFS); err != nil {
|
if err := sqlite.PokreniMigracije(db, migrFS); err != nil {
|
||||||
slog.Error("Greška pri migracijama", "error", err); os.Exit(1)
|
slog.Error("Greška pri migracijama", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
slog.Info("migracije uspešno izvršene")
|
slog.Info("migracije uspešno izvršene")
|
||||||
|
|
||||||
@@ -111,7 +113,8 @@ func main() {
|
|||||||
// ključ za šifrovanje TOTP tajni u mirovanju (AES-256-GCM)
|
// ključ za šifrovanje TOTP tajni u mirovanju (AES-256-GCM)
|
||||||
totpKljuc, err := ucitajTotpKljuc()
|
totpKljuc, err := ucitajTotpKljuc()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Greška pri učitavanju ključa za TOTP", "error", err); os.Exit(1)
|
slog.Error("Greška pri učitavanju ključa za TOTP", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// jednokratno šifruj eventualne stare TOTP tajne koje su ostale kao čist tekst
|
// jednokratno šifruj eventualne stare TOTP tajne koje su ostale kao čist tekst
|
||||||
@@ -137,7 +140,8 @@ func main() {
|
|||||||
if os.Getenv("NTECH_ENV") == "production" {
|
if os.Getenv("NTECH_ENV") == "production" {
|
||||||
kes, err := handler.KreirajKes(templFS)
|
kes, err := handler.KreirajKes(templFS)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Greška pri kreiranju keša šablona", "error", err); os.Exit(1)
|
slog.Error("Greška pri kreiranju keša šablona", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
h.Templates = kes
|
h.Templates = kes
|
||||||
slog.Info("keš šablona kreiran", "broj", len(kes))
|
slog.Info("keš šablona kreiran", "broj", len(kes))
|
||||||
@@ -229,6 +233,10 @@ func main() {
|
|||||||
r.Get("/admin/podesavanja/opste", h.PodesavanjaOpste)
|
r.Get("/admin/podesavanja/opste", h.PodesavanjaOpste)
|
||||||
r.Get("/admin/podesavanja/izgled", h.PodesavanjaIzgled)
|
r.Get("/admin/podesavanja/izgled", h.PodesavanjaIzgled)
|
||||||
r.Get("/admin/podesavanja/sistem", h.PodesavanjaSistem)
|
r.Get("/admin/podesavanja/sistem", h.PodesavanjaSistem)
|
||||||
|
r.Get("/admin/podesavanja/pdv-stope", h.PdvStope)
|
||||||
|
r.With(doz("podesavanja.izmeni")).Post("/podesavanja/pdv-stope/dodaj", h.DodajPdvStopu)
|
||||||
|
r.With(doz("podesavanja.izmeni")).Post("/podesavanja/pdv-stope/{id}/izmeni", h.IzmeniPdvStopu)
|
||||||
|
r.With(doz("podesavanja.izmeni")).Post("/podesavanja/pdv-stope/{id}/aktivnost", h.PromeniAktivnostPdvStope)
|
||||||
r.With(doz("podesavanja.izmeni")).Post("/podesavanja/sacuvaj", h.SacuvajPodesavanja)
|
r.With(doz("podesavanja.izmeni")).Post("/podesavanja/sacuvaj", h.SacuvajPodesavanja)
|
||||||
r.With(doz("podesavanja.izmeni")).Post("/podesavanja/logo", h.OtpremiLogo)
|
r.With(doz("podesavanja.izmeni")).Post("/podesavanja/logo", h.OtpremiLogo)
|
||||||
r.With(doz("podesavanja.login_pozadina")).Post("/podesavanja/login-pozadina", h.OtpremiLoginPozadinu)
|
r.With(doz("podesavanja.login_pozadina")).Post("/podesavanja/login-pozadina", h.OtpremiLoginPozadinu)
|
||||||
@@ -327,7 +335,8 @@ func main() {
|
|||||||
slog.Info("NTech pokrenut", "port", port)
|
slog.Info("NTech pokrenut", "port", port)
|
||||||
err = http.ListenAndServe(":"+port, r)
|
err = http.ListenAndServe(":"+port, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("port je zauzet ili nije dostupan", "port", port); os.Exit(1)
|
slog.Error("port je zauzet ili nije dostupan", "port", port)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -419,4 +428,3 @@ func ocistiStareBackupe(folder string, max int) {
|
|||||||
_ = os.Remove(f)
|
_ = os.Remove(f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ var saSidebar = []string{
|
|||||||
"magacin", "magacin_forma",
|
"magacin", "magacin_forma",
|
||||||
"nabavke", "nabavka_forma", "nabavka_detalji",
|
"nabavke", "nabavka_forma", "nabavka_detalji",
|
||||||
"podesavanja", "podesavanja_opste", "podesavanja_izgled", "podesavanja_sistem",
|
"podesavanja", "podesavanja_opste", "podesavanja_izgled", "podesavanja_sistem",
|
||||||
|
"pdv_stope",
|
||||||
"podsetnici", "podsetnik_forma",
|
"podsetnici", "podsetnik_forma",
|
||||||
"profil_tema",
|
"profil_tema",
|
||||||
"prodaja", "prodaja_detalji", "prodaja_forma",
|
"prodaja", "prodaja_detalji", "prodaja_forma",
|
||||||
|
|||||||
@@ -0,0 +1,154 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"ntech/internal/db/sqlite"
|
||||||
|
"ntech/internal/middleware"
|
||||||
|
"ntech/internal/model"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
// validneOznakeStope su dozvoljene oznake PDV stope (vrsta po zakonu)
|
||||||
|
var validneOznakeStope = map[string]bool{
|
||||||
|
"opsta": true,
|
||||||
|
"posebna": true,
|
||||||
|
"oslobodjeno": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// PodaciPdvStope su podaci za stranicu šifarnika PDV stopa
|
||||||
|
type PodaciPdvStope struct {
|
||||||
|
model.PodaciStranice
|
||||||
|
Stope []model.PdvStopa
|
||||||
|
}
|
||||||
|
|
||||||
|
// PdvStope renderuje šifarnik PDV stopa (sve stope, uključujući arhivirane)
|
||||||
|
func (h *Handler) PdvStope(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if _, ok := h.zahtevajDozvolu(w, r, "podesavanja.pregled"); !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stope, err := h.PdvStopeRepo.Lista(r.Context(), false)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Greška pri učitavanju PDV stopa", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ps := h.popuniPodaciStranice(r, podesavanja)
|
||||||
|
ps.Stranica = "podesavanja-pdv-stope"
|
||||||
|
ps.NaslovStranice = "PDV stope"
|
||||||
|
h.renderujTemplate(w, "pdv_stope", PodaciPdvStope{PodaciStranice: ps, Stope: stope})
|
||||||
|
}
|
||||||
|
|
||||||
|
// parsePdvStopuForma čita i proverava polja forme; vraća popunjenu stopu i poruku o grešci
|
||||||
|
func parsePdvStopuForma(r *http.Request) (model.PdvStopa, string) {
|
||||||
|
naziv := strings.TrimSpace(r.FormValue("naziv"))
|
||||||
|
oznaka := strings.TrimSpace(r.FormValue("oznaka"))
|
||||||
|
stopaTekst := strings.TrimSpace(strings.Replace(r.FormValue("stopa"), ",", ".", 1))
|
||||||
|
redosled, _ := strconv.Atoi(strings.TrimSpace(r.FormValue("redosled")))
|
||||||
|
|
||||||
|
if naziv == "" {
|
||||||
|
return model.PdvStopa{}, "Naziv stope je obavezan."
|
||||||
|
}
|
||||||
|
if !validneOznakeStope[oznaka] {
|
||||||
|
return model.PdvStopa{}, "Oznaka mora biti opšta, posebna ili oslobođeno."
|
||||||
|
}
|
||||||
|
stopa, err := strconv.ParseFloat(stopaTekst, 64)
|
||||||
|
if err != nil || stopa < 0 || stopa > 100 {
|
||||||
|
return model.PdvStopa{}, "Stopa mora biti broj između 0 i 100."
|
||||||
|
}
|
||||||
|
|
||||||
|
return model.PdvStopa{
|
||||||
|
Naziv: naziv,
|
||||||
|
Stopa: stopa,
|
||||||
|
Oznaka: oznaka,
|
||||||
|
Aktivna: true,
|
||||||
|
Redosled: redosled,
|
||||||
|
}, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// DodajPdvStopu prima POST i upisuje novu stopu u šifarnik
|
||||||
|
func (h *Handler) DodajPdvStopu(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if _, ok := h.zahtevajDozvolu(w, r, "podesavanja.izmeni"); !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stopa, greska := parsePdvStopuForma(r)
|
||||||
|
if greska != "" {
|
||||||
|
middleware.SetFlash(w, r, h.DB, "greska", greska)
|
||||||
|
http.Redirect(w, r, "/admin/podesavanja/pdv-stope", http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := h.PdvStopeRepo.Kreiraj(r.Context(), &stopa); err != nil {
|
||||||
|
http.Error(w, "Greška pri čuvanju PDV stope", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
middleware.SetFlash(w, r, h.DB, "uspeh", "PDV stopa je dodata.")
|
||||||
|
http.Redirect(w, r, "/admin/podesavanja/pdv-stope", http.StatusSeeOther)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IzmeniPdvStopu prima POST i menja postojeću stopu
|
||||||
|
func (h *Handler) IzmeniPdvStopu(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if _, ok := h.zahtevajDozvolu(w, r, "podesavanja.izmeni"); !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, err := parseID(chi.URLParam(r, "id"))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Neispravan ID stope", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stopa, greska := parsePdvStopuForma(r)
|
||||||
|
if greska != "" {
|
||||||
|
middleware.SetFlash(w, r, h.DB, "greska", greska)
|
||||||
|
http.Redirect(w, r, "/admin/podesavanja/pdv-stope", http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stopa.ID = id
|
||||||
|
if err := h.PdvStopeRepo.Izmeni(r.Context(), &stopa); err != nil {
|
||||||
|
http.Error(w, "Greška pri izmeni PDV stope", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
middleware.SetFlash(w, r, h.DB, "uspeh", "PDV stopa je izmenjena.")
|
||||||
|
http.Redirect(w, r, "/admin/podesavanja/pdv-stope", http.StatusSeeOther)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PromeniAktivnostPdvStope arhivira ili vraća stopu u upotrebu (toggle, bez brisanja)
|
||||||
|
func (h *Handler) PromeniAktivnostPdvStope(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if _, ok := h.zahtevajDozvolu(w, r, "podesavanja.izmeni"); !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, err := parseID(chi.URLParam(r, "id"))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Neispravan ID stope", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
postojeca, err := h.PdvStopeRepo.DohvatiID(r.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "PDV stopa nije pronađena", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := h.PdvStopeRepo.PostaviAktivnu(r.Context(), id, !postojeca.Aktivna); err != nil {
|
||||||
|
http.Error(w, "Greška pri promeni statusa PDV stope", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
poruka := "PDV stopa je arhivirana."
|
||||||
|
if !postojeca.Aktivna {
|
||||||
|
poruka = "PDV stopa je vraćena u upotrebu."
|
||||||
|
}
|
||||||
|
middleware.SetFlash(w, r, h.DB, "uspeh", poruka)
|
||||||
|
http.Redirect(w, r, "/admin/podesavanja/pdv-stope", http.StatusSeeOther)
|
||||||
|
}
|
||||||
@@ -147,19 +147,19 @@
|
|||||||
{{if index .Dozvole "podesavanja.pregled"}}
|
{{if index .Dozvole "podesavanja.pregled"}}
|
||||||
<div>
|
<div>
|
||||||
<button type="button" data-podmeni-dugme
|
<button type="button" data-podmeni-dugme
|
||||||
class="nav-stavka {{if or (eq .Stranica "podesavanja") (eq .Stranica "podesavanja-opste") (eq .Stranica "podesavanja-izgled") (eq .Stranica "podesavanja-sistem") (eq .Stranica "dozvole")}}aktivan{{end}}"
|
class="nav-stavka {{if or (eq .Stranica "podesavanja") (eq .Stranica "podesavanja-opste") (eq .Stranica "podesavanja-izgled") (eq .Stranica "podesavanja-sistem") (eq .Stranica "podesavanja-pdv-stope") (eq .Stranica "dozvole")}}aktivan{{end}}"
|
||||||
style="width:100%;background:none;border:none;cursor:pointer;">
|
style="width:100%;background:none;border:none;cursor:pointer;">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06-.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06-.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
|
||||||
<span>Podešavanja</span>
|
<span>Podešavanja</span>
|
||||||
<span class="nav-strelica">
|
<span class="nav-strelica">
|
||||||
<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"
|
<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"
|
||||||
style="transition:transform 0.2s;transform:{{if or (eq .Stranica "podesavanja") (eq .Stranica "podesavanja-opste") (eq .Stranica "podesavanja-izgled") (eq .Stranica "podesavanja-sistem") (eq .Stranica "dozvole")}}rotate(180deg){{else}}rotate(0deg){{end}}">
|
style="transition:transform 0.2s;transform:{{if or (eq .Stranica "podesavanja") (eq .Stranica "podesavanja-opste") (eq .Stranica "podesavanja-izgled") (eq .Stranica "podesavanja-sistem") (eq .Stranica "podesavanja-pdv-stope") (eq .Stranica "dozvole")}}rotate(180deg){{else}}rotate(0deg){{end}}">
|
||||||
<polyline points="6 9 12 15 18 9"/>
|
<polyline points="6 9 12 15 18 9"/>
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<span class="nav-tooltip">Podešavanja</span>
|
<span class="nav-tooltip">Podešavanja</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="nav-podmeni {{if or (eq .Stranica "podesavanja") (eq .Stranica "podesavanja-opste") (eq .Stranica "podesavanja-izgled") (eq .Stranica "podesavanja-sistem") (eq .Stranica "dozvole")}}otvoren{{end}}">
|
<div class="nav-podmeni {{if or (eq .Stranica "podesavanja") (eq .Stranica "podesavanja-opste") (eq .Stranica "podesavanja-izgled") (eq .Stranica "podesavanja-sistem") (eq .Stranica "podesavanja-pdv-stope") (eq .Stranica "dozvole")}}otvoren{{end}}">
|
||||||
<a href="/admin/podesavanja/opste" class="nav-stavka nav-podstavka {{if eq .Stranica "podesavanja-opste"}}aktivan{{end}}">
|
<a href="/admin/podesavanja/opste" class="nav-stavka nav-podstavka {{if eq .Stranica "podesavanja-opste"}}aktivan{{end}}">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21h18"/><path d="M5 21V7a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v14"/><path d="M9 21v-5h6v5"/><path d="M9 9h.01M15 9h.01M9 13h.01M15 13h.01"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21h18"/><path d="M5 21V7a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v14"/><path d="M9 21v-5h6v5"/><path d="M9 9h.01M15 9h.01M9 13h.01M15 13h.01"/></svg>
|
||||||
<span>Opšte</span>
|
<span>Opšte</span>
|
||||||
@@ -175,6 +175,11 @@
|
|||||||
<span>Sistem</span>
|
<span>Sistem</span>
|
||||||
<span class="nav-tooltip">Sistem</span>
|
<span class="nav-tooltip">Sistem</span>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="/admin/podesavanja/pdv-stope" class="nav-stavka nav-podstavka {{if eq .Stranica "podesavanja-pdv-stope"}}aktivan{{end}}">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="5" x2="5" y2="19"/><circle cx="6.5" cy="6.5" r="2.5"/><circle cx="17.5" cy="17.5" r="2.5"/></svg>
|
||||||
|
<span>PDV stope</span>
|
||||||
|
<span class="nav-tooltip">PDV stope</span>
|
||||||
|
</a>
|
||||||
{{if or (eq .KorisnikUloga "superadmin") (eq .KorisnikUloga "admin")}}
|
{{if or (eq .KorisnikUloga "superadmin") (eq .KorisnikUloga "admin")}}
|
||||||
<a href="/admin/dozvole" class="nav-stavka nav-podstavka {{if eq .Stranica "dozvole"}}aktivan{{end}}">
|
<a href="/admin/dozvole" class="nav-stavka nav-podstavka {{if eq .Stranica "dozvole"}}aktivan{{end}}">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
{{template "base" .}}
|
||||||
|
|
||||||
|
{{define "naslov"}}Podešavanja — PDV stope — NTech{{end}}
|
||||||
|
|
||||||
|
{{define "sadrzaj"}}
|
||||||
|
<div class="stranica-stack" style="width:100%;max-width:100%;">
|
||||||
|
|
||||||
|
<!-- postojeće stope: svaki red je forma za izmenu + zasebna forma za arhiviranje -->
|
||||||
|
<div class="kartica animiraj" style="margin-bottom:16px;">
|
||||||
|
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px;padding-bottom:12px;border-bottom:0.5px solid var(--ivica);">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--sb-akcent)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="5" x2="5" y2="19"/><circle cx="6.5" cy="6.5" r="2.5"/><circle cx="17.5" cy="17.5" r="2.5"/></svg>
|
||||||
|
<span style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">Šifarnik PDV stopa</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pomocni-tekst" style="font-size:12px;margin-bottom:14px;">
|
||||||
|
Stope se koriste u kalkulaciji cena i PDV evidenciji. Stopa koja je negde upotrebljena se ne briše — arhivira se (nestaje iz padajućih lista, a postojeći zapisi ostaju ispravni).
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{if not .Stope}}
|
||||||
|
<div class="pomocni-tekst">Nema unetih PDV stopa.</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{range .Stope}}
|
||||||
|
<div style="display:flex;gap:8px;align-items:flex-end;flex-wrap:wrap;padding:10px 0;border-bottom:0.5px solid var(--ivica);{{if not .Aktivna}}opacity:0.5;{{end}}">
|
||||||
|
<form method="POST" action="/podesavanja/pdv-stope/{{.ID}}/izmeni" style="display:flex;gap:8px;align-items:flex-end;flex-wrap:wrap;flex:1;">
|
||||||
|
<div style="flex:2;min-width:140px;">
|
||||||
|
<label class="polje-labela">Naziv</label>
|
||||||
|
<input type="text" name="naziv" value="{{.Naziv}}" style="width:100%;padding:7px 10px;border:0.5px solid var(--ivica);border-radius:8px;font-size:13px;background:var(--pozadina);color:var(--tekst-glavni);outline:none;">
|
||||||
|
</div>
|
||||||
|
<div style="width:80px;">
|
||||||
|
<label class="polje-labela">Stopa %</label>
|
||||||
|
<input type="text" name="stopa" value="{{printf "%g" .Stopa}}" style="width:100%;padding:7px 10px;border:0.5px solid var(--ivica);border-radius:8px;font-size:13px;background:var(--pozadina);color:var(--tekst-glavni);outline:none;">
|
||||||
|
</div>
|
||||||
|
<div style="width:150px;">
|
||||||
|
<label class="polje-labela">Vrsta</label>
|
||||||
|
<select name="oznaka" style="width:100%;padding:7px 10px;border:0.5px solid var(--ivica);border-radius:8px;font-size:13px;background:var(--pozadina);color:var(--tekst-glavni);outline:none;">
|
||||||
|
<option value="opsta" {{if eq .Oznaka "opsta"}}selected{{end}}>Opšta</option>
|
||||||
|
<option value="posebna" {{if eq .Oznaka "posebna"}}selected{{end}}>Posebna</option>
|
||||||
|
<option value="oslobodjeno" {{if eq .Oznaka "oslobodjeno"}}selected{{end}}>Oslobođeno</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div style="width:80px;">
|
||||||
|
<label class="polje-labela">Redosled</label>
|
||||||
|
<input type="text" name="redosled" value="{{.Redosled}}" style="width:100%;padding:7px 10px;border:0.5px solid var(--ivica);border-radius:8px;font-size:13px;background:var(--pozadina);color:var(--tekst-glavni);outline:none;">
|
||||||
|
</div>
|
||||||
|
<button type="submit" style="padding:7px 14px;background:var(--sb-akcent);color:#fff;border:none;border-radius:8px;font-size:13px;cursor:pointer;white-space:nowrap;">Sačuvaj</button>
|
||||||
|
</form>
|
||||||
|
<form method="POST" action="/podesavanja/pdv-stope/{{.ID}}/aktivnost">
|
||||||
|
<button type="submit" style="padding:7px 14px;background:var(--sb-aktivan);color:var(--tekst-jak);border:0.5px solid var(--ivica);border-radius:8px;font-size:13px;cursor:pointer;white-space:nowrap;">{{if .Aktivna}}Arhiviraj{{else}}Vrati{{end}}</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- dodavanje nove stope -->
|
||||||
|
<form method="POST" action="/podesavanja/pdv-stope/dodaj">
|
||||||
|
<div class="kartica animiraj" style="margin-bottom:16px;">
|
||||||
|
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px;padding-bottom:12px;border-bottom:0.5px solid var(--ivica);">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--sb-akcent)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||||||
|
<span style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">Nova stopa</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display:flex;gap:8px;align-items:flex-end;flex-wrap:wrap;">
|
||||||
|
<div style="flex:2;min-width:140px;">
|
||||||
|
<label class="polje-labela">Naziv</label>
|
||||||
|
<input type="text" name="naziv" placeholder="npr. Opšta stopa" style="width:100%;padding:7px 10px;border:0.5px solid var(--ivica);border-radius:8px;font-size:13px;background:var(--pozadina);color:var(--tekst-glavni);outline:none;">
|
||||||
|
</div>
|
||||||
|
<div style="width:80px;">
|
||||||
|
<label class="polje-labela">Stopa %</label>
|
||||||
|
<input type="text" name="stopa" placeholder="20" style="width:100%;padding:7px 10px;border:0.5px solid var(--ivica);border-radius:8px;font-size:13px;background:var(--pozadina);color:var(--tekst-glavni);outline:none;">
|
||||||
|
</div>
|
||||||
|
<div style="width:150px;">
|
||||||
|
<label class="polje-labela">Vrsta</label>
|
||||||
|
<select name="oznaka" style="width:100%;padding:7px 10px;border:0.5px solid var(--ivica);border-radius:8px;font-size:13px;background:var(--pozadina);color:var(--tekst-glavni);outline:none;">
|
||||||
|
<option value="opsta">Opšta</option>
|
||||||
|
<option value="posebna">Posebna</option>
|
||||||
|
<option value="oslobodjeno">Oslobođeno</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div style="width:80px;">
|
||||||
|
<label class="polje-labela">Redosled</label>
|
||||||
|
<input type="text" name="redosled" placeholder="0" style="width:100%;padding:7px 10px;border:0.5px solid var(--ivica);border-radius:8px;font-size:13px;background:var(--pozadina);color:var(--tekst-glavni);outline:none;">
|
||||||
|
</div>
|
||||||
|
<button type="submit" style="padding:7px 16px;background:var(--sb-akcent);color:#fff;border:none;border-radius:8px;font-size:13px;font-weight:500;cursor:pointer;white-space:nowrap;">Dodaj</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
Reference in New Issue
Block a user