Izmena u radu tema

This commit is contained in:
2026-06-06 17:47:52 +02:00
parent 6a3500c25a
commit 5d94ea34cf
32 changed files with 1182 additions and 179 deletions
+60 -3
View File
@@ -30,7 +30,22 @@ type podaciAdminProfil struct {
TotpURI string
TotpTajna string
TotpQR template.URL
TotpAktivan bool
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
}
// AdminKorisnici prikazuje listu korisnika
@@ -285,11 +300,53 @@ func (h *Handler) AdminProfil(w http.ResponseWriter, r *http.Request) {
ps.NaslovStranice = "Moj profil"
h.renderujTemplate(w, "admin_profil", podaciAdminProfil{
PodaciStranice: ps,
TotpAktivan: svezi.TotpTajna != "",
PodaciStranice: ps,
TotpAktivan: svezi.TotpTajna != "",
LokalnaTema: svezi.LokalnaTema,
KoristiLokalnuTemu: svezi.KoristiLokalnuTemu,
GlobalnaTema: podesavanja["globalna_tema"],
})
}
// AdminSacuvajLokalnuTemu čuva korisnikovu lokalnu temu
func (h *Handler) AdminSacuvajLokalnuTemu(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if k == nil {
http.Redirect(w, r, "/prijava", http.StatusSeeOther)
return
}
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "tema.lokalno") {
http.Error(w, "Pristup odbijen", http.StatusForbidden)
return
}
if err := r.ParseForm(); err != nil {
middleware.SetFlash(w, r, h.DB, "greska", "Greška. Pokušajte ponovo.")
http.Redirect(w, r, "/admin/profil", http.StatusSeeOther)
return
}
koristi := r.FormValue("koristi_lokalnu_temu") == "1"
lokalnaTema := r.FormValue("lokalna_tema")
if lokalnaTema != "tamna" && lokalnaTema != "svetla" {
lokalnaTema = "tamna"
}
if err := h.KorisniciRepo.SacuvajLokalnuTemu(r.Context(), k.ID, lokalnaTema, koristi); err != nil {
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju. Pokušajte ponovo.")
http.Redirect(w, r, "/admin/profil", http.StatusSeeOther)
return
}
middleware.SetFlash(w, r, h.DB, "uspeh", "Tema je sačuvana.")
// vrati korisnika na stranicu odakle je došao (Referer), ili na profil kao fallback
if ref := r.Referer(); ref != "" {
http.Redirect(w, r, ref, http.StatusSeeOther)
return
}
http.Redirect(w, r, "/admin/profil", http.StatusSeeOther)
}
// AdminPromeniLozinku menja lozinku prijavljenog korisnika
func (h *Handler) AdminPromeniLozinku(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
+16 -6
View File
@@ -5,7 +5,9 @@ import (
"net/http"
"time"
appdb "ntech/internal/db"
"ntech/internal/db/sqlite"
"ntech/internal/middleware"
"ntech/internal/model"
)
@@ -47,11 +49,15 @@ func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) {
log.Printf("dashboard: aktivni servisi: %v", err)
}
if err := h.DB.QueryRowContext(ctx, `
SELECT COALESCE(SUM(ukupno), 0) FROM prodajni_nalozi
WHERE substr(datum, 1, 7) = strftime('%Y-%m', 'now', 'localtime')`,
).Scan(&prihodOvogMeseca); err != nil {
log.Printf("dashboard: prihod ovog meseca: %v", err)
// prihod se dohvata samo ako korisnik ima dozvolu dashboard.prihod
korisnikDash := middleware.KorisnikIzKonteksta(ctx)
if h.DozvoleRepo.ImaDozvolu(ctx, korisnikDash.Uloga, "dashboard.prihod") {
if err := h.DB.QueryRowContext(ctx, `
SELECT COALESCE(SUM(ukupno), 0) FROM prodajni_nalozi
WHERE substr(datum, 1, 7) = strftime('%Y-%m', 'now', 'localtime')`,
).Scan(&prihodOvogMeseca); err != nil {
log.Printf("dashboard: prihod ovog meseca: %v", err)
}
}
if err := h.DB.QueryRowContext(ctx,
@@ -60,7 +66,11 @@ func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) {
log.Printf("dashboard: kriticna zaliha: %v", err)
}
if n, err := h.PodsetniciFRepo.BrojAktivnih(ctx); err != nil {
korisnikFilter := appdb.PodsetnikFilter{}
if korisnikDash.Uloga == "radnik" {
korisnikFilter.KorisnikID = &korisnikDash.ID
}
if n, err := h.PodsetniciFRepo.BrojAktivnih(ctx, korisnikFilter); err != nil {
log.Printf("dashboard: aktivni podsetnici: %v", err)
} else {
aktivniPodsetnici = n
+69 -13
View File
@@ -74,35 +74,91 @@ 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
ps := model.PodaciStranice{
Tema: podesavanja["tema"],
Tema: tema,
NazivFirme: podesavanja["naziv_firme"],
Podnazlov: podesavanja["podnazlov"],
LogoTip: podesavanja["logo_tip"],
LogoPutanja: podesavanja["logo_putanja"],
Korisnik: "Admin",
}
var korisnik *model.Korisnik
if k := middleware.KorisnikIzKonteksta(r.Context()); k != nil {
korisnik = k
ps.Korisnik = k.KorisnickoIme
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 != "" {
ps.Tema = k.LokalnaTema
}
}
ps.CsrfToken = middleware.CsrfToken(r.Context())
ps.Flash = middleware.GetFlash(r, h.DB)
ps.AppPozadina = podesavanja["app_pozadina"]
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"
// logika pozadine:
// - lična pozadina (samo kada je lokalni režim aktivan) → zamenjuje globalnu
// - 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 != "" {
ps.AppPozadina = korisnik.LokalnaPozadina
ps.Tema = "tamna"
ps.AppPozadinaOpacity = korisnik.LokalnaPozadinaOpacity
if ps.AppPozadinaOpacity == "" {
ps.AppPozadinaOpacity = "50"
}
ps.AppPozadinaBlur = korisnik.LokalnaPozadinaBlur
if ps.AppPozadinaBlur == "" {
ps.AppPozadinaBlur = "12"
}
ps.AppPozadinaBlurPozadine = korisnik.LokalnaPozadinaBlurPozadine
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"
}
}
}
return ps
+1
View File
@@ -26,6 +26,7 @@ var saSidebar = []string{
"nabavke", "nabavka_forma", "nabavka_detalji",
"podesavanja", "podesavanja_opste", "podesavanja_izgled", "podesavanja_sistem",
"podsetnici", "podsetnik_forma",
"profil_tema",
"prodaja", "prodaja_detalji", "prodaja_forma",
"servis", "servis_forma", "servis_detalji",
}
+18
View File
@@ -57,6 +57,12 @@ func artikalUJSON(artikli []model.ArtikalSaKategorijom) template.JS {
// Nabavke renderuje listu svih nabavki
func (h *Handler) Nabavke(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "nabavka.pregled") {
http.Error(w, "Nemate dozvolu za pregled nabavki.", http.StatusForbidden)
return
}
podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
if err != nil {
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
@@ -84,6 +90,12 @@ func (h *Handler) Nabavke(w http.ResponseWriter, r *http.Request) {
// NovaNabavka prikazuje formu za unos nove nabavke
func (h *Handler) NovaNabavka(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "nabavka.pregled") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
return
}
podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
if err != nil {
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
@@ -163,6 +175,12 @@ func (h *Handler) SacuvajNabavku(w http.ResponseWriter, r *http.Request) {
// DetaljiNabavke prikazuje pregled jedne nabavke sa svim stavkama
func (h *Handler) DetaljiNabavke(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "nabavka.pregled") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
return
}
id, err := parseID(chi.URLParam(r, "id"))
if err != nil {
http.Error(w, "Neispravan ID nabavke", http.StatusBadRequest)
+149 -58
View File
@@ -32,7 +32,7 @@ type PodaciPodesavanja struct {
PIB string
LogoTip string
LogoPutanja string
Tema string
GlobalnaTema string // stvarna vrednost iz podešavanja (ne senči PodaciStranice.Tema)
Sacuvano bool
Verzija string
LogoGreska string
@@ -83,7 +83,15 @@ func (h *Handler) Podesavanja(w http.ResponseWriter, r *http.Request) {
PIB: podesavanja["pib"],
LogoTip: podesavanja["logo_tip"],
LogoPutanja: podesavanja["logo_putanja"],
Tema: podesavanja["tema"],
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,
@@ -252,13 +260,14 @@ 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"),
"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"),
}
for kljuc, vrednost := range polja {
@@ -409,21 +418,21 @@ func (h *Handler) OtpremiLoginPozadinu(w http.ResponseWriter, r *http.Request) {
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, "/podesavanja", http.StatusSeeOther)
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
return
}
fajl, zaglavlje, err := r.FormFile("login_pozadina")
if err != nil {
middleware.SetFlash(w, r, h.DB, "greska", "Nije odabran fajl.")
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
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, "/podesavanja", http.StatusSeeOther)
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
return
}
@@ -437,7 +446,7 @@ func (h *Handler) OtpremiLoginPozadinu(w http.ResponseWriter, r *http.Request) {
ocekivaniMime, ok := dozvoljenoExt[ext]
if !ok {
middleware.SetFlash(w, r, h.DB, "greska", "Dozvoljeni formati su JPG, PNG i WebP.")
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
return
}
@@ -447,12 +456,12 @@ func (h *Handler) OtpremiLoginPozadinu(w http.ResponseWriter, r *http.Request) {
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, "/podesavanja", http.StatusSeeOther)
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, "/podesavanja", http.StatusSeeOther)
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
return
}
@@ -469,7 +478,7 @@ func (h *Handler) OtpremiLoginPozadinu(w http.ResponseWriter, r *http.Request) {
if err != nil {
log.Printf("upload login pozadine: greška pri generisanju imena: %v", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju fajla.")
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
return
}
@@ -478,7 +487,7 @@ func (h *Handler) OtpremiLoginPozadinu(w http.ResponseWriter, r *http.Request) {
if err != nil {
log.Printf("upload login pozadine: ne mogu kreirati fajl: %v", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju fajla.")
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
return
}
defer dst.Close()
@@ -486,7 +495,7 @@ func (h *Handler) OtpremiLoginPozadinu(w http.ResponseWriter, r *http.Request) {
if _, err := io.Copy(dst, fajl); err != nil {
log.Printf("upload login pozadine: greška pri kopiranju: %v", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju fajla.")
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
return
}
@@ -494,12 +503,12 @@ func (h *Handler) OtpremiLoginPozadinu(w http.ResponseWriter, r *http.Request) {
if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "login_pozadina", putanja); err != nil {
log.Printf("upload login 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, "/podesavanja", http.StatusSeeOther)
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
return
}
middleware.SetFlash(w, r, h.DB, "uspeh", "Pozadinska slika je uspešno otpremljena.")
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
}
// UkloniLoginPozadinu briše pozadinsku sliku login stranice sa diska i iz podešavanja
@@ -522,12 +531,12 @@ func (h *Handler) UkloniLoginPozadinu(w http.ResponseWriter, r *http.Request) {
if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "login_pozadina", ""); err != nil {
log.Printf("ukloni login pozadinu: greška pri čuvanju: %v", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri uklanjanju slike.")
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
return
}
middleware.SetFlash(w, r, h.DB, "uspeh", "Pozadinska slika je uklonjena.")
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
}
// OtpremiAppPozadinu prima multipart upload slike i čuva je kao pozadinsku sliku aplikacije
@@ -541,21 +550,21 @@ func (h *Handler) OtpremiAppPozadinu(w http.ResponseWriter, r *http.Request) {
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, "/podesavanja", http.StatusSeeOther)
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, "/podesavanja", http.StatusSeeOther)
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, "/podesavanja", http.StatusSeeOther)
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
return
}
@@ -569,7 +578,7 @@ func (h *Handler) OtpremiAppPozadinu(w http.ResponseWriter, r *http.Request) {
ocekivaniMime, ok := dozvoljenoExt[ext]
if !ok {
middleware.SetFlash(w, r, h.DB, "greska", "Dozvoljeni formati su JPG, PNG i WebP.")
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
return
}
@@ -578,12 +587,12 @@ func (h *Handler) OtpremiAppPozadinu(w http.ResponseWriter, r *http.Request) {
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, "/podesavanja", http.StatusSeeOther)
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, "/podesavanja", http.StatusSeeOther)
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
return
}
@@ -599,7 +608,7 @@ func (h *Handler) OtpremiAppPozadinu(w http.ResponseWriter, r *http.Request) {
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, "/podesavanja", http.StatusSeeOther)
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
return
}
@@ -608,7 +617,7 @@ func (h *Handler) OtpremiAppPozadinu(w http.ResponseWriter, r *http.Request) {
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, "/podesavanja", http.StatusSeeOther)
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
return
}
defer dst.Close()
@@ -616,7 +625,7 @@ func (h *Handler) OtpremiAppPozadinu(w http.ResponseWriter, r *http.Request) {
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, "/podesavanja", http.StatusSeeOther)
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
return
}
@@ -624,12 +633,23 @@ func (h *Handler) OtpremiAppPozadinu(w http.ResponseWriter, r *http.Request) {
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, "/podesavanja", http.StatusSeeOther)
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, "/podesavanja", http.StatusSeeOther)
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
}
// UkloniAppPozadinu briše pozadinsku sliku aplikacije sa diska i iz podešavanja
@@ -652,12 +672,22 @@ func (h *Handler) UkloniAppPozadinu(w http.ResponseWriter, r *http.Request) {
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, "/podesavanja", http.StatusSeeOther)
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, "/podesavanja", http.StatusSeeOther)
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
}
// SacuvajAppPozadinaStilove čuva vrednosti zamućenja i prozirnosti pozadine aplikacije
@@ -669,7 +699,7 @@ func (h *Handler) SacuvajAppPozadinaStilove(w http.ResponseWriter, r *http.Reque
}
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)
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
return
}
@@ -680,19 +710,19 @@ func (h *Handler) SacuvajAppPozadinaStilove(w http.ResponseWriter, r *http.Reque
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, "/podesavanja", http.StatusSeeOther)
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, "/podesavanja", http.StatusSeeOther)
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, "/podesavanja", http.StatusSeeOther)
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
return
}
@@ -704,13 +734,13 @@ func (h *Handler) SacuvajAppPozadinaStilove(w http.ResponseWriter, r *http.Reque
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)
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, "/podesavanja", http.StatusSeeOther)
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
}
// SacuvajLoginPozadinaStilove čuva vrednosti zamućenja i prozirnosti pozadine login stranice
@@ -722,7 +752,7 @@ func (h *Handler) SacuvajLoginPozadinaStilove(w http.ResponseWriter, r *http.Req
}
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)
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
return
}
@@ -733,19 +763,19 @@ func (h *Handler) SacuvajLoginPozadinaStilove(w http.ResponseWriter, r *http.Req
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)
http.Redirect(w, r, "/admin/podesavanja/izgled", 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)
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, "/podesavanja", http.StatusSeeOther)
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
return
}
@@ -757,13 +787,13 @@ func (h *Handler) SacuvajLoginPozadinaStilove(w http.ResponseWriter, r *http.Req
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)
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
return
}
}
middleware.SetFlash(w, r, h.DB, "uspeh", "Izgled pozadine prijave je sačuvan.")
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
}
// napuniPodaciPodesavanja učitava sva podešavanja i kreira strukturu za template
@@ -784,7 +814,15 @@ func (h *Handler) napuniPodaciPodesavanja(r *http.Request, naslov string) (Podac
PIB: podesavanja["pib"],
LogoTip: podesavanja["logo_tip"],
LogoPutanja: podesavanja["logo_putanja"],
Tema: podesavanja["tema"],
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,
@@ -876,28 +914,81 @@ 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
// 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")
// proveravamo da li je validna tema
validne := map[string]bool{
"tamna": true, "svetla": true,
}
validne := map[string]bool{"tamna": true, "svetla": true}
if !validne[tema] {
http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
return
}
if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "tema", tema); err != nil {
http.Error(w, "Greška pri promeni teme", http.StatusInternalServerError)
return
}
_ = ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "globalna_tema", tema)
// vraćamo se na stranicu sa koje je kliknut dugmić
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)
}
+78 -30
View File
@@ -3,16 +3,19 @@ package handler
import (
"log"
"net/http"
"strconv"
"strings"
"time"
"ntech/internal/db"
"ntech/internal/db/sqlite"
"ntech/internal/middleware"
"ntech/internal/model"
"github.com/go-chi/chi/v5"
)
// podaciPodsetnici su podaci za stranicu sa listom podsetnika
type podaciPodsetnici struct {
model.PodaciStranice
@@ -28,10 +31,13 @@ type podaciPodsetnikForma struct {
Podsetnik model.Podsetnik
Greska string
Izmena bool
Korisnici []model.Korisnik
}
// Podsetnici renderuje listu podsetnika
func (h *Handler) Podsetnici(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
if err != nil {
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
@@ -40,7 +46,13 @@ func (h *Handler) Podsetnici(w http.ResponseWriter, r *http.Request) {
samoAktivni := r.URL.Query().Get("samo_aktivni") == "1"
lista, err := h.PodsetniciFRepo.Lista(r.Context(), db.PodsetnikFilter{SamoAktivni: samoAktivni})
// radnik vidi samo podsetnik koji su mu dodeljeni
filter := db.PodsetnikFilter{SamoAktivni: samoAktivni}
if k.Uloga == "radnik" {
filter.KorisnikID = &k.ID
}
lista, err := h.PodsetniciFRepo.Lista(r.Context(), filter)
if err != nil {
http.Error(w, "Greška pri učitavanju podsetnika", http.StatusInternalServerError)
return
@@ -63,6 +75,8 @@ func (h *Handler) Podsetnici(w http.ResponseWriter, r *http.Request) {
// NoviPodsetnik prikazuje praznu formu za unos novog podsetnika
func (h *Handler) NoviPodsetnik(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
if err != nil {
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
@@ -73,46 +87,55 @@ func (h *Handler) NoviPodsetnik(w http.ResponseWriter, r *http.Request) {
ps.Stranica = "podsetnici"
ps.NaslovStranice = "Novi podsetnik"
var korisnici []model.Korisnik
if k.Uloga == "admin" || k.Uloga == "superadmin" {
korisnici, _ = h.KorisniciRepo.Lista(r.Context())
}
h.renderujFormuPodsetnika(w, podaciPodsetnikForma{
PodaciStranice: ps,
Izmena: false,
Korisnici: korisnici,
})
}
// SacuvajPodsetnik prima POST formu i upisuje novi podsetnik u bazu
func (h *Handler) SacuvajPodsetnik(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if err := r.ParseForm(); err != nil {
http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest)
return
}
podsetnik, greska := parseFormuPodsetnika(r)
if greska != "" {
podsetnik, greska := parseFormuPodsetnika(r, k)
prikaziGresku := func(poruka string) {
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "podsetnici"
ps.NaslovStranice = "Novi podsetnik"
var korisnici []model.Korisnik
if k.Uloga == "admin" || k.Uloga == "superadmin" {
korisnici, _ = h.KorisniciRepo.Lista(r.Context())
}
h.renderujFormuPodsetnika(w, podaciPodsetnikForma{
PodaciStranice: ps,
Podsetnik: podsetnik,
Greska: greska,
Greska: poruka,
Izmena: false,
Korisnici: korisnici,
})
}
if greska != "" {
prikaziGresku(greska)
return
}
if _, err := h.PodsetniciFRepo.Kreiraj(r.Context(), &podsetnik); err != nil {
log.Printf("greška pri čuvanju podsetnika: %v", err)
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "podsetnici"
ps.NaslovStranice = "Novi podsetnik"
h.renderujFormuPodsetnika(w, podaciPodsetnikForma{
PodaciStranice: ps,
Podsetnik: podsetnik,
Greska: "Došlo je do greške pri čuvanju. Pokušajte ponovo.",
Izmena: false,
})
prikaziGresku("Došlo je do greške pri čuvanju. Pokušajte ponovo.")
return
}
@@ -121,6 +144,8 @@ func (h *Handler) SacuvajPodsetnik(w http.ResponseWriter, r *http.Request) {
// IzmeniPodsetnik učitava podsetnik po ID-u i prikazuje popunjenu formu za izmenu
func (h *Handler) IzmeniPodsetnik(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
id, err := parseID(chi.URLParam(r, "id"))
if err != nil {
http.Error(w, "Neispravan ID podsetnika", http.StatusBadRequest)
@@ -143,15 +168,23 @@ func (h *Handler) IzmeniPodsetnik(w http.ResponseWriter, r *http.Request) {
ps.Stranica = "podsetnici"
ps.NaslovStranice = "Izmeni podsetnik"
var korisnici []model.Korisnik
if k.Uloga == "admin" || k.Uloga == "superadmin" {
korisnici, _ = h.KorisniciRepo.Lista(r.Context())
}
h.renderujFormuPodsetnika(w, podaciPodsetnikForma{
PodaciStranice: ps,
Podsetnik: *podsetnik,
Izmena: true,
Korisnici: korisnici,
})
}
// SacuvajIzmenePodsetnika prima POST formu i ažurira postojeći podsetnik u bazi
func (h *Handler) SacuvajIzmenePodsetnika(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
id, err := parseID(chi.URLParam(r, "id"))
if err != nil {
http.Error(w, "Neispravan ID podsetnika", http.StatusBadRequest)
@@ -163,34 +196,35 @@ func (h *Handler) SacuvajIzmenePodsetnika(w http.ResponseWriter, r *http.Request
return
}
podsetnik, greska := parseFormuPodsetnika(r)
podsetnik, greska := parseFormuPodsetnika(r, k)
podsetnik.ID = id
if greska != "" {
prikaziGresku := func(poruka string) {
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "podsetnici"
ps.NaslovStranice = "Izmeni podsetnik"
var korisnici []model.Korisnik
if k.Uloga == "admin" || k.Uloga == "superadmin" {
korisnici, _ = h.KorisniciRepo.Lista(r.Context())
}
h.renderujFormuPodsetnika(w, podaciPodsetnikForma{
PodaciStranice: ps,
Podsetnik: podsetnik,
Greska: greska,
Greska: poruka,
Izmena: true,
Korisnici: korisnici,
})
}
if greska != "" {
prikaziGresku(greska)
return
}
if err := h.PodsetniciFRepo.Izmeni(r.Context(), &podsetnik); err != nil {
log.Printf("greška pri čuvanju izmene podsetnika: %v", err)
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "podsetnici"
ps.NaslovStranice = "Izmeni podsetnik"
h.renderujFormuPodsetnika(w, podaciPodsetnikForma{
PodaciStranice: ps,
Podsetnik: podsetnik,
Greska: "Došlo je do greške pri čuvanju. Pokušajte ponovo.",
Izmena: true,
})
prikaziGresku("Došlo je do greške pri čuvanju. Pokušajte ponovo.")
return
}
@@ -237,7 +271,7 @@ func (h *Handler) ObrisiPodsetnik(w http.ResponseWriter, r *http.Request) {
}
// parseFormuPodsetnika čita polja iz HTTP forme, validira ih i vraća model i eventualnu grešku
func parseFormuPodsetnika(r *http.Request) (model.Podsetnik, string) {
func parseFormuPodsetnika(r *http.Request, k *model.Korisnik) (model.Podsetnik, string) {
naslov := strings.TrimSpace(r.FormValue("naslov"))
if naslov == "" {
return model.Podsetnik{}, "Naslov je obavezan."
@@ -253,12 +287,26 @@ func parseFormuPodsetnika(r *http.Request) (model.Podsetnik, string) {
return model.Podsetnik{Naslov: naslov}, "Datum podsećanja nije u ispravnom formatu."
}
return model.Podsetnik{
p := model.Podsetnik{
Naslov: naslov,
Napomena: strings.TrimSpace(r.FormValue("napomena")),
DatumPodsecanja: datum,
Tip: model.TipOpsti,
}, ""
}
// admin/superadmin mogu dodeliti podsetnik drugom korisniku
if k.Uloga == "admin" || k.Uloga == "superadmin" {
if kidStr := strings.TrimSpace(r.FormValue("korisnik_id")); kidStr != "" {
if kid, err := strconv.ParseInt(kidStr, 10, 64); err == nil && kid > 0 {
p.KorisnikID = &kid
}
}
} else {
// radnik dobija podsetnik dodeljen sebi
p.KorisnikID = &k.ID
}
return p, ""
}
// renderujFormuPodsetnika renderuje HTML šablon forme za unos ili izmenu podsetnika
+20
View File
@@ -376,6 +376,26 @@ func parseFormuProdaje(r *http.Request) (model.ProdajniNalog, []model.StavkaProd
return nalog, stavke, ""
}
// StornoProdaje stornira prodajni nalog: vraća artikle na stanje i briše nalog
func (h *Handler) StornoProdaje(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "prodaja.storno") {
http.Error(w, "Nemate dozvolu za storniranje prodaje.", http.StatusForbidden)
return
}
id, err := parseID(chi.URLParam(r, "id"))
if err != nil {
http.Error(w, "Neispravan ID naloga", http.StatusBadRequest)
return
}
if err := h.ProdajaRepo.Obrisi(r.Context(), id); err != nil {
http.Error(w, "Greška pri storniranju naloga", http.StatusInternalServerError)
return
}
middleware.SetFlash(w, r, h.DB, "uspeh", "Prodajni nalog je storniran.")
http.Redirect(w, r, "/prodaja", http.StatusSeeOther)
}
// renderujFormuProdaje renderuje HTML šablon forme za unos nove prodaje
func (h *Handler) renderujFormuProdaje(w http.ResponseWriter, podaci PodaciFormeProdaje) {
h.renderujTemplate(w, "prodaja_forma", podaci)
+242
View File
@@ -0,0 +1,242 @@
package handler
import (
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"ntech/internal/db/sqlite"
"ntech/internal/middleware"
)
// ProfilTema prikazuje stranicu lične teme i pozadine
func (h *Handler) ProfilTema(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "tema.lokalno") {
http.Error(w, "Pristup odbijen.", http.StatusForbidden)
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
}
svezi, err := h.KorisniciRepo.DohvatiPoID(r.Context(), k.ID)
if err != nil {
http.Error(w, "Greška pri učitavanju profila", http.StatusInternalServerError)
return
}
ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "profil-tema"
ps.NaslovStranice = "Moja tema"
podaci := podaciProfilTema{
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,
}
if podaci.LokalnaPozadinaOpacity == "" {
podaci.LokalnaPozadinaOpacity = "50"
}
if podaci.LokalnaPozadinaBlur == "" {
podaci.LokalnaPozadinaBlur = "12"
}
if podaci.LokalnaPozadinaBlurPozadine == "" {
podaci.LokalnaPozadinaBlurPozadine = "0"
}
h.renderujTemplate(w, "profil_tema", podaci)
}
// ProfilOtpremiPozadinu prima upload lične pozadinske slike
func (h *Handler) ProfilOtpremiPozadinu(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "tema.lokalno") {
http.Error(w, "Pristup odbijen.", 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, "/profil/tema", http.StatusSeeOther)
return
}
fajl, zaglavlje, err := r.FormFile("lokalna_pozadina")
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()
if zaglavlje.Size > 5<<20 {
middleware.SetFlash(w, r, h.DB, "greska", "Fajl je prevelik (maksimum 5 MB).")
http.Redirect(w, r, "/profil/tema", 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, "/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 staru ličnu pozadinu sa diska ako postoji
svezi, _ := h.KorisniciRepo.DohvatiPoID(r.Context(), k.ID)
if svezi != nil && svezi.LokalnaPozadina != "" {
deo, _, _ := strings.Cut(svezi.LokalnaPozadina, "?")
os.Remove(filepath.Join("web/static/uploads", filepath.Base(deo)))
}
// ime fajla: korisnik_{id}_pozadina.ext — deterministično, lako obrisati
novoIme := fmt.Sprintf("korisnik_%d_pozadina%s", k.ID, ext)
odrediste := filepath.Join("web/static/uploads", novoIme)
dst, err := os.Create(odrediste)
if err != nil {
log.Printf("ProfilOtpremiPozadinu: ne mogu kreirati fajl: %v", 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 {
log.Printf("ProfilOtpremiPozadinu: greška pri kopiranju: %v", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju fajla.")
http.Redirect(w, r, "/profil/tema", http.StatusSeeOther)
return
}
putanja := fmt.Sprintf("/static/uploads/%s?v=%d", novoIme, time.Now().Unix())
opacity := "50"
blur := "12"
blurPozadine := "0"
if svezi != nil {
if svezi.LokalnaPozadinaOpacity != "" {
opacity = svezi.LokalnaPozadinaOpacity
}
if svezi.LokalnaPozadinaBlur != "" {
blur = svezi.LokalnaPozadinaBlur
}
if svezi.LokalnaPozadinaBlurPozadine != "" {
blurPozadine = svezi.LokalnaPozadinaBlurPozadine
}
}
if err := h.KorisniciRepo.SacuvajLokalnuPozadinu(r.Context(), k.ID, putanja, opacity, blur, blurPozadine); 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)
return
}
middleware.SetFlash(w, r, h.DB, "uspeh", "Pozadinska slika je uspešno otpremljena.")
http.Redirect(w, r, "/profil/tema", http.StatusSeeOther)
}
// ProfilUkloniPozadinu briše ličnu pozadinsku sliku korisnika
func (h *Handler) ProfilUkloniPozadinu(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "tema.lokalno") {
http.Error(w, "Pristup odbijen.", http.StatusForbidden)
return
}
svezi, _ := h.KorisniciRepo.DohvatiPoID(r.Context(), k.ID)
if svezi != nil && svezi.LokalnaPozadina != "" {
deo, _, _ := strings.Cut(svezi.LokalnaPozadina, "?")
os.Remove(filepath.Join("web/static/uploads", filepath.Base(deo)))
}
if err := h.KorisniciRepo.SacuvajLokalnuPozadinu(r.Context(), k.ID, "", "50", "12", "0"); 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)
return
}
middleware.SetFlash(w, r, h.DB, "uspeh", "Pozadinska slika je uklonjena.")
http.Redirect(w, r, "/profil/tema", http.StatusSeeOther)
}
// ProfilSacuvajPozadinuStilove čuva opacity i blur lične pozadine
func (h *Handler) ProfilSacuvajPozadinuStilove(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "tema.lokalno") {
http.Error(w, "Pristup odbijen.", 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, "/profil/tema", http.StatusSeeOther)
return
}
svezi, _ := h.KorisniciRepo.DohvatiPoID(r.Context(), k.ID)
pozadina := ""
if svezi != nil {
pozadina = svezi.LokalnaPozadina
}
opacity := r.FormValue("lokalna_pozadina_opacity")
blur := r.FormValue("lokalna_pozadina_blur")
blurPozadineSt := r.FormValue("lokalna_pozadina_blur_pozadine")
if opacity == "" {
opacity = "50"
}
if blur == "" {
blur = "12"
}
if blurPozadineSt == "" {
blurPozadineSt = "0"
}
if err := h.KorisniciRepo.SacuvajLokalnuPozadinu(r.Context(), k.ID, pozadina, opacity, blur, blurPozadineSt); 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)
return
}
middleware.SetFlash(w, r, h.DB, "uspeh", "Podešavanja su sačuvana.")
http.Redirect(w, r, "/profil/tema", http.StatusSeeOther)
}