diff --git a/cmd/ntech/main.go b/cmd/ntech/main.go index 602c1a6..66a9a5e 100644 --- a/cmd/ntech/main.go +++ b/cmd/ntech/main.go @@ -234,7 +234,7 @@ 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.Post("/profil/tema", h.SacuvajLokalnuTemu) r.Get("/profil/tema", h.ProfilTema) r.Post("/profil/pozadina", h.ProfilOtpremiPozadinu) r.Post("/profil/pozadina/ukloni", h.ProfilUkloniPozadinu) diff --git a/internal/db/sqlite/korisnici.go b/internal/db/sqlite/korisnici.go index 9ccead6..fe6746d 100644 --- a/internal/db/sqlite/korisnici.go +++ b/internal/db/sqlite/korisnici.go @@ -11,6 +11,33 @@ import ( type sqliteKorisniciRepo struct{ db *sql.DB } +// skeniraiKorisnika čita jedan red iz baze i popunjava model.Korisnik +func skeniraiKorisnika(row interface{ Scan(...any) error }) (*model.Korisnik, error) { + k := &model.Korisnik{} + var aktivan, koristiLokalnuTemu int + var lokalnaTema sql.NullString + var lokalnaPozadina, lokalnaPozadinaOpacity, lokalnaPozadinaBlur, lokalnaPozadinaBlurPozadine, lokalnaPozadinaGlassOpacity sql.NullString + var datumKreiranja time.Time + if err := row.Scan( + &k.ID, &k.KorisnickoIme, &k.LozinkaHash, &k.Uloga, &aktivan, &k.TotpTajna, + &lokalnaTema, &koristiLokalnuTemu, &datumKreiranja, + &lokalnaPozadina, &lokalnaPozadinaOpacity, &lokalnaPozadinaBlur, + &lokalnaPozadinaBlurPozadine, &lokalnaPozadinaGlassOpacity, + ); err != nil { + return nil, 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 + k.LokalnaPozadinaGlassOpacity = lokalnaPozadinaGlassOpacity.String + return k, nil +} + // NoviKorisniciRepo kreira SQLite implementaciju KorisniciRepository func NoviKorisniciRepo(db *sql.DB) *sqliteKorisniciRepo { return &sqliteKorisniciRepo{db: db} @@ -28,67 +55,32 @@ 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, koristiLokalnuTemu int - var totpTajna, lokalnaTema sql.NullString - var lokalnaPozadina, lokalnaPozadinaOpacity, lokalnaPozadinaBlur, lokalnaPozadinaBlurPozadine, lokalnaPozadinaGlassOpacity sql.NullString - var datumKreiranja time.Time - err := r.db.QueryRowContext(ctx, + row := r.db.QueryRowContext(ctx, `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'), COALESCE(lokalna_pozadina_glass_opacity, '10') - FROM korisnici WHERE korisnicko_ime = ?`, korisnickoIme). - Scan(&k.ID, &k.KorisnickoIme, &k.LozinkaHash, &k.Uloga, &aktivan, &totpTajna, - &lokalnaTema, &koristiLokalnuTemu, &datumKreiranja, - &lokalnaPozadina, &lokalnaPozadinaOpacity, &lokalnaPozadinaBlur, &lokalnaPozadinaBlurPozadine, - &lokalnaPozadinaGlassOpacity) + FROM korisnici WHERE korisnicko_ime = ?`, korisnickoIme) + k, err := skeniraiKorisnika(row) 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 - k.LokalnaPozadinaGlassOpacity = lokalnaPozadinaGlassOpacity.String return k, nil } func (r *sqliteKorisniciRepo) DohvatiPoID(ctx context.Context, id int64) (*model.Korisnik, error) { - k := &model.Korisnik{} - var aktivan, koristiLokalnuTemu int - var lokalnaTema sql.NullString - var lokalnaPozadina, lokalnaPozadinaOpacity, lokalnaPozadinaBlur, lokalnaPozadinaBlurPozadine, lokalnaPozadinaGlassOpacity sql.NullString - var datumKreiranja time.Time - err := r.db.QueryRowContext(ctx, + row := r.db.QueryRowContext(ctx, `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'), COALESCE(lokalna_pozadina_glass_opacity, '10') - FROM korisnici WHERE id = ?`, id). - Scan(&k.ID, &k.KorisnickoIme, &k.LozinkaHash, &k.Uloga, &aktivan, &k.TotpTajna, - &lokalnaTema, &koristiLokalnuTemu, &datumKreiranja, - &lokalnaPozadina, &lokalnaPozadinaOpacity, &lokalnaPozadinaBlur, &lokalnaPozadinaBlurPozadine, - &lokalnaPozadinaGlassOpacity) + FROM korisnici WHERE id = ?`, id) + k, err := skeniraiKorisnika(row) 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 - k.LokalnaPozadinaGlassOpacity = lokalnaPozadinaGlassOpacity.String return k, nil } diff --git a/internal/handler/admin.go b/internal/handler/admin.go index 5efb5f8..61e4ad2 100644 --- a/internal/handler/admin.go +++ b/internal/handler/admin.go @@ -305,44 +305,6 @@ func (h *Handler) AdminProfil(w http.ResponseWriter, r *http.Request) { }) } -// 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) { diff --git a/internal/handler/dashboard.go b/internal/handler/dashboard.go index 9fec234..ba9f61c 100644 --- a/internal/handler/dashboard.go +++ b/internal/handler/dashboard.go @@ -70,7 +70,7 @@ func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) { if korisnikDash.Uloga == "radnik" { korisnikFilter.KorisnikID = &korisnikDash.ID } - if n, err := h.PodsetniciFRepo.BrojAktivnih(ctx, korisnikFilter); err != nil { + if n, err := h.PodsetnikRepo.BrojAktivnih(ctx, korisnikFilter); err != nil { log.Printf("dashboard: aktivni podsetnici: %v", err) } else { aktivniPodsetnici = n diff --git a/internal/handler/handler.go b/internal/handler/handler.go index 83906d0..5d9dac1 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -25,7 +25,7 @@ type Handler struct { ProdajaRepo db.ProdajaRepository KorisniciRepo db.KorisniciRepository SesijeRepo db.SesijeRepository - PodsetniciFRepo db.PodsetnikRepository + PodsetnikRepo db.PodsetnikRepository PokusajiRepo db.PokusajiPrijaveRepository LoginIstorijsaRepo db.LoginIstorijsaRepository DozvoleRepo db.DozvoleRepository @@ -47,15 +47,15 @@ func Novi(baza *sql.DB) *Handler { ProdajaRepo: sqlite.NoviProdajaRepo(baza), KorisniciRepo: sqlite.NoviKorisniciRepo(baza), SesijeRepo: sqlite.NoviSesijeRepo(baza), - PodsetniciFRepo: sqlite.NoviPodsetnikRepo(baza), + PodsetnikRepo: sqlite.NoviPodsetnikRepo(baza), PokusajiRepo: sqlite.NoviPokusajiPrijaveRepo(baza), LoginIstorijsaRepo: sqlite.NoviLoginIstorijsaRepo(baza), DozvoleRepo: sqlite.NoviDozvoleRepo(baza, middleware.ImaDozvolu, middleware.SveAkcije()), } } -// reinicijalzijRepozitorijume zamenjuje sve repozitorijume posle obnove baze -func (h *Handler) reinicijalzijRepozitorijume(novaDB *sql.DB) { +// reinicijalizujRepozitorijume zamenjuje sve repozitorijume posle obnove baze +func (h *Handler) reinicijalizujRepozitorijume(novaDB *sql.DB) { h.DB = novaDB h.Artikli = sqlite.NoviArtikalRepo(novaDB) h.KategorijeRepo = sqlite.NovaKategorijaRepo(novaDB) @@ -66,7 +66,7 @@ func (h *Handler) reinicijalzijRepozitorijume(novaDB *sql.DB) { h.ProdajaRepo = sqlite.NoviProdajaRepo(novaDB) h.KorisniciRepo = sqlite.NoviKorisniciRepo(novaDB) h.SesijeRepo = sqlite.NoviSesijeRepo(novaDB) - h.PodsetniciFRepo = sqlite.NoviPodsetnikRepo(novaDB) + h.PodsetnikRepo = sqlite.NoviPodsetnikRepo(novaDB) h.PokusajiRepo = sqlite.NoviPokusajiPrijaveRepo(novaDB) h.LoginIstorijsaRepo = sqlite.NoviLoginIstorijsaRepo(novaDB) h.DozvoleRepo = sqlite.NoviDozvoleRepo(novaDB, middleware.ImaDozvolu, middleware.SveAkcije()) diff --git a/internal/handler/podesavanja.go b/internal/handler/podesavanja.go index 30ee89e..1c3c585 100644 --- a/internal/handler/podesavanja.go +++ b/internal/handler/podesavanja.go @@ -82,27 +82,11 @@ func (h *Handler) Podesavanja(w http.ResponseWriter, r *http.Request) { Verzija: h.Verzija, LogoGreska: r.URL.Query().Get("logo_greska"), Backupi: ucitajListuBackupa(), - LoginPozadina: podesavanja["login_pozadina"], - LoginPozadinaOpacity: func() string { - v := podesavanja["login_pozadina_opacity"] - if v == "" { return "50" } - return v - }(), - LoginPozadinaBlurPozadine: func() string { - v := podesavanja["login_pozadina_blur_pozadine"] - if v == "" { return "0" } - return v - }(), - LoginPozadinaBlurKartice: func() string { - v := podesavanja["login_pozadina_blur_kartice"] - if v == "" { return "12" } - return v - }(), - LoginPozadinaZatamnjenjeKartice: func() string { - v := podesavanja["login_pozadina_zatamnjenje_kartice"] - if v == "" { return "0" } - return v - }(), + LoginPozadina: podesavanja["login_pozadina"], + LoginPozadinaOpacity: vrednostIliDefault(podesavanja, "login_pozadina_opacity", "50"), + LoginPozadinaBlurPozadine: vrednostIliDefault(podesavanja, "login_pozadina_blur_pozadine", "0"), + LoginPozadinaBlurKartice: vrednostIliDefault(podesavanja, "login_pozadina_blur_kartice", "12"), + LoginPozadinaZatamnjenjeKartice: vrednostIliDefault(podesavanja, "login_pozadina_zatamnjenje_kartice", "0"), } h.renderujTemplate(w, "podesavanja", podaci) @@ -136,6 +120,14 @@ func ucitajListuBackupa() []BackupInfo { return lista } +// vrednostIliDefault vraća vrednost iz mape ako postoji i nije prazan string, inače vraća podrazumevanu vrednost +func vrednostIliDefault(m map[string]string, kljuc, podrazumevano string) string { + if v := m[kljuc]; v != "" { + return v + } + return podrazumevano +} + // VratiBackup zamenjuje trenutnu bazu sa izabranim backup fajlom func (h *Handler) VratiBackup(w http.ResponseWriter, r *http.Request) { k := middleware.KorisnikIzKonteksta(r.Context()) @@ -197,7 +189,7 @@ func (h *Handler) VratiBackup(w http.ResponseWriter, r *http.Request) { return } - h.reinicijalzijRepozitorijume(novaDB) + h.reinicijalizujRepozitorijume(novaDB) log.Printf("Baza uspešno obnovljena iz: %s", ime) http.Redirect(w, r, "/podesavanja?sacuvano=vraceno", http.StatusSeeOther) @@ -594,31 +586,11 @@ func (h *Handler) napuniPodaciPodesavanja(r *http.Request, naslov string) (Podac Verzija: h.Verzija, LogoGreska: r.URL.Query().Get("logo_greska"), Backupi: ucitajListuBackupa(), - LoginPozadina: podesavanja["login_pozadina"], - LoginPozadinaOpacity: func() string { - if v := podesavanja["login_pozadina_opacity"]; v != "" { - return v - } - return "50" - }(), - LoginPozadinaBlurPozadine: func() string { - if v := podesavanja["login_pozadina_blur_pozadine"]; v != "" { - return v - } - return "0" - }(), - LoginPozadinaBlurKartice: func() string { - if v := podesavanja["login_pozadina_blur_kartice"]; v != "" { - return v - } - return "12" - }(), - LoginPozadinaZatamnjenjeKartice: func() string { - if v := podesavanja["login_pozadina_zatamnjenje_kartice"]; v != "" { - return v - } - return "0" - }(), + LoginPozadina: podesavanja["login_pozadina"], + LoginPozadinaOpacity: vrednostIliDefault(podesavanja, "login_pozadina_opacity", "50"), + LoginPozadinaBlurPozadine: vrednostIliDefault(podesavanja, "login_pozadina_blur_pozadine", "0"), + LoginPozadinaBlurKartice: vrednostIliDefault(podesavanja, "login_pozadina_blur_kartice", "12"), + LoginPozadinaZatamnjenjeKartice: vrednostIliDefault(podesavanja, "login_pozadina_zatamnjenje_kartice", "0"), }, nil } diff --git a/internal/handler/podsetnici.go b/internal/handler/podsetnici.go index defdb41..ea18812 100644 --- a/internal/handler/podsetnici.go +++ b/internal/handler/podsetnici.go @@ -52,7 +52,7 @@ func (h *Handler) Podsetnici(w http.ResponseWriter, r *http.Request) { filter.KorisnikID = &k.ID } - lista, err := h.PodsetniciFRepo.Lista(r.Context(), filter) + lista, err := h.PodsetnikRepo.Lista(r.Context(), filter) if err != nil { http.Error(w, "Greška pri učitavanju podsetnika", http.StatusInternalServerError) return @@ -88,7 +88,7 @@ func (h *Handler) NoviPodsetnik(w http.ResponseWriter, r *http.Request) { ps.NaslovStranice = "Novi podsetnik" var korisnici []model.Korisnik - if k.Uloga == "admin" || k.Uloga == "superadmin" { + if middleware.JeAdmin(k) { korisnici, _ = h.KorisniciRepo.Lista(r.Context()) } @@ -110,32 +110,14 @@ func (h *Handler) SacuvajPodsetnik(w http.ResponseWriter, r *http.Request) { 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: poruka, - Izmena: false, - Korisnici: korisnici, - }) - } - if greska != "" { - prikaziGresku(greska) + h.prikaziGreskuPodsetnika(w, r, k, podsetnik, greska, false) return } - if _, err := h.PodsetniciFRepo.Kreiraj(r.Context(), &podsetnik); err != nil { + if _, err := h.PodsetnikRepo.Kreiraj(r.Context(), &podsetnik); err != nil { log.Printf("greška pri čuvanju podsetnika: %v", err) - prikaziGresku("Došlo je do greške pri čuvanju. Pokušajte ponovo.") + h.prikaziGreskuPodsetnika(w, r, k, podsetnik, "Došlo je do greške pri čuvanju. Pokušajte ponovo.", false) return } @@ -152,7 +134,7 @@ func (h *Handler) IzmeniPodsetnik(w http.ResponseWriter, r *http.Request) { return } - podsetnik, err := h.PodsetniciFRepo.DohvatiID(r.Context(), id) + podsetnik, err := h.PodsetnikRepo.DohvatiID(r.Context(), id) if err != nil { http.Error(w, "Podsetnik nije pronađen", http.StatusNotFound) return @@ -169,7 +151,7 @@ func (h *Handler) IzmeniPodsetnik(w http.ResponseWriter, r *http.Request) { ps.NaslovStranice = "Izmeni podsetnik" var korisnici []model.Korisnik - if k.Uloga == "admin" || k.Uloga == "superadmin" { + if middleware.JeAdmin(k) { korisnici, _ = h.KorisniciRepo.Lista(r.Context()) } @@ -199,32 +181,14 @@ func (h *Handler) SacuvajIzmenePodsetnika(w http.ResponseWriter, r *http.Request podsetnik, greska := parseFormuPodsetnika(r, k) podsetnik.ID = id - 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: poruka, - Izmena: true, - Korisnici: korisnici, - }) - } - if greska != "" { - prikaziGresku(greska) + h.prikaziGreskuPodsetnika(w, r, k, podsetnik, greska, true) return } - if err := h.PodsetniciFRepo.Izmeni(r.Context(), &podsetnik); err != nil { + if err := h.PodsetnikRepo.Izmeni(r.Context(), &podsetnik); err != nil { log.Printf("greška pri čuvanju izmene podsetnika: %v", err) - prikaziGresku("Došlo je do greške pri čuvanju. Pokušajte ponovo.") + h.prikaziGreskuPodsetnika(w, r, k, podsetnik, "Došlo je do greške pri čuvanju. Pokušajte ponovo.", true) return } @@ -240,13 +204,13 @@ func (h *Handler) OznaciPodsetnik(w http.ResponseWriter, r *http.Request) { } // učitamo trenutni status pa ga preokrenemo - podsetnik, err := h.PodsetniciFRepo.DohvatiID(r.Context(), id) + podsetnik, err := h.PodsetnikRepo.DohvatiID(r.Context(), id) if err != nil { http.Error(w, "Podsetnik nije pronađen", http.StatusNotFound) return } - if err := h.PodsetniciFRepo.OznaciZavrsenim(r.Context(), id, !podsetnik.Zavrseno); err != nil { + if err := h.PodsetnikRepo.OznaciZavrsenim(r.Context(), id, !podsetnik.Zavrseno); err != nil { http.Error(w, "Greška pri ažuriranju statusa", http.StatusInternalServerError) return } @@ -262,7 +226,7 @@ func (h *Handler) ObrisiPodsetnik(w http.ResponseWriter, r *http.Request) { return } - if err := h.PodsetniciFRepo.Obrisi(r.Context(), id); err != nil { + if err := h.PodsetnikRepo.Obrisi(r.Context(), id); err != nil { http.Error(w, "Greška pri brisanju podsetnika", http.StatusInternalServerError) return } @@ -295,7 +259,7 @@ func parseFormuPodsetnika(r *http.Request, k *model.Korisnik) (model.Podsetnik, } // admin/superadmin mogu dodeliti podsetnik drugom korisniku - if k.Uloga == "admin" || k.Uloga == "superadmin" { + if middleware.JeAdmin(k) { if kidStr := strings.TrimSpace(r.FormValue("korisnik_id")); kidStr != "" { if kid, err := strconv.ParseInt(kidStr, 10, 64); err == nil && kid > 0 { p.KorisnikID = &kid @@ -309,6 +273,29 @@ func parseFormuPodsetnika(r *http.Request, k *model.Korisnik) (model.Podsetnik, return p, "" } +// prikaziGreskuPodsetnika prikazuje formu podsetnika sa porukom o grešci +func (h *Handler) prikaziGreskuPodsetnika(w http.ResponseWriter, r *http.Request, k *model.Korisnik, podsetnik model.Podsetnik, poruka string, izmena bool) { + podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB) + ps := h.popuniPodaciStranice(r, podesavanja) + ps.Stranica = "podsetnici" + if izmena { + ps.NaslovStranice = "Izmeni podsetnik" + } else { + ps.NaslovStranice = "Novi podsetnik" + } + var korisnici []model.Korisnik + if middleware.JeAdmin(k) { + korisnici, _ = h.KorisniciRepo.Lista(r.Context()) + } + h.renderujFormuPodsetnika(w, podaciPodsetnikForma{ + PodaciStranice: ps, + Podsetnik: podsetnik, + Greska: poruka, + Izmena: izmena, + Korisnici: korisnici, + }) +} + // renderujFormuPodsetnika renderuje HTML šablon forme za unos ili izmenu podsetnika func (h *Handler) renderujFormuPodsetnika(w http.ResponseWriter, podaci podaciPodsetnikForma) { h.renderujTemplate(w, "podsetnik_forma", podaci) diff --git a/internal/handler/profil.go b/internal/handler/profil.go index 5b61d72..8e21138 100644 --- a/internal/handler/profil.go +++ b/internal/handler/profil.go @@ -250,3 +250,42 @@ func (h *Handler) ProfilSacuvajPozadinuStilove(w http.ResponseWriter, r *http.Re middleware.SetFlash(w, r, h.DB, "uspeh", "Podešavanja su sačuvana.") http.Redirect(w, r, "/profil/tema", http.StatusSeeOther) } + +// SacuvajLokalnuTemu čuva korisnikovu lokalnu temu +func (h *Handler) SacuvajLokalnuTemu(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) +} diff --git a/internal/middleware/bezbednost.go b/internal/middleware/bezbednost.go index d27b275..96254c6 100644 --- a/internal/middleware/bezbednost.go +++ b/internal/middleware/bezbednost.go @@ -14,7 +14,8 @@ func BezbednostHeaders() func(http.Handler) http.Handler { h.Set("Content-Security-Policy", "default-src 'self'; "+ "style-src 'self' 'unsafe-inline' https://cdn.tailwindcss.com; "+ - "script-src 'self' 'unsafe-inline' https://cdn.tailwindcss.com https://cdn.jsdelivr.net; "+ + // Alpine.js v3 koristi new Function() interno — unsafe-eval je neophodan; CSP build zahteva značajan refaktoring. + "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.tailwindcss.com https://cdn.jsdelivr.net; "+ "img-src 'self' data: blob:; "+ "font-src 'self'; "+ "connect-src 'self'") diff --git a/web/templates/setup/index.html b/web/templates/setup/index.html index 8fdbbfe..5c8ea83 100644 --- a/web/templates/setup/index.html +++ b/web/templates/setup/index.html @@ -4,142 +4,26 @@ NTech — Konfiguracija diff --git a/web/templates/stranice/admin_dozvole.html b/web/templates/stranice/admin_dozvole.html index d817b6b..fa8addc 100644 --- a/web/templates/stranice/admin_dozvole.html +++ b/web/templates/stranice/admin_dozvole.html @@ -10,16 +10,7 @@ .dozvole-tabela tbody tr:nth-child(4) { animation-delay: 0.16s; } .dozvole-tabela tbody tr:nth-child(5) { animation-delay: 0.20s; } - .matrica-modul td { - padding: 8px 16px; - font-size: 12px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--tekst-sporedni); - background: var(--pozadina); - border-top: 0.5px solid var(--ivica); - } + .matrica-modul td { padding: 8px 16px; font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--tekst-sporedni); background: var(--pozadina); border-top: 0.5px solid var(--ivica); } .matrica-checkbox { text-align: center; padding: 8px 16px; } .matrica-checkbox input[type=checkbox] { width: 16px; height: 16px; cursor: pointer; accent-color: var(--sb-akcent); } diff --git a/web/templates/stranice/admin_korisnici.html b/web/templates/stranice/admin_korisnici.html index 7ee9509..041bdd2 100644 --- a/web/templates/stranice/admin_korisnici.html +++ b/web/templates/stranice/admin_korisnici.html @@ -17,9 +17,7 @@ .korisnici-tabela tbody tr:nth-child(9) { animation-delay: 0.36s; } .korisnici-tabela tbody tr:nth-child(10) { animation-delay: 0.40s; } - .nova-forma-kartica { - animation-delay: 0.30s; - } + .nova-forma-kartica { animation-delay: 0.30s; } {{end}} diff --git a/web/templates/stranice/admin_login_istorija.html b/web/templates/stranice/admin_login_istorija.html index b2a64be..d383c8a 100644 --- a/web/templates/stranice/admin_login_istorija.html +++ b/web/templates/stranice/admin_login_istorija.html @@ -1,86 +1,59 @@ -{{template "base" .}} - -{{define "naslov"}}Istorija prijava — NTech{{end}} - -{{define "dodatni-css"}} +{{template "base" .}} {{define "naslov"}}Istorija prijava — NTech{{end}} {{define "dodatni-css"}} -{{end}} +{{end}} {{define "sadrzaj"}} +
+
+ ← Nazad + Istorija prijava — {{.PrikazKorisnik.KorisnickoIme}} +
-{{define "sadrzaj"}} -
- -
- - ← Nazad - - - Istorija prijava — {{.PrikazKorisnik.KorisnickoIme}} - +
+
+ Poslednjih 50 pokušaja + Vreme prikazano po lokalnom vremenu servera
-
-
- Poslednjih 50 pokušaja - Vreme prikazano po lokalnom vremenu servera -
- - {{if .Istorija}} -
- - - - - - - - - - - - {{range .Istorija}} - - - - - - - - {{end}} - -
Datum i vremeIP adresaPregledačStatusRazlog
- {{.Vreme.Format "02.01.2006. 15:04:05"}} - - {{.IP}} - - {{.UserAgent}} - - {{if .Uspeh}} - Uspeh - {{else}} - Neuspeh - {{end}} - - {{if eq .Razlog "korisnik_ne_postoji"}}Korisnik ne postoji - {{else if eq .Razlog "nalog_neaktivan"}}Nalog neaktivan - {{else if eq .Razlog "pogrešna_lozinka"}}Pogrešna lozinka - {{else if eq .Razlog "ip_zaklucano"}}IP zaključano - {{else if .Uspeh}}— - {{else}}{{.Razlog}} - {{end}} -
-
- {{else}} -
- Nema zabeleženih pokušaja prijave za ovog korisnika. -
- {{end}} + {{if .Istorija}} +
+ + + + + + + + + + + + {{range .Istorija}} + + + + + + + + {{end}} + +
Datum i vremeIP adresaPregledačStatusRazlog
{{.Vreme.Format "02.01.2006. 15:04:05"}}{{.IP}}{{.UserAgent}} + {{if .Uspeh}} + Uspeh + {{else}} + Neuspeh + {{end}} + + {{if eq .Razlog "korisnik_ne_postoji"}}Korisnik ne postoji {{else if eq .Razlog "nalog_neaktivan"}}Nalog neaktivan {{else if eq .Razlog "pogrešna_lozinka"}}Pogrešna lozinka {{else if eq .Razlog "ip_zaklucano"}}IP zaključano {{else if .Uspeh}}— {{else}}{{.Razlog}} {{end}} +
- + {{else}} +
Nema zabeleženih pokušaja prijave za ovog korisnika.
+ {{end}} +
{{end}} diff --git a/web/templates/stranice/admin_profil.html b/web/templates/stranice/admin_profil.html index 41c52ef..83ebcaa 100644 --- a/web/templates/stranice/admin_profil.html +++ b/web/templates/stranice/admin_profil.html @@ -1,133 +1,111 @@ -{{template "base" .}} - -{{define "naslov"}}Moj profil — NTech{{end}} - -{{define "dodatni-css"}} +{{ template "base" . }} {{ define "naslov" }}Moj profil — NTech{{ end }} +{{ define "dodatni-css" }} -{{end}} +{{ end }} +{{ define "sadrzaj" }} +
+ +
+
Promena lozinke
-{{define "sadrzaj"}} -
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+
- -
-
- Promena lozinke -
+ +
+
Dvostepena verifikacija (2FA)
-
-
-
- - -
-
- - -
-
- - -
-
- -
-
-
-
+ {{ if .TotpURI }} + + {{ if eq .Greska "totp" }} +
Neispravan kod. Pokušajte ponovo.
+ {{ end }} - -
-
- Dvostepena verifikacija (2FA) -
+

Skenirajte QR kod u aplikaciji (Google Authenticator, Authy...), pa unesite generisani kod da potvrdite podešavanje.

- {{if .TotpURI}} - - {{if eq .Greska "totp"}} -
Neispravan kod. Pokušajte ponovo.
- {{end}} +
+ QR kod +
-

- Skenirajte QR kod u aplikaciji (Google Authenticator, Authy...), pa unesite generisani kod da potvrdite podešavanje. -

+
+ Prikaži tajnu ručno + {{ .TotpTajna }} +
-
- QR kod -
+
+ +
+ + +
+ +
-
- Prikaži tajnu ručno - {{.TotpTajna}} -
+ {{ else if .TotpAktivan }} + +
+
+
✓ Dvostepena verifikacija je uključena
+
Prijava zahteva TOTP kod pored lozinke.
+
+
+ +
+
-
- -
- - -
- -
- - {{else if .TotpAktivan}} - -
-
-
✓ Dvostepena verifikacija je uključena
-
Prijava zahteva TOTP kod pored lozinke.
-
-
- -
-
- - {{else}} - -
-
-
Status: Isključena
-
Preporučujemo uključivanje dvostepene verifikacije.
-
- - Podesi 2FA - -
- {{end}} - -
- - - {{if index .Dozvole "tema.lokalno"}} -
-
- - - Podešavanja teme i pozadine nalaze se na - stranici Tema. - -
-
- {{end}} + {{ else }} + +
+
+
+ Status: + Isključena +
+
Preporučujemo uključivanje dvostepene verifikacije.
+
+ Podesi 2FA +
+ {{ end }} +
+ {{ if index .Dozvole "tema.lokalno" }} +
+
+ + + + + + Podešavanja teme i pozadine nalaze se na + stranici Tema + . + +
+
+ {{ end }}
-{{end}} +{{ end }} \ No newline at end of file diff --git a/web/templates/stranice/dashboard.html b/web/templates/stranice/dashboard.html index 33c654c..2882cd0 100644 --- a/web/templates/stranice/dashboard.html +++ b/web/templates/stranice/dashboard.html @@ -1,125 +1,139 @@ -{{template "base" .}} +{{ template "base" . }} -{{define "naslov"}}Dashboard — NTech{{end}} +{{ define "naslov" }}Dashboard — NTech{{ end }} -{{define "dodatni-css"}} +{{ define "dodatni-css" }} -{{end}} +{{ end }} -{{define "sadrzaj"}} -{{if .FlashGreska}} -
{{.FlashGreska}}
-{{end}} +{{ define "sadrzaj" }} +{{ if .FlashGreska }} +
{{ .FlashGreska }}
+{{ end }}
- -
-
- -
-
{{.BrojArtikala}}
-
Artikala na stanju
-
+ +
+
+ + + +
+
{{ .BrojArtikala }}
+
Artikala na stanju
+
-
-
- -
-
{{.AktivniServisi}}
-
Aktivnih servisa
-
+
+
+ + + +
+
{{ .AktivniServisi }}
+
Aktivnih servisa
+
- {{if index .Dozvole "dashboard.prihod"}} -
-
- -
-
{{printf "%.0f" .PrihodOvogMeseca}} din
-
Prihod ovog meseca
-
- {{end}} + {{ if index .Dozvole "dashboard.prihod" }} +
+
+ + + + + +
+
{{ printf "%.0f" .PrihodOvogMeseca }} din
+
Prihod ovog meseca
+
+ {{ end }} -
-
- -
-
{{.KriticnaZaliha}}
-
Kritično niska zaliha
-
+
+
+ + + + + +
+
{{ .KriticnaZaliha }}
+
Kritično niska zaliha
+
- -
- -
-
{{.AktivniPodsetnici}}
-
Aktivnih podsetnika
-
+ +
+ + + + +
+
{{ .AktivniPodsetnici }}
+
Aktivnih podsetnika
+
- -
-
- Poslednji servisi -
- {{range .PoslednjiServisi}} -
-
- {{.Uredjaj}} - {{.DatumPrijema}} -
- {{else}} -
Nema servisnih naloga.
- {{end}} -
+ +
+
+ Poslednji servisi +
+ {{ range .PoslednjiServisi }} +
+
+ {{ .Uredjaj }} + {{ .DatumPrijema }} +
+ {{ else }} +
Nema servisnih naloga.
+ {{ end }} +
- -
-
- Kritične zalihe - Upozorenje -
- {{range .KriticneZalihe}} -
-
- {{.Naziv}} - {{.Kolicina}} kom -
- {{else}} -
Sve zalihe su uredne.
- {{end}} -
+ +
+
+ Kritične zalihe + Upozorenje +
+ {{ range .KriticneZalihe }} +
+
+ {{ .Naziv }} + {{ .Kolicina }} kom +
+ {{ else }} +
Sve zalihe su uredne.
+ {{ end }} +
- -
-
- Poslednje prodaje -
- {{range .PoslednjeProdaje}} -
-
-
{{.BrojNaloga}}
- {{if .KlijentNaziv}} -
{{.KlijentNaziv}}
- {{end}} -
-
-
{{printf "%.0f" .Ukupno}} din
-
{{.Datum}}
-
-
- {{else}} -
Nema prodajnih naloga.
- {{end}} -
+ +
+
+ Poslednje prodaje +
+ {{ range .PoslednjeProdaje }} +
+
+
{{ .BrojNaloga }}
+ {{ if .KlijentNaziv }} +
{{ .KlijentNaziv }}
+ {{ end }} +
+
+
{{ printf "%.0f" .Ukupno }} din
+
{{ .Datum }}
+
+
+ {{ else }} +
Nema prodajnih naloga.
+ {{ end }} +
-{{end}} +{{ end }} \ No newline at end of file diff --git a/web/templates/stranice/dobavljaci.html b/web/templates/stranice/dobavljaci.html index 25bb191..870f08a 100644 --- a/web/templates/stranice/dobavljaci.html +++ b/web/templates/stranice/dobavljaci.html @@ -4,35 +4,32 @@ {{define "dodatni-css"}} {{end}} @@ -163,4 +160,4 @@
-{{end}} +{{end}} \ No newline at end of file diff --git a/web/templates/stranice/izvestaji.html b/web/templates/stranice/izvestaji.html index 2e8d35c..a0c4f9e 100644 --- a/web/templates/stranice/izvestaji.html +++ b/web/templates/stranice/izvestaji.html @@ -5,88 +5,22 @@ {{define "dodatni-css"}} diff --git a/web/templates/stranice/kategorije.html b/web/templates/stranice/kategorije.html index 3155cee..d387f12 100644 --- a/web/templates/stranice/kategorije.html +++ b/web/templates/stranice/kategorije.html @@ -5,11 +5,7 @@ {{define "dodatni-css"}} {{end}} diff --git a/web/templates/stranice/magacin.html b/web/templates/stranice/magacin.html index d8d1617..91198e4 100644 --- a/web/templates/stranice/magacin.html +++ b/web/templates/stranice/magacin.html @@ -4,33 +4,23 @@ {{define "dodatni-css"}} {{end}} diff --git a/web/templates/stranice/nabavka_detalji.html b/web/templates/stranice/nabavka_detalji.html index fe77932..3f732e9 100644 --- a/web/templates/stranice/nabavka_detalji.html +++ b/web/templates/stranice/nabavka_detalji.html @@ -7,23 +7,13 @@ .detalji-kartica:nth-child(1) { animation-delay: 0.04s; } .detalji-kartica:nth-child(2) { animation-delay: 0.12s; } .detalji-kartica:nth-child(3) { animation-delay: 0.20s; } - .stavke-tabela tbody tr:nth-child(1) { animation-delay: 0.16s; } .stavke-tabela tbody tr:nth-child(2) { animation-delay: 0.20s; } .stavke-tabela tbody tr:nth-child(3) { animation-delay: 0.24s; } .stavke-tabela tbody tr:nth-child(4) { animation-delay: 0.28s; } .stavke-tabela tbody tr:nth-child(5) { animation-delay: 0.32s; } - - .stavke-kartice { - display: none; - flex-direction: column; - gap: 10px; - } - - @media (max-width: 768px) { - .stavke-tabela-wrapper { display: none; } - .stavke-kartice { display: flex; } - } + .stavke-kartice { display: none; flex-direction: column; gap: 10px; } + @media (max-width: 768px) { .stavke-tabela-wrapper { display: none; } .stavke-kartice { display: flex; } } {{end}} diff --git a/web/templates/stranice/nabavka_forma.html b/web/templates/stranice/nabavka_forma.html index 80f2c61..d9be7a8 100644 --- a/web/templates/stranice/nabavka_forma.html +++ b/web/templates/stranice/nabavka_forma.html @@ -4,22 +4,12 @@ {{define "dodatni-css"}} {{end}} diff --git a/web/templates/stranice/nabavke.html b/web/templates/stranice/nabavke.html index cb4d527..caceaef 100644 --- a/web/templates/stranice/nabavke.html +++ b/web/templates/stranice/nabavke.html @@ -5,34 +5,23 @@ {{define "dodatni-css"}} {{end}} diff --git a/web/templates/stranice/podesavanja.html b/web/templates/stranice/podesavanja.html index 64554dd..90cd96a 100644 --- a/web/templates/stranice/podesavanja.html +++ b/web/templates/stranice/podesavanja.html @@ -8,74 +8,17 @@ .animiraj:nth-child(2) { animation-delay: 0.16s; } .animiraj:nth-child(3) { animation-delay: 0.22s; } .animiraj:nth-child(4) { animation-delay: 0.28s; } - - /* dvostupčani raspored za app pozadinu */ - .app-poz-grid { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 20px; - align-items: start; - } - @media (max-width: 680px) { - .app-poz-grid { grid-template-columns: 1fr; } - } - - /* custom file input */ - .custom-file-label { - display: flex; - align-items: center; - gap: 8px; - padding: 8px 12px; - background: var(--pozadina); - border: 0.5px solid var(--ivica); - border-radius: 8px; - font-size: 13px; - color: var(--tekst-sporedni); - cursor: pointer; - transition: border-color 0.2s; - min-width: 0; - overflow: hidden; - } + .app-poz-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; align-items: start; } + @media (max-width: 680px) { .app-poz-grid { grid-template-columns: 1fr; } } + .custom-file-label { display: flex; align-items: center; gap: 8px; padding: 8px 12px; background: var(--pozadina); border: 0.5px solid var(--ivica); border-radius: 8px; font-size: 13px; color: var(--tekst-sporedni); cursor: pointer; transition: border-color 0.2s; min-width: 0; overflow: hidden; } .custom-file-label:hover { border-color: var(--sb-akcent); color: var(--tekst-glavni); } .custom-file-label span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } - - .toast { - position: fixed; - bottom: 24px; - right: 24px; - z-index: 9999; - display: flex; - align-items: center; - gap: 10px; - padding: 12px 18px; - border-radius: 10px; - font-size: 13px; - font-weight: 500; - box-shadow: 0 4px 20px rgba(0,0,0,0.15); - animation: toastIn 0.3s ease forwards; - max-width: 340px; - } - .toast-greska { - background: #fef2f2; - color: #dc2626; - border: 0.5px solid #fca5a5; - } - .toast-uspeh { - background: #f0fdf4; - color: #16a34a; - border: 0.5px solid #86efac; - } - @keyframes toastIn { - from { opacity: 0; transform: translateY(12px); } - to { opacity: 1; transform: translateY(0); } - } - @keyframes toastOut { - from { opacity: 1; transform: translateY(0); } - to { opacity: 0; transform: translateY(12px); } - } - .toast.nestaje { - animation: toastOut 0.3s ease forwards; - } + .toast { position: fixed; bottom: 24px; right: 24px; z-index: 9999; display: flex; align-items: center; gap: 10px; padding: 12px 18px; border-radius: 10px; font-size: 13px; font-weight: 500; box-shadow: 0 4px 20px rgba(0,0,0,0.15); animation: toastIn 0.3s ease forwards; max-width: 340px; } + .toast-greska { background: #fef2f2; color: #dc2626; border: 0.5px solid #fca5a5; } + .toast-uspeh { background: #f0fdf4; color: #16a34a; border: 0.5px solid #86efac; } + @keyframes toastIn { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } } + @keyframes toastOut { from { opacity: 1; transform: translateY(0); } to { opacity: 0; transform: translateY(12px); } } + .toast.nestaje { animation: toastOut 0.3s ease forwards; } {{end}} diff --git a/web/templates/stranice/podesavanja_izgled.html b/web/templates/stranice/podesavanja_izgled.html index c4d4f81..d72ba8d 100644 --- a/web/templates/stranice/podesavanja_izgled.html +++ b/web/templates/stranice/podesavanja_izgled.html @@ -7,16 +7,8 @@ .animiraj:nth-child(1) { animation-delay: 0.10s; } .animiraj:nth-child(2) { animation-delay: 0.16s; } .animiraj:nth-child(3) { animation-delay: 0.22s; } - - .app-poz-grid { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 20px; - align-items: start; - } - @media (max-width: 680px) { - .app-poz-grid { grid-template-columns: 1fr; } - } + .app-poz-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; align-items: start; } + @media (max-width: 680px) { .app-poz-grid { grid-template-columns: 1fr; } } {{end}} diff --git a/web/templates/stranice/podesavanja_opste.html b/web/templates/stranice/podesavanja_opste.html index 8a83918..bebf83c 100644 --- a/web/templates/stranice/podesavanja_opste.html +++ b/web/templates/stranice/podesavanja_opste.html @@ -7,43 +7,12 @@ .animiraj:nth-child(1) { animation-delay: 0.10s; } .animiraj:nth-child(2) { animation-delay: 0.16s; } - .toast { - position: fixed; - bottom: 24px; - right: 24px; - z-index: 9999; - display: flex; - align-items: center; - gap: 10px; - padding: 12px 18px; - border-radius: 10px; - font-size: 13px; - font-weight: 500; - box-shadow: 0 4px 20px rgba(0,0,0,0.15); - animation: toastIn 0.3s ease forwards; - max-width: 340px; - } - .toast-greska { - background: #fef2f2; - color: #dc2626; - border: 0.5px solid #fca5a5; - } - .toast-uspeh { - background: #f0fdf4; - color: #16a34a; - border: 0.5px solid #86efac; - } - @keyframes toastIn { - from { opacity: 0; transform: translateY(12px); } - to { opacity: 1; transform: translateY(0); } - } - @keyframes toastOut { - from { opacity: 1; transform: translateY(0); } - to { opacity: 0; transform: translateY(12px); } - } - .toast.nestaje { - animation: toastOut 0.3s ease forwards; - } + .toast { position: fixed; bottom: 24px; right: 24px; z-index: 9999; display: flex; align-items: center; gap: 10px; padding: 12px 18px; border-radius: 10px; font-size: 13px; font-weight: 500; box-shadow: 0 4px 20px rgba(0,0,0,0.15); animation: toastIn 0.3s ease forwards; max-width: 340px; } + .toast-greska { background: #fef2f2; color: #dc2626; border: 0.5px solid #fca5a5; } + .toast-uspeh { background: #f0fdf4; color: #16a34a; border: 0.5px solid #86efac; } + @keyframes toastIn { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } } + @keyframes toastOut { from { opacity: 1; transform: translateY(0); } to { opacity: 0; transform: translateY(12px); } } + .toast.nestaje { animation: toastOut 0.3s ease forwards; } {{end}} diff --git a/web/templates/stranice/podsetnici.html b/web/templates/stranice/podsetnici.html index 36cf4ed..ea25d4b 100644 --- a/web/templates/stranice/podsetnici.html +++ b/web/templates/stranice/podsetnici.html @@ -17,26 +17,14 @@ .pod-tabela tbody tr:nth-child(9) { animation-delay: 0.36s; } .pod-tabela tbody tr:nth-child(10) { animation-delay: 0.40s; } - .pod-kartice { - display: none; - flex-direction: column; - gap: 12px; - } - + .pod-kartice { display: none; flex-direction: column; gap: 12px; } .pod-kartica:nth-child(1) { animation-delay: 0.04s; } .pod-kartica:nth-child(2) { animation-delay: 0.10s; } .pod-kartica:nth-child(3) { animation-delay: 0.16s; } .pod-kartica:nth-child(4) { animation-delay: 0.22s; } .pod-kartica:nth-child(5) { animation-delay: 0.28s; } - - .red-prekoracen td { - background: rgba(207, 87, 87, 0.06); - } - - @media (max-width: 768px) { - .pod-tabela { display: none; } - .pod-kartice { display: flex; } - } + .red-prekoracen td { background: rgba(207, 87, 87, 0.06); } + @media (max-width: 768px) { .pod-tabela { display: none; } .pod-kartice { display: flex; } } {{end}} diff --git a/web/templates/stranice/podsetnik_forma.html b/web/templates/stranice/podsetnik_forma.html index bcfefe6..e6a00f7 100644 --- a/web/templates/stranice/podsetnik_forma.html +++ b/web/templates/stranice/podsetnik_forma.html @@ -4,9 +4,7 @@ {{define "dodatni-css"}} {{end}} diff --git a/web/templates/stranice/prijava.html b/web/templates/stranice/prijava.html index 404f1d8..0d5a019 100644 --- a/web/templates/stranice/prijava.html +++ b/web/templates/stranice/prijava.html @@ -1,160 +1,74 @@ - - - - Prijava — NTech - + + {{if .LoginPozadina}} - .kartica { - background: rgba(0,0,0,{{.LoginPozadinaZatamnjenjeKartice}}%) !important; - backdrop-filter: blur({{.LoginPozadinaBlurKartice}}px); - -webkit-backdrop-filter: blur({{.LoginPozadinaBlurKartice}}px); - border: 1px solid rgba(255,255,255,0.18) !important; - box-shadow: 0 8px 32px rgba(0,0,0,0.3); - } - {{end}} - .logo { - text-align: center; - margin-bottom: 32px; - } - .logo-naziv { - font-size: 22px; - font-weight: 600; - color: #fff; - letter-spacing: -0.3px; - } - .logo-podnazlov { - font-size: 13px; - color: #6b7280; - margin-top: 4px; - } - h1 { - font-size: 18px; - font-weight: 600; - color: #fff; - margin-bottom: 24px; - } - .polje { margin-bottom: 16px; } - label { - display: block; - font-size: 13px; - color: #9ca3af; - margin-bottom: 6px; - } - input { - width: 100%; - padding: 8px 12px; - background: #0f1117; - border: 0.5px solid #2d3148; - border-radius: 8px; - font-size: 14px; - color: #fff; - outline: none; - transition: border-color 0.2s; - } - input:focus { border-color: #e53e3e; } - .dugme { - width: 100%; - padding: 11px; - background: #e53e3e; - color: #fff; - border: none; - border-radius: 8px; - font-size: 14px; - font-weight: 500; - cursor: pointer; - margin-top: 8px; - transition: opacity 0.2s; - } - .dugme:hover { opacity: 0.88; } - .greska { - background: #fef2f2; - border: 0.5px solid #fca5a5; - color: #dc2626; - border-radius: 8px; - padding: 10px 14px; - font-size: 13px; - margin-bottom: 20px; - } - .uspeh { - background: #f0fdf4; - border: 0.5px solid #86efac; - color: #16a34a; - border-radius: 8px; - padding: 10px 14px; - font-size: 13px; - margin-bottom: 20px; - } - - - - {{if .LoginPozadina}} -
-
-
- {{end}} +
+
+
+ {{end}} -
- +
+ -

Prijava

+

Prijava

- {{if eq .Greska "1"}} -
Pogrešno korisničko ime ili lozinka.
- {{else if eq .Greska "2"}} -
Greška na serveru. Pokušajte ponovo.
- {{else if eq .Greska "zakljucano"}} -
- Previše neuspelih pokušaja prijave. IP adresa je privremeno blokirana. - {{if .Preostalo}}
Pokušajte ponovo za: {{.Preostalo}}{{end}} + {{if eq .Greska "1"}} +
Pogrešno korisničko ime ili lozinka.
+ {{else if eq .Greska "2"}} +
Greška na serveru. Pokušajte ponovo.
+ {{else if eq .Greska "zakljucano"}} +
+ Previše neuspelih pokušaja prijave. IP adresa je privremeno blokirana. + {{if .Preostalo}}
Pokušajte ponovo za: {{.Preostalo}}{{end}} +
+ {{end}} + + {{if .Sacuvano}} +
Nalog je kreiran. Možete se prijaviti.
+ {{end}} + +
+ +
+ + +
+
+ + +
+ +
+
+ + {{if .LoginPozadina}}
{{end}} - - {{if .Sacuvano}} -
Nalog je kreiran. Možete se prijaviti.
- {{end}} - -
- -
- - -
-
- - -
- -
-
- - {{if .LoginPozadina}} -
- {{end}} - - + + \ No newline at end of file diff --git a/web/templates/stranice/prodaja.html b/web/templates/stranice/prodaja.html index 71a0de5..a143001 100644 --- a/web/templates/stranice/prodaja.html +++ b/web/templates/stranice/prodaja.html @@ -1,103 +1,71 @@ -{{template "base" .}} - -{{define "naslov"}}Prodaja — NTech{{end}} - -{{define "dodatni-css"}} +{{template "base" .}} {{define "naslov"}}Prodaja — NTech{{end}} {{define "dodatni-css"}} -{{end}} - -{{define "sadrzaj"}} -
- +{{end}} {{define "sadrzaj"}} +
{{if .Sacuvano}}
Prodaja je uspešno sačuvana.
- {{end}} - {{if .Obrisan}} + {{end}} {{if .Obrisan}}
Prodajni nalog je uspešno obrisan.
{{end}} -
-
- - +
+ + + {{if .Pretraga}} - ✕ Resetuj + ✕ Resetuj {{end}} + Nova prodaja
-
-
- +
+
+
- - - - - - + + + + + + {{range .Nalozi}} - - - - - + + + + {{else}} - @@ -123,34 +89,23 @@
{{range .Nalozi}}
-
+
-
{{.BrojNaloga}}
-
- {{if .KlijentNaziv}}{{.KlijentNaziv}}{{else}}Bez klijenta{{end}} -
-
- {{.Datum.Format "02.01.2006."}} -
-
-
- {{printf "%.2f" .Ukupno}} din +
{{.BrojNaloga}}
+
{{if .KlijentNaziv}}{{.KlijentNaziv}}{{else}}Bez klijenta{{end}}
+
{{.Datum.Format "02.01.2006."}}
+
{{printf "%.2f" .Ukupno}} din
- - Detalji - + Detalji
{{else}} -
- {{if $.Pretraga}} - Nema naloga koji odgovaraju pretrazi. - {{else}} - Nema prodajnih naloga. Dodaj prvu prodaju. +
+ {{if $.Pretraga}} Nema naloga koji odgovaraju pretrazi. {{else}} Nema prodajnih naloga. + Dodaj prvu prodaju. {{end}}
{{end}}
-
{{end}} diff --git a/web/templates/stranice/prodaja_detalji.html b/web/templates/stranice/prodaja_detalji.html index 228114f..414ef5c 100644 --- a/web/templates/stranice/prodaja_detalji.html +++ b/web/templates/stranice/prodaja_detalji.html @@ -1,119 +1,304 @@ -{{template "base" .}} - -{{define "naslov"}}Detalji prodaje — NTech{{end}} - -{{define "dodatni-css"}} +{{template "base" .}} {{define "naslov"}}Detalji prodaje — NTech{{end}} {{define +"dodatni-css"}} -{{end}} +{{end}} {{define "sadrzaj"}} +
+ {{if .Sacuvano}} +
Prodaja je uspešno sačuvana.
+ {{end}} -{{define "sadrzaj"}} -
+ + + + Nazad na prodaju + - {{if .Sacuvano}} -
Prodaja je uspešno sačuvana.
- {{end}} - - - - - Nazad na prodaju - - - -
-
- - {{.Nalog.BrojNaloga}} - - - - Štampaj - -
-
-
-
Datum prodaje
-
{{.Nalog.Datum.Format "02.01.2006. u 15:04"}}
-
-
-
Klijent
-
- {{if .KlijentNaziv}}{{.KlijentNaziv}}{{else}}—{{end}} -
-
-
-
Ukupno
-
{{printf "%.2f" .Nalog.Ukupno}} din
-
- {{if .Nalog.Napomena}} -
-
Napomena
-
{{.Nalog.Napomena}}
-
- {{end}} -
+ +
+
+ + {{.Nalog.BrojNaloga}} + + + + Štampaj +
- - -
-
- Stavke +
+
+
+ Datum prodaje
-
-
Broj nalogaDatumKlijentUkupnoAkcije
Broj nalogaDatumKlijentUkupnoAkcije
- {{.BrojNaloga}} - - {{.Datum.Format "02.01.2006."}} - - {{if .KlijentNaziv}}{{.KlijentNaziv}}{{else}}—{{end}} - - {{printf "%.2f" .Ukupno}} din - -
+
{{.BrojNaloga}}{{.Datum.Format "02.01.2006."}}{{if .KlijentNaziv}}{{.KlijentNaziv}}{{else}}—{{end}}{{printf "%.2f" .Ukupno}} din +
Detalji {{if index $.Dozvole "prodaja.storno"}} -
- - + + +
{{end}}
@@ -105,11 +73,9 @@
- {{if $.Pretraga}} - Nema naloga koji odgovaraju pretrazi. - {{else}} - Nema prodajnih naloga. Dodaj prvu prodaju. + + {{if $.Pretraga}} Nema naloga koji odgovaraju pretrazi. {{else}} Nema prodajnih naloga. + Dodaj prvu prodaju. {{end}}
- - - - - - - - - - {{range .Stavke}} - - - - - - - {{end}} - - - - - - - -
ArtikalKoličinaCena/komUkupno
{{.ArtikalNaziv}}{{.Kolicina}}{{printf "%.2f" .CenaPoKomadu}} din{{printf "%.2f" .Ukupno}} din
Ukupno:{{printf "%.2f" .Nalog.Ukupno}} din
+
+ {{.Nalog.Datum.Format "02.01.2006. u 15:04"}}
+
+
+
+ Klijent +
+
+ {{if .KlijentNaziv}}{{.KlijentNaziv}}{{else}}—{{end}} +
+
+
+
+ Ukupno +
+
+ {{printf "%.2f" .Nalog.Ukupno}} din +
+
+ {{if .Nalog.Napomena}} +
+
+ Napomena +
+
+ {{.Nalog.Napomena}} +
+
+ {{end}}
+
- {{if index $.Dozvole "prodaja.obrisi"}} - -
-
-
-
Brisanje naloga
-
- Brisanje je trajno. Količine artikala biće vraćene na stanje u magacinu. -
-
-
- -
-
+ +
+
+ Stavke
- {{end}} +
+ + + + + + + + + + + {{range .Stavke}} + + + + + + + {{end}} + + + + + + + +
+ Artikal + + Količina + + Cena/kom + + Ukupno +
+ {{.ArtikalNaziv}} + + {{.Kolicina}} + + {{printf "%.2f" .CenaPoKomadu}} din + + {{printf "%.2f" .Ukupno}} din +
+ Ukupno: + + {{printf "%.2f" .Nalog.Ukupno}} din +
+
+
+ {{if index $.Dozvole "prodaja.obrisi"}} + +
+
+
+
+ Brisanje naloga +
+
+ Brisanje je trajno. Količine artikala biće vraćene na stanje u + magacinu. +
+
+
+ +
+
+
+ {{end}}
-{{end}} +{{end}} \ No newline at end of file diff --git a/web/templates/stranice/prodaja_forma.html b/web/templates/stranice/prodaja_forma.html index 152bcd0..2007797 100644 --- a/web/templates/stranice/prodaja_forma.html +++ b/web/templates/stranice/prodaja_forma.html @@ -1,28 +1,21 @@ -{{template "base" .}} - -{{define "naslov"}}Nova prodaja — NTech{{end}} - -{{define "dodatni-css"}} +{{template "base" .}} {{define "naslov"}}Nova prodaja — NTech{{end}} {{define +"dodatni-css"}} -{{end}} - -{{define "sadrzaj"}} +{{end}} {{define "sadrzaj"}} - + -
+ + + + Nazad na prodaju + - - - - Nazad na prodaju - +
+ {{if .Greska}} +
{{.Greska}}
+ {{end}} - + +
+
+ Nova prodaja +
+
+
+ + +
+
+ + +
+
+
- {{if .Greska}} -
{{.Greska}}
- {{end}} + +
+
+ Stavke + +
- -
-
- Nova prodaja + +
+ + + + + + + + + + + + + + + + + + + + +
+ Artikal + + Količina + + Cena/kom (din) + + Ukupno +
+ Ukupno: + + +
+
+ + + +
- -
-
- Stavke - -
- - -
- - - - - - - - - - - - - - - - - - - - -
ArtikalKoličinaCena/kom (din)Ukupno
Ukupno: - -
-
- - - -
- - -
- Odustani - -
- - - + +
+ Odustani + +
+
-{{end}} +{{end}} \ No newline at end of file diff --git a/web/templates/stranice/prodaja_stampa.html b/web/templates/stranice/prodaja_stampa.html index afa7399..e7e339b 100644 --- a/web/templates/stranice/prodaja_stampa.html +++ b/web/templates/stranice/prodaja_stampa.html @@ -1,255 +1,136 @@ - + - - - + + + Priznanica {{.Nalog.BrojNaloga}} + + +
+
+
+
{{.NazivFirme}}
+ {{if .Podnazlov}} +
{{.Podnazlov}}
+ {{end}} {{if .Adresa}} +
{{.Adresa}}
+ {{end}} {{if .Telefon}} +
{{.Telefon}}
+ {{end}} {{if .PIB}} +
PIB: {{.PIB}}
+ {{end}} +
+
+
{{.Nalog.BrojNaloga}}
+
+ {{.Nalog.Datum.Format "02.01.2006. u 15:04"}} +
+
+
- body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; - font-size: 14px; - color: #111; - background: #f5f5f5; - padding: 32px 16px; - } + {{if .KlijentNaziv}} +
+
+ + {{.KlijentNaziv}} +
+
+ {{end}} - .stranica { - background: #fff; - max-width: 640px; - margin: 0 auto; - padding: 40px; - border-radius: 8px; - box-shadow: 0 1px 8px rgba(0,0,0,0.08); - } + + + + + + + + + + + {{range .Stavke}} + + + + + + + {{end}} + + + + + + + +
ArtikalKol.Cena/komUkupno
{{.ArtikalNaziv}}{{.Kolicina}}{{printf "%.2f" .CenaPoKomadu}} din{{printf "%.2f" .Ukupno}} din
Ukupno za naplatu:{{printf "%.2f" .Nalog.Ukupno}} din
- .zaglavlje { - display: flex; - justify-content: space-between; - align-items: flex-start; - margin-bottom: 32px; - padding-bottom: 24px; - border-bottom: 1px solid #e5e7eb; - } - - .firma-naziv { - font-size: 20px; - font-weight: 600; - color: #111; - } - - .firma-podnazlov { - font-size: 13px; - color: #6b7280; - margin-top: 3px; - } - - .firma-kontakt { - font-size: 12px; - color: #6b7280; - margin-top: 2px; - } - - .nalog-info { - text-align: right; - } - - .nalog-broj { - font-size: 16px; - font-weight: 600; - font-family: monospace; - color: #111; - } - - .nalog-datum { - font-size: 13px; - color: #6b7280; - margin-top: 3px; - } - - .meta { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 16px; - margin-bottom: 28px; - } - - .meta-stavka label { + {{if .Nalog.Napomena}} +
+
- -
-
-
{{.NazivFirme}}
- {{if .Podnazlov}}
{{.Podnazlov}}
{{end}} - {{if .Adresa}}
{{.Adresa}}
{{end}} - {{if .Telefon}}
{{.Telefon}}
{{end}} - {{if .PIB}}
PIB: {{.PIB}}
{{end}} -
-
-
{{.Nalog.BrojNaloga}}
-
{{.Nalog.Datum.Format "02.01.2006. u 15:04"}}
-
-
- - {{if .KlijentNaziv}} -
-
- - {{.KlijentNaziv}} -
-
- {{end}} - - - - - - - - - - - - {{range .Stavke}} - - - - - - - {{end}} - - - - - - - -
ArtikalKol.Cena/komUkupno
{{.ArtikalNaziv}}{{.Kolicina}}{{printf "%.2f" .CenaPoKomadu}} din{{printf "%.2f" .Ukupno}} din
Ukupno za naplatu:{{printf "%.2f" .Nalog.Ukupno}} din
- - {{if .Nalog.Napomena}} -
-
Napomena
-
{{.Nalog.Napomena}}
-
- {{end}} - -
- - + margin-bottom: 4px; + "> + Napomena
+
{{.Nalog.Napomena}}
+
+ {{end}} +
+ + +
- - - + + + \ No newline at end of file diff --git a/web/templates/stranice/servis.html b/web/templates/stranice/servis.html index 7d75120..de4efcf 100644 --- a/web/templates/stranice/servis.html +++ b/web/templates/stranice/servis.html @@ -17,26 +17,13 @@ .servis-tabela tbody tr:nth-child(9) { animation-delay: 0.36s; } .servis-tabela tbody tr:nth-child(10) { animation-delay: 0.40s; } - .servis-kartice { - display: none; - flex-direction: column; - gap: 12px; - } - - .servis-kartica:nth-child(1) { animation-delay: 0.04s; } - .servis-kartica:nth-child(2) { animation-delay: 0.10s; } - .servis-kartica:nth-child(3) { animation-delay: 0.16s; } - .servis-kartica:nth-child(4) { animation-delay: 0.22s; } - .servis-kartica:nth-child(5) { animation-delay: 0.28s; } - - .status-badge { - display: inline-block; - padding: 3px 10px; - border-radius: 20px; - font-size: 12px; - font-weight: 500; - white-space: nowrap; - } + .servis-kartice { display: none; flex-direction: column; gap: 12px; } + .servis-kartica:nth-child(1) { animation-delay: 0.04s; } + .servis-kartica:nth-child(2) { animation-delay: 0.10s; } + .servis-kartica:nth-child(3) { animation-delay: 0.16s; } + .servis-kartica:nth-child(4) { animation-delay: 0.22s; } + .servis-kartica:nth-child(5) { animation-delay: 0.28s; } + .status-badge { display: inline-block; padding: 3px 10px; border-radius: 20px; font-size: 12px; font-weight: 500; white-space: nowrap; } .status-primljeno { background: rgba(148,163,184,0.15); color: #94a3b8; } .status-dijagnostika { background: rgba(59,130,246,0.15); color: #3b82f6; } diff --git a/web/templates/stranice/servis_detalji.html b/web/templates/stranice/servis_detalji.html index 8ac3686..b64f9ca 100644 --- a/web/templates/stranice/servis_detalji.html +++ b/web/templates/stranice/servis_detalji.html @@ -9,24 +9,14 @@ .detalji-kartica:nth-child(3) { animation-delay: 0.20s; } .detalji-kartica:nth-child(4) { animation-delay: 0.28s; } .detalji-kartica:nth-child(5) { animation-delay: 0.36s; } - .poruka-animacija { animation: slideDown 0.3s ease forwards; } - - .status-badge { - display: inline-block; - padding: 4px 12px; - border-radius: 20px; - font-size: 13px; - font-weight: 500; - white-space: nowrap; - } - - .status-primljeno { background: rgba(148,163,184,0.15); color: #94a3b8; } - .status-dijagnostika { background: rgba(59,130,246,0.15); color: #3b82f6; } - .status-ceka { background: rgba(249,115,22,0.15); color: #f97316; } - .status-popravka { background: rgba(234,179,8,0.15); color: #ca8a04; } - .status-zavrseno { background: rgba(34,197,94,0.15); color: #16a34a; } - .status-preuzeto { background: rgba(21,128,61,0.15); color: #15803d; } + .status-badge { display: inline-block; padding: 4px 12px; border-radius: 20px; font-size: 13px; font-weight: 500; white-space: nowrap; } + .status-primljeno { background: rgba(148,163,184,0.15); color: #94a3b8; } + .status-dijagnostika { background: rgba(59,130,246,0.15); color: #3b82f6; } + .status-ceka { background: rgba(249,115,22,0.15); color: #f97316; } + .status-popravka { background: rgba(234,179,8,0.15); color: #ca8a04; } + .status-zavrseno { background: rgba(34,197,94,0.15); color: #16a34a; } + .status-preuzeto { background: rgba(21,128,61,0.15); color: #15803d; } {{end}} diff --git a/web/templates/stranice/totp_provera.html b/web/templates/stranice/totp_provera.html index 8fc28c8..d5a5a42 100644 --- a/web/templates/stranice/totp_provera.html +++ b/web/templates/stranice/totp_provera.html @@ -6,74 +6,20 @@ Dvostepena verifikacija — NTech diff --git a/web/templates/teme/podrazumevana/base.html b/web/templates/teme/podrazumevana/base.html index bd7c61c..4dab0ae 100644 --- a/web/templates/teme/podrazumevana/base.html +++ b/web/templates/teme/podrazumevana/base.html @@ -38,91 +38,21 @@ {{if .AppPozadina}}