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
+18 -2
View File
@@ -118,9 +118,17 @@ func main() {
r.Use(middleware.Compress(5)) r.Use(middleware.Compress(5))
// uploads su uvek na disku — korisnički fajlovi, ne ugrađuju se // 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 // 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) // javne rute (bez autentifikacije)
r.Get("/prijava", h.PrikazPrijave) r.Get("/prijava", h.PrikazPrijave)
@@ -154,6 +162,8 @@ func main() {
r.Get("/podesavanja/backup", h.BackupBaze) r.Get("/podesavanja/backup", h.BackupBaze)
r.Post("/podesavanja/backup/vrati", h.VratiBackup) r.Post("/podesavanja/backup/vrati", h.VratiBackup)
r.Get("/tema/{tema}", h.PromeniTemu) 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", h.Magacin)
r.Get("/magacin/novi", h.NoviArtikal) r.Get("/magacin/novi", h.NoviArtikal)
r.Post("/magacin/novi", h.SacuvajArtikal) r.Post("/magacin/novi", h.SacuvajArtikal)
@@ -192,6 +202,7 @@ func main() {
r.Get("/prodaja/nova", h.NovaProdaja) r.Get("/prodaja/nova", h.NovaProdaja)
r.Post("/prodaja/nova", h.SacuvajProdaju) r.Post("/prodaja/nova", h.SacuvajProdaju)
r.Post("/prodaja/obrisi/{id}", h.ObrisiProdaju) 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}/stampa", h.StampaProdaje)
r.Get("/prodaja/{id}", h.DetaljiProdaje) r.Get("/prodaja/{id}", h.DetaljiProdaje)
@@ -229,6 +240,11 @@ func main() {
r.Get("/admin/profil/totp/pokreni", h.AdminTotpPokreni) r.Get("/admin/profil/totp/pokreni", h.AdminTotpPokreni)
r.Post("/admin/profil/totp/aktiviraj", h.AdminTotpAktivacija) r.Post("/admin/profil/totp/aktiviraj", h.AdminTotpAktivacija)
r.Post("/admin/profil/totp/deaktiviraj", h.AdminTotpDeaktivacija) 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) log.Printf("NTech pokrenut na portu %s", port)
+4 -1
View File
@@ -86,6 +86,8 @@ type KorisniciRepository interface {
AzurirajAktivan(ctx context.Context, id int64, aktivan bool) error AzurirajAktivan(ctx context.Context, id int64, aktivan bool) error
PromeniLozinku(ctx context.Context, id int64, hash string) error PromeniLozinku(ctx context.Context, id int64, hash string) error
SacuvajTotpTajnu(ctx context.Context, id int64, tajna 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) PostojiIjedan(ctx context.Context) (bool, error)
Obrisi(ctx context.Context, id int64) error Obrisi(ctx context.Context, id int64) error
} }
@@ -102,6 +104,7 @@ type SesijeRepository interface {
// PodsetnikFilter definiše parametre za filtriranje liste podsetnika // PodsetnikFilter definiše parametre za filtriranje liste podsetnika
type PodsetnikFilter struct { 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 // PokusajiPrijaveRepository definiše operacije nad evidencijom pokušaja prijave
@@ -134,5 +137,5 @@ type PodsetnikRepository interface {
Izmeni(ctx context.Context, p *model.Podsetnik) error Izmeni(ctx context.Context, p *model.Podsetnik) error
OznaciZavrsenim(ctx context.Context, id int64, zavrseno bool) error OznaciZavrsenim(ctx context.Context, id int64, zavrseno bool) error
Obrisi(ctx context.Context, id int64) error Obrisi(ctx context.Context, id int64) error
BrojAktivnih(ctx context.Context) (int, error) BrojAktivnih(ctx context.Context, filter PodsetnikFilter) (int, error)
} }
+78 -10
View File
@@ -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) { func (r *sqliteKorisniciRepo) DohvatiPoImenu(ctx context.Context, korisnickoIme string) (*model.Korisnik, error) {
k := &model.Korisnik{} k := &model.Korisnik{}
var aktivan int var aktivan, koristiLokalnuTemu int
var totpTajna sql.NullString var totpTajna, lokalnaTema sql.NullString
var lokalnaPozadina, lokalnaPozadinaOpacity, lokalnaPozadinaBlur, lokalnaPozadinaBlurPozadine sql.NullString
var datumKreiranja time.Time var datumKreiranja time.Time
err := r.db.QueryRowContext(ctx, 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). 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 { if err != nil {
return nil, fmt.Errorf("ntech: korisnici.DohvatiPoImenu: %w", err) return nil, fmt.Errorf("ntech: korisnici.DohvatiPoImenu: %w", err)
} }
k.Aktivan = aktivan == 1 k.Aktivan = aktivan == 1
k.TotpTajna = totpTajna.String k.TotpTajna = totpTajna.String
k.LokalnaTema = lokalnaTema.String
k.KoristiLokalnuTemu = koristiLokalnuTemu == 1
k.DatumKreiranja = datumKreiranja k.DatumKreiranja = datumKreiranja
k.LokalnaPozadina = lokalnaPozadina.String
k.LokalnaPozadinaOpacity = lokalnaPozadinaOpacity.String
k.LokalnaPozadinaBlur = lokalnaPozadinaBlur.String
k.LokalnaPozadinaBlurPozadine = lokalnaPozadinaBlurPozadine.String
return k, nil return k, nil
} }
func (r *sqliteKorisniciRepo) DohvatiPoID(ctx context.Context, id int64) (*model.Korisnik, error) { func (r *sqliteKorisniciRepo) DohvatiPoID(ctx context.Context, id int64) (*model.Korisnik, error) {
k := &model.Korisnik{} 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 var datumKreiranja time.Time
err := r.db.QueryRowContext(ctx, 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). 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 { if err != nil {
return nil, fmt.Errorf("ntech: korisnici.DohvatiPoID: %w", err) return nil, fmt.Errorf("ntech: korisnici.DohvatiPoID: %w", err)
} }
k.Aktivan = aktivan == 1 k.Aktivan = aktivan == 1
k.LokalnaTema = lokalnaTema.String
k.KoristiLokalnuTemu = koristiLokalnuTemu == 1
k.DatumKreiranja = datumKreiranja k.DatumKreiranja = datumKreiranja
k.LokalnaPozadina = lokalnaPozadina.String
k.LokalnaPozadinaOpacity = lokalnaPozadinaOpacity.String
k.LokalnaPozadinaBlur = lokalnaPozadinaBlur.String
k.LokalnaPozadinaBlurPozadine = lokalnaPozadinaBlurPozadine.String
return k, nil return k, nil
} }
func (r *sqliteKorisniciRepo) Lista(ctx context.Context) ([]model.Korisnik, error) { func (r *sqliteKorisniciRepo) Lista(ctx context.Context) ([]model.Korisnik, error) {
rows, err := r.db.QueryContext(ctx, 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`) FROM korisnici ORDER BY datum_kreiranja ASC`)
if err != nil { if err != nil {
return nil, fmt.Errorf("ntech: korisnici.Lista: %w", err) 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 var lista []model.Korisnik
for rows.Next() { for rows.Next() {
var k model.Korisnik 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 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) return nil, fmt.Errorf("ntech: korisnici.Lista: %w", err)
} }
k.Aktivan = aktivan == 1 k.Aktivan = aktivan == 1
k.LokalnaTema = lokalnaTema.String
k.KoristiLokalnuTemu = koristiLokalnuTemu == 1
k.DatumKreiranja = datumKreiranja k.DatumKreiranja = datumKreiranja
k.LokalnaPozadina = lokalnaPozadina.String
k.LokalnaPozadinaOpacity = lokalnaPozadinaOpacity.String
k.LokalnaPozadinaBlur = lokalnaPozadinaBlur.String
k.LokalnaPozadinaBlurPozadine = lokalnaPozadinaBlurPozadine.String
lista = append(lista, k) lista = append(lista, k)
} }
return lista, nil 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 { 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) _, err := r.db.ExecContext(ctx, `UPDATE korisnici SET uloga = ? WHERE id = ?`, uloga, id)
if err != nil { if err != nil {
+46 -15
View File
@@ -22,17 +22,24 @@ func NoviPodsetnikRepo(db *sql.DB) *PodsetnikRepo {
// Lista vraća listu podsetnika sa opcionim filterom // Lista vraća listu podsetnika sa opcionim filterom
func (r *PodsetnikRepo) Lista(ctx context.Context, filter db.PodsetnikFilter) ([]model.Podsetnik, error) { func (r *PodsetnikRepo) Lista(ctx context.Context, filter db.PodsetnikFilter) ([]model.Podsetnik, error) {
upit := ` 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 FROM podsetnici
WHERE 1=1` WHERE 1=1`
var args []any
if filter.SamoAktivni { if filter.SamoAktivni {
upit += " AND zavrseno = 0" upit += " AND zavrseno = 0"
} }
if filter.KorisnikID != nil {
upit += " AND korisnik_id = ?"
args = append(args, *filter.KorisnikID)
}
upit += " ORDER BY datum_podsecanja ASC" upit += " ORDER BY datum_podsecanja ASC"
redovi, err := r.db.QueryContext(ctx, upit) redovi, err := r.db.QueryContext(ctx, upit, args...)
if err != nil { if err != nil {
return nil, fmt.Errorf("ntech: PodsetnikRepo.Lista: %w", err) 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() { for redovi.Next() {
var p model.Podsetnik var p model.Podsetnik
var napomena sql.NullString var napomena sql.NullString
var korisnikID sql.NullInt64
var zavrseno int var zavrseno int
err := redovi.Scan( 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 { if err != nil {
return nil, fmt.Errorf("ntech: PodsetnikRepo.Lista: scan: %w", err) return nil, fmt.Errorf("ntech: PodsetnikRepo.Lista: scan: %w", err)
} }
p.Napomena = napomena.String p.Napomena = napomena.String
p.Zavrseno = zavrseno == 1 p.Zavrseno = zavrseno == 1
if korisnikID.Valid {
p.KorisnikID = &korisnikID.Int64
}
rezultat = append(rezultat, p) 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) { func (r *PodsetnikRepo) DohvatiID(ctx context.Context, id int64) (*model.Podsetnik, error) {
var p model.Podsetnik var p model.Podsetnik
var napomena sql.NullString var napomena sql.NullString
var korisnikID sql.NullInt64
var zavrseno int var zavrseno int
err := r.db.QueryRowContext(ctx, ` 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( 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 { if err != nil {
return nil, fmt.Errorf("ntech: PodsetnikRepo.DohvatiID: %w", err) 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.Napomena = napomena.String
p.Zavrseno = zavrseno == 1 p.Zavrseno = zavrseno == 1
if korisnikID.Valid {
p.KorisnikID = &korisnikID.Int64
}
return &p, nil return &p, nil
} }
// Kreiraj dodaje novi podsetnik u bazu // Kreiraj dodaje novi podsetnik u bazu
func (r *PodsetnikRepo) Kreiraj(ctx context.Context, p *model.Podsetnik) (int64, error) { 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, ` rezultat, err := r.db.ExecContext(ctx, `
INSERT INTO podsetnici (naslov, napomena, datum_podsecanja, tip) INSERT INTO podsetnici (naslov, napomena, datum_podsecanja, tip, korisnik_id)
VALUES (?, ?, ?, ?)`, VALUES (?, ?, ?, ?, ?)`,
p.Naslov, nullString(p.Napomena), p.DatumPodsecanja, p.Tip, p.Naslov, nullString(p.Napomena), p.DatumPodsecanja, p.Tip, korisnikID,
) )
if err != nil { if err != nil {
return 0, fmt.Errorf("ntech: PodsetnikRepo.Kreiraj: %w", err) 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 // Izmeni ažurira postojeći podsetnik
func (r *PodsetnikRepo) Izmeni(ctx context.Context, p *model.Podsetnik) error { 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, ` _, err := r.db.ExecContext(ctx, `
UPDATE podsetnici SET UPDATE podsetnici SET
naslov = ?, napomena = ?, datum_podsecanja = ?, tip = ? naslov = ?, napomena = ?, datum_podsecanja = ?, tip = ?, korisnik_id = ?
WHERE 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 { if err != nil {
return fmt.Errorf("ntech: PodsetnikRepo.Izmeni: %w", err) return fmt.Errorf("ntech: PodsetnikRepo.Izmeni: %w", err)
@@ -138,12 +163,18 @@ func (r *PodsetnikRepo) Obrisi(ctx context.Context, id int64) error {
return nil return nil
} }
// BrojAktivnih vraća broj nezavršenih podsetnika // BrojAktivnih vraća broj nezavršenih podsetnika, opcionalno filtrirano po korisniku
func (r *PodsetnikRepo) BrojAktivnih(ctx context.Context) (int, error) { 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 var broj int
err := r.db.QueryRowContext(ctx, err := r.db.QueryRowContext(ctx, upit, args...).Scan(&broj)
"SELECT COUNT(*) FROM podsetnici WHERE zavrseno = 0",
).Scan(&broj)
if err != nil { if err != nil {
return 0, fmt.Errorf("ntech: PodsetnikRepo.BrojAktivnih: %w", err) return 0, fmt.Errorf("ntech: PodsetnikRepo.BrojAktivnih: %w", err)
} }
+57
View File
@@ -31,6 +31,21 @@ type podaciAdminProfil struct {
TotpTajna string TotpTajna string
TotpQR template.URL 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 // AdminKorisnici prikazuje listu korisnika
@@ -287,9 +302,51 @@ func (h *Handler) AdminProfil(w http.ResponseWriter, r *http.Request) {
h.renderujTemplate(w, "admin_profil", podaciAdminProfil{ h.renderujTemplate(w, "admin_profil", podaciAdminProfil{
PodaciStranice: ps, PodaciStranice: ps,
TotpAktivan: svezi.TotpTajna != "", 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 // AdminPromeniLozinku menja lozinku prijavljenog korisnika
func (h *Handler) AdminPromeniLozinku(w http.ResponseWriter, r *http.Request) { func (h *Handler) AdminPromeniLozinku(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context()) k := middleware.KorisnikIzKonteksta(r.Context())
+11 -1
View File
@@ -5,7 +5,9 @@ import (
"net/http" "net/http"
"time" "time"
appdb "ntech/internal/db"
"ntech/internal/db/sqlite" "ntech/internal/db/sqlite"
"ntech/internal/middleware"
"ntech/internal/model" "ntech/internal/model"
) )
@@ -47,12 +49,16 @@ func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) {
log.Printf("dashboard: aktivni servisi: %v", err) log.Printf("dashboard: aktivni servisi: %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, ` if err := h.DB.QueryRowContext(ctx, `
SELECT COALESCE(SUM(ukupno), 0) FROM prodajni_nalozi SELECT COALESCE(SUM(ukupno), 0) FROM prodajni_nalozi
WHERE substr(datum, 1, 7) = strftime('%Y-%m', 'now', 'localtime')`, WHERE substr(datum, 1, 7) = strftime('%Y-%m', 'now', 'localtime')`,
).Scan(&prihodOvogMeseca); err != nil { ).Scan(&prihodOvogMeseca); err != nil {
log.Printf("dashboard: prihod ovog meseca: %v", err) log.Printf("dashboard: prihod ovog meseca: %v", err)
} }
}
if err := h.DB.QueryRowContext(ctx, if err := h.DB.QueryRowContext(ctx,
"SELECT COUNT(*) FROM artikli WHERE kolicina <= kolicina_min", "SELECT COUNT(*) FROM artikli WHERE kolicina <= kolicina_min",
@@ -60,7 +66,11 @@ func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) {
log.Printf("dashboard: kriticna zaliha: %v", err) 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) log.Printf("dashboard: aktivni podsetnici: %v", err)
} else { } else {
aktivniPodsetnici = n aktivniPodsetnici = n
+57 -1
View File
@@ -74,24 +74,65 @@ func (h *Handler) reinicijalzijRepozitorijume(novaDB *sql.DB) {
// popuniPodaciStranice popunjava zajednička polja stranice uključujući prijavljenog korisnika // popuniPodaciStranice popunjava zajednička polja stranice uključujući prijavljenog korisnika
func (h *Handler) popuniPodaciStranice(r *http.Request, podesavanja map[string]string) model.PodaciStranice { 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{ ps := model.PodaciStranice{
Tema: podesavanja["tema"], Tema: tema,
NazivFirme: podesavanja["naziv_firme"], NazivFirme: podesavanja["naziv_firme"],
Podnazlov: podesavanja["podnazlov"], Podnazlov: podesavanja["podnazlov"],
LogoTip: podesavanja["logo_tip"], LogoTip: podesavanja["logo_tip"],
LogoPutanja: podesavanja["logo_putanja"], LogoPutanja: podesavanja["logo_putanja"],
Korisnik: "Admin", Korisnik: "Admin",
} }
var korisnik *model.Korisnik
if k := middleware.KorisnikIzKonteksta(r.Context()); k != nil { if k := middleware.KorisnikIzKonteksta(r.Context()); k != nil {
korisnik = k
ps.Korisnik = k.KorisnickoIme ps.Korisnik = k.KorisnickoIme
ps.KorisnikIme = k.KorisnickoIme ps.KorisnikIme = k.KorisnickoIme
ps.KorisnikUloga = k.Uloga ps.KorisnikUloga = k.Uloga
ps.Dozvole = h.DozvoleRepo.SveDozvole(r.Context(), 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.CsrfToken = middleware.CsrfToken(r.Context())
ps.Flash = middleware.GetFlash(r, h.DB) ps.Flash = middleware.GetFlash(r, h.DB)
// 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"] 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"] ps.AppPozadinaOpacity = podesavanja["app_pozadina_opacity"]
if ps.AppPozadinaOpacity == "" { if ps.AppPozadinaOpacity == "" {
ps.AppPozadinaOpacity = "50" ps.AppPozadinaOpacity = "50"
@@ -104,6 +145,21 @@ func (h *Handler) popuniPodaciStranice(r *http.Request, podesavanja map[string]s
if ps.AppPozadinaBlurPozadine == "" { if ps.AppPozadinaBlurPozadine == "" {
ps.AppPozadinaBlurPozadine = "0" 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 return ps
} }
+1
View File
@@ -26,6 +26,7 @@ var saSidebar = []string{
"nabavke", "nabavka_forma", "nabavka_detalji", "nabavke", "nabavka_forma", "nabavka_detalji",
"podesavanja", "podesavanja_opste", "podesavanja_izgled", "podesavanja_sistem", "podesavanja", "podesavanja_opste", "podesavanja_izgled", "podesavanja_sistem",
"podsetnici", "podsetnik_forma", "podsetnici", "podsetnik_forma",
"profil_tema",
"prodaja", "prodaja_detalji", "prodaja_forma", "prodaja", "prodaja_detalji", "prodaja_forma",
"servis", "servis_forma", "servis_detalji", "servis", "servis_forma", "servis_detalji",
} }
+18
View File
@@ -57,6 +57,12 @@ func artikalUJSON(artikli []model.ArtikalSaKategorijom) template.JS {
// Nabavke renderuje listu svih nabavki // Nabavke renderuje listu svih nabavki
func (h *Handler) Nabavke(w http.ResponseWriter, r *http.Request) { 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) podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
if err != nil { if err != nil {
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError) 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 // NovaNabavka prikazuje formu za unos nove nabavke
func (h *Handler) NovaNabavka(w http.ResponseWriter, r *http.Request) { 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) podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
if err != nil { if err != nil {
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError) 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 // DetaljiNabavke prikazuje pregled jedne nabavke sa svim stavkama
func (h *Handler) DetaljiNabavke(w http.ResponseWriter, r *http.Request) { 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")) id, err := parseID(chi.URLParam(r, "id"))
if err != nil { if err != nil {
http.Error(w, "Neispravan ID nabavke", http.StatusBadRequest) http.Error(w, "Neispravan ID nabavke", http.StatusBadRequest)
+142 -51
View File
@@ -32,7 +32,7 @@ type PodaciPodesavanja struct {
PIB string PIB string
LogoTip string LogoTip string
LogoPutanja string LogoPutanja string
Tema string GlobalnaTema string // stvarna vrednost iz podešavanja (ne senči PodaciStranice.Tema)
Sacuvano bool Sacuvano bool
Verzija string Verzija string
LogoGreska string LogoGreska string
@@ -83,7 +83,15 @@ func (h *Handler) Podesavanja(w http.ResponseWriter, r *http.Request) {
PIB: podesavanja["pib"], PIB: podesavanja["pib"],
LogoTip: podesavanja["logo_tip"], LogoTip: podesavanja["logo_tip"],
LogoPutanja: podesavanja["logo_putanja"], 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", Sacuvano: r.URL.Query().Get("sacuvano") == "1",
BackupVracen: r.URL.Query().Get("sacuvano") == "vraceno", BackupVracen: r.URL.Query().Get("sacuvano") == "vraceno",
Verzija: h.Verzija, Verzija: h.Verzija,
@@ -259,6 +267,7 @@ func (h *Handler) SacuvajPodesavanja(w http.ResponseWriter, r *http.Request) {
"pib": r.FormValue("pib"), "pib": r.FormValue("pib"),
"logo_tip": r.FormValue("logo_tip"), "logo_tip": r.FormValue("logo_tip"),
"tema": r.FormValue("tema"), "tema": r.FormValue("tema"),
"globalna_tema": r.FormValue("globalna_tema"),
} }
for kljuc, vrednost := range polja { 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) r.Body = http.MaxBytesReader(w, r.Body, 5<<20+4096)
if err := r.ParseMultipartForm(5 << 20); err != nil { if err := r.ParseMultipartForm(5 << 20); err != nil {
middleware.SetFlash(w, r, h.DB, "greska", "Fajl je prevelik (maksimum 5 MB).") 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 return
} }
fajl, zaglavlje, err := r.FormFile("login_pozadina") fajl, zaglavlje, err := r.FormFile("login_pozadina")
if err != nil { if err != nil {
middleware.SetFlash(w, r, h.DB, "greska", "Nije odabran fajl.") 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 return
} }
defer fajl.Close() defer fajl.Close()
if zaglavlje.Size > 5<<20 { if zaglavlje.Size > 5<<20 {
middleware.SetFlash(w, r, h.DB, "greska", "Fajl je prevelik (maksimum 5 MB).") 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 return
} }
@@ -437,7 +446,7 @@ func (h *Handler) OtpremiLoginPozadinu(w http.ResponseWriter, r *http.Request) {
ocekivaniMime, ok := dozvoljenoExt[ext] ocekivaniMime, ok := dozvoljenoExt[ext]
if !ok { if !ok {
middleware.SetFlash(w, r, h.DB, "greska", "Dozvoljeni formati su JPG, PNG i WebP.") 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 return
} }
@@ -447,12 +456,12 @@ func (h *Handler) OtpremiLoginPozadinu(w http.ResponseWriter, r *http.Request) {
stvarniMime := http.DetectContentType(buf[:n]) stvarniMime := http.DetectContentType(buf[:n])
if !strings.HasPrefix(stvarniMime, ocekivaniMime) { if !strings.HasPrefix(stvarniMime, ocekivaniMime) {
middleware.SetFlash(w, r, h.DB, "greska", "Sadržaj fajla ne odgovara odabranoj ekstenziji.") 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 return
} }
if _, err := fajl.Seek(0, io.SeekStart); err != nil { if _, err := fajl.Seek(0, io.SeekStart); err != nil {
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri obradi fajla.") 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 return
} }
@@ -469,7 +478,7 @@ func (h *Handler) OtpremiLoginPozadinu(w http.ResponseWriter, r *http.Request) {
if err != nil { if err != nil {
log.Printf("upload login pozadine: greška pri generisanju imena: %v", err) log.Printf("upload login pozadine: greška pri generisanju imena: %v", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju fajla.") 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 return
} }
@@ -478,7 +487,7 @@ func (h *Handler) OtpremiLoginPozadinu(w http.ResponseWriter, r *http.Request) {
if err != nil { if err != nil {
log.Printf("upload login pozadine: ne mogu kreirati fajl: %v", err) log.Printf("upload login pozadine: ne mogu kreirati fajl: %v", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju fajla.") 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 return
} }
defer dst.Close() 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 { if _, err := io.Copy(dst, fajl); err != nil {
log.Printf("upload login pozadine: greška pri kopiranju: %v", err) log.Printf("upload login pozadine: greška pri kopiranju: %v", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju fajla.") 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 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 { 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) 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.") 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 return
} }
middleware.SetFlash(w, r, h.DB, "uspeh", "Pozadinska slika je uspešno otpremljena.") 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 // 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 { if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "login_pozadina", ""); err != nil {
log.Printf("ukloni login pozadinu: greška pri čuvanju: %v", err) log.Printf("ukloni login pozadinu: greška pri čuvanju: %v", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri uklanjanju slike.") 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 return
} }
middleware.SetFlash(w, r, h.DB, "uspeh", "Pozadinska slika je uklonjena.") 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 // 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) r.Body = http.MaxBytesReader(w, r.Body, 5<<20+4096)
if err := r.ParseMultipartForm(5 << 20); err != nil { if err := r.ParseMultipartForm(5 << 20); err != nil {
middleware.SetFlash(w, r, h.DB, "greska", "Fajl je prevelik (maksimum 5 MB).") 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 return
} }
fajl, zaglavlje, err := r.FormFile("app_pozadina") fajl, zaglavlje, err := r.FormFile("app_pozadina")
if err != nil { if err != nil {
middleware.SetFlash(w, r, h.DB, "greska", "Nije odabran fajl.") 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 return
} }
defer fajl.Close() defer fajl.Close()
if zaglavlje.Size > 5<<20 { if zaglavlje.Size > 5<<20 {
middleware.SetFlash(w, r, h.DB, "greska", "Fajl je prevelik (maksimum 5 MB).") 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 return
} }
@@ -569,7 +578,7 @@ func (h *Handler) OtpremiAppPozadinu(w http.ResponseWriter, r *http.Request) {
ocekivaniMime, ok := dozvoljenoExt[ext] ocekivaniMime, ok := dozvoljenoExt[ext]
if !ok { if !ok {
middleware.SetFlash(w, r, h.DB, "greska", "Dozvoljeni formati su JPG, PNG i WebP.") 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 return
} }
@@ -578,12 +587,12 @@ func (h *Handler) OtpremiAppPozadinu(w http.ResponseWriter, r *http.Request) {
stvarniMime := http.DetectContentType(buf[:n]) stvarniMime := http.DetectContentType(buf[:n])
if !strings.HasPrefix(stvarniMime, ocekivaniMime) { if !strings.HasPrefix(stvarniMime, ocekivaniMime) {
middleware.SetFlash(w, r, h.DB, "greska", "Sadržaj fajla ne odgovara odabranoj ekstenziji.") 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 return
} }
if _, err := fajl.Seek(0, io.SeekStart); err != nil { if _, err := fajl.Seek(0, io.SeekStart); err != nil {
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri obradi fajla.") 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 return
} }
@@ -599,7 +608,7 @@ func (h *Handler) OtpremiAppPozadinu(w http.ResponseWriter, r *http.Request) {
if err != nil { if err != nil {
log.Printf("upload app pozadine: greška pri generisanju imena: %v", err) log.Printf("upload app pozadine: greška pri generisanju imena: %v", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju fajla.") 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 return
} }
@@ -608,7 +617,7 @@ func (h *Handler) OtpremiAppPozadinu(w http.ResponseWriter, r *http.Request) {
if err != nil { if err != nil {
log.Printf("upload app pozadine: ne mogu kreirati fajl: %v", err) log.Printf("upload app pozadine: ne mogu kreirati fajl: %v", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju fajla.") 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 return
} }
defer dst.Close() 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 { if _, err := io.Copy(dst, fajl); err != nil {
log.Printf("upload app pozadine: greška pri kopiranju: %v", err) log.Printf("upload app pozadine: greška pri kopiranju: %v", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju fajla.") 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 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 { 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) 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.") 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 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.") 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 // 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 { if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "app_pozadina", ""); err != nil {
log.Printf("ukloni app pozadinu: greška pri čuvanju: %v", err) log.Printf("ukloni app pozadinu: greška pri čuvanju: %v", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri uklanjanju slike.") 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 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.") 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 // 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 { if err := r.ParseForm(); err != nil {
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čitanju forme.") 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 return
} }
@@ -680,19 +710,19 @@ func (h *Handler) SacuvajAppPozadinaStilove(w http.ResponseWriter, r *http.Reque
blurVal, err := strconv.Atoi(blurStr) blurVal, err := strconv.Atoi(blurStr)
if err != nil || blurVal < 0 || blurVal > 20 { if err != nil || blurVal < 0 || blurVal > 20 {
middleware.SetFlash(w, r, h.DB, "greska", "Neispravna vrednost zamućenja stakla.") 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 return
} }
blurPozadineVal, err := strconv.Atoi(blurPozadineStr) blurPozadineVal, err := strconv.Atoi(blurPozadineStr)
if err != nil || blurPozadineVal < 0 || blurPozadineVal > 20 { if err != nil || blurPozadineVal < 0 || blurPozadineVal > 20 {
middleware.SetFlash(w, r, h.DB, "greska", "Neispravna vrednost zamućenja pozadine.") 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 return
} }
opacityVal, err := strconv.Atoi(opacityStr) opacityVal, err := strconv.Atoi(opacityStr)
if err != nil || opacityVal < 0 || opacityVal > 80 { if err != nil || opacityVal < 0 || opacityVal > 80 {
middleware.SetFlash(w, r, h.DB, "greska", "Neispravna vrednost prozirnosti.") 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 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 { 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) 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.") 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 return
} }
} }
middleware.SetFlash(w, r, h.DB, "uspeh", "Izgled pozadine aplikacije je sačuvan.") 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 // 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 { if err := r.ParseForm(); err != nil {
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čitanju forme.") 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 return
} }
@@ -733,19 +763,19 @@ func (h *Handler) SacuvajLoginPozadinaStilove(w http.ResponseWriter, r *http.Req
blurPozadineVal, err := strconv.Atoi(blurPozadineStr) blurPozadineVal, err := strconv.Atoi(blurPozadineStr)
if err != nil || blurPozadineVal < 0 || blurPozadineVal > 20 { if err != nil || blurPozadineVal < 0 || blurPozadineVal > 20 {
middleware.SetFlash(w, r, h.DB, "greska", "Neispravna vrednost zamućenja pozadine.") 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 return
} }
blurKarticeVal, err := strconv.Atoi(blurKarticeStr) blurKarticeVal, err := strconv.Atoi(blurKarticeStr)
if err != nil || blurKarticeVal < 0 || blurKarticeVal > 20 { if err != nil || blurKarticeVal < 0 || blurKarticeVal > 20 {
middleware.SetFlash(w, r, h.DB, "greska", "Neispravna vrednost zamućenja kartice.") 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 return
} }
opacityVal, err := strconv.Atoi(opacityStr) opacityVal, err := strconv.Atoi(opacityStr)
if err != nil || opacityVal < 0 || opacityVal > 80 { if err != nil || opacityVal < 0 || opacityVal > 80 {
middleware.SetFlash(w, r, h.DB, "greska", "Neispravna vrednost prozirnosti.") 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 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 { 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) 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.") 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 return
} }
} }
middleware.SetFlash(w, r, h.DB, "uspeh", "Izgled pozadine prijave je sačuvan.") 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 // 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"], PIB: podesavanja["pib"],
LogoTip: podesavanja["logo_tip"], LogoTip: podesavanja["logo_tip"],
LogoPutanja: podesavanja["logo_putanja"], 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", Sacuvano: r.URL.Query().Get("sacuvano") == "1",
BackupVracen: r.URL.Query().Get("sacuvano") == "vraceno", BackupVracen: r.URL.Query().Get("sacuvano") == "vraceno",
Verzija: h.Verzija, Verzija: h.Verzija,
@@ -876,28 +914,81 @@ func (h *Handler) PodesavanjaSistem(w http.ResponseWriter, r *http.Request) {
h.renderujTemplate(w, "podesavanja_sistem", podaci) 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) { func (h *Handler) PromeniTemu(w http.ResponseWriter, r *http.Request) {
tema := chi.URLParam(r, "tema") 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] { if !validne[tema] {
http.Redirect(w, r, "/dashboard", http.StatusSeeOther) http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
return return
} }
if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "tema", tema); err != nil { _ = ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "globalna_tema", tema)
http.Error(w, "Greška pri promeni teme", http.StatusInternalServerError)
return
}
// vraćamo se na stranicu sa koje je kliknut dugmić
referer := r.Header.Get("Referer") referer := r.Header.Get("Referer")
if referer == "" { if referer == "" {
referer = "/dashboard" referer = "/dashboard"
} }
http.Redirect(w, r, referer, http.StatusSeeOther) 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 ( import (
"log" "log"
"net/http" "net/http"
"strconv"
"strings" "strings"
"time" "time"
"ntech/internal/db" "ntech/internal/db"
"ntech/internal/db/sqlite" "ntech/internal/db/sqlite"
"ntech/internal/middleware"
"ntech/internal/model" "ntech/internal/model"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
) )
// podaciPodsetnici su podaci za stranicu sa listom podsetnika // podaciPodsetnici su podaci za stranicu sa listom podsetnika
type podaciPodsetnici struct { type podaciPodsetnici struct {
model.PodaciStranice model.PodaciStranice
@@ -28,10 +31,13 @@ type podaciPodsetnikForma struct {
Podsetnik model.Podsetnik Podsetnik model.Podsetnik
Greska string Greska string
Izmena bool Izmena bool
Korisnici []model.Korisnik
} }
// Podsetnici renderuje listu podsetnika // Podsetnici renderuje listu podsetnika
func (h *Handler) Podsetnici(w http.ResponseWriter, r *http.Request) { func (h *Handler) Podsetnici(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB) podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
if err != nil { if err != nil {
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError) 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" 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 { if err != nil {
http.Error(w, "Greška pri učitavanju podsetnika", http.StatusInternalServerError) http.Error(w, "Greška pri učitavanju podsetnika", http.StatusInternalServerError)
return return
@@ -63,6 +75,8 @@ func (h *Handler) Podsetnici(w http.ResponseWriter, r *http.Request) {
// NoviPodsetnik prikazuje praznu formu za unos novog podsetnika // NoviPodsetnik prikazuje praznu formu za unos novog podsetnika
func (h *Handler) NoviPodsetnik(w http.ResponseWriter, r *http.Request) { func (h *Handler) NoviPodsetnik(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB) podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
if err != nil { if err != nil {
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError) 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.Stranica = "podsetnici"
ps.NaslovStranice = "Novi podsetnik" 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{ h.renderujFormuPodsetnika(w, podaciPodsetnikForma{
PodaciStranice: ps, PodaciStranice: ps,
Izmena: false, Izmena: false,
Korisnici: korisnici,
}) })
} }
// SacuvajPodsetnik prima POST formu i upisuje novi podsetnik u bazu // SacuvajPodsetnik prima POST formu i upisuje novi podsetnik u bazu
func (h *Handler) SacuvajPodsetnik(w http.ResponseWriter, r *http.Request) { func (h *Handler) SacuvajPodsetnik(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if err := r.ParseForm(); err != nil { if err := r.ParseForm(); err != nil {
http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest) http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest)
return return
} }
podsetnik, greska := parseFormuPodsetnika(r) podsetnik, greska := parseFormuPodsetnika(r, k)
if greska != "" {
prikaziGresku := func(poruka string) {
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB) podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
ps := h.popuniPodaciStranice(r, podesavanja) ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "podsetnici" ps.Stranica = "podsetnici"
ps.NaslovStranice = "Novi podsetnik" 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{ h.renderujFormuPodsetnika(w, podaciPodsetnikForma{
PodaciStranice: ps, PodaciStranice: ps,
Podsetnik: podsetnik, Podsetnik: podsetnik,
Greska: greska, Greska: poruka,
Izmena: false, Izmena: false,
Korisnici: korisnici,
}) })
}
if greska != "" {
prikaziGresku(greska)
return return
} }
if _, err := h.PodsetniciFRepo.Kreiraj(r.Context(), &podsetnik); err != nil { if _, err := h.PodsetniciFRepo.Kreiraj(r.Context(), &podsetnik); err != nil {
log.Printf("greška pri čuvanju podsetnika: %v", err) log.Printf("greška pri čuvanju podsetnika: %v", err)
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB) prikaziGresku("Došlo je do greške pri čuvanju. Pokušajte ponovo.")
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,
})
return 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 // IzmeniPodsetnik učitava podsetnik po ID-u i prikazuje popunjenu formu za izmenu
func (h *Handler) IzmeniPodsetnik(w http.ResponseWriter, r *http.Request) { func (h *Handler) IzmeniPodsetnik(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
id, err := parseID(chi.URLParam(r, "id")) id, err := parseID(chi.URLParam(r, "id"))
if err != nil { if err != nil {
http.Error(w, "Neispravan ID podsetnika", http.StatusBadRequest) 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.Stranica = "podsetnici"
ps.NaslovStranice = "Izmeni podsetnik" 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{ h.renderujFormuPodsetnika(w, podaciPodsetnikForma{
PodaciStranice: ps, PodaciStranice: ps,
Podsetnik: *podsetnik, Podsetnik: *podsetnik,
Izmena: true, Izmena: true,
Korisnici: korisnici,
}) })
} }
// SacuvajIzmenePodsetnika prima POST formu i ažurira postojeći podsetnik u bazi // SacuvajIzmenePodsetnika prima POST formu i ažurira postojeći podsetnik u bazi
func (h *Handler) SacuvajIzmenePodsetnika(w http.ResponseWriter, r *http.Request) { func (h *Handler) SacuvajIzmenePodsetnika(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
id, err := parseID(chi.URLParam(r, "id")) id, err := parseID(chi.URLParam(r, "id"))
if err != nil { if err != nil {
http.Error(w, "Neispravan ID podsetnika", http.StatusBadRequest) http.Error(w, "Neispravan ID podsetnika", http.StatusBadRequest)
@@ -163,34 +196,35 @@ func (h *Handler) SacuvajIzmenePodsetnika(w http.ResponseWriter, r *http.Request
return return
} }
podsetnik, greska := parseFormuPodsetnika(r) podsetnik, greska := parseFormuPodsetnika(r, k)
podsetnik.ID = id podsetnik.ID = id
if greska != "" {
prikaziGresku := func(poruka string) {
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB) podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
ps := h.popuniPodaciStranice(r, podesavanja) ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "podsetnici" ps.Stranica = "podsetnici"
ps.NaslovStranice = "Izmeni podsetnik" 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{ h.renderujFormuPodsetnika(w, podaciPodsetnikForma{
PodaciStranice: ps, PodaciStranice: ps,
Podsetnik: podsetnik, Podsetnik: podsetnik,
Greska: greska, Greska: poruka,
Izmena: true, Izmena: true,
Korisnici: korisnici,
}) })
}
if greska != "" {
prikaziGresku(greska)
return return
} }
if err := h.PodsetniciFRepo.Izmeni(r.Context(), &podsetnik); err != nil { if err := h.PodsetniciFRepo.Izmeni(r.Context(), &podsetnik); err != nil {
log.Printf("greška pri čuvanju izmene podsetnika: %v", err) log.Printf("greška pri čuvanju izmene podsetnika: %v", err)
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB) prikaziGresku("Došlo je do greške pri čuvanju. Pokušajte ponovo.")
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,
})
return 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 // 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")) naslov := strings.TrimSpace(r.FormValue("naslov"))
if naslov == "" { if naslov == "" {
return model.Podsetnik{}, "Naslov je obavezan." 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{Naslov: naslov}, "Datum podsećanja nije u ispravnom formatu."
} }
return model.Podsetnik{ p := model.Podsetnik{
Naslov: naslov, Naslov: naslov,
Napomena: strings.TrimSpace(r.FormValue("napomena")), Napomena: strings.TrimSpace(r.FormValue("napomena")),
DatumPodsecanja: datum, DatumPodsecanja: datum,
Tip: model.TipOpsti, 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 // 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, "" 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 // renderujFormuProdaje renderuje HTML šablon forme za unos nove prodaje
func (h *Handler) renderujFormuProdaje(w http.ResponseWriter, podaci PodaciFormeProdaje) { func (h *Handler) renderujFormuProdaje(w http.ResponseWriter, podaci PodaciFormeProdaje) {
h.renderujTemplate(w, "prodaja_forma", podaci) 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)
}
+17 -7
View File
@@ -45,6 +45,10 @@ var sveAkcije = []string{
"backup.pokreni", "backup.pokreni",
"podesavanja.login_pozadina", "podesavanja.login_pozadina",
"podesavanja.app_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 // 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": "servis.izmeni", "servis.obrisi":
return true return true
// prodaja // prodaja
case "prodaja.pregled", "prodaja.dodaj", "prodaja.obrisi": case "prodaja.pregled", "prodaja.dodaj", "prodaja.obrisi", "prodaja.storno":
return true return true
// klijent // klijent
case "klijent.pregled", "klijent.dodaj", case "klijent.pregled", "klijent.dodaj",
@@ -105,27 +109,30 @@ func ImaDozvolu(uloga, akcija string) bool {
// pozadinske slike // pozadinske slike
case "podesavanja.login_pozadina", "podesavanja.app_pozadina": case "podesavanja.login_pozadina", "podesavanja.app_pozadina":
return true return true
// teme
case "tema.globalno", "tema.lokalno":
return true
// dashboard — prihod samo admin+
case "dashboard.prihod":
return true
} }
return false return false
case "radnik": case "radnik":
switch akcija { switch akcija {
// artikal — bez brisanja i premeštanja // artikal — samo pregled
case "artikal.pregled", "artikal.dodaj", "artikal.izmeni": case "artikal.pregled":
return true return true
// kategorija — samo pregled // kategorija — samo pregled
case "kategorija.pregled": case "kategorija.pregled":
return true return true
// nabavka — bez brisanja
case "nabavka.pregled", "nabavka.dodaj":
return true
// dobavljač — bez brisanja // dobavljač — bez brisanja
case "dobavljac.pregled", "dobavljac.dodaj", "dobavljac.izmeni": case "dobavljac.pregled", "dobavljac.dodaj", "dobavljac.izmeni":
return true return true
// servis — bez brisanja // servis — bez brisanja
case "servis.pregled", "servis.dodaj", "servis.izmeni": case "servis.pregled", "servis.dodaj", "servis.izmeni":
return true return true
// prodaja — bez brisanja // prodaja — bez brisanja i storna
case "prodaja.pregled", "prodaja.dodaj": case "prodaja.pregled", "prodaja.dodaj":
return true return true
// klijent — bez brisanja // klijent — bez brisanja
@@ -135,6 +142,9 @@ func ImaDozvolu(uloga, akcija string) bool {
case "podsetnik.pregled", "podsetnik.dodaj", case "podsetnik.pregled", "podsetnik.dodaj",
"podsetnik.izmeni", "podsetnik.obrisi": "podsetnik.izmeni", "podsetnik.obrisi":
return true return true
// lokalna tema
case "tema.lokalno":
return true
} }
return false return false
} }
+6
View File
@@ -10,7 +10,13 @@ type Korisnik struct {
Uloga string // "superadmin" | "admin" | "radnik" Uloga string // "superadmin" | "admin" | "radnik"
Aktivan bool Aktivan bool
TotpTajna string TotpTajna string
LokalnaTema string // "tamna" | "svetla" | ""
KoristiLokalnuTemu bool
DatumKreiranja time.Time DatumKreiranja time.Time
LokalnaPozadina string
LokalnaPozadinaOpacity string
LokalnaPozadinaBlur string
LokalnaPozadinaBlurPozadine string
} }
// Sesija predstavlja aktivnu sesiju prijavljenog korisnika // Sesija predstavlja aktivnu sesiju prijavljenog korisnika
+9
View File
@@ -14,9 +14,18 @@ type Podsetnik struct {
Zavrseno bool Zavrseno bool
Tip string Tip string
DatumUnosa time.Time 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 // JePrekoracen vraća true ako datum podsećanja je prošao a podsetnik nije završen
func (p Podsetnik) JePrekoracen() bool { func (p Podsetnik) JePrekoracen() bool {
return !p.Zavrseno && p.DatumPodsecanja.Before(time.Now()) 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
}
+8
View File
@@ -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;
+3
View File
@@ -0,0 +1,3 @@
INSERT INTO podesavanja (kljuc, vrednost)
VALUES ('tema_pre_slike', '')
ON CONFLICT(kljuc) DO NOTHING;
+1
View File
@@ -0,0 +1 @@
ALTER TABLE podsetnici ADD COLUMN korisnik_id INTEGER REFERENCES korisnici(id) ON DELETE SET NULL;
+3
View File
@@ -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';
@@ -0,0 +1 @@
ALTER TABLE korisnici ADD COLUMN lokalna_pozadina_blur_pozadine TEXT DEFAULT '0';
+34
View File
@@ -39,11 +39,13 @@
<span class="nav-tooltip">Magacin</span> <span class="nav-tooltip">Magacin</span>
</a> </a>
{{if index .Dozvole "nabavka.pregled"}}
<a href="/nabavke" class="nav-stavka {{if eq .Stranica "nabavke"}}aktivan{{end}}"> <a href="/nabavke" class="nav-stavka {{if eq .Stranica "nabavke"}}aktivan{{end}}">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>
<span>Nabavke</span> <span>Nabavke</span>
<span class="nav-tooltip">Nabavke</span> <span class="nav-tooltip">Nabavke</span>
</a> </a>
{{end}}
<a href="/servis" class="nav-stavka {{if eq .Stranica "servis"}}aktivan{{end}}"> <a href="/servis" class="nav-stavka {{if eq .Stranica "servis"}}aktivan{{end}}">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>
@@ -86,11 +88,41 @@
<div class="nav-separator"></div> <div class="nav-separator"></div>
<div class="nav-oznaka">Nalog</div> <div class="nav-oznaka">Nalog</div>
{{if index .Dozvole "tema.lokalno"}}
<div x-data="{ otvoren: {{if or (eq .Stranica "profil") (eq .Stranica "profil-tema")}}true{{else}}false{{end}} }">
<button type="button" @click="otvoren = !otvoren"
class="nav-stavka {{if or (eq .Stranica "profil") (eq .Stranica "profil-tema")}}aktivan{{end}}"
style="width:100%;background:none;border:none;cursor:pointer;">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
<span>Moj profil</span>
<span class="nav-strelica">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
:style="`transition:transform 0.2s;transform:${otvoren?'rotate(180deg)':'rotate(0deg)'}`">
<polyline points="6 9 12 15 18 9"/>
</svg>
</span>
<span class="nav-tooltip">Moj profil</span>
</button>
<div class="nav-podmeni" x-show="otvoren" x-transition>
<a href="/admin/profil" class="nav-stavka nav-podstavka {{if eq .Stranica "profil"}}aktivan{{end}}">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
<span>Opšte</span>
<span class="nav-tooltip">Opšte</span>
</a>
<a href="/profil/tema" class="nav-stavka nav-podstavka {{if eq .Stranica "profil-tema"}}aktivan{{end}}">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M12 1v4M12 19v4M4.22 4.22l2.83 2.83M16.95 16.95l2.83 2.83M1 12h4M19 12h4M4.22 19.78l2.83-2.83M16.95 7.05l2.83-2.83"/></svg>
<span>Tema</span>
<span class="nav-tooltip">Tema</span>
</a>
</div>
</div>
{{else}}
<a href="/admin/profil" class="nav-stavka {{if eq .Stranica "profil"}}aktivan{{end}}"> <a href="/admin/profil" class="nav-stavka {{if eq .Stranica "profil"}}aktivan{{end}}">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
<span>Moj profil</span> <span>Moj profil</span>
<span class="nav-tooltip">Moj profil</span> <span class="nav-tooltip">Moj profil</span>
</a> </a>
{{end}}
{{if or (eq .KorisnikUloga "superadmin") (eq .KorisnikUloga "admin")}} {{if or (eq .KorisnikUloga "superadmin") (eq .KorisnikUloga "admin")}}
<a href="/admin/korisnici" class="nav-stavka {{if eq .Stranica "admin"}}aktivan{{end}}"> <a href="/admin/korisnici" class="nav-stavka {{if eq .Stranica "admin"}}aktivan{{end}}">
@@ -108,6 +140,7 @@
</a> </a>
{{end}} {{end}}
{{if ne .KorisnikUloga "radnik"}}
<div class="nav-separator"></div> <div class="nav-separator"></div>
<div class="nav-oznaka">Sistem</div> <div class="nav-oznaka">Sistem</div>
@@ -145,6 +178,7 @@
</div> </div>
</div> </div>
{{end}} {{end}}
{{end}}{{/* kraj ne radnik */}}
</nav> </nav>
<div class="sidebar-dno"> <div class="sidebar-dno">
+14 -2
View File
@@ -12,10 +12,22 @@
<span class="topbar-naslov">{{.NaslovStranice}}</span> <span class="topbar-naslov">{{.NaslovStranice}}</span>
{{if not .AppPozadina}}
<div class="topbar-teme" style="display:flex;align-items:center;gap:8px;"> <div class="topbar-teme" style="display:flex;align-items:center;gap:8px;">
<a href="/tema/tamna" class="tema-krug {{if eq .Tema "tamna"}}tema-krug-aktivan{{end}}" style="background:#1a1d27;" title="Tamna"></a> <form method="POST" action="/podesavanja/tema" style="margin:0;padding:0;">
<a href="/tema/svetla" class="tema-krug {{if eq .Tema "svetla"}}tema-krug-aktivan{{end}}" style="background:#f0f2f5;border:1px solid #e2e6ed;" title="Svetla"></a> <input type="hidden" name="_csrf" value="{{.CsrfToken}}">
<input type="hidden" name="tema" value="tamna">
<button type="submit" class="tema-krug {{if eq .Tema "tamna"}}tema-krug-aktivan{{end}}"
style="background:#1a1d27;border:none;padding:0;cursor:pointer;" title="Tamna"></button>
</form>
<form method="POST" action="/podesavanja/tema" style="margin:0;padding:0;">
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
<input type="hidden" name="tema" value="svetla">
<button type="submit" class="tema-krug {{if eq .Tema "svetla"}}tema-krug-aktivan{{end}}"
style="background:#f0f2f5;border:1px solid #e2e6ed;padding:0;cursor:pointer;" title="Svetla"></button>
</form>
</div> </div>
{{end}}
<div style="position:relative;" id="avatar-wrapper"> <div style="position:relative;" id="avatar-wrapper">
<div class="avatar-korisnik" id="avatar-dugme" style="cursor:pointer;" title="{{.Korisnik}}"> <div class="avatar-korisnik" id="avatar-dugme" style="cursor:pointer;" title="{{.Korisnik}}">
+14
View File
@@ -8,6 +8,7 @@
.animiraj:nth-child(2) { animation-delay: 0.16s; } .animiraj:nth-child(2) { animation-delay: 0.16s; }
.animiraj:nth-child(3) { animation-delay: 0.22s; } .animiraj:nth-child(3) { animation-delay: 0.22s; }
.animiraj:nth-child(4) { animation-delay: 0.28s; } .animiraj:nth-child(4) { animation-delay: 0.28s; }
.animiraj:nth-child(5) { animation-delay: 0.34s; }
</style> </style>
{{end}} {{end}}
@@ -115,5 +116,18 @@
</div> </div>
{{if index .Dozvole "tema.lokalno"}}
<div class="kartica animiraj" style="background:var(--pozadina);border:0.5px solid var(--ivica);">
<div style="display:flex;align-items:center;gap:10px;">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--sb-akcent)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M12 1v4M12 19v4M4.22 4.22l2.83 2.83M16.95 16.95l2.83 2.83M1 12h4M19 12h4M4.22 19.78l2.83-2.83M16.95 7.05l2.83-2.83"/></svg>
<span style="font-size:14px;color:var(--tekst-glavni);">
Podešavanja teme i pozadine nalaze se na
<a href="/profil/tema" style="color:var(--sb-akcent);text-decoration:none;font-weight:500;">stranici Tema</a>.
</span>
</div>
</div>
{{end}}
</div> </div>
{{end}} {{end}}
+2
View File
@@ -38,6 +38,7 @@
<div style="font-size:12px;color:var(--tekst-sporedni);margin-top:4px;">Aktivnih servisa</div> <div style="font-size:12px;color:var(--tekst-sporedni);margin-top:4px;">Aktivnih servisa</div>
</div> </div>
{{if index .Dozvole "dashboard.prihod"}}
<div class="kartica dash-stat animiraj"> <div class="kartica dash-stat animiraj">
<div style="width:36px;height:36px;border-radius:8px;background:#fff7ed;display:flex;align-items:center;justify-content:center;margin-bottom:10px;"> <div style="width:36px;height:36px;border-radius:8px;background:#fff7ed;display:flex;align-items:center;justify-content:center;margin-bottom:10px;">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#ea580c" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="21" r="1"/><circle cx="20" cy="21" r="1"/><path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"/></svg> <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#ea580c" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="21" r="1"/><circle cx="20" cy="21" r="1"/><path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"/></svg>
@@ -45,6 +46,7 @@
<div style="font-size:22px;font-weight:500;color:var(--tekst-glavni);">{{printf "%.0f" .PrihodOvogMeseca}} din</div> <div style="font-size:22px;font-weight:500;color:var(--tekst-glavni);">{{printf "%.0f" .PrihodOvogMeseca}} din</div>
<div style="font-size:12px;color:var(--tekst-sporedni);margin-top:4px;">Prihod ovog meseca</div> <div style="font-size:12px;color:var(--tekst-sporedni);margin-top:4px;">Prihod ovog meseca</div>
</div> </div>
{{end}}
<div class="kartica dash-stat animiraj"> <div class="kartica dash-stat animiraj">
<div style="width:36px;height:36px;border-radius:8px;background:#fef2f2;display:flex;align-items:center;justify-content:center;margin-bottom:10px;"> <div style="width:36px;height:36px;border-radius:8px;background:#fef2f2;display:flex;align-items:center;justify-content:center;margin-bottom:10px;">
+8
View File
@@ -46,8 +46,12 @@
<!-- dugmad --> <!-- dugmad -->
<div style="display:flex;gap:10px;flex-wrap:wrap;"> <div style="display:flex;gap:10px;flex-wrap:wrap;">
{{if index .Dozvole "artikal.dodaj"}}
<a href="/magacin/novi" class="btn-primarno">+ Novi artikal</a> <a href="/magacin/novi" class="btn-primarno">+ Novi artikal</a>
{{end}}
{{if index .Dozvole "kategorija.pregled"}}
<a href="/magacin/kategorije" class="btn-primarno">Kategorije</a> <a href="/magacin/kategorije" class="btn-primarno">Kategorije</a>
{{end}}
</div> </div>
<!-- pretraga i filteri --> <!-- pretraga i filteri -->
@@ -104,9 +108,11 @@
</td> </td>
<td style="padding:12px 16px;text-align:center;"> <td style="padding:12px 16px;text-align:center;">
<div style="display:flex;align-items:center;justify-content:center;gap:8px;"> <div style="display:flex;align-items:center;justify-content:center;gap:8px;">
{{if index $.Dozvole "artikal.izmeni"}}
<a href="/magacin/izmeni/{{.ID}}" class="btn-primarno-malo"> <a href="/magacin/izmeni/{{.ID}}" class="btn-primarno-malo">
Izmeni Izmeni
</a> </a>
{{end}}
{{if index $.Dozvole "artikal.obrisi"}} {{if index $.Dozvole "artikal.obrisi"}}
<a href="/magacin/obrisi/{{.ID}}" class="btn-obrisi-malo" <a href="/magacin/obrisi/{{.ID}}" class="btn-obrisi-malo"
data-potvrda="Da li ste sigurni da želite da obrišete ovaj artikal?"> data-potvrda="Da li ste sigurni da želite da obrišete ovaj artikal?">
@@ -140,7 +146,9 @@
{{end}} {{end}}
</div> </div>
<div style="display:flex;gap:8px;flex-shrink:0;"> <div style="display:flex;gap:8px;flex-shrink:0;">
{{if index $.Dozvole "artikal.izmeni"}}
<a href="/magacin/izmeni/{{.ID}}" class="btn-primarno-malo">Izmeni</a> <a href="/magacin/izmeni/{{.ID}}" class="btn-primarno-malo">Izmeni</a>
{{end}}
{{if index $.Dozvole "artikal.obrisi"}} {{if index $.Dozvole "artikal.obrisi"}}
<a href="/magacin/obrisi/{{.ID}}" class="btn-obrisi-malo" <a href="/magacin/obrisi/{{.ID}}" class="btn-obrisi-malo"
data-potvrda="Da li ste sigurni da želite da obrišete ovaj artikal?"> data-potvrda="Da li ste sigurni da želite da obrišete ovaj artikal?">
+4 -4
View File
@@ -442,13 +442,13 @@
<div> <div>
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:8px;">Izaberi temu</label> <label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:8px;">Izaberi temu</label>
<div class="teme-grid" style="display:flex;gap:10px;"> <div class="teme-grid" style="display:flex;gap:10px;">
<label style="display:flex;align-items:center;gap:8px;padding:10px 14px;border:0.5px solid var(--ivica);border-radius:8px;cursor:pointer;flex:1;{{if eq .Tema "tamna"}}border-color:var(--sb-akcent);{{end}}"> <label style="display:flex;align-items:center;gap:8px;padding:10px 14px;border:0.5px solid var(--ivica);border-radius:8px;cursor:pointer;flex:1;{{if eq .GlobalnaTema "tamna"}}border-color:var(--sb-akcent);{{end}}">
<input type="radio" name="tema" value="tamna" {{if eq .Tema "tamna"}}checked{{end}} style="accent-color:var(--sb-akcent);"> <input type="radio" name="globalna_tema" value="tamna" {{if eq .GlobalnaTema "tamna"}}checked{{end}} style="accent-color:var(--sb-akcent);">
<div style="width:16px;height:16px;border-radius:50%;background:#1a1d27;flex-shrink:0;"></div> <div style="width:16px;height:16px;border-radius:50%;background:#1a1d27;flex-shrink:0;"></div>
<span style="font-size:13px;color:var(--tekst-glavni);">Tamna</span> <span style="font-size:13px;color:var(--tekst-glavni);">Tamna</span>
</label> </label>
<label style="display:flex;align-items:center;gap:8px;padding:10px 14px;border:0.5px solid var(--ivica);border-radius:8px;cursor:pointer;flex:1;{{if eq .Tema "svetla"}}border-color:var(--sb-akcent);{{end}}"> <label style="display:flex;align-items:center;gap:8px;padding:10px 14px;border:0.5px solid var(--ivica);border-radius:8px;cursor:pointer;flex:1;{{if eq .GlobalnaTema "svetla"}}border-color:var(--sb-akcent);{{end}}">
<input type="radio" name="tema" value="svetla" {{if eq .Tema "svetla"}}checked{{end}} style="accent-color:var(--sb-akcent);"> <input type="radio" name="globalna_tema" value="svetla" {{if eq .GlobalnaTema "svetla"}}checked{{end}} style="accent-color:var(--sb-akcent);">
<div style="width:16px;height:16px;border-radius:50%;background:#f0f2f5;border:0.5px solid #e2e6ed;flex-shrink:0;"></div> <div style="width:16px;height:16px;border-radius:50%;background:#f0f2f5;border:0.5px solid #e2e6ed;flex-shrink:0;"></div>
<span style="font-size:13px;color:var(--tekst-glavni);">Svetla</span> <span style="font-size:13px;color:var(--tekst-glavni);">Svetla</span>
</label> </label>
+21 -9
View File
@@ -136,6 +136,7 @@
</div> </div>
{{if index .Dozvole "podesavanja.login_pozadina"}}
<!-- pozadinska slika login stranice --> <!-- pozadinska slika login stranice -->
<div class="kartica animiraj" style="margin-bottom:16px;"> <div class="kartica animiraj" style="margin-bottom:16px;">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px;padding-bottom:12px;border-bottom:0.5px solid var(--ivica);"> <div style="display:flex;align-items:center;gap:10px;margin-bottom:16px;padding-bottom:12px;border-bottom:0.5px solid var(--ivica);">
@@ -223,41 +224,52 @@
</div><!-- /x-data --> </div><!-- /x-data -->
</div> </div>
{{end}}
<!-- tema --> {{if index .Dozvole "tema.globalno"}}
<!-- globalna tema -->
<form method="POST" action="/podesavanja/sacuvaj"> <form method="POST" action="/podesavanja/sacuvaj">
<input type="hidden" name="_next" value="/admin/podesavanja/izgled"> <input type="hidden" name="_next" value="/admin/podesavanja/izgled">
<div class="kartica animiraj" style="margin-bottom:16px;"> <div class="kartica animiraj" style="margin-bottom:16px;">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px;padding-bottom:12px;border-bottom:0.5px solid var(--ivica);"> <div style="display:flex;align-items:center;gap:10px;margin-bottom:16px;padding-bottom:12px;border-bottom:0.5px solid var(--ivica);">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--sb-akcent)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/></svg> <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--sb-akcent)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/></svg>
<span style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">Tema</span> <span style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">Globalna tema</span>
</div> </div>
<div style="font-size:13px;color:var(--tekst-sporedni);margin-bottom:12px;">Primenuje se na sve korisnike koji ne koriste svoju lokalnu temu.</div>
<div> <div>
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:8px;">Izaberi temu</label>
<div class="teme-grid" style="display:flex;gap:10px;"> <div class="teme-grid" style="display:flex;gap:10px;">
<label style="display:flex;align-items:center;gap:8px;padding:10px 14px;border:0.5px solid var(--ivica);border-radius:8px;cursor:pointer;flex:1;{{if eq .Tema "tamna"}}border-color:var(--sb-akcent);{{end}}"> <label style="display:flex;align-items:center;gap:8px;padding:10px 14px;border:0.5px solid var(--ivica);border-radius:8px;cursor:pointer;flex:1;{{if eq .GlobalnaTema "tamna"}}border-color:var(--sb-akcent);{{end}}">
<input type="radio" name="tema" value="tamna" {{if eq .Tema "tamna"}}checked{{end}} style="accent-color:var(--sb-akcent);"> <input type="radio" name="globalna_tema" value="tamna" {{if eq .GlobalnaTema "tamna"}}checked{{end}} style="accent-color:var(--sb-akcent);">
<div style="width:16px;height:16px;border-radius:50%;background:#1a1d27;flex-shrink:0;"></div> <div style="width:16px;height:16px;border-radius:50%;background:#1a1d27;flex-shrink:0;"></div>
<span style="font-size:13px;color:var(--tekst-glavni);">Tamna</span> <span style="font-size:13px;color:var(--tekst-glavni);">Tamna</span>
</label> </label>
<label style="display:flex;align-items:center;gap:8px;padding:10px 14px;border:0.5px solid var(--ivica);border-radius:8px;cursor:pointer;flex:1;{{if eq .Tema "svetla"}}border-color:var(--sb-akcent);{{end}}"> <label style="display:flex;align-items:center;gap:8px;padding:10px 14px;border:0.5px solid var(--ivica);border-radius:8px;cursor:pointer;flex:1;{{if eq .GlobalnaTema "svetla"}}border-color:var(--sb-akcent);{{end}}">
<input type="radio" name="tema" value="svetla" {{if eq .Tema "svetla"}}checked{{end}} style="accent-color:var(--sb-akcent);"> <input type="radio" name="globalna_tema" value="svetla"
{{if eq .GlobalnaTema "svetla"}}checked{{end}}
style="accent-color:var(--sb-akcent);">
<div style="width:16px;height:16px;border-radius:50%;background:#f0f2f5;border:0.5px solid #e2e6ed;flex-shrink:0;"></div> <div style="width:16px;height:16px;border-radius:50%;background:#f0f2f5;border:0.5px solid #e2e6ed;flex-shrink:0;"></div>
<span style="font-size:13px;color:var(--tekst-glavni);">Svetla</span> <span style="font-size:13px;color:var(--tekst-glavni);">Svetla</span>
</label> </label>
</div> </div>
</div> </div>
<div style="display:flex;justify-content:flex-end;margin-top:20px;"> <div style="display:flex;justify-content:flex-end;gap:10px;margin-top:20px;">
<button type="submit" formaction="/podesavanja/tema/globalno"
style="background:transparent;color:var(--sb-akcent);border:0.5px solid var(--sb-akcent);padding:10px 24px;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;transition:opacity 0.2s;"
onmouseover="this.style.opacity='0.75'" onmouseout="this.style.opacity='1'"
title="Primeni ovu temu na sve korisnike, poništi njihove lokalne teme">
Primeni globalno
</button>
<button type="submit" <button type="submit"
style="background:var(--sb-akcent);color:#fff;border:none;padding:10px 24px;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;transition:opacity 0.2s;" style="background:var(--sb-akcent);color:#fff;border:none;padding:10px 24px;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;transition:opacity 0.2s;"
onmouseover="this.style.opacity='0.85'" onmouseout="this.style.opacity='1'"> onmouseover="this.style.opacity='0.85'" onmouseout="this.style.opacity='1'">
Sačuvaj izgled Sačuvaj
</button> </button>
</div> </div>
</div> </div>
</form> </form>
{{end}}
</div> </div>
{{end}} {{end}}
@@ -57,6 +57,23 @@
style="width:100%;resize:vertical;">{{.Podsetnik.Napomena}}</textarea> style="width:100%;resize:vertical;">{{.Podsetnik.Napomena}}</textarea>
</div> </div>
{{if .Korisnici}}
<div>
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:6px;">
Dodeli korisniku
</label>
<select name="korisnik_id" style="width:100%;">
<option value="">— Nije dodeljeno —</option>
{{range .Korisnici}}
<option value="{{.ID}}"
{{if eq $.Podsetnik.KorisnikIDVal .ID}}selected{{end}}>
{{.KorisnickoIme}} ({{.Uloga}})
</option>
{{end}}
</select>
</div>
{{end}}
<div style="display:flex;justify-content:flex-end;gap:10px;margin-top:4px;"> <div style="display:flex;justify-content:flex-end;gap:10px;margin-top:4px;">
<a href="/podsetnici" class="btn-sekundarno">Odustani</a> <a href="/podsetnici" class="btn-sekundarno">Odustani</a>
<button type="submit" class="btn-primarno"> <button type="submit" class="btn-primarno">
+12 -3
View File
@@ -89,9 +89,18 @@
{{printf "%.2f" .Ukupno}} din {{printf "%.2f" .Ukupno}} din
</td> </td>
<td style="padding:12px 16px;text-align:center;"> <td style="padding:12px 16px;text-align:center;">
<a href="/prodaja/{{.ID}}" class="btn-primarno-malo"> <div style="display:flex;align-items:center;justify-content:center;gap:8px;">
Detalji <a href="/prodaja/{{.ID}}" class="btn-primarno-malo">Detalji</a>
</a> {{if index $.Dozvole "prodaja.storno"}}
<form method="POST" action="/prodaja/storno/{{.ID}}" style="margin:0;padding:0;">
<input type="hidden" name="_csrf" value="{{$.CsrfToken}}">
<button type="submit" class="btn-obrisi-malo"
data-potvrda="Da li ste sigurni da želite da stornirate ovaj nalog? Artikli će biti vraćeni na stanje.">
Storno
</button>
</form>
{{end}}
</div>
</td> </td>
</tr> </tr>
{{else}} {{else}}
+201
View File
@@ -0,0 +1,201 @@
{{template "base" .}}
{{define "naslov"}}Moja tema — NTech{{end}}
{{define "dodatni-css"}}
<style>
.animiraj:nth-child(1) { animation-delay: 0.10s; }
.animiraj:nth-child(2) { animation-delay: 0.16s; }
.animiraj:nth-child(3) { animation-delay: 0.22s; }
</style>
{{end}}
{{define "sadrzaj"}}
<div style="width:100%;max-width:100%;">
<!-- kartica: moja tema -->
<div class="kartica animiraj" x-data="{
globalnu: {{if .KoristiGlobalnuTemu}}true{{else}}false{{end}},
tema: '{{if .LokalnaTema}}{{.LokalnaTema}}{{else}}tamna{{end}}'
}" style="margin-bottom:16px;">
<div style="font-size:15px;font-weight:500;color:var(--tekst-glavni);margin-bottom:16px;padding-bottom:12px;border-bottom:0.5px solid var(--ivica);">
Moja tema
</div>
{{if not .AppPozadina}}
<div style="display:flex;align-items:center;justify-content:space-between;opacity:0.5;cursor:not-allowed;"
title="Administrator nije postavio pozadinsku sliku">
<div>
<div style="font-size:14px;color:var(--tekst-glavni);font-weight:500;">Koristi globalnu temu</div>
<div style="font-size:12px;color:var(--tekst-sporedni);margin-top:2px;">Nema aktivne pozadinske slike</div>
</div>
<label style="position:relative;display:inline-block;width:44px;height:24px;pointer-events:none;">
<span style="position:absolute;inset:0;border-radius:24px;background:var(--ivica);"></span>
<span style="position:absolute;left:2px;top:2px;width:20px;height:20px;background:#fff;border-radius:50%;"></span>
</label>
</div>
{{else}}
<form method="POST" action="/profil/tema">
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
<div style="display:flex;flex-direction:column;gap:14px;">
<!-- svič: globalnu=true → koristi globalnu; globalnu=false → koristi lokalnu -->
<div style="display:flex;align-items:center;justify-content:space-between;">
<div>
<div style="font-size:14px;color:var(--tekst-glavni);font-weight:500;">Koristi globalnu temu</div>
<div style="font-size:12px;color:var(--tekst-sporedni);margin-top:2px;" x-text="globalnu ? 'Primenjuje se globalna tema sistema' : 'Primenjuje se vaša lična tema'"></div>
</div>
<label style="position:relative;display:inline-block;width:44px;height:24px;cursor:pointer;">
<input type="checkbox" style="opacity:0;width:0;height:0;position:absolute;"
x-model="globalnu">
<span :style="'position:absolute;inset:0;border-radius:24px;transition:0.2s;background:' + (globalnu ? 'var(--sb-akcent)' : 'var(--ivica)')"></span>
<span :style="'position:absolute;left:' + (globalnu ? '22px' : '2px') + ';top:2px;width:20px;height:20px;background:#fff;border-radius:50%;transition:0.2s;'"></span>
</label>
<!-- globalnu=true → koristi_lokalnu_temu=0; globalnu=false → koristi_lokalnu_temu=1 -->
<input type="hidden" name="koristi_lokalnu_temu" :value="globalnu ? '0' : '1'">
</div>
<!-- radio dugmići vidljivi samo kada je lokalna tema aktivna -->
<div x-show="!globalnu" x-transition style="padding:12px;background:var(--pozadina);border-radius:8px;border:0.5px solid var(--ivica);">
<div style="font-size:13px;color:var(--tekst-sporedni);margin-bottom:10px;">Izaberite temu:</div>
<div style="display:flex;gap:16px;">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;font-size:14px;color:var(--tekst-glavni);">
<input type="radio" name="lokalna_tema" value="tamna" x-model="tema">
Tamna
</label>
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;font-size:14px;color:var(--tekst-glavni);">
<input type="radio" name="lokalna_tema" value="svetla" x-model="tema">
Svetla
</label>
</div>
</div>
<div>
<button type="submit"
style="padding:9px 20px;background:var(--sb-akcent);color:#fff;border:none;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;">
Sačuvaj
</button>
</div>
</div>
</form>
{{end}}
</div>
<!-- kartica: moja pozadinska slika -->
<div class="kartica animiraj" style="margin-bottom:16px;">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px;padding-bottom:12px;border-bottom:0.5px solid var(--ivica);">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--sb-akcent)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
<span style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">Moja pozadinska slika</span>
</div>
{{if .LokalnaPozadina}}
<div style="display:flex;align-items:center;gap:12px;margin-bottom:14px;">
<img src="{{.LokalnaPozadina}}" alt="Trenutna lična pozadina"
style="width:120px;height:70px;object-fit:cover;border-radius:6px;border:0.5px solid var(--ivica);flex-shrink:0;">
<form method="POST" action="/profil/pozadina/ukloni">
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
<button type="submit"
style="padding:7px 14px;background:transparent;border:0.5px solid #fca5a5;border-radius:8px;color:#dc2626;font-size:13px;cursor:pointer;"
data-potvrda="Ukloniti ličnu pozadinsku sliku?">
Ukloni
</button>
</form>
</div>
{{end}}
<form method="POST" action="/profil/pozadina" enctype="multipart/form-data">
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;">
<input type="file" id="lok-bg-file" name="lokalna_pozadina" accept=".jpg,.jpeg,.png,.webp"
style="display:none;">
<label for="lok-bg-file"
style="display:inline-flex;align-items:center;padding:8px 14px;background:var(--pozadina);border:0.5px solid var(--ivica);border-radius:8px;font-size:13px;color:var(--tekst-sporedni);cursor:pointer;user-select:none;">
Odaberi sliku
</label>
<button type="submit"
style="padding:8px 16px;background:var(--sb-akcent);color:#fff;border:none;border-radius:8px;font-size:13px;font-weight:500;cursor:pointer;">
Otpremi sliku
</button>
</div>
<div style="font-size:12px;color:var(--tekst-sporedni);">JPG, PNG ili WebP — maksimum 5 MB</div>
</form>
<div x-data="{ blur: {{.LokalnaPozadinaBlur}}, opacity: {{.LokalnaPozadinaOpacity}}, blurPozadine: {{.LokalnaPozadinaBlurPozadine}} }"
style="margin-top:20px;">
<div style="position:relative;width:100%;height:180px;border-radius:8px;overflow:hidden;">
<div :style="`position:absolute;inset:0;{{if .LokalnaPozadina}}background:url('{{.LokalnaPozadina}}') center/cover;{{else}}background:#1a2033;{{end}}z-index:0;filter:blur(${blurPozadine}px);-webkit-filter:blur(${blurPozadine}px);transform:scale(1.05);`"></div>
<div :style="`position:absolute;inset:0;z-index:1;pointer-events:none;background:rgba(0,0,0,${opacity/100})`"></div>
<div :style="`position:absolute;top:0;left:0;bottom:0;z-index:2;width:56px;background:rgba(255,255,255,0.10);backdrop-filter:blur(${blur}px);-webkit-backdrop-filter:blur(${blur}px);border-right:1px solid rgba(255,255,255,0.15);display:flex;flex-direction:column;align-items:center;padding-top:10px;gap:12px`">
<div style="width:20px;height:20px;border-radius:50%;background:rgba(255,255,255,0.25);flex-shrink:0;"></div>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,0.85)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0;"><rect x="3" y="3" width="7" height="9" rx="1"/><rect x="14" y="3" width="7" height="5" rx="1"/><rect x="14" y="12" width="7" height="9" rx="1"/><rect x="3" y="16" width="7" height="5" rx="1"/></svg>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,0.85)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0;"><path d="M14.7 6.3a1 1 0 000 1.4l1.6 1.6a1 1 0 001.4 0l3.77-3.77a6 6 0 01-7.94 7.94l-6.91 6.91a2.12 2.12 0 01-3-3l6.91-6.91a6 6 0 017.94-7.94l-3.76 3.76z"/></svg>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,0.85)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0;"><path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 00-3-3.87M16 3.13a4 4 0 010 7.75"/></svg>
</div>
<div style="position:absolute;top:12px;left:68px;right:12px;z-index:2;display:flex;flex-direction:column;gap:7px;">
<div :style="`height:36px;border-radius:6px;background:rgba(255,255,255,0.10);backdrop-filter:blur(${blur}px);-webkit-backdrop-filter:blur(${blur}px);border:1px solid rgba(255,255,255,0.15);display:flex;flex-direction:column;justify-content:center;padding:0 10px`">
<div style="display:block;font-size:9px;color:white;opacity:0.7;line-height:1.2;">Artikli</div>
<div style="display:block;font-size:13px;color:white;font-weight:500;line-height:1.3;">1.284</div>
</div>
<div :style="`height:36px;border-radius:6px;background:rgba(255,255,255,0.10);backdrop-filter:blur(${blur}px);-webkit-backdrop-filter:blur(${blur}px);border:1px solid rgba(255,255,255,0.15);display:flex;flex-direction:column;justify-content:center;padding:0 10px`">
<div style="display:block;font-size:9px;color:white;opacity:0.7;line-height:1.2;">Servis</div>
<div style="display:block;font-size:13px;color:white;font-weight:500;line-height:1.3;">47</div>
</div>
</div>
</div>
<div style="margin-top:16px;">
<div style="display:flex;justify-content:space-between;margin-bottom:6px;">
<span style="font-size:13px;color:var(--tekst-sporedni);">Zamućenje pozadine</span>
<span style="font-size:13px;color:var(--tekst-glavni);font-weight:500;" x-text="blurPozadine + 'px'"></span>
</div>
<input type="range" x-model.number="blurPozadine" min="0" max="20" step="1"
style="width:100%;accent-color:var(--sb-akcent);cursor:pointer;">
</div>
<div style="margin-top:12px;">
<div style="display:flex;justify-content:space-between;margin-bottom:6px;">
<span style="font-size:13px;color:var(--tekst-sporedni);">Zamućenje stakla</span>
<span style="font-size:13px;color:var(--tekst-glavni);font-weight:500;" x-text="blur + 'px'"></span>
</div>
<input type="range" x-model.number="blur" min="0" max="20" step="1"
style="width:100%;accent-color:var(--sb-akcent);cursor:pointer;">
</div>
<div style="margin-top:12px;">
<div style="display:flex;justify-content:space-between;margin-bottom:6px;">
<span style="font-size:13px;color:var(--tekst-sporedni);">Zatamnjivanje</span>
<span style="font-size:13px;color:var(--tekst-glavni);font-weight:500;" x-text="opacity + '%'"></span>
</div>
<input type="range" x-model.number="opacity" min="0" max="80" step="1"
style="width:100%;accent-color:var(--sb-akcent);cursor:pointer;">
</div>
<form method="POST" action="/profil/pozadina/stilovi" style="margin-top:16px;">
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
<input type="hidden" name="lokalna_pozadina_opacity" :value="opacity">
<input type="hidden" name="lokalna_pozadina_blur" :value="blur">
<input type="hidden" name="lokalna_pozadina_blur_pozadine" :value="blurPozadine">
<button type="submit"
style="padding:9px 22px;background:var(--sb-akcent);color:#fff;border:none;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;">
Sačuvaj izgled
</button>
</form>
</div><!-- /x-data -->
</div>
{{if index .Dozvole "podesavanja.login_pozadina"}}
<!-- kartica: pozadinska slika prijave — samo za admin/superadmin -->
<div class="kartica animiraj" style="margin-bottom:16px;">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px;padding-bottom:12px;border-bottom:0.5px solid var(--ivica);">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--sb-akcent)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
<span style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">Pozadinska slika prijave</span>
</div>
<p style="font-size:13px;color:var(--tekst-sporedni);margin:0 0 12px;">
Ova slika se prikazuje na stranici za prijavu svim korisnicima.
Podešavanja su dostupna u
<a href="/admin/podesavanja/izgled" style="color:var(--sb-akcent);text-decoration:none;">Podešavanjima → Izgled</a>.
</p>
</div>
{{end}}
</div>
{{end}}
+5 -13
View File
@@ -36,6 +36,11 @@
filter: blur({{.AppPozadinaBlurPozadine}}px); filter: blur({{.AppPozadinaBlurPozadine}}px);
pointer-events: none; pointer-events: none;
z-index: 0; z-index: 0;
animation: appBgFadeIn 0.35s ease forwards;
}
@keyframes appBgFadeIn {
from { opacity: 0; }
to { opacity: 1; }
} }
.app-overlay { .app-overlay {
position: fixed; position: fixed;
@@ -122,19 +127,6 @@
meni.style.background = 'var(--kartica)'; meni.style.background = 'var(--kartica)';
}); });
</script> </script>
{{if ne .Tema "tamna"}}
<script>window.location.replace('/tema/tamna');</script>
{{end}}
<script>
document.addEventListener('DOMContentLoaded', function() {
var poruka = 'Uklonite pozadinsku sliku da biste mogli da menjate temu';
document.querySelectorAll('.tema-krug').forEach(function(el) {
el.style.pointerEvents = 'none';
el.style.opacity = '0.4';
el.title = poruka;
});
});
</script>
{{end}} {{end}}
</head> </head>
<body> <body>