Izmena u radu tema
This commit is contained in:
+18
-2
@@ -118,9 +118,17 @@ func main() {
|
||||
r.Use(middleware.Compress(5))
|
||||
|
||||
// uploads su uvek na disku — korisnički fajlovi, ne ugrađuju se
|
||||
r.Handle("/static/uploads/*", http.StripPrefix("/static/uploads/", http.FileServer(http.Dir("web/static/uploads"))))
|
||||
r.Handle("/static/uploads/*", http.StripPrefix("/static/uploads/",
|
||||
http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
|
||||
http.FileServer(http.Dir("web/static/uploads")).ServeHTTP(w, req)
|
||||
})))
|
||||
// ostali statični fajlovi: disk ako postoji web/static, inače embed
|
||||
r.Handle("/static/*", http.StripPrefix("/static/", http.FileServer(http.FS(staticFS))))
|
||||
r.Handle("/static/*", http.StripPrefix("/static/",
|
||||
http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
|
||||
http.FileServer(http.FS(staticFS)).ServeHTTP(w, req)
|
||||
})))
|
||||
|
||||
// javne rute (bez autentifikacije)
|
||||
r.Get("/prijava", h.PrikazPrijave)
|
||||
@@ -154,6 +162,8 @@ func main() {
|
||||
r.Get("/podesavanja/backup", h.BackupBaze)
|
||||
r.Post("/podesavanja/backup/vrati", h.VratiBackup)
|
||||
r.Get("/tema/{tema}", h.PromeniTemu)
|
||||
r.Post("/podesavanja/tema", h.PromeniGlobalnuTemu)
|
||||
r.Post("/podesavanja/tema/globalno", h.PrimeniGlobalnuTemu)
|
||||
r.Get("/magacin", h.Magacin)
|
||||
r.Get("/magacin/novi", h.NoviArtikal)
|
||||
r.Post("/magacin/novi", h.SacuvajArtikal)
|
||||
@@ -192,6 +202,7 @@ func main() {
|
||||
r.Get("/prodaja/nova", h.NovaProdaja)
|
||||
r.Post("/prodaja/nova", h.SacuvajProdaju)
|
||||
r.Post("/prodaja/obrisi/{id}", h.ObrisiProdaju)
|
||||
r.Post("/prodaja/storno/{id}", h.StornoProdaje)
|
||||
r.Get("/prodaja/{id}/stampa", h.StampaProdaje)
|
||||
r.Get("/prodaja/{id}", h.DetaljiProdaje)
|
||||
|
||||
@@ -229,6 +240,11 @@ func main() {
|
||||
r.Get("/admin/profil/totp/pokreni", h.AdminTotpPokreni)
|
||||
r.Post("/admin/profil/totp/aktiviraj", h.AdminTotpAktivacija)
|
||||
r.Post("/admin/profil/totp/deaktiviraj", h.AdminTotpDeaktivacija)
|
||||
r.Post("/profil/tema", h.AdminSacuvajLokalnuTemu)
|
||||
r.Get("/profil/tema", h.ProfilTema)
|
||||
r.Post("/profil/pozadina", h.ProfilOtpremiPozadinu)
|
||||
r.Post("/profil/pozadina/ukloni", h.ProfilUkloniPozadinu)
|
||||
r.Post("/profil/pozadina/stilovi", h.ProfilSacuvajPozadinuStilove)
|
||||
})
|
||||
|
||||
log.Printf("NTech pokrenut na portu %s", port)
|
||||
|
||||
@@ -86,6 +86,8 @@ type KorisniciRepository interface {
|
||||
AzurirajAktivan(ctx context.Context, id int64, aktivan bool) error
|
||||
PromeniLozinku(ctx context.Context, id int64, hash string) error
|
||||
SacuvajTotpTajnu(ctx context.Context, id int64, tajna string) error
|
||||
SacuvajLokalnuTemu(ctx context.Context, id int64, lokalnaTema string, koristi bool) error
|
||||
SacuvajLokalnuPozadinu(ctx context.Context, id int64, pozadina, opacity, blur, blurPozadine string) error
|
||||
PostojiIjedan(ctx context.Context) (bool, error)
|
||||
Obrisi(ctx context.Context, id int64) error
|
||||
}
|
||||
@@ -102,6 +104,7 @@ type SesijeRepository interface {
|
||||
// PodsetnikFilter definiše parametre za filtriranje liste podsetnika
|
||||
type PodsetnikFilter struct {
|
||||
SamoAktivni bool // true = samo nezavršeni; false = svi
|
||||
KorisnikID *int64 // ako nije nil — samo podsetnici tog korisnika
|
||||
}
|
||||
|
||||
// PokusajiPrijaveRepository definiše operacije nad evidencijom pokušaja prijave
|
||||
@@ -134,5 +137,5 @@ type PodsetnikRepository interface {
|
||||
Izmeni(ctx context.Context, p *model.Podsetnik) error
|
||||
OznaciZavrsenim(ctx context.Context, id int64, zavrseno bool) error
|
||||
Obrisi(ctx context.Context, id int64) error
|
||||
BrojAktivnih(ctx context.Context) (int, error)
|
||||
BrojAktivnih(ctx context.Context, filter PodsetnikFilter) (int, error)
|
||||
}
|
||||
|
||||
@@ -29,41 +29,69 @@ func (r *sqliteKorisniciRepo) Kreiraj(ctx context.Context, korisnickoIme, lozink
|
||||
|
||||
func (r *sqliteKorisniciRepo) DohvatiPoImenu(ctx context.Context, korisnickoIme string) (*model.Korisnik, error) {
|
||||
k := &model.Korisnik{}
|
||||
var aktivan int
|
||||
var totpTajna sql.NullString
|
||||
var aktivan, koristiLokalnuTemu int
|
||||
var totpTajna, lokalnaTema sql.NullString
|
||||
var lokalnaPozadina, lokalnaPozadinaOpacity, lokalnaPozadinaBlur, lokalnaPozadinaBlurPozadine sql.NullString
|
||||
var datumKreiranja time.Time
|
||||
err := r.db.QueryRowContext(ctx,
|
||||
`SELECT id, korisnicko_ime, lozinka_hash, uloga, aktivan, COALESCE(totp_tajna, ''), datum_kreiranja
|
||||
`SELECT id, korisnicko_ime, lozinka_hash, uloga, aktivan, COALESCE(totp_tajna, ''),
|
||||
COALESCE(lokalna_tema, ''), koristi_lokalnu_temu, datum_kreiranja,
|
||||
COALESCE(lokalna_pozadina, ''), COALESCE(lokalna_pozadina_opacity, '50'),
|
||||
COALESCE(lokalna_pozadina_blur, '12'), COALESCE(lokalna_pozadina_blur_pozadine, '0')
|
||||
FROM korisnici WHERE korisnicko_ime = ?`, korisnickoIme).
|
||||
Scan(&k.ID, &k.KorisnickoIme, &k.LozinkaHash, &k.Uloga, &aktivan, &totpTajna, &datumKreiranja)
|
||||
Scan(&k.ID, &k.KorisnickoIme, &k.LozinkaHash, &k.Uloga, &aktivan, &totpTajna,
|
||||
&lokalnaTema, &koristiLokalnuTemu, &datumKreiranja,
|
||||
&lokalnaPozadina, &lokalnaPozadinaOpacity, &lokalnaPozadinaBlur, &lokalnaPozadinaBlurPozadine)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ntech: korisnici.DohvatiPoImenu: %w", err)
|
||||
}
|
||||
k.Aktivan = aktivan == 1
|
||||
k.TotpTajna = totpTajna.String
|
||||
k.LokalnaTema = lokalnaTema.String
|
||||
k.KoristiLokalnuTemu = koristiLokalnuTemu == 1
|
||||
k.DatumKreiranja = datumKreiranja
|
||||
k.LokalnaPozadina = lokalnaPozadina.String
|
||||
k.LokalnaPozadinaOpacity = lokalnaPozadinaOpacity.String
|
||||
k.LokalnaPozadinaBlur = lokalnaPozadinaBlur.String
|
||||
k.LokalnaPozadinaBlurPozadine = lokalnaPozadinaBlurPozadine.String
|
||||
return k, nil
|
||||
}
|
||||
|
||||
func (r *sqliteKorisniciRepo) DohvatiPoID(ctx context.Context, id int64) (*model.Korisnik, error) {
|
||||
k := &model.Korisnik{}
|
||||
var aktivan int
|
||||
var aktivan, koristiLokalnuTemu int
|
||||
var lokalnaTema sql.NullString
|
||||
var lokalnaPozadina, lokalnaPozadinaOpacity, lokalnaPozadinaBlur, lokalnaPozadinaBlurPozadine sql.NullString
|
||||
var datumKreiranja time.Time
|
||||
err := r.db.QueryRowContext(ctx,
|
||||
`SELECT id, korisnicko_ime, lozinka_hash, uloga, aktivan, COALESCE(totp_tajna, ''), datum_kreiranja
|
||||
`SELECT id, korisnicko_ime, lozinka_hash, uloga, aktivan, COALESCE(totp_tajna, ''),
|
||||
COALESCE(lokalna_tema, ''), koristi_lokalnu_temu, datum_kreiranja,
|
||||
COALESCE(lokalna_pozadina, ''), COALESCE(lokalna_pozadina_opacity, '50'),
|
||||
COALESCE(lokalna_pozadina_blur, '12'), COALESCE(lokalna_pozadina_blur_pozadine, '0')
|
||||
FROM korisnici WHERE id = ?`, id).
|
||||
Scan(&k.ID, &k.KorisnickoIme, &k.LozinkaHash, &k.Uloga, &aktivan, &k.TotpTajna, &datumKreiranja)
|
||||
Scan(&k.ID, &k.KorisnickoIme, &k.LozinkaHash, &k.Uloga, &aktivan, &k.TotpTajna,
|
||||
&lokalnaTema, &koristiLokalnuTemu, &datumKreiranja,
|
||||
&lokalnaPozadina, &lokalnaPozadinaOpacity, &lokalnaPozadinaBlur, &lokalnaPozadinaBlurPozadine)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ntech: korisnici.DohvatiPoID: %w", err)
|
||||
}
|
||||
k.Aktivan = aktivan == 1
|
||||
k.LokalnaTema = lokalnaTema.String
|
||||
k.KoristiLokalnuTemu = koristiLokalnuTemu == 1
|
||||
k.DatumKreiranja = datumKreiranja
|
||||
k.LokalnaPozadina = lokalnaPozadina.String
|
||||
k.LokalnaPozadinaOpacity = lokalnaPozadinaOpacity.String
|
||||
k.LokalnaPozadinaBlur = lokalnaPozadinaBlur.String
|
||||
k.LokalnaPozadinaBlurPozadine = lokalnaPozadinaBlurPozadine.String
|
||||
return k, nil
|
||||
}
|
||||
|
||||
func (r *sqliteKorisniciRepo) Lista(ctx context.Context) ([]model.Korisnik, error) {
|
||||
rows, err := r.db.QueryContext(ctx,
|
||||
`SELECT id, korisnicko_ime, lozinka_hash, uloga, aktivan, COALESCE(totp_tajna, ''), datum_kreiranja
|
||||
`SELECT id, korisnicko_ime, lozinka_hash, uloga, aktivan, COALESCE(totp_tajna, ''),
|
||||
COALESCE(lokalna_tema, ''), koristi_lokalnu_temu, datum_kreiranja,
|
||||
COALESCE(lokalna_pozadina, ''), COALESCE(lokalna_pozadina_opacity, '50'),
|
||||
COALESCE(lokalna_pozadina_blur, '12'), COALESCE(lokalna_pozadina_blur_pozadine, '0')
|
||||
FROM korisnici ORDER BY datum_kreiranja ASC`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ntech: korisnici.Lista: %w", err)
|
||||
@@ -72,18 +100,58 @@ func (r *sqliteKorisniciRepo) Lista(ctx context.Context) ([]model.Korisnik, erro
|
||||
var lista []model.Korisnik
|
||||
for rows.Next() {
|
||||
var k model.Korisnik
|
||||
var aktivan int
|
||||
var aktivan, koristiLokalnuTemu int
|
||||
var lokalnaTema sql.NullString
|
||||
var lokalnaPozadina, lokalnaPozadinaOpacity, lokalnaPozadinaBlur, lokalnaPozadinaBlurPozadine sql.NullString
|
||||
var datumKreiranja time.Time
|
||||
if err := rows.Scan(&k.ID, &k.KorisnickoIme, &k.LozinkaHash, &k.Uloga, &aktivan, &k.TotpTajna, &datumKreiranja); err != nil {
|
||||
if err := rows.Scan(&k.ID, &k.KorisnickoIme, &k.LozinkaHash, &k.Uloga, &aktivan, &k.TotpTajna,
|
||||
&lokalnaTema, &koristiLokalnuTemu, &datumKreiranja,
|
||||
&lokalnaPozadina, &lokalnaPozadinaOpacity, &lokalnaPozadinaBlur, &lokalnaPozadinaBlurPozadine); err != nil {
|
||||
return nil, fmt.Errorf("ntech: korisnici.Lista: %w", err)
|
||||
}
|
||||
k.Aktivan = aktivan == 1
|
||||
k.LokalnaTema = lokalnaTema.String
|
||||
k.KoristiLokalnuTemu = koristiLokalnuTemu == 1
|
||||
k.DatumKreiranja = datumKreiranja
|
||||
k.LokalnaPozadina = lokalnaPozadina.String
|
||||
k.LokalnaPozadinaOpacity = lokalnaPozadinaOpacity.String
|
||||
k.LokalnaPozadinaBlur = lokalnaPozadinaBlur.String
|
||||
k.LokalnaPozadinaBlurPozadine = lokalnaPozadinaBlurPozadine.String
|
||||
lista = append(lista, k)
|
||||
}
|
||||
return lista, nil
|
||||
}
|
||||
|
||||
func (r *sqliteKorisniciRepo) SacuvajLokalnuPozadinu(ctx context.Context, id int64, pozadina, opacity, blur, blurPozadine string) error {
|
||||
_, err := r.db.ExecContext(ctx,
|
||||
`UPDATE korisnici SET lokalna_pozadina = ?, lokalna_pozadina_opacity = ?, lokalna_pozadina_blur = ?, lokalna_pozadina_blur_pozadine = ? WHERE id = ?`,
|
||||
pozadina, opacity, blur, blurPozadine, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntech: korisnici.SacuvajLokalnuPozadinu: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *sqliteKorisniciRepo) SacuvajLokalnuTemu(ctx context.Context, id int64, lokalnaTema string, koristi bool) error {
|
||||
koristiInt := 0
|
||||
if koristi {
|
||||
koristiInt = 1
|
||||
}
|
||||
var tema interface{}
|
||||
if lokalnaTema == "" {
|
||||
tema = nil
|
||||
} else {
|
||||
tema = lokalnaTema
|
||||
}
|
||||
_, err := r.db.ExecContext(ctx,
|
||||
`UPDATE korisnici SET lokalna_tema = ?, koristi_lokalnu_temu = ? WHERE id = ?`,
|
||||
tema, koristiInt, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntech: korisnici.SacuvajLokalnuTemu: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *sqliteKorisniciRepo) AzurirajUlogu(ctx context.Context, id int64, uloga string) error {
|
||||
_, err := r.db.ExecContext(ctx, `UPDATE korisnici SET uloga = ? WHERE id = ?`, uloga, id)
|
||||
if err != nil {
|
||||
|
||||
@@ -22,17 +22,24 @@ func NoviPodsetnikRepo(db *sql.DB) *PodsetnikRepo {
|
||||
// Lista vraća listu podsetnika sa opcionim filterom
|
||||
func (r *PodsetnikRepo) Lista(ctx context.Context, filter db.PodsetnikFilter) ([]model.Podsetnik, error) {
|
||||
upit := `
|
||||
SELECT id, naslov, napomena, datum_podsecanja, zavrseno, tip, datum_unosa
|
||||
SELECT id, naslov, napomena, datum_podsecanja, zavrseno, tip, datum_unosa, korisnik_id
|
||||
FROM podsetnici
|
||||
WHERE 1=1`
|
||||
|
||||
var args []any
|
||||
|
||||
if filter.SamoAktivni {
|
||||
upit += " AND zavrseno = 0"
|
||||
}
|
||||
|
||||
if filter.KorisnikID != nil {
|
||||
upit += " AND korisnik_id = ?"
|
||||
args = append(args, *filter.KorisnikID)
|
||||
}
|
||||
|
||||
upit += " ORDER BY datum_podsecanja ASC"
|
||||
|
||||
redovi, err := r.db.QueryContext(ctx, upit)
|
||||
redovi, err := r.db.QueryContext(ctx, upit, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ntech: PodsetnikRepo.Lista: %w", err)
|
||||
}
|
||||
@@ -42,15 +49,19 @@ func (r *PodsetnikRepo) Lista(ctx context.Context, filter db.PodsetnikFilter) ([
|
||||
for redovi.Next() {
|
||||
var p model.Podsetnik
|
||||
var napomena sql.NullString
|
||||
var korisnikID sql.NullInt64
|
||||
var zavrseno int
|
||||
err := redovi.Scan(
|
||||
&p.ID, &p.Naslov, &napomena, &p.DatumPodsecanja, &zavrseno, &p.Tip, &p.DatumUnosa,
|
||||
&p.ID, &p.Naslov, &napomena, &p.DatumPodsecanja, &zavrseno, &p.Tip, &p.DatumUnosa, &korisnikID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ntech: PodsetnikRepo.Lista: scan: %w", err)
|
||||
}
|
||||
p.Napomena = napomena.String
|
||||
p.Zavrseno = zavrseno == 1
|
||||
if korisnikID.Valid {
|
||||
p.KorisnikID = &korisnikID.Int64
|
||||
}
|
||||
rezultat = append(rezultat, p)
|
||||
}
|
||||
|
||||
@@ -61,12 +72,13 @@ func (r *PodsetnikRepo) Lista(ctx context.Context, filter db.PodsetnikFilter) ([
|
||||
func (r *PodsetnikRepo) DohvatiID(ctx context.Context, id int64) (*model.Podsetnik, error) {
|
||||
var p model.Podsetnik
|
||||
var napomena sql.NullString
|
||||
var korisnikID sql.NullInt64
|
||||
var zavrseno int
|
||||
|
||||
err := r.db.QueryRowContext(ctx, `
|
||||
SELECT id, naslov, napomena, datum_podsecanja, zavrseno, tip, datum_unosa
|
||||
SELECT id, naslov, napomena, datum_podsecanja, zavrseno, tip, datum_unosa, korisnik_id
|
||||
FROM podsetnici WHERE id = ?`, id).Scan(
|
||||
&p.ID, &p.Naslov, &napomena, &p.DatumPodsecanja, &zavrseno, &p.Tip, &p.DatumUnosa,
|
||||
&p.ID, &p.Naslov, &napomena, &p.DatumPodsecanja, &zavrseno, &p.Tip, &p.DatumUnosa, &korisnikID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ntech: PodsetnikRepo.DohvatiID: %w", err)
|
||||
@@ -74,16 +86,24 @@ func (r *PodsetnikRepo) DohvatiID(ctx context.Context, id int64) (*model.Podsetn
|
||||
|
||||
p.Napomena = napomena.String
|
||||
p.Zavrseno = zavrseno == 1
|
||||
if korisnikID.Valid {
|
||||
p.KorisnikID = &korisnikID.Int64
|
||||
}
|
||||
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
// Kreiraj dodaje novi podsetnik u bazu
|
||||
func (r *PodsetnikRepo) Kreiraj(ctx context.Context, p *model.Podsetnik) (int64, error) {
|
||||
var korisnikID interface{}
|
||||
if p.KorisnikID != nil {
|
||||
korisnikID = *p.KorisnikID
|
||||
}
|
||||
|
||||
rezultat, err := r.db.ExecContext(ctx, `
|
||||
INSERT INTO podsetnici (naslov, napomena, datum_podsecanja, tip)
|
||||
VALUES (?, ?, ?, ?)`,
|
||||
p.Naslov, nullString(p.Napomena), p.DatumPodsecanja, p.Tip,
|
||||
INSERT INTO podsetnici (naslov, napomena, datum_podsecanja, tip, korisnik_id)
|
||||
VALUES (?, ?, ?, ?, ?)`,
|
||||
p.Naslov, nullString(p.Napomena), p.DatumPodsecanja, p.Tip, korisnikID,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("ntech: PodsetnikRepo.Kreiraj: %w", err)
|
||||
@@ -99,11 +119,16 @@ func (r *PodsetnikRepo) Kreiraj(ctx context.Context, p *model.Podsetnik) (int64,
|
||||
|
||||
// Izmeni ažurira postojeći podsetnik
|
||||
func (r *PodsetnikRepo) Izmeni(ctx context.Context, p *model.Podsetnik) error {
|
||||
var korisnikID interface{}
|
||||
if p.KorisnikID != nil {
|
||||
korisnikID = *p.KorisnikID
|
||||
}
|
||||
|
||||
_, err := r.db.ExecContext(ctx, `
|
||||
UPDATE podsetnici SET
|
||||
naslov = ?, napomena = ?, datum_podsecanja = ?, tip = ?
|
||||
naslov = ?, napomena = ?, datum_podsecanja = ?, tip = ?, korisnik_id = ?
|
||||
WHERE id = ?`,
|
||||
p.Naslov, nullString(p.Napomena), p.DatumPodsecanja, p.Tip, p.ID,
|
||||
p.Naslov, nullString(p.Napomena), p.DatumPodsecanja, p.Tip, korisnikID, p.ID,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntech: PodsetnikRepo.Izmeni: %w", err)
|
||||
@@ -138,12 +163,18 @@ func (r *PodsetnikRepo) Obrisi(ctx context.Context, id int64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// BrojAktivnih vraća broj nezavršenih podsetnika
|
||||
func (r *PodsetnikRepo) BrojAktivnih(ctx context.Context) (int, error) {
|
||||
// BrojAktivnih vraća broj nezavršenih podsetnika, opcionalno filtrirano po korisniku
|
||||
func (r *PodsetnikRepo) BrojAktivnih(ctx context.Context, filter db.PodsetnikFilter) (int, error) {
|
||||
upit := "SELECT COUNT(*) FROM podsetnici WHERE zavrseno = 0"
|
||||
var args []any
|
||||
|
||||
if filter.KorisnikID != nil {
|
||||
upit += " AND korisnik_id = ?"
|
||||
args = append(args, *filter.KorisnikID)
|
||||
}
|
||||
|
||||
var broj int
|
||||
err := r.db.QueryRowContext(ctx,
|
||||
"SELECT COUNT(*) FROM podsetnici WHERE zavrseno = 0",
|
||||
).Scan(&broj)
|
||||
err := r.db.QueryRowContext(ctx, upit, args...).Scan(&broj)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("ntech: PodsetnikRepo.BrojAktivnih: %w", err)
|
||||
}
|
||||
|
||||
@@ -31,6 +31,21 @@ type podaciAdminProfil struct {
|
||||
TotpTajna string
|
||||
TotpQR template.URL
|
||||
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
|
||||
@@ -287,9 +302,51 @@ func (h *Handler) AdminProfil(w http.ResponseWriter, r *http.Request) {
|
||||
h.renderujTemplate(w, "admin_profil", podaciAdminProfil{
|
||||
PodaciStranice: ps,
|
||||
TotpAktivan: svezi.TotpTajna != "",
|
||||
LokalnaTema: svezi.LokalnaTema,
|
||||
KoristiLokalnuTemu: svezi.KoristiLokalnuTemu,
|
||||
GlobalnaTema: podesavanja["globalna_tema"],
|
||||
})
|
||||
}
|
||||
|
||||
// AdminSacuvajLokalnuTemu čuva korisnikovu lokalnu temu
|
||||
func (h *Handler) AdminSacuvajLokalnuTemu(w http.ResponseWriter, r *http.Request) {
|
||||
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||
if k == nil {
|
||||
http.Redirect(w, r, "/prijava", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "tema.lokalno") {
|
||||
http.Error(w, "Pristup odbijen", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if err := r.ParseForm(); err != nil {
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Greška. Pokušajte ponovo.")
|
||||
http.Redirect(w, r, "/admin/profil", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
koristi := r.FormValue("koristi_lokalnu_temu") == "1"
|
||||
lokalnaTema := r.FormValue("lokalna_tema")
|
||||
if lokalnaTema != "tamna" && lokalnaTema != "svetla" {
|
||||
lokalnaTema = "tamna"
|
||||
}
|
||||
|
||||
if err := h.KorisniciRepo.SacuvajLokalnuTemu(r.Context(), k.ID, lokalnaTema, koristi); err != nil {
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju. Pokušajte ponovo.")
|
||||
http.Redirect(w, r, "/admin/profil", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Tema je sačuvana.")
|
||||
// vrati korisnika na stranicu odakle je došao (Referer), ili na profil kao fallback
|
||||
if ref := r.Referer(); ref != "" {
|
||||
http.Redirect(w, r, ref, http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/admin/profil", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// AdminPromeniLozinku menja lozinku prijavljenog korisnika
|
||||
func (h *Handler) AdminPromeniLozinku(w http.ResponseWriter, r *http.Request) {
|
||||
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||
|
||||
@@ -5,7 +5,9 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
appdb "ntech/internal/db"
|
||||
"ntech/internal/db/sqlite"
|
||||
"ntech/internal/middleware"
|
||||
"ntech/internal/model"
|
||||
)
|
||||
|
||||
@@ -47,12 +49,16 @@ func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) {
|
||||
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, `
|
||||
SELECT COALESCE(SUM(ukupno), 0) FROM prodajni_nalozi
|
||||
WHERE substr(datum, 1, 7) = strftime('%Y-%m', 'now', 'localtime')`,
|
||||
).Scan(&prihodOvogMeseca); err != nil {
|
||||
log.Printf("dashboard: prihod ovog meseca: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := h.DB.QueryRowContext(ctx,
|
||||
"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)
|
||||
}
|
||||
|
||||
if n, err := h.PodsetniciFRepo.BrojAktivnih(ctx); err != nil {
|
||||
korisnikFilter := appdb.PodsetnikFilter{}
|
||||
if korisnikDash.Uloga == "radnik" {
|
||||
korisnikFilter.KorisnikID = &korisnikDash.ID
|
||||
}
|
||||
if n, err := h.PodsetniciFRepo.BrojAktivnih(ctx, korisnikFilter); err != nil {
|
||||
log.Printf("dashboard: aktivni podsetnici: %v", err)
|
||||
} else {
|
||||
aktivniPodsetnici = n
|
||||
|
||||
@@ -74,24 +74,65 @@ func (h *Handler) reinicijalzijRepozitorijume(novaDB *sql.DB) {
|
||||
|
||||
// popuniPodaciStranice popunjava zajednička polja stranice uključujući prijavljenog korisnika
|
||||
func (h *Handler) popuniPodaciStranice(r *http.Request, podesavanja map[string]string) model.PodaciStranice {
|
||||
// redosled prioriteta teme: pozadinska slika → lokalna → globalna → fallback
|
||||
globalnaTema := podesavanja["globalna_tema"]
|
||||
if globalnaTema == "" {
|
||||
globalnaTema = podesavanja["tema"]
|
||||
}
|
||||
if globalnaTema == "" {
|
||||
globalnaTema = "tamna"
|
||||
}
|
||||
tema := globalnaTema
|
||||
|
||||
ps := model.PodaciStranice{
|
||||
Tema: podesavanja["tema"],
|
||||
Tema: tema,
|
||||
NazivFirme: podesavanja["naziv_firme"],
|
||||
Podnazlov: podesavanja["podnazlov"],
|
||||
LogoTip: podesavanja["logo_tip"],
|
||||
LogoPutanja: podesavanja["logo_putanja"],
|
||||
Korisnik: "Admin",
|
||||
}
|
||||
var korisnik *model.Korisnik
|
||||
if k := middleware.KorisnikIzKonteksta(r.Context()); k != nil {
|
||||
korisnik = k
|
||||
ps.Korisnik = k.KorisnickoIme
|
||||
ps.KorisnikIme = k.KorisnickoIme
|
||||
ps.KorisnikUloga = k.Uloga
|
||||
ps.Dozvole = h.DozvoleRepo.SveDozvole(r.Context(), k.Uloga)
|
||||
// lokalna tema korisnika
|
||||
if k.KoristiLokalnuTemu && k.LokalnaTema != "" {
|
||||
ps.Tema = k.LokalnaTema
|
||||
}
|
||||
}
|
||||
ps.CsrfToken = middleware.CsrfToken(r.Context())
|
||||
ps.Flash = middleware.GetFlash(r, h.DB)
|
||||
|
||||
// logika pozadine:
|
||||
// - lična pozadina (samo kada je lokalni režim aktivan) → zamenjuje globalnu
|
||||
// - globalna pozadina → prikazuje se svima koji nemaju ličnu
|
||||
// KoristiLokalnuTemu utiče na izbor tamne/svetle teme, ne na vidljivost pozadine
|
||||
if korisnik != nil && korisnik.KoristiLokalnuTemu && korisnik.LokalnaPozadina != "" {
|
||||
ps.AppPozadina = korisnik.LokalnaPozadina
|
||||
ps.Tema = "tamna"
|
||||
ps.AppPozadinaOpacity = korisnik.LokalnaPozadinaOpacity
|
||||
if ps.AppPozadinaOpacity == "" {
|
||||
ps.AppPozadinaOpacity = "50"
|
||||
}
|
||||
ps.AppPozadinaBlur = korisnik.LokalnaPozadinaBlur
|
||||
if ps.AppPozadinaBlur == "" {
|
||||
ps.AppPozadinaBlur = "12"
|
||||
}
|
||||
ps.AppPozadinaBlurPozadine = korisnik.LokalnaPozadinaBlurPozadine
|
||||
if ps.AppPozadinaBlurPozadine == "" {
|
||||
ps.AppPozadinaBlurPozadine = "0"
|
||||
}
|
||||
} else {
|
||||
ps.AppPozadina = podesavanja["app_pozadina"]
|
||||
if ps.AppPozadina != "" {
|
||||
// globalna pozadina forsira tamnu temu, osim ako korisnik ima aktivnu lokalnu temu
|
||||
if korisnik == nil || !korisnik.KoristiLokalnuTemu {
|
||||
ps.Tema = "tamna"
|
||||
}
|
||||
ps.AppPozadinaOpacity = podesavanja["app_pozadina_opacity"]
|
||||
if ps.AppPozadinaOpacity == "" {
|
||||
ps.AppPozadinaOpacity = "50"
|
||||
@@ -104,6 +145,21 @@ func (h *Handler) popuniPodaciStranice(r *http.Request, podesavanja map[string]s
|
||||
if ps.AppPozadinaBlurPozadine == "" {
|
||||
ps.AppPozadinaBlurPozadine = "0"
|
||||
}
|
||||
} else {
|
||||
ps.AppPozadinaOpacity = podesavanja["app_pozadina_opacity"]
|
||||
if ps.AppPozadinaOpacity == "" {
|
||||
ps.AppPozadinaOpacity = "50"
|
||||
}
|
||||
ps.AppPozadinaBlur = podesavanja["app_pozadina_blur"]
|
||||
if ps.AppPozadinaBlur == "" {
|
||||
ps.AppPozadinaBlur = "12"
|
||||
}
|
||||
ps.AppPozadinaBlurPozadine = podesavanja["app_pozadina_blur_pozadine"]
|
||||
if ps.AppPozadinaBlurPozadine == "" {
|
||||
ps.AppPozadinaBlurPozadine = "0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ps
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ var saSidebar = []string{
|
||||
"nabavke", "nabavka_forma", "nabavka_detalji",
|
||||
"podesavanja", "podesavanja_opste", "podesavanja_izgled", "podesavanja_sistem",
|
||||
"podsetnici", "podsetnik_forma",
|
||||
"profil_tema",
|
||||
"prodaja", "prodaja_detalji", "prodaja_forma",
|
||||
"servis", "servis_forma", "servis_detalji",
|
||||
}
|
||||
|
||||
@@ -57,6 +57,12 @@ func artikalUJSON(artikli []model.ArtikalSaKategorijom) template.JS {
|
||||
|
||||
// Nabavke renderuje listu svih nabavki
|
||||
func (h *Handler) Nabavke(w http.ResponseWriter, r *http.Request) {
|
||||
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "nabavka.pregled") {
|
||||
http.Error(w, "Nemate dozvolu za pregled nabavki.", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
|
||||
@@ -84,6 +90,12 @@ func (h *Handler) Nabavke(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// NovaNabavka prikazuje formu za unos nove nabavke
|
||||
func (h *Handler) NovaNabavka(w http.ResponseWriter, r *http.Request) {
|
||||
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "nabavka.pregled") {
|
||||
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
|
||||
@@ -163,6 +175,12 @@ func (h *Handler) SacuvajNabavku(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// DetaljiNabavke prikazuje pregled jedne nabavke sa svim stavkama
|
||||
func (h *Handler) DetaljiNabavke(w http.ResponseWriter, r *http.Request) {
|
||||
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "nabavka.pregled") {
|
||||
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
id, err := parseID(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
http.Error(w, "Neispravan ID nabavke", http.StatusBadRequest)
|
||||
|
||||
+142
-51
@@ -32,7 +32,7 @@ type PodaciPodesavanja struct {
|
||||
PIB string
|
||||
LogoTip string
|
||||
LogoPutanja string
|
||||
Tema string
|
||||
GlobalnaTema string // stvarna vrednost iz podešavanja (ne senči PodaciStranice.Tema)
|
||||
Sacuvano bool
|
||||
Verzija string
|
||||
LogoGreska string
|
||||
@@ -83,7 +83,15 @@ func (h *Handler) Podesavanja(w http.ResponseWriter, r *http.Request) {
|
||||
PIB: podesavanja["pib"],
|
||||
LogoTip: podesavanja["logo_tip"],
|
||||
LogoPutanja: podesavanja["logo_putanja"],
|
||||
Tema: podesavanja["tema"],
|
||||
GlobalnaTema: func() string {
|
||||
if v := podesavanja["globalna_tema"]; v != "" {
|
||||
return v
|
||||
}
|
||||
if v := podesavanja["tema"]; v != "" {
|
||||
return v
|
||||
}
|
||||
return "tamna"
|
||||
}(),
|
||||
Sacuvano: r.URL.Query().Get("sacuvano") == "1",
|
||||
BackupVracen: r.URL.Query().Get("sacuvano") == "vraceno",
|
||||
Verzija: h.Verzija,
|
||||
@@ -259,6 +267,7 @@ func (h *Handler) SacuvajPodesavanja(w http.ResponseWriter, r *http.Request) {
|
||||
"pib": r.FormValue("pib"),
|
||||
"logo_tip": r.FormValue("logo_tip"),
|
||||
"tema": r.FormValue("tema"),
|
||||
"globalna_tema": r.FormValue("globalna_tema"),
|
||||
}
|
||||
|
||||
for kljuc, vrednost := range polja {
|
||||
@@ -409,21 +418,21 @@ func (h *Handler) OtpremiLoginPozadinu(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, 5<<20+4096)
|
||||
if err := r.ParseMultipartForm(5 << 20); err != nil {
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Fajl je prevelik (maksimum 5 MB).")
|
||||
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
fajl, zaglavlje, err := r.FormFile("login_pozadina")
|
||||
if err != nil {
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Nije odabran fajl.")
|
||||
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
defer fajl.Close()
|
||||
|
||||
if zaglavlje.Size > 5<<20 {
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Fajl je prevelik (maksimum 5 MB).")
|
||||
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -437,7 +446,7 @@ func (h *Handler) OtpremiLoginPozadinu(w http.ResponseWriter, r *http.Request) {
|
||||
ocekivaniMime, ok := dozvoljenoExt[ext]
|
||||
if !ok {
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Dozvoljeni formati su JPG, PNG i WebP.")
|
||||
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -447,12 +456,12 @@ func (h *Handler) OtpremiLoginPozadinu(w http.ResponseWriter, r *http.Request) {
|
||||
stvarniMime := http.DetectContentType(buf[:n])
|
||||
if !strings.HasPrefix(stvarniMime, ocekivaniMime) {
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Sadržaj fajla ne odgovara odabranoj ekstenziji.")
|
||||
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
if _, err := fajl.Seek(0, io.SeekStart); err != nil {
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri obradi fajla.")
|
||||
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -469,7 +478,7 @@ func (h *Handler) OtpremiLoginPozadinu(w http.ResponseWriter, r *http.Request) {
|
||||
if err != nil {
|
||||
log.Printf("upload login pozadine: greška pri generisanju imena: %v", err)
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju fajla.")
|
||||
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -478,7 +487,7 @@ func (h *Handler) OtpremiLoginPozadinu(w http.ResponseWriter, r *http.Request) {
|
||||
if err != nil {
|
||||
log.Printf("upload login pozadine: ne mogu kreirati fajl: %v", err)
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju fajla.")
|
||||
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
defer dst.Close()
|
||||
@@ -486,7 +495,7 @@ func (h *Handler) OtpremiLoginPozadinu(w http.ResponseWriter, r *http.Request) {
|
||||
if _, err := io.Copy(dst, fajl); err != nil {
|
||||
log.Printf("upload login pozadine: greška pri kopiranju: %v", err)
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju fajla.")
|
||||
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -494,12 +503,12 @@ func (h *Handler) OtpremiLoginPozadinu(w http.ResponseWriter, r *http.Request) {
|
||||
if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "login_pozadina", putanja); err != nil {
|
||||
log.Printf("upload login pozadine: greška pri čuvanju putanje: %v", err)
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju podešavanja.")
|
||||
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Pozadinska slika je uspešno otpremljena.")
|
||||
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// UkloniLoginPozadinu briše pozadinsku sliku login stranice sa diska i iz podešavanja
|
||||
@@ -522,12 +531,12 @@ func (h *Handler) UkloniLoginPozadinu(w http.ResponseWriter, r *http.Request) {
|
||||
if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "login_pozadina", ""); err != nil {
|
||||
log.Printf("ukloni login pozadinu: greška pri čuvanju: %v", err)
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri uklanjanju slike.")
|
||||
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Pozadinska slika je uklonjena.")
|
||||
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// OtpremiAppPozadinu prima multipart upload slike i čuva je kao pozadinsku sliku aplikacije
|
||||
@@ -541,21 +550,21 @@ func (h *Handler) OtpremiAppPozadinu(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, 5<<20+4096)
|
||||
if err := r.ParseMultipartForm(5 << 20); err != nil {
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Fajl je prevelik (maksimum 5 MB).")
|
||||
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
fajl, zaglavlje, err := r.FormFile("app_pozadina")
|
||||
if err != nil {
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Nije odabran fajl.")
|
||||
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
defer fajl.Close()
|
||||
|
||||
if zaglavlje.Size > 5<<20 {
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Fajl je prevelik (maksimum 5 MB).")
|
||||
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -569,7 +578,7 @@ func (h *Handler) OtpremiAppPozadinu(w http.ResponseWriter, r *http.Request) {
|
||||
ocekivaniMime, ok := dozvoljenoExt[ext]
|
||||
if !ok {
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Dozvoljeni formati su JPG, PNG i WebP.")
|
||||
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -578,12 +587,12 @@ func (h *Handler) OtpremiAppPozadinu(w http.ResponseWriter, r *http.Request) {
|
||||
stvarniMime := http.DetectContentType(buf[:n])
|
||||
if !strings.HasPrefix(stvarniMime, ocekivaniMime) {
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Sadržaj fajla ne odgovara odabranoj ekstenziji.")
|
||||
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
if _, err := fajl.Seek(0, io.SeekStart); err != nil {
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri obradi fajla.")
|
||||
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -599,7 +608,7 @@ func (h *Handler) OtpremiAppPozadinu(w http.ResponseWriter, r *http.Request) {
|
||||
if err != nil {
|
||||
log.Printf("upload app pozadine: greška pri generisanju imena: %v", err)
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju fajla.")
|
||||
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -608,7 +617,7 @@ func (h *Handler) OtpremiAppPozadinu(w http.ResponseWriter, r *http.Request) {
|
||||
if err != nil {
|
||||
log.Printf("upload app pozadine: ne mogu kreirati fajl: %v", err)
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju fajla.")
|
||||
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
defer dst.Close()
|
||||
@@ -616,7 +625,7 @@ func (h *Handler) OtpremiAppPozadinu(w http.ResponseWriter, r *http.Request) {
|
||||
if _, err := io.Copy(dst, fajl); err != nil {
|
||||
log.Printf("upload app pozadine: greška pri kopiranju: %v", err)
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju fajla.")
|
||||
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -624,12 +633,23 @@ func (h *Handler) OtpremiAppPozadinu(w http.ResponseWriter, r *http.Request) {
|
||||
if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "app_pozadina", putanja); err != nil {
|
||||
log.Printf("upload app pozadine: greška pri čuvanju putanje: %v", err)
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju podešavanja.")
|
||||
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
// zapamti trenutnu globalnu temu pre nego što je forsiramo na tamnu
|
||||
trenutnaTema := staraPodesavanja["globalna_tema"]
|
||||
if trenutnaTema == "" {
|
||||
trenutnaTema = staraPodesavanja["tema"]
|
||||
}
|
||||
if trenutnaTema == "" {
|
||||
trenutnaTema = "tamna"
|
||||
}
|
||||
_ = ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "tema_pre_slike", trenutnaTema)
|
||||
_ = ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "globalna_tema", "tamna")
|
||||
|
||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Pozadinska slika aplikacije je uspešno otpremljena.")
|
||||
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// UkloniAppPozadinu briše pozadinsku sliku aplikacije sa diska i iz podešavanja
|
||||
@@ -652,12 +672,22 @@ func (h *Handler) UkloniAppPozadinu(w http.ResponseWriter, r *http.Request) {
|
||||
if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "app_pozadina", ""); err != nil {
|
||||
log.Printf("ukloni app pozadinu: greška pri čuvanju: %v", err)
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri uklanjanju slike.")
|
||||
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
// vrati temu koja je bila aktivna pre dodavanja slike
|
||||
if err == nil {
|
||||
temaPreSlike := podesavanja["tema_pre_slike"]
|
||||
if temaPreSlike == "" {
|
||||
temaPreSlike = "tamna"
|
||||
}
|
||||
_ = ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "globalna_tema", temaPreSlike)
|
||||
_ = ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "tema_pre_slike", "")
|
||||
}
|
||||
|
||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Pozadinska slika aplikacije je uklonjena.")
|
||||
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// SacuvajAppPozadinaStilove čuva vrednosti zamućenja i prozirnosti pozadine aplikacije
|
||||
@@ -669,7 +699,7 @@ func (h *Handler) SacuvajAppPozadinaStilove(w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
if err := r.ParseForm(); err != nil {
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čitanju forme.")
|
||||
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -680,19 +710,19 @@ func (h *Handler) SacuvajAppPozadinaStilove(w http.ResponseWriter, r *http.Reque
|
||||
blurVal, err := strconv.Atoi(blurStr)
|
||||
if err != nil || blurVal < 0 || blurVal > 20 {
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Neispravna vrednost zamućenja stakla.")
|
||||
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
blurPozadineVal, err := strconv.Atoi(blurPozadineStr)
|
||||
if err != nil || blurPozadineVal < 0 || blurPozadineVal > 20 {
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Neispravna vrednost zamućenja pozadine.")
|
||||
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
opacityVal, err := strconv.Atoi(opacityStr)
|
||||
if err != nil || opacityVal < 0 || opacityVal > 80 {
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Neispravna vrednost prozirnosti.")
|
||||
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -704,13 +734,13 @@ func (h *Handler) SacuvajAppPozadinaStilove(w http.ResponseWriter, r *http.Reque
|
||||
if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, kljuc, vrednost); err != nil {
|
||||
log.Printf("stilovi app pozadine: greška pri čuvanju %s: %v", kljuc, err)
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju podešavanja.")
|
||||
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Izgled pozadine aplikacije je sačuvan.")
|
||||
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// SacuvajLoginPozadinaStilove čuva vrednosti zamućenja i prozirnosti pozadine login stranice
|
||||
@@ -722,7 +752,7 @@ func (h *Handler) SacuvajLoginPozadinaStilove(w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
if err := r.ParseForm(); err != nil {
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čitanju forme.")
|
||||
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -733,19 +763,19 @@ func (h *Handler) SacuvajLoginPozadinaStilove(w http.ResponseWriter, r *http.Req
|
||||
blurPozadineVal, err := strconv.Atoi(blurPozadineStr)
|
||||
if err != nil || blurPozadineVal < 0 || blurPozadineVal > 20 {
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Neispravna vrednost zamućenja pozadine.")
|
||||
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
blurKarticeVal, err := strconv.Atoi(blurKarticeStr)
|
||||
if err != nil || blurKarticeVal < 0 || blurKarticeVal > 20 {
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Neispravna vrednost zamućenja kartice.")
|
||||
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
opacityVal, err := strconv.Atoi(opacityStr)
|
||||
if err != nil || opacityVal < 0 || opacityVal > 80 {
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Neispravna vrednost prozirnosti.")
|
||||
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -757,13 +787,13 @@ func (h *Handler) SacuvajLoginPozadinaStilove(w http.ResponseWriter, r *http.Req
|
||||
if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, kljuc, vrednost); err != nil {
|
||||
log.Printf("stilovi login pozadine: greška pri čuvanju %s: %v", kljuc, err)
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju podešavanja.")
|
||||
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Izgled pozadine prijave je sačuvan.")
|
||||
http.Redirect(w, r, "/podesavanja", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// napuniPodaciPodesavanja učitava sva podešavanja i kreira strukturu za template
|
||||
@@ -784,7 +814,15 @@ func (h *Handler) napuniPodaciPodesavanja(r *http.Request, naslov string) (Podac
|
||||
PIB: podesavanja["pib"],
|
||||
LogoTip: podesavanja["logo_tip"],
|
||||
LogoPutanja: podesavanja["logo_putanja"],
|
||||
Tema: podesavanja["tema"],
|
||||
GlobalnaTema: func() string {
|
||||
if v := podesavanja["globalna_tema"]; v != "" {
|
||||
return v
|
||||
}
|
||||
if v := podesavanja["tema"]; v != "" {
|
||||
return v
|
||||
}
|
||||
return "tamna"
|
||||
}(),
|
||||
Sacuvano: r.URL.Query().Get("sacuvano") == "1",
|
||||
BackupVracen: r.URL.Query().Get("sacuvano") == "vraceno",
|
||||
Verzija: h.Verzija,
|
||||
@@ -876,28 +914,81 @@ func (h *Handler) PodesavanjaSistem(w http.ResponseWriter, r *http.Request) {
|
||||
h.renderujTemplate(w, "podesavanja_sistem", podaci)
|
||||
}
|
||||
|
||||
// PromeniTemu menja aktivnu temu i vraća na prethodnu stranicu
|
||||
// PromeniTemu menja aktivnu temu i vraća na prethodnu stranicu (stara GET ruta, zadržana za kompatibilnost)
|
||||
func (h *Handler) PromeniTemu(w http.ResponseWriter, r *http.Request) {
|
||||
tema := chi.URLParam(r, "tema")
|
||||
|
||||
// proveravamo da li je validna tema
|
||||
validne := map[string]bool{
|
||||
"tamna": true, "svetla": true,
|
||||
}
|
||||
validne := map[string]bool{"tamna": true, "svetla": true}
|
||||
if !validne[tema] {
|
||||
http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "tema", tema); err != nil {
|
||||
http.Error(w, "Greška pri promeni teme", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
_ = ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "globalna_tema", tema)
|
||||
|
||||
// vraćamo se na stranicu sa koje je kliknut dugmić
|
||||
referer := r.Header.Get("Referer")
|
||||
if referer == "" {
|
||||
referer = "/dashboard"
|
||||
}
|
||||
http.Redirect(w, r, referer, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// PromeniGlobalnuTemu prima POST sa poljem "tema" i čuva u globalna_tema u podešavanjima
|
||||
func (h *Handler) PromeniGlobalnuTemu(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
tema := r.FormValue("tema")
|
||||
validne := map[string]bool{"tamna": true, "svetla": true}
|
||||
if !validne[tema] {
|
||||
tema = "tamna"
|
||||
}
|
||||
|
||||
if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "globalna_tema", tema); err != nil {
|
||||
http.Error(w, "Greška pri promeni teme", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
referer := r.Header.Get("Referer")
|
||||
if referer == "" {
|
||||
referer = "/dashboard"
|
||||
}
|
||||
http.Redirect(w, r, referer, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// PrimeniGlobalnuTemu čuva globalnu temu i resetuje lokalne teme svih korisnika
|
||||
func (h *Handler) PrimeniGlobalnuTemu(w http.ResponseWriter, r *http.Request) {
|
||||
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "tema.globalno") {
|
||||
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
tema := r.FormValue("globalna_tema")
|
||||
validne := map[string]bool{"tamna": true, "svetla": true}
|
||||
if !validne[tema] {
|
||||
tema = "tamna"
|
||||
}
|
||||
|
||||
if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "globalna_tema", tema); err != nil {
|
||||
http.Error(w, "Greška pri čuvanju teme", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// poništi lokalne teme svih korisnika — tema se primenjuje globalno
|
||||
if _, err := h.DB.ExecContext(r.Context(),
|
||||
"UPDATE korisnici SET koristi_lokalnu_temu = 0, lokalna_tema = ''",
|
||||
); err != nil {
|
||||
log.Printf("PrimeniGlobalnuTemu: reset lokalnih tema: %v", err)
|
||||
}
|
||||
|
||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Globalna tema primenjena na sve korisnike.")
|
||||
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
@@ -3,16 +3,19 @@ package handler
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"ntech/internal/db"
|
||||
"ntech/internal/db/sqlite"
|
||||
"ntech/internal/middleware"
|
||||
"ntech/internal/model"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
|
||||
// podaciPodsetnici su podaci za stranicu sa listom podsetnika
|
||||
type podaciPodsetnici struct {
|
||||
model.PodaciStranice
|
||||
@@ -28,10 +31,13 @@ type podaciPodsetnikForma struct {
|
||||
Podsetnik model.Podsetnik
|
||||
Greska string
|
||||
Izmena bool
|
||||
Korisnici []model.Korisnik
|
||||
}
|
||||
|
||||
// Podsetnici renderuje listu podsetnika
|
||||
func (h *Handler) Podsetnici(w http.ResponseWriter, r *http.Request) {
|
||||
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||
|
||||
podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
|
||||
@@ -40,7 +46,13 @@ func (h *Handler) Podsetnici(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
samoAktivni := r.URL.Query().Get("samo_aktivni") == "1"
|
||||
|
||||
lista, err := h.PodsetniciFRepo.Lista(r.Context(), db.PodsetnikFilter{SamoAktivni: samoAktivni})
|
||||
// radnik vidi samo podsetnik koji su mu dodeljeni
|
||||
filter := db.PodsetnikFilter{SamoAktivni: samoAktivni}
|
||||
if k.Uloga == "radnik" {
|
||||
filter.KorisnikID = &k.ID
|
||||
}
|
||||
|
||||
lista, err := h.PodsetniciFRepo.Lista(r.Context(), filter)
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri učitavanju podsetnika", http.StatusInternalServerError)
|
||||
return
|
||||
@@ -63,6 +75,8 @@ func (h *Handler) Podsetnici(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// NoviPodsetnik prikazuje praznu formu za unos novog podsetnika
|
||||
func (h *Handler) NoviPodsetnik(w http.ResponseWriter, r *http.Request) {
|
||||
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||
|
||||
podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
|
||||
@@ -73,46 +87,55 @@ func (h *Handler) NoviPodsetnik(w http.ResponseWriter, r *http.Request) {
|
||||
ps.Stranica = "podsetnici"
|
||||
ps.NaslovStranice = "Novi podsetnik"
|
||||
|
||||
var korisnici []model.Korisnik
|
||||
if k.Uloga == "admin" || k.Uloga == "superadmin" {
|
||||
korisnici, _ = h.KorisniciRepo.Lista(r.Context())
|
||||
}
|
||||
|
||||
h.renderujFormuPodsetnika(w, podaciPodsetnikForma{
|
||||
PodaciStranice: ps,
|
||||
Izmena: false,
|
||||
Korisnici: korisnici,
|
||||
})
|
||||
}
|
||||
|
||||
// SacuvajPodsetnik prima POST formu i upisuje novi podsetnik u bazu
|
||||
func (h *Handler) SacuvajPodsetnik(w http.ResponseWriter, r *http.Request) {
|
||||
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
podsetnik, greska := parseFormuPodsetnika(r)
|
||||
if greska != "" {
|
||||
podsetnik, greska := parseFormuPodsetnika(r, k)
|
||||
|
||||
prikaziGresku := func(poruka string) {
|
||||
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||
ps := h.popuniPodaciStranice(r, podesavanja)
|
||||
ps.Stranica = "podsetnici"
|
||||
ps.NaslovStranice = "Novi podsetnik"
|
||||
var korisnici []model.Korisnik
|
||||
if k.Uloga == "admin" || k.Uloga == "superadmin" {
|
||||
korisnici, _ = h.KorisniciRepo.Lista(r.Context())
|
||||
}
|
||||
h.renderujFormuPodsetnika(w, podaciPodsetnikForma{
|
||||
PodaciStranice: ps,
|
||||
Podsetnik: podsetnik,
|
||||
Greska: greska,
|
||||
Greska: poruka,
|
||||
Izmena: false,
|
||||
Korisnici: korisnici,
|
||||
})
|
||||
}
|
||||
|
||||
if greska != "" {
|
||||
prikaziGresku(greska)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := h.PodsetniciFRepo.Kreiraj(r.Context(), &podsetnik); err != nil {
|
||||
log.Printf("greška pri čuvanju podsetnika: %v", err)
|
||||
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||
ps := h.popuniPodaciStranice(r, podesavanja)
|
||||
ps.Stranica = "podsetnici"
|
||||
ps.NaslovStranice = "Novi podsetnik"
|
||||
h.renderujFormuPodsetnika(w, podaciPodsetnikForma{
|
||||
PodaciStranice: ps,
|
||||
Podsetnik: podsetnik,
|
||||
Greska: "Došlo je do greške pri čuvanju. Pokušajte ponovo.",
|
||||
Izmena: false,
|
||||
})
|
||||
prikaziGresku("Došlo je do greške pri čuvanju. Pokušajte ponovo.")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -121,6 +144,8 @@ func (h *Handler) SacuvajPodsetnik(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// IzmeniPodsetnik učitava podsetnik po ID-u i prikazuje popunjenu formu za izmenu
|
||||
func (h *Handler) IzmeniPodsetnik(w http.ResponseWriter, r *http.Request) {
|
||||
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||
|
||||
id, err := parseID(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
http.Error(w, "Neispravan ID podsetnika", http.StatusBadRequest)
|
||||
@@ -143,15 +168,23 @@ func (h *Handler) IzmeniPodsetnik(w http.ResponseWriter, r *http.Request) {
|
||||
ps.Stranica = "podsetnici"
|
||||
ps.NaslovStranice = "Izmeni podsetnik"
|
||||
|
||||
var korisnici []model.Korisnik
|
||||
if k.Uloga == "admin" || k.Uloga == "superadmin" {
|
||||
korisnici, _ = h.KorisniciRepo.Lista(r.Context())
|
||||
}
|
||||
|
||||
h.renderujFormuPodsetnika(w, podaciPodsetnikForma{
|
||||
PodaciStranice: ps,
|
||||
Podsetnik: *podsetnik,
|
||||
Izmena: true,
|
||||
Korisnici: korisnici,
|
||||
})
|
||||
}
|
||||
|
||||
// SacuvajIzmenePodsetnika prima POST formu i ažurira postojeći podsetnik u bazi
|
||||
func (h *Handler) SacuvajIzmenePodsetnika(w http.ResponseWriter, r *http.Request) {
|
||||
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||
|
||||
id, err := parseID(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
http.Error(w, "Neispravan ID podsetnika", http.StatusBadRequest)
|
||||
@@ -163,34 +196,35 @@ func (h *Handler) SacuvajIzmenePodsetnika(w http.ResponseWriter, r *http.Request
|
||||
return
|
||||
}
|
||||
|
||||
podsetnik, greska := parseFormuPodsetnika(r)
|
||||
podsetnik, greska := parseFormuPodsetnika(r, k)
|
||||
podsetnik.ID = id
|
||||
if greska != "" {
|
||||
|
||||
prikaziGresku := func(poruka string) {
|
||||
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||
ps := h.popuniPodaciStranice(r, podesavanja)
|
||||
ps.Stranica = "podsetnici"
|
||||
ps.NaslovStranice = "Izmeni podsetnik"
|
||||
var korisnici []model.Korisnik
|
||||
if k.Uloga == "admin" || k.Uloga == "superadmin" {
|
||||
korisnici, _ = h.KorisniciRepo.Lista(r.Context())
|
||||
}
|
||||
h.renderujFormuPodsetnika(w, podaciPodsetnikForma{
|
||||
PodaciStranice: ps,
|
||||
Podsetnik: podsetnik,
|
||||
Greska: greska,
|
||||
Greska: poruka,
|
||||
Izmena: true,
|
||||
Korisnici: korisnici,
|
||||
})
|
||||
}
|
||||
|
||||
if greska != "" {
|
||||
prikaziGresku(greska)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.PodsetniciFRepo.Izmeni(r.Context(), &podsetnik); err != nil {
|
||||
log.Printf("greška pri čuvanju izmene podsetnika: %v", err)
|
||||
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||
ps := h.popuniPodaciStranice(r, podesavanja)
|
||||
ps.Stranica = "podsetnici"
|
||||
ps.NaslovStranice = "Izmeni podsetnik"
|
||||
h.renderujFormuPodsetnika(w, podaciPodsetnikForma{
|
||||
PodaciStranice: ps,
|
||||
Podsetnik: podsetnik,
|
||||
Greska: "Došlo je do greške pri čuvanju. Pokušajte ponovo.",
|
||||
Izmena: true,
|
||||
})
|
||||
prikaziGresku("Došlo je do greške pri čuvanju. Pokušajte ponovo.")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -237,7 +271,7 @@ func (h *Handler) ObrisiPodsetnik(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// parseFormuPodsetnika čita polja iz HTTP forme, validira ih i vraća model i eventualnu grešku
|
||||
func parseFormuPodsetnika(r *http.Request) (model.Podsetnik, string) {
|
||||
func parseFormuPodsetnika(r *http.Request, k *model.Korisnik) (model.Podsetnik, string) {
|
||||
naslov := strings.TrimSpace(r.FormValue("naslov"))
|
||||
if naslov == "" {
|
||||
return model.Podsetnik{}, "Naslov je obavezan."
|
||||
@@ -253,12 +287,26 @@ func parseFormuPodsetnika(r *http.Request) (model.Podsetnik, string) {
|
||||
return model.Podsetnik{Naslov: naslov}, "Datum podsećanja nije u ispravnom formatu."
|
||||
}
|
||||
|
||||
return model.Podsetnik{
|
||||
p := model.Podsetnik{
|
||||
Naslov: naslov,
|
||||
Napomena: strings.TrimSpace(r.FormValue("napomena")),
|
||||
DatumPodsecanja: datum,
|
||||
Tip: model.TipOpsti,
|
||||
}, ""
|
||||
}
|
||||
|
||||
// admin/superadmin mogu dodeliti podsetnik drugom korisniku
|
||||
if k.Uloga == "admin" || k.Uloga == "superadmin" {
|
||||
if kidStr := strings.TrimSpace(r.FormValue("korisnik_id")); kidStr != "" {
|
||||
if kid, err := strconv.ParseInt(kidStr, 10, 64); err == nil && kid > 0 {
|
||||
p.KorisnikID = &kid
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// radnik dobija podsetnik dodeljen sebi
|
||||
p.KorisnikID = &k.ID
|
||||
}
|
||||
|
||||
return p, ""
|
||||
}
|
||||
|
||||
// renderujFormuPodsetnika renderuje HTML šablon forme za unos ili izmenu podsetnika
|
||||
|
||||
@@ -376,6 +376,26 @@ func parseFormuProdaje(r *http.Request) (model.ProdajniNalog, []model.StavkaProd
|
||||
return nalog, stavke, ""
|
||||
}
|
||||
|
||||
// StornoProdaje stornira prodajni nalog: vraća artikle na stanje i briše nalog
|
||||
func (h *Handler) StornoProdaje(w http.ResponseWriter, r *http.Request) {
|
||||
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "prodaja.storno") {
|
||||
http.Error(w, "Nemate dozvolu za storniranje prodaje.", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
id, err := parseID(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
http.Error(w, "Neispravan ID naloga", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := h.ProdajaRepo.Obrisi(r.Context(), id); err != nil {
|
||||
http.Error(w, "Greška pri storniranju naloga", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Prodajni nalog je storniran.")
|
||||
http.Redirect(w, r, "/prodaja", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// renderujFormuProdaje renderuje HTML šablon forme za unos nove prodaje
|
||||
func (h *Handler) renderujFormuProdaje(w http.ResponseWriter, podaci PodaciFormeProdaje) {
|
||||
h.renderujTemplate(w, "prodaja_forma", podaci)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -45,6 +45,10 @@ var sveAkcije = []string{
|
||||
"backup.pokreni",
|
||||
"podesavanja.login_pozadina",
|
||||
"podesavanja.app_pozadina",
|
||||
"tema.globalno",
|
||||
"tema.lokalno",
|
||||
"dashboard.prihod",
|
||||
"prodaja.storno",
|
||||
}
|
||||
|
||||
// SveAkcije vraća listu svih poznatih akcija — koristi se pri inicijalizaciji baze i resetu
|
||||
@@ -81,7 +85,7 @@ func ImaDozvolu(uloga, akcija string) bool {
|
||||
"servis.izmeni", "servis.obrisi":
|
||||
return true
|
||||
// prodaja
|
||||
case "prodaja.pregled", "prodaja.dodaj", "prodaja.obrisi":
|
||||
case "prodaja.pregled", "prodaja.dodaj", "prodaja.obrisi", "prodaja.storno":
|
||||
return true
|
||||
// klijent
|
||||
case "klijent.pregled", "klijent.dodaj",
|
||||
@@ -105,27 +109,30 @@ func ImaDozvolu(uloga, akcija string) bool {
|
||||
// pozadinske slike
|
||||
case "podesavanja.login_pozadina", "podesavanja.app_pozadina":
|
||||
return true
|
||||
// teme
|
||||
case "tema.globalno", "tema.lokalno":
|
||||
return true
|
||||
// dashboard — prihod samo admin+
|
||||
case "dashboard.prihod":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
||||
case "radnik":
|
||||
switch akcija {
|
||||
// artikal — bez brisanja i premeštanja
|
||||
case "artikal.pregled", "artikal.dodaj", "artikal.izmeni":
|
||||
// artikal — samo pregled
|
||||
case "artikal.pregled":
|
||||
return true
|
||||
// kategorija — samo pregled
|
||||
case "kategorija.pregled":
|
||||
return true
|
||||
// nabavka — bez brisanja
|
||||
case "nabavka.pregled", "nabavka.dodaj":
|
||||
return true
|
||||
// dobavljač — bez brisanja
|
||||
case "dobavljac.pregled", "dobavljac.dodaj", "dobavljac.izmeni":
|
||||
return true
|
||||
// servis — bez brisanja
|
||||
case "servis.pregled", "servis.dodaj", "servis.izmeni":
|
||||
return true
|
||||
// prodaja — bez brisanja
|
||||
// prodaja — bez brisanja i storna
|
||||
case "prodaja.pregled", "prodaja.dodaj":
|
||||
return true
|
||||
// klijent — bez brisanja
|
||||
@@ -135,6 +142,9 @@ func ImaDozvolu(uloga, akcija string) bool {
|
||||
case "podsetnik.pregled", "podsetnik.dodaj",
|
||||
"podsetnik.izmeni", "podsetnik.obrisi":
|
||||
return true
|
||||
// lokalna tema
|
||||
case "tema.lokalno":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -10,7 +10,13 @@ type Korisnik struct {
|
||||
Uloga string // "superadmin" | "admin" | "radnik"
|
||||
Aktivan bool
|
||||
TotpTajna string
|
||||
LokalnaTema string // "tamna" | "svetla" | ""
|
||||
KoristiLokalnuTemu bool
|
||||
DatumKreiranja time.Time
|
||||
LokalnaPozadina string
|
||||
LokalnaPozadinaOpacity string
|
||||
LokalnaPozadinaBlur string
|
||||
LokalnaPozadinaBlurPozadine string
|
||||
}
|
||||
|
||||
// Sesija predstavlja aktivnu sesiju prijavljenog korisnika
|
||||
|
||||
@@ -14,9 +14,18 @@ type Podsetnik struct {
|
||||
Zavrseno bool
|
||||
Tip string
|
||||
DatumUnosa time.Time
|
||||
KorisnikID *int64 // ako nil — podsetnik nije dodeljen konkretnom korisniku
|
||||
}
|
||||
|
||||
// JePrekoracen vraća true ako datum podsećanja je prošao a podsetnik nije završen
|
||||
func (p Podsetnik) JePrekoracen() bool {
|
||||
return !p.Zavrseno && p.DatumPodsecanja.Before(time.Now())
|
||||
}
|
||||
|
||||
// KorisnikIDVal vraća vrednost KorisnikID pokazivača ili 0 ako je nil — za poređenje u šablonima
|
||||
func (p Podsetnik) KorisnikIDVal() int64 {
|
||||
if p.KorisnikID == nil {
|
||||
return 0
|
||||
}
|
||||
return *p.KorisnikID
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -0,0 +1,3 @@
|
||||
INSERT INTO podesavanja (kljuc, vrednost)
|
||||
VALUES ('tema_pre_slike', '')
|
||||
ON CONFLICT(kljuc) DO NOTHING;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE podsetnici ADD COLUMN korisnik_id INTEGER REFERENCES korisnici(id) ON DELETE SET NULL;
|
||||
@@ -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';
|
||||
@@ -39,11 +39,13 @@
|
||||
<span class="nav-tooltip">Magacin</span>
|
||||
</a>
|
||||
|
||||
{{if index .Dozvole "nabavka.pregled"}}
|
||||
<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>
|
||||
<span>Nabavke</span>
|
||||
<span class="nav-tooltip">Nabavke</span>
|
||||
</a>
|
||||
{{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>
|
||||
@@ -86,11 +88,41 @@
|
||||
<div class="nav-separator"></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}}">
|
||||
<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-tooltip">Moj profil</span>
|
||||
</a>
|
||||
{{end}}
|
||||
|
||||
{{if or (eq .KorisnikUloga "superadmin") (eq .KorisnikUloga "admin")}}
|
||||
<a href="/admin/korisnici" class="nav-stavka {{if eq .Stranica "admin"}}aktivan{{end}}">
|
||||
@@ -108,6 +140,7 @@
|
||||
</a>
|
||||
{{end}}
|
||||
|
||||
{{if ne .KorisnikUloga "radnik"}}
|
||||
<div class="nav-separator"></div>
|
||||
<div class="nav-oznaka">Sistem</div>
|
||||
|
||||
@@ -145,6 +178,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}{{/* kraj ne radnik */}}
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-dno">
|
||||
|
||||
@@ -12,10 +12,22 @@
|
||||
|
||||
<span class="topbar-naslov">{{.NaslovStranice}}</span>
|
||||
|
||||
{{if not .AppPozadina}}
|
||||
<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>
|
||||
<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>
|
||||
<form method="POST" action="/podesavanja/tema" style="margin:0;padding:0;">
|
||||
<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>
|
||||
{{end}}
|
||||
|
||||
<div style="position:relative;" id="avatar-wrapper">
|
||||
<div class="avatar-korisnik" id="avatar-dugme" style="cursor:pointer;" title="{{.Korisnik}}">
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
.animiraj:nth-child(2) { animation-delay: 0.16s; }
|
||||
.animiraj:nth-child(3) { animation-delay: 0.22s; }
|
||||
.animiraj:nth-child(4) { animation-delay: 0.28s; }
|
||||
.animiraj:nth-child(5) { animation-delay: 0.34s; }
|
||||
</style>
|
||||
{{end}}
|
||||
|
||||
@@ -115,5 +116,18 @@
|
||||
|
||||
</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>
|
||||
{{end}}
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
<div style="font-size:12px;color:var(--tekst-sporedni);margin-top:4px;">Aktivnih servisa</div>
|
||||
</div>
|
||||
|
||||
{{if index .Dozvole "dashboard.prihod"}}
|
||||
<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;">
|
||||
<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:12px;color:var(--tekst-sporedni);margin-top:4px;">Prihod ovog meseca</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<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;">
|
||||
|
||||
@@ -46,8 +46,12 @@
|
||||
|
||||
<!-- dugmad -->
|
||||
<div style="display:flex;gap:10px;flex-wrap:wrap;">
|
||||
{{if index .Dozvole "artikal.dodaj"}}
|
||||
<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>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<!-- pretraga i filteri -->
|
||||
@@ -104,9 +108,11 @@
|
||||
</td>
|
||||
<td style="padding:12px 16px;text-align:center;">
|
||||
<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">
|
||||
Izmeni
|
||||
</a>
|
||||
{{end}}
|
||||
{{if index $.Dozvole "artikal.obrisi"}}
|
||||
<a href="/magacin/obrisi/{{.ID}}" class="btn-obrisi-malo"
|
||||
data-potvrda="Da li ste sigurni da želite da obrišete ovaj artikal?">
|
||||
@@ -140,7 +146,9 @@
|
||||
{{end}}
|
||||
</div>
|
||||
<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>
|
||||
{{end}}
|
||||
{{if index $.Dozvole "artikal.obrisi"}}
|
||||
<a href="/magacin/obrisi/{{.ID}}" class="btn-obrisi-malo"
|
||||
data-potvrda="Da li ste sigurni da želite da obrišete ovaj artikal?">
|
||||
|
||||
@@ -442,13 +442,13 @@
|
||||
<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;">
|
||||
<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}}">
|
||||
<input type="radio" name="tema" value="tamna" {{if eq .Tema "tamna"}}checked{{end}} style="accent-color:var(--sb-akcent);">
|
||||
<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="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>
|
||||
<span style="font-size:13px;color:var(--tekst-glavni);">Tamna</span>
|
||||
</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}}">
|
||||
<input type="radio" name="tema" value="svetla" {{if eq .Tema "svetla"}}checked{{end}} style="accent-color:var(--sb-akcent);">
|
||||
<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="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>
|
||||
<span style="font-size:13px;color:var(--tekst-glavni);">Svetla</span>
|
||||
</label>
|
||||
|
||||
@@ -136,6 +136,7 @@
|
||||
|
||||
</div>
|
||||
|
||||
{{if index .Dozvole "podesavanja.login_pozadina"}}
|
||||
<!-- pozadinska slika login stranice -->
|
||||
<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);">
|
||||
@@ -223,41 +224,52 @@
|
||||
|
||||
</div><!-- /x-data -->
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<!-- tema -->
|
||||
{{if index .Dozvole "tema.globalno"}}
|
||||
<!-- globalna tema -->
|
||||
<form method="POST" action="/podesavanja/sacuvaj">
|
||||
<input type="hidden" name="_next" value="/admin/podesavanja/izgled">
|
||||
<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"><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 style="font-size:13px;color:var(--tekst-sporedni);margin-bottom:12px;">Primenuje se na sve korisnike koji ne koriste svoju lokalnu temu.</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;">
|
||||
<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}}">
|
||||
<input type="radio" name="tema" value="tamna" {{if eq .Tema "tamna"}}checked{{end}} style="accent-color:var(--sb-akcent);">
|
||||
<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="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>
|
||||
<span style="font-size:13px;color:var(--tekst-glavni);">Tamna</span>
|
||||
</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}}">
|
||||
<input type="radio" name="tema" value="svetla" {{if eq .Tema "svetla"}}checked{{end}} style="accent-color:var(--sb-akcent);">
|
||||
<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="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>
|
||||
<span style="font-size:13px;color:var(--tekst-glavni);">Svetla</span>
|
||||
</label>
|
||||
</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"
|
||||
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'">
|
||||
Sačuvaj izgled
|
||||
Sačuvaj
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{{end}}
|
||||
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
@@ -57,6 +57,23 @@
|
||||
style="width:100%;resize:vertical;">{{.Podsetnik.Napomena}}</textarea>
|
||||
</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;">
|
||||
<a href="/podsetnici" class="btn-sekundarno">Odustani</a>
|
||||
<button type="submit" class="btn-primarno">
|
||||
|
||||
@@ -89,9 +89,18 @@
|
||||
{{printf "%.2f" .Ukupno}} din
|
||||
</td>
|
||||
<td style="padding:12px 16px;text-align:center;">
|
||||
<a href="/prodaja/{{.ID}}" class="btn-primarno-malo">
|
||||
Detalji
|
||||
</a>
|
||||
<div style="display:flex;align-items:center;justify-content:center;gap:8px;">
|
||||
<a href="/prodaja/{{.ID}}" class="btn-primarno-malo">Detalji</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>
|
||||
</tr>
|
||||
{{else}}
|
||||
|
||||
@@ -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}}
|
||||
@@ -36,6 +36,11 @@
|
||||
filter: blur({{.AppPozadinaBlurPozadine}}px);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
animation: appBgFadeIn 0.35s ease forwards;
|
||||
}
|
||||
@keyframes appBgFadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
.app-overlay {
|
||||
position: fixed;
|
||||
@@ -122,19 +127,6 @@
|
||||
meni.style.background = 'var(--kartica)';
|
||||
});
|
||||
</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}}
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Reference in New Issue
Block a user