Compare commits

...

4 Commits

Author SHA1 Message Date
Dasko 85cb1e25c7 Topbar: logo firme + naslov; avatar upload; uklanjanje logo zone
- Topbar: logo slika firme (toggle on/off) pa naslov stranice; bez teksta firme
- Sidebar: samo naziv firme i podnaslov (tekst), bez slike loga
- Avatar: korisnik uploaduje ličnu sliku u Profil > Tema > Avatar;
  prikazuje se kao dugme za meni (desno u topbaru); fallback inicijali
- Logo firme kartica: dugme "Ukloni sliku" + ruta /podesavanja/logo/ukloni
- Logo zona iz podešavanja uklonjena; jedan iOS toggle za prikaz loga u topbaru
- Migracije 049 (topbar_logo_slika/tekst) i 050 (avatar_putanja na korisnicima)
- iOS-style .toggl switch u main.css
2026-06-16 02:46:48 +02:00
Dasko 3c5c8060c1 Magacin: pretraga po nazivu, lokaciji i kategoriji; ispravka skakanja dashboard kartica
- artikal.go: pretraga obuhvata naziv, lokaciju i naziv kategorije (OR LIKE)
- main.css: dashboard stat kartice dobijaju stagger delay da ne krenemo pre view-transition
- base.html: vraćen <link rel="preload"> za sliku pozadine (uklonjen greškom)
2026-06-16 02:04:32 +02:00
Dasko 330f30d8bb Ispravka boje tačke na kritičnim zalihama i uklanjanje CSS warning-a
- Boja tačke na kritičnim zalihama: crvena kad je količina upola manja od minimalne
- Uklonjen preload pozadinske slike (nepotreban, izazivao warning u konzoli)
- @view-transition umotan u @supports da ne izaziva warning u browserima koji ne podržavaju
2026-06-16 01:40:43 +02:00
Dasko c934dde4c0 Dashboard: linkovi sa stat kartica na odgovarajuće stranice
Artikala na stanju -> /magacin
Aktivnih servisa -> /servis
Prihod ovog meseca -> /izvestaji
Kritično niska zaliha -> /magacin?kriticni=1
2026-06-15 23:55:10 +02:00
21 changed files with 353 additions and 83 deletions
+3
View File
@@ -253,6 +253,7 @@ func main() {
r.With(doz("podesavanja.izmeni")).Post("/podesavanja/pdv-stope/{id}/aktivnost", h.PromeniAktivnostPdvStope)
r.With(doz("podesavanja.izmeni")).Post("/podesavanja/sacuvaj", h.SacuvajPodesavanja)
r.With(doz("podesavanja.izmeni")).Post("/podesavanja/logo", h.OtpremiLogo)
r.With(doz("podesavanja.izmeni")).Post("/podesavanja/logo/ukloni", h.UkloniLogo)
r.With(doz("podesavanja.login_pozadina")).Post("/podesavanja/login-pozadina", h.OtpremiLoginPozadinu)
r.With(doz("podesavanja.login_pozadina")).Post("/podesavanja/login-pozadina/ukloni", h.UkloniLoginPozadinu)
r.With(doz("podesavanja.login_pozadina")).Post("/podesavanja/login-pozadina/stilovi", h.SacuvajLoginPozadinaStilove)
@@ -359,6 +360,8 @@ func main() {
r.With(doz("tema.lokalno")).Post("/profil/pozadina", h.ProfilOtpremiPozadinu)
r.With(doz("tema.lokalno")).Post("/profil/pozadina/ukloni", h.ProfilUkloniPozadinu)
r.With(doz("tema.lokalno")).Post("/profil/pozadina/stilovi", h.ProfilSacuvajPozadinuStilove)
r.Post("/profil/avatar", h.ProfilOtpremiAvatar)
r.Post("/profil/avatar/ukloni", h.ProfilUkloniAvatar)
})
slog.Info("NTech pokrenut", "port", port)
+1
View File
@@ -149,6 +149,7 @@ type KorisniciRepository interface {
SacuvajTotpTajnu(ctx context.Context, id int64, tajna string) error
SacuvajLokalnuTemu(ctx context.Context, id int64, lokalnaTema string, koristi bool) error
SacuvajLokalnuPozadinu(ctx context.Context, id int64, pozadina, opacity, blur, blurPozadine, glassOpacity string) error
SacuvajAvatar(ctx context.Context, id int64, putanja string) error
PostojiIjedan(ctx context.Context) (bool, error)
Obrisi(ctx context.Context, id int64) error
}
+3 -2
View File
@@ -34,8 +34,9 @@ func (r *ArtikalRepo) Lista(ctx context.Context, filter db.ArtikalFilter) ([]mod
args := []any{}
if filter.Pretraga != "" {
upit += " AND a.naziv LIKE ?"
args = append(args, "%"+filter.Pretraga+"%")
upit += " AND (a.naziv LIKE ? OR a.lokacija LIKE ? OR k.naziv LIKE ?)"
t := "%" + filter.Pretraga + "%"
args = append(args, t, t, t)
}
if filter.KategorijaID != nil {
+2 -2
View File
@@ -75,7 +75,7 @@ func (r *sqliteIzvestajRepo) PoslednjiServisi(ctx context.Context, limit int) ([
func (r *sqliteIzvestajRepo) KriticneZalihe(ctx context.Context, limit int) ([]model.ZalihaRed, error) {
rows, err := r.db.QueryContext(ctx, `
SELECT naziv, kolicina FROM artikli
SELECT naziv, kolicina, kolicina_min FROM artikli
WHERE kolicina <= kolicina_min
ORDER BY kolicina ASC LIMIT ?`, limit)
if err != nil {
@@ -85,7 +85,7 @@ func (r *sqliteIzvestajRepo) KriticneZalihe(ctx context.Context, limit int) ([]m
var lista []model.ZalihaRed
for rows.Next() {
var z model.ZalihaRed
if err := rows.Scan(&z.Naziv, &z.Kolicina); err != nil {
if err := rows.Scan(&z.Naziv, &z.Kolicina, &z.KolicinaMin); err != nil {
return nil, fmt.Errorf("ntech: izvestaj.KriticneZalihe: %w", err)
}
lista = append(lista, z)
+20 -8
View File
@@ -21,7 +21,7 @@ type sqliteKorisniciRepo struct {
// NULL vrednosti — deljeno između skeniraiKorisnika (jedan red) i Lista (više redova)
func dodeliOpcijeKorisnika(k *model.Korisnik, aktivan, koristiLokalnuTemu int,
lokalnaTema, lokalnaPozadina, lokalnaPozadinaOpacity, lokalnaPozadinaBlur,
lokalnaPozadinaBlurPozadine, lokalnaPozadinaGlassOpacity sql.NullString,
lokalnaPozadinaBlurPozadine, lokalnaPozadinaGlassOpacity, avatarPutanja sql.NullString,
datumKreiranja time.Time) {
k.Aktivan = aktivan == 1
k.LokalnaTema = lokalnaTema.String
@@ -32,6 +32,7 @@ func dodeliOpcijeKorisnika(k *model.Korisnik, aktivan, koristiLokalnuTemu int,
k.LokalnaPozadinaBlur = lokalnaPozadinaBlur.String
k.LokalnaPozadinaBlurPozadine = lokalnaPozadinaBlurPozadine.String
k.LokalnaPozadinaGlassOpacity = lokalnaPozadinaGlassOpacity.String
k.AvatarPutanja = avatarPutanja.String
}
// skeniraiKorisnika čita jedan red iz baze i popunjava model.Korisnik
@@ -40,18 +41,19 @@ func skeniraiKorisnika(row interface{ Scan(...any) error }) (*model.Korisnik, er
var aktivan, koristiLokalnuTemu int
var lokalnaTema sql.NullString
var lokalnaPozadina, lokalnaPozadinaOpacity, lokalnaPozadinaBlur, lokalnaPozadinaBlurPozadine, lokalnaPozadinaGlassOpacity sql.NullString
var avatarPutanja 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,
&lokalnaPozadinaBlurPozadine, &lokalnaPozadinaGlassOpacity, &avatarPutanja,
); err != nil {
return nil, err
}
dodeliOpcijeKorisnika(k, aktivan, koristiLokalnuTemu, lokalnaTema,
lokalnaPozadina, lokalnaPozadinaOpacity, lokalnaPozadinaBlur,
lokalnaPozadinaBlurPozadine, lokalnaPozadinaGlassOpacity, datumKreiranja)
lokalnaPozadinaBlurPozadine, lokalnaPozadinaGlassOpacity, avatarPutanja, datumKreiranja)
return k, nil
}
@@ -91,7 +93,7 @@ func (r *sqliteKorisniciRepo) DohvatiPoImenu(ctx context.Context, korisnickoIme
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')
COALESCE(lokalna_pozadina_glass_opacity, '10'), COALESCE(avatar_putanja, '')
FROM korisnici WHERE korisnicko_ime = ?`, korisnickoIme)
k, err := skeniraiKorisnika(row)
if err != nil {
@@ -107,7 +109,7 @@ func (r *sqliteKorisniciRepo) DohvatiPoID(ctx context.Context, id int64) (*model
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')
COALESCE(lokalna_pozadina_glass_opacity, '10'), COALESCE(avatar_putanja, '')
FROM korisnici WHERE id = ?`, id)
k, err := skeniraiKorisnika(row)
if err != nil {
@@ -123,7 +125,7 @@ func (r *sqliteKorisniciRepo) Lista(ctx context.Context) ([]model.Korisnik, erro
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')
COALESCE(lokalna_pozadina_glass_opacity, '10'), COALESCE(avatar_putanja, '')
FROM korisnici ORDER BY datum_kreiranja ASC`)
if err != nil {
return nil, fmt.Errorf("ntech: korisnici.Lista: %w", err)
@@ -135,22 +137,32 @@ func (r *sqliteKorisniciRepo) Lista(ctx context.Context) ([]model.Korisnik, erro
var aktivan, koristiLokalnuTemu int
var lokalnaTema sql.NullString
var lokalnaPozadina, lokalnaPozadinaOpacity, lokalnaPozadinaBlur, lokalnaPozadinaBlurPozadine, lokalnaPozadinaGlassOpacity sql.NullString
var avatarPutanja sql.NullString
var datumKreiranja time.Time
if err := rows.Scan(&k.ID, &k.KorisnickoIme, &k.Uloga, &aktivan, &k.TotpTajna,
&lokalnaTema, &koristiLokalnuTemu, &datumKreiranja,
&lokalnaPozadina, &lokalnaPozadinaOpacity, &lokalnaPozadinaBlur, &lokalnaPozadinaBlurPozadine,
&lokalnaPozadinaGlassOpacity); err != nil {
&lokalnaPozadinaGlassOpacity, &avatarPutanja); err != nil {
return nil, fmt.Errorf("ntech: korisnici.Lista: %w", err)
}
dodeliOpcijeKorisnika(&k, aktivan, koristiLokalnuTemu, lokalnaTema,
lokalnaPozadina, lokalnaPozadinaOpacity, lokalnaPozadinaBlur,
lokalnaPozadinaBlurPozadine, lokalnaPozadinaGlassOpacity, datumKreiranja)
lokalnaPozadinaBlurPozadine, lokalnaPozadinaGlassOpacity, avatarPutanja, datumKreiranja)
r.desifrujTotpTajnu(&k)
lista = append(lista, k)
}
return lista, nil
}
func (r *sqliteKorisniciRepo) SacuvajAvatar(ctx context.Context, id int64, putanja string) error {
_, err := r.db.ExecContext(ctx,
`UPDATE korisnici SET avatar_putanja = ? WHERE id = ?`, putanja, id)
if err != nil {
return fmt.Errorf("ntech: korisnici.SacuvajAvatar: %w", err)
}
return nil
}
func (r *sqliteKorisniciRepo) SacuvajLokalnuPozadinu(ctx context.Context, id int64, pozadina, opacity, blur, blurPozadine, glassOpacity string) error {
_, err := r.db.ExecContext(ctx,
`UPDATE korisnici SET lokalna_pozadina = ?, lokalna_pozadina_opacity = ?, lokalna_pozadina_blur = ?, lokalna_pozadina_blur_pozadine = ?, lokalna_pozadina_glass_opacity = ? WHERE id = ?`,
+1 -1
View File
@@ -95,7 +95,7 @@ func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) {
} else {
for _, z := range redovi {
boja := "#f97316"
if z.Kolicina == 0 {
if z.Kolicina == 0 || (z.KolicinaMin > 0 && z.Kolicina <= z.KolicinaMin/2) {
boja = "#dc2626"
}
kriticneZalihe = append(kriticneZalihe, model.StavkaZalihe{
+8 -6
View File
@@ -161,12 +161,13 @@ func (h *Handler) popuniPodaciStranice(r *http.Request, podesavanja map[string]s
tema := "tamna"
ps := model.PodaciStranice{
Tema: tema,
NazivFirme: podesavanja["naziv_firme"],
Podnazlov: podesavanja["podnazlov"],
LogoTip: podesavanja["logo_tip"],
LogoPutanja: podesavanja["logo_putanja"],
Korisnik: "Admin",
Tema: tema,
NazivFirme: podesavanja["naziv_firme"],
Podnazlov: podesavanja["podnazlov"],
LogoPutanja: podesavanja["logo_putanja"],
TopbarLogoSlika: podesavanja["topbar_logo_slika"] == "1",
TopbarLogoTekst: podesavanja["topbar_logo_tekst"] == "1",
Korisnik: "Admin",
}
var korisnik *model.Korisnik
if k := middleware.KorisnikIzKonteksta(r.Context()); k != nil {
@@ -174,6 +175,7 @@ func (h *Handler) popuniPodaciStranice(r *http.Request, podesavanja map[string]s
ps.Korisnik = k.KorisnickoIme
ps.KorisnikIme = k.KorisnickoIme
ps.KorisnikUloga = k.Uloga
ps.AvatarPutanja = k.AvatarPutanja
ps.Dozvole = h.DozvoleRepo.SveDozvole(r.Context(), k.Uloga)
// lokalna tema korisnika — primenjuje se uvek kada je postavljena, bez obzira na KoristiLokalnuTemu
if k.LokalnaTema != "" {
+39 -10
View File
@@ -28,8 +28,9 @@ type PodaciPodesavanja struct {
Adresa string
Telefon string
PIB string
LogoTip string
LogoPutanja string
LogoPutanja string
TopbarLogoSlika bool
TopbarLogoTekst bool
// profil firme — pravni/poreski status (Faza 0); određuje koji se zakonski moduli pale
FirmaPravniOblik string
FirmaPdvObveznik string
@@ -81,8 +82,9 @@ func (h *Handler) Podesavanja(w http.ResponseWriter, r *http.Request) {
Adresa: podesavanja["adresa"],
Telefon: podesavanja["telefon"],
PIB: podesavanja["pib"],
LogoTip: podesavanja["logo_tip"],
LogoPutanja: podesavanja["logo_putanja"],
TopbarLogoSlika: podesavanja["topbar_logo_slika"] == "1",
TopbarLogoTekst: podesavanja["topbar_logo_tekst"] == "1",
Sacuvano: r.URL.Query().Get("sacuvano") == "1",
BackupVracen: r.URL.Query().Get("sacuvano") == "vraceno",
Verzija: h.Verzija,
@@ -233,13 +235,24 @@ func (h *Handler) SacuvajPodesavanja(w http.ResponseWriter, r *http.Request) {
return
}
// checkbox-i: šalju vrednost samo kada su čekirani, pa ih uvek eksplicitno čitamo
topbarLogoSlika := "0"
if r.FormValue("topbar_logo_slika") == "1" {
topbarLogoSlika = "1"
}
topbarLogoTekst := "0"
if r.FormValue("topbar_logo_tekst") == "1" {
topbarLogoTekst = "1"
}
polja := map[string]string{
"naziv_firme": r.FormValue("naziv_firme"),
"podnazlov": r.FormValue("podnazlov"),
"adresa": r.FormValue("adresa"),
"telefon": r.FormValue("telefon"),
"pib": r.FormValue("pib"),
"logo_tip": r.FormValue("logo_tip"),
"naziv_firme": r.FormValue("naziv_firme"),
"podnazlov": r.FormValue("podnazlov"),
"adresa": r.FormValue("adresa"),
"telefon": r.FormValue("telefon"),
"pib": r.FormValue("pib"),
"topbar_logo_slika": topbarLogoSlika,
"topbar_logo_tekst": topbarLogoTekst,
// profil firme (Faza 0) — radio dugmad uvek šalju vrednost, pa se uredno čuvaju
"firma_pravni_oblik": r.FormValue("firma_pravni_oblik"),
"firma_pdv_obveznik": r.FormValue("firma_pdv_obveznik"),
@@ -412,6 +425,21 @@ func (h *Handler) OtpremiLogo(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/podesavanja?sacuvano=1", http.StatusSeeOther)
}
// UkloniLogo briše logo fajl i čisti putanju iz podešavanja
func (h *Handler) UkloniLogo(w http.ResponseWriter, r *http.Request) {
if _, ok := h.zahtevajDozvolu(w, r, "podesavanja.izmeni"); !ok {
return
}
stari, _ := filepath.Glob("web/static/uploads/logo.*")
for _, s := range stari {
os.Remove(s)
}
if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "logo_putanja", ""); err != nil {
slog.Error("ukloni logo: greška pri čuvanju", "error", err)
}
http.Redirect(w, r, "/admin/podesavanja/opste?sacuvano=1", http.StatusSeeOther)
}
// generisiImeUploada vraća slučajno hex ime (16 bajtova) sa datom ekstenzijom
func generisiImeUploada(ext string) (string, error) {
buf := make([]byte, 16)
@@ -624,8 +652,9 @@ func (h *Handler) napuniPodaciPodesavanja(r *http.Request, naslov string) (Podac
Adresa: podesavanja["adresa"],
Telefon: podesavanja["telefon"],
PIB: podesavanja["pib"],
LogoTip: podesavanja["logo_tip"],
LogoPutanja: podesavanja["logo_putanja"],
TopbarLogoSlika: podesavanja["topbar_logo_slika"] == "1",
TopbarLogoTekst: podesavanja["topbar_logo_tekst"] == "1",
FirmaPravniOblik: vrednostIliDefault(podesavanja, "firma_pravni_oblik", "pausalac"),
FirmaPdvObveznik: vrednostIliDefault(podesavanja, "firma_pdv_obveznik", "ne"),
FirmaFiskalizacija: vrednostIliDefault(podesavanja, "firma_fiskalizacija", "ne"),
+115
View File
@@ -289,3 +289,118 @@ func (h *Handler) SacuvajLokalnuTemu(w http.ResponseWriter, r *http.Request) {
}
http.Redirect(w, r, "/admin/profil", http.StatusSeeOther)
}
// ProfilOtpremiAvatar prima upload lične avatar slike korisnika
func (h *Handler) ProfilOtpremiAvatar(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if k == nil {
http.Redirect(w, r, "/prijava", http.StatusSeeOther)
return
}
r.Body = http.MaxBytesReader(w, r.Body, 2<<20+4096)
if err := r.ParseMultipartForm(2 << 20); err != nil {
middleware.SetFlash(w, r, h.DB, "greska", "Fajl je prevelik (maksimum 2 MB).")
http.Redirect(w, r, "/profil/tema", http.StatusSeeOther)
return
}
fajl, zaglavlje, err := r.FormFile("avatar")
if err != nil {
middleware.SetFlash(w, r, h.DB, "greska", "Nije odabran fajl.")
http.Redirect(w, r, "/profil/tema", http.StatusSeeOther)
return
}
defer fajl.Close()
ext := strings.ToLower(filepath.Ext(zaglavlje.Filename))
dozvoljenoExt := map[string]string{
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".webp": "image/webp",
}
ocekivaniMime, ok := dozvoljenoExt[ext]
if !ok {
middleware.SetFlash(w, r, h.DB, "greska", "Dozvoljeni formati su JPG, PNG i WebP.")
http.Redirect(w, r, "/profil/tema", http.StatusSeeOther)
return
}
buf := make([]byte, 512)
n, _ := fajl.Read(buf)
stvarniMime := http.DetectContentType(buf[:n])
if !strings.HasPrefix(stvarniMime, ocekivaniMime) {
middleware.SetFlash(w, r, h.DB, "greska", "Sadržaj fajla ne odgovara odabranoj ekstenziji.")
http.Redirect(w, r, "/profil/tema", http.StatusSeeOther)
return
}
if _, err := fajl.Seek(0, io.SeekStart); err != nil {
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri obradi fajla.")
http.Redirect(w, r, "/profil/tema", http.StatusSeeOther)
return
}
// briše stari avatar sa diska
svezi, _ := h.KorisniciRepo.DohvatiPoID(r.Context(), k.ID)
if svezi != nil && svezi.AvatarPutanja != "" {
deo, _, _ := strings.Cut(svezi.AvatarPutanja, "?")
os.Remove(filepath.Join("web/static/uploads", filepath.Base(deo)))
}
novoIme := fmt.Sprintf("korisnik_%d_avatar%s", k.ID, ext)
odrediste := filepath.Join("web/static/uploads", novoIme)
dst, err := os.Create(odrediste)
if err != nil {
slog.Error("ProfilOtpremiAvatar: ne mogu kreirati fajl", "error", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju fajla.")
http.Redirect(w, r, "/profil/tema", http.StatusSeeOther)
return
}
defer dst.Close()
if _, err := io.Copy(dst, fajl); err != nil {
slog.Error("ProfilOtpremiAvatar: greška pri kopiranju", "error", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju fajla.")
http.Redirect(w, r, "/profil/tema", http.StatusSeeOther)
return
}
// cache-buster verzija u URL-u
v := fmt.Sprintf("%d", time.Now().Unix())
putanja := fmt.Sprintf("/static/uploads/%s?v=%s", novoIme, v)
if err := h.KorisniciRepo.SacuvajAvatar(r.Context(), k.ID, putanja); err != nil {
slog.Error("ProfilOtpremiAvatar: greška pri čuvanju u bazi", "error", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju.")
http.Redirect(w, r, "/profil/tema", http.StatusSeeOther)
return
}
middleware.SetFlash(w, r, h.DB, "uspeh", "Avatar je sačuvan.")
http.Redirect(w, r, "/profil/tema", http.StatusSeeOther)
}
// ProfilUkloniAvatar briše ličnu avatar sliku korisnika
func (h *Handler) ProfilUkloniAvatar(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if k == nil {
http.Redirect(w, r, "/prijava", http.StatusSeeOther)
return
}
svezi, _ := h.KorisniciRepo.DohvatiPoID(r.Context(), k.ID)
if svezi != nil && svezi.AvatarPutanja != "" {
deo, _, _ := strings.Cut(svezi.AvatarPutanja, "?")
os.Remove(filepath.Join("web/static/uploads", filepath.Base(deo)))
}
if err := h.KorisniciRepo.SacuvajAvatar(r.Context(), k.ID, ""); err != nil {
slog.Error("ProfilUkloniAvatar", "error", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri uklanjanju avatara.")
http.Redirect(w, r, "/profil/tema", http.StatusSeeOther)
return
}
middleware.SetFlash(w, r, h.DB, "uspeh", "Avatar je uklonjen.")
http.Redirect(w, r, "/profil/tema", http.StatusSeeOther)
}
+3 -2
View File
@@ -15,8 +15,9 @@ type ServisRedDashboard struct {
// ZalihaRed je artikal sa kritičnom zalihom (naziv + količina)
type ZalihaRed struct {
Naziv string
Kolicina int
Naziv string
Kolicina int
KolicinaMin int
}
// ProdajaRedDashboard je jedan red za listu poslednjih prodaja na dashboardu
+1
View File
@@ -18,6 +18,7 @@ type Korisnik struct {
LokalnaPozadinaBlur string
LokalnaPozadinaBlurPozadine string
LokalnaPozadinaGlassOpacity string
AvatarPutanja string
}
// Sesija predstavlja aktivnu sesiju prijavljenog korisnika
+5 -3
View File
@@ -36,9 +36,11 @@ type PodaciStranice struct {
Tema string
NazivFirme string
Podnazlov string
LogoTip string // "sa_nazivom", "bez_naziva", "slika"
LogoPutanja string // putanja do slike, koristi se samo kada je LogoTip "slika"
Korisnik string
LogoPutanja string // putanja do slike loga firme
TopbarLogoSlika bool // prikaži logo sliku u topbaru
TopbarLogoTekst bool // prikaži naziv firme u topbaru
AvatarPutanja string // putanja do lične avatar slike korisnika
Korisnik string
KorisnikIme string // korisničko ime prijavljenog korisnika
KorisnikUloga string // uloga: "superadmin", "admin", "radnik"
CsrfToken string // CSRF zaštitni token za forme
+4
View File
@@ -0,0 +1,4 @@
-- topbar podešavanja: switch za logo sliku i switch za naziv firme
INSERT OR IGNORE INTO podesavanja (kljuc, vrednost) VALUES
('topbar_logo_slika', '1'),
('topbar_logo_tekst', '1');
+2
View File
@@ -0,0 +1,2 @@
-- lična avatar slika korisnika (prazno = koristi inicijale)
ALTER TABLE korisnici ADD COLUMN avatar_putanja TEXT NOT NULL DEFAULT '';
+53 -6
View File
@@ -303,12 +303,28 @@ body {
}
.topbar-naslov {
font-weight: 500;
font-size: 15px;
font-weight: 600;
font-size: 18px;
color: var(--tekst-glavni);
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* identifikacija firme u topbaru (naziv + logo) — pomera se i može da se skrati */
.topbar-firma { flex-shrink: 0; line-height: 1.2; min-width: 0; }
.topbar-firma .topbar-firma-naziv {
font-weight: 700; font-size: 14px; color: var(--tekst-glavni); letter-spacing: -0.2px;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.topbar-firma .topbar-firma-podnaziv {
font-size: 11px; color: var(--tekst-sporedni);
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.topbar-logo { height: 34px; width: auto; border-radius: 6px; flex-shrink: 0; }
/* sadržaj stranice */
.sadrzaj {
flex: 1;
@@ -583,7 +599,7 @@ body {
.avatar-korisnik {
width: 32px;
height: 32px;
border-radius: 50%;
border-radius: 9px;
background: var(--sb-akcent);
display: flex;
align-items: center;
@@ -918,6 +934,11 @@ select {
#hamburger-topbar { display: flex !important; color: var(--tekst-glavni); }
#hamburger-topbar:hover { background: var(--pozadina); }
/* na telefonu sklanjamo identifikaciju firme iz topbara — naziv i logo su
već vidljivi u bočnom meniju, pa naslov stranice dobija ceo prostor */
.topbar-firma, .topbar-logo { display: none; }
.topbar-naslov { font-size: 16px; }
/* teme */
.topbar-teme { display: none; }
.teme-grid { flex-direction: column !important; }
@@ -1017,6 +1038,13 @@ select {
.stranica-stack .animiraj:nth-child(4) { animation-delay: 0.28s; }
.stranica-stack .animiraj:nth-child(5) { animation-delay: 0.34s; }
/* Dashboard stat kartice — delay da ne krenemo pre nego što view-transition završi */
.dash-stat.animiraj:nth-child(1) { animation-delay: 0.08s; }
.dash-stat.animiraj:nth-child(2) { animation-delay: 0.13s; }
.dash-stat.animiraj:nth-child(3) { animation-delay: 0.18s; }
.dash-stat.animiraj:nth-child(4) { animation-delay: 0.23s; }
.dash-stat.animiraj:nth-child(5) { animation-delay: 0.28s; }
/* Bedž statusa servisnog naloga — JEDNO mesto za izgled i boje statusa (lista i detalji).
Mora biti u main.css: HTMX navigacija odbacuje <head>, pa page <style> ne bi važio. */
.status-badge { display: inline-block; padding: 3px 10px; border-radius: 20px; font-size: 12px; font-weight: 500; white-space: nowrap; }
@@ -1153,11 +1181,30 @@ select {
position: relative;
}
/* sprečava treperenje pozadine pri navigaciji između stranica (Chrome/Opera/noviji Firefox) */
@view-transition {
navigation: auto;
/* sprečava treperenje pozadine pri navigaciji između stranica (samo browseri koji podržavaju) */
@supports (view-transition-name: none) {
@view-transition {
navigation: auto;
}
}
/* iOS-style toggle switch (.toggl > input[type=checkbox] + .toggl-klizac) */
.toggl { position:relative; display:inline-block; width:44px; height:26px; flex-shrink:0; }
.toggl input { opacity:0; width:0; height:0; position:absolute; }
.toggl-klizac {
position:absolute; inset:0; border-radius:26px; cursor:pointer;
background:var(--ivica); transition:background 0.2s;
}
.toggl-klizac::before {
content:''; position:absolute;
width:20px; height:20px; border-radius:50%;
left:3px; top:3px;
background:#fff; box-shadow:0 1px 3px rgba(0,0,0,0.25);
transition:transform 0.2s;
}
.toggl input:checked + .toggl-klizac { background:var(--sb-akcent); }
.toggl input:checked + .toggl-klizac::before { transform:translateX(18px); }
/* pomoćne klase (ranije iz Tailwind-a, sada lokalno da ne zavisimo od CDN-a) */
.grid { display: grid; }
.gap-3 { gap: 12px; }
+4 -12
View File
@@ -9,18 +9,10 @@
</svg>
</button>
<div class="logo-zona">
{{if eq .LogoTip "slika"}}
<img src="{{.LogoPutanja}}" alt="Logo"
style="max-height:48px;width:auto;border-radius:8px;padding:4px;background:var(--kartica);box-shadow:var(--senka);transition:transform 0.2s;flex-shrink:0;"
onmouseover="this.style.transform='scale(1.02)'"
onmouseout="this.style.transform='scale(1)'">
{{else if eq .LogoTip "bez_naziva"}}
{{else}}
<div>
<div class="logo-naziv">{{.NazivFirme}}</div>
<div class="logo-podnazlov">{{.Podnazlov}}</div>
</div>
{{end}}
<div>
<div class="logo-naziv">{{.NazivFirme}}</div>
{{if .Podnazlov}}<div class="logo-podnazlov">{{.Podnazlov}}</div>{{end}}
</div>
</div>
</div>
+9 -1
View File
@@ -10,11 +10,19 @@
</svg>
</button>
{{if and .TopbarLogoSlika .LogoPutanja}}
<img src="{{.LogoPutanja}}" alt="Logo" class="topbar-logo">
{{end}}
<span class="topbar-naslov">{{.NaslovStranice}}</span>
<div style="position:relative;" id="avatar-wrapper">
<div class="avatar-korisnik" id="avatar-dugme" style="cursor:pointer;" title="{{.Korisnik}}">
<div class="avatar-korisnik" id="avatar-dugme" style="cursor:pointer;overflow:hidden;" title="{{.Korisnik}}">
{{if .AvatarPutanja}}
<img src="{{.AvatarPutanja}}" alt="Avatar" style="width:100%;height:100%;object-fit:cover;display:block;">
{{else}}
{{if .Korisnik}}{{slice .Korisnik 0 2}}{{else}}NT{{end}}
{{end}}
</div>
<div id="avatar-meni" style="display:none;position:fixed;background:var(--kartica);backdrop-filter:none;-webkit-backdrop-filter:none;border:0.5px solid var(--ivica);border-radius:10px;box-shadow:var(--senka);min-width:160px;z-index:9999;overflow:hidden;transform-origin:top right;">
<a href="/admin/profil" style="display:flex;align-items:center;gap:10px;padding:10px 14px;font-size:14px;color:var(--tekst-glavni);text-decoration:none;transition:background 0.15s;"
+9 -10
View File
@@ -21,7 +21,7 @@
{{ end }}
<div class="grid grid-cols-2 md:grid-cols-5 gap-3 mb-6">
<!-- stat kartice -->
<div class="kartica dash-stat animiraj">
<a href="/magacin" class="kartica dash-stat animiraj" style="text-decoration:none;display:block;cursor:pointer;">
<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" />
@@ -29,9 +29,9 @@
</div>
<div style="font-size:22px;font-weight:500;color:var(--tekst-glavni);">{{ .BrojArtikala }}</div>
<div style="font-size:14px;color:var(--tekst-glavni);margin-top:4px;font-weight:500;">Artikala na stanju</div>
</div>
</a>
<div class="kartica dash-stat animiraj">
<a href="/servis" class="kartica dash-stat animiraj" style="text-decoration:none;display:block;cursor:pointer;">
<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" />
@@ -39,10 +39,10 @@
</div>
<div style="font-size:22px;font-weight:500;color:var(--tekst-glavni);">{{ .AktivniServisi }}</div>
<div style="font-size:14px;color:var(--tekst-glavni);margin-top:4px;font-weight:500;">Aktivnih servisa</div>
</div>
</a>
{{ if index .Dozvole "dashboard.prihod" }}
<div class="kartica dash-stat animiraj">
<a href="/izvestaji" class="kartica dash-stat animiraj" style="text-decoration:none;display:block;cursor:pointer;">
<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" />
@@ -52,10 +52,10 @@
</div>
<div style="font-size:22px;font-weight:500;color:var(--tekst-glavni);">{{ printf "%.0f" .PrihodOvogMeseca }} din</div>
<div style="font-size:14px;color:var(--tekst-glavni);margin-top:4px;font-weight:500;">Prihod ovog meseca</div>
</div>
</a>
{{ end }}
<div class="kartica dash-stat animiraj">
<a href="/magacin?kriticni=1" class="kartica dash-stat animiraj" style="text-decoration:none;display:block;cursor:pointer;">
<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" />
@@ -65,9 +65,9 @@
</div>
<div style="font-size:22px;font-weight:500;color:var(--tekst-glavni);">{{ .KriticnaZaliha }}</div>
<div style="font-size:14px;color:var(--tekst-glavni);margin-top:4px;font-weight:500;">Kritično niska zaliha</div>
</div>
</a>
<a href="/podsetnici" class="kartica dash-stat animiraj" style="text-decoration:none;display:block;">
<a href="/podsetnici" class="kartica dash-stat animiraj" style="text-decoration:none;display:block;cursor:pointer;">
<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" />
@@ -100,7 +100,6 @@
<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);">
+20 -17
View File
@@ -11,15 +11,23 @@
<!-- upload loga — posebna forma jer je multipart, mora biti van glavne forme -->
<form method="POST" action="/podesavanja/logo" enctype="multipart/form-data">
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
<div class="kartica animiraj" style="margin-bottom:16px;">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px;padding-bottom:12px;border-bottom:0.5px solid var(--ivica);">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--sb-akcent)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
<span style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">Logo firme</span>
</div>
{{if .LogoPutanja}}
<div style="margin-bottom:12px;">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:12px;">
<img src="{{.LogoPutanja}}" alt="Trenutni logo"
style="max-height:60px;max-width:200px;object-fit:contain;border:0.5px solid var(--ivica);border-radius:8px;padding:6px;background:var(--kartica);">
style="max-height:60px;max-width:200px;object-fit:contain;border:0.5px solid var(--ivica);border-radius:8px;padding:6px;background:var(--kartica);flex-shrink:0;">
<form method="POST" action="/podesavanja/logo/ukloni" style="margin:0;">
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
<button type="submit"
style="padding:6px 14px;background:none;border:0.5px solid #dc2626;color:#dc2626;border-radius:8px;font-size:13px;cursor:pointer;white-space:nowrap;">
Ukloni sliku
</button>
</form>
</div>
{{end}}
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;">
@@ -43,6 +51,7 @@
<!-- firma: naziv, podnazlov, adresa, telefon, PIB, logo zona -->
<form method="POST" action="/podesavanja/sacuvaj">
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
<input type="hidden" name="_next" value="/admin/podesavanja/opste">
<div class="kartica animiraj" style="margin-bottom:16px;">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px;padding-bottom:12px;border-bottom:0.5px solid var(--ivica);">
@@ -87,22 +96,15 @@
</div>
</div>
<div>
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:8px;">Logo zona</label>
<div style="display:flex;gap:10px;flex-wrap:wrap;">
<label style="display:flex;align-items:center;gap:8px;padding:10px 14px;border:0.5px solid var(--ivica);border-radius:8px;cursor:pointer;flex:1;{{if or (eq .LogoTip "sa_nazivom") (eq .LogoTip "tekst") (eq .LogoTip "ikonica") (not .LogoTip)}}border-color:var(--sb-akcent);background:var(--pozadina);{{end}}">
<input type="radio" name="logo_tip" value="sa_nazivom" {{if or (eq .LogoTip "sa_nazivom") (eq .LogoTip "tekst") (eq .LogoTip "ikonica") (not .LogoTip)}}checked{{end}} style="accent-color:var(--sb-akcent);">
<span style="font-size:13px;color:var(--tekst-glavni);">Sa nazivom</span>
</label>
<label style="display:flex;align-items:center;gap:8px;padding:10px 14px;border:0.5px solid var(--ivica);border-radius:8px;cursor:pointer;flex:1;{{if eq .LogoTip "bez_naziva"}}border-color:var(--sb-akcent);background:var(--pozadina);{{end}}">
<input type="radio" name="logo_tip" value="bez_naziva" {{if eq .LogoTip "bez_naziva"}}checked{{end}} style="accent-color:var(--sb-akcent);">
<span style="font-size:13px;color:var(--tekst-glavni);">Bez naziva</span>
</label>
<label style="display:flex;align-items:center;gap:8px;padding:10px 14px;border:0.5px solid var(--ivica);border-radius:8px;flex:1;{{if eq .LogoTip "slika"}}border-color:var(--sb-akcent);background:var(--pozadina);{{end}}{{if not .LogoPutanja}}opacity:0.45;cursor:not-allowed;{{else}}cursor:pointer;{{end}}">
<input type="radio" name="logo_tip" value="slika" {{if eq .LogoTip "slika"}}checked{{end}} {{if not .LogoPutanja}}disabled{{end}} style="accent-color:var(--sb-akcent);">
<span style="font-size:13px;color:var(--tekst-glavni);">Sa logom</span>
</label>
<div style="display:flex;align-items:center;justify-content:space-between;padding:10px 14px;border:0.5px solid var(--ivica);border-radius:8px;">
<div>
<div style="font-size:13px;font-weight:500;color:var(--tekst-glavni);">Prikaži logo u gornjoj traci</div>
<div style="font-size:12px;color:var(--tekst-sporedni);">Logo slika se prikazuje pored naslova stranice</div>
</div>
<label class="toggl">
<input type="checkbox" name="topbar_logo_slika" value="1" {{if .TopbarLogoSlika}}checked{{end}}>
<span class="toggl-klizac"></span>
</label>
</div>
</div>
@@ -119,6 +121,7 @@
<!-- profil firme: pravni/poreski status (Faza 0) — određuje koji se zakonski moduli pale -->
<form method="POST" action="/podesavanja/sacuvaj">
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
<input type="hidden" name="_next" value="/admin/podesavanja/opste">
<div class="kartica animiraj" style="margin-bottom:16px;">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px;padding-bottom:12px;border-bottom:0.5px solid var(--ivica);">
+48
View File
@@ -74,6 +74,54 @@
{{end}}
</div>
<!-- kartica: moj avatar -->
<div class="kartica animiraj" style="margin-bottom:16px;">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px;padding-bottom:12px;border-bottom:0.5px solid var(--ivica);">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--sb-akcent)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
<span style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">Moj avatar</span>
</div>
<div style="display:flex;align-items:center;gap:16px;margin-bottom:16px;">
<div style="width:64px;height:64px;border-radius:10px;overflow:hidden;border:0.5px solid var(--ivica);flex-shrink:0;background:var(--pozadina);display:flex;align-items:center;justify-content:center;">
{{if .AvatarPutanja}}
<img src="{{.AvatarPutanja}}" alt="Moj avatar" style="width:100%;height:100%;object-fit:cover;">
{{else}}
<span style="font-size:22px;font-weight:600;color:var(--tekst-sporedni);">{{if .KorisnikIme}}{{slice .KorisnikIme 0 2}}{{else}}NT{{end}}</span>
{{end}}
</div>
<div style="font-size:13px;color:var(--tekst-sporedni);line-height:1.5;">
Slika se prikazuje kao dugme u gornjoj traci.<br>
Dozvoljeni formati: JPG, PNG, WebP. Maksimum 2 MB.
</div>
</div>
{{if .AvatarPutanja}}
<form method="POST" action="/profil/avatar/ukloni" style="display:inline;margin-bottom:12px;">
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
<button type="submit" style="background:none;border:0.5px solid #dc2626;color:#dc2626;padding:6px 14px;border-radius:8px;font-size:13px;cursor:pointer;margin-bottom:12px;">
Ukloni avatar
</button>
</form>
{{end}}
<form method="POST" action="/profil/avatar" enctype="multipart/form-data">
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;">
<input type="file" id="avatar-file" name="avatar" accept=".jpg,.jpeg,.png,.webp"
style="display:none;" onchange="document.getElementById('avatar-ime').textContent=this.files[0]?this.files[0].name:'Nijedan fajl nije izabran'">
<label for="avatar-file"
style="display:inline-flex;align-items:center;gap:6px;padding:8px 14px;border:0.5px solid var(--ivica);border-radius:8px;cursor:pointer;font-size:13px;color:var(--tekst-glavni);background:var(--pozadina);">
Izaberi sliku
</label>
<span id="avatar-ime" class="pomocni-tekst">Nijedan fajl nije izabran</span>
<button type="submit"
style="background:var(--sb-akcent);color:#fff;border:none;padding:8px 16px;border-radius:8px;font-size:13px;cursor:pointer;">
Otpremi avatar
</button>
</div>
</form>
</div>
<!-- kartica: moja pozadinska slika -->
<div class="kartica animiraj" style="margin-bottom:16px;">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px;padding-bottom:12px;border-bottom:0.5px solid var(--ivica);">
+2 -2
View File
@@ -14,8 +14,6 @@
}
</script>
{{if .AppPozadina}}<link rel="preload" as="image" href="{{.AppPozadina}}">{{end}}
<!-- tema — učitava se prva -->
<link rel="stylesheet" href="/static/css/teme/{{.Tema}}.css?v={{.AssetV}}" />
@@ -24,6 +22,8 @@
{{block "dodatni-css" .}}{{end}}
{{if .AppPozadina}}<link rel="preload" as="image" href="{{.AppPozadina}}">{{end}}
{{if .AppPozadina}}
<style>
html {