diff --git a/cmd/ntech/main.go b/cmd/ntech/main.go
index 8885162..dd60fbe 100644
--- a/cmd/ntech/main.go
+++ b/cmd/ntech/main.go
@@ -118,9 +118,17 @@ func main() {
r.Use(middleware.Compress(5))
// uploads su uvek na disku — korisnički fajlovi, ne ugrađuju se
- r.Handle("/static/uploads/*", http.StripPrefix("/static/uploads/", http.FileServer(http.Dir("web/static/uploads"))))
+ r.Handle("/static/uploads/*", http.StripPrefix("/static/uploads/",
+ http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+ w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
+ http.FileServer(http.Dir("web/static/uploads")).ServeHTTP(w, req)
+ })))
// ostali statični fajlovi: disk ako postoji web/static, inače embed
- r.Handle("/static/*", http.StripPrefix("/static/", http.FileServer(http.FS(staticFS))))
+ r.Handle("/static/*", http.StripPrefix("/static/",
+ http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+ w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
+ http.FileServer(http.FS(staticFS)).ServeHTTP(w, req)
+ })))
// javne rute (bez autentifikacije)
r.Get("/prijava", h.PrikazPrijave)
@@ -154,6 +162,8 @@ func main() {
r.Get("/podesavanja/backup", h.BackupBaze)
r.Post("/podesavanja/backup/vrati", h.VratiBackup)
r.Get("/tema/{tema}", h.PromeniTemu)
+ r.Post("/podesavanja/tema", h.PromeniGlobalnuTemu)
+ r.Post("/podesavanja/tema/globalno", h.PrimeniGlobalnuTemu)
r.Get("/magacin", h.Magacin)
r.Get("/magacin/novi", h.NoviArtikal)
r.Post("/magacin/novi", h.SacuvajArtikal)
@@ -192,6 +202,7 @@ func main() {
r.Get("/prodaja/nova", h.NovaProdaja)
r.Post("/prodaja/nova", h.SacuvajProdaju)
r.Post("/prodaja/obrisi/{id}", h.ObrisiProdaju)
+ r.Post("/prodaja/storno/{id}", h.StornoProdaje)
r.Get("/prodaja/{id}/stampa", h.StampaProdaje)
r.Get("/prodaja/{id}", h.DetaljiProdaje)
@@ -229,6 +240,11 @@ func main() {
r.Get("/admin/profil/totp/pokreni", h.AdminTotpPokreni)
r.Post("/admin/profil/totp/aktiviraj", h.AdminTotpAktivacija)
r.Post("/admin/profil/totp/deaktiviraj", h.AdminTotpDeaktivacija)
+ r.Post("/profil/tema", h.AdminSacuvajLokalnuTemu)
+ r.Get("/profil/tema", h.ProfilTema)
+ r.Post("/profil/pozadina", h.ProfilOtpremiPozadinu)
+ r.Post("/profil/pozadina/ukloni", h.ProfilUkloniPozadinu)
+ r.Post("/profil/pozadina/stilovi", h.ProfilSacuvajPozadinuStilove)
})
log.Printf("NTech pokrenut na portu %s", port)
diff --git a/internal/db/repository.go b/internal/db/repository.go
index 39ac308..e7bb78c 100644
--- a/internal/db/repository.go
+++ b/internal/db/repository.go
@@ -86,6 +86,8 @@ type KorisniciRepository interface {
AzurirajAktivan(ctx context.Context, id int64, aktivan bool) error
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
PostojiIjedan(ctx context.Context) (bool, error)
Obrisi(ctx context.Context, id int64) error
}
@@ -101,7 +103,8 @@ type SesijeRepository interface {
// PodsetnikFilter definiše parametre za filtriranje liste podsetnika
type PodsetnikFilter struct {
- SamoAktivni bool // true = samo nezavršeni; false = svi
+ SamoAktivni bool // true = samo nezavršeni; false = svi
+ KorisnikID *int64 // ako nije nil — samo podsetnici tog korisnika
}
// PokusajiPrijaveRepository definiše operacije nad evidencijom pokušaja prijave
@@ -134,5 +137,5 @@ type PodsetnikRepository interface {
Izmeni(ctx context.Context, p *model.Podsetnik) error
OznaciZavrsenim(ctx context.Context, id int64, zavrseno bool) error
Obrisi(ctx context.Context, id int64) error
- BrojAktivnih(ctx context.Context) (int, error)
+ BrojAktivnih(ctx context.Context, filter PodsetnikFilter) (int, error)
}
diff --git a/internal/db/sqlite/korisnici.go b/internal/db/sqlite/korisnici.go
index 861fe4a..b691a4c 100644
--- a/internal/db/sqlite/korisnici.go
+++ b/internal/db/sqlite/korisnici.go
@@ -29,41 +29,69 @@ func (r *sqliteKorisniciRepo) Kreiraj(ctx context.Context, korisnickoIme, lozink
func (r *sqliteKorisniciRepo) DohvatiPoImenu(ctx context.Context, korisnickoIme string) (*model.Korisnik, error) {
k := &model.Korisnik{}
- var aktivan int
- var totpTajna sql.NullString
+ var aktivan, koristiLokalnuTemu int
+ var totpTajna, lokalnaTema sql.NullString
+ var lokalnaPozadina, lokalnaPozadinaOpacity, lokalnaPozadinaBlur, lokalnaPozadinaBlurPozadine sql.NullString
var datumKreiranja time.Time
err := r.db.QueryRowContext(ctx,
- `SELECT id, korisnicko_ime, lozinka_hash, uloga, aktivan, COALESCE(totp_tajna, ''), datum_kreiranja
+ `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')
FROM korisnici WHERE korisnicko_ime = ?`, korisnickoIme).
- Scan(&k.ID, &k.KorisnickoIme, &k.LozinkaHash, &k.Uloga, &aktivan, &totpTajna, &datumKreiranja)
+ Scan(&k.ID, &k.KorisnickoIme, &k.LozinkaHash, &k.Uloga, &aktivan, &totpTajna,
+ &lokalnaTema, &koristiLokalnuTemu, &datumKreiranja,
+ &lokalnaPozadina, &lokalnaPozadinaOpacity, &lokalnaPozadinaBlur, &lokalnaPozadinaBlurPozadine)
if err != nil {
return nil, fmt.Errorf("ntech: korisnici.DohvatiPoImenu: %w", err)
}
k.Aktivan = aktivan == 1
k.TotpTajna = totpTajna.String
+ k.LokalnaTema = lokalnaTema.String
+ k.KoristiLokalnuTemu = koristiLokalnuTemu == 1
k.DatumKreiranja = datumKreiranja
+ k.LokalnaPozadina = lokalnaPozadina.String
+ k.LokalnaPozadinaOpacity = lokalnaPozadinaOpacity.String
+ k.LokalnaPozadinaBlur = lokalnaPozadinaBlur.String
+ k.LokalnaPozadinaBlurPozadine = lokalnaPozadinaBlurPozadine.String
return k, nil
}
func (r *sqliteKorisniciRepo) DohvatiPoID(ctx context.Context, id int64) (*model.Korisnik, error) {
k := &model.Korisnik{}
- var aktivan int
+ var aktivan, koristiLokalnuTemu int
+ var lokalnaTema sql.NullString
+ var lokalnaPozadina, lokalnaPozadinaOpacity, lokalnaPozadinaBlur, lokalnaPozadinaBlurPozadine sql.NullString
var datumKreiranja time.Time
err := r.db.QueryRowContext(ctx,
- `SELECT id, korisnicko_ime, lozinka_hash, uloga, aktivan, COALESCE(totp_tajna, ''), datum_kreiranja
+ `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')
FROM korisnici WHERE id = ?`, id).
- Scan(&k.ID, &k.KorisnickoIme, &k.LozinkaHash, &k.Uloga, &aktivan, &k.TotpTajna, &datumKreiranja)
+ Scan(&k.ID, &k.KorisnickoIme, &k.LozinkaHash, &k.Uloga, &aktivan, &k.TotpTajna,
+ &lokalnaTema, &koristiLokalnuTemu, &datumKreiranja,
+ &lokalnaPozadina, &lokalnaPozadinaOpacity, &lokalnaPozadinaBlur, &lokalnaPozadinaBlurPozadine)
if err != nil {
return nil, fmt.Errorf("ntech: korisnici.DohvatiPoID: %w", err)
}
k.Aktivan = aktivan == 1
+ k.LokalnaTema = lokalnaTema.String
+ k.KoristiLokalnuTemu = koristiLokalnuTemu == 1
k.DatumKreiranja = datumKreiranja
+ k.LokalnaPozadina = lokalnaPozadina.String
+ k.LokalnaPozadinaOpacity = lokalnaPozadinaOpacity.String
+ k.LokalnaPozadinaBlur = lokalnaPozadinaBlur.String
+ k.LokalnaPozadinaBlurPozadine = lokalnaPozadinaBlurPozadine.String
return k, nil
}
func (r *sqliteKorisniciRepo) Lista(ctx context.Context) ([]model.Korisnik, error) {
rows, err := r.db.QueryContext(ctx,
- `SELECT id, korisnicko_ime, lozinka_hash, uloga, aktivan, COALESCE(totp_tajna, ''), datum_kreiranja
+ `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')
FROM korisnici ORDER BY datum_kreiranja ASC`)
if err != nil {
return nil, fmt.Errorf("ntech: korisnici.Lista: %w", err)
@@ -72,18 +100,58 @@ func (r *sqliteKorisniciRepo) Lista(ctx context.Context) ([]model.Korisnik, erro
var lista []model.Korisnik
for rows.Next() {
var k model.Korisnik
- var aktivan int
+ var aktivan, koristiLokalnuTemu int
+ var lokalnaTema sql.NullString
+ var lokalnaPozadina, lokalnaPozadinaOpacity, lokalnaPozadinaBlur, lokalnaPozadinaBlurPozadine sql.NullString
var datumKreiranja time.Time
- if err := rows.Scan(&k.ID, &k.KorisnickoIme, &k.LozinkaHash, &k.Uloga, &aktivan, &k.TotpTajna, &datumKreiranja); err != nil {
+ if err := rows.Scan(&k.ID, &k.KorisnickoIme, &k.LozinkaHash, &k.Uloga, &aktivan, &k.TotpTajna,
+ &lokalnaTema, &koristiLokalnuTemu, &datumKreiranja,
+ &lokalnaPozadina, &lokalnaPozadinaOpacity, &lokalnaPozadinaBlur, &lokalnaPozadinaBlurPozadine); err != nil {
return nil, fmt.Errorf("ntech: korisnici.Lista: %w", err)
}
k.Aktivan = aktivan == 1
+ k.LokalnaTema = lokalnaTema.String
+ k.KoristiLokalnuTemu = koristiLokalnuTemu == 1
k.DatumKreiranja = datumKreiranja
+ k.LokalnaPozadina = lokalnaPozadina.String
+ k.LokalnaPozadinaOpacity = lokalnaPozadinaOpacity.String
+ k.LokalnaPozadinaBlur = lokalnaPozadinaBlur.String
+ k.LokalnaPozadinaBlurPozadine = lokalnaPozadinaBlurPozadine.String
lista = append(lista, k)
}
return lista, nil
}
+func (r *sqliteKorisniciRepo) SacuvajLokalnuPozadinu(ctx context.Context, id int64, pozadina, opacity, blur, blurPozadine 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)
+ if err != nil {
+ return fmt.Errorf("ntech: korisnici.SacuvajLokalnuPozadinu: %w", err)
+ }
+ return nil
+}
+
+func (r *sqliteKorisniciRepo) SacuvajLokalnuTemu(ctx context.Context, id int64, lokalnaTema string, koristi bool) error {
+ koristiInt := 0
+ if koristi {
+ koristiInt = 1
+ }
+ var tema interface{}
+ if lokalnaTema == "" {
+ tema = nil
+ } else {
+ tema = lokalnaTema
+ }
+ _, err := r.db.ExecContext(ctx,
+ `UPDATE korisnici SET lokalna_tema = ?, koristi_lokalnu_temu = ? WHERE id = ?`,
+ tema, koristiInt, id)
+ if err != nil {
+ return fmt.Errorf("ntech: korisnici.SacuvajLokalnuTemu: %w", err)
+ }
+ return nil
+}
+
func (r *sqliteKorisniciRepo) AzurirajUlogu(ctx context.Context, id int64, uloga string) error {
_, err := r.db.ExecContext(ctx, `UPDATE korisnici SET uloga = ? WHERE id = ?`, uloga, id)
if err != nil {
diff --git a/internal/db/sqlite/podsetnici.go b/internal/db/sqlite/podsetnici.go
index a66e5fb..dd80c92 100644
--- a/internal/db/sqlite/podsetnici.go
+++ b/internal/db/sqlite/podsetnici.go
@@ -22,17 +22,24 @@ func NoviPodsetnikRepo(db *sql.DB) *PodsetnikRepo {
// Lista vraća listu podsetnika sa opcionim filterom
func (r *PodsetnikRepo) Lista(ctx context.Context, filter db.PodsetnikFilter) ([]model.Podsetnik, error) {
upit := `
- SELECT id, naslov, napomena, datum_podsecanja, zavrseno, tip, datum_unosa
+ SELECT id, naslov, napomena, datum_podsecanja, zavrseno, tip, datum_unosa, korisnik_id
FROM podsetnici
WHERE 1=1`
+ var args []any
+
if filter.SamoAktivni {
upit += " AND zavrseno = 0"
}
+ if filter.KorisnikID != nil {
+ upit += " AND korisnik_id = ?"
+ args = append(args, *filter.KorisnikID)
+ }
+
upit += " ORDER BY datum_podsecanja ASC"
- redovi, err := r.db.QueryContext(ctx, upit)
+ redovi, err := r.db.QueryContext(ctx, upit, args...)
if err != nil {
return nil, fmt.Errorf("ntech: PodsetnikRepo.Lista: %w", err)
}
@@ -42,15 +49,19 @@ func (r *PodsetnikRepo) Lista(ctx context.Context, filter db.PodsetnikFilter) ([
for redovi.Next() {
var p model.Podsetnik
var napomena sql.NullString
+ var korisnikID sql.NullInt64
var zavrseno int
err := redovi.Scan(
- &p.ID, &p.Naslov, &napomena, &p.DatumPodsecanja, &zavrseno, &p.Tip, &p.DatumUnosa,
+ &p.ID, &p.Naslov, &napomena, &p.DatumPodsecanja, &zavrseno, &p.Tip, &p.DatumUnosa, &korisnikID,
)
if err != nil {
return nil, fmt.Errorf("ntech: PodsetnikRepo.Lista: scan: %w", err)
}
p.Napomena = napomena.String
p.Zavrseno = zavrseno == 1
+ if korisnikID.Valid {
+ p.KorisnikID = &korisnikID.Int64
+ }
rezultat = append(rezultat, p)
}
@@ -61,12 +72,13 @@ func (r *PodsetnikRepo) Lista(ctx context.Context, filter db.PodsetnikFilter) ([
func (r *PodsetnikRepo) DohvatiID(ctx context.Context, id int64) (*model.Podsetnik, error) {
var p model.Podsetnik
var napomena sql.NullString
+ var korisnikID sql.NullInt64
var zavrseno int
err := r.db.QueryRowContext(ctx, `
- SELECT id, naslov, napomena, datum_podsecanja, zavrseno, tip, datum_unosa
+ SELECT id, naslov, napomena, datum_podsecanja, zavrseno, tip, datum_unosa, korisnik_id
FROM podsetnici WHERE id = ?`, id).Scan(
- &p.ID, &p.Naslov, &napomena, &p.DatumPodsecanja, &zavrseno, &p.Tip, &p.DatumUnosa,
+ &p.ID, &p.Naslov, &napomena, &p.DatumPodsecanja, &zavrseno, &p.Tip, &p.DatumUnosa, &korisnikID,
)
if err != nil {
return nil, fmt.Errorf("ntech: PodsetnikRepo.DohvatiID: %w", err)
@@ -74,16 +86,24 @@ func (r *PodsetnikRepo) DohvatiID(ctx context.Context, id int64) (*model.Podsetn
p.Napomena = napomena.String
p.Zavrseno = zavrseno == 1
+ if korisnikID.Valid {
+ p.KorisnikID = &korisnikID.Int64
+ }
return &p, nil
}
// Kreiraj dodaje novi podsetnik u bazu
func (r *PodsetnikRepo) Kreiraj(ctx context.Context, p *model.Podsetnik) (int64, error) {
+ var korisnikID interface{}
+ if p.KorisnikID != nil {
+ korisnikID = *p.KorisnikID
+ }
+
rezultat, err := r.db.ExecContext(ctx, `
- INSERT INTO podsetnici (naslov, napomena, datum_podsecanja, tip)
- VALUES (?, ?, ?, ?)`,
- p.Naslov, nullString(p.Napomena), p.DatumPodsecanja, p.Tip,
+ INSERT INTO podsetnici (naslov, napomena, datum_podsecanja, tip, korisnik_id)
+ VALUES (?, ?, ?, ?, ?)`,
+ p.Naslov, nullString(p.Napomena), p.DatumPodsecanja, p.Tip, korisnikID,
)
if err != nil {
return 0, fmt.Errorf("ntech: PodsetnikRepo.Kreiraj: %w", err)
@@ -99,11 +119,16 @@ func (r *PodsetnikRepo) Kreiraj(ctx context.Context, p *model.Podsetnik) (int64,
// Izmeni ažurira postojeći podsetnik
func (r *PodsetnikRepo) Izmeni(ctx context.Context, p *model.Podsetnik) error {
+ var korisnikID interface{}
+ if p.KorisnikID != nil {
+ korisnikID = *p.KorisnikID
+ }
+
_, err := r.db.ExecContext(ctx, `
UPDATE podsetnici SET
- naslov = ?, napomena = ?, datum_podsecanja = ?, tip = ?
+ naslov = ?, napomena = ?, datum_podsecanja = ?, tip = ?, korisnik_id = ?
WHERE id = ?`,
- p.Naslov, nullString(p.Napomena), p.DatumPodsecanja, p.Tip, p.ID,
+ p.Naslov, nullString(p.Napomena), p.DatumPodsecanja, p.Tip, korisnikID, p.ID,
)
if err != nil {
return fmt.Errorf("ntech: PodsetnikRepo.Izmeni: %w", err)
@@ -138,12 +163,18 @@ func (r *PodsetnikRepo) Obrisi(ctx context.Context, id int64) error {
return nil
}
-// BrojAktivnih vraća broj nezavršenih podsetnika
-func (r *PodsetnikRepo) BrojAktivnih(ctx context.Context) (int, error) {
+// BrojAktivnih vraća broj nezavršenih podsetnika, opcionalno filtrirano po korisniku
+func (r *PodsetnikRepo) BrojAktivnih(ctx context.Context, filter db.PodsetnikFilter) (int, error) {
+ upit := "SELECT COUNT(*) FROM podsetnici WHERE zavrseno = 0"
+ var args []any
+
+ if filter.KorisnikID != nil {
+ upit += " AND korisnik_id = ?"
+ args = append(args, *filter.KorisnikID)
+ }
+
var broj int
- err := r.db.QueryRowContext(ctx,
- "SELECT COUNT(*) FROM podsetnici WHERE zavrseno = 0",
- ).Scan(&broj)
+ err := r.db.QueryRowContext(ctx, upit, args...).Scan(&broj)
if err != nil {
return 0, fmt.Errorf("ntech: PodsetnikRepo.BrojAktivnih: %w", err)
}
diff --git a/internal/handler/admin.go b/internal/handler/admin.go
index dc69163..8fadd01 100644
--- a/internal/handler/admin.go
+++ b/internal/handler/admin.go
@@ -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())
diff --git a/internal/handler/dashboard.go b/internal/handler/dashboard.go
index d0dd464..9fec234 100644
--- a/internal/handler/dashboard.go
+++ b/internal/handler/dashboard.go
@@ -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
diff --git a/internal/handler/handler.go b/internal/handler/handler.go
index bf12c00..6b26d2c 100644
--- a/internal/handler/handler.go
+++ b/internal/handler/handler.go
@@ -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
diff --git a/internal/handler/kes.go b/internal/handler/kes.go
index 15cd121..3f36a22 100644
--- a/internal/handler/kes.go
+++ b/internal/handler/kes.go
@@ -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",
}
diff --git a/internal/handler/nabavka.go b/internal/handler/nabavka.go
index efa36e0..6b58e57 100644
--- a/internal/handler/nabavka.go
+++ b/internal/handler/nabavka.go
@@ -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)
diff --git a/internal/handler/podesavanja.go b/internal/handler/podesavanja.go
index 446ecde..53ea3a2 100644
--- a/internal/handler/podesavanja.go
+++ b/internal/handler/podesavanja.go
@@ -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)
+}
diff --git a/internal/handler/podsetnici.go b/internal/handler/podsetnici.go
index ee7a67a..defdb41 100644
--- a/internal/handler/podsetnici.go
+++ b/internal/handler/podsetnici.go
@@ -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
diff --git a/internal/handler/prodaja.go b/internal/handler/prodaja.go
index f4b2721..8c82075 100644
--- a/internal/handler/prodaja.go
+++ b/internal/handler/prodaja.go
@@ -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)
diff --git a/internal/handler/profil.go b/internal/handler/profil.go
new file mode 100644
index 0000000..201bb2b
--- /dev/null
+++ b/internal/handler/profil.go
@@ -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)
+}
diff --git a/internal/middleware/dozvole.go b/internal/middleware/dozvole.go
index a672e2d..6becf02 100644
--- a/internal/middleware/dozvole.go
+++ b/internal/middleware/dozvole.go
@@ -45,6 +45,10 @@ var sveAkcije = []string{
"backup.pokreni",
"podesavanja.login_pozadina",
"podesavanja.app_pozadina",
+ "tema.globalno",
+ "tema.lokalno",
+ "dashboard.prihod",
+ "prodaja.storno",
}
// SveAkcije vraća listu svih poznatih akcija — koristi se pri inicijalizaciji baze i resetu
@@ -81,7 +85,7 @@ func ImaDozvolu(uloga, akcija string) bool {
"servis.izmeni", "servis.obrisi":
return true
// prodaja
- case "prodaja.pregled", "prodaja.dodaj", "prodaja.obrisi":
+ case "prodaja.pregled", "prodaja.dodaj", "prodaja.obrisi", "prodaja.storno":
return true
// klijent
case "klijent.pregled", "klijent.dodaj",
@@ -105,27 +109,30 @@ func ImaDozvolu(uloga, akcija string) bool {
// pozadinske slike
case "podesavanja.login_pozadina", "podesavanja.app_pozadina":
return true
+ // teme
+ case "tema.globalno", "tema.lokalno":
+ return true
+ // dashboard — prihod samo admin+
+ case "dashboard.prihod":
+ return true
}
return false
case "radnik":
switch akcija {
- // artikal — bez brisanja i premeštanja
- case "artikal.pregled", "artikal.dodaj", "artikal.izmeni":
+ // artikal — samo pregled
+ case "artikal.pregled":
return true
// kategorija — samo pregled
case "kategorija.pregled":
return true
- // nabavka — bez brisanja
- case "nabavka.pregled", "nabavka.dodaj":
- return true
// dobavljač — bez brisanja
case "dobavljac.pregled", "dobavljac.dodaj", "dobavljac.izmeni":
return true
// servis — bez brisanja
case "servis.pregled", "servis.dodaj", "servis.izmeni":
return true
- // prodaja — bez brisanja
+ // prodaja — bez brisanja i storna
case "prodaja.pregled", "prodaja.dodaj":
return true
// klijent — bez brisanja
@@ -135,6 +142,9 @@ func ImaDozvolu(uloga, akcija string) bool {
case "podsetnik.pregled", "podsetnik.dodaj",
"podsetnik.izmeni", "podsetnik.obrisi":
return true
+ // lokalna tema
+ case "tema.lokalno":
+ return true
}
return false
}
diff --git a/internal/model/korisnik.go b/internal/model/korisnik.go
index 4580c6e..c171b45 100644
--- a/internal/model/korisnik.go
+++ b/internal/model/korisnik.go
@@ -9,8 +9,14 @@ type Korisnik struct {
LozinkaHash string
Uloga string // "superadmin" | "admin" | "radnik"
Aktivan bool
- TotpTajna string
- DatumKreiranja time.Time
+ TotpTajna string
+ LokalnaTema string // "tamna" | "svetla" | ""
+ KoristiLokalnuTemu bool
+ DatumKreiranja time.Time
+ LokalnaPozadina string
+ LokalnaPozadinaOpacity string
+ LokalnaPozadinaBlur string
+ LokalnaPozadinaBlurPozadine string
}
// Sesija predstavlja aktivnu sesiju prijavljenog korisnika
diff --git a/internal/model/podsetnik.go b/internal/model/podsetnik.go
index f983874..5270ff2 100644
--- a/internal/model/podsetnik.go
+++ b/internal/model/podsetnik.go
@@ -14,9 +14,18 @@ type Podsetnik struct {
Zavrseno bool
Tip string
DatumUnosa time.Time
+ KorisnikID *int64 // ako nil — podsetnik nije dodeljen konkretnom korisniku
}
// JePrekoracen vraća true ako datum podsećanja je prošao a podsetnik nije završen
func (p Podsetnik) JePrekoracen() bool {
return !p.Zavrseno && p.DatumPodsecanja.Before(time.Now())
}
+
+// KorisnikIDVal vraća vrednost KorisnikID pokazivača ili 0 ako je nil — za poređenje u šablonima
+func (p Podsetnik) KorisnikIDVal() int64 {
+ if p.KorisnikID == nil {
+ return 0
+ }
+ return *p.KorisnikID
+}
diff --git a/migrations/027_lokalna_tema.sql b/migrations/027_lokalna_tema.sql
new file mode 100644
index 0000000..0efbfbd
--- /dev/null
+++ b/migrations/027_lokalna_tema.sql
@@ -0,0 +1,8 @@
+-- globalna tema u podešavanjima (podrazumevano tamna)
+INSERT INTO podesavanja (kljuc, vrednost)
+VALUES ('globalna_tema', 'tamna')
+ON CONFLICT(kljuc) DO NOTHING;
+
+-- lokalna tema korisnika
+ALTER TABLE korisnici ADD COLUMN lokalna_tema TEXT;
+ALTER TABLE korisnici ADD COLUMN koristi_lokalnu_temu INTEGER NOT NULL DEFAULT 0;
diff --git a/migrations/028_tema_pre_slike.sql b/migrations/028_tema_pre_slike.sql
new file mode 100644
index 0000000..54f2ad5
--- /dev/null
+++ b/migrations/028_tema_pre_slike.sql
@@ -0,0 +1,3 @@
+INSERT INTO podesavanja (kljuc, vrednost)
+VALUES ('tema_pre_slike', '')
+ON CONFLICT(kljuc) DO NOTHING;
diff --git a/migrations/029_podsetnik_korisnik.sql b/migrations/029_podsetnik_korisnik.sql
new file mode 100644
index 0000000..3ca4548
--- /dev/null
+++ b/migrations/029_podsetnik_korisnik.sql
@@ -0,0 +1 @@
+ALTER TABLE podsetnici ADD COLUMN korisnik_id INTEGER REFERENCES korisnici(id) ON DELETE SET NULL;
diff --git a/migrations/030_korisnik_pozadina.sql b/migrations/030_korisnik_pozadina.sql
new file mode 100644
index 0000000..c64fa63
--- /dev/null
+++ b/migrations/030_korisnik_pozadina.sql
@@ -0,0 +1,3 @@
+ALTER TABLE korisnici ADD COLUMN lokalna_pozadina TEXT DEFAULT '';
+ALTER TABLE korisnici ADD COLUMN lokalna_pozadina_opacity TEXT DEFAULT '50';
+ALTER TABLE korisnici ADD COLUMN lokalna_pozadina_blur TEXT DEFAULT '12';
diff --git a/migrations/031_korisnik_pozadina_blur_pozadine.sql b/migrations/031_korisnik_pozadina_blur_pozadine.sql
new file mode 100644
index 0000000..c44532f
--- /dev/null
+++ b/migrations/031_korisnik_pozadina_blur_pozadine.sql
@@ -0,0 +1 @@
+ALTER TABLE korisnici ADD COLUMN lokalna_pozadina_blur_pozadine TEXT DEFAULT '0';
diff --git a/web/templates/komponente/sidebar.html b/web/templates/komponente/sidebar.html
index d20f0ba..e9a7470 100644
--- a/web/templates/komponente/sidebar.html
+++ b/web/templates/komponente/sidebar.html
@@ -39,11 +39,13 @@
+ {{if index .Dozvole "nabavka.pregled"}}
Nabavke
+ {{end}}
@@ -86,11 +88,41 @@
+ {{if index .Dozvole "tema.lokalno"}}
+
+ Ova slika se prikazuje na stranici za prijavu svim korisnicima. + Podešavanja su dostupna u + Podešavanjima → Izgled. +
+