Bezbednosni audit i refaktoring: HP popravke, RBAC, flash poruke, go:embed, CSP

This commit is contained in:
2026-06-07 16:10:41 +02:00
parent 301bcaf5c4
commit 16b993933c
37 changed files with 1513 additions and 1949 deletions
+1 -1
View File
@@ -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)
+33 -41
View File
@@ -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
}
-38
View File
@@ -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) {
+1 -1
View File
@@ -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
+5 -5
View File
@@ -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())
+19 -47
View File
@@ -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
}
+37 -50
View File
@@ -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)
+39
View File
@@ -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)
}
+2 -1
View File
@@ -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'")
+20 -136
View File
@@ -4,142 +4,26 @@
<meta charset="UTF-8" />
<title>NTech — Konfiguracija</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #1a1a2e;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
.kartica {
background: #16213e;
padding: 2rem;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
width: 410px;
border: 1px solid #0f3460;
}
.zaglavlje {
text-align: center;
margin-bottom: 1.3rem;
}
.zaglavlje h1 {
color: #f8e45c;
font-size: 1.4rem;
margin-bottom: 0.25rem;
}
.zaglavlje p {
color: #a0a0b0;
font-size: 0.85rem;
}
.sekcija-naslov {
color: #a0a0b0;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.4rem;
}
.port-opcija {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 0.75rem;
margin: 0.25rem 0;
border: 2px solid #0f3460;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
color: #e0e0e0;
}
.port-opcija:hover {
border-color: #e94560;
}
.port-opcija.izabran {
border-color: #e94560;
background: rgba(233, 69, 96, 0.1);
}
.port-opcija.zauzet {
opacity: 0.5;
cursor: not-allowed;
}
.slobodan {
color: #2ecc71;
font-size: 0.85rem;
}
.zauzet-tekst {
color: #e74c3c;
font-size: 0.85rem;
}
.razdvojnik {
border: none;
border-top: 1px solid #0f3460;
margin: 0.75rem 0;
}
input[type="number"] {
width: 100%;
padding: 0.5rem 0.75rem;
background: #0f3460;
border: 2px solid #0f3460;
border-radius: 8px;
font-size: 0.9rem;
color: #e0e0e0;
outline: none;
transition: border-color 0.2s;
}
input[type="number"]:focus {
border-color: #e94560;
}
input[type="number"]::placeholder {
color: #606080;
}
button {
width: 100%;
padding: 0.65rem;
margin-top: 0.75rem;
background: #e94560;
color: white;
border: none;
border-radius: 8px;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
button:hover {
background: #c73652;
}
.poruka {
text-align: center;
padding: 0.5rem;
font-size: 0.85rem;
color: #2ecc71;
display: none;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #1a1a2e; display: flex; justify-content: center; align-items: center; height: 100vh; }
.kartica { background: #16213e; padding: 2rem; border-radius: 12px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); width: 410px; border: 1px solid #0f3460; }
.zaglavlje { text-align: center; margin-bottom: 1.3rem; }
.zaglavlje h1 { color: #f8e45c; font-size: 1.4rem; margin-bottom: 0.25rem; }
.zaglavlje p { color: #a0a0b0; font-size: 0.85rem; }
.sekcija-naslov { color: #a0a0b0; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.4rem; }
.port-opcija { display: flex; justify-content: space-between; align-items: center; padding: 0.5rem 0.75rem; margin: 0.25rem 0; border: 2px solid #0f3460; border-radius: 8px; cursor: pointer; transition: all 0.2s; color: #e0e0e0; }
.port-opcija:hover { border-color: #e94560; }
.port-opcija.izabran { border-color: #e94560; background: rgba(233, 69, 96, 0.1); }
.port-opcija.zauzet { opacity: 0.5; cursor: not-allowed; }
.slobodan { color: #2ecc71; font-size: 0.85rem; }
.zauzet-tekst { color: #e74c3c; font-size: 0.85rem; }
.razdvojnik { border: none; border-top: 1px solid #0f3460; margin: 0.75rem 0; }
input[type="number"] { width: 100%; padding: 0.5rem 0.75rem; background: #0f3460; border: 2px solid #0f3460; border-radius: 8px; font-size: 0.9rem; color: #e0e0e0; outline: none; transition: border-color 0.2s; }
input[type="number"]:focus { border-color: #e94560; }
input[type="number"]::placeholder { color: #606080; }
button { width: 100%; padding: 0.65rem; margin-top: 0.75rem; background: #e94560; color: white; border: none; border-radius: 8px; font-size: 0.95rem; font-weight: 600; cursor: pointer; transition: background 0.2s; }
button:hover { background: #c73652; }
.poruka { text-align: center; padding: 0.5rem; font-size: 0.85rem; color: #2ecc71; display: none; }
</style>
</head>
+1 -10
View File
@@ -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); }
+1 -3
View File
@@ -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; }
</style>
{{end}}
@@ -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"}}
<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; }
.animiraj:nth-child(4) { animation-delay: 0.28s; }
.animiraj:nth-child(1) { animation-delay: 0.1s; }
.animiraj:nth-child(2) { animation-delay: 0.16s; }
.animiraj:nth-child(3) { animation-delay: 0.22s; }
.animiraj:nth-child(4) { animation-delay: 0.28s; }
</style>
{{end}}
{{end}} {{define "sadrzaj"}}
<div style="display: flex; flex-direction: column; gap: 16px">
<div style="display: flex; align-items: center; gap: 12px; flex-wrap: wrap">
<a href="/admin/korisnici" style="display: inline-flex; align-items: center; gap: 6px; padding: 7px 14px; border: 0.5px solid var(--ivica); border-radius: 8px; color: var(--tekst-sporedni); font-size: 13px; text-decoration: none">← Nazad</a>
<span style="font-size: 15px; font-weight: 500; color: var(--tekst-glavni)">Istorija prijava — {{.PrikazKorisnik.KorisnickoIme}}</span>
</div>
{{define "sadrzaj"}}
<div style="display:flex;flex-direction:column;gap:16px;">
<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;">
<a href="/admin/korisnici"
style="display:inline-flex;align-items:center;gap:6px;padding:7px 14px;border:0.5px solid var(--ivica);border-radius:8px;color:var(--tekst-sporedni);font-size:13px;text-decoration:none;">
← Nazad
</a>
<span style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">
Istorija prijava — {{.PrikazKorisnik.KorisnickoIme}}
</span>
<div class="kartica animiraj" style="padding: 0; overflow: hidden">
<div style="padding: 16px 20px; border-bottom: 0.5px solid var(--ivica); display: flex; align-items: center; justify-content: space-between">
<span style="font-size: 15px; font-weight: 500; color: var(--tekst-glavni)">Poslednjih 50 pokušaja</span>
<span style="font-size: 12px; color: var(--tekst-sporedni)">Vreme prikazano po lokalnom vremenu servera</span>
</div>
<div class="kartica animiraj" style="padding:0;overflow:hidden;">
<div style="padding:16px 20px;border-bottom:0.5px solid var(--ivica);display:flex;align-items:center;justify-content:space-between;">
<span style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">Poslednjih 50 pokušaja</span>
<span style="font-size:12px;color:var(--tekst-sporedni);">Vreme prikazano po lokalnom vremenu servera</span>
</div>
{{if .Istorija}}
<div style="overflow-x:auto;">
<table style="width:100%;border-collapse:collapse;">
<thead>
<tr style="border-bottom:0.5px solid var(--ivica);">
<th style="padding:10px 20px;text-align:left;font-size:12px;font-weight:500;color:var(--tekst-sporedni);white-space:nowrap;">Datum i vreme</th>
<th style="padding:10px 20px;text-align:left;font-size:12px;font-weight:500;color:var(--tekst-sporedni);">IP adresa</th>
<th style="padding:10px 20px;text-align:left;font-size:12px;font-weight:500;color:var(--tekst-sporedni);">Pregledač</th>
<th style="padding:10px 20px;text-align:center;font-size:12px;font-weight:500;color:var(--tekst-sporedni);">Status</th>
<th style="padding:10px 20px;text-align:left;font-size:12px;font-weight:500;color:var(--tekst-sporedni);">Razlog</th>
</tr>
</thead>
<tbody>
{{range .Istorija}}
<tr class="animiraj" style="border-bottom:0.5px solid var(--ivica);">
<td style="padding:10px 20px;font-size:13px;color:var(--tekst-sporedni);white-space:nowrap;">
{{.Vreme.Format "02.01.2006. 15:04:05"}}
</td>
<td style="padding:10px 20px;font-size:13px;color:var(--tekst-glavni);font-family:monospace;">
{{.IP}}
</td>
<td style="padding:10px 20px;font-size:12px;color:var(--tekst-sporedni);max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">
{{.UserAgent}}
</td>
<td style="padding:10px 20px;text-align:center;">
{{if .Uspeh}}
<span style="display:inline-block;padding:2px 10px;border-radius:20px;background:#f0fdf4;color:#16a34a;font-size:11px;font-weight:500;">Uspeh</span>
{{else}}
<span style="display:inline-block;padding:2px 10px;border-radius:20px;background:#fef2f2;color:#dc2626;font-size:11px;font-weight:500;">Neuspeh</span>
{{end}}
</td>
<td style="padding:10px 20px;font-size:13px;color:var(--tekst-sporedni);">
{{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}}
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}
<div style="padding:40px 20px;text-align:center;color:var(--tekst-sporedni);font-size:14px;">
Nema zabeleženih pokušaja prijave za ovog korisnika.
</div>
{{end}}
{{if .Istorija}}
<div style="overflow-x: auto">
<table style="width: 100%; border-collapse: collapse">
<thead>
<tr style="border-bottom: 0.5px solid var(--ivica)">
<th style="padding: 10px 20px; text-align: left; font-size: 12px; font-weight: 500; color: var(--tekst-sporedni); white-space: nowrap">Datum i vreme</th>
<th style="padding: 10px 20px; text-align: left; font-size: 12px; font-weight: 500; color: var(--tekst-sporedni)">IP adresa</th>
<th style="padding: 10px 20px; text-align: left; font-size: 12px; font-weight: 500; color: var(--tekst-sporedni)">Pregledač</th>
<th style="padding: 10px 20px; text-align: center; font-size: 12px; font-weight: 500; color: var(--tekst-sporedni)">Status</th>
<th style="padding: 10px 20px; text-align: left; font-size: 12px; font-weight: 500; color: var(--tekst-sporedni)">Razlog</th>
</tr>
</thead>
<tbody>
{{range .Istorija}}
<tr class="animiraj" style="border-bottom: 0.5px solid var(--ivica)">
<td style="padding: 10px 20px; font-size: 13px; color: var(--tekst-sporedni); white-space: nowrap">{{.Vreme.Format "02.01.2006. 15:04:05"}}</td>
<td style="padding: 10px 20px; font-size: 13px; color: var(--tekst-glavni); font-family: monospace">{{.IP}}</td>
<td style="padding: 10px 20px; font-size: 12px; color: var(--tekst-sporedni); max-width: 260px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap">{{.UserAgent}}</td>
<td style="padding: 10px 20px; text-align: center">
{{if .Uspeh}}
<span style="display: inline-block; padding: 2px 10px; border-radius: 20px; background: #035f1f; color: #16a34a; font-size: 11px; font-weight: 500">Uspeh</span>
{{else}}
<span style="display: inline-block; padding: 2px 10px; border-radius: 20px; background: #e21313; color: #dc2626; font-size: 11px; font-weight: 500">Neuspeh</span>
{{end}}
</td>
<td style="padding: 10px 20px; font-size: 13px; color: var(--tekst-sporedni)">
{{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}}
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}
<div style="padding: 40px 20px; text-align: center; color: var(--tekst-sporedni); font-size: 14px">Nema zabeleženih pokušaja prijave za ovog korisnika.</div>
{{end}}
</div>
</div>
{{end}}
+98 -120
View File
@@ -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" }}
<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; }
.animiraj:nth-child(4) { animation-delay: 0.28s; }
.animiraj:nth-child(5) { animation-delay: 0.34s; }
.animiraj:nth-child(1) { animation-delay: 0.1s; }
.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}}
{{ end }}
{{ define "sadrzaj" }}
<div style="display: flex; flex-direction: column; gap: 16px; max-width: 560px">
<!-- promena lozinke -->
<div class="kartica animiraj">
<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)">Promena lozinke</div>
{{define "sadrzaj"}}
<div style="display:flex;flex-direction:column;gap:16px;max-width:560px;">
<form method="POST" action="/admin/profil/lozinka">
<div style="display: flex; flex-direction: column; gap: 12px">
<div>
<label style="font-size: 13px; color: var(--tekst-sporedni); display: block; margin-bottom: 6px">Trenutna lozinka</label>
<input type="password" name="stara_lozinka" required style="width: 100%" />
</div>
<div>
<label style="font-size: 13px; color: var(--tekst-sporedni); display: block; margin-bottom: 6px">Nova lozinka</label>
<input type="password" name="nova_lozinka" required minlength="8" style="width: 100%" />
</div>
<div>
<label style="font-size: 13px; color: var(--tekst-sporedni); display: block; margin-bottom: 6px">Potvrda nove lozinke</label>
<input type="password" name="nova_lozinka_potvrda" required style="width: 100%" />
</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 novu lozinku</button>
</div>
</div>
</form>
</div>
<!-- promena lozinke -->
<div class="kartica animiraj">
<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);">
Promena lozinke
</div>
<!-- TOTP / 2FA -->
<div class="kartica animiraj">
<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)">Dvostepena verifikacija (2FA)</div>
<form method="POST" action="/admin/profil/lozinka">
<div style="display:flex;flex-direction:column;gap:12px;">
<div>
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:6px;">Trenutna lozinka</label>
<input type="password" name="stara_lozinka" required style="width:100%;">
</div>
<div>
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:6px;">Nova lozinka</label>
<input type="password" name="nova_lozinka" required minlength="8" style="width:100%;">
</div>
<div>
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:6px;">Potvrda nove lozinke</label>
<input type="password" name="nova_lozinka_potvrda" required style="width:100%;">
</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 novu lozinku
</button>
</div>
</div>
</form>
</div>
{{ if .TotpURI }}
<!-- postavljanje TOTP -->
{{ if eq .Greska "totp" }}
<div class="poruka-greska" style="margin-bottom: 14px">Neispravan kod. Pokušajte ponovo.</div>
{{ end }}
<!-- TOTP / 2FA -->
<div class="kartica animiraj">
<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);">
Dvostepena verifikacija (2FA)
</div>
<p style="font-size: 13px; color: var(--tekst-sporedni); margin-bottom: 16px; line-height: 1.5">Skenirajte QR kod u aplikaciji (Google Authenticator, Authy...), pa unesite generisani kod da potvrdite podešavanje.</p>
{{if .TotpURI}}
<!-- postavljanje TOTP -->
{{if eq .Greska "totp"}}
<div class="poruka-greska" style="margin-bottom:14px;">Neispravan kod. Pokušajte ponovo.</div>
{{end}}
<div style="text-align: center; margin-bottom: 16px">
<img src="{{ .TotpQR }}" alt="QR kod" style="width: 200px; height: 200px; border-radius: 8px; display: block; margin: 0 auto" />
</div>
<p style="font-size:13px;color:var(--tekst-sporedni);margin-bottom:16px;line-height:1.5;">
Skenirajte QR kod u aplikaciji (Google Authenticator, Authy...), pa unesite generisani kod da potvrdite podešavanje.
</p>
<details style="margin-bottom: 16px">
<summary style="font-size: 12px; color: var(--tekst-sporedni); cursor: pointer">Prikaži tajnu ručno</summary>
<code style="font-size: 12px; background: var(--pozadina); padding: 6px 10px; border-radius: 6px; display: block; margin-top: 8px; word-break: break-all; color: var(--tekst-glavni)">{{ .TotpTajna }}</code>
</details>
<div style="text-align:center;margin-bottom:16px;">
<img src="{{.TotpQR}}" alt="QR kod" style="width:200px;height:200px;border-radius:8px;display:block;margin:0 auto;">
</div>
<form method="POST" action="/admin/profil/totp/aktiviraj">
<input type="hidden" name="totp_tajna" value="{{ .TotpTajna }}" />
<div style="margin-bottom: 12px">
<label style="font-size: 13px; color: var(--tekst-sporedni); display: block; margin-bottom: 6px">Verifikacioni kod</label>
<input type="text" name="kod" inputmode="numeric" pattern="[0-9]{6}" maxlength="6" required autofocus style="width: 160px; font-size: 18px; text-align: center; letter-spacing: 4px" />
</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">Potvrdi i uključi 2FA</button>
</form>
<details style="margin-bottom:16px;">
<summary style="font-size:12px;color:var(--tekst-sporedni);cursor:pointer;">Prikaži tajnu ručno</summary>
<code style="font-size:12px;background:var(--pozadina);padding:6px 10px;border-radius:6px;display:block;margin-top:8px;word-break:break-all;color:var(--tekst-glavni);">{{.TotpTajna}}</code>
</details>
{{ else if .TotpAktivan }}
<!-- 2FA je uključena -->
<div style="display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px">
<div>
<div style="font-size: 14px; color: #16a34a; margin-bottom: 4px; font-weight: 500">✓ Dvostepena verifikacija je uključena</div>
<div style="font-size: 13px; color: var(--tekst-sporedni)">Prijava zahteva TOTP kod pored lozinke.</div>
</div>
<form method="POST" action="/admin/profil/totp/deaktiviraj" onsubmit="return confirm('Da li ste sigurni? Ovo će isključiti dvostepenu verifikaciju.');">
<button type="submit" style="padding: 9px 18px; background: #dc2626; color: #fff; border: none; border-radius: 8px; font-size: 14px; font-weight: 500; cursor: pointer">Deaktiviraj 2FA</button>
</form>
</div>
<form method="POST" action="/admin/profil/totp/aktiviraj">
<input type="hidden" name="totp_tajna" value="{{.TotpTajna}}">
<div style="margin-bottom:12px;">
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:6px;">Verifikacioni kod</label>
<input type="text" name="kod" inputmode="numeric" pattern="[0-9]{6}"
maxlength="6" required autofocus
style="width:160px;font-size:18px;text-align:center;letter-spacing:4px;">
</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;">
Potvrdi i uključi 2FA
</button>
</form>
{{else if .TotpAktivan}}
<!-- 2FA je uključena -->
<div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px;">
<div>
<div style="font-size:14px;color:#16a34a;margin-bottom:4px;font-weight:500;">✓ Dvostepena verifikacija je uključena</div>
<div style="font-size:13px;color:var(--tekst-sporedni);">Prijava zahteva TOTP kod pored lozinke.</div>
</div>
<form method="POST" action="/admin/profil/totp/deaktiviraj"
onsubmit="return confirm('Da li ste sigurni? Ovo će isključiti dvostepenu verifikaciju.')">
<button type="submit"
style="padding:9px 18px;background:#dc2626;color:#fff;border:none;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;">
Deaktiviraj 2FA
</button>
</form>
</div>
{{else}}
<!-- 2FA nije uključena -->
<div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px;">
<div>
<div style="font-size:14px;color:var(--tekst-glavni);margin-bottom:4px;">Status: <strong>Isključena</strong></div>
<div style="font-size:13px;color:var(--tekst-sporedni);">Preporučujemo uključivanje dvostepene verifikacije.</div>
</div>
<a href="/admin/profil/totp/pokreni"
style="padding:9px 18px;background:var(--sb-akcent);color:#fff;border-radius:8px;font-size:14px;font-weight:500;text-decoration:none;">
Podesi 2FA
</a>
</div>
{{end}}
</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}}
{{ else }}
<!-- 2FA nije uključena -->
<div style="display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px">
<div>
<div style="font-size: 14px; color: var(--tekst-glavni); margin-bottom: 4px">
Status:
<strong>Isključena</strong>
</div>
<div style="font-size: 13px; color: var(--tekst-sporedni)">Preporučujemo uključivanje dvostepene verifikacije.</div>
</div>
<a href="/admin/profil/totp/pokreni" style="padding: 9px 18px; background: var(--sb-akcent); color: #fff; border-radius: 8px; font-size: 14px; font-weight: 500; text-decoration: none">Podesi 2FA</a>
</div>
{{ end }}
</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}}
{{ end }}
+123 -109
View File
@@ -1,125 +1,139 @@
{{template "base" .}}
{{ template "base" . }}
{{define "naslov"}}Dashboard — NTech{{end}}
{{ define "naslov" }}Dashboard — NTech{{ end }}
{{define "dodatni-css"}}
{{ define "dodatni-css" }}
<style>
.dash-stat:nth-child(1) { animation-delay: 0.04s; }
.dash-stat:nth-child(2) { animation-delay: 0.10s; }
.dash-stat:nth-child(3) { animation-delay: 0.16s; }
.dash-stat:nth-child(4) { animation-delay: 0.22s; }
.dash-stat:nth-child(5) { animation-delay: 0.28s; }
.dash-kartica:nth-child(1) { animation-delay: 0.20s; }
.dash-kartica:nth-child(2) { animation-delay: 0.28s; }
.dash-kartica:nth-child(3) { animation-delay: 0.36s; }
.dash-stat:nth-child(1) { animation-delay: 0.04s; }
.dash-stat:nth-child(2) { animation-delay: 0.10s; }
.dash-stat:nth-child(3) { animation-delay: 0.16s; }
.dash-stat:nth-child(4) { animation-delay: 0.22s; }
.dash-stat:nth-child(5) { animation-delay: 0.28s; }
.dash-kartica:nth-child(1) { animation-delay: 0.20s; }
.dash-kartica:nth-child(2) { animation-delay: 0.28s; }
.dash-kartica:nth-child(3) { animation-delay: 0.36s; }
</style>
{{end}}
{{ end }}
{{define "sadrzaj"}}
{{if .FlashGreska}}
<div class="poruka-greska animiraj" style="margin-bottom:16px;">{{.FlashGreska}}</div>
{{end}}
{{ define "sadrzaj" }}
{{ if .FlashGreska }}
<div class="poruka-greska animiraj" style="margin-bottom:16px;">{{ .FlashGreska }}</div>
{{ end }}
<div class="grid grid-cols-2 md:grid-cols-5 gap-3 mb-6">
<!-- stat kartice -->
<div class="kartica dash-stat animiraj">
<div style="width:36px;height:36px;border-radius:8px;background:#eff2ff;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="#4f7ef8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/></svg>
</div>
<div style="font-size:22px;font-weight:500;color:var(--tekst-glavni);">{{.BrojArtikala}}</div>
<div style="font-size:12px;color:var(--tekst-sporedni);margin-top:4px;">Artikala na stanju</div>
</div>
<!-- stat kartice -->
<div class="kartica dash-stat animiraj">
<div style="width:36px;height:36px;border-radius:8px;background:#eff2ff;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="#4f7ef8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" />
</svg>
</div>
<div style="font-size:22px;font-weight:500;color:var(--tekst-glavni);">{{ .BrojArtikala }}</div>
<div style="font-size:12px;color:var(--tekst-sporedni);margin-top:4px;">Artikala na stanju</div>
</div>
<div class="kartica dash-stat animiraj">
<div style="width:36px;height:36px;border-radius:8px;background:#f0fdf4;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="#16a34a" 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>
</div>
<div style="font-size:22px;font-weight:500;color:var(--tekst-glavni);">{{.AktivniServisi}}</div>
<div style="font-size:12px;color:var(--tekst-sporedni);margin-top:4px;">Aktivnih servisa</div>
</div>
<div class="kartica dash-stat animiraj">
<div style="width:36px;height:36px;border-radius:8px;background:#f0fdf4;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="#16a34a" 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>
</div>
<div style="font-size:22px;font-weight:500;color:var(--tekst-glavni);">{{ .AktivniServisi }}</div>
<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>
</div>
<div style="font-size:22px;font-weight:500;color:var(--tekst-glavni);">{{printf "%.0f" .PrihodOvogMeseca}} din</div>
<div style="font-size:12px;color:var(--tekst-sporedni);margin-top:4px;">Prihod ovog meseca</div>
</div>
{{end}}
{{ 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>
</div>
<div style="font-size:22px;font-weight:500;color:var(--tekst-glavni);">{{ printf "%.0f" .PrihodOvogMeseca }} din</div>
<div style="font-size:12px;color:var(--tekst-sporedni);margin-top:4px;">Prihod ovog meseca</div>
</div>
{{ 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;">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#dc2626" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
</div>
<div style="font-size:22px;font-weight:500;color:var(--tekst-glavni);">{{.KriticnaZaliha}}</div>
<div style="font-size:12px;color:var(--tekst-sporedni);margin-top:4px;">Kritično niska zaliha</div>
</div>
<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;">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#dc2626" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
<line x1="12" y1="9" x2="12" y2="13" />
<line x1="12" y1="17" x2="12.01" y2="17" />
</svg>
</div>
<div style="font-size:22px;font-weight:500;color:var(--tekst-glavni);">{{ .KriticnaZaliha }}</div>
<div style="font-size:12px;color:var(--tekst-sporedni);margin-top:4px;">Kritično niska zaliha</div>
</div>
<a href="/podsetnici" class="kartica dash-stat animiraj" style="text-decoration:none;display:block;">
<div style="width:36px;height:36px;border-radius:8px;background:#f0f9ff;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="#0284c7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>
</div>
<div style="font-size:22px;font-weight:500;color:var(--tekst-glavni);">{{.AktivniPodsetnici}}</div>
<div style="font-size:12px;color:var(--tekst-sporedni);margin-top:4px;">Aktivnih podsetnika</div>
</a>
<a href="/podsetnici" class="kartica dash-stat animiraj" style="text-decoration:none;display:block;">
<div style="width:36px;height:36px;border-radius:8px;background:#f0f9ff;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="#0284c7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" />
<path d="M13.73 21a2 2 0 0 1-3.46 0" />
</svg>
</div>
<div style="font-size:22px;font-weight:500;color:var(--tekst-glavni);">{{ .AktivniPodsetnici }}</div>
<div style="font-size:12px;color:var(--tekst-sporedni);margin-top:4px;">Aktivnih podsetnika</div>
</a>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<!-- poslednji servisi -->
<div class="kartica dash-kartica animiraj">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:14px;">
<span style="font-size:14px;font-weight:500;color:var(--tekst-glavni);">Poslednji servisi</span>
</div>
{{range .PoslednjiServisi}}
<div style="display:flex;align-items:center;gap:10px;padding:8px 0;border-bottom:0.5px solid var(--ivica);">
<div style="width:8px;height:8px;border-radius:50%;flex-shrink:0;background:{{.BojaTacke}};"></div>
<span style="font-size:13px;color:var(--tekst-glavni);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">{{.Uredjaj}}</span>
<span style="font-size:11px;color:var(--tekst-sporedni);white-space:nowrap;">{{.DatumPrijema}}</span>
</div>
{{else}}
<div style="font-size:13px;color:var(--tekst-sporedni);padding:8px 0;">Nema servisnih naloga.</div>
{{end}}
</div>
<!-- poslednji servisi -->
<div class="kartica dash-kartica animiraj">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:14px;">
<span style="font-size:14px;font-weight:500;color:var(--tekst-glavni);">Poslednji servisi</span>
</div>
{{ range .PoslednjiServisi }}
<div style="display:flex;align-items:center;gap:10px;padding:8px 0;border-bottom:0.5px solid var(--ivica);">
<div style="width:8px;height:8px;border-radius:50%;flex-shrink:0;background:{{ .BojaTacke }};"></div>
<span style="font-size:13px;color:var(--tekst-glavni);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">{{ .Uredjaj }}</span>
<span style="font-size:11px;color:var(--tekst-sporedni);white-space:nowrap;">{{ .DatumPrijema }}</span>
</div>
{{ else }}
<div style="font-size:13px;color:var(--tekst-sporedni);padding:8px 0;">Nema servisnih naloga.</div>
{{ end }}
</div>
<!-- kritične zalihe -->
<div class="kartica dash-kartica animiraj">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:14px;">
<span style="font-size:14px;font-weight:500;color:var(--tekst-glavni);">Kritične zalihe</span>
<span style="font-size:11px;padding:2px 8px;border-radius:20px;background:rgba(220,38,38,0.15);color:#ef4444;font-weight:500;">Upozorenje</span>
</div>
{{range .KriticneZalihe}}
<div style="display:flex;align-items:center;gap:10px;padding:8px 0;border-bottom:0.5px solid var(--ivica);">
<div style="width:8px;height:8px;border-radius:50%;flex-shrink:0;background:{{.BojaTacke}};"></div>
<span style="font-size:13px;color:var(--tekst-glavni);flex:1;">{{.Naziv}}</span>
<span style="font-size:12px;color:var(--tekst-sporedni);">{{.Kolicina}} kom</span>
</div>
{{else}}
<div style="font-size:13px;color:var(--tekst-sporedni);padding:8px 0;">Sve zalihe su uredne.</div>
{{end}}
</div>
<!-- kritične zalihe -->
<div class="kartica dash-kartica animiraj">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:14px;">
<span style="font-size:14px;font-weight:500;color:var(--tekst-glavni);">Kritične zalihe</span>
<span style="font-size:11px;padding:2px 8px;border-radius:20px;background:rgba(220,38,38,0.15);color:#ef4444;font-weight:500;">Upozorenje</span>
</div>
{{ range .KriticneZalihe }}
<div style="display:flex;align-items:center;gap:10px;padding:8px 0;border-bottom:0.5px solid var(--ivica);">
<div style="width:8px;height:8px;border-radius:50%;flex-shrink:0;background:{{ .BojaTacke }};"></div>
<span style="font-size:13px;color:var(--tekst-glavni);flex:1;">{{ .Naziv }}</span>
<span style="font-size:12px;color:var(--tekst-sporedni);">{{ .Kolicina }} kom</span>
</div>
{{ else }}
<div style="font-size:13px;color:var(--tekst-sporedni);padding:8px 0;">Sve zalihe su uredne.</div>
{{ end }}
</div>
<!-- poslednje prodaje -->
<div class="kartica dash-kartica animiraj">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:14px;">
<span style="font-size:14px;font-weight:500;color:var(--tekst-glavni);">Poslednje prodaje</span>
</div>
{{range .PoslednjeProdaje}}
<div style="display:flex;align-items:center;gap:10px;padding:8px 0;border-bottom:0.5px solid var(--ivica);">
<div style="flex:1;min-width:0;">
<div style="font-size:13px;color:var(--tekst-glavni);font-family:monospace;">{{.BrojNaloga}}</div>
{{if .KlijentNaziv}}
<div style="font-size:11px;color:var(--tekst-sporedni);margin-top:1px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">{{.KlijentNaziv}}</div>
{{end}}
</div>
<div style="text-align:right;flex-shrink:0;">
<div style="font-size:13px;font-weight:500;color:var(--tekst-glavni);">{{printf "%.0f" .Ukupno}} din</div>
<div style="font-size:11px;color:var(--tekst-sporedni);margin-top:1px;">{{.Datum}}</div>
</div>
</div>
{{else}}
<div style="font-size:13px;color:var(--tekst-sporedni);padding:8px 0;">Nema prodajnih naloga.</div>
{{end}}
</div>
<!-- poslednje prodaje -->
<div class="kartica dash-kartica animiraj">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:14px;">
<span style="font-size:14px;font-weight:500;color:var(--tekst-glavni);">Poslednje prodaje</span>
</div>
{{ range .PoslednjeProdaje }}
<div style="display:flex;align-items:center;gap:10px;padding:8px 0;border-bottom:0.5px solid var(--ivica);">
<div style="flex:1;min-width:0;">
<div style="font-size:13px;color:var(--tekst-glavni);font-family:monospace;">{{ .BrojNaloga }}</div>
{{ if .KlijentNaziv }}
<div style="font-size:11px;color:var(--tekst-sporedni);margin-top:1px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">{{ .KlijentNaziv }}</div>
{{ end }}
</div>
<div style="text-align:right;flex-shrink:0;">
<div style="font-size:13px;font-weight:500;color:var(--tekst-glavni);">{{ printf "%.0f" .Ukupno }} din</div>
<div style="font-size:11px;color:var(--tekst-sporedni);margin-top:1px;">{{ .Datum }}</div>
</div>
</div>
{{ else }}
<div style="font-size:13px;color:var(--tekst-sporedni);padding:8px 0;">Nema prodajnih naloga.</div>
{{ end }}
</div>
</div>
{{end}}
{{ end }}
+24 -27
View File
@@ -4,35 +4,32 @@
{{define "dodatni-css"}}
<style>
.poruka-animacija { animation: slideDown 0.3s ease forwards; }
/* stagger — svaki red se pojavljuje malo kasnije */
.dobavljaci-tabela tbody tr:nth-child(1) { animation-delay: 0.04s; }
.dobavljaci-tabela tbody tr:nth-child(2) { animation-delay: 0.08s; }
.dobavljaci-tabela tbody tr:nth-child(3) { animation-delay: 0.12s; }
.dobavljaci-tabela tbody tr:nth-child(4) { animation-delay: 0.16s; }
.dobavljaci-tabela tbody tr:nth-child(5) { animation-delay: 0.20s; }
.dobavljaci-tabela tbody tr:nth-child(6) { animation-delay: 0.24s; }
.dobavljaci-tabela tbody tr:nth-child(7) { animation-delay: 0.28s; }
.dobavljaci-tabela tbody tr:nth-child(8) { animation-delay: 0.32s; }
.dobavljaci-tabela tbody tr:nth-child(9) { animation-delay: 0.36s; }
.dobavljaci-tabela tbody tr:nth-child(10) { animation-delay: 0.40s; }
.dobavljaci-kartice {
display: none;
flex-direction: column;
gap: 12px;
}
.dobavljac-kartica:nth-child(1) { animation-delay: 0.04s; }
.dobavljac-kartica:nth-child(2) { animation-delay: 0.10s; }
.dobavljac-kartica:nth-child(3) { animation-delay: 0.16s; }
.dobavljac-kartica:nth-child(4) { animation-delay: 0.22s; }
.dobavljac-kartica:nth-child(5) { animation-delay: 0.28s; }
.poruka-animacija { animation: slideDown 0.3s ease forwards; }
.dobavljaci-tabela tbody tr:nth-child(1) { animation-delay: 0.04s; }
.dobavljaci-tabela tbody tr:nth-child(2) { animation-delay: 0.08s; }
.dobavljaci-tabela tbody tr:nth-child(3) { animation-delay: 0.12s; }
.dobavljaci-tabela tbody tr:nth-child(4) { animation-delay: 0.16s; }
.dobavljaci-tabela tbody tr:nth-child(5) { animation-delay: 0.20s; }
.dobavljaci-tabela tbody tr:nth-child(6) { animation-delay: 0.24s; }
.dobavljaci-tabela tbody tr:nth-child(7) { animation-delay: 0.28s; }
.dobavljaci-tabela tbody tr:nth-child(8) { animation-delay: 0.32s; }
.dobavljaci-tabela tbody tr:nth-child(9) { animation-delay: 0.36s; }
.dobavljaci-tabela tbody tr:nth-child(10) { animation-delay: 0.40s; }
.dobavljaci-kartice { display: none; flex-direction: column; gap: 12px; }
.dobavljac-kartica:nth-child(1) { animation-delay: 0.04s; }
.dobavljac-kartica:nth-child(2) { animation-delay: 0.10s; }
.dobavljac-kartica:nth-child(3) { animation-delay: 0.16s; }
.dobavljac-kartica:nth-child(4) { animation-delay: 0.22s; }
.dobavljac-kartica:nth-child(5) { animation-delay: 0.28s; }
@media (max-width: 768px) {
.dobavljaci-tabela { display: none; }
.dobavljaci-kartice { display: flex; }
.dobavljaci-tabela {
display: none;
}
.dobavljaci-kartice {
display: flex;
}
}
</style>
{{end}}
+8 -74
View File
@@ -5,88 +5,22 @@
{{define "dodatni-css"}}
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.4/dist/chart.umd.min.js"></script>
<style>
.izv-naslov {
font-size: 13px;
font-weight: 500;
color: var(--tekst-sporedni);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 14px;
}
.toggle-red {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 16px;
}
.toggle-switch {
position: relative;
display: inline-block;
width: 40px;
height: 22px;
flex-shrink: 0;
}
.izv-naslov { font-size: 13px; font-weight: 500; color: var(--tekst-sporedni); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 14px; }
.toggle-red { display: flex; align-items: center; gap: 10px; margin-bottom: 16px; }
.toggle-switch { position: relative; display: inline-block; width: 40px; height: 22px; flex-shrink: 0; }
.toggle-switch input { opacity: 0; width: 0; height: 0; }
.toggle-slider {
position: absolute;
inset: 0;
background: var(--ivica);
border-radius: 22px;
cursor: pointer;
transition: background 0.2s;
}
.toggle-slider::before {
content: '';
position: absolute;
width: 16px;
height: 16px;
left: 3px;
top: 3px;
background: #fff;
border-radius: 50%;
transition: transform 0.2s;
}
.toggle-slider { position: absolute; inset: 0; background: var(--ivica); border-radius: 22px; cursor: pointer; transition: background 0.2s; }
.toggle-slider::before { content: ''; position: absolute; width: 16px; height: 16px; left: 3px; top: 3px; background: #fff; border-radius: 50%; transition: transform 0.2s; }
.toggle-switch input:checked + .toggle-slider { background: var(--sb-akcent); }
.toggle-switch input:checked + .toggle-slider::before { transform: translateX(18px); }
.rang-broj {
display: inline-flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
border-radius: 50%;
background: var(--pozadina);
font-size: 11px;
font-weight: 600;
color: var(--tekst-sporedni);
flex-shrink: 0;
}
.rang-broj { display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; border-radius: 50%; background: var(--pozadina); font-size: 11px; font-weight: 600; color: var(--tekst-sporedni); flex-shrink: 0; }
.rang-broj.zlatni { background: #fef3c7; color: #92400e; }
.badge-dana {
display: inline-block;
padding: 2px 8px;
border-radius: 20px;
font-size: 11px;
font-weight: 500;
white-space: nowrap;
}
.badge-dana { display: inline-block; padding: 2px 8px; border-radius: 20px; font-size: 11px; font-weight: 500; white-space: nowrap; }
.badge-dana.upozorenje { background: #fffbeb; color: #b45309; }
.badge-dana.kritican { background: #fef2f2; color: #dc2626; }
.badge-dana.kritican { background: #fef2f2; color: #dc2626; }
.izv-sekcija:nth-child(1) { animation-delay: 0.04s; }
.izv-sekcija:nth-child(2) { animation-delay: 0.12s; }
.izv-sekcija:nth-child(3) { animation-delay: 0.20s; }
.izv-grid-kartica:nth-child(1) { animation-delay: 0.24s; }
.izv-grid-kartica:nth-child(2) { animation-delay: 0.32s; }
</style>
+1 -5
View File
@@ -5,11 +5,7 @@
{{define "dodatni-css"}}
<style>
.poruka-animacija { animation: slideDown 0.3s ease forwards; }
.kat-forma-kartica {
animation-delay: 0.04s;
}
.kat-forma-kartica { animation-delay: 0.04s; }
.kat-red:nth-child(1) { animation-delay: 0.12s; }
.kat-red:nth-child(2) { animation-delay: 0.18s; }
.kat-red:nth-child(3) { animation-delay: 0.24s; }
+16 -27
View File
@@ -5,34 +5,23 @@
{{define "dodatni-css"}}
<style>
.poruka-animacija { animation: slideDown 0.3s ease forwards; }
.klijenti-tabela tbody tr:nth-child(1) { animation-delay: 0.04s; }
.klijenti-tabela tbody tr:nth-child(2) { animation-delay: 0.08s; }
.klijenti-tabela tbody tr:nth-child(3) { animation-delay: 0.12s; }
.klijenti-tabela tbody tr:nth-child(4) { animation-delay: 0.16s; }
.klijenti-tabela tbody tr:nth-child(5) { animation-delay: 0.20s; }
.klijenti-tabela tbody tr:nth-child(6) { animation-delay: 0.24s; }
.klijenti-tabela tbody tr:nth-child(7) { animation-delay: 0.28s; }
.klijenti-tabela tbody tr:nth-child(8) { animation-delay: 0.32s; }
.klijenti-tabela tbody tr:nth-child(9) { animation-delay: 0.36s; }
.klijenti-tabela tbody tr:nth-child(1) { animation-delay: 0.04s; }
.klijenti-tabela tbody tr:nth-child(2) { animation-delay: 0.08s; }
.klijenti-tabela tbody tr:nth-child(3) { animation-delay: 0.12s; }
.klijenti-tabela tbody tr:nth-child(4) { animation-delay: 0.16s; }
.klijenti-tabela tbody tr:nth-child(5) { animation-delay: 0.20s; }
.klijenti-tabela tbody tr:nth-child(6) { animation-delay: 0.24s; }
.klijenti-tabela tbody tr:nth-child(7) { animation-delay: 0.28s; }
.klijenti-tabela tbody tr:nth-child(8) { animation-delay: 0.32s; }
.klijenti-tabela tbody tr:nth-child(9) { animation-delay: 0.36s; }
.klijenti-tabela tbody tr:nth-child(10) { animation-delay: 0.40s; }
.klijenti-kartice {
display: none;
flex-direction: column;
gap: 12px;
}
.klijent-kartica:nth-child(1) { animation-delay: 0.04s; }
.klijent-kartica:nth-child(2) { animation-delay: 0.10s; }
.klijent-kartica:nth-child(3) { animation-delay: 0.16s; }
.klijent-kartica:nth-child(4) { animation-delay: 0.22s; }
.klijent-kartica:nth-child(5) { animation-delay: 0.28s; }
@media (max-width: 768px) {
.klijenti-tabela { display: none; }
.klijenti-kartice { display: flex; }
}
.klijenti-kartice { display: none; flex-direction: column; gap: 12px; }
.klijent-kartica:nth-child(1) { animation-delay: 0.04s; }
.klijent-kartica:nth-child(2) { animation-delay: 0.10s; }
.klijent-kartica:nth-child(3) { animation-delay: 0.16s; }
.klijent-kartica:nth-child(4) { animation-delay: 0.22s; }
.klijent-kartica:nth-child(5) { animation-delay: 0.28s; }
@media (max-width: 768px) { .klijenti-tabela { display: none; } .klijenti-kartice { display: flex; } }
</style>
{{end}}
+11 -21
View File
@@ -4,33 +4,23 @@
{{define "dodatni-css"}}
<style>
.magacin-tabela tbody tr:nth-child(1) { animation-delay: 0.04s; }
.magacin-tabela tbody tr:nth-child(2) { animation-delay: 0.08s; }
.magacin-tabela tbody tr:nth-child(3) { animation-delay: 0.12s; }
.magacin-tabela tbody tr:nth-child(4) { animation-delay: 0.16s; }
.magacin-tabela tbody tr:nth-child(5) { animation-delay: 0.20s; }
.magacin-tabela tbody tr:nth-child(6) { animation-delay: 0.24s; }
.magacin-tabela tbody tr:nth-child(7) { animation-delay: 0.28s; }
.magacin-tabela tbody tr:nth-child(8) { animation-delay: 0.32s; }
.magacin-tabela tbody tr:nth-child(9) { animation-delay: 0.36s; }
.magacin-tabela tbody tr:nth-child(1) { animation-delay: 0.04s; }
.magacin-tabela tbody tr:nth-child(2) { animation-delay: 0.08s; }
.magacin-tabela tbody tr:nth-child(3) { animation-delay: 0.12s; }
.magacin-tabela tbody tr:nth-child(4) { animation-delay: 0.16s; }
.magacin-tabela tbody tr:nth-child(5) { animation-delay: 0.20s; }
.magacin-tabela tbody tr:nth-child(6) { animation-delay: 0.24s; }
.magacin-tabela tbody tr:nth-child(7) { animation-delay: 0.28s; }
.magacin-tabela tbody tr:nth-child(8) { animation-delay: 0.32s; }
.magacin-tabela tbody tr:nth-child(9) { animation-delay: 0.36s; }
.magacin-tabela tbody tr:nth-child(10) { animation-delay: 0.40s; }
.magacin-kartice {
display: none;
flex-direction: column;
gap: 12px;
}
.magacin-kartice { display: none; flex-direction: column; gap: 12px; }
.magacin-kartica:nth-child(1) { animation-delay: 0.04s; }
.magacin-kartica:nth-child(2) { animation-delay: 0.10s; }
.magacin-kartica:nth-child(3) { animation-delay: 0.16s; }
.magacin-kartica:nth-child(4) { animation-delay: 0.22s; }
.magacin-kartica:nth-child(5) { animation-delay: 0.28s; }
@media (max-width: 768px) {
.magacin-tabela { display: none; }
.magacin-kartice { display: flex; }
}
@media (max-width: 768px) { .magacin-tabela { display: none; } .magacin-kartice { display: flex; } }
</style>
{{end}}
+2 -12
View File
@@ -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; } }
</style>
{{end}}
+2 -12
View File
@@ -4,22 +4,12 @@
{{define "dodatni-css"}}
<style>
@keyframes modalIn {
from { opacity: 0; transform: translateY(-16px) scale(0.97); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes modalIn { from { opacity: 0; transform: translateY(-16px) scale(0.97); } to { opacity: 1; transform: translateY(0) scale(1); } }
.forma-kartica:nth-child(1) { animation-delay: 0.04s; }
.forma-kartica:nth-child(2) { animation-delay: 0.12s; }
.greska-animacija { animation: shake 0.4s ease; }
.modal-sadrzaj { animation: modalIn 0.25s ease forwards; }
@media (max-width: 768px) {
.stavke-tabela-wrapper { display: none; }
.stavke-kartice { display: flex !important; }
}
@media (max-width: 768px) { .stavke-tabela-wrapper { display: none; } .stavke-kartice { display: flex !important; } }
</style>
{{end}}
+11 -22
View File
@@ -5,34 +5,23 @@
{{define "dodatni-css"}}
<style>
.poruka-animacija { animation: slideDown 0.3s ease forwards; }
.nabavke-tabela tbody tr:nth-child(1) { animation-delay: 0.04s; }
.nabavke-tabela tbody tr:nth-child(2) { animation-delay: 0.08s; }
.nabavke-tabela tbody tr:nth-child(3) { animation-delay: 0.12s; }
.nabavke-tabela tbody tr:nth-child(4) { animation-delay: 0.16s; }
.nabavke-tabela tbody tr:nth-child(5) { animation-delay: 0.20s; }
.nabavke-tabela tbody tr:nth-child(6) { animation-delay: 0.24s; }
.nabavke-tabela tbody tr:nth-child(7) { animation-delay: 0.28s; }
.nabavke-tabela tbody tr:nth-child(8) { animation-delay: 0.32s; }
.nabavke-tabela tbody tr:nth-child(9) { animation-delay: 0.36s; }
.nabavke-tabela tbody tr:nth-child(1) { animation-delay: 0.04s; }
.nabavke-tabela tbody tr:nth-child(2) { animation-delay: 0.08s; }
.nabavke-tabela tbody tr:nth-child(3) { animation-delay: 0.12s; }
.nabavke-tabela tbody tr:nth-child(4) { animation-delay: 0.16s; }
.nabavke-tabela tbody tr:nth-child(5) { animation-delay: 0.20s; }
.nabavke-tabela tbody tr:nth-child(6) { animation-delay: 0.24s; }
.nabavke-tabela tbody tr:nth-child(7) { animation-delay: 0.28s; }
.nabavke-tabela tbody tr:nth-child(8) { animation-delay: 0.32s; }
.nabavke-tabela tbody tr:nth-child(9) { animation-delay: 0.36s; }
.nabavke-tabela tbody tr:nth-child(10) { animation-delay: 0.40s; }
.nabavke-kartice {
display: none;
flex-direction: column;
gap: 12px;
}
.nabavke-kartice { display: none; flex-direction: column; gap: 12px; }
.nabavka-kartica:nth-child(1) { animation-delay: 0.04s; }
.nabavka-kartica:nth-child(2) { animation-delay: 0.10s; }
.nabavka-kartica:nth-child(3) { animation-delay: 0.16s; }
.nabavka-kartica:nth-child(4) { animation-delay: 0.22s; }
.nabavka-kartica:nth-child(5) { animation-delay: 0.28s; }
@media (max-width: 768px) {
.nabavke-tabela { display: none; }
.nabavke-kartice { display: flex; }
}
@media (max-width: 768px) { .nabavke-tabela { display: none; } .nabavke-kartice { display: flex; } }
</style>
{{end}}
+9 -66
View File
@@ -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; }
</style>
{{end}}
+2 -10
View File
@@ -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; } }
</style>
{{end}}
+6 -37
View File
@@ -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; }
</style>
{{end}}
+3 -15
View File
@@ -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; } }
</style>
{{end}}
+1 -3
View File
@@ -4,9 +4,7 @@
{{define "dodatni-css"}}
<style>
.greska-animacija {
animation: shake 0.4s ease;
}
.greska-animacija { animation: shake 0.4s ease; }
</style>
{{end}}
+65 -151
View File
@@ -1,160 +1,74 @@
<!DOCTYPE html>
<html lang="sr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Prijava — NTech</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #0f1117;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
position: relative;
}
.kartica {
background: #1a1d27;
border: 0.5px solid #2d3148;
border-radius: 16px;
padding: 40px;
width: 100%;
max-width: 380px;
}
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Prijava — NTech</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f1117; min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 16px; position: relative; }
.kartica { background: #1a1d27; border: 0.5px solid #2d3148; border-radius: 16px; padding: 40px; width: 100%; max-width: 380px; }
{{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; }
</style>
</head>
<body>
{{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;
}
</style>
</head>
<body>
{{if .LoginPozadina}}
<div style="position:fixed;inset:0;z-index:-1;background-image:url('{{.LoginPozadina}}');background-size:cover;background-position:center;{{if ne .LoginPozadinaBlurPozadine "0"}}filter:blur({{.LoginPozadinaBlurPozadine}}px);transform:scale(1.05);{{end}}"></div>
<div style="position:fixed;inset:0;z-index:0;background:rgba(0,0,0,{{.LoginPozadinaOpacity}}%);"></div>
<div style="position:fixed;inset:0;display:flex;align-items:center;justify-content:center;z-index:1;padding:16px;">
{{end}}
<div style="position:fixed;inset:0;z-index:-1;background-image:url('{{.LoginPozadina}}');background-size:cover;background-position:center;{{if ne .LoginPozadinaBlurPozadine " 0"}}filter:blur({{.LoginPozadinaBlurPozadine}}px);transform:scale(1.05);{{end}}"></div>
<div style="position:fixed;inset:0;z-index:0;background:rgba(0,0,0,{{.LoginPozadinaOpacity}}%);"></div>
<div style="position:fixed;inset:0;display:flex;align-items:center;justify-content:center;z-index:1;padding:16px;">
{{end}}
<div class="kartica animiraj">
<div class="logo">
<div class="logo-naziv">NTech</div>
<div class="logo-podnazlov">Sistem za upravljanje</div>
</div>
<div class="kartica animiraj">
<div class="logo">
<div class="logo-naziv">NTech</div>
<div class="logo-podnazlov">Sistem za upravljanje</div>
</div>
<h1>Prijava</h1>
<h1>Prijava</h1>
{{if eq .Greska "1"}}
<div class="greska">Pogrešno korisničko ime ili lozinka.</div>
{{else if eq .Greska "2"}}
<div class="greska">Greška na serveru. Pokušajte ponovo.</div>
{{else if eq .Greska "zakljucano"}}
<div class="greska">
Previše neuspelih pokušaja prijave. IP adresa je privremeno blokirana.
{{if .Preostalo}}<br>Pokušajte ponovo za: <strong>{{.Preostalo}}</strong>{{end}}
{{if eq .Greska "1"}}
<div class="greska">Pogrešno korisničko ime ili lozinka.</div>
{{else if eq .Greska "2"}}
<div class="greska">Greška na serveru. Pokušajte ponovo.</div>
{{else if eq .Greska "zakljucano"}}
<div class="greska">
Previše neuspelih pokušaja prijave. IP adresa je privremeno blokirana.
{{if .Preostalo}}<br>Pokušajte ponovo za: <strong>{{.Preostalo}}</strong>{{end}}
</div>
{{end}}
{{if .Sacuvano}}
<div class="uspeh">Nalog je kreiran. Možete se prijaviti.</div>
{{end}}
<form method="POST" action="/prijava">
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
<div class="polje">
<label for="korisnicko_ime">Korisničko ime</label>
<input type="text" id="korisnicko_ime" name="korisnicko_ime" autocomplete="username" autofocus required>
</div>
<div class="polje">
<label for="lozinka">Lozinka</label>
<input type="password" id="lozinka" name="lozinka" autocomplete="current-password" required>
</div>
<button type="submit" class="dugme">Prijavi se</button>
</form>
</div>
{{if .LoginPozadina}}
</div>
{{end}}
{{if .Sacuvano}}
<div class="uspeh">Nalog je kreiran. Možete se prijaviti.</div>
{{end}}
<form method="POST" action="/prijava">
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
<div class="polje">
<label for="korisnicko_ime">Korisničko ime</label>
<input type="text" id="korisnicko_ime" name="korisnicko_ime"
autocomplete="username" autofocus required>
</div>
<div class="polje">
<label for="lozinka">Lozinka</label>
<input type="password" id="lozinka" name="lozinka"
autocomplete="current-password" required>
</div>
<button type="submit" class="dugme">Prijavi se</button>
</form>
</div>
{{if .LoginPozadina}}
</div>
{{end}}
</body>
</body>
</html>
+57 -102
View File
@@ -1,103 +1,71 @@
{{template "base" .}}
{{define "naslov"}}Prodaja — NTech{{end}}
{{define "dodatni-css"}}
{{template "base" .}} {{define "naslov"}}Prodaja — NTech{{end}} {{define "dodatni-css"}}
<style>
.poruka-animacija { animation: slideDown 0.3s ease forwards; }
.prodaja-tabela tbody tr:nth-child(1) { animation-delay: 0.04s; }
.prodaja-tabela tbody tr:nth-child(2) { animation-delay: 0.08s; }
.prodaja-tabela tbody tr:nth-child(3) { animation-delay: 0.12s; }
.prodaja-tabela tbody tr:nth-child(4) { animation-delay: 0.16s; }
.prodaja-tabela tbody tr:nth-child(5) { animation-delay: 0.20s; }
.prodaja-tabela tbody tr:nth-child(6) { animation-delay: 0.24s; }
.prodaja-tabela tbody tr:nth-child(7) { animation-delay: 0.28s; }
.prodaja-tabela tbody tr:nth-child(8) { animation-delay: 0.32s; }
.prodaja-tabela tbody tr:nth-child(9) { animation-delay: 0.36s; }
.prodaja-tabela tbody tr:nth-child(10) { animation-delay: 0.40s; }
.prodaja-kartice {
display: none;
flex-direction: column;
gap: 12px;
}
.prodaja-kartica:nth-child(1) { animation-delay: 0.04s; }
.prodaja-kartica:nth-child(2) { animation-delay: 0.10s; }
.prodaja-kartica:nth-child(3) { animation-delay: 0.16s; }
.prodaja-kartica:nth-child(4) { animation-delay: 0.22s; }
.prodaja-kartica:nth-child(5) { animation-delay: 0.28s; }
@media (max-width: 768px) {
.prodaja-tabela { display: none; }
.prodaja-kartice { display: flex; }
}
.poruka-animacija { animation: slideDown 0.3s ease forwards; }
.prodaja-tabela tbody tr:nth-child(1) { animation-delay: 0.04s; }
.prodaja-tabela tbody tr:nth-child(2) { animation-delay: 0.08s; }
.prodaja-tabela tbody tr:nth-child(3) { animation-delay: 0.12s; }
.prodaja-tabela tbody tr:nth-child(4) { animation-delay: 0.16s; }
.prodaja-tabela tbody tr:nth-child(5) { animation-delay: 0.2s; }
.prodaja-tabela tbody tr:nth-child(6) { animation-delay: 0.24s; }
.prodaja-tabela tbody tr:nth-child(7) { animation-delay: 0.28s; }
.prodaja-tabela tbody tr:nth-child(8) { animation-delay: 0.32s; }
.prodaja-tabela tbody tr:nth-child(9) { animation-delay: 0.36s; }
.prodaja-tabela tbody tr:nth-child(10) { animation-delay: 0.4s; }
.prodaja-kartice { display: none; flex-direction: column; gap: 12px; }
.prodaja-kartica:nth-child(1) { animation-delay: 0.04s; }
.prodaja-kartica:nth-child(2) { animation-delay: 0.1s; }
.prodaja-kartica:nth-child(3) { animation-delay: 0.16s; }
.prodaja-kartica:nth-child(4) { animation-delay: 0.22s; }
.prodaja-kartica:nth-child(5) { animation-delay: 0.28s; }
@media (max-width: 768px) { .prodaja-tabela { display: none; } .prodaja-kartice { display: flex; } }
</style>
{{end}}
{{define "sadrzaj"}}
<div style="display:flex;flex-direction:column;gap:16px;">
{{end}} {{define "sadrzaj"}}
<div style="display: flex; flex-direction: column; gap: 16px">
{{if .Sacuvano}}
<div class="poruka-uspeh poruka-animacija">Prodaja je uspešno sačuvana.</div>
{{end}}
{{if .Obrisan}}
{{end}} {{if .Obrisan}}
<div class="poruka-uspeh poruka-animacija">Prodajni nalog je uspešno obrisan.</div>
{{end}}
<!-- zaglavlje sa pretragom i dugmetom -->
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;">
<form method="GET" action="/prodaja" style="display:flex;gap:8px;flex:1;min-width:200px;">
<input type="text" name="pretraga" value="{{.Pretraga}}"
placeholder="Pretraži po broju naloga..."
style="flex:1;">
<button type="submit" class="btn-sekundarno" style="white-space:nowrap;">Pretraži</button>
<div style="display: flex; align-items: center; gap: 10px; flex-wrap: wrap">
<form method="GET" action="/prodaja" style="display: flex; gap: 8px; flex: 1; min-width: 200px">
<input type="text" name="pretraga" value="{{.Pretraga}}" placeholder="Pretraži po broju naloga..." style="flex: 1" />
<button type="submit" class="btn-sekundarno" style="white-space: nowrap">Pretraži</button>
{{if .Pretraga}}
<a href="/prodaja" class="btn-sekundarno" style="white-space:nowrap;">✕ Resetuj</a>
<a href="/prodaja" class="btn-sekundarno" style="white-space: nowrap">✕ Resetuj</a>
{{end}}
</form>
<a href="/prodaja/nova" class="btn-primarno">+ Nova prodaja</a>
</div>
<!-- desktop tabela -->
<div class="kartica prodaja-tabela animiraj" style="padding:0;overflow:hidden;">
<div style="overflow-x:auto;">
<table style="width:100%;border-collapse:collapse;">
<div class="kartica prodaja-tabela animiraj" style="padding: 0; overflow: hidden">
<div style="overflow-x: auto">
<table style="width: 100%; border-collapse: collapse">
<thead>
<tr style="border-bottom:0.5px solid var(--ivica);">
<th style="padding:12px 16px;text-align:left;font-size:12px;font-weight:500;color:var(--tekst-sporedni);">Broj naloga</th>
<th style="padding:12px 16px;text-align:left;font-size:12px;font-weight:500;color:var(--tekst-sporedni);">Datum</th>
<th style="padding:12px 16px;text-align:left;font-size:12px;font-weight:500;color:var(--tekst-sporedni);">Klijent</th>
<th style="padding:12px 16px;text-align:right;font-size:12px;font-weight:500;color:var(--tekst-sporedni);">Ukupno</th>
<th style="padding:12px 16px;text-align:center;font-size:12px;font-weight:500;color:var(--tekst-sporedni);">Akcije</th>
<tr style="border-bottom: 0.5px solid var(--ivica)">
<th style="padding: 12px 16px; text-align: left; font-size: 12px; font-weight: 500; color: var(--tekst-sporedni)">Broj naloga</th>
<th style="padding: 12px 16px; text-align: left; font-size: 12px; font-weight: 500; color: var(--tekst-sporedni)">Datum</th>
<th style="padding: 12px 16px; text-align: left; font-size: 12px; font-weight: 500; color: var(--tekst-sporedni)">Klijent</th>
<th style="padding: 12px 16px; text-align: right; font-size: 12px; font-weight: 500; color: var(--tekst-sporedni)">Ukupno</th>
<th style="padding: 12px 16px; text-align: center; font-size: 12px; font-weight: 500; color: var(--tekst-sporedni)">Akcije</th>
</tr>
</thead>
<tbody>
{{range .Nalozi}}
<tr class="animiraj red-tabele">
<td style="padding:12px 16px;font-size:13px;font-family:monospace;color:var(--tekst-glavni);">
{{.BrojNaloga}}
</td>
<td style="padding:12px 16px;font-size:13px;color:var(--tekst-sporedni);white-space:nowrap;">
{{.Datum.Format "02.01.2006."}}
</td>
<td style="padding:12px 16px;font-size:14px;font-weight:500;color:var(--tekst-glavni);">
{{if .KlijentNaziv}}{{.KlijentNaziv}}{{else}}—{{end}}
</td>
<td style="padding:12px 16px;text-align:right;font-size:14px;font-weight:500;color:var(--tekst-glavni);">
{{printf "%.2f" .Ukupno}} din
</td>
<td style="padding:12px 16px;text-align:center;">
<div style="display:flex;align-items:center;justify-content:center;gap:8px;">
<td style="padding: 12px 16px; font-size: 13px; font-family: monospace; color: var(--tekst-glavni)">{{.BrojNaloga}}</td>
<td style="padding: 12px 16px; font-size: 13px; color: var(--tekst-sporedni); white-space: nowrap">{{.Datum.Format "02.01.2006."}}</td>
<td style="padding: 12px 16px; font-size: 14px; font-weight: 500; color: var(--tekst-glavni)">{{if .KlijentNaziv}}{{.KlijentNaziv}}{{else}}—{{end}}</td>
<td style="padding: 12px 16px; text-align: right; font-size: 14px; font-weight: 500; color: var(--tekst-glavni)">{{printf "%.2f" .Ukupno}} din</td>
<td style="padding: 12px 16px; text-align: center">
<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 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>
@@ -105,11 +73,9 @@
</tr>
{{else}}
<tr>
<td colspan="5" style="padding:32px;text-align:center;font-size:14px;color:var(--tekst-sporedni);">
{{if $.Pretraga}}
Nema naloga koji odgovaraju pretrazi.
{{else}}
Nema prodajnih naloga. <a href="/prodaja/nova" style="color:var(--sb-akcent);">Dodaj prvu prodaju.</a>
<td colspan="5" style="padding: 32px; text-align: center; font-size: 14px; color: var(--tekst-sporedni)">
{{if $.Pretraga}} Nema naloga koji odgovaraju pretrazi. {{else}} Nema prodajnih naloga.
<a href="/prodaja/nova" style="color: var(--sb-akcent)">Dodaj prvu prodaju.</a>
{{end}}
</td>
</tr>
@@ -123,34 +89,23 @@
<div class="prodaja-kartice">
{{range .Nalozi}}
<div class="kartica prodaja-kartica animiraj">
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:12px;margin-bottom:10px;">
<div style="display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; margin-bottom: 10px">
<div>
<div style="font-size:13px;font-family:monospace;color:var(--tekst-glavni);">{{.BrojNaloga}}</div>
<div style="font-size:14px;font-weight:500;color:var(--tekst-glavni);margin-top:2px;">
{{if .KlijentNaziv}}{{.KlijentNaziv}}{{else}}Bez klijenta{{end}}
</div>
<div style="font-size:12px;color:var(--tekst-sporedni);margin-top:2px;">
{{.Datum.Format "02.01.2006."}}
</div>
</div>
<div style="font-size:15px;font-weight:500;color:var(--tekst-glavni);white-space:nowrap;">
{{printf "%.2f" .Ukupno}} din
<div style="font-size: 13px; font-family: monospace; color: var(--tekst-glavni)">{{.BrojNaloga}}</div>
<div style="font-size: 14px; font-weight: 500; color: var(--tekst-glavni); margin-top: 2px">{{if .KlijentNaziv}}{{.KlijentNaziv}}{{else}}Bez klijenta{{end}}</div>
<div style="font-size: 12px; color: var(--tekst-sporedni); margin-top: 2px">{{.Datum.Format "02.01.2006."}}</div>
</div>
<div style="font-size: 15px; font-weight: 500; color: var(--tekst-glavni); white-space: nowrap">{{printf "%.2f" .Ukupno}} din</div>
</div>
<a href="/prodaja/{{.ID}}" class="btn-primarno-malo" style="justify-content:center;width:100%;box-sizing:border-box;">
Detalji
</a>
<a href="/prodaja/{{.ID}}" class="btn-primarno-malo" style="justify-content: center; width: 100%; box-sizing: border-box">Detalji</a>
</div>
{{else}}
<div style="padding:32px;text-align:center;font-size:14px;color:var(--tekst-sporedni);">
{{if $.Pretraga}}
Nema naloga koji odgovaraju pretrazi.
{{else}}
Nema prodajnih naloga. <a href="/prodaja/nova" style="color:var(--sb-akcent);">Dodaj prvu prodaju.</a>
<div style="padding: 32px; text-align: center; font-size: 14px; color: var(--tekst-sporedni)">
{{if $.Pretraga}} Nema naloga koji odgovaraju pretrazi. {{else}} Nema prodajnih naloga.
<a href="/prodaja/nova" style="color: var(--sb-akcent)">Dodaj prvu prodaju.</a>
{{end}}
</div>
{{end}}
</div>
</div>
{{end}}
+291 -106
View File
@@ -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"}}
<style>
.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; }
.poruka-animacija { animation: slideDown 0.3s ease forwards; }
.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.2s; }
.poruka-animacija { animation: slideDown 0.3s ease forwards; }
</style>
{{end}}
{{end}} {{define "sadrzaj"}}
<div style="display: flex; flex-direction: column; gap: 16px">
{{if .Sacuvano}}
<div class="poruka-uspeh poruka-animacija">Prodaja je uspešno sačuvana.</div>
{{end}}
{{define "sadrzaj"}}
<div style="display:flex;flex-direction:column;gap:16px;">
<!-- nazad dugme -->
<a href="/prodaja" class="nazad-link">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true">
<polyline points="15 18 9 12 15 6" />
</svg>
Nazad na prodaju
</a>
{{if .Sacuvano}}
<div class="poruka-uspeh poruka-animacija">Prodaja je uspešno sačuvana.</div>
{{end}}
<!-- nazad dugme -->
<a href="/prodaja" class="nazad-link">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="15 18 9 12 15 6"/></svg>
Nazad na prodaju
</a>
<!-- zaglavlje naloga -->
<div class="kartica detalji-kartica animiraj">
<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:10px;margin-bottom:16px;padding-bottom:14px;border-bottom:0.5px solid var(--ivica);">
<span style="font-size:20px;font-weight:600;color:var(--tekst-glavni);font-family:monospace;">
{{.Nalog.BrojNaloga}}
</span>
<a href="/prodaja/{{.Nalog.ID}}/stampa" target="_blank" class="btn-sekundarno" style="gap:6px;">
<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" aria-hidden="true"><polyline points="6 9 6 2 18 2 18 9"/><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect x="6" y="14" width="12" height="8"/></svg>
Štampaj
</a>
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fit, minmax(140px, 1fr));gap:16px;">
<div>
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:4px;">Datum prodaje</div>
<div style="font-size:14px;color:var(--tekst-glavni);">{{.Nalog.Datum.Format "02.01.2006. u 15:04"}}</div>
</div>
<div>
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:4px;">Klijent</div>
<div style="font-size:14px;font-weight:500;color:var(--tekst-glavni);">
{{if .KlijentNaziv}}{{.KlijentNaziv}}{{else}}—{{end}}
</div>
</div>
<div>
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:4px;">Ukupno</div>
<div style="font-size:20px;font-weight:600;color:var(--sb-akcent);">{{printf "%.2f" .Nalog.Ukupno}} din</div>
</div>
{{if .Nalog.Napomena}}
<div style="grid-column:1/-1;">
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:4px;">Napomena</div>
<div style="font-size:13px;color:var(--tekst-sporedni);">{{.Nalog.Napomena}}</div>
</div>
{{end}}
</div>
<!-- zaglavlje naloga -->
<div class="kartica detalji-kartica animiraj">
<div
style="
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 16px;
padding-bottom: 14px;
border-bottom: 0.5px solid var(--ivica);
">
<span
style="
font-size: 20px;
font-weight: 600;
color: var(--tekst-glavni);
font-family: monospace;
">
{{.Nalog.BrojNaloga}}
</span>
<a
href="/prodaja/{{.Nalog.ID}}/stampa"
target="_blank"
class="btn-sekundarno"
style="gap: 6px">
<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"
aria-hidden="true">
<polyline points="6 9 6 2 18 2 18 9" />
<path
d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2" />
<rect x="6" y="14" width="12" height="8" />
</svg>
Štampaj
</a>
</div>
<!-- stavke -->
<div class="kartica detalji-kartica animiraj" style="padding:0;overflow:hidden;">
<div style="padding:16px 20px;border-bottom:0.5px solid var(--ivica);">
<span style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">Stavke</span>
<div
style="
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 16px;
">
<div>
<div
style="
font-size: 12px;
color: var(--tekst-sporedni);
margin-bottom: 4px;
">
Datum prodaje
</div>
<div style="overflow-x:auto;">
<table style="width:100%;border-collapse:collapse;">
<thead>
<tr style="border-bottom:0.5px solid var(--ivica);">
<th style="padding:10px 20px;text-align:left;font-size:12px;font-weight:500;color:var(--tekst-sporedni);">Artikal</th>
<th style="padding:10px 20px;text-align:center;font-size:12px;font-weight:500;color:var(--tekst-sporedni);width:90px;">Količina</th>
<th style="padding:10px 20px;text-align:right;font-size:12px;font-weight:500;color:var(--tekst-sporedni);width:140px;">Cena/kom</th>
<th style="padding:10px 20px;text-align:right;font-size:12px;font-weight:500;color:var(--tekst-sporedni);width:120px;">Ukupno</th>
</tr>
</thead>
<tbody>
{{range .Stavke}}
<tr style="border-bottom:0.5px solid var(--ivica);">
<td style="padding:10px 20px;font-size:14px;color:var(--tekst-glavni);">{{.ArtikalNaziv}}</td>
<td style="padding:10px 20px;text-align:center;font-size:14px;color:var(--tekst-glavni);">{{.Kolicina}}</td>
<td style="padding:10px 20px;text-align:right;font-size:14px;color:var(--tekst-sporedni);">{{printf "%.2f" .CenaPoKomadu}} din</td>
<td style="padding:10px 20px;text-align:right;font-size:14px;font-weight:500;color:var(--tekst-glavni);">{{printf "%.2f" .Ukupno}} din</td>
</tr>
{{end}}
</tbody>
<tfoot>
<tr style="border-top:0.5px solid var(--ivica);">
<td colspan="3" style="padding:12px 20px;text-align:right;font-size:13px;font-weight:500;color:var(--tekst-sporedni);">Ukupno:</td>
<td style="padding:12px 20px;text-align:right;font-size:16px;font-weight:600;color:var(--sb-akcent);">{{printf "%.2f" .Nalog.Ukupno}} din</td>
</tr>
</tfoot>
</table>
<div style="font-size: 14px; color: var(--tekst-glavni)">
{{.Nalog.Datum.Format "02.01.2006. u 15:04"}}
</div>
</div>
<div>
<div
style="
font-size: 12px;
color: var(--tekst-sporedni);
margin-bottom: 4px;
">
Klijent
</div>
<div
style="font-size: 14px; font-weight: 500; color: var(--tekst-glavni)">
{{if .KlijentNaziv}}{{.KlijentNaziv}}{{else}}—{{end}}
</div>
</div>
<div>
<div
style="
font-size: 12px;
color: var(--tekst-sporedni);
margin-bottom: 4px;
">
Ukupno
</div>
<div style="font-size: 20px; font-weight: 600; color: var(--sb-akcent)">
{{printf "%.2f" .Nalog.Ukupno}} din
</div>
</div>
{{if .Nalog.Napomena}}
<div style="grid-column: 1/-1">
<div
style="
font-size: 12px;
color: var(--tekst-sporedni);
margin-bottom: 4px;
">
Napomena
</div>
<div style="font-size: 13px; color: var(--tekst-sporedni)">
{{.Nalog.Napomena}}
</div>
</div>
{{end}}
</div>
</div>
{{if index $.Dozvole "prodaja.obrisi"}}
<!-- zona za brisanje -->
<div class="kartica detalji-kartica animiraj" style="border-color:#dc262633;">
<div style="display:flex;align-items:flex-start;gap:12px;flex-wrap:wrap;">
<div style="flex:1;min-width:200px;">
<div style="font-size:14px;font-weight:500;color:#dc2626;margin-bottom:4px;">Brisanje naloga</div>
<div style="font-size:13px;color:var(--tekst-sporedni);">
Brisanje je trajno. Količine artikala biće vraćene na stanje u magacinu.
</div>
</div>
<form method="POST" action="/prodaja/obrisi/{{.Nalog.ID}}">
<button type="submit" class="btn-primarno" style="background:#dc2626;"
data-potvrda="Da li ste sigurni da želite da obrišete nalog {{.Nalog.BrojNaloga}}? Količine artikala biće vraćene na stanje.">
Obriši nalog
</button>
</form>
</div>
<!-- stavke -->
<div
class="kartica detalji-kartica animiraj"
style="padding: 0; overflow: hidden">
<div style="padding: 16px 20px; border-bottom: 0.5px solid var(--ivica)">
<span
style="font-size: 15px; font-weight: 500; color: var(--tekst-glavni)">Stavke</span>
</div>
{{end}}
<div style="overflow-x: auto">
<table style="width: 100%; border-collapse: collapse">
<thead>
<tr style="border-bottom: 0.5px solid var(--ivica)">
<th
style="
padding: 10px 20px;
text-align: left;
font-size: 12px;
font-weight: 500;
color: var(--tekst-sporedni);
">
Artikal
</th>
<th
style="
padding: 10px 20px;
text-align: center;
font-size: 12px;
font-weight: 500;
color: var(--tekst-sporedni);
width: 90px;
">
Količina
</th>
<th
style="
padding: 10px 20px;
text-align: right;
font-size: 12px;
font-weight: 500;
color: var(--tekst-sporedni);
width: 140px;
">
Cena/kom
</th>
<th
style="
padding: 10px 20px;
text-align: right;
font-size: 12px;
font-weight: 500;
color: var(--tekst-sporedni);
width: 120px;
">
Ukupno
</th>
</tr>
</thead>
<tbody>
{{range .Stavke}}
<tr style="border-bottom: 0.5px solid var(--ivica)">
<td
style="
padding: 10px 20px;
font-size: 14px;
color: var(--tekst-glavni);
">
{{.ArtikalNaziv}}
</td>
<td
style="
padding: 10px 20px;
text-align: center;
font-size: 14px;
color: var(--tekst-glavni);
">
{{.Kolicina}}
</td>
<td
style="
padding: 10px 20px;
text-align: right;
font-size: 14px;
color: var(--tekst-sporedni);
">
{{printf "%.2f" .CenaPoKomadu}} din
</td>
<td
style="
padding: 10px 20px;
text-align: right;
font-size: 14px;
font-weight: 500;
color: var(--tekst-glavni);
">
{{printf "%.2f" .Ukupno}} din
</td>
</tr>
{{end}}
</tbody>
<tfoot>
<tr style="border-top: 0.5px solid var(--ivica)">
<td
colspan="3"
style="
padding: 12px 20px;
text-align: right;
font-size: 13px;
font-weight: 500;
color: var(--tekst-sporedni);
">
Ukupno:
</td>
<td
style="
padding: 12px 20px;
text-align: right;
font-size: 16px;
font-weight: 600;
color: var(--sb-akcent);
">
{{printf "%.2f" .Nalog.Ukupno}} din
</td>
</tr>
</tfoot>
</table>
</div>
</div>
{{if index $.Dozvole "prodaja.obrisi"}}
<!-- zona za brisanje -->
<div class="kartica detalji-kartica animiraj" style="border-color: #dc262633">
<div
style="display: flex; align-items: flex-start; gap: 12px; flex-wrap: wrap">
<div style="flex: 1; min-width: 200px">
<div
style="
font-size: 14px;
font-weight: 500;
color: #dc2626;
margin-bottom: 4px;
">
Brisanje naloga
</div>
<div style="font-size: 13px; color: var(--tekst-sporedni)">
Brisanje je trajno. Količine artikala biće vraćene na stanje u
magacinu.
</div>
</div>
<form method="POST" action="/prodaja/obrisi/{{.Nalog.ID}}">
<button
type="submit"
class="btn-primarno"
style="background: #dc2626"
data-potvrda="Da li ste sigurni da želite da obrišete nalog {{.Nalog.BrojNaloga}}? Količine artikala biće vraćene na stanje.">
Obriši nalog
</button>
</form>
</div>
</div>
{{end}}
</div>
{{end}}
+413 -193
View File
@@ -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"}}
<style>
.forma-kartica:nth-child(1) { animation-delay: 0.04s; }
.forma-kartica:nth-child(2) { animation-delay: 0.12s; }
.greska-animacija { animation: shake 0.4s ease; }
@media (max-width: 768px) {
.stavke-tabela-wrapper { display: none; }
.stavke-kartice { display: flex !important; }
}
.forma-kartica:nth-child(1) { animation-delay: 0.04s; }
.forma-kartica:nth-child(2) { animation-delay: 0.12s; }
.greska-animacija { animation: shake 0.4s ease; }
@media (max-width: 768px) { .stavke-tabela-wrapper { display: none; } .stavke-kartice { display: flex !important; } }
</style>
{{end}}
{{define "sadrzaj"}}
{{end}} {{define "sadrzaj"}}
<!-- lista artikala kao JSON — sadrži id, naziv i prodajnu cenu -->
<script>var _ntechArtikli = {{.ArtikliJSON}};</script>
<script>
var _ntechArtikli = {{.ArtikliJSON }};
</script>
<div style="width:100%;"
x-data="{
<div
style="width: 100%"
x-data="{
stavke: [{artikal_id: '', kolicina: 1, cena: 0}],
artikliOpcije: _ntechArtikli,
isMobile: window.matchMedia('(max-width: 768px)').matches,
@@ -66,190 +59,417 @@
return this.stavke.reduce((z, s) => z + (parseFloat(s.kolicina) * parseFloat(s.cena) || 0), 0).toFixed(2);
}
}">
<!-- nazad dugme -->
<a href="/prodaja" class="nazad-link">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true">
<polyline points="15 18 9 12 15 6" />
</svg>
Nazad na prodaju
</a>
<!-- nazad dugme -->
<a href="/prodaja" class="nazad-link">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="15 18 9 12 15 6"/></svg>
Nazad na prodaju
</a>
<form method="POST" action="/prodaja/nova">
{{if .Greska}}
<div class="poruka-greska greska-animacija">{{.Greska}}</div>
{{end}}
<form method="POST" action="/prodaja/nova">
<!-- zaglavlje prodaje -->
<div class="kartica forma-kartica animiraj" style="margin-bottom: 16px">
<div
style="
margin-bottom: 16px;
padding-bottom: 14px;
border-bottom: 0.5px solid var(--ivica);
">
<span
style="font-size: 16px; font-weight: 500; color: var(--tekst-glavni)">Nova prodaja</span>
</div>
<div style="display: flex; flex-direction: column; gap: 14px">
<div>
<label
style="
font-size: 13px;
color: var(--tekst-sporedni);
display: block;
margin-bottom: 6px;
">Klijent</label>
<select name="klijent_id" style="width: 100%">
<option value="">— bez klijenta —</option>
{{range .Klijenti}}
<option value="{{.ID}}">
{{if .NazivFirme}}{{.NazivFirme}}{{else}}{{.Ime}}
{{.Prezime}}{{end}}
</option>
{{end}}
</select>
</div>
<div>
<label
style="
font-size: 13px;
color: var(--tekst-sporedni);
display: block;
margin-bottom: 6px;
">Napomena</label>
<textarea
name="napomena"
rows="2"
placeholder="Interna napomena o prodaji..."
style="width: 100%; resize: vertical"></textarea>
</div>
</div>
</div>
{{if .Greska}}
<div class="poruka-greska greska-animacija">{{.Greska}}</div>
{{end}}
<!-- stavke -->
<div class="kartica forma-kartica animiraj" style="margin-bottom: 16px">
<div
style="
margin-bottom: 16px;
padding-bottom: 14px;
border-bottom: 0.5px solid var(--ivica);
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 8px;
">
<span
style="font-size: 16px; font-weight: 500; color: var(--tekst-glavni)">Stavke</span>
<button
type="button"
@click="dodajStavku()"
class="btn-primarno"
style="font-size: 13px; padding: 6px 14px">
+ Dodaj stavku
</button>
</div>
<!-- zaglavlje prodaje -->
<div class="kartica forma-kartica animiraj" style="margin-bottom:16px;">
<div style="margin-bottom:16px;padding-bottom:14px;border-bottom:0.5px solid var(--ivica);">
<span style="font-size:16px;font-weight:500;color:var(--tekst-glavni);">Nova prodaja</span>
<!-- desktop tabela stavki -->
<div class="stavke-tabela-wrapper" style="overflow-x: auto">
<table style="width: 100%; border-collapse: collapse">
<thead>
<tr style="border-bottom: 0.5px solid var(--ivica)">
<th
style="
padding: 8px 10px;
text-align: left;
font-size: 12px;
font-weight: 500;
color: var(--tekst-sporedni);
">
Artikal
</th>
<th
style="
padding: 8px 10px;
text-align: center;
font-size: 12px;
font-weight: 500;
color: var(--tekst-sporedni);
width: 90px;
">
Količina
</th>
<th
style="
padding: 8px 10px;
text-align: right;
font-size: 12px;
font-weight: 500;
color: var(--tekst-sporedni);
width: 140px;
">
Cena/kom (din)
</th>
<th
style="
padding: 8px 10px;
text-align: right;
font-size: 12px;
font-weight: 500;
color: var(--tekst-sporedni);
width: 110px;
">
Ukupno
</th>
<th style="width: 40px"></th>
</tr>
</thead>
<tbody>
<template x-for="(stavka, i) in stavke" :key="i">
<tr style="border-bottom: 0.5px solid var(--ivica)">
<td style="padding: 8px 10px">
<select
:name="'artikal_id[]'"
x-model="stavka.artikal_id"
@change="popuniCenu(stavka)"
:disabled="isMobile"
style="width: 100%">
<option value="">— odaberi artikal —</option>
<template x-for="a in artikliOpcije" :key="a.id">
<option :value="a.id" x-text="a.naziv"></option>
</template>
</select>
</td>
<td style="padding: 8px 10px">
<input
type="number"
:name="'kolicina[]'"
x-model="stavka.kolicina"
min="1"
:disabled="isMobile"
:style="prekoracenje(i) ? 'border-color:#dc2626;' : ''"
style="width: 100%; text-align: center" />
<div
x-show="stavka.artikal_id"
:style="prekoracenje(i) ? 'color:#dc2626' : 'color:var(--tekst-sporedni)'"
x-text="'Na stanju: ' + dostupnaKolicina(i)"
style="
font-size: 11px;
margin-top: 3px;
text-align: center;
white-space: nowrap;
"></div>
<div
x-show="prekoracenje(i)"
style="
font-size: 11px;
color: #dc2626;
margin-top: 1px;
text-align: center;
white-space: nowrap;
">
Prekoračena količina
</div>
</td>
<td style="padding: 8px 10px">
<input
type="number"
:name="'cena_po_komadu[]'"
x-model="stavka.cena"
min="0"
step="0.01"
:disabled="isMobile"
style="width: 100%; text-align: right" />
</td>
<td
style="
padding: 8px 10px;
text-align: right;
font-size: 14px;
font-weight: 500;
color: var(--tekst-glavni);
">
<span x-text="ukupnoStavke(stavka) + ' din'"></span>
</td>
<td style="padding: 8px 10px; text-align: center">
<button
type="button"
@click="ukloniStavku(i)"
x-show="stavke.length > 1"
style="
background: none;
border: none;
cursor: pointer;
color: #dc2626;
font-size: 18px;
line-height: 1;
padding: 2px 6px;
border-radius: 4px;
transition: background 0.15s;
"
onmouseover="this.style.background = 'rgba(220,38,38,0.08)'"
onmouseout="this.style.background = 'none'"
title="Ukloni stavku">
×
</button>
</td>
</tr>
</template>
</tbody>
<tfoot>
<tr style="border-top: 0.5px solid var(--ivica)">
<td
colspan="3"
style="
padding: 10px;
text-align: right;
font-size: 13px;
color: var(--tekst-sporedni);
font-weight: 500;
">
Ukupno:
</td>
<td
style="
padding: 10px;
text-align: right;
font-size: 15px;
font-weight: 600;
color: var(--tekst-glavni);
">
<span x-text="ukupnoSvega() + ' din'"></span>
</td>
<td></td>
</tr>
</tfoot>
</table>
</div>
<!-- mobilne kartice stavki -->
<div
class="stavke-kartice"
style="display: none; flex-direction: column; gap: 10px">
<template x-for="(stavka, i) in stavke" :key="i">
<div
style="
border: 0.5px solid var(--ivica);
border-radius: 8px;
padding: 12px;
">
<div
style="
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
">
<span
style="
font-size: 13px;
font-weight: 500;
color: var(--tekst-sporedni);
"
x-text="'Stavka ' + (i + 1)"></span>
<button
type="button"
@click="ukloniStavku(i)"
x-show="stavke.length > 1"
style="
background: none;
border: 0.5px solid #dc2626;
color: #dc2626;
cursor: pointer;
font-size: 13px;
padding: 2px 8px;
border-radius: 4px;
">
Ukloni
</button>
</div>
<div style="display:flex;flex-direction:column;gap:14px;">
<div style="display: flex; flex-direction: column; gap: 10px">
<div>
<label
style="
font-size: 12px;
color: var(--tekst-sporedni);
display: block;
margin-bottom: 4px;
">Artikal</label>
<select
:name="'artikal_id[]'"
x-model="stavka.artikal_id"
@change="popuniCenu(stavka)"
:disabled="!isMobile"
style="width: 100%">
<option value="">— odaberi artikal —</option>
<template x-for="a in artikliOpcije" :key="a.id">
<option :value="a.id" x-text="a.naziv"></option>
</template>
</select>
</div>
<div
style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px">
<div>
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:6px;">Klijent</label>
<select name="klijent_id" style="width:100%;">
<option value="">— bez klijenta —</option>
{{range .Klijenti}}
<option value="{{.ID}}">
{{if .NazivFirme}}{{.NazivFirme}}{{else}}{{.Ime}} {{.Prezime}}{{end}}
</option>
{{end}}
</select>
<label
style="
font-size: 12px;
color: var(--tekst-sporedni);
display: block;
margin-bottom: 4px;
">Količina</label>
<input
type="number"
:name="'kolicina[]'"
x-model="stavka.kolicina"
min="1"
:disabled="!isMobile"
:style="prekoracenje(i) ? 'border-color:#dc2626;' : ''"
style="width: 100%" />
<div
x-show="stavka.artikal_id"
:style="prekoracenje(i) ? 'color:#dc2626' : 'color:var(--tekst-sporedni)'"
x-text="'Na stanju: ' + dostupnaKolicina(i)"
style="font-size: 11px; margin-top: 3px"></div>
<div
x-show="prekoracenje(i)"
style="font-size: 11px; color: #dc2626; margin-top: 1px">
Prekoračena količina
</div>
</div>
<div>
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:6px;">Napomena</label>
<textarea name="napomena" rows="2"
placeholder="Interna napomena o prodaji..."
style="width:100%;resize:vertical;"></textarea>
<label
style="
font-size: 12px;
color: var(--tekst-sporedni);
display: block;
margin-bottom: 4px;
">Cena/kom (din)</label>
<input
type="number"
:name="'cena_po_komadu[]'"
x-model="stavka.cena"
min="0"
step="0.01"
:disabled="!isMobile"
style="width: 100%" />
</div>
</div>
<div
style="
text-align: right;
font-size: 14px;
font-weight: 500;
color: var(--tekst-glavni);
">
Ukupno: <span x-text="ukupnoStavke(stavka) + ' din'"></span>
</div>
</div>
</div>
</template>
<div
style="
text-align: right;
font-size: 15px;
font-weight: 600;
color: var(--tekst-glavni);
padding: 8px 4px;
">
Ukupno: <span x-text="ukupnoSvega() + ' din'"></span>
</div>
</div>
</div>
<!-- stavke -->
<div class="kartica forma-kartica animiraj" style="margin-bottom:16px;">
<div style="margin-bottom:16px;padding-bottom:14px;border-bottom:0.5px solid var(--ivica);display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px;">
<span style="font-size:16px;font-weight:500;color:var(--tekst-glavni);">Stavke</span>
<button type="button" @click="dodajStavku()" class="btn-primarno" style="font-size:13px;padding:6px 14px;">
+ Dodaj stavku
</button>
</div>
<!-- desktop tabela stavki -->
<div class="stavke-tabela-wrapper" style="overflow-x:auto;">
<table style="width:100%;border-collapse:collapse;">
<thead>
<tr style="border-bottom:0.5px solid var(--ivica);">
<th style="padding:8px 10px;text-align:left;font-size:12px;font-weight:500;color:var(--tekst-sporedni);">Artikal</th>
<th style="padding:8px 10px;text-align:center;font-size:12px;font-weight:500;color:var(--tekst-sporedni);width:90px;">Količina</th>
<th style="padding:8px 10px;text-align:right;font-size:12px;font-weight:500;color:var(--tekst-sporedni);width:140px;">Cena/kom (din)</th>
<th style="padding:8px 10px;text-align:right;font-size:12px;font-weight:500;color:var(--tekst-sporedni);width:110px;">Ukupno</th>
<th style="width:40px;"></th>
</tr>
</thead>
<tbody>
<template x-for="(stavka, i) in stavke" :key="i">
<tr style="border-bottom:0.5px solid var(--ivica);">
<td style="padding:8px 10px;">
<select :name="'artikal_id[]'" x-model="stavka.artikal_id"
@change="popuniCenu(stavka)"
:disabled="isMobile"
style="width:100%;">
<option value="">— odaberi artikal —</option>
<template x-for="a in artikliOpcije" :key="a.id">
<option :value="a.id" x-text="a.naziv"></option>
</template>
</select>
</td>
<td style="padding:8px 10px;">
<input type="number" :name="'kolicina[]'" x-model="stavka.kolicina"
min="1" :disabled="isMobile"
:style="prekoracenje(i) ? 'border-color:#dc2626;' : ''"
style="width:100%;text-align:center;">
<div x-show="stavka.artikal_id"
:style="prekoracenje(i) ? 'color:#dc2626' : 'color:var(--tekst-sporedni)'"
x-text="'Na stanju: ' + dostupnaKolicina(i)"
style="font-size:11px;margin-top:3px;text-align:center;white-space:nowrap;"></div>
<div x-show="prekoracenje(i)"
style="font-size:11px;color:#dc2626;margin-top:1px;text-align:center;white-space:nowrap;">
Prekoračena količina
</div>
</td>
<td style="padding:8px 10px;">
<input type="number" :name="'cena_po_komadu[]'" x-model="stavka.cena"
min="0" step="0.01" :disabled="isMobile" style="width:100%;text-align:right;">
</td>
<td style="padding:8px 10px;text-align:right;font-size:14px;font-weight:500;color:var(--tekst-glavni);">
<span x-text="ukupnoStavke(stavka) + ' din'"></span>
</td>
<td style="padding:8px 10px;text-align:center;">
<button type="button" @click="ukloniStavku(i)"
x-show="stavke.length > 1"
style="background:none;border:none;cursor:pointer;color:#dc2626;font-size:18px;line-height:1;padding:2px 6px;border-radius:4px;transition:background 0.15s;"
onmouseover="this.style.background='rgba(220,38,38,0.08)'"
onmouseout="this.style.background='none'"
title="Ukloni stavku">×</button>
</td>
</tr>
</template>
</tbody>
<tfoot>
<tr style="border-top:0.5px solid var(--ivica);">
<td colspan="3" style="padding:10px;text-align:right;font-size:13px;color:var(--tekst-sporedni);font-weight:500;">Ukupno:</td>
<td style="padding:10px;text-align:right;font-size:15px;font-weight:600;color:var(--tekst-glavni);">
<span x-text="ukupnoSvega() + ' din'"></span>
</td>
<td></td>
</tr>
</tfoot>
</table>
</div>
<!-- mobilne kartice stavki -->
<div class="stavke-kartice" style="display:none;flex-direction:column;gap:10px;">
<template x-for="(stavka, i) in stavke" :key="i">
<div style="border:0.5px solid var(--ivica);border-radius:8px;padding:12px;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
<span style="font-size:13px;font-weight:500;color:var(--tekst-sporedni);"
x-text="'Stavka ' + (i + 1)"></span>
<button type="button" @click="ukloniStavku(i)"
x-show="stavke.length > 1"
style="background:none;border:0.5px solid #dc2626;color:#dc2626;cursor:pointer;font-size:13px;padding:2px 8px;border-radius:4px;">
Ukloni
</button>
</div>
<div style="display:flex;flex-direction:column;gap:10px;">
<div>
<label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">Artikal</label>
<select :name="'artikal_id[]'" x-model="stavka.artikal_id"
@change="popuniCenu(stavka)"
:disabled="!isMobile"
style="width:100%;">
<option value="">— odaberi artikal —</option>
<template x-for="a in artikliOpcije" :key="a.id">
<option :value="a.id" x-text="a.naziv"></option>
</template>
</select>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
<div>
<label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">Količina</label>
<input type="number" :name="'kolicina[]'" x-model="stavka.kolicina" min="1" :disabled="!isMobile"
:style="prekoracenje(i) ? 'border-color:#dc2626;' : ''"
style="width:100%;">
<div x-show="stavka.artikal_id"
:style="prekoracenje(i) ? 'color:#dc2626' : 'color:var(--tekst-sporedni)'"
x-text="'Na stanju: ' + dostupnaKolicina(i)"
style="font-size:11px;margin-top:3px;"></div>
<div x-show="prekoracenje(i)" style="font-size:11px;color:#dc2626;margin-top:1px;">Prekoračena količina</div>
</div>
<div>
<label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">Cena/kom (din)</label>
<input type="number" :name="'cena_po_komadu[]'" x-model="stavka.cena" min="0" step="0.01" :disabled="!isMobile" style="width:100%;">
</div>
</div>
<div style="text-align:right;font-size:14px;font-weight:500;color:var(--tekst-glavni);">
Ukupno: <span x-text="ukupnoStavke(stavka) + ' din'"></span>
</div>
</div>
</div>
</template>
<div style="text-align:right;font-size:15px;font-weight:600;color:var(--tekst-glavni);padding:8px 4px;">
Ukupno: <span x-text="ukupnoSvega() + ' din'"></span>
</div>
</div>
</div>
<!-- dugmad forme -->
<div style="display:flex;justify-content:flex-end;gap:10px;">
<a href="/prodaja" class="btn-sekundarno">Odustani</a>
<button type="submit" class="btn-primarno"
:disabled="imaPrekoracenja()"
:style="imaPrekoracenja() ? 'opacity:0.4;cursor:not-allowed;' : ''">
Sačuvaj prodaju
</button>
</div>
</form>
<!-- dugmad forme -->
<div style="display: flex; justify-content: flex-end; gap: 10px">
<a href="/prodaja" class="btn-sekundarno">Odustani</a>
<button
type="submit"
class="btn-primarno"
:disabled="imaPrekoracenja()"
:style="imaPrekoracenja() ? 'opacity:0.4;cursor:not-allowed;' : ''">
Sačuvaj prodaju
</button>
</div>
</form>
</div>
{{end}}
+120 -239
View File
@@ -1,255 +1,136 @@
<!DOCTYPE html>
<!doctype html>
<html lang="sr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Priznanica {{.Nalog.BrojNaloga}}</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; font-size: 14px; color: #111; background: #f5f5f5; padding: 32px 16px; }
.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); }
.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 { font-size: 11px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.05em; color: #9ca3af; display: block; margin-bottom: 3px; }
.meta-stavka span { font-size: 14px; color: #111; }
table { width: 100%; border-collapse: collapse; margin-bottom: 0; }
thead tr { border-bottom: 1.5px solid #111; }
thead th { padding: 8px 10px; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; color: #6b7280; text-align: left; }
thead th:not(:first-child) { text-align: right; }
tbody tr { border-bottom: 1px solid #e5e7eb; }
tbody td { padding: 10px 10px; font-size: 14px; color: #111; }
tbody td:not(:first-child) { text-align: right; }
tfoot tr { border-top: 1.5px solid #111; }
tfoot td { padding: 12px 10px; }
.ukupno-label { text-align: right; font-size: 13px; font-weight: 500; color: #6b7280; }
.ukupno-iznos { text-align: right; font-size: 18px; font-weight: 700; color: #111; }
.ekran-dugmad { display: flex; gap: 10px; justify-content: flex-end; margin-top: 28px; }
.btn { padding: 9px 20px; border-radius: 8px; font-size: 14px; font-weight: 500; cursor: pointer; border: none; text-decoration: none; display: inline-block; }
.btn-primarni { background: #2563eb; color: #fff; }
.btn-sekundarni { background: #f3f4f6; color: #374151; }
@media print { body { background: #fff; padding: 0; } .stranica { box-shadow: none; border-radius: 0; padding: 20px; max-width: 100%; } .ekran-dugmad { display: none; } }
</style>
</head>
<body>
<div class="stranica">
<div class="zaglavlje">
<div>
<div class="firma-naziv">{{.NazivFirme}}</div>
{{if .Podnazlov}}
<div class="firma-podnazlov">{{.Podnazlov}}</div>
{{end}} {{if .Adresa}}
<div class="firma-kontakt">{{.Adresa}}</div>
{{end}} {{if .Telefon}}
<div class="firma-kontakt">{{.Telefon}}</div>
{{end}} {{if .PIB}}
<div class="firma-kontakt">PIB: {{.PIB}}</div>
{{end}}
</div>
<div class="nalog-info">
<div class="nalog-broj">{{.Nalog.BrojNaloga}}</div>
<div class="nalog-datum">
{{.Nalog.Datum.Format "02.01.2006. u 15:04"}}
</div>
</div>
</div>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: 14px;
color: #111;
background: #f5f5f5;
padding: 32px 16px;
}
{{if .KlijentNaziv}}
<div class="meta">
<div class="meta-stavka">
<label>Klijent</label>
<span>{{.KlijentNaziv}}</span>
</div>
</div>
{{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);
}
<table>
<thead>
<tr>
<th>Artikal</th>
<th>Kol.</th>
<th>Cena/kom</th>
<th>Ukupno</th>
</tr>
</thead>
<tbody>
{{range .Stavke}}
<tr>
<td>{{.ArtikalNaziv}}</td>
<td>{{.Kolicina}}</td>
<td>{{printf "%.2f" .CenaPoKomadu}} din</td>
<td>{{printf "%.2f" .Ukupno}} din</td>
</tr>
{{end}}
</tbody>
<tfoot>
<tr>
<td colspan="3" class="ukupno-label">Ukupno za naplatu:</td>
<td class="ukupno-iznos">{{printf "%.2f" .Nalog.Ukupno}} din</td>
</tr>
</tfoot>
</table>
.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}}
<div
style="
margin-top: 20px;
padding-top: 16px;
border-top: 1px solid #e5e7eb;
">
<div
style="
font-size: 11px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #9ca3af;
display: block;
margin-bottom: 3px;
}
.meta-stavka span {
font-size: 14px;
color: #111;
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 0;
}
thead tr {
border-bottom: 1.5px solid #111;
}
thead th {
padding: 8px 10px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
color: #6b7280;
text-align: left;
}
thead th:not(:first-child) { text-align: right; }
tbody tr {
border-bottom: 1px solid #e5e7eb;
}
tbody td {
padding: 10px 10px;
font-size: 14px;
color: #111;
}
tbody td:not(:first-child) {
text-align: right;
}
tfoot tr {
border-top: 1.5px solid #111;
}
tfoot td {
padding: 12px 10px;
}
.ukupno-label {
text-align: right;
font-size: 13px;
font-weight: 500;
color: #6b7280;
}
.ukupno-iznos {
text-align: right;
font-size: 18px;
font-weight: 700;
color: #111;
}
.ekran-dugmad {
display: flex;
gap: 10px;
justify-content: flex-end;
margin-top: 28px;
}
.btn {
padding: 9px 20px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
border: none;
text-decoration: none;
display: inline-block;
}
.btn-primarni {
background: #2563eb;
color: #fff;
}
.btn-sekundarni {
background: #f3f4f6;
color: #374151;
}
@media print {
body { background: #fff; padding: 0; }
.stranica { box-shadow: none; border-radius: 0; padding: 20px; max-width: 100%; }
.ekran-dugmad { display: none; }
}
</style>
</head>
<body>
<div class="stranica">
<div class="zaglavlje">
<div>
<div class="firma-naziv">{{.NazivFirme}}</div>
{{if .Podnazlov}}<div class="firma-podnazlov">{{.Podnazlov}}</div>{{end}}
{{if .Adresa}}<div class="firma-kontakt">{{.Adresa}}</div>{{end}}
{{if .Telefon}}<div class="firma-kontakt">{{.Telefon}}</div>{{end}}
{{if .PIB}}<div class="firma-kontakt">PIB: {{.PIB}}</div>{{end}}
</div>
<div class="nalog-info">
<div class="nalog-broj">{{.Nalog.BrojNaloga}}</div>
<div class="nalog-datum">{{.Nalog.Datum.Format "02.01.2006. u 15:04"}}</div>
</div>
</div>
{{if .KlijentNaziv}}
<div class="meta">
<div class="meta-stavka">
<label>Klijent</label>
<span>{{.KlijentNaziv}}</span>
</div>
</div>
{{end}}
<table>
<thead>
<tr>
<th>Artikal</th>
<th>Kol.</th>
<th>Cena/kom</th>
<th>Ukupno</th>
</tr>
</thead>
<tbody>
{{range .Stavke}}
<tr>
<td>{{.ArtikalNaziv}}</td>
<td>{{.Kolicina}}</td>
<td>{{printf "%.2f" .CenaPoKomadu}} din</td>
<td>{{printf "%.2f" .Ukupno}} din</td>
</tr>
{{end}}
</tbody>
<tfoot>
<tr>
<td colspan="3" class="ukupno-label">Ukupno za naplatu:</td>
<td class="ukupno-iznos">{{printf "%.2f" .Nalog.Ukupno}} din</td>
</tr>
</tfoot>
</table>
{{if .Nalog.Napomena}}
<div style="margin-top:20px;padding-top:16px;border-top:1px solid #e5e7eb;">
<div style="font-size:11px;font-weight:500;text-transform:uppercase;letter-spacing:0.05em;color:#9ca3af;margin-bottom:4px;">Napomena</div>
<div style="font-size:13px;color:#374151;">{{.Nalog.Napomena}}</div>
</div>
{{end}}
<div class="ekran-dugmad">
<button class="btn btn-sekundarni" onclick="window.close()">Zatvori</button>
<button class="btn btn-primarni" onclick="window.print()">Štampaj</button>
margin-bottom: 4px;
">
Napomena
</div>
<div style="font-size: 13px; color: #374151">{{.Nalog.Napomena}}</div>
</div>
{{end}}
<div class="ekran-dugmad">
<button class="btn btn-sekundarni" onclick="window.close()">
Zatvori
</button>
<button class="btn btn-primarni" onclick="window.print()">
Štampaj
</button>
</div>
</div>
<script>window.onload = function() { window.print(); };</script>
</body>
<script>
window.onload = function () {
window.print();
};
</script>
</body>
</html>
+7 -20
View File
@@ -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; }
+7 -17
View File
@@ -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; }
</style>
{{end}}
+6 -60
View File
@@ -6,74 +6,20 @@
<title>Dvostepena verifikacija — NTech</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #0f1117;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
}
.kartica {
background: #1a1d27;
border: 0.5px solid #2d3148;
border-radius: 16px;
padding: 40px;
width: 100%;
max-width: 380px;
}
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f1117; min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 16px; }
.kartica { background: #1a1d27; border: 0.5px solid #2d3148; border-radius: 16px; padding: 40px; width: 100%; max-width: 380px; }
.logo { text-align: center; margin-bottom: 32px; }
.logo-naziv { font-size: 22px; font-weight: 600; color: #fff; }
.opis { font-size: 13px; color: #9ca3af; margin-bottom: 24px; line-height: 1.5; }
h1 { font-size: 18px; font-weight: 600; color: #fff; margin-bottom: 8px; }
.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: 20px;
color: #fff;
outline: none;
text-align: center;
letter-spacing: 6px;
transition: border-color 0.2s;
}
input { width: 100%; padding: 8px 12px; background: #0f1117; border: 0.5px solid #2d3148; border-radius: 8px; font-size: 20px; color: #fff; outline: none; text-align: center; letter-spacing: 6px; 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 { 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;
}
.nazad {
display: block;
text-align: center;
margin-top: 16px;
font-size: 13px;
color: #6b7280;
text-decoration: none;
}
.greska { background: #fef2f2; border: 0.5px solid #fca5a5; color: #dc2626; border-radius: 8px; padding: 10px 14px; font-size: 13px; margin-bottom: 20px; }
.nazad { display: block; text-align: center; margin-top: 16px; font-size: 13px; color: #6b7280; text-decoration: none; }
.nazad:hover { color: #9ca3af; }
</style>
</head>
+15 -85
View File
@@ -38,91 +38,21 @@
{{if .AppPozadina}}
<style>
.app-bg {
position: fixed;
inset: {{if ne .AppPozadinaBlurPozadine "0"}}-20px{{else}}0{{end}};
background-image: url('{{.AppPozadina}}');
background-size: cover;
background-position: center;
filter: blur({{.AppPozadinaBlurPozadine}}px);
pointer-events: none;
z-index: 0;
}
.app-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,{{.AppPozadinaOpacity}}%);
pointer-events: none;
z-index: 1;
}
.raspored {
position: relative;
z-index: 2;
}
.sidebar {
background: rgba(0,0,0,{{if .AppPozadinaGlassOpacity}}{{.AppPozadinaGlassOpacity}}%{{else}}0.3{{end}}) !important;
backdrop-filter: blur({{.AppPozadinaBlur}}px);
-webkit-backdrop-filter: blur({{.AppPozadinaBlur}}px);
border-right: 1px solid rgba(255,255,255,0.12) !important;
}
.sidebar .nav-stavka,
.sidebar .logo-naziv,
.sidebar .logo-podnazlov {
text-shadow: 0 1px 3px rgba(0,0,0,0.8);
color: rgba(255,255,255,0.95) !important;
}
.sidebar .nav-stavka svg {
color: rgba(255,255,255,0.95) !important;
stroke: rgba(255,255,255,0.95) !important;
}
.sidebar .nav-oznaka {
text-shadow: 0 1px 3px rgba(0,0,0,0.8);
color: rgba(255,255,255,0.7) !important;
}
.topbar {
background: rgba(0,0,0,{{if .AppPozadinaGlassOpacity}}{{.AppPozadinaGlassOpacity}}%{{else}}0.08{{end}}) !important;
backdrop-filter: blur({{.AppPozadinaBlur}}px);
-webkit-backdrop-filter: blur({{.AppPozadinaBlur}}px);
border-bottom: 1px solid rgba(255,255,255,0.12) !important;
}
.kartica {
background: rgba(0,0,0,{{if .AppPozadinaGlassOpacity}}{{.AppPozadinaGlassOpacity}}%{{else}}0.08{{end}}) !important;
backdrop-filter: blur({{.AppPozadinaBlur}}px);
-webkit-backdrop-filter: blur({{.AppPozadinaBlur}}px);
border: 1px solid rgba(255,255,255,0.12) !important;
}
.kartica p,
.kartica span,
.kartica h1,
.kartica h2,
.kartica h3,
.kartica h4,
.kartica label,
.kartica td,
.kartica th,
.kartica li,
.kartica a {
color: rgba(255,255,255,0.95) !important;
text-shadow: 0 1px 3px rgba(0,0,0,0.7);
}
table, th, td {
color: rgba(255,255,255,0.95) !important;
text-shadow: 0 1px 3px rgba(0,0,0,0.8);
}
tr {
background: rgba(0,0,0,0.2);
}
tr:hover {
background: rgba(0,0,0,0.35);
}
thead th {
background: rgba(0,0,0,0.4) !important;
}
div:has(> canvas) {
background: rgba(0,0,0,0.3);
border-radius: 8px;
padding: 8px;
}
.app-bg { position: fixed; inset: {{if ne .AppPozadinaBlurPozadine "0"}}-20px{{else}}0{{end}}; background-image: url('{{.AppPozadina}}'); background-size: cover; background-position: center; filter: blur({{.AppPozadinaBlurPozadine}}px); pointer-events: none; z-index: 0; }
.app-overlay { position: fixed; inset: 0; background: rgba(0,0,0,{{.AppPozadinaOpacity}}%); pointer-events: none; z-index: 1; }
.raspored { position: relative; z-index: 2; }
.sidebar { background: rgba(0,0,0,{{if .AppPozadinaGlassOpacity}}{{.AppPozadinaGlassOpacity}}%{{else}}0.3{{end}}) !important; backdrop-filter: blur({{.AppPozadinaBlur}}px); -webkit-backdrop-filter: blur({{.AppPozadinaBlur}}px); border-right: 1px solid rgba(255,255,255,0.12) !important; }
.sidebar .nav-stavka, .sidebar .logo-naziv, .sidebar .logo-podnazlov { text-shadow: 0 1px 3px rgba(0,0,0,0.8); color: rgba(255,255,255,0.95) !important; }
.sidebar .nav-stavka svg { color: rgba(255,255,255,0.95) !important; stroke: rgba(255,255,255,0.95) !important; }
.sidebar .nav-oznaka { text-shadow: 0 1px 3px rgba(0,0,0,0.8); color: rgba(255,255,255,0.7) !important; }
.topbar { background: rgba(0,0,0,{{if .AppPozadinaGlassOpacity}}{{.AppPozadinaGlassOpacity}}%{{else}}0.08{{end}}) !important; backdrop-filter: blur({{.AppPozadinaBlur}}px); -webkit-backdrop-filter: blur({{.AppPozadinaBlur}}px); border-bottom: 1px solid rgba(255,255,255,0.12) !important; }
.kartica { background: rgba(0,0,0,{{if .AppPozadinaGlassOpacity}}{{.AppPozadinaGlassOpacity}}%{{else}}0.08{{end}}) !important; backdrop-filter: blur({{.AppPozadinaBlur}}px); -webkit-backdrop-filter: blur({{.AppPozadinaBlur}}px); border: 1px solid rgba(255,255,255,0.12) !important; }
.kartica p, .kartica span, .kartica h1, .kartica h2, .kartica h3, .kartica h4, .kartica label, .kartica td, .kartica th, .kartica li, .kartica a { color: rgba(255,255,255,0.95) !important; text-shadow: 0 1px 3px rgba(0,0,0,0.7); }
table, th, td { color: rgba(255,255,255,0.95) !important; text-shadow: 0 1px 3px rgba(0,0,0,0.8); }
tr { background: rgba(0,0,0,0.2); }
tr:hover { background: rgba(0,0,0,0.35); }
thead th { background: rgba(0,0,0,0.4) !important; }
div:has(> canvas) { background: rgba(0,0,0,0.3); border-radius: 8px; padding: 8px; }
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {