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
+1 -1
View File
@@ -87,7 +87,7 @@ type KorisniciRepository interface {
PromeniLozinku(ctx context.Context, id int64, hash string) error
SacuvajTotpTajnu(ctx context.Context, id int64, tajna string) error
SacuvajLokalnuTemu(ctx context.Context, id int64, lokalnaTema string, koristi bool) error
SacuvajLokalnuPozadinu(ctx context.Context, id int64, pozadina, opacity, blur, blurPozadine string) error
SacuvajLokalnuPozadinu(ctx context.Context, id int64, pozadina, opacity, blur, blurPozadine, glassOpacity string) error
PostojiIjedan(ctx context.Context) (bool, error)
Obrisi(ctx context.Context, id int64) error
}
+21 -12
View File
@@ -31,17 +31,19 @@ func (r *sqliteKorisniciRepo) DohvatiPoImenu(ctx context.Context, korisnickoIme
k := &model.Korisnik{}
var aktivan, koristiLokalnuTemu int
var totpTajna, lokalnaTema sql.NullString
var lokalnaPozadina, lokalnaPozadinaOpacity, lokalnaPozadinaBlur, lokalnaPozadinaBlurPozadine sql.NullString
var lokalnaPozadina, lokalnaPozadinaOpacity, lokalnaPozadinaBlur, lokalnaPozadinaBlurPozadine, lokalnaPozadinaGlassOpacity sql.NullString
var datumKreiranja time.Time
err := r.db.QueryRowContext(ctx,
`SELECT id, korisnicko_ime, lozinka_hash, uloga, aktivan, COALESCE(totp_tajna, ''),
COALESCE(lokalna_tema, ''), koristi_lokalnu_temu, datum_kreiranja,
COALESCE(lokalna_pozadina, ''), COALESCE(lokalna_pozadina_opacity, '50'),
COALESCE(lokalna_pozadina_blur, '12'), COALESCE(lokalna_pozadina_blur_pozadine, '0')
COALESCE(lokalna_pozadina_blur, '12'), COALESCE(lokalna_pozadina_blur_pozadine, '0'),
COALESCE(lokalna_pozadina_glass_opacity, '10')
FROM korisnici WHERE korisnicko_ime = ?`, korisnickoIme).
Scan(&k.ID, &k.KorisnickoIme, &k.LozinkaHash, &k.Uloga, &aktivan, &totpTajna,
&lokalnaTema, &koristiLokalnuTemu, &datumKreiranja,
&lokalnaPozadina, &lokalnaPozadinaOpacity, &lokalnaPozadinaBlur, &lokalnaPozadinaBlurPozadine)
&lokalnaPozadina, &lokalnaPozadinaOpacity, &lokalnaPozadinaBlur, &lokalnaPozadinaBlurPozadine,
&lokalnaPozadinaGlassOpacity)
if err != nil {
return nil, fmt.Errorf("ntech: korisnici.DohvatiPoImenu: %w", err)
}
@@ -54,6 +56,7 @@ func (r *sqliteKorisniciRepo) DohvatiPoImenu(ctx context.Context, korisnickoIme
k.LokalnaPozadinaOpacity = lokalnaPozadinaOpacity.String
k.LokalnaPozadinaBlur = lokalnaPozadinaBlur.String
k.LokalnaPozadinaBlurPozadine = lokalnaPozadinaBlurPozadine.String
k.LokalnaPozadinaGlassOpacity = lokalnaPozadinaGlassOpacity.String
return k, nil
}
@@ -61,17 +64,19 @@ func (r *sqliteKorisniciRepo) DohvatiPoID(ctx context.Context, id int64) (*model
k := &model.Korisnik{}
var aktivan, koristiLokalnuTemu int
var lokalnaTema sql.NullString
var lokalnaPozadina, lokalnaPozadinaOpacity, lokalnaPozadinaBlur, lokalnaPozadinaBlurPozadine sql.NullString
var lokalnaPozadina, lokalnaPozadinaOpacity, lokalnaPozadinaBlur, lokalnaPozadinaBlurPozadine, lokalnaPozadinaGlassOpacity sql.NullString
var datumKreiranja time.Time
err := r.db.QueryRowContext(ctx,
`SELECT id, korisnicko_ime, lozinka_hash, uloga, aktivan, COALESCE(totp_tajna, ''),
COALESCE(lokalna_tema, ''), koristi_lokalnu_temu, datum_kreiranja,
COALESCE(lokalna_pozadina, ''), COALESCE(lokalna_pozadina_opacity, '50'),
COALESCE(lokalna_pozadina_blur, '12'), COALESCE(lokalna_pozadina_blur_pozadine, '0')
COALESCE(lokalna_pozadina_blur, '12'), COALESCE(lokalna_pozadina_blur_pozadine, '0'),
COALESCE(lokalna_pozadina_glass_opacity, '10')
FROM korisnici WHERE id = ?`, id).
Scan(&k.ID, &k.KorisnickoIme, &k.LozinkaHash, &k.Uloga, &aktivan, &k.TotpTajna,
&lokalnaTema, &koristiLokalnuTemu, &datumKreiranja,
&lokalnaPozadina, &lokalnaPozadinaOpacity, &lokalnaPozadinaBlur, &lokalnaPozadinaBlurPozadine)
&lokalnaPozadina, &lokalnaPozadinaOpacity, &lokalnaPozadinaBlur, &lokalnaPozadinaBlurPozadine,
&lokalnaPozadinaGlassOpacity)
if err != nil {
return nil, fmt.Errorf("ntech: korisnici.DohvatiPoID: %w", err)
}
@@ -83,6 +88,7 @@ func (r *sqliteKorisniciRepo) DohvatiPoID(ctx context.Context, id int64) (*model
k.LokalnaPozadinaOpacity = lokalnaPozadinaOpacity.String
k.LokalnaPozadinaBlur = lokalnaPozadinaBlur.String
k.LokalnaPozadinaBlurPozadine = lokalnaPozadinaBlurPozadine.String
k.LokalnaPozadinaGlassOpacity = lokalnaPozadinaGlassOpacity.String
return k, nil
}
@@ -91,7 +97,8 @@ func (r *sqliteKorisniciRepo) Lista(ctx context.Context) ([]model.Korisnik, erro
`SELECT id, korisnicko_ime, lozinka_hash, uloga, aktivan, COALESCE(totp_tajna, ''),
COALESCE(lokalna_tema, ''), koristi_lokalnu_temu, datum_kreiranja,
COALESCE(lokalna_pozadina, ''), COALESCE(lokalna_pozadina_opacity, '50'),
COALESCE(lokalna_pozadina_blur, '12'), COALESCE(lokalna_pozadina_blur_pozadine, '0')
COALESCE(lokalna_pozadina_blur, '12'), COALESCE(lokalna_pozadina_blur_pozadine, '0'),
COALESCE(lokalna_pozadina_glass_opacity, '10')
FROM korisnici ORDER BY datum_kreiranja ASC`)
if err != nil {
return nil, fmt.Errorf("ntech: korisnici.Lista: %w", err)
@@ -102,11 +109,12 @@ func (r *sqliteKorisniciRepo) Lista(ctx context.Context) ([]model.Korisnik, erro
var k model.Korisnik
var aktivan, koristiLokalnuTemu int
var lokalnaTema sql.NullString
var lokalnaPozadina, lokalnaPozadinaOpacity, lokalnaPozadinaBlur, lokalnaPozadinaBlurPozadine sql.NullString
var lokalnaPozadina, lokalnaPozadinaOpacity, lokalnaPozadinaBlur, lokalnaPozadinaBlurPozadine, lokalnaPozadinaGlassOpacity sql.NullString
var datumKreiranja time.Time
if err := rows.Scan(&k.ID, &k.KorisnickoIme, &k.LozinkaHash, &k.Uloga, &aktivan, &k.TotpTajna,
&lokalnaTema, &koristiLokalnuTemu, &datumKreiranja,
&lokalnaPozadina, &lokalnaPozadinaOpacity, &lokalnaPozadinaBlur, &lokalnaPozadinaBlurPozadine); err != nil {
&lokalnaPozadina, &lokalnaPozadinaOpacity, &lokalnaPozadinaBlur, &lokalnaPozadinaBlurPozadine,
&lokalnaPozadinaGlassOpacity); err != nil {
return nil, fmt.Errorf("ntech: korisnici.Lista: %w", err)
}
k.Aktivan = aktivan == 1
@@ -117,15 +125,16 @@ func (r *sqliteKorisniciRepo) Lista(ctx context.Context) ([]model.Korisnik, erro
k.LokalnaPozadinaOpacity = lokalnaPozadinaOpacity.String
k.LokalnaPozadinaBlur = lokalnaPozadinaBlur.String
k.LokalnaPozadinaBlurPozadine = lokalnaPozadinaBlurPozadine.String
k.LokalnaPozadinaGlassOpacity = lokalnaPozadinaGlassOpacity.String
lista = append(lista, k)
}
return lista, nil
}
func (r *sqliteKorisniciRepo) SacuvajLokalnuPozadinu(ctx context.Context, id int64, pozadina, opacity, blur, blurPozadine string) error {
func (r *sqliteKorisniciRepo) SacuvajLokalnuPozadinu(ctx context.Context, id int64, pozadina, opacity, blur, blurPozadine, glassOpacity string) error {
_, err := r.db.ExecContext(ctx,
`UPDATE korisnici SET lokalna_pozadina = ?, lokalna_pozadina_opacity = ?, lokalna_pozadina_blur = ?, lokalna_pozadina_blur_pozadine = ? WHERE id = ?`,
pozadina, opacity, blur, blurPozadine, id)
`UPDATE korisnici SET lokalna_pozadina = ?, lokalna_pozadina_opacity = ?, lokalna_pozadina_blur = ?, lokalna_pozadina_blur_pozadine = ?, lokalna_pozadina_glass_opacity = ? WHERE id = ?`,
pozadina, opacity, blur, blurPozadine, glassOpacity, id)
if err != nil {
return fmt.Errorf("ntech: korisnici.SacuvajLokalnuPozadinu: %w", err)
}
+16 -8
View File
@@ -33,19 +33,17 @@ type podaciAdminProfil struct {
TotpAktivan bool
LokalnaTema string
KoristiLokalnuTemu bool
GlobalnaTema string
}
type podaciProfilTema struct {
model.PodaciStranice
LokalnaTema string
KoristiLokalnuTemu bool
KoristiGlobalnuTemu bool // !KoristiLokalnuTemu — za Alpine.js svič "Koristi globalnu temu"
GlobalnaTema string
LokalnaPozadina string
LokalnaPozadinaOpacity string
LokalnaPozadinaBlur string
LokalnaPozadinaBlurPozadine string
LokalnaPozadinaGlassOpacity string
}
// AdminKorisnici prikazuje listu korisnika
@@ -304,7 +302,6 @@ func (h *Handler) AdminProfil(w http.ResponseWriter, r *http.Request) {
TotpAktivan: svezi.TotpTajna != "",
LokalnaTema: svezi.LokalnaTema,
KoristiLokalnuTemu: svezi.KoristiLokalnuTemu,
GlobalnaTema: podesavanja["globalna_tema"],
})
}
@@ -561,13 +558,20 @@ func (h *Handler) AdminDozvole(w http.ResponseWriter, r *http.Request) {
ps.Stranica = "dozvole"
ps.NaslovStranice = "Dozvole"
dozvoleAdmin := map[string]bool{}
dozvoleSuperadmin := map[string]bool{}
if k.Uloga == "superadmin" {
dozvoleAdmin = h.DozvoleRepo.SveDozvole(r.Context(), "admin")
dozvoleSuperadmin = h.DozvoleRepo.SveDozvole(r.Context(), "superadmin")
}
h.renderujTemplate(w, "admin_dozvole", podaciAdminDozvole{
PodaciStranice: ps,
Korisnici: lista,
TrenutniID: k.ID,
DozvoleRadnik: h.DozvoleRepo.SveDozvole(r.Context(), "radnik"),
DozvoleAdmin: h.DozvoleRepo.SveDozvole(r.Context(), "admin"),
DozvoleSuperadmin: h.DozvoleRepo.SveDozvole(r.Context(), "superadmin"),
DozvoleAdmin: dozvoleAdmin,
DozvoleSuperadmin: dozvoleSuperadmin,
})
}
@@ -644,8 +648,12 @@ func (h *Handler) AdminDozvoleSacuvaj(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/admin/dozvole", http.StatusSeeOther)
return
}
// čuvamo dozvole samo za radnik i admin — superadmin uvek ima sve
for _, uloga := range []string{"radnik", "admin"} {
// čuvamo dozvole: superadmin menja radnik i admin, admin menja samo radnik
uloge := []string{"radnik", "admin"}
if k.Uloga != "superadmin" {
uloge = []string{"radnik"}
}
for _, uloga := range uloge {
for _, akcija := range middleware.SveAkcije() {
kljuc := uloga + "__" + akcija
dozvoljeno := r.FormValue(kljuc) == "on"
+10 -46
View File
@@ -74,15 +74,8 @@ func (h *Handler) reinicijalzijRepozitorijume(novaDB *sql.DB) {
// popuniPodaciStranice popunjava zajednička polja stranice uključujući prijavljenog korisnika
func (h *Handler) popuniPodaciStranice(r *http.Request, podesavanja map[string]string) model.PodaciStranice {
// redosled prioriteta teme: pozadinska slika → lokalna → globalna → fallback
globalnaTema := podesavanja["globalna_tema"]
if globalnaTema == "" {
globalnaTema = podesavanja["tema"]
}
if globalnaTema == "" {
globalnaTema = "tamna"
}
tema := globalnaTema
// podrazumevana tema je tamna; korisnik može imati svoju lokalnu temu
tema := "tamna"
ps := model.PodaciStranice{
Tema: tema,
@@ -99,8 +92,8 @@ func (h *Handler) popuniPodaciStranice(r *http.Request, podesavanja map[string]s
ps.KorisnikIme = k.KorisnickoIme
ps.KorisnikUloga = k.Uloga
ps.Dozvole = h.DozvoleRepo.SveDozvole(r.Context(), k.Uloga)
// lokalna tema korisnika
if k.KoristiLokalnuTemu && k.LokalnaTema != "" {
// lokalna tema korisnika — primenjuje se uvek kada je postavljena, bez obzira na KoristiLokalnuTemu
if k.LokalnaTema != "" {
ps.Tema = k.LokalnaTema
}
}
@@ -108,10 +101,10 @@ func (h *Handler) popuniPodaciStranice(r *http.Request, podesavanja map[string]s
ps.Flash = middleware.GetFlash(r, h.DB)
// logika pozadine:
// - lična pozadina (samo kada je lokalni režim aktivan) → zamenjuje globalnu
// - lična pozadina → uvek se prikazuje i forsira tamnu temu, bez obzira na KoristiLokalnuTemu
// - globalna pozadina → prikazuje se svima koji nemaju ličnu
// KoristiLokalnuTemu utiče na izbor tamne/svetle teme, ne na vidljivost pozadine
if korisnik != nil && korisnik.KoristiLokalnuTemu && korisnik.LokalnaPozadina != "" {
// KoristiLokalnuTemu i LokalnaTema važe samo kada nema lične pozadine
if korisnik != nil && korisnik.LokalnaPozadina != "" {
ps.AppPozadina = korisnik.LokalnaPozadina
ps.Tema = "tamna"
ps.AppPozadinaOpacity = korisnik.LokalnaPozadinaOpacity
@@ -126,38 +119,9 @@ func (h *Handler) popuniPodaciStranice(r *http.Request, podesavanja map[string]s
if ps.AppPozadinaBlurPozadine == "" {
ps.AppPozadinaBlurPozadine = "0"
}
} else {
ps.AppPozadina = podesavanja["app_pozadina"]
if ps.AppPozadina != "" {
// globalna pozadina forsira tamnu temu, osim ako korisnik ima aktivnu lokalnu temu
if korisnik == nil || !korisnik.KoristiLokalnuTemu {
ps.Tema = "tamna"
}
ps.AppPozadinaOpacity = podesavanja["app_pozadina_opacity"]
if ps.AppPozadinaOpacity == "" {
ps.AppPozadinaOpacity = "50"
}
ps.AppPozadinaBlur = podesavanja["app_pozadina_blur"]
if ps.AppPozadinaBlur == "" {
ps.AppPozadinaBlur = "12"
}
ps.AppPozadinaBlurPozadine = podesavanja["app_pozadina_blur_pozadine"]
if ps.AppPozadinaBlurPozadine == "" {
ps.AppPozadinaBlurPozadine = "0"
}
} else {
ps.AppPozadinaOpacity = podesavanja["app_pozadina_opacity"]
if ps.AppPozadinaOpacity == "" {
ps.AppPozadinaOpacity = "50"
}
ps.AppPozadinaBlur = podesavanja["app_pozadina_blur"]
if ps.AppPozadinaBlur == "" {
ps.AppPozadinaBlur = "12"
}
ps.AppPozadinaBlurPozadine = podesavanja["app_pozadina_blur_pozadine"]
if ps.AppPozadinaBlurPozadine == "" {
ps.AppPozadinaBlurPozadine = "0"
}
ps.AppPozadinaGlassOpacity = korisnik.LokalnaPozadinaGlassOpacity
if ps.AppPozadinaGlassOpacity == "" {
ps.AppPozadinaGlassOpacity = "10"
}
}
+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)
}
+11 -6
View File
@@ -36,6 +36,7 @@ func (h *Handler) PrikazPrijave(w http.ResponseWriter, r *http.Request) {
loginOpacity := "50"
loginBlurPozadine := "0"
loginBlurKartice := "12"
loginZatamnjenjeKartice := "0"
if podesavanja, err := ntechsqlite.DohvatiSvaPodesavanja(context.Background(), h.DB); err == nil {
loginPozadina = podesavanja["login_pozadina"]
if v := podesavanja["login_pozadina_opacity"]; v != "" {
@@ -47,15 +48,19 @@ func (h *Handler) PrikazPrijave(w http.ResponseWriter, r *http.Request) {
if v := podesavanja["login_pozadina_blur_kartice"]; v != "" {
loginBlurKartice = v
}
if v := podesavanja["login_pozadina_zatamnjenje_kartice"]; v != "" {
loginZatamnjenjeKartice = v
}
}
h.renderujStandalone(w, "prijava", map[string]any{
"Greska": greska,
"CsrfToken": middleware.CsrfToken(r.Context()),
"LoginPozadina": loginPozadina,
"LoginPozadinaOpacity": loginOpacity,
"LoginPozadinaBlurPozadine": loginBlurPozadine,
"LoginPozadinaBlurKartice": loginBlurKartice,
"Greska": greska,
"CsrfToken": middleware.CsrfToken(r.Context()),
"LoginPozadina": loginPozadina,
"LoginPozadinaOpacity": loginOpacity,
"LoginPozadinaBlurPozadine": loginBlurPozadine,
"LoginPozadinaBlurKartice": loginBlurKartice,
"LoginPozadinaZatamnjenjeKartice": loginZatamnjenjeKartice,
})
}
+15 -5
View File
@@ -42,12 +42,11 @@ func (h *Handler) ProfilTema(w http.ResponseWriter, r *http.Request) {
PodaciStranice: ps,
LokalnaTema: svezi.LokalnaTema,
KoristiLokalnuTemu: svezi.KoristiLokalnuTemu,
KoristiGlobalnuTemu: !svezi.KoristiLokalnuTemu,
GlobalnaTema: podesavanja["globalna_tema"],
LokalnaPozadina: svezi.LokalnaPozadina,
LokalnaPozadinaOpacity: svezi.LokalnaPozadinaOpacity,
LokalnaPozadinaBlur: svezi.LokalnaPozadinaBlur,
LokalnaPozadinaBlurPozadine: svezi.LokalnaPozadinaBlurPozadine,
LokalnaPozadinaGlassOpacity: svezi.LokalnaPozadinaGlassOpacity,
}
if podaci.LokalnaPozadinaOpacity == "" {
podaci.LokalnaPozadinaOpacity = "50"
@@ -58,6 +57,9 @@ func (h *Handler) ProfilTema(w http.ResponseWriter, r *http.Request) {
if podaci.LokalnaPozadinaBlurPozadine == "" {
podaci.LokalnaPozadinaBlurPozadine = "0"
}
if podaci.LokalnaPozadinaGlassOpacity == "" {
podaci.LokalnaPozadinaGlassOpacity = "10"
}
h.renderujTemplate(w, "profil_tema", podaci)
}
@@ -149,6 +151,7 @@ func (h *Handler) ProfilOtpremiPozadinu(w http.ResponseWriter, r *http.Request)
opacity := "50"
blur := "12"
blurPozadine := "0"
glassOpacity := "10"
if svezi != nil {
if svezi.LokalnaPozadinaOpacity != "" {
opacity = svezi.LokalnaPozadinaOpacity
@@ -159,9 +162,12 @@ func (h *Handler) ProfilOtpremiPozadinu(w http.ResponseWriter, r *http.Request)
if svezi.LokalnaPozadinaBlurPozadine != "" {
blurPozadine = svezi.LokalnaPozadinaBlurPozadine
}
if svezi.LokalnaPozadinaGlassOpacity != "" {
glassOpacity = svezi.LokalnaPozadinaGlassOpacity
}
}
if err := h.KorisniciRepo.SacuvajLokalnuPozadinu(r.Context(), k.ID, putanja, opacity, blur, blurPozadine); err != nil {
if err := h.KorisniciRepo.SacuvajLokalnuPozadinu(r.Context(), k.ID, putanja, opacity, blur, blurPozadine, glassOpacity); err != nil {
log.Printf("ProfilOtpremiPozadinu: greška pri čuvanju: %v", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju podešavanja.")
http.Redirect(w, r, "/profil/tema", http.StatusSeeOther)
@@ -186,7 +192,7 @@ func (h *Handler) ProfilUkloniPozadinu(w http.ResponseWriter, r *http.Request) {
os.Remove(filepath.Join("web/static/uploads", filepath.Base(deo)))
}
if err := h.KorisniciRepo.SacuvajLokalnuPozadinu(r.Context(), k.ID, "", "50", "12", "0"); err != nil {
if err := h.KorisniciRepo.SacuvajLokalnuPozadinu(r.Context(), k.ID, "", "50", "12", "0", "10"); err != nil {
log.Printf("ProfilUkloniPozadinu: %v", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri uklanjanju slike.")
http.Redirect(w, r, "/profil/tema", http.StatusSeeOther)
@@ -220,6 +226,7 @@ func (h *Handler) ProfilSacuvajPozadinuStilove(w http.ResponseWriter, r *http.Re
opacity := r.FormValue("lokalna_pozadina_opacity")
blur := r.FormValue("lokalna_pozadina_blur")
blurPozadineSt := r.FormValue("lokalna_pozadina_blur_pozadine")
glassOpacitySt := r.FormValue("lokalna_pozadina_glass_opacity")
if opacity == "" {
opacity = "50"
}
@@ -229,8 +236,11 @@ func (h *Handler) ProfilSacuvajPozadinuStilove(w http.ResponseWriter, r *http.Re
if blurPozadineSt == "" {
blurPozadineSt = "0"
}
if glassOpacitySt == "" {
glassOpacitySt = "10"
}
if err := h.KorisniciRepo.SacuvajLokalnuPozadinu(r.Context(), k.ID, pozadina, opacity, blur, blurPozadineSt); err != nil {
if err := h.KorisniciRepo.SacuvajLokalnuPozadinu(r.Context(), k.ID, pozadina, opacity, blur, blurPozadineSt, glassOpacitySt); err != nil {
log.Printf("ProfilSacuvajPozadinuStilove: %v", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju podešavanja.")
http.Redirect(w, r, "/profil/tema", http.StatusSeeOther)
+1
View File
@@ -17,6 +17,7 @@ type Korisnik struct {
LokalnaPozadinaOpacity string
LokalnaPozadinaBlur string
LokalnaPozadinaBlurPozadine string
LokalnaPozadinaGlassOpacity string
}
// Sesija predstavlja aktivnu sesiju prijavljenog korisnika
+1
View File
@@ -49,6 +49,7 @@ type PodaciStranice struct {
AppPozadinaOpacity string // vrednost 0-80 (% overlay zatamnjivanja)
AppPozadinaBlur string // vrednost 0-20 (px backdrop-filter blur na elementima)
AppPozadinaBlurPozadine string // vrednost 0-20 (px filter blur na pozadinskoj slici)
AppPozadinaGlassOpacity string // vrednost 0-80 (% zatamnjivanje glass elemenata) — samo za ličnu pozadinu
}
// PodaciDashboarda su podaci specifični za dashboard stranicu