Dodata zahtevajDozvolu metoda koja zamenjuje dupliranu proveru dozvola u handlerima.

Uvedena `AssetV` promenljiva za cache-busting statičkih fajlova, koja se postavlja pri svakom pokretanju.
Kreiran pogled `klijent_prikaz` (migracija 038) za jedinstveno prikazivanje imena klijenta, čime se eliminiše ponavljanje COALESCE logike u upitima.
Izvučena `dodeliOpcijeKorisnika` funkcija u korisnici.go radi DRY.
Zamenjeni inline stilovi u HTML šablonima CSS klasama (`.polje-labela`, `.obavezno`, `.pomocni-tekst`, `.tabela`, `.kartica-tabela`, `.prazno-stanje`).
Dodat `width: 100%` na inpute i `resize: vertical` na textarea u main.css.
This commit is contained in:
2026-06-12 00:43:58 +02:00
parent 19b66adba9
commit 726a1dbbf7
22 changed files with 247 additions and 193 deletions
+17 -22
View File
@@ -1,29 +1,20 @@
# Binarne datoteke
# Build — izlazni binarni fajlovi
/ntech
/ntech.exe
# Baza podataka
*.db
*.db-shm
*.db-wal
# Promenljive okruženja
ntech.env
ntech-*
# Privremeni fajlovi
*.tmp
*.log
# Kate
*.kate-swp
.*.kate-swp
# Promenljive okruženja
ntech.env
.env
# VSCodium/VSCode
.vscode/
# IDE podešavanja
.idea/
*.swp
# Baza podataka
*.db
*.db-shm
*.db-wal
# Backup fajlovi
backups/
@@ -34,9 +25,13 @@ logs/
# Upload fajlovi
web/static/uploads/
.env
# Kate editor
*.kate-swp
.*.kate-swp
# Buildovani binarni fajlovi
ntech
ntech-*
# IDE podešavanja
.vscode/
.idea/
*.swp
ARHITEKTURA.md
+12 -2
View File
@@ -120,6 +120,9 @@ func main() {
h := handler.Novi(db)
h.Verzija = Verzija
// verzija statičkih fajlova za cache-busting — menja se pri svakom pokretanju,
// pa novi build/restart natera brauzer da povuče sveži CSS/JS (umesto starog iz keša)
h.AssetV = strconv.FormatInt(time.Now().Unix(), 36)
h.PutanjaBaze = putanjaBaze
// čuva odabrani FS (disk ili embed) za hot-reload u razvoju i za keš u produkciji
h.TemplatesFS = templFS
@@ -144,10 +147,17 @@ func main() {
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
http.FileServer(http.Dir("web/static/uploads")).ServeHTTP(w, req)
})))
// ostali statični fajlovi: disk ako postoji web/static, inače embed
// ostali statični fajlovi: disk ako postoji web/static, inače embed.
// U produkciji dug immutable keš (URL nosi ?v=verzija za cache-busting pri novom buildu);
// u razvoju bez keša, da izmene CSS/JS odmah budu vidljive bez ručnog osvežavanja.
produkcija := os.Getenv("NTECH_ENV") == "production"
r.Handle("/static/*", http.StripPrefix("/static/",
http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
if produkcija {
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
} else {
w.Header().Set("Cache-Control", "no-cache")
}
http.FileServer(http.FS(staticFS)).ServeHTTP(w, req)
})))
+23 -18
View File
@@ -11,6 +11,23 @@ import (
type sqliteKorisniciRepo struct{ db *sql.DB }
// dodeliOpcijeKorisnika popunjava bool i opciona polja korisnika iz skeniranih
// 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,
datumKreiranja time.Time) {
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
}
// skeniraiKorisnika čita jedan red iz baze i popunjava model.Korisnik
func skeniraiKorisnika(row interface{ Scan(...any) error }) (*model.Korisnik, error) {
k := &model.Korisnik{}
@@ -26,15 +43,9 @@ func skeniraiKorisnika(row interface{ Scan(...any) error }) (*model.Korisnik, er
); 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
dodeliOpcijeKorisnika(k, aktivan, koristiLokalnuTemu, lokalnaTema,
lokalnaPozadina, lokalnaPozadinaOpacity, lokalnaPozadinaBlur,
lokalnaPozadinaBlurPozadine, lokalnaPozadinaGlassOpacity, datumKreiranja)
return k, nil
}
@@ -109,15 +120,9 @@ func (r *sqliteKorisniciRepo) Lista(ctx context.Context) ([]model.Korisnik, erro
&lokalnaPozadinaGlassOpacity); err != nil {
return nil, fmt.Errorf("ntech: korisnici.Lista: %w", err)
}
k.Aktivan = aktivan == 1
k.LokalnaTema = lokalnaTema.String
k.KoristiLokalnuTemu = koristiLokalnuTemu == 1
k.DatumKreiranja = datumKreiranja
k.LokalnaPozadina = lokalnaPozadina.String
k.LokalnaPozadinaOpacity = lokalnaPozadinaOpacity.String
k.LokalnaPozadinaBlur = lokalnaPozadinaBlur.String
k.LokalnaPozadinaBlurPozadine = lokalnaPozadinaBlurPozadine.String
k.LokalnaPozadinaGlassOpacity = lokalnaPozadinaGlassOpacity.String
dodeliOpcijeKorisnika(&k, aktivan, koristiLokalnuTemu, lokalnaTema,
lokalnaPozadina, lokalnaPozadinaOpacity, lokalnaPozadinaBlur,
lokalnaPozadinaBlurPozadine, lokalnaPozadinaGlassOpacity, datumKreiranja)
lista = append(lista, k)
}
return lista, nil
+2 -2
View File
@@ -43,9 +43,9 @@ func (r *ProdajaRepo) Lista(ctx context.Context, pretraga string) ([]model.Proda
SELECT
pn.id, pn.klijent_id, pn.broj_naloga, pn.napomena, pn.ukupno,
pn.nacin_placanja, pn.stornirano, pn.datum,
COALESCE(NULLIF(k.naziv_firme, ''), TRIM(COALESCE(k.ime, '') || ' ' || COALESCE(k.prezime, '')), '') AS klijent_naziv
COALESCE(kp.naziv, '') AS klijent_naziv
FROM prodajni_nalozi pn
LEFT JOIN klijenti k ON k.id = pn.klijent_id
LEFT JOIN klijent_prikaz kp ON kp.id = pn.klijent_id
WHERE 1=1`
args := []any{}
+2 -2
View File
@@ -43,9 +43,9 @@ func (r *ServisRepo) Lista(ctx context.Context, pretraga, status string) ([]mode
sn.id, sn.klijent_id, sn.tehnicar_id, sn.broj_naloga, sn.uredjaj, sn.serijski_broj,
sn.opis_kvara, sn.status, sn.cena_od, sn.cena_do, sn.cena_konacna,
sn.avans, sn.napomena, sn.garancija_do, sn.datum_prijema, sn.datum_zavrsetka,
COALESCE(NULLIF(k.naziv_firme, ''), TRIM(COALESCE(k.ime, '') || ' ' || COALESCE(k.prezime, '')), '') AS klijent_naziv
COALESCE(kp.naziv, '') AS klijent_naziv
FROM servisni_nalozi sn
LEFT JOIN klijenti k ON k.id = sn.klijent_id
LEFT JOIN klijent_prikaz kp ON kp.id = sn.klijent_id
WHERE 1=1`
args := []any{}
+2 -2
View File
@@ -127,9 +127,9 @@ func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) {
prodajaRedovi, err := h.DB.QueryContext(ctx, `
SELECT
pn.broj_naloga, pn.ukupno, pn.datum,
COALESCE(NULLIF(k.naziv_firme, ''), TRIM(COALESCE(k.ime, '') || ' ' || COALESCE(k.prezime, '')), '') AS klijent_naziv
COALESCE(kp.naziv, '') AS klijent_naziv
FROM prodajni_nalozi pn
LEFT JOIN klijenti k ON k.id = pn.klijent_id
LEFT JOIN klijent_prikaz kp ON kp.id = pn.klijent_id
ORDER BY pn.datum DESC LIMIT 5`)
if err != nil {
log.Printf("dashboard: poslednje prodaje: %v", err)
+3 -10
View File
@@ -5,7 +5,6 @@ import (
"strings"
"ntech/internal/db/sqlite"
"ntech/internal/middleware"
"ntech/internal/model"
"github.com/go-chi/chi/v5"
@@ -77,9 +76,7 @@ func (h *Handler) NoviDobavljac(w http.ResponseWriter, r *http.Request) {
// SacuvajDobavljaca prima POST formu i upisuje novog dobavljača u bazu
func (h *Handler) SacuvajDobavljaca(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "dobavljac.dodaj") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
if _, ok := h.zahtevajDozvolu(w, r, "dobavljac.dodaj"); !ok {
return
}
if err := r.ParseForm(); err != nil {
@@ -142,9 +139,7 @@ func (h *Handler) IzmeniDobavljaca(w http.ResponseWriter, r *http.Request) {
// SacuvajIzmeneDobavljaca prima POST formu i ažurira postojećeg dobavljača u bazi
func (h *Handler) SacuvajIzmeneDobavljaca(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "dobavljac.izmeni") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
if _, ok := h.zahtevajDozvolu(w, r, "dobavljac.izmeni"); !ok {
return
}
id, err := parseID(chi.URLParam(r, "id"))
@@ -185,9 +180,7 @@ func (h *Handler) SacuvajIzmeneDobavljaca(w http.ResponseWriter, r *http.Request
// ObrisiDobavljaca prima POST zahtev i briše dobavljača po ID-u
func (h *Handler) ObrisiDobavljaca(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "dobavljac.obrisi") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
if _, ok := h.zahtevajDozvolu(w, r, "dobavljac.obrisi"); !ok {
return
}
id, err := parseID(chi.URLParam(r, "id"))
+13
View File
@@ -32,6 +32,7 @@ type Handler struct {
LoginIstorijsaRepo db.LoginIstorijsaRepository
DozvoleRepo db.DozvoleRepository
Verzija string
AssetV string // verzija statičkih fajlova za cache-busting (postavlja se pri pokretanju)
Templates map[string]*template.Template
TemplatesFS fs.FS
}
@@ -78,6 +79,17 @@ func (h *Handler) reinicijalizujRepozitorijume(novaDB *sql.DB) {
h.DozvoleRepo = sqlite.NoviDozvoleRepo(novaDB, middleware.ImaDozvolu, middleware.SveAkcije())
}
// zahtevajDozvolu vraća prijavljenog korisnika ako njegova uloga sme da izvrši akciju.
// U suprotnom šalje 403 sa srpskom porukom i vraća ok=false (handler tada return-uje).
func (h *Handler) zahtevajDozvolu(w http.ResponseWriter, r *http.Request, akcija string) (*model.Korisnik, bool) {
k := middleware.KorisnikIzKonteksta(r.Context())
if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, akcija) {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
return nil, false
}
return k, true
}
// popuniPodaciStranice popunjava zajednička polja stranice uključujući prijavljenog korisnika
func (h *Handler) popuniPodaciStranice(r *http.Request, podesavanja map[string]string) model.PodaciStranice {
// podrazumevana tema je tamna; korisnik može imati svoju lokalnu temu
@@ -104,6 +116,7 @@ func (h *Handler) popuniPodaciStranice(r *http.Request, podesavanja map[string]s
}
}
ps.CsrfToken = middleware.CsrfToken(r.Context())
ps.AssetV = h.AssetV
ps.Flash = middleware.GetFlash(r, h.DB)
// logika pozadine:
+5 -7
View File
@@ -9,7 +9,6 @@ import (
"time"
"ntech/internal/db/sqlite"
"ntech/internal/middleware"
"ntech/internal/model"
)
@@ -74,9 +73,7 @@ type TopKlijent struct {
// Izvestaji renderuje stranicu sa izveštajima
func (h *Handler) Izvestaji(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "izvestaj.pregled") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
if _, ok := h.zahtevajDozvolu(w, r, "izvestaj.pregled"); !ok {
return
}
ctx := r.Context()
@@ -165,9 +162,9 @@ func (h *Handler) Izvestaji(w http.ResponseWriter, r *http.Request) {
// --- stari otvoreni nalozi (>14 dana bez završetka) ---
stariRed, err := h.DB.QueryContext(ctx, `
SELECT sn.id, sn.broj_naloga, sn.uredjaj, sn.status, sn.datum_prijema,
COALESCE(NULLIF(k.naziv_firme, ''), TRIM(COALESCE(k.ime, '') || ' ' || COALESCE(k.prezime, '')), '—') AS klijent_naziv
COALESCE(NULLIF(kp.naziv, ''), '—') AS klijent_naziv
FROM servisni_nalozi sn
LEFT JOIN klijenti k ON k.id = sn.klijent_id
LEFT JOIN klijent_prikaz kp ON kp.id = sn.klijent_id
WHERE sn.datum_zavrsetka IS NULL
AND substr(sn.datum_prijema, 1, 10) <= date('now', '-14 days')
ORDER BY sn.datum_prijema ASC`)
@@ -215,10 +212,11 @@ func (h *Handler) Izvestaji(w http.ResponseWriter, r *http.Request) {
// --- top 10 klijenata po ukupnoj vrednosti ---
klijRed, err := h.DB.QueryContext(ctx, `
SELECT
COALESCE(NULLIF(k.naziv_firme, ''), TRIM(COALESCE(k.ime, '') || ' ' || COALESCE(k.prezime, ''))) AS naziv,
kp.naziv AS naziv,
COALESCE(p.ukupno_prodaja, 0) + COALESCE(s.ukupno_servis, 0) AS ukupno_vrednost,
COALESCE(p.broj_prodaja, 0) + COALESCE(s.broj_servisa, 0) AS broj_naloga
FROM klijenti k
LEFT JOIN klijent_prikaz kp ON kp.id = k.id
LEFT JOIN (
SELECT klijent_id, SUM(ukupno) AS ukupno_prodaja, COUNT(*) AS broj_prodaja
FROM prodajni_nalozi GROUP BY klijent_id
+2 -7
View File
@@ -5,7 +5,6 @@ import (
"strconv"
"ntech/internal/db/sqlite"
"ntech/internal/middleware"
"ntech/internal/model"
"github.com/go-chi/chi/v5"
@@ -48,9 +47,7 @@ func (h *Handler) Kategorije(w http.ResponseWriter, r *http.Request) {
// DodajKategoriju prima POST i čuva novu kategoriju
func (h *Handler) DodajKategoriju(w http.ResponseWriter, r *http.Request) {
kor := middleware.KorisnikIzKonteksta(r.Context())
if kor == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), kor.Uloga, "kategorija.dodaj") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
if _, ok := h.zahtevajDozvolu(w, r, "kategorija.dodaj"); !ok {
return
}
if err := r.ParseForm(); err != nil {
@@ -79,9 +76,7 @@ func (h *Handler) DodajKategoriju(w http.ResponseWriter, r *http.Request) {
// ObrisiKategoriju briše kategoriju po ID-u
func (h *Handler) ObrisiKategoriju(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "kategorija.obrisi") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
if _, ok := h.zahtevajDozvolu(w, r, "kategorija.obrisi"); !ok {
return
}
idStr := chi.URLParam(r, "id")
+3 -10
View File
@@ -6,7 +6,6 @@ import (
"strings"
"ntech/internal/db/sqlite"
"ntech/internal/middleware"
"ntech/internal/model"
"github.com/go-chi/chi/v5"
@@ -78,9 +77,7 @@ func (h *Handler) NoviKlijent(w http.ResponseWriter, r *http.Request) {
// SacuvajKlijenta prima POST formu i upisuje novog klijenta u bazu
func (h *Handler) SacuvajKlijenta(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "klijent.dodaj") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
if _, ok := h.zahtevajDozvolu(w, r, "klijent.dodaj"); !ok {
return
}
if err := r.ParseForm(); err != nil {
@@ -153,9 +150,7 @@ func (h *Handler) IzmeniKlijenta(w http.ResponseWriter, r *http.Request) {
// SacuvajIzmenuKlijenta prima POST formu i ažurira postojećeg klijenta u bazi
func (h *Handler) SacuvajIzmenuKlijenta(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "klijent.izmeni") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
if _, ok := h.zahtevajDozvolu(w, r, "klijent.izmeni"); !ok {
return
}
id, err := parseID(chi.URLParam(r, "id"))
@@ -206,9 +201,7 @@ func (h *Handler) SacuvajIzmenuKlijenta(w http.ResponseWriter, r *http.Request)
// ObrisiKlijenta prima POST zahtev i briše klijenta po ID-u
func (h *Handler) ObrisiKlijenta(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "klijent.obrisi") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
if _, ok := h.zahtevajDozvolu(w, r, "klijent.obrisi"); !ok {
return
}
id, err := parseID(chi.URLParam(r, "id"))
+2 -7
View File
@@ -6,7 +6,6 @@ import (
"ntech/internal/db"
"ntech/internal/db/sqlite"
"ntech/internal/middleware"
"ntech/internal/model"
"github.com/go-chi/chi/v5"
@@ -78,9 +77,7 @@ func (h *Handler) Magacin(w http.ResponseWriter, r *http.Request) {
// PremestiArtikal menja kategoriju artikla (premeštanje u drugu kategoriju).
// Prazno polje kategorija_id znači premeštanje u "bez kategorije".
func (h *Handler) PremestiArtikal(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "artikal.premesti") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
if _, ok := h.zahtevajDozvolu(w, r, "artikal.premesti"); !ok {
return
}
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
@@ -109,9 +106,7 @@ func (h *Handler) PremestiArtikal(w http.ResponseWriter, r *http.Request) {
// ObrisiArtikal briše artikal po ID-u
func (h *Handler) ObrisiArtikal(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "artikal.obrisi") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
if _, ok := h.zahtevajDozvolu(w, r, "artikal.obrisi"); !ok {
return
}
idStr := chi.URLParam(r, "id")
+2 -7
View File
@@ -9,7 +9,6 @@ import (
"ntech/internal/db"
"ntech/internal/db/sqlite"
"ntech/internal/middleware"
"ntech/internal/model"
"github.com/go-chi/chi/v5"
@@ -122,9 +121,7 @@ func (h *Handler) NovaNabavka(w http.ResponseWriter, r *http.Request) {
// SacuvajNabavku prima POST formu, parsira stavke i upisuje nabavku u bazu
func (h *Handler) SacuvajNabavku(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "nabavka.dodaj") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
if _, ok := h.zahtevajDozvolu(w, r, "nabavka.dodaj"); !ok {
return
}
if err := r.ParseForm(); err != nil {
@@ -211,9 +208,7 @@ func (h *Handler) DetaljiNabavke(w http.ResponseWriter, r *http.Request) {
// ObrisiNabavku prima POST zahtev i briše nabavku po ID-u
func (h *Handler) ObrisiNabavku(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "nabavka.obrisi") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
if _, ok := h.zahtevajDozvolu(w, r, "nabavka.obrisi"); !ok {
return
}
id, err := parseID(chi.URLParam(r, "id"))
+11 -33
View File
@@ -56,9 +56,7 @@ var validnoImeBackupa = regexp.MustCompile(`^ntech_\d{8}_\d{6}\.db$`)
// Podesavanja renderuje stranicu podešavanja
func (h *Handler) Podesavanja(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "podesavanja.pregled") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
if _, ok := h.zahtevajDozvolu(w, r, "podesavanja.pregled"); !ok {
return
}
podesavanja, err := ntechsqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
@@ -134,9 +132,7 @@ func vrednostIliDefault(m map[string]string, kljuc, podrazumevano string) string
// VratiBackup zamenjuje trenutnu bazu sa izabranim backup fajlom
func (h *Handler) VratiBackup(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "backup.pokreni") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
if _, ok := h.zahtevajDozvolu(w, r, "backup.pokreni"); !ok {
return
}
if err := r.ParseForm(); err != nil {
@@ -219,9 +215,7 @@ func kopiraFajl(izvor, odrediste string) error {
// SacuvajPodesavanja prima POST i čuva podešavanja u bazu
func (h *Handler) SacuvajPodesavanja(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "podesavanja.izmeni") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
if _, ok := h.zahtevajDozvolu(w, r, "podesavanja.izmeni"); !ok {
return
}
if err := r.ParseForm(); err != nil {
@@ -285,9 +279,7 @@ func (h *Handler) SacuvajPodesavanja(w http.ResponseWriter, r *http.Request) {
// BackupBaze kreira konzistentnu kopiju baze i šalje je kao attachment
func (h *Handler) BackupBaze(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "backup.pregled") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
if _, ok := h.zahtevajDozvolu(w, r, "backup.pregled"); !ok {
return
}
privremeni := fmt.Sprintf("%s/ntech_backup_%s.db", os.TempDir(), time.Now().Format("20060102_150405"))
@@ -306,9 +298,7 @@ func (h *Handler) BackupBaze(w http.ResponseWriter, r *http.Request) {
// OtpremiLogo prima multipart upload slike loga i čuva je u web/static/uploads/
func (h *Handler) OtpremiLogo(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "podesavanja.izmeni") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
if _, ok := h.zahtevajDozvolu(w, r, "podesavanja.izmeni"); !ok {
return
}
// ograničavamo telo zahteva na 2MB + malo za zaglavlja forme
@@ -404,9 +394,7 @@ func generisiImeUploada(ext string) (string, error) {
// OtpremiLoginPozadinu prima multipart upload slike i čuva je kao pozadinsku sliku login stranice
func (h *Handler) OtpremiLoginPozadinu(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "podesavanja.login_pozadina") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
if _, ok := h.zahtevajDozvolu(w, r, "podesavanja.login_pozadina"); !ok {
return
}
@@ -508,9 +496,7 @@ func (h *Handler) OtpremiLoginPozadinu(w http.ResponseWriter, r *http.Request) {
// UkloniLoginPozadinu briše pozadinsku sliku login stranice sa diska i iz podešavanja
func (h *Handler) UkloniLoginPozadinu(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "podesavanja.login_pozadina") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
if _, ok := h.zahtevajDozvolu(w, r, "podesavanja.login_pozadina"); !ok {
return
}
@@ -536,9 +522,7 @@ func (h *Handler) UkloniLoginPozadinu(w http.ResponseWriter, r *http.Request) {
// SacuvajLoginPozadinaStilove čuva vrednosti zamućenja i prozirnosti pozadine login stranice
func (h *Handler) SacuvajLoginPozadinaStilove(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "podesavanja.login_pozadina") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
if _, ok := h.zahtevajDozvolu(w, r, "podesavanja.login_pozadina"); !ok {
return
}
if err := r.ParseForm(); err != nil {
@@ -630,9 +614,7 @@ func (h *Handler) napuniPodaciPodesavanja(r *http.Request, naslov string) (Podac
// PodesavanjaOpste renderuje stranicu sa opštim podešavanjima (firma i logo)
func (h *Handler) PodesavanjaOpste(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "podesavanja.pregled") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
if _, ok := h.zahtevajDozvolu(w, r, "podesavanja.pregled"); !ok {
return
}
podaci, err := h.napuniPodaciPodesavanja(r, "Podešavanja — Opšte")
@@ -646,9 +628,7 @@ func (h *Handler) PodesavanjaOpste(w http.ResponseWriter, r *http.Request) {
// PodesavanjaIzgled renderuje stranicu sa podešavanjima izgleda (pozadine i tema)
func (h *Handler) PodesavanjaIzgled(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "podesavanja.pregled") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
if _, ok := h.zahtevajDozvolu(w, r, "podesavanja.pregled"); !ok {
return
}
podaci, err := h.napuniPodaciPodesavanja(r, "Podešavanja — Izgled")
@@ -662,9 +642,7 @@ func (h *Handler) PodesavanjaIzgled(w http.ResponseWriter, r *http.Request) {
// PodesavanjaSistem renderuje stranicu sa sistemskim podešavanjima (backup)
func (h *Handler) PodesavanjaSistem(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "podesavanja.pregled") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
if _, ok := h.zahtevajDozvolu(w, r, "podesavanja.pregled"); !ok {
return
}
podaci, err := h.napuniPodaciPodesavanja(r, "Podešavanja — Sistem")
+3 -6
View File
@@ -137,9 +137,8 @@ func (h *Handler) NovaProdaja(w http.ResponseWriter, r *http.Request) {
// SacuvajProdaju prima POST formu, parsira stavke i upisuje prodajni nalog u bazu
func (h *Handler) SacuvajProdaju(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "prodaja.dodaj") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
k, ok := h.zahtevajDozvolu(w, r, "prodaja.dodaj")
if !ok {
return
}
if err := r.ParseForm(); err != nil {
@@ -307,9 +306,7 @@ func (h *Handler) StampaProdaje(w http.ResponseWriter, r *http.Request) {
// ObrisiProdaju prima POST zahtev, vraća stanje na magacin i briše nalog
func (h *Handler) ObrisiProdaju(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "prodaja.obrisi") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
if _, ok := h.zahtevajDozvolu(w, r, "prodaja.obrisi"); !ok {
return
}
id, err := parseID(chi.URLParam(r, "id"))
+7 -15
View File
@@ -122,9 +122,7 @@ func (h *Handler) NoviNalog(w http.ResponseWriter, r *http.Request) {
// SacuvajNalog prima POST formu i upisuje novi servisni nalog u bazu
func (h *Handler) SacuvajNalog(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "servis.dodaj") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
if _, ok := h.zahtevajDozvolu(w, r, "servis.dodaj"); !ok {
return
}
if err := r.ParseForm(); err != nil {
@@ -223,9 +221,7 @@ func (h *Handler) IzmeniNalog(w http.ResponseWriter, r *http.Request) {
// SacuvajIzmenaNaloga prima POST formu i ažurira postojeći servisni nalog
func (h *Handler) SacuvajIzmenaNaloga(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "servis.izmeni") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
if _, ok := h.zahtevajDozvolu(w, r, "servis.izmeni"); !ok {
return
}
id, err := parseID(chi.URLParam(r, "id"))
@@ -286,9 +282,7 @@ func (h *Handler) SacuvajIzmenaNaloga(w http.ResponseWriter, r *http.Request) {
// ObrisiNalog prima POST zahtev i briše servisni nalog po ID-u
func (h *Handler) ObrisiNalog(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "servis.obrisi") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
if _, ok := h.zahtevajDozvolu(w, r, "servis.obrisi"); !ok {
return
}
id, err := parseID(chi.URLParam(r, "id"))
@@ -370,9 +364,8 @@ func (h *Handler) DetaljiNaloga(w http.ResponseWriter, r *http.Request) {
// DodajDeloNalogu prima POST formu i dodaje artikal kao deo servisnog naloga
func (h *Handler) DodajDeloNalogu(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "servis.izmeni") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
k, ok := h.zahtevajDozvolu(w, r, "servis.izmeni")
if !ok {
return
}
@@ -421,9 +414,8 @@ func (h *Handler) DodajDeloNalogu(w http.ResponseWriter, r *http.Request) {
// ObrisiDeloNaloga prima POST zahtev i uklanja deo iz servisnog naloga
func (h *Handler) ObrisiDeloNaloga(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "servis.izmeni") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
k, ok := h.zahtevajDozvolu(w, r, "servis.izmeni")
if !ok {
return
}
+1
View File
@@ -42,6 +42,7 @@ type PodaciStranice struct {
KorisnikIme string // korisničko ime prijavljenog korisnika
KorisnikUloga string // uloga: "superadmin", "admin", "radnik"
CsrfToken string // CSRF zaštitni token za forme
AssetV string // verzija statičkih fajlova (cache-busting za CSS/JS)
Dozvole map[string]bool // mapa akcija → dozvoljeno/nije
Flash *FlashPoruka // jednokratna poruka nakon redirecta
// app pozadina — popunjava se iz podešavanja za sve stranice
+8
View File
@@ -0,0 +1,8 @@
-- View za prikazni naziv klijenta — objedinjuje logiku koja se ranije ponavljala
-- u upitima prodaje, servisa, dashboarda i izveštaja.
-- Za pravno lice vraća naziv firme; za fizičko "ime prezime"; inače prazan string.
CREATE VIEW IF NOT EXISTS klijent_prikaz AS
SELECT id,
COALESCE(NULLIF(naziv_firme, ''),
TRIM(COALESCE(ime, '') || ' ' || COALESCE(prezime, '')), '') AS naziv
FROM klijenti;
+87
View File
@@ -602,6 +602,7 @@ input[type="number"],
input[type="date"],
select {
height: 38px;
width: 100%;
padding: 8px 12px;
box-sizing: border-box;
line-height: 1.5;
@@ -616,10 +617,12 @@ select {
textarea {
height: auto;
width: 100%;
min-height: 80px;
padding: 8px 12px;
box-sizing: border-box;
line-height: 1.5;
resize: vertical;
background: var(--kartica) !important;
color: var(--tekst-glavni) !important;
border: 0.5px solid var(--ivica) !important;
@@ -666,6 +669,90 @@ select {
color: var(--greska);
}
/* ============================================================
Komponente forme i tabela zamena za ponavljane inline stilove.
Cilj: čitljiviji template, jedno mesto istine za izgled.
============================================================ */
/* labela iznad polja forme */
.polje-labela {
display: block;
font-size: 13px;
color: var(--tekst-sporedni);
margin-bottom: 6px;
}
/* oznaka obaveznog polja (zvezdica) */
.obavezno {
color: var(--greska);
}
/* sitan pomoćni/sporedni tekst (objašnjenja, vrednosti u listama) */
.pomocni-tekst {
font-size: 13px;
color: var(--tekst-sporedni);
}
/* vertikalni raspored polja unutar forme */
.forma-kolona {
display: flex;
flex-direction: column;
gap: 18px;
}
/* grid raspored polja osnovne (desktop) vrednosti;
responsivni override na uže ekrane je niže u media bloku */
.forma-grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.forma-grid-4 {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
gap: 12px;
}
/* === tabele === */
.tabela {
width: 100%;
border-collapse: collapse;
}
.tabela thead tr {
border-bottom: 0.5px solid var(--ivica);
}
.tabela th {
padding: 12px 16px;
text-align: left;
font-size: 12px;
font-weight: 500;
color: var(--tekst-jak);
}
.tabela td {
padding: 12px 16px;
}
.tabela .centar {
text-align: center;
}
/* kartica koja sadrži tabelu — bez paddinga, zaobljeni uglovi sečeni */
.kartica-tabela {
padding: 0;
overflow: hidden;
}
/* horizontalni skrol omotač oko tabele na uskim ekranima */
.tabela-skrol {
overflow-x: auto;
}
/* prazno stanje (nema podataka) u tabeli ili listi kartica */
.prazno-stanje {
padding: 32px;
text-align: center;
font-size: 14px;
color: var(--tekst-sporedni);
}
/* overlay za mobilni — tamni sloj iza sidebara */
.sidebar-overlay {
display: none;
+23 -24
View File
@@ -20,7 +20,7 @@
{{end}}
{{define "sadrzaj"}}
<div style="width:100%;">
<div>
<a href="/klijenti" 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>
@@ -40,7 +40,7 @@
<form method="POST" action="{{if .Izmena}}/klijenti/izmeni/{{.Klijent.ID}}{{else}}/klijenti/novi{{end}}">
<input type="hidden" name="csrf_token" value="{{.CsrfToken}}">
<div style="display:flex;flex-direction:column;gap:18px;">
<div class="forma-kolona">
<!-- tip klijenta -->
<div>
@@ -64,42 +64,42 @@
<!-- sekcija fizičko lice -->
<div id="sec-fizicko" style="display:flex;flex-direction:column;gap:12px;">
<div class="sekcija-naslov">Ime i prezime</div>
<div class="forma-grid-2" style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
<div class="forma-grid-2">
<div>
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:6px;">
Ime <span id="ime-oznaka" style="color:#dc2626;">*</span>
<label class="polje-labela">
Ime <span id="ime-oznaka" class="obavezno">*</span>
</label>
<input type="text" name="ime" value="{{.Klijent.Ime}}"
placeholder="npr. Marko" style="width:100%;">
placeholder="npr. Marko">
</div>
<div>
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:6px;">Prezime</label>
<label class="polje-labela">Prezime</label>
<input type="text" name="prezime" value="{{.Klijent.Prezime}}"
placeholder="npr. Petrović" style="width:100%;">
placeholder="npr. Petrović">
</div>
</div>
<div style="max-width:260px;">
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:6px;">JMBG</label>
<label class="polje-labela">JMBG</label>
<input type="text" name="jmbg" value="{{.Klijent.JMBG}}"
placeholder="13 cifara" maxlength="13" style="width:100%;">
placeholder="13 cifara" maxlength="13">
</div>
</div>
<!-- sekcija pravno lice -->
<div id="sec-pravno" style="display:none;flex-direction:column;gap:12px;">
<div class="sekcija-naslov">Firma</div>
<div class="forma-grid-2" style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
<div class="forma-grid-2">
<div>
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:6px;">
Naziv firme <span style="color:#dc2626;">*</span>
<label class="polje-labela">
Naziv firme <span class="obavezno">*</span>
</label>
<input type="text" name="naziv_firme" value="{{.Klijent.NazivFirme}}"
placeholder="npr. TechSolutions d.o.o." style="width:100%;">
placeholder="npr. TechSolutions d.o.o.">
</div>
<div>
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:6px;">PIB</label>
<label class="polje-labela">PIB</label>
<input type="text" name="pib" value="{{.Klijent.PIB}}"
placeholder="npr. 123456789" style="width:100%;">
placeholder="npr. 123456789">
</div>
</div>
</div>
@@ -107,26 +107,25 @@
<!-- kontakt -->
<div>
<div class="sekcija-naslov">Kontakt</div>
<div class="forma-grid-2" style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
<div class="forma-grid-2">
<div>
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:6px;">Telefon</label>
<label class="polje-labela">Telefon</label>
<input type="text" name="telefon" value="{{.Klijent.Telefon}}"
placeholder="npr. 060 123 4567" style="width:100%;">
placeholder="npr. 060 123 4567">
</div>
<div>
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:6px;">E-pošta</label>
<label class="polje-labela">E-pošta</label>
<input type="text" name="email" value="{{.Klijent.Email}}"
placeholder="npr. marko@example.com" style="width:100%;">
placeholder="npr. marko@example.com">
</div>
</div>
</div>
<!-- napomena -->
<div>
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:6px;">Napomena</label>
<label class="polje-labela">Napomena</label>
<textarea name="napomena" rows="3"
placeholder="Interna napomena o klijentu..."
style="width:100%;resize:vertical;">{{.Klijent.Napomena}}</textarea>
placeholder="Interna napomena o klijentu...">{{.Klijent.Napomena}}</textarea>
</div>
<!-- dugmad -->
+16 -16
View File
@@ -47,22 +47,22 @@
</div>
<!-- desktop tabela -->
<div class="kartica klijenti-tabela animiraj" style="padding:0;overflow:hidden;">
<div style="overflow-x:auto;">
<table style="width:100%;border-collapse:collapse;">
<div class="kartica klijenti-tabela kartica-tabela animiraj">
<div class="tabela-skrol">
<table class="tabela">
<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-jak);">Ime / Naziv firme</th>
<th style="padding:12px 16px;text-align:left;font-size:12px;font-weight:500;color:var(--tekst-jak);">Telefon</th>
<th style="padding:12px 16px;text-align:left;font-size:12px;font-weight:500;color:var(--tekst-jak);">E-pošta</th>
<th style="padding:12px 16px;text-align:left;font-size:12px;font-weight:500;color:var(--tekst-jak);">Datum unosa</th>
<th style="padding:12px 16px;text-align:center;font-size:12px;font-weight:500;color:var(--tekst-jak);">Akcije</th>
<tr>
<th>Ime / Naziv firme</th>
<th>Telefon</th>
<th>E-pošta</th>
<th>Datum unosa</th>
<th class="centar">Akcije</th>
</tr>
</thead>
<tbody>
{{range .Klijenti}}
<tr class="animiraj red-tabele">
<td style="padding:12px 16px;">
<td>
{{if .NazivFirme}}
<div style="font-size:14px;font-weight:500;color:var(--tekst-glavni);">{{.NazivFirme}}</div>
{{if or .Ime .Prezime}}
@@ -72,16 +72,16 @@
<div style="font-size:14px;font-weight:500;color:var(--tekst-glavni);">{{.Ime}} {{.Prezime}}</div>
{{end}}
</td>
<td style="padding:12px 16px;font-size:13px;color:var(--tekst-sporedni);">
<td class="pomocni-tekst">
{{if .Telefon}}{{.Telefon}}{{else}}—{{end}}
</td>
<td style="padding:12px 16px;font-size:13px;color:var(--tekst-sporedni);">
<td class="pomocni-tekst">
{{if .Email}}{{.Email}}{{else}}—{{end}}
</td>
<td style="padding:12px 16px;font-size:13px;color:var(--tekst-sporedni);">
<td class="pomocni-tekst">
{{.DatumUnosa.Format "02.01.2006."}}
</td>
<td style="padding:12px 16px;text-align:center;">
<td class="centar">
<div style="display:flex;align-items:center;justify-content:center;gap:8px;">
<a href="/klijenti/izmeni/{{.ID}}" class="btn-primarno-malo">
Izmeni
@@ -99,7 +99,7 @@
</tr>
{{else}}
<tr>
<td colspan="5" style="padding:32px;text-align:center;font-size:14px;color:var(--tekst-sporedni);">
<td colspan="5" class="prazno-stanje">
Nema klijenata. <a href="/klijenti/novi" style="color:var(--sb-akcent);">Dodaj prvog klijenta.</a>
</td>
</tr>
@@ -155,7 +155,7 @@
</div>
</div>
{{else}}
<div style="padding:32px;text-align:center;font-size:14px;color:var(--tekst-sporedni);">
<div class="prazno-stanje">
Nema klijenata. <a href="/klijenti/novi" style="color:var(--sb-akcent);">Dodaj prvog klijenta.</a>
</div>
{{end}}
+3 -3
View File
@@ -17,10 +17,10 @@
{{if .AppPozadina}}<link rel="preload" as="image" href="{{.AppPozadina}}">{{end}}
<!-- tema — učitava se prva -->
<link rel="stylesheet" href="/static/css/teme/{{.Tema}}.css" />
<link rel="stylesheet" href="/static/css/teme/{{.Tema}}.css?v={{.AssetV}}" />
<!-- glavni stilovi -->
<link rel="stylesheet" href="/static/css/main.css" />
<link rel="stylesheet" href="/static/css/main.css?v={{.AssetV}}" />
{{block "dodatni-css" .}}{{end}}
@@ -108,7 +108,7 @@
</div>
<!-- alpine.js komponente (mora biti pre Alpine-a) -->
<script src="/static/js/ntech.js" defer></script>
<script src="/static/js/ntech.js?v={{.AssetV}}" defer></script>
<!-- alpine.js CSP build (lokalno, bez unsafe-eval) -->
<script src="/static/js/alpine.csp.min.js" defer></script>