Topbar: logo firme + naslov; avatar upload; uklanjanje logo zone

- Topbar: logo slika firme (toggle on/off) pa naslov stranice; bez teksta firme
- Sidebar: samo naziv firme i podnaslov (tekst), bez slike loga
- Avatar: korisnik uploaduje ličnu sliku u Profil > Tema > Avatar;
  prikazuje se kao dugme za meni (desno u topbaru); fallback inicijali
- Logo firme kartica: dugme "Ukloni sliku" + ruta /podesavanja/logo/ukloni
- Logo zona iz podešavanja uklonjena; jedan iOS toggle za prikaz loga u topbaru
- Migracije 049 (topbar_logo_slika/tekst) i 050 (avatar_putanja na korisnicima)
- iOS-style .toggl switch u main.css
This commit is contained in:
2026-06-16 02:46:48 +02:00
parent 3c5c8060c1
commit 85cb1e25c7
15 changed files with 320 additions and 60 deletions
+8 -6
View File
@@ -161,12 +161,13 @@ func (h *Handler) popuniPodaciStranice(r *http.Request, podesavanja map[string]s
tema := "tamna"
ps := model.PodaciStranice{
Tema: tema,
NazivFirme: podesavanja["naziv_firme"],
Podnazlov: podesavanja["podnazlov"],
LogoTip: podesavanja["logo_tip"],
LogoPutanja: podesavanja["logo_putanja"],
Korisnik: "Admin",
Tema: tema,
NazivFirme: podesavanja["naziv_firme"],
Podnazlov: podesavanja["podnazlov"],
LogoPutanja: podesavanja["logo_putanja"],
TopbarLogoSlika: podesavanja["topbar_logo_slika"] == "1",
TopbarLogoTekst: podesavanja["topbar_logo_tekst"] == "1",
Korisnik: "Admin",
}
var korisnik *model.Korisnik
if k := middleware.KorisnikIzKonteksta(r.Context()); k != nil {
@@ -174,6 +175,7 @@ func (h *Handler) popuniPodaciStranice(r *http.Request, podesavanja map[string]s
ps.Korisnik = k.KorisnickoIme
ps.KorisnikIme = k.KorisnickoIme
ps.KorisnikUloga = k.Uloga
ps.AvatarPutanja = k.AvatarPutanja
ps.Dozvole = h.DozvoleRepo.SveDozvole(r.Context(), k.Uloga)
// lokalna tema korisnika — primenjuje se uvek kada je postavljena, bez obzira na KoristiLokalnuTemu
if k.LokalnaTema != "" {
+39 -10
View File
@@ -28,8 +28,9 @@ type PodaciPodesavanja struct {
Adresa string
Telefon string
PIB string
LogoTip string
LogoPutanja string
LogoPutanja string
TopbarLogoSlika bool
TopbarLogoTekst bool
// profil firme — pravni/poreski status (Faza 0); određuje koji se zakonski moduli pale
FirmaPravniOblik string
FirmaPdvObveznik string
@@ -81,8 +82,9 @@ func (h *Handler) Podesavanja(w http.ResponseWriter, r *http.Request) {
Adresa: podesavanja["adresa"],
Telefon: podesavanja["telefon"],
PIB: podesavanja["pib"],
LogoTip: podesavanja["logo_tip"],
LogoPutanja: podesavanja["logo_putanja"],
TopbarLogoSlika: podesavanja["topbar_logo_slika"] == "1",
TopbarLogoTekst: podesavanja["topbar_logo_tekst"] == "1",
Sacuvano: r.URL.Query().Get("sacuvano") == "1",
BackupVracen: r.URL.Query().Get("sacuvano") == "vraceno",
Verzija: h.Verzija,
@@ -233,13 +235,24 @@ func (h *Handler) SacuvajPodesavanja(w http.ResponseWriter, r *http.Request) {
return
}
// checkbox-i: šalju vrednost samo kada su čekirani, pa ih uvek eksplicitno čitamo
topbarLogoSlika := "0"
if r.FormValue("topbar_logo_slika") == "1" {
topbarLogoSlika = "1"
}
topbarLogoTekst := "0"
if r.FormValue("topbar_logo_tekst") == "1" {
topbarLogoTekst = "1"
}
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"),
"naziv_firme": r.FormValue("naziv_firme"),
"podnazlov": r.FormValue("podnazlov"),
"adresa": r.FormValue("adresa"),
"telefon": r.FormValue("telefon"),
"pib": r.FormValue("pib"),
"topbar_logo_slika": topbarLogoSlika,
"topbar_logo_tekst": topbarLogoTekst,
// profil firme (Faza 0) — radio dugmad uvek šalju vrednost, pa se uredno čuvaju
"firma_pravni_oblik": r.FormValue("firma_pravni_oblik"),
"firma_pdv_obveznik": r.FormValue("firma_pdv_obveznik"),
@@ -412,6 +425,21 @@ func (h *Handler) OtpremiLogo(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/podesavanja?sacuvano=1", http.StatusSeeOther)
}
// UkloniLogo briše logo fajl i čisti putanju iz podešavanja
func (h *Handler) UkloniLogo(w http.ResponseWriter, r *http.Request) {
if _, ok := h.zahtevajDozvolu(w, r, "podesavanja.izmeni"); !ok {
return
}
stari, _ := filepath.Glob("web/static/uploads/logo.*")
for _, s := range stari {
os.Remove(s)
}
if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "logo_putanja", ""); err != nil {
slog.Error("ukloni logo: greška pri čuvanju", "error", err)
}
http.Redirect(w, r, "/admin/podesavanja/opste?sacuvano=1", http.StatusSeeOther)
}
// generisiImeUploada vraća slučajno hex ime (16 bajtova) sa datom ekstenzijom
func generisiImeUploada(ext string) (string, error) {
buf := make([]byte, 16)
@@ -624,8 +652,9 @@ func (h *Handler) napuniPodaciPodesavanja(r *http.Request, naslov string) (Podac
Adresa: podesavanja["adresa"],
Telefon: podesavanja["telefon"],
PIB: podesavanja["pib"],
LogoTip: podesavanja["logo_tip"],
LogoPutanja: podesavanja["logo_putanja"],
TopbarLogoSlika: podesavanja["topbar_logo_slika"] == "1",
TopbarLogoTekst: podesavanja["topbar_logo_tekst"] == "1",
FirmaPravniOblik: vrednostIliDefault(podesavanja, "firma_pravni_oblik", "pausalac"),
FirmaPdvObveznik: vrednostIliDefault(podesavanja, "firma_pdv_obveznik", "ne"),
FirmaFiskalizacija: vrednostIliDefault(podesavanja, "firma_fiskalizacija", "ne"),
+115
View File
@@ -289,3 +289,118 @@ func (h *Handler) SacuvajLokalnuTemu(w http.ResponseWriter, r *http.Request) {
}
http.Redirect(w, r, "/admin/profil", http.StatusSeeOther)
}
// ProfilOtpremiAvatar prima upload lične avatar slike korisnika
func (h *Handler) ProfilOtpremiAvatar(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if k == nil {
http.Redirect(w, r, "/prijava", http.StatusSeeOther)
return
}
r.Body = http.MaxBytesReader(w, r.Body, 2<<20+4096)
if err := r.ParseMultipartForm(2 << 20); err != nil {
middleware.SetFlash(w, r, h.DB, "greska", "Fajl je prevelik (maksimum 2 MB).")
http.Redirect(w, r, "/profil/tema", http.StatusSeeOther)
return
}
fajl, zaglavlje, err := r.FormFile("avatar")
if err != nil {
middleware.SetFlash(w, r, h.DB, "greska", "Nije odabran fajl.")
http.Redirect(w, r, "/profil/tema", http.StatusSeeOther)
return
}
defer fajl.Close()
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, "/profil/tema", 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, "/profil/tema", 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, "/profil/tema", http.StatusSeeOther)
return
}
// briše stari avatar sa diska
svezi, _ := h.KorisniciRepo.DohvatiPoID(r.Context(), k.ID)
if svezi != nil && svezi.AvatarPutanja != "" {
deo, _, _ := strings.Cut(svezi.AvatarPutanja, "?")
os.Remove(filepath.Join("web/static/uploads", filepath.Base(deo)))
}
novoIme := fmt.Sprintf("korisnik_%d_avatar%s", k.ID, ext)
odrediste := filepath.Join("web/static/uploads", novoIme)
dst, err := os.Create(odrediste)
if err != nil {
slog.Error("ProfilOtpremiAvatar: ne mogu kreirati fajl", "error", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju fajla.")
http.Redirect(w, r, "/profil/tema", http.StatusSeeOther)
return
}
defer dst.Close()
if _, err := io.Copy(dst, fajl); err != nil {
slog.Error("ProfilOtpremiAvatar: greška pri kopiranju", "error", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju fajla.")
http.Redirect(w, r, "/profil/tema", http.StatusSeeOther)
return
}
// cache-buster verzija u URL-u
v := fmt.Sprintf("%d", time.Now().Unix())
putanja := fmt.Sprintf("/static/uploads/%s?v=%s", novoIme, v)
if err := h.KorisniciRepo.SacuvajAvatar(r.Context(), k.ID, putanja); err != nil {
slog.Error("ProfilOtpremiAvatar: greška pri čuvanju u bazi", "error", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju.")
http.Redirect(w, r, "/profil/tema", http.StatusSeeOther)
return
}
middleware.SetFlash(w, r, h.DB, "uspeh", "Avatar je sačuvan.")
http.Redirect(w, r, "/profil/tema", http.StatusSeeOther)
}
// ProfilUkloniAvatar briše ličnu avatar sliku korisnika
func (h *Handler) ProfilUkloniAvatar(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if k == nil {
http.Redirect(w, r, "/prijava", http.StatusSeeOther)
return
}
svezi, _ := h.KorisniciRepo.DohvatiPoID(r.Context(), k.ID)
if svezi != nil && svezi.AvatarPutanja != "" {
deo, _, _ := strings.Cut(svezi.AvatarPutanja, "?")
os.Remove(filepath.Join("web/static/uploads", filepath.Base(deo)))
}
if err := h.KorisniciRepo.SacuvajAvatar(r.Context(), k.ID, ""); err != nil {
slog.Error("ProfilUkloniAvatar", "error", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri uklanjanju avatara.")
http.Redirect(w, r, "/profil/tema", http.StatusSeeOther)
return
}
middleware.SetFlash(w, r, h.DB, "uspeh", "Avatar je uklonjen.")
http.Redirect(w, r, "/profil/tema", http.StatusSeeOther)
}