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:
+17
-22
@@ -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
@@ -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)
|
||||||
})))
|
})))
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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{}
|
||||||
|
|||||||
@@ -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{}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 -->
|
||||||
|
|||||||
@@ -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}}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user