Refaktoring: uklanjanje globalne teme i app pozadine, dozvole u podešavanja, UI ispravke

- Uklonjena globalna tema i pozadinska slika aplikacije (ostala samo lična pozadina po korisniku)
- Uklonjena animacija treperenja pozadine pri navigaciji; dodat sessionStorage za instant prikaz
- Dozvole premeštene iz sidebar-a u Podešavanja → Sistem; vidljive i adminu (samo Radnik kolona)
- Admin može menjati samo dozvole uloge Radnik, superadmin menja i Radnik i Admin
- Zatamnjivanje kartice NTech na stranici prijave — novi slider u Podešavanja → Izgled
- Upozorenje na dashboard-u (kritične zalihe) — popravljen kontrast boje
This commit is contained in:
2026-06-06 21:07:01 +02:00
parent 5d94ea34cf
commit 8cec26a03f
22 changed files with 269 additions and 888 deletions
+26 -350
View File
@@ -18,8 +18,6 @@ import (
ntechsqlite "ntech/internal/db/sqlite"
"ntech/internal/middleware"
"ntech/internal/model"
"github.com/go-chi/chi/v5"
)
// PodaciPodesavanja su podaci za stranicu podešavanja
@@ -32,20 +30,16 @@ type PodaciPodesavanja struct {
PIB string
LogoTip string
LogoPutanja string
GlobalnaTema string // stvarna vrednost iz podešavanja (ne senči PodaciStranice.Tema)
Sacuvano bool
Verzija string
LogoGreska string
BackupVracen bool
Backupi []BackupInfo
LoginPozadina string
LoginPozadinaOpacity string
LoginPozadinaBlurPozadine string
LoginPozadinaBlurKartice string
AppPozadina string
AppPozadinaOpacity string
AppPozadinaBlur string
AppPozadinaBlurPozadine string
LoginPozadina string
LoginPozadinaOpacity string
LoginPozadinaBlurPozadine string
LoginPozadinaBlurKartice string
LoginPozadinaZatamnjenjeKartice string
}
// BackupInfo opisuje jedan backup fajl
@@ -83,15 +77,6 @@ func (h *Handler) Podesavanja(w http.ResponseWriter, r *http.Request) {
PIB: podesavanja["pib"],
LogoTip: podesavanja["logo_tip"],
LogoPutanja: podesavanja["logo_putanja"],
GlobalnaTema: func() string {
if v := podesavanja["globalna_tema"]; v != "" {
return v
}
if v := podesavanja["tema"]; v != "" {
return v
}
return "tamna"
}(),
Sacuvano: r.URL.Query().Get("sacuvano") == "1",
BackupVracen: r.URL.Query().Get("sacuvano") == "vraceno",
Verzija: h.Verzija,
@@ -113,19 +98,8 @@ func (h *Handler) Podesavanja(w http.ResponseWriter, r *http.Request) {
if v == "" { return "12" }
return v
}(),
AppPozadina: podesavanja["app_pozadina"],
AppPozadinaOpacity: func() string {
v := podesavanja["app_pozadina_opacity"]
if v == "" { return "50" }
return v
}(),
AppPozadinaBlur: func() string {
v := podesavanja["app_pozadina_blur"]
if v == "" { return "12" }
return v
}(),
AppPozadinaBlurPozadine: func() string {
v := podesavanja["app_pozadina_blur_pozadine"]
LoginPozadinaZatamnjenjeKartice: func() string {
v := podesavanja["login_pozadina_zatamnjenje_kartice"]
if v == "" { return "0" }
return v
}(),
@@ -260,14 +234,12 @@ func (h *Handler) SacuvajPodesavanja(w http.ResponseWriter, r *http.Request) {
}
polja := map[string]string{
"naziv_firme": r.FormValue("naziv_firme"),
"podnazlov": r.FormValue("podnazlov"),
"adresa": r.FormValue("adresa"),
"telefon": r.FormValue("telefon"),
"pib": r.FormValue("pib"),
"logo_tip": r.FormValue("logo_tip"),
"tema": r.FormValue("tema"),
"globalna_tema": r.FormValue("globalna_tema"),
"naziv_firme": r.FormValue("naziv_firme"),
"podnazlov": r.FormValue("podnazlov"),
"adresa": r.FormValue("adresa"),
"telefon": r.FormValue("telefon"),
"pib": r.FormValue("pib"),
"logo_tip": r.FormValue("logo_tip"),
}
for kljuc, vrednost := range polja {
@@ -539,210 +511,6 @@ func (h *Handler) UkloniLoginPozadinu(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
}
// OtpremiAppPozadinu prima multipart upload slike i čuva je kao pozadinsku sliku aplikacije
func (h *Handler) OtpremiAppPozadinu(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "podesavanja.app_pozadina") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
return
}
r.Body = http.MaxBytesReader(w, r.Body, 5<<20+4096)
if err := r.ParseMultipartForm(5 << 20); err != nil {
middleware.SetFlash(w, r, h.DB, "greska", "Fajl je prevelik (maksimum 5 MB).")
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
return
}
fajl, zaglavlje, err := r.FormFile("app_pozadina")
if err != nil {
middleware.SetFlash(w, r, h.DB, "greska", "Nije odabran fajl.")
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
return
}
defer fajl.Close()
if zaglavlje.Size > 5<<20 {
middleware.SetFlash(w, r, h.DB, "greska", "Fajl je prevelik (maksimum 5 MB).")
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
return
}
ext := strings.ToLower(filepath.Ext(zaglavlje.Filename))
dozvoljenoExt := map[string]string{
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".webp": "image/webp",
}
ocekivaniMime, ok := dozvoljenoExt[ext]
if !ok {
middleware.SetFlash(w, r, h.DB, "greska", "Dozvoljeni formati su JPG, PNG i WebP.")
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
return
}
buf := make([]byte, 512)
n, _ := fajl.Read(buf)
stvarniMime := http.DetectContentType(buf[:n])
if !strings.HasPrefix(stvarniMime, ocekivaniMime) {
middleware.SetFlash(w, r, h.DB, "greska", "Sadržaj fajla ne odgovara odabranoj ekstenziji.")
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
return
}
if _, err := fajl.Seek(0, io.SeekStart); err != nil {
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri obradi fajla.")
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
return
}
// briše staru app pozadinu sa diska ako postoji
staraPodesavanja, _ := ntechsqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
if stara := staraPodesavanja["app_pozadina"]; stara != "" {
deoBezverzije := strings.Split(stara, "?")[0]
staroIme := filepath.Base(deoBezverzije)
os.Remove(filepath.Join("web/static/uploads", staroIme))
}
novoIme, err := generisiImeUploada(ext)
if err != nil {
log.Printf("upload app pozadine: greška pri generisanju imena: %v", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju fajla.")
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
return
}
odrediste := filepath.Join("web/static/uploads", novoIme)
dst, err := os.Create(odrediste)
if err != nil {
log.Printf("upload app pozadine: ne mogu kreirati fajl: %v", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju fajla.")
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
return
}
defer dst.Close()
if _, err := io.Copy(dst, fajl); err != nil {
log.Printf("upload app pozadine: greška pri kopiranju: %v", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju fajla.")
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
return
}
putanja := fmt.Sprintf("/static/uploads/%s?v=%d", novoIme, time.Now().Unix())
if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "app_pozadina", putanja); err != nil {
log.Printf("upload app pozadine: greška pri čuvanju putanje: %v", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju podešavanja.")
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
return
}
// zapamti trenutnu globalnu temu pre nego što je forsiramo na tamnu
trenutnaTema := staraPodesavanja["globalna_tema"]
if trenutnaTema == "" {
trenutnaTema = staraPodesavanja["tema"]
}
if trenutnaTema == "" {
trenutnaTema = "tamna"
}
_ = ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "tema_pre_slike", trenutnaTema)
_ = ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "globalna_tema", "tamna")
middleware.SetFlash(w, r, h.DB, "uspeh", "Pozadinska slika aplikacije je uspešno otpremljena.")
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
}
// UkloniAppPozadinu briše pozadinsku sliku aplikacije sa diska i iz podešavanja
func (h *Handler) UkloniAppPozadinu(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "podesavanja.app_pozadina") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
return
}
podesavanja, err := ntechsqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
if err == nil {
if stara := podesavanja["app_pozadina"]; stara != "" {
deoBezverzije := strings.Split(stara, "?")[0]
staroIme := filepath.Base(deoBezverzije)
os.Remove(filepath.Join("web/static/uploads", staroIme))
}
}
if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "app_pozadina", ""); err != nil {
log.Printf("ukloni app pozadinu: greška pri čuvanju: %v", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri uklanjanju slike.")
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
return
}
// vrati temu koja je bila aktivna pre dodavanja slike
if err == nil {
temaPreSlike := podesavanja["tema_pre_slike"]
if temaPreSlike == "" {
temaPreSlike = "tamna"
}
_ = ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "globalna_tema", temaPreSlike)
_ = ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "tema_pre_slike", "")
}
middleware.SetFlash(w, r, h.DB, "uspeh", "Pozadinska slika aplikacije je uklonjena.")
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
}
// SacuvajAppPozadinaStilove čuva vrednosti zamućenja i prozirnosti pozadine aplikacije
func (h *Handler) SacuvajAppPozadinaStilove(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "podesavanja.app_pozadina") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
return
}
if err := r.ParseForm(); err != nil {
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čitanju forme.")
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
return
}
blurStr := r.FormValue("blur")
blurPozadineStr := r.FormValue("blur_pozadine")
opacityStr := r.FormValue("opacity")
blurVal, err := strconv.Atoi(blurStr)
if err != nil || blurVal < 0 || blurVal > 20 {
middleware.SetFlash(w, r, h.DB, "greska", "Neispravna vrednost zamućenja stakla.")
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
return
}
blurPozadineVal, err := strconv.Atoi(blurPozadineStr)
if err != nil || blurPozadineVal < 0 || blurPozadineVal > 20 {
middleware.SetFlash(w, r, h.DB, "greska", "Neispravna vrednost zamućenja pozadine.")
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
return
}
opacityVal, err := strconv.Atoi(opacityStr)
if err != nil || opacityVal < 0 || opacityVal > 80 {
middleware.SetFlash(w, r, h.DB, "greska", "Neispravna vrednost prozirnosti.")
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
return
}
for kljuc, vrednost := range map[string]string{
"app_pozadina_blur": blurStr,
"app_pozadina_blur_pozadine": blurPozadineStr,
"app_pozadina_opacity": opacityStr,
} {
if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, kljuc, vrednost); err != nil {
log.Printf("stilovi app pozadine: greška pri čuvanju %s: %v", kljuc, err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju podešavanja.")
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
return
}
}
middleware.SetFlash(w, r, h.DB, "uspeh", "Izgled pozadine aplikacije je sačuvan.")
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
}
// SacuvajLoginPozadinaStilove čuva vrednosti zamućenja i prozirnosti pozadine login stranice
func (h *Handler) SacuvajLoginPozadinaStilove(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
@@ -759,6 +527,7 @@ func (h *Handler) SacuvajLoginPozadinaStilove(w http.ResponseWriter, r *http.Req
blurPozadineStr := r.FormValue("blur_pozadine")
blurKarticeStr := r.FormValue("blur_kartice")
opacityStr := r.FormValue("opacity")
zatamnjenjeKarticeStr := r.FormValue("zatamnjenje_kartice")
blurPozadineVal, err := strconv.Atoi(blurPozadineStr)
if err != nil || blurPozadineVal < 0 || blurPozadineVal > 20 {
@@ -778,11 +547,18 @@ func (h *Handler) SacuvajLoginPozadinaStilove(w http.ResponseWriter, r *http.Req
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
return
}
zatamnjenjeKarticeVal, err := strconv.Atoi(zatamnjenjeKarticeStr)
if err != nil || zatamnjenjeKarticeVal < 0 || zatamnjenjeKarticeVal > 80 {
middleware.SetFlash(w, r, h.DB, "greska", "Neispravna vrednost zatamnjivanja kartice.")
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
return
}
for kljuc, vrednost := range map[string]string{
"login_pozadina_blur_pozadine": blurPozadineStr,
"login_pozadina_blur_kartice": blurKarticeStr,
"login_pozadina_opacity": opacityStr,
"login_pozadina_blur_pozadine": blurPozadineStr,
"login_pozadina_blur_kartice": blurKarticeStr,
"login_pozadina_opacity": opacityStr,
"login_pozadina_zatamnjenje_kartice": zatamnjenjeKarticeStr,
} {
if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, kljuc, vrednost); err != nil {
log.Printf("stilovi login pozadine: greška pri čuvanju %s: %v", kljuc, err)
@@ -814,15 +590,6 @@ func (h *Handler) napuniPodaciPodesavanja(r *http.Request, naslov string) (Podac
PIB: podesavanja["pib"],
LogoTip: podesavanja["logo_tip"],
LogoPutanja: podesavanja["logo_putanja"],
GlobalnaTema: func() string {
if v := podesavanja["globalna_tema"]; v != "" {
return v
}
if v := podesavanja["tema"]; v != "" {
return v
}
return "tamna"
}(),
Sacuvano: r.URL.Query().Get("sacuvano") == "1",
BackupVracen: r.URL.Query().Get("sacuvano") == "vraceno",
Verzija: h.Verzija,
@@ -847,21 +614,8 @@ func (h *Handler) napuniPodaciPodesavanja(r *http.Request, naslov string) (Podac
}
return "12"
}(),
AppPozadina: podesavanja["app_pozadina"],
AppPozadinaOpacity: func() string {
if v := podesavanja["app_pozadina_opacity"]; v != "" {
return v
}
return "50"
}(),
AppPozadinaBlur: func() string {
if v := podesavanja["app_pozadina_blur"]; v != "" {
return v
}
return "12"
}(),
AppPozadinaBlurPozadine: func() string {
if v := podesavanja["app_pozadina_blur_pozadine"]; v != "" {
LoginPozadinaZatamnjenjeKartice: func() string {
if v := podesavanja["login_pozadina_zatamnjenje_kartice"]; v != "" {
return v
}
return "0"
@@ -914,81 +668,3 @@ func (h *Handler) PodesavanjaSistem(w http.ResponseWriter, r *http.Request) {
h.renderujTemplate(w, "podesavanja_sistem", podaci)
}
// PromeniTemu menja aktivnu temu i vraća na prethodnu stranicu (stara GET ruta, zadržana za kompatibilnost)
func (h *Handler) PromeniTemu(w http.ResponseWriter, r *http.Request) {
tema := chi.URLParam(r, "tema")
validne := map[string]bool{"tamna": true, "svetla": true}
if !validne[tema] {
http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
return
}
_ = ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "globalna_tema", tema)
referer := r.Header.Get("Referer")
if referer == "" {
referer = "/dashboard"
}
http.Redirect(w, r, referer, http.StatusSeeOther)
}
// PromeniGlobalnuTemu prima POST sa poljem "tema" i čuva u globalna_tema u podešavanjima
func (h *Handler) PromeniGlobalnuTemu(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest)
return
}
tema := r.FormValue("tema")
validne := map[string]bool{"tamna": true, "svetla": true}
if !validne[tema] {
tema = "tamna"
}
if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "globalna_tema", tema); err != nil {
http.Error(w, "Greška pri promeni teme", http.StatusInternalServerError)
return
}
referer := r.Header.Get("Referer")
if referer == "" {
referer = "/dashboard"
}
http.Redirect(w, r, referer, http.StatusSeeOther)
}
// PrimeniGlobalnuTemu čuva globalnu temu i resetuje lokalne teme svih korisnika
func (h *Handler) PrimeniGlobalnuTemu(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "tema.globalno") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
return
}
if err := r.ParseForm(); err != nil {
http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest)
return
}
tema := r.FormValue("globalna_tema")
validne := map[string]bool{"tamna": true, "svetla": true}
if !validne[tema] {
tema = "tamna"
}
if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "globalna_tema", tema); err != nil {
http.Error(w, "Greška pri čuvanju teme", http.StatusInternalServerError)
return
}
// poništi lokalne teme svih korisnika — tema se primenjuje globalno
if _, err := h.DB.ExecContext(r.Context(),
"UPDATE korisnici SET koristi_lokalnu_temu = 0, lokalna_tema = ''",
); err != nil {
log.Printf("PrimeniGlobalnuTemu: reset lokalnih tema: %v", err)
}
middleware.SetFlash(w, r, h.DB, "uspeh", "Globalna tema primenjena na sve korisnike.")
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
}