diff --git a/cmd/ntech/main.go b/cmd/ntech/main.go index 301ad58..8885162 100644 --- a/cmd/ntech/main.go +++ b/cmd/ntech/main.go @@ -140,10 +140,14 @@ func main() { }) r.Get("/dashboard", h.Dashboard) r.Get("/podesavanja", h.Podesavanja) + r.Get("/admin/podesavanja/opste", h.PodesavanjaOpste) + r.Get("/admin/podesavanja/izgled", h.PodesavanjaIzgled) + r.Get("/admin/podesavanja/sistem", h.PodesavanjaSistem) r.Post("/podesavanja/sacuvaj", h.SacuvajPodesavanja) r.Post("/podesavanja/logo", h.OtpremiLogo) r.Post("/podesavanja/login-pozadina", h.OtpremiLoginPozadinu) r.Post("/podesavanja/login-pozadina/ukloni", h.UkloniLoginPozadinu) + r.Post("/podesavanja/login-pozadina/stilovi", h.SacuvajLoginPozadinaStilove) r.Post("/podesavanja/app-pozadina", h.OtpremiAppPozadinu) r.Post("/podesavanja/app-pozadina/ukloni", h.UkloniAppPozadinu) r.Post("/podesavanja/app-pozadina/stilovi", h.SacuvajAppPozadinaStilove) diff --git a/internal/handler/handler.go b/internal/handler/handler.go index 2fda06a..bf12c00 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -100,6 +100,10 @@ func (h *Handler) popuniPodaciStranice(r *http.Request, podesavanja map[string]s if ps.AppPozadinaBlur == "" { ps.AppPozadinaBlur = "12" } + ps.AppPozadinaBlurPozadine = podesavanja["app_pozadina_blur_pozadine"] + if ps.AppPozadinaBlurPozadine == "" { + ps.AppPozadinaBlurPozadine = "0" + } return ps } diff --git a/internal/handler/kes.go b/internal/handler/kes.go index faf38a4..15cd121 100644 --- a/internal/handler/kes.go +++ b/internal/handler/kes.go @@ -24,7 +24,7 @@ var saSidebar = []string{ "klijenti", "klijent_forma", "magacin", "magacin_forma", "nabavke", "nabavka_forma", "nabavka_detalji", - "podesavanja", + "podesavanja", "podesavanja_opste", "podesavanja_izgled", "podesavanja_sistem", "podsetnici", "podsetnik_forma", "prodaja", "prodaja_detalji", "prodaja_forma", "servis", "servis_forma", "servis_detalji", diff --git a/internal/handler/podesavanja.go b/internal/handler/podesavanja.go index b555eda..446ecde 100644 --- a/internal/handler/podesavanja.go +++ b/internal/handler/podesavanja.go @@ -38,10 +38,14 @@ type PodaciPodesavanja struct { LogoGreska string BackupVracen bool Backupi []BackupInfo - LoginPozadina string - AppPozadina string - AppPozadinaOpacity string - AppPozadinaBlur string + LoginPozadina string + LoginPozadinaOpacity string + LoginPozadinaBlurPozadine string + LoginPozadinaBlurKartice string + AppPozadina string + AppPozadinaOpacity string + AppPozadinaBlur string + AppPozadinaBlurPozadine string } // BackupInfo opisuje jedan backup fajl @@ -85,8 +89,23 @@ func (h *Handler) Podesavanja(w http.ResponseWriter, r *http.Request) { Verzija: h.Verzija, LogoGreska: r.URL.Query().Get("logo_greska"), Backupi: ucitajListuBackupa(), - LoginPozadina: podesavanja["login_pozadina"], - AppPozadina: podesavanja["app_pozadina"], + LoginPozadina: podesavanja["login_pozadina"], + LoginPozadinaOpacity: func() string { + v := podesavanja["login_pozadina_opacity"] + if v == "" { return "50" } + return v + }(), + LoginPozadinaBlurPozadine: func() string { + v := podesavanja["login_pozadina_blur_pozadine"] + if v == "" { return "0" } + return v + }(), + LoginPozadinaBlurKartice: func() string { + v := podesavanja["login_pozadina_blur_kartice"] + if v == "" { return "12" } + return v + }(), + AppPozadina: podesavanja["app_pozadina"], AppPozadinaOpacity: func() string { v := podesavanja["app_pozadina_opacity"] if v == "" { return "50" } @@ -97,6 +116,11 @@ func (h *Handler) Podesavanja(w http.ResponseWriter, r *http.Request) { if v == "" { return "12" } return v }(), + AppPozadinaBlurPozadine: func() string { + v := podesavanja["app_pozadina_blur_pozadine"] + if v == "" { return "0" } + return v + }(), } h.renderujTemplate(w, "podesavanja", podaci) @@ -247,7 +271,12 @@ func (h *Handler) SacuvajPodesavanja(w http.ResponseWriter, r *http.Request) { } } - http.Redirect(w, r, "/podesavanja?sacuvano=1", http.StatusSeeOther) + sledeci := r.FormValue("_next") + if sledeci == "" { + http.Redirect(w, r, "/podesavanja?sacuvano=1", http.StatusSeeOther) + } else { + http.Redirect(w, r, sledeci+"?sacuvano=1", http.StatusSeeOther) + } } // BackupBaze kreira konzistentnu kopiju baze i šalje je kao attachment @@ -645,11 +674,18 @@ func (h *Handler) SacuvajAppPozadinaStilove(w http.ResponseWriter, r *http.Reque } 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.") + middleware.SetFlash(w, r, h.DB, "greska", "Neispravna vrednost zamućenja stakla.") + http.Redirect(w, r, "/podesavanja", 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, "/podesavanja", http.StatusSeeOther) return } @@ -660,23 +696,186 @@ func (h *Handler) SacuvajAppPozadinaStilove(w http.ResponseWriter, r *http.Reque return } - if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "app_pozadina_blur", blurStr); err != nil { - log.Printf("stilovi app pozadine: greška pri čuvanju blur: %v", err) - middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju podešavanja.") - http.Redirect(w, r, "/podesavanja", http.StatusSeeOther) - return - } - if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "app_pozadina_opacity", opacityStr); err != nil { - log.Printf("stilovi app pozadine: greška pri čuvanju opacity: %v", err) - middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju podešavanja.") - http.Redirect(w, r, "/podesavanja", 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, "/podesavanja", http.StatusSeeOther) + return + } } middleware.SetFlash(w, r, h.DB, "uspeh", "Izgled pozadine aplikacije je sačuvan.") http.Redirect(w, r, "/podesavanja", 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()) + if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "podesavanja.login_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, "/podesavanja", http.StatusSeeOther) + return + } + + blurPozadineStr := r.FormValue("blur_pozadine") + blurKarticeStr := r.FormValue("blur_kartice") + opacityStr := r.FormValue("opacity") + + 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, "/podesavanja", http.StatusSeeOther) + return + } + blurKarticeVal, err := strconv.Atoi(blurKarticeStr) + if err != nil || blurKarticeVal < 0 || blurKarticeVal > 20 { + middleware.SetFlash(w, r, h.DB, "greska", "Neispravna vrednost zamućenja kartice.") + http.Redirect(w, r, "/podesavanja", 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, "/podesavanja", http.StatusSeeOther) + return + } + + for kljuc, vrednost := range map[string]string{ + "login_pozadina_blur_pozadine": blurPozadineStr, + "login_pozadina_blur_kartice": blurKarticeStr, + "login_pozadina_opacity": opacityStr, + } { + 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) + middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju podešavanja.") + http.Redirect(w, r, "/podesavanja", http.StatusSeeOther) + return + } + } + + middleware.SetFlash(w, r, h.DB, "uspeh", "Izgled pozadine prijave je sačuvan.") + http.Redirect(w, r, "/podesavanja", http.StatusSeeOther) +} + +// napuniPodaciPodesavanja učitava sva podešavanja i kreira strukturu za template +func (h *Handler) napuniPodaciPodesavanja(r *http.Request, naslov string) (PodaciPodesavanja, error) { + podesavanja, err := ntechsqlite.DohvatiSvaPodesavanja(r.Context(), h.DB) + if err != nil { + return PodaciPodesavanja{}, err + } + ps := h.popuniPodaciStranice(r, podesavanja) + ps.Stranica = "podesavanja" + ps.NaslovStranice = naslov + return PodaciPodesavanja{ + PodaciStranice: ps, + NazivFirme: podesavanja["naziv_firme"], + Podnazlov: podesavanja["podnazlov"], + Adresa: podesavanja["adresa"], + Telefon: podesavanja["telefon"], + PIB: podesavanja["pib"], + LogoTip: podesavanja["logo_tip"], + LogoPutanja: podesavanja["logo_putanja"], + Tema: podesavanja["tema"], + Sacuvano: r.URL.Query().Get("sacuvano") == "1", + BackupVracen: r.URL.Query().Get("sacuvano") == "vraceno", + Verzija: h.Verzija, + LogoGreska: r.URL.Query().Get("logo_greska"), + Backupi: ucitajListuBackupa(), + LoginPozadina: podesavanja["login_pozadina"], + LoginPozadinaOpacity: func() string { + if v := podesavanja["login_pozadina_opacity"]; v != "" { + return v + } + return "50" + }(), + LoginPozadinaBlurPozadine: func() string { + if v := podesavanja["login_pozadina_blur_pozadine"]; v != "" { + return v + } + return "0" + }(), + LoginPozadinaBlurKartice: func() string { + if v := podesavanja["login_pozadina_blur_kartice"]; v != "" { + return v + } + 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 != "" { + return v + } + return "0" + }(), + }, nil +} + +// PodesavanjaOpste renderuje stranicu sa opštim podešavanjima (firma i logo) +func (h *Handler) PodesavanjaOpste(w http.ResponseWriter, r *http.Request) { + k := middleware.KorisnikIzKonteksta(r.Context()) + if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "podesavanja.pregled") { + http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden) + return + } + podaci, err := h.napuniPodaciPodesavanja(r, "Podešavanja — Opšte") + if err != nil { + http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError) + return + } + h.renderujTemplate(w, "podesavanja_opste", podaci) +} + +// PodesavanjaIzgled renderuje stranicu sa podešavanjima izgleda (pozadine i tema) +func (h *Handler) PodesavanjaIzgled(w http.ResponseWriter, r *http.Request) { + k := middleware.KorisnikIzKonteksta(r.Context()) + if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "podesavanja.pregled") { + http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden) + return + } + podaci, err := h.napuniPodaciPodesavanja(r, "Podešavanja — Izgled") + if err != nil { + http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError) + return + } + h.renderujTemplate(w, "podesavanja_izgled", podaci) +} + +// PodesavanjaSistem renderuje stranicu sa sistemskim podešavanjima (backup) +func (h *Handler) PodesavanjaSistem(w http.ResponseWriter, r *http.Request) { + k := middleware.KorisnikIzKonteksta(r.Context()) + if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "podesavanja.pregled") { + http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden) + return + } + podaci, err := h.napuniPodaciPodesavanja(r, "Podešavanja — Sistem") + if err != nil { + http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError) + return + } + h.renderujTemplate(w, "podesavanja_sistem", podaci) +} + // PromeniTemu menja aktivnu temu i vraća na prethodnu stranicu func (h *Handler) PromeniTemu(w http.ResponseWriter, r *http.Request) { tema := chi.URLParam(r, "tema") diff --git a/internal/handler/prijava.go b/internal/handler/prijava.go index e842306..5113a65 100644 --- a/internal/handler/prijava.go +++ b/internal/handler/prijava.go @@ -31,16 +31,31 @@ func (h *Handler) PrikazPrijave(w http.ResponseWriter, r *http.Request) { greska := r.URL.Query().Get("greska") - // login_pozadina se čita bez prijavljenog korisnika — koristimo background kontekst + // login_pozadina i stilovi se čitaju bez prijavljenog korisnika — koristimo background kontekst loginPozadina := "" + loginOpacity := "50" + loginBlurPozadine := "0" + loginBlurKartice := "12" if podesavanja, err := ntechsqlite.DohvatiSvaPodesavanja(context.Background(), h.DB); err == nil { loginPozadina = podesavanja["login_pozadina"] + if v := podesavanja["login_pozadina_opacity"]; v != "" { + loginOpacity = v + } + if v := podesavanja["login_pozadina_blur_pozadine"]; v != "" { + loginBlurPozadine = v + } + if v := podesavanja["login_pozadina_blur_kartice"]; v != "" { + loginBlurKartice = v + } } h.renderujStandalone(w, "prijava", map[string]any{ - "Greska": greska, - "CsrfToken": middleware.CsrfToken(r.Context()), - "LoginPozadina": loginPozadina, + "Greska": greska, + "CsrfToken": middleware.CsrfToken(r.Context()), + "LoginPozadina": loginPozadina, + "LoginPozadinaOpacity": loginOpacity, + "LoginPozadinaBlurPozadine": loginBlurPozadine, + "LoginPozadinaBlurKartice": loginBlurKartice, }) } diff --git a/internal/model/stranica.go b/internal/model/stranica.go index 658a191..9d769c3 100644 --- a/internal/model/stranica.go +++ b/internal/model/stranica.go @@ -45,9 +45,10 @@ type PodaciStranice struct { Dozvole map[string]bool // mapa akcija → dozvoljeno/nije Flash *FlashPoruka // jednokratna poruka nakon redirecta // app pozadina — popunjava se iz podešavanja za sve stranice - AppPozadina string - AppPozadinaOpacity string // vrednost 0-80 (% overlay zatamnjivanja) - AppPozadinaBlur string // vrednost 0-20 (px backdrop-filter blur) + AppPozadina string + 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) } // PodaciDashboarda su podaci specifični za dashboard stranicu diff --git a/migrations/025_login_pozadina_stilovi.sql b/migrations/025_login_pozadina_stilovi.sql new file mode 100644 index 0000000..0f4b035 --- /dev/null +++ b/migrations/025_login_pozadina_stilovi.sql @@ -0,0 +1,3 @@ +INSERT OR IGNORE INTO podesavanja (kljuc, vrednost) VALUES ('login_pozadina_blur_pozadine', '0'); +INSERT OR IGNORE INTO podesavanja (kljuc, vrednost) VALUES ('login_pozadina_blur_kartice', '12'); +INSERT OR IGNORE INTO podesavanja (kljuc, vrednost) VALUES ('login_pozadina_opacity', '50'); diff --git a/migrations/026_app_pozadina_blur_pozadine.sql b/migrations/026_app_pozadina_blur_pozadine.sql new file mode 100644 index 0000000..e36a075 --- /dev/null +++ b/migrations/026_app_pozadina_blur_pozadine.sql @@ -0,0 +1 @@ +INSERT OR IGNORE INTO podesavanja (kljuc, vrednost) VALUES ('app_pozadina_blur_pozadine', '0'); diff --git a/web/static/css/main.css b/web/static/css/main.css index 293efac..5aebd9c 100644 --- a/web/static/css/main.css +++ b/web/static/css/main.css @@ -201,6 +201,29 @@ body { pointer-events: none; } +/* accordion podmeni u sidebaru */ +.nav-podmeni { + overflow: hidden; +} +/* u skupljenom modu alpine x-show kontroliše vidljivost — ne skrivamo display:none */ +.nav-podstavka { + padding-left: 48px !important; +} +.nav-podstavka span { + font-size: 13px !important; +} +/* u skupljenom modu reset uvlačenja — ikonica se prikazuje na istoj poziciji */ +.sidebar.skupljen .nav-podstavka { + padding-left: 16px !important; +} +/* strelica za accordion — sakriva se zajedno sa tekstom u skupljenom modu */ +.nav-strelica { + margin-left: auto; + display: flex; + align-items: center; + flex-shrink: 0; +} + .nav-separator { height: 0.5px; background: var(--ivica); diff --git a/web/templates/komponente/sidebar.html b/web/templates/komponente/sidebar.html index 30034f4..d20f0ba 100644 --- a/web/templates/komponente/sidebar.html +++ b/web/templates/komponente/sidebar.html @@ -112,11 +112,38 @@
{{if index .Dozvole "podesavanja.pregled"}} - - - Podešavanja - - +