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
/ntech.exe /ntech.exe
ntech-*
# Baza podataka
*.db
*.db-shm
*.db-wal
# Promenljive okruženja
ntech.env
# Privremeni fajlovi # Privremeni fajlovi
*.tmp *.tmp
*.log *.log
# Kate # Promenljive okruženja
*.kate-swp ntech.env
.*.kate-swp .env
# VSCodium/VSCode # Baza podataka
.vscode/ *.db
*.db-shm
# IDE podešavanja *.db-wal
.idea/
*.swp
# Backup fajlovi # Backup fajlovi
backups/ backups/
@@ -34,9 +25,13 @@ logs/
# Upload fajlovi # Upload fajlovi
web/static/uploads/ web/static/uploads/
.env # Kate editor
*.kate-swp
.*.kate-swp
# Buildovani binarni fajlovi # IDE podešavanja
ntech .vscode/
ntech-* .idea/
*.swp
ARHITEKTURA.md
+12 -2
View File
@@ -120,6 +120,9 @@ func main() {
h := handler.Novi(db) h := handler.Novi(db)
h.Verzija = Verzija 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 h.PutanjaBaze = putanjaBaze
// čuva odabrani FS (disk ili embed) za hot-reload u razvoju i za keš u produkciji // čuva odabrani FS (disk ili embed) za hot-reload u razvoju i za keš u produkciji
h.TemplatesFS = templFS h.TemplatesFS = templFS
@@ -144,10 +147,17 @@ func main() {
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
http.FileServer(http.Dir("web/static/uploads")).ServeHTTP(w, req) 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/", r.Handle("/static/*", http.StripPrefix("/static/",
http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 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) http.FileServer(http.FS(staticFS)).ServeHTTP(w, req)
}))) })))
+23 -18
View File
@@ -11,6 +11,23 @@ import (
type sqliteKorisniciRepo struct{ db *sql.DB } 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 // skeniraiKorisnika čita jedan red iz baze i popunjava model.Korisnik
func skeniraiKorisnika(row interface{ Scan(...any) error }) (*model.Korisnik, error) { func skeniraiKorisnika(row interface{ Scan(...any) error }) (*model.Korisnik, error) {
k := &model.Korisnik{} k := &model.Korisnik{}
@@ -26,15 +43,9 @@ func skeniraiKorisnika(row interface{ Scan(...any) error }) (*model.Korisnik, er
); err != nil { ); err != nil {
return nil, err return nil, err
} }
k.Aktivan = aktivan == 1 dodeliOpcijeKorisnika(k, aktivan, koristiLokalnuTemu, lokalnaTema,
k.LokalnaTema = lokalnaTema.String lokalnaPozadina, lokalnaPozadinaOpacity, lokalnaPozadinaBlur,
k.KoristiLokalnuTemu = koristiLokalnuTemu == 1 lokalnaPozadinaBlurPozadine, lokalnaPozadinaGlassOpacity, datumKreiranja)
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 return k, nil
} }
@@ -109,15 +120,9 @@ func (r *sqliteKorisniciRepo) Lista(ctx context.Context) ([]model.Korisnik, erro
&lokalnaPozadinaGlassOpacity); err != nil { &lokalnaPozadinaGlassOpacity); err != nil {
return nil, fmt.Errorf("ntech: korisnici.Lista: %w", err) return nil, fmt.Errorf("ntech: korisnici.Lista: %w", err)
} }
k.Aktivan = aktivan == 1 dodeliOpcijeKorisnika(&k, aktivan, koristiLokalnuTemu, lokalnaTema,
k.LokalnaTema = lokalnaTema.String lokalnaPozadina, lokalnaPozadinaOpacity, lokalnaPozadinaBlur,
k.KoristiLokalnuTemu = koristiLokalnuTemu == 1 lokalnaPozadinaBlurPozadine, lokalnaPozadinaGlassOpacity, datumKreiranja)
k.DatumKreiranja = datumKreiranja
k.LokalnaPozadina = lokalnaPozadina.String
k.LokalnaPozadinaOpacity = lokalnaPozadinaOpacity.String
k.LokalnaPozadinaBlur = lokalnaPozadinaBlur.String
k.LokalnaPozadinaBlurPozadine = lokalnaPozadinaBlurPozadine.String
k.LokalnaPozadinaGlassOpacity = lokalnaPozadinaGlassOpacity.String
lista = append(lista, k) lista = append(lista, k)
} }
return lista, nil return lista, nil
+2 -2
View File
@@ -43,9 +43,9 @@ func (r *ProdajaRepo) Lista(ctx context.Context, pretraga string) ([]model.Proda
SELECT SELECT
pn.id, pn.klijent_id, pn.broj_naloga, pn.napomena, pn.ukupno, pn.id, pn.klijent_id, pn.broj_naloga, pn.napomena, pn.ukupno,
pn.nacin_placanja, pn.stornirano, pn.datum, 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 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` WHERE 1=1`
args := []any{} 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.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.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, 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 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` WHERE 1=1`
args := []any{} 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, ` prodajaRedovi, err := h.DB.QueryContext(ctx, `
SELECT SELECT
pn.broj_naloga, pn.ukupno, pn.datum, 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 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`) ORDER BY pn.datum DESC LIMIT 5`)
if err != nil { if err != nil {
log.Printf("dashboard: poslednje prodaje: %v", err) log.Printf("dashboard: poslednje prodaje: %v", err)
+3 -10
View File
@@ -5,7 +5,6 @@ import (
"strings" "strings"
"ntech/internal/db/sqlite" "ntech/internal/db/sqlite"
"ntech/internal/middleware"
"ntech/internal/model" "ntech/internal/model"
"github.com/go-chi/chi/v5" "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 // SacuvajDobavljaca prima POST formu i upisuje novog dobavljača u bazu
func (h *Handler) SacuvajDobavljaca(w http.ResponseWriter, r *http.Request) { func (h *Handler) SacuvajDobavljaca(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context()) if _, ok := h.zahtevajDozvolu(w, r, "dobavljac.dodaj"); !ok {
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "dobavljac.dodaj") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
return return
} }
if err := r.ParseForm(); err != nil { 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 // SacuvajIzmeneDobavljaca prima POST formu i ažurira postojećeg dobavljača u bazi
func (h *Handler) SacuvajIzmeneDobavljaca(w http.ResponseWriter, r *http.Request) { func (h *Handler) SacuvajIzmeneDobavljaca(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context()) if _, ok := h.zahtevajDozvolu(w, r, "dobavljac.izmeni"); !ok {
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "dobavljac.izmeni") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
return return
} }
id, err := parseID(chi.URLParam(r, "id")) 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 // ObrisiDobavljaca prima POST zahtev i briše dobavljača po ID-u
func (h *Handler) ObrisiDobavljaca(w http.ResponseWriter, r *http.Request) { func (h *Handler) ObrisiDobavljaca(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context()) if _, ok := h.zahtevajDozvolu(w, r, "dobavljac.obrisi"); !ok {
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "dobavljac.obrisi") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
return return
} }
id, err := parseID(chi.URLParam(r, "id")) id, err := parseID(chi.URLParam(r, "id"))
+13
View File
@@ -32,6 +32,7 @@ type Handler struct {
LoginIstorijsaRepo db.LoginIstorijsaRepository LoginIstorijsaRepo db.LoginIstorijsaRepository
DozvoleRepo db.DozvoleRepository DozvoleRepo db.DozvoleRepository
Verzija string Verzija string
AssetV string // verzija statičkih fajlova za cache-busting (postavlja se pri pokretanju)
Templates map[string]*template.Template Templates map[string]*template.Template
TemplatesFS fs.FS TemplatesFS fs.FS
} }
@@ -78,6 +79,17 @@ func (h *Handler) reinicijalizujRepozitorijume(novaDB *sql.DB) {
h.DozvoleRepo = sqlite.NoviDozvoleRepo(novaDB, middleware.ImaDozvolu, middleware.SveAkcije()) 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 // popuniPodaciStranice popunjava zajednička polja stranice uključujući prijavljenog korisnika
func (h *Handler) popuniPodaciStranice(r *http.Request, podesavanja map[string]string) model.PodaciStranice { func (h *Handler) popuniPodaciStranice(r *http.Request, podesavanja map[string]string) model.PodaciStranice {
// podrazumevana tema je tamna; korisnik može imati svoju lokalnu temu // 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.CsrfToken = middleware.CsrfToken(r.Context())
ps.AssetV = h.AssetV
ps.Flash = middleware.GetFlash(r, h.DB) ps.Flash = middleware.GetFlash(r, h.DB)
// logika pozadine: // logika pozadine:
+5 -7
View File
@@ -9,7 +9,6 @@ import (
"time" "time"
"ntech/internal/db/sqlite" "ntech/internal/db/sqlite"
"ntech/internal/middleware"
"ntech/internal/model" "ntech/internal/model"
) )
@@ -74,9 +73,7 @@ type TopKlijent struct {
// Izvestaji renderuje stranicu sa izveštajima // Izvestaji renderuje stranicu sa izveštajima
func (h *Handler) Izvestaji(w http.ResponseWriter, r *http.Request) { func (h *Handler) Izvestaji(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context()) if _, ok := h.zahtevajDozvolu(w, r, "izvestaj.pregled"); !ok {
if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "izvestaj.pregled") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
return return
} }
ctx := r.Context() 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) --- // --- stari otvoreni nalozi (>14 dana bez završetka) ---
stariRed, err := h.DB.QueryContext(ctx, ` stariRed, err := h.DB.QueryContext(ctx, `
SELECT sn.id, sn.broj_naloga, sn.uredjaj, sn.status, sn.datum_prijema, 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 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 WHERE sn.datum_zavrsetka IS NULL
AND substr(sn.datum_prijema, 1, 10) <= date('now', '-14 days') AND substr(sn.datum_prijema, 1, 10) <= date('now', '-14 days')
ORDER BY sn.datum_prijema ASC`) 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 --- // --- top 10 klijenata po ukupnoj vrednosti ---
klijRed, err := h.DB.QueryContext(ctx, ` klijRed, err := h.DB.QueryContext(ctx, `
SELECT 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.ukupno_prodaja, 0) + COALESCE(s.ukupno_servis, 0) AS ukupno_vrednost,
COALESCE(p.broj_prodaja, 0) + COALESCE(s.broj_servisa, 0) AS broj_naloga COALESCE(p.broj_prodaja, 0) + COALESCE(s.broj_servisa, 0) AS broj_naloga
FROM klijenti k FROM klijenti k
LEFT JOIN klijent_prikaz kp ON kp.id = k.id
LEFT JOIN ( LEFT JOIN (
SELECT klijent_id, SUM(ukupno) AS ukupno_prodaja, COUNT(*) AS broj_prodaja SELECT klijent_id, SUM(ukupno) AS ukupno_prodaja, COUNT(*) AS broj_prodaja
FROM prodajni_nalozi GROUP BY klijent_id FROM prodajni_nalozi GROUP BY klijent_id
+2 -7
View File
@@ -5,7 +5,6 @@ import (
"strconv" "strconv"
"ntech/internal/db/sqlite" "ntech/internal/db/sqlite"
"ntech/internal/middleware"
"ntech/internal/model" "ntech/internal/model"
"github.com/go-chi/chi/v5" "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 // DodajKategoriju prima POST i čuva novu kategoriju
func (h *Handler) DodajKategoriju(w http.ResponseWriter, r *http.Request) { func (h *Handler) DodajKategoriju(w http.ResponseWriter, r *http.Request) {
kor := middleware.KorisnikIzKonteksta(r.Context()) if _, ok := h.zahtevajDozvolu(w, r, "kategorija.dodaj"); !ok {
if kor == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), kor.Uloga, "kategorija.dodaj") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
return return
} }
if err := r.ParseForm(); err != nil { 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 // ObrisiKategoriju briše kategoriju po ID-u
func (h *Handler) ObrisiKategoriju(w http.ResponseWriter, r *http.Request) { func (h *Handler) ObrisiKategoriju(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context()) if _, ok := h.zahtevajDozvolu(w, r, "kategorija.obrisi"); !ok {
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "kategorija.obrisi") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
return return
} }
idStr := chi.URLParam(r, "id") idStr := chi.URLParam(r, "id")
+3 -10
View File
@@ -6,7 +6,6 @@ import (
"strings" "strings"
"ntech/internal/db/sqlite" "ntech/internal/db/sqlite"
"ntech/internal/middleware"
"ntech/internal/model" "ntech/internal/model"
"github.com/go-chi/chi/v5" "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 // SacuvajKlijenta prima POST formu i upisuje novog klijenta u bazu
func (h *Handler) SacuvajKlijenta(w http.ResponseWriter, r *http.Request) { func (h *Handler) SacuvajKlijenta(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context()) if _, ok := h.zahtevajDozvolu(w, r, "klijent.dodaj"); !ok {
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "klijent.dodaj") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
return return
} }
if err := r.ParseForm(); err != nil { 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 // SacuvajIzmenuKlijenta prima POST formu i ažurira postojećeg klijenta u bazi
func (h *Handler) SacuvajIzmenuKlijenta(w http.ResponseWriter, r *http.Request) { func (h *Handler) SacuvajIzmenuKlijenta(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context()) if _, ok := h.zahtevajDozvolu(w, r, "klijent.izmeni"); !ok {
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "klijent.izmeni") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
return return
} }
id, err := parseID(chi.URLParam(r, "id")) 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 // ObrisiKlijenta prima POST zahtev i briše klijenta po ID-u
func (h *Handler) ObrisiKlijenta(w http.ResponseWriter, r *http.Request) { func (h *Handler) ObrisiKlijenta(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context()) if _, ok := h.zahtevajDozvolu(w, r, "klijent.obrisi"); !ok {
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "klijent.obrisi") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
return return
} }
id, err := parseID(chi.URLParam(r, "id")) id, err := parseID(chi.URLParam(r, "id"))
+2 -7
View File
@@ -6,7 +6,6 @@ import (
"ntech/internal/db" "ntech/internal/db"
"ntech/internal/db/sqlite" "ntech/internal/db/sqlite"
"ntech/internal/middleware"
"ntech/internal/model" "ntech/internal/model"
"github.com/go-chi/chi/v5" "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). // PremestiArtikal menja kategoriju artikla (premeštanje u drugu kategoriju).
// Prazno polje kategorija_id znači premeštanje u "bez kategorije". // Prazno polje kategorija_id znači premeštanje u "bez kategorije".
func (h *Handler) PremestiArtikal(w http.ResponseWriter, r *http.Request) { func (h *Handler) PremestiArtikal(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context()) if _, ok := h.zahtevajDozvolu(w, r, "artikal.premesti"); !ok {
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "artikal.premesti") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
return return
} }
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) 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 // ObrisiArtikal briše artikal po ID-u
func (h *Handler) ObrisiArtikal(w http.ResponseWriter, r *http.Request) { func (h *Handler) ObrisiArtikal(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context()) if _, ok := h.zahtevajDozvolu(w, r, "artikal.obrisi"); !ok {
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "artikal.obrisi") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
return return
} }
idStr := chi.URLParam(r, "id") idStr := chi.URLParam(r, "id")
+2 -7
View File
@@ -9,7 +9,6 @@ import (
"ntech/internal/db" "ntech/internal/db"
"ntech/internal/db/sqlite" "ntech/internal/db/sqlite"
"ntech/internal/middleware"
"ntech/internal/model" "ntech/internal/model"
"github.com/go-chi/chi/v5" "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 // SacuvajNabavku prima POST formu, parsira stavke i upisuje nabavku u bazu
func (h *Handler) SacuvajNabavku(w http.ResponseWriter, r *http.Request) { func (h *Handler) SacuvajNabavku(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context()) if _, ok := h.zahtevajDozvolu(w, r, "nabavka.dodaj"); !ok {
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "nabavka.dodaj") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
return return
} }
if err := r.ParseForm(); err != nil { 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 // ObrisiNabavku prima POST zahtev i briše nabavku po ID-u
func (h *Handler) ObrisiNabavku(w http.ResponseWriter, r *http.Request) { func (h *Handler) ObrisiNabavku(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context()) if _, ok := h.zahtevajDozvolu(w, r, "nabavka.obrisi"); !ok {
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "nabavka.obrisi") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
return return
} }
id, err := parseID(chi.URLParam(r, "id")) 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 // Podesavanja renderuje stranicu podešavanja
func (h *Handler) Podesavanja(w http.ResponseWriter, r *http.Request) { func (h *Handler) Podesavanja(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context()) if _, ok := h.zahtevajDozvolu(w, r, "podesavanja.pregled"); !ok {
if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "podesavanja.pregled") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
return return
} }
podesavanja, err := ntechsqlite.DohvatiSvaPodesavanja(r.Context(), h.DB) 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 // VratiBackup zamenjuje trenutnu bazu sa izabranim backup fajlom
func (h *Handler) VratiBackup(w http.ResponseWriter, r *http.Request) { func (h *Handler) VratiBackup(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context()) if _, ok := h.zahtevajDozvolu(w, r, "backup.pokreni"); !ok {
if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "backup.pokreni") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
return return
} }
if err := r.ParseForm(); err != nil { 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 // SacuvajPodesavanja prima POST i čuva podešavanja u bazu
func (h *Handler) SacuvajPodesavanja(w http.ResponseWriter, r *http.Request) { func (h *Handler) SacuvajPodesavanja(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context()) if _, ok := h.zahtevajDozvolu(w, r, "podesavanja.izmeni"); !ok {
if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "podesavanja.izmeni") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
return return
} }
if err := r.ParseForm(); err != nil { 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 // BackupBaze kreira konzistentnu kopiju baze i šalje je kao attachment
func (h *Handler) BackupBaze(w http.ResponseWriter, r *http.Request) { func (h *Handler) BackupBaze(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context()) if _, ok := h.zahtevajDozvolu(w, r, "backup.pregled"); !ok {
if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "backup.pregled") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
return return
} }
privremeni := fmt.Sprintf("%s/ntech_backup_%s.db", os.TempDir(), time.Now().Format("20060102_150405")) 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/ // OtpremiLogo prima multipart upload slike loga i čuva je u web/static/uploads/
func (h *Handler) OtpremiLogo(w http.ResponseWriter, r *http.Request) { func (h *Handler) OtpremiLogo(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context()) if _, ok := h.zahtevajDozvolu(w, r, "podesavanja.izmeni"); !ok {
if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "podesavanja.izmeni") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
return return
} }
// ograničavamo telo zahteva na 2MB + malo za zaglavlja forme // 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 // OtpremiLoginPozadinu prima multipart upload slike i čuva je kao pozadinsku sliku login stranice
func (h *Handler) OtpremiLoginPozadinu(w http.ResponseWriter, r *http.Request) { func (h *Handler) OtpremiLoginPozadinu(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context()) if _, ok := h.zahtevajDozvolu(w, r, "podesavanja.login_pozadina"); !ok {
if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "podesavanja.login_pozadina") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
return 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 // UkloniLoginPozadinu briše pozadinsku sliku login stranice sa diska i iz podešavanja
func (h *Handler) UkloniLoginPozadinu(w http.ResponseWriter, r *http.Request) { func (h *Handler) UkloniLoginPozadinu(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context()) if _, ok := h.zahtevajDozvolu(w, r, "podesavanja.login_pozadina"); !ok {
if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "podesavanja.login_pozadina") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
return 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 // SacuvajLoginPozadinaStilove čuva vrednosti zamućenja i prozirnosti pozadine login stranice
func (h *Handler) SacuvajLoginPozadinaStilove(w http.ResponseWriter, r *http.Request) { func (h *Handler) SacuvajLoginPozadinaStilove(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context()) if _, ok := h.zahtevajDozvolu(w, r, "podesavanja.login_pozadina"); !ok {
if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "podesavanja.login_pozadina") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
return return
} }
if err := r.ParseForm(); err != nil { 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) // PodesavanjaOpste renderuje stranicu sa opštim podešavanjima (firma i logo)
func (h *Handler) PodesavanjaOpste(w http.ResponseWriter, r *http.Request) { func (h *Handler) PodesavanjaOpste(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context()) if _, ok := h.zahtevajDozvolu(w, r, "podesavanja.pregled"); !ok {
if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "podesavanja.pregled") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
return return
} }
podaci, err := h.napuniPodaciPodesavanja(r, "Podešavanja — Opšte") 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) // PodesavanjaIzgled renderuje stranicu sa podešavanjima izgleda (pozadine i tema)
func (h *Handler) PodesavanjaIzgled(w http.ResponseWriter, r *http.Request) { func (h *Handler) PodesavanjaIzgled(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context()) if _, ok := h.zahtevajDozvolu(w, r, "podesavanja.pregled"); !ok {
if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "podesavanja.pregled") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
return return
} }
podaci, err := h.napuniPodaciPodesavanja(r, "Podešavanja — Izgled") 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) // PodesavanjaSistem renderuje stranicu sa sistemskim podešavanjima (backup)
func (h *Handler) PodesavanjaSistem(w http.ResponseWriter, r *http.Request) { func (h *Handler) PodesavanjaSistem(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context()) if _, ok := h.zahtevajDozvolu(w, r, "podesavanja.pregled"); !ok {
if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "podesavanja.pregled") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
return return
} }
podaci, err := h.napuniPodaciPodesavanja(r, "Podešavanja — Sistem") 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 // SacuvajProdaju prima POST formu, parsira stavke i upisuje prodajni nalog u bazu
func (h *Handler) SacuvajProdaju(w http.ResponseWriter, r *http.Request) { func (h *Handler) SacuvajProdaju(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context()) k, ok := h.zahtevajDozvolu(w, r, "prodaja.dodaj")
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "prodaja.dodaj") { if !ok {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
return return
} }
if err := r.ParseForm(); err != nil { 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 // ObrisiProdaju prima POST zahtev, vraća stanje na magacin i briše nalog
func (h *Handler) ObrisiProdaju(w http.ResponseWriter, r *http.Request) { func (h *Handler) ObrisiProdaju(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context()) if _, ok := h.zahtevajDozvolu(w, r, "prodaja.obrisi"); !ok {
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "prodaja.obrisi") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
return return
} }
id, err := parseID(chi.URLParam(r, "id")) 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 // SacuvajNalog prima POST formu i upisuje novi servisni nalog u bazu
func (h *Handler) SacuvajNalog(w http.ResponseWriter, r *http.Request) { func (h *Handler) SacuvajNalog(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context()) if _, ok := h.zahtevajDozvolu(w, r, "servis.dodaj"); !ok {
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "servis.dodaj") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
return return
} }
if err := r.ParseForm(); err != nil { 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 // SacuvajIzmenaNaloga prima POST formu i ažurira postojeći servisni nalog
func (h *Handler) SacuvajIzmenaNaloga(w http.ResponseWriter, r *http.Request) { func (h *Handler) SacuvajIzmenaNaloga(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context()) if _, ok := h.zahtevajDozvolu(w, r, "servis.izmeni"); !ok {
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "servis.izmeni") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
return return
} }
id, err := parseID(chi.URLParam(r, "id")) 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 // ObrisiNalog prima POST zahtev i briše servisni nalog po ID-u
func (h *Handler) ObrisiNalog(w http.ResponseWriter, r *http.Request) { func (h *Handler) ObrisiNalog(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context()) if _, ok := h.zahtevajDozvolu(w, r, "servis.obrisi"); !ok {
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "servis.obrisi") {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
return return
} }
id, err := parseID(chi.URLParam(r, "id")) 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 // DodajDeloNalogu prima POST formu i dodaje artikal kao deo servisnog naloga
func (h *Handler) DodajDeloNalogu(w http.ResponseWriter, r *http.Request) { func (h *Handler) DodajDeloNalogu(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context()) k, ok := h.zahtevajDozvolu(w, r, "servis.izmeni")
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "servis.izmeni") { if !ok {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
return 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 // ObrisiDeloNaloga prima POST zahtev i uklanja deo iz servisnog naloga
func (h *Handler) ObrisiDeloNaloga(w http.ResponseWriter, r *http.Request) { func (h *Handler) ObrisiDeloNaloga(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context()) k, ok := h.zahtevajDozvolu(w, r, "servis.izmeni")
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "servis.izmeni") { if !ok {
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
return return
} }
+1
View File
@@ -42,6 +42,7 @@ type PodaciStranice struct {
KorisnikIme string // korisničko ime prijavljenog korisnika KorisnikIme string // korisničko ime prijavljenog korisnika
KorisnikUloga string // uloga: "superadmin", "admin", "radnik" KorisnikUloga string // uloga: "superadmin", "admin", "radnik"
CsrfToken string // CSRF zaštitni token za forme 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 Dozvole map[string]bool // mapa akcija → dozvoljeno/nije
Flash *FlashPoruka // jednokratna poruka nakon redirecta Flash *FlashPoruka // jednokratna poruka nakon redirecta
// app pozadina — popunjava se iz podešavanja za sve stranice // 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"], input[type="date"],
select { select {
height: 38px; height: 38px;
width: 100%;
padding: 8px 12px; padding: 8px 12px;
box-sizing: border-box; box-sizing: border-box;
line-height: 1.5; line-height: 1.5;
@@ -616,10 +617,12 @@ select {
textarea { textarea {
height: auto; height: auto;
width: 100%;
min-height: 80px; min-height: 80px;
padding: 8px 12px; padding: 8px 12px;
box-sizing: border-box; box-sizing: border-box;
line-height: 1.5; line-height: 1.5;
resize: vertical;
background: var(--kartica) !important; background: var(--kartica) !important;
color: var(--tekst-glavni) !important; color: var(--tekst-glavni) !important;
border: 0.5px solid var(--ivica) !important; border: 0.5px solid var(--ivica) !important;
@@ -666,6 +669,90 @@ select {
color: var(--greska); 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 */ /* overlay za mobilni — tamni sloj iza sidebara */
.sidebar-overlay { .sidebar-overlay {
display: none; display: none;
+23 -24
View File
@@ -20,7 +20,7 @@
{{end}} {{end}}
{{define "sadrzaj"}} {{define "sadrzaj"}}
<div style="width:100%;"> <div>
<a href="/klijenti" class="nazad-link"> <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> <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}}"> <form method="POST" action="{{if .Izmena}}/klijenti/izmeni/{{.Klijent.ID}}{{else}}/klijenti/novi{{end}}">
<input type="hidden" name="csrf_token" value="{{.CsrfToken}}"> <input type="hidden" name="csrf_token" value="{{.CsrfToken}}">
<div style="display:flex;flex-direction:column;gap:18px;"> <div class="forma-kolona">
<!-- tip klijenta --> <!-- tip klijenta -->
<div> <div>
@@ -64,42 +64,42 @@
<!-- sekcija fizičko lice --> <!-- sekcija fizičko lice -->
<div id="sec-fizicko" style="display:flex;flex-direction:column;gap:12px;"> <div id="sec-fizicko" style="display:flex;flex-direction:column;gap:12px;">
<div class="sekcija-naslov">Ime i prezime</div> <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> <div>
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:6px;"> <label class="polje-labela">
Ime <span id="ime-oznaka" style="color:#dc2626;">*</span> Ime <span id="ime-oznaka" class="obavezno">*</span>
</label> </label>
<input type="text" name="ime" value="{{.Klijent.Ime}}" <input type="text" name="ime" value="{{.Klijent.Ime}}"
placeholder="npr. Marko" style="width:100%;"> placeholder="npr. Marko">
</div> </div>
<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}}" <input type="text" name="prezime" value="{{.Klijent.Prezime}}"
placeholder="npr. Petrović" style="width:100%;"> placeholder="npr. Petrović">
</div> </div>
</div> </div>
<div style="max-width:260px;"> <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}}" <input type="text" name="jmbg" value="{{.Klijent.JMBG}}"
placeholder="13 cifara" maxlength="13" style="width:100%;"> placeholder="13 cifara" maxlength="13">
</div> </div>
</div> </div>
<!-- sekcija pravno lice --> <!-- sekcija pravno lice -->
<div id="sec-pravno" style="display:none;flex-direction:column;gap:12px;"> <div id="sec-pravno" style="display:none;flex-direction:column;gap:12px;">
<div class="sekcija-naslov">Firma</div> <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> <div>
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:6px;"> <label class="polje-labela">
Naziv firme <span style="color:#dc2626;">*</span> Naziv firme <span class="obavezno">*</span>
</label> </label>
<input type="text" name="naziv_firme" value="{{.Klijent.NazivFirme}}" <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>
<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}}" <input type="text" name="pib" value="{{.Klijent.PIB}}"
placeholder="npr. 123456789" style="width:100%;"> placeholder="npr. 123456789">
</div> </div>
</div> </div>
</div> </div>
@@ -107,26 +107,25 @@
<!-- kontakt --> <!-- kontakt -->
<div> <div>
<div class="sekcija-naslov">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> <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}}" <input type="text" name="telefon" value="{{.Klijent.Telefon}}"
placeholder="npr. 060 123 4567" style="width:100%;"> placeholder="npr. 060 123 4567">
</div> </div>
<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}}" <input type="text" name="email" value="{{.Klijent.Email}}"
placeholder="npr. marko@example.com" style="width:100%;"> placeholder="npr. marko@example.com">
</div> </div>
</div> </div>
</div> </div>
<!-- napomena --> <!-- napomena -->
<div> <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" <textarea name="napomena" rows="3"
placeholder="Interna napomena o klijentu..." placeholder="Interna napomena o klijentu...">{{.Klijent.Napomena}}</textarea>
style="width:100%;resize:vertical;">{{.Klijent.Napomena}}</textarea>
</div> </div>
<!-- dugmad --> <!-- dugmad -->
+16 -16
View File
@@ -47,22 +47,22 @@
</div> </div>
<!-- desktop tabela --> <!-- desktop tabela -->
<div class="kartica klijenti-tabela animiraj" style="padding:0;overflow:hidden;"> <div class="kartica klijenti-tabela kartica-tabela animiraj">
<div style="overflow-x:auto;"> <div class="tabela-skrol">
<table style="width:100%;border-collapse:collapse;"> <table class="tabela">
<thead> <thead>
<tr style="border-bottom:0.5px solid var(--ivica);"> <tr>
<th style="padding:12px 16px;text-align:left;font-size:12px;font-weight:500;color:var(--tekst-jak);">Ime / Naziv firme</th> <th>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>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>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>Datum unosa</th>
<th style="padding:12px 16px;text-align:center;font-size:12px;font-weight:500;color:var(--tekst-jak);">Akcije</th> <th class="centar">Akcije</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{{range .Klijenti}} {{range .Klijenti}}
<tr class="animiraj red-tabele"> <tr class="animiraj red-tabele">
<td style="padding:12px 16px;"> <td>
{{if .NazivFirme}} {{if .NazivFirme}}
<div style="font-size:14px;font-weight:500;color:var(--tekst-glavni);">{{.NazivFirme}}</div> <div style="font-size:14px;font-weight:500;color:var(--tekst-glavni);">{{.NazivFirme}}</div>
{{if or .Ime .Prezime}} {{if or .Ime .Prezime}}
@@ -72,16 +72,16 @@
<div style="font-size:14px;font-weight:500;color:var(--tekst-glavni);">{{.Ime}} {{.Prezime}}</div> <div style="font-size:14px;font-weight:500;color:var(--tekst-glavni);">{{.Ime}} {{.Prezime}}</div>
{{end}} {{end}}
</td> </td>
<td style="padding:12px 16px;font-size:13px;color:var(--tekst-sporedni);"> <td class="pomocni-tekst">
{{if .Telefon}}{{.Telefon}}{{else}}—{{end}} {{if .Telefon}}{{.Telefon}}{{else}}—{{end}}
</td> </td>
<td style="padding:12px 16px;font-size:13px;color:var(--tekst-sporedni);"> <td class="pomocni-tekst">
{{if .Email}}{{.Email}}{{else}}—{{end}} {{if .Email}}{{.Email}}{{else}}—{{end}}
</td> </td>
<td style="padding:12px 16px;font-size:13px;color:var(--tekst-sporedni);"> <td class="pomocni-tekst">
{{.DatumUnosa.Format "02.01.2006."}} {{.DatumUnosa.Format "02.01.2006."}}
</td> </td>
<td style="padding:12px 16px;text-align:center;"> <td class="centar">
<div style="display:flex;align-items:center;justify-content:center;gap:8px;"> <div style="display:flex;align-items:center;justify-content:center;gap:8px;">
<a href="/klijenti/izmeni/{{.ID}}" class="btn-primarno-malo"> <a href="/klijenti/izmeni/{{.ID}}" class="btn-primarno-malo">
Izmeni Izmeni
@@ -99,7 +99,7 @@
</tr> </tr>
{{else}} {{else}}
<tr> <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> Nema klijenata. <a href="/klijenti/novi" style="color:var(--sb-akcent);">Dodaj prvog klijenta.</a>
</td> </td>
</tr> </tr>
@@ -155,7 +155,7 @@
</div> </div>
</div> </div>
{{else}} {{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> Nema klijenata. <a href="/klijenti/novi" style="color:var(--sb-akcent);">Dodaj prvog klijenta.</a>
</div> </div>
{{end}} {{end}}
+3 -3
View File
@@ -17,10 +17,10 @@
{{if .AppPozadina}}<link rel="preload" as="image" href="{{.AppPozadina}}">{{end}} {{if .AppPozadina}}<link rel="preload" as="image" href="{{.AppPozadina}}">{{end}}
<!-- tema — učitava se prva --> <!-- 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 --> <!-- glavni stilovi -->
<link rel="stylesheet" href="/static/css/main.css" /> <link rel="stylesheet" href="/static/css/main.css?v={{.AssetV}}" />
{{block "dodatni-css" .}}{{end}} {{block "dodatni-css" .}}{{end}}
@@ -108,7 +108,7 @@
</div> </div>
<!-- alpine.js komponente (mora biti pre Alpine-a) --> <!-- 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) --> <!-- alpine.js CSP build (lokalno, bez unsafe-eval) -->
<script src="/static/js/alpine.csp.min.js" defer></script> <script src="/static/js/alpine.csp.min.js" defer></script>