Bezbednost: ispravke kontrole pristupa u admin i handler sloju
This commit is contained in:
+17
-2
@@ -76,6 +76,11 @@ func main() {
|
||||
}
|
||||
log.Println("Migracije uspešno izvršene")
|
||||
|
||||
// popuni tabelu dozvola podrazumevanim vrednostima ako je prazna
|
||||
if err := sqlite.InicijalizujDozvole(context.Background(), db, ntechmw.ImaDozvolu, ntechmw.SveAkcije()); err != nil {
|
||||
log.Printf("Upozorenje: greška pri inicijalizaciji dozvola: %v", err)
|
||||
}
|
||||
|
||||
napraviStartupBackup(putanjaBaze)
|
||||
|
||||
// periodično brisanje isteklih sesija i starih pokušaja prijave
|
||||
@@ -190,15 +195,25 @@ func main() {
|
||||
r.Post("/podsetnici/zavrseno/{id}", h.OznaciPodsetnik)
|
||||
r.Post("/podsetnici/obrisi/{id}", h.ObrisiPodsetnik)
|
||||
|
||||
// rute dostupne samo superadminu
|
||||
// rute dostupne adminu i superadminu (superadmin vidi sve, admin ne vidi superadmin naloge)
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(ntechmw.RequireSuperAdmin)
|
||||
r.Use(ntechmw.RequireAdmin)
|
||||
r.Get("/admin/korisnici", h.AdminKorisnici)
|
||||
r.Get("/admin/korisnici/{id}/istorija", h.AdminLoginIstorija)
|
||||
r.Post("/admin/korisnici/novi", h.AdminSacuvajKorisnika)
|
||||
r.Post("/admin/korisnici/{id}/aktivan", h.AdminToggleAktivan)
|
||||
r.Post("/admin/korisnici/{id}/uloga", h.AdminPromeniUlogu)
|
||||
r.Post("/admin/korisnici/{id}/obrisi", h.AdminObrisiKorisnika)
|
||||
// dozvole — pregled i izmena matrice dostupni adminu, promena uloge samo superadminu
|
||||
r.Get("/admin/dozvole", h.AdminDozvole)
|
||||
r.Post("/admin/dozvole/sacuvaj", h.AdminDozvoleSacuvaj)
|
||||
r.Post("/admin/dozvole/reset", h.AdminDozvoleReset)
|
||||
})
|
||||
|
||||
// rute dostupne samo superadminu
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(ntechmw.RequireSuperAdmin)
|
||||
r.Post("/admin/dozvole/uloga/{id}", h.AdminDozvolePromeniUlogu)
|
||||
})
|
||||
r.Get("/admin/profil", h.AdminProfil)
|
||||
r.Post("/admin/profil/lozinka", h.AdminPromeniLozinku)
|
||||
|
||||
@@ -118,6 +118,14 @@ type LoginIstorijsaRepository interface {
|
||||
ListaZaKorisnika(ctx context.Context, korisnikID int64, limit int) ([]*model.LoginPokusaj, error)
|
||||
}
|
||||
|
||||
// DozvoleRepository definiše operacije nad dozvolama po ulogama
|
||||
type DozvoleRepository interface {
|
||||
ImaDozvolu(ctx context.Context, uloga, akcija string) bool
|
||||
SveDozvole(ctx context.Context, uloga string) map[string]bool
|
||||
Sacuvaj(ctx context.Context, uloga, akcija string, dozvoljeno bool) error
|
||||
Reset(ctx context.Context) error
|
||||
}
|
||||
|
||||
// PodsetnikRepository definiše operacije nad podsetnicima
|
||||
type PodsetnikRepository interface {
|
||||
Lista(ctx context.Context, filter PodsetnikFilter) ([]model.Podsetnik, error)
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type sqliteDozvoleRepo struct {
|
||||
db *sql.DB
|
||||
defaultFn func(uloga, akcija string) bool // podrazumevane vrednosti iz middleware/dozvole.go
|
||||
sveAkcije []string
|
||||
}
|
||||
|
||||
// NoviDozvoleRepo kreira SQLite implementaciju DozvoleRepository
|
||||
func NoviDozvoleRepo(db *sql.DB, defaultFn func(uloga, akcija string) bool, sveAkcije []string) *sqliteDozvoleRepo {
|
||||
return &sqliteDozvoleRepo{db: db, defaultFn: defaultFn, sveAkcije: sveAkcije}
|
||||
}
|
||||
|
||||
// InicijalizujDozvole popunjava tabelu podrazumevanim vrednostima ako je prazna
|
||||
func InicijalizujDozvole(ctx context.Context, db *sql.DB, defaultFn func(uloga, akcija string) bool, sveAkcije []string) error {
|
||||
var br int
|
||||
_ = db.QueryRowContext(ctx, `SELECT COUNT(*) FROM dozvole`).Scan(&br)
|
||||
if br > 0 {
|
||||
return nil
|
||||
}
|
||||
return popuniPodrazumevano(ctx, db, defaultFn, sveAkcije)
|
||||
}
|
||||
|
||||
// popuniPodrazumevano upisuje sve podrazumevane dozvole u bazu
|
||||
func popuniPodrazumevano(ctx context.Context, db *sql.DB, defaultFn func(uloga, akcija string) bool, sveAkcije []string) error {
|
||||
for _, uloga := range []string{"radnik", "admin", "superadmin"} {
|
||||
for _, akcija := range sveAkcije {
|
||||
dozvoljeno := 0
|
||||
if defaultFn(uloga, akcija) {
|
||||
dozvoljeno = 1
|
||||
}
|
||||
if _, err := db.ExecContext(ctx,
|
||||
`INSERT OR IGNORE INTO dozvole (uloga, akcija, dozvoljeno) VALUES (?, ?, ?)`,
|
||||
uloga, akcija, dozvoljeno); err != nil {
|
||||
return fmt.Errorf("ntech: dozvole.Inicijalizuj: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *sqliteDozvoleRepo) ImaDozvolu(ctx context.Context, uloga, akcija string) bool {
|
||||
var dozvoljeno int
|
||||
err := r.db.QueryRowContext(ctx,
|
||||
`SELECT dozvoljeno FROM dozvole WHERE uloga = ? AND akcija = ?`, uloga, akcija).Scan(&dozvoljeno)
|
||||
if err != nil {
|
||||
// fallback na podrazumevano ako red nije pronađen
|
||||
return r.defaultFn(uloga, akcija)
|
||||
}
|
||||
return dozvoljeno == 1
|
||||
}
|
||||
|
||||
func (r *sqliteDozvoleRepo) SveDozvole(ctx context.Context, uloga string) map[string]bool {
|
||||
rows, err := r.db.QueryContext(ctx,
|
||||
`SELECT akcija, dozvoljeno FROM dozvole WHERE uloga = ?`, uloga)
|
||||
if err != nil {
|
||||
// fallback na podrazumevano
|
||||
m := make(map[string]bool, len(r.sveAkcije))
|
||||
for _, a := range r.sveAkcije {
|
||||
m[a] = r.defaultFn(uloga, a)
|
||||
}
|
||||
return m
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
m := make(map[string]bool, len(r.sveAkcije))
|
||||
for rows.Next() {
|
||||
var akcija string
|
||||
var dozvoljeno int
|
||||
if err := rows.Scan(&akcija, &dozvoljeno); err == nil {
|
||||
m[akcija] = dozvoljeno == 1
|
||||
}
|
||||
}
|
||||
// popuni eventualno nedostajuće akcije podrazumevanim vrednostima
|
||||
for _, a := range r.sveAkcije {
|
||||
if _, ok := m[a]; !ok {
|
||||
m[a] = r.defaultFn(uloga, a)
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (r *sqliteDozvoleRepo) Sacuvaj(ctx context.Context, uloga, akcija string, dozvoljeno bool) error {
|
||||
d := 0
|
||||
if dozvoljeno {
|
||||
d = 1
|
||||
}
|
||||
_, err := r.db.ExecContext(ctx,
|
||||
`INSERT INTO dozvole (uloga, akcija, dozvoljeno) VALUES (?, ?, ?)
|
||||
ON CONFLICT (uloga, akcija) DO UPDATE SET dozvoljeno = excluded.dozvoljeno`,
|
||||
uloga, akcija, d)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntech: dozvole.Sacuvaj: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *sqliteDozvoleRepo) Reset(ctx context.Context) error {
|
||||
if _, err := r.db.ExecContext(ctx, `DELETE FROM dozvole`); err != nil {
|
||||
return fmt.Errorf("ntech: dozvole.Reset: %w", err)
|
||||
}
|
||||
return popuniPodrazumevano(ctx, r.db, r.defaultFn, r.sveAkcije)
|
||||
}
|
||||
+156
-5
@@ -3,7 +3,6 @@ package handler
|
||||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"ntech/internal/auth"
|
||||
"ntech/internal/db/sqlite"
|
||||
@@ -51,6 +50,17 @@ func (h *Handler) AdminKorisnici(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// admin ne sme da vidi superadmin naloge — filtriramo ih iz liste
|
||||
if k.Uloga != "superadmin" {
|
||||
filtrirano := lista[:0]
|
||||
for _, kor := range lista {
|
||||
if kor.Uloga != "superadmin" {
|
||||
filtrirano = append(filtrirano, kor)
|
||||
}
|
||||
}
|
||||
lista = filtrirano
|
||||
}
|
||||
|
||||
ps := h.popuniPodaciStranice(r, podesavanja)
|
||||
ps.Stranica = "admin"
|
||||
ps.NaslovStranice = "Korisnici"
|
||||
@@ -129,6 +139,12 @@ func (h *Handler) AdminToggleAktivan(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// admin ne sme da menja status drugog admina ni superadmina
|
||||
if korisnik.Uloga == "superadmin" || (korisnik.Uloga == "admin" && k.Uloga != "superadmin") {
|
||||
http.Redirect(w, r, "/admin/korisnici?greska=3", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.KorisniciRepo.AzurirajAktivan(r.Context(), id, !korisnik.Aktivan); err != nil {
|
||||
http.Redirect(w, r, "/admin/korisnici?greska=2", http.StatusSeeOther)
|
||||
return
|
||||
@@ -413,6 +429,12 @@ func (h *Handler) AdminLoginIstorija(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// admin ne sme da vidi istoriju superadmin naloga
|
||||
if korisnik.Uloga == "superadmin" && k.Uloga != "superadmin" {
|
||||
http.Error(w, "Pristup odbijen", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
istorija, err := h.LoginIstorijsaRepo.ListaZaKorisnika(r.Context(), id, 50)
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri učitavanju istorije", http.StatusInternalServerError)
|
||||
@@ -431,8 +453,137 @@ func (h *Handler) AdminLoginIstorija(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
// parseBoolForm čita boolean vrednost iz forme
|
||||
func parseBoolForm(s string) bool {
|
||||
b, _ := strconv.ParseBool(s)
|
||||
return b
|
||||
type podaciAdminDozvole struct {
|
||||
model.PodaciStranice
|
||||
Korisnici []model.Korisnik
|
||||
TrenutniID int64
|
||||
DozvoleRadnik map[string]bool
|
||||
DozvoleAdmin map[string]bool
|
||||
DozvoleSuperadmin map[string]bool
|
||||
Greska string
|
||||
Sacuvano bool
|
||||
}
|
||||
|
||||
// AdminDozvole prikazuje stranicu za upravljanje ulogama i pregled dozvola
|
||||
func (h *Handler) AdminDozvole(w http.ResponseWriter, r *http.Request) {
|
||||
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||
if !middleware.JeAdmin(k) {
|
||||
http.Error(w, "Pristup odbijen", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||
lista, err := h.KorisniciRepo.Lista(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri učitavanju korisnika", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
ps := h.popuniPodaciStranice(r, podesavanja)
|
||||
ps.Stranica = "dozvole"
|
||||
ps.NaslovStranice = "Dozvole"
|
||||
|
||||
h.renderujTemplate(w, "admin_dozvole", podaciAdminDozvole{
|
||||
PodaciStranice: ps,
|
||||
Korisnici: lista,
|
||||
TrenutniID: k.ID,
|
||||
DozvoleRadnik: h.DozvoleRepo.SveDozvole(r.Context(), "radnik"),
|
||||
DozvoleAdmin: h.DozvoleRepo.SveDozvole(r.Context(), "admin"),
|
||||
DozvoleSuperadmin: h.DozvoleRepo.SveDozvole(r.Context(), "superadmin"),
|
||||
Greska: r.URL.Query().Get("greska"),
|
||||
Sacuvano: r.URL.Query().Get("sacuvano") == "1",
|
||||
})
|
||||
}
|
||||
|
||||
// AdminDozvolePromeniUlogu menja ulogu korisnika sa stranice dozvola
|
||||
func (h *Handler) AdminDozvolePromeniUlogu(w http.ResponseWriter, r *http.Request) {
|
||||
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||
if k == nil || k.Uloga != "superadmin" {
|
||||
http.Error(w, "Pristup odbijen", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
id, err := parseID(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/admin/dozvole?greska=1", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Redirect(w, r, "/admin/dozvole?greska=1", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
// superadmin ne može menjati svoju vlastitu ulogu
|
||||
if id == k.ID {
|
||||
http.Redirect(w, r, "/admin/dozvole?greska=3", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
uloga := r.FormValue("uloga")
|
||||
// superadmin uloga se ne može dodeliti kroz interfejs
|
||||
validneUloge := map[string]bool{"admin": true, "radnik": true}
|
||||
if !validneUloge[uloga] {
|
||||
http.Redirect(w, r, "/admin/dozvole?greska=1", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
ciljni, err := h.KorisniciRepo.DohvatiPoID(r.Context(), id)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/admin/dozvole?greska=2", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
// superadmin uloga se ne može menjati
|
||||
if ciljni.Uloga == "superadmin" {
|
||||
http.Redirect(w, r, "/admin/dozvole?greska=3", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.KorisniciRepo.AzurirajUlogu(r.Context(), id, uloga); err != nil {
|
||||
http.Redirect(w, r, "/admin/dozvole?greska=2", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/admin/dozvole?sacuvano=1", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// AdminDozvoleSacuvaj prima POST i čuva promene dozvola za radnik i admin uloge
|
||||
func (h *Handler) AdminDozvoleSacuvaj(w http.ResponseWriter, r *http.Request) {
|
||||
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||
if !middleware.JeAdmin(k) {
|
||||
http.Error(w, "Pristup odbijen", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Redirect(w, r, "/admin/dozvole?greska=1", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
// čuvamo dozvole samo za radnik i admin — superadmin uvek ima sve
|
||||
for _, uloga := range []string{"radnik", "admin"} {
|
||||
for _, akcija := range middleware.SveAkcije() {
|
||||
kljuc := uloga + "__" + akcija
|
||||
dozvoljeno := r.FormValue(kljuc) == "on"
|
||||
if err := h.DozvoleRepo.Sacuvaj(r.Context(), uloga, akcija, dozvoljeno); err != nil {
|
||||
http.Redirect(w, r, "/admin/dozvole?greska=2", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
http.Redirect(w, r, "/admin/dozvole?sacuvano=1", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// AdminDozvoleReset vraća sve dozvole na podrazumevane vrednosti — samo superadmin
|
||||
func (h *Handler) AdminDozvoleReset(w http.ResponseWriter, r *http.Request) {
|
||||
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||
if k == nil || k.Uloga != "superadmin" {
|
||||
http.Error(w, "Pristup odbijen", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if err := h.DozvoleRepo.Reset(r.Context()); err != nil {
|
||||
http.Redirect(w, r, "/admin/dozvole?greska=2", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/admin/dozvole?sacuvano=1", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,18 @@ import (
|
||||
func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// pročitaj i odmah obrišt flash poruku ako postoji
|
||||
var flashGreska string
|
||||
if kol, err := r.Cookie("ntech_flash_greska"); err == nil {
|
||||
flashGreska = kol.Value
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "ntech_flash_greska",
|
||||
Value: "",
|
||||
Path: "/",
|
||||
MaxAge: -1,
|
||||
})
|
||||
}
|
||||
|
||||
podesavanja, err := sqlite.DohvatiSvaPodesavanja(ctx, h.DB)
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
|
||||
@@ -126,17 +138,12 @@ func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
ps := h.popuniPodaciStranice(r, podesavanja)
|
||||
ps.Stranica = "dashboard"
|
||||
ps.NaslovStranice = "Dashboard"
|
||||
|
||||
podaci := model.PodaciDashboarda{
|
||||
PodaciStranice: model.PodaciStranice{
|
||||
Stranica: "dashboard",
|
||||
NaslovStranice: "Dashboard",
|
||||
Tema: podesavanja["tema"],
|
||||
NazivFirme: podesavanja["naziv_firme"],
|
||||
Podnazlov: podesavanja["podnazlov"],
|
||||
LogoTip: podesavanja["logo_tip"],
|
||||
LogoPutanja: podesavanja["logo_putanja"],
|
||||
Korisnik: "Admin",
|
||||
},
|
||||
PodaciStranice: ps,
|
||||
BrojArtikala: brojArtikala,
|
||||
AktivniServisi: aktivniServisi,
|
||||
PrihodOvogMeseca: prihodOvogMeseca,
|
||||
@@ -145,6 +152,7 @@ func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) {
|
||||
PoslednjiServisi: poslednjiServisi,
|
||||
KriticneZalihe: kriticneZalihe,
|
||||
PoslednjeProdaje: poslednjeProdaje,
|
||||
FlashGreska: flashGreska,
|
||||
}
|
||||
|
||||
h.renderujTemplate(w, "dashboard", podaci)
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"ntech/internal/db/sqlite"
|
||||
"ntech/internal/middleware"
|
||||
"ntech/internal/model"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
@@ -76,6 +77,11 @@ func (h *Handler) NoviDobavljac(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// SacuvajDobavljaca prima POST formu i upisuje novog dobavljača u bazu
|
||||
func (h *Handler) SacuvajDobavljaca(w http.ResponseWriter, r *http.Request) {
|
||||
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "dobavljac.dodaj") {
|
||||
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest)
|
||||
return
|
||||
@@ -136,6 +142,11 @@ func (h *Handler) IzmeniDobavljaca(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// SacuvajIzmeneDobavljaca prima POST formu i ažurira postojećeg dobavljača u bazi
|
||||
func (h *Handler) SacuvajIzmeneDobavljaca(w http.ResponseWriter, r *http.Request) {
|
||||
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "dobavljac.izmeni") {
|
||||
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
id, err := parseID(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
http.Error(w, "Neispravan ID dobavljača", http.StatusBadRequest)
|
||||
@@ -174,6 +185,11 @@ func (h *Handler) SacuvajIzmeneDobavljaca(w http.ResponseWriter, r *http.Request
|
||||
|
||||
// ObrisiDobavljaca prima POST zahtev i briše dobavljača po ID-u
|
||||
func (h *Handler) ObrisiDobavljaca(w http.ResponseWriter, r *http.Request) {
|
||||
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "dobavljac.obrisi") {
|
||||
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
id, err := parseID(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
http.Error(w, "Neispravan ID dobavljača", http.StatusBadRequest)
|
||||
|
||||
@@ -28,6 +28,7 @@ type Handler struct {
|
||||
PodsetniciFRepo db.PodsetnikRepository
|
||||
PokusajiRepo db.PokusajiPrijaveRepository
|
||||
LoginIstorijsaRepo db.LoginIstorijsaRepository
|
||||
DozvoleRepo db.DozvoleRepository
|
||||
Verzija string
|
||||
Templates map[string]*template.Template
|
||||
TemplatesFS fs.FS
|
||||
@@ -49,6 +50,7 @@ func Novi(baza *sql.DB) *Handler {
|
||||
PodsetniciFRepo: sqlite.NoviPodsetnikRepo(baza),
|
||||
PokusajiRepo: sqlite.NoviPokusajiPrijaveRepo(baza),
|
||||
LoginIstorijsaRepo: sqlite.NoviLoginIstorijsaRepo(baza),
|
||||
DozvoleRepo: sqlite.NoviDozvoleRepo(baza, middleware.ImaDozvolu, middleware.SveAkcije()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +69,7 @@ func (h *Handler) reinicijalzijRepozitorijume(novaDB *sql.DB) {
|
||||
h.PodsetniciFRepo = sqlite.NoviPodsetnikRepo(novaDB)
|
||||
h.PokusajiRepo = sqlite.NoviPokusajiPrijaveRepo(novaDB)
|
||||
h.LoginIstorijsaRepo = sqlite.NoviLoginIstorijsaRepo(novaDB)
|
||||
h.DozvoleRepo = sqlite.NoviDozvoleRepo(novaDB, middleware.ImaDozvolu, middleware.SveAkcije())
|
||||
}
|
||||
|
||||
// popuniPodaciStranice popunjava zajednička polja stranice uključujući prijavljenog korisnika
|
||||
@@ -83,6 +86,7 @@ func (h *Handler) popuniPodaciStranice(r *http.Request, podesavanja map[string]s
|
||||
ps.Korisnik = k.KorisnickoIme
|
||||
ps.KorisnikIme = k.KorisnickoIme
|
||||
ps.KorisnikUloga = k.Uloga
|
||||
ps.Dozvole = h.DozvoleRepo.SveDozvole(r.Context(), k.Uloga)
|
||||
}
|
||||
ps.CsrfToken = middleware.CsrfToken(r.Context())
|
||||
return ps
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"ntech/internal/db/sqlite"
|
||||
"ntech/internal/middleware"
|
||||
"ntech/internal/model"
|
||||
)
|
||||
|
||||
@@ -73,6 +74,11 @@ type TopKlijent struct {
|
||||
|
||||
// Izvestaji renderuje stranicu sa izveštajima
|
||||
func (h *Handler) Izvestaji(w http.ResponseWriter, r *http.Request) {
|
||||
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||
if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "izvestaj.pregled") {
|
||||
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
ctx := r.Context()
|
||||
|
||||
podesavanja, err := sqlite.DohvatiSvaPodesavanja(ctx, h.DB)
|
||||
@@ -239,17 +245,12 @@ func (h *Handler) Izvestaji(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
ps := h.popuniPodaciStranice(r, podesavanja)
|
||||
ps.Stranica = "izvestaji"
|
||||
ps.NaslovStranice = "Izveštaji"
|
||||
|
||||
podaci := PodaciIzvestaja{
|
||||
PodaciStranice: model.PodaciStranice{
|
||||
Stranica: "izvestaji",
|
||||
NaslovStranice: "Izveštaji",
|
||||
Tema: podesavanja["tema"],
|
||||
NazivFirme: podesavanja["naziv_firme"],
|
||||
Podnazlov: podesavanja["podnazlov"],
|
||||
LogoTip: podesavanja["logo_tip"],
|
||||
LogoPutanja: podesavanja["logo_putanja"],
|
||||
Korisnik: "Admin",
|
||||
},
|
||||
PodaciStranice: ps,
|
||||
MesecniPrihodi: mesecniPrihodi,
|
||||
GrafikonJSON: template.JS(jsonBytes),
|
||||
StariNalozi: stariNalozi,
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"ntech/internal/db/sqlite"
|
||||
"ntech/internal/middleware"
|
||||
"ntech/internal/model"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
@@ -47,6 +48,11 @@ func (h *Handler) Kategorije(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// DodajKategoriju prima POST i čuva novu kategoriju
|
||||
func (h *Handler) DodajKategoriju(w http.ResponseWriter, r *http.Request) {
|
||||
kor := middleware.KorisnikIzKonteksta(r.Context())
|
||||
if kor == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), kor.Uloga, "kategorija.dodaj") {
|
||||
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest)
|
||||
return
|
||||
@@ -73,6 +79,11 @@ func (h *Handler) DodajKategoriju(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// ObrisiKategoriju briše kategoriju po ID-u
|
||||
func (h *Handler) ObrisiKategoriju(w http.ResponseWriter, r *http.Request) {
|
||||
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "kategorija.obrisi") {
|
||||
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
idStr := chi.URLParam(r, "id")
|
||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||
if err != nil {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"ntech/internal/db/sqlite"
|
||||
"ntech/internal/middleware"
|
||||
"ntech/internal/model"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
@@ -77,6 +78,11 @@ func (h *Handler) NoviKlijent(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// SacuvajKlijenta prima POST formu i upisuje novog klijenta u bazu
|
||||
func (h *Handler) SacuvajKlijenta(w http.ResponseWriter, r *http.Request) {
|
||||
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "klijent.dodaj") {
|
||||
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest)
|
||||
return
|
||||
@@ -147,6 +153,11 @@ func (h *Handler) IzmeniKlijenta(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// SacuvajIzmenuKlijenta prima POST formu i ažurira postojećeg klijenta u bazi
|
||||
func (h *Handler) SacuvajIzmenuKlijenta(w http.ResponseWriter, r *http.Request) {
|
||||
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "klijent.izmeni") {
|
||||
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
id, err := parseID(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
http.Error(w, "Neispravan ID klijenta", http.StatusBadRequest)
|
||||
@@ -195,6 +206,11 @@ func (h *Handler) SacuvajIzmenuKlijenta(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
// ObrisiKlijenta prima POST zahtev i briše klijenta po ID-u
|
||||
func (h *Handler) ObrisiKlijenta(w http.ResponseWriter, r *http.Request) {
|
||||
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "klijent.obrisi") {
|
||||
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
id, err := parseID(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
http.Error(w, "Neispravan ID klijenta", http.StatusBadRequest)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"ntech/internal/db"
|
||||
"ntech/internal/db/sqlite"
|
||||
"ntech/internal/middleware"
|
||||
"ntech/internal/model"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
@@ -74,6 +75,11 @@ func (h *Handler) Magacin(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// ObrisiArtikal briše artikal po ID-u
|
||||
func (h *Handler) ObrisiArtikal(w http.ResponseWriter, r *http.Request) {
|
||||
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "artikal.obrisi") {
|
||||
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
idStr := chi.URLParam(r, "id")
|
||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||
if err != nil {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"ntech/internal/db/sqlite"
|
||||
"ntech/internal/middleware"
|
||||
"ntech/internal/model"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
@@ -47,6 +48,11 @@ func (h *Handler) NoviArtikal(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// SacuvajArtikal prima POST formu i čuva novi artikal
|
||||
func (h *Handler) SacuvajArtikal(w http.ResponseWriter, r *http.Request) {
|
||||
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "artikal.dodaj") {
|
||||
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest)
|
||||
return
|
||||
@@ -136,6 +142,11 @@ func (h *Handler) IzmeniArtikal(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// SacuvajIzmenuArtikla prima POST formu i čuva izmenu artikla
|
||||
func (h *Handler) SacuvajIzmenuArtikla(w http.ResponseWriter, r *http.Request) {
|
||||
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "artikal.izmeni") {
|
||||
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
idStr := chi.URLParam(r, "id")
|
||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||
if err != nil {
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"ntech/internal/db"
|
||||
"ntech/internal/db/sqlite"
|
||||
"ntech/internal/middleware"
|
||||
"ntech/internal/model"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
@@ -121,6 +122,11 @@ func (h *Handler) NovaNabavka(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// SacuvajNabavku prima POST formu, parsira stavke i upisuje nabavku u bazu
|
||||
func (h *Handler) SacuvajNabavku(w http.ResponseWriter, r *http.Request) {
|
||||
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "nabavka.dodaj") {
|
||||
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest)
|
||||
return
|
||||
@@ -205,6 +211,11 @@ func (h *Handler) DetaljiNabavke(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// ObrisiNabavku prima POST zahtev i briše nabavku po ID-u
|
||||
func (h *Handler) ObrisiNabavku(w http.ResponseWriter, r *http.Request) {
|
||||
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "nabavka.obrisi") {
|
||||
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
id, err := parseID(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
http.Error(w, "Neispravan ID nabavke", http.StatusBadRequest)
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"time"
|
||||
|
||||
ntechsqlite "ntech/internal/db/sqlite"
|
||||
"ntech/internal/middleware"
|
||||
"ntech/internal/model"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
@@ -48,6 +49,11 @@ var validnoImeBackupa = regexp.MustCompile(`^ntech_\d{8}_\d{6}\.db$`)
|
||||
|
||||
// Podesavanja renderuje stranicu podešavanja
|
||||
func (h *Handler) Podesavanja(w http.ResponseWriter, r *http.Request) {
|
||||
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||
if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "podesavanja.pregled") {
|
||||
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
podesavanja, err := ntechsqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
|
||||
@@ -107,6 +113,11 @@ func ucitajListuBackupa() []BackupInfo {
|
||||
|
||||
// VratiBackup zamenjuje trenutnu bazu sa izabranim backup fajlom
|
||||
func (h *Handler) VratiBackup(w http.ResponseWriter, r *http.Request) {
|
||||
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||
if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "backup.pokreni") {
|
||||
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Redirect(w, r, "/podesavanja?backup_greska=Greška+pri+čitanju+zahteva", http.StatusSeeOther)
|
||||
return
|
||||
@@ -187,6 +198,11 @@ func kopiraFajl(izvor, odrediste string) error {
|
||||
|
||||
// SacuvajPodesavanja prima POST i čuva podešavanja u bazu
|
||||
func (h *Handler) SacuvajPodesavanja(w http.ResponseWriter, r *http.Request) {
|
||||
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||
if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "podesavanja.izmeni") {
|
||||
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest)
|
||||
return
|
||||
@@ -217,6 +233,11 @@ func (h *Handler) SacuvajPodesavanja(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// BackupBaze kreira konzistentnu kopiju baze i šalje je kao attachment
|
||||
func (h *Handler) BackupBaze(w http.ResponseWriter, r *http.Request) {
|
||||
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||
if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "backup.pregled") {
|
||||
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
privremeni := fmt.Sprintf("%s/ntech_backup_%s.db", os.TempDir(), time.Now().Format("20060102_150405"))
|
||||
|
||||
if _, err := h.DB.ExecContext(r.Context(), "VACUUM INTO ?", privremeni); err != nil {
|
||||
@@ -233,6 +254,11 @@ func (h *Handler) BackupBaze(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// OtpremiLogo prima multipart upload slike loga i čuva je u web/static/uploads/
|
||||
func (h *Handler) OtpremiLogo(w http.ResponseWriter, r *http.Request) {
|
||||
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||
if k == nil || !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "podesavanja.izmeni") {
|
||||
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
// ograničavamo telo zahteva na 2MB + malo za zaglavlja forme
|
||||
r.Body = http.MaxBytesReader(w, r.Body, 2<<20+4096)
|
||||
if err := r.ParseMultipartForm(2 << 20); err != nil {
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
appdb "ntech/internal/db"
|
||||
"ntech/internal/db/sqlite"
|
||||
"ntech/internal/middleware"
|
||||
"ntech/internal/model"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
@@ -135,6 +136,11 @@ func (h *Handler) NovaProdaja(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// SacuvajProdaju prima POST formu, parsira stavke i upisuje prodajni nalog u bazu
|
||||
func (h *Handler) SacuvajProdaju(w http.ResponseWriter, r *http.Request) {
|
||||
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "prodaja.dodaj") {
|
||||
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest)
|
||||
return
|
||||
@@ -300,6 +306,11 @@ func (h *Handler) StampaProdaje(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// ObrisiProdaju prima POST zahtev, vraća stanje na magacin i briše nalog
|
||||
func (h *Handler) ObrisiProdaju(w http.ResponseWriter, r *http.Request) {
|
||||
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "prodaja.obrisi") {
|
||||
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
id, err := parseID(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
http.Error(w, "Neispravan ID naloga", http.StatusBadRequest)
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
"ntech/internal/db/sqlite"
|
||||
"ntech/internal/middleware"
|
||||
"ntech/internal/model"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
@@ -109,6 +110,11 @@ func (h *Handler) NoviNalog(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// SacuvajNalog prima POST formu i upisuje novi servisni nalog u bazu
|
||||
func (h *Handler) SacuvajNalog(w http.ResponseWriter, r *http.Request) {
|
||||
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "servis.dodaj") {
|
||||
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest)
|
||||
return
|
||||
@@ -194,6 +200,11 @@ func (h *Handler) IzmeniNalog(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// SacuvajIzmenaNaloga prima POST formu i ažurira postojeći servisni nalog
|
||||
func (h *Handler) SacuvajIzmenaNaloga(w http.ResponseWriter, r *http.Request) {
|
||||
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "servis.izmeni") {
|
||||
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
id, err := parseID(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
http.Error(w, "Neispravan ID naloga", http.StatusBadRequest)
|
||||
@@ -248,6 +259,11 @@ func (h *Handler) SacuvajIzmenaNaloga(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// ObrisiNalog prima POST zahtev i briše servisni nalog po ID-u
|
||||
func (h *Handler) ObrisiNalog(w http.ResponseWriter, r *http.Request) {
|
||||
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "servis.obrisi") {
|
||||
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
id, err := parseID(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
http.Error(w, "Neispravan ID naloga", http.StatusBadRequest)
|
||||
|
||||
@@ -88,12 +88,37 @@ func RequireSuperAdmin(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
k := KorisnikIzKonteksta(r.Context())
|
||||
if k == nil || k.Uloga != "superadmin" {
|
||||
http.Error(w, "Pristup odbijen", http.StatusForbidden)
|
||||
postaviFlashGresku(w, "Nemate dozvolu za ovu stranicu.")
|
||||
http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// RequireAdmin je middleware koji propušta admin i superadmin korisnike
|
||||
func RequireAdmin(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
k := KorisnikIzKonteksta(r.Context())
|
||||
if k == nil || (k.Uloga != "admin" && k.Uloga != "superadmin") {
|
||||
postaviFlashGresku(w, "Nemate dozvolu za ovu stranicu.")
|
||||
http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// postaviFlashGresku upisuje jednokratnu poruku o grešci u kolačić
|
||||
func postaviFlashGresku(w http.ResponseWriter, poruka string) {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "ntech_flash_greska",
|
||||
Value: poruka,
|
||||
Path: "/",
|
||||
MaxAge: 60,
|
||||
HttpOnly: true,
|
||||
})
|
||||
}
|
||||
|
||||
// ErrNijePrijavljen se vraća kada korisnik nije u contextu
|
||||
var ErrNijePrijavljen = errors.New("korisnik nije prijavljen")
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
package middleware
|
||||
|
||||
// sve poznate akcije u sistemu
|
||||
var sveAkcije = []string{
|
||||
"artikal.pregled",
|
||||
"artikal.dodaj",
|
||||
"artikal.izmeni",
|
||||
"artikal.obrisi",
|
||||
"artikal.premesti",
|
||||
"kategorija.pregled",
|
||||
"kategorija.dodaj",
|
||||
"kategorija.izmeni",
|
||||
"kategorija.obrisi",
|
||||
"nabavka.pregled",
|
||||
"nabavka.dodaj",
|
||||
"nabavka.obrisi",
|
||||
"dobavljac.pregled",
|
||||
"dobavljac.dodaj",
|
||||
"dobavljac.izmeni",
|
||||
"dobavljac.obrisi",
|
||||
"servis.pregled",
|
||||
"servis.dodaj",
|
||||
"servis.izmeni",
|
||||
"servis.obrisi",
|
||||
"prodaja.pregled",
|
||||
"prodaja.dodaj",
|
||||
"prodaja.obrisi",
|
||||
"klijent.pregled",
|
||||
"klijent.dodaj",
|
||||
"klijent.izmeni",
|
||||
"klijent.obrisi",
|
||||
"podsetnik.pregled",
|
||||
"podsetnik.dodaj",
|
||||
"podsetnik.izmeni",
|
||||
"podsetnik.obrisi",
|
||||
"izvestaj.pregled",
|
||||
"podesavanja.pregled",
|
||||
"podesavanja.izmeni",
|
||||
"korisnik.pregled",
|
||||
"korisnik.dodaj",
|
||||
"korisnik.izmeni",
|
||||
"korisnik.obrisi",
|
||||
"korisnik.uloga",
|
||||
"backup.pregled",
|
||||
"backup.pokreni",
|
||||
}
|
||||
|
||||
// SveAkcije vraća listu svih poznatih akcija — koristi se pri inicijalizaciji baze i resetu
|
||||
func SveAkcije() []string {
|
||||
return sveAkcije
|
||||
}
|
||||
|
||||
// ImaDozvolu proverava da li data uloga sme da izvrši traženu akciju
|
||||
func ImaDozvolu(uloga, akcija string) bool {
|
||||
switch uloga {
|
||||
case "superadmin":
|
||||
// superadmin sme sve
|
||||
return true
|
||||
|
||||
case "admin":
|
||||
switch akcija {
|
||||
// artikal
|
||||
case "artikal.pregled", "artikal.dodaj", "artikal.izmeni",
|
||||
"artikal.obrisi", "artikal.premesti":
|
||||
return true
|
||||
// kategorija
|
||||
case "kategorija.pregled", "kategorija.dodaj",
|
||||
"kategorija.izmeni", "kategorija.obrisi":
|
||||
return true
|
||||
// nabavka
|
||||
case "nabavka.pregled", "nabavka.dodaj", "nabavka.obrisi":
|
||||
return true
|
||||
// dobavljač
|
||||
case "dobavljac.pregled", "dobavljac.dodaj",
|
||||
"dobavljac.izmeni", "dobavljac.obrisi":
|
||||
return true
|
||||
// servis
|
||||
case "servis.pregled", "servis.dodaj",
|
||||
"servis.izmeni", "servis.obrisi":
|
||||
return true
|
||||
// prodaja
|
||||
case "prodaja.pregled", "prodaja.dodaj", "prodaja.obrisi":
|
||||
return true
|
||||
// klijent
|
||||
case "klijent.pregled", "klijent.dodaj",
|
||||
"klijent.izmeni", "klijent.obrisi":
|
||||
return true
|
||||
// podsetnik
|
||||
case "podsetnik.pregled", "podsetnik.dodaj",
|
||||
"podsetnik.izmeni", "podsetnik.obrisi":
|
||||
return true
|
||||
// izveštaji i podešavanja
|
||||
case "izvestaj.pregled",
|
||||
"podesavanja.pregled", "podesavanja.izmeni":
|
||||
return true
|
||||
// korisnici (bez promene uloge)
|
||||
case "korisnik.pregled", "korisnik.dodaj",
|
||||
"korisnik.izmeni", "korisnik.obrisi":
|
||||
return true
|
||||
// backup
|
||||
case "backup.pregled", "backup.pokreni":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
||||
case "radnik":
|
||||
switch akcija {
|
||||
// artikal — bez brisanja i premeštanja
|
||||
case "artikal.pregled", "artikal.dodaj", "artikal.izmeni":
|
||||
return true
|
||||
// kategorija — samo pregled
|
||||
case "kategorija.pregled":
|
||||
return true
|
||||
// nabavka — bez brisanja
|
||||
case "nabavka.pregled", "nabavka.dodaj":
|
||||
return true
|
||||
// dobavljač — bez brisanja
|
||||
case "dobavljac.pregled", "dobavljac.dodaj", "dobavljac.izmeni":
|
||||
return true
|
||||
// servis — bez brisanja
|
||||
case "servis.pregled", "servis.dodaj", "servis.izmeni":
|
||||
return true
|
||||
// prodaja — bez brisanja
|
||||
case "prodaja.pregled", "prodaja.dodaj":
|
||||
return true
|
||||
// klijent — bez brisanja
|
||||
case "klijent.pregled", "klijent.dodaj", "klijent.izmeni":
|
||||
return true
|
||||
// podsetnik — sve
|
||||
case "podsetnik.pregled", "podsetnik.dodaj",
|
||||
"podsetnik.izmeni", "podsetnik.obrisi":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// SveDozvole vraća mapu svih akcija sa vrednošću true/false za datu ulogu
|
||||
func SveDozvole(uloga string) map[string]bool {
|
||||
m := make(map[string]bool, len(sveAkcije))
|
||||
for _, akcija := range sveAkcije {
|
||||
m[akcija] = ImaDozvolu(uloga, akcija)
|
||||
}
|
||||
return m
|
||||
}
|
||||
@@ -36,6 +36,7 @@ type PodaciStranice struct {
|
||||
KorisnikIme string // korisničko ime prijavljenog korisnika
|
||||
KorisnikUloga string // uloga: "superadmin", "admin", "radnik"
|
||||
CsrfToken string // CSRF zaštitni token za forme
|
||||
Dozvole map[string]bool // mapa akcija → dozvoljeno/nije
|
||||
}
|
||||
|
||||
// PodaciDashboarda su podaci specifični za dashboard stranicu
|
||||
@@ -49,4 +50,5 @@ type PodaciDashboarda struct {
|
||||
PoslednjiServisi []StavkaServisa
|
||||
KriticneZalihe []StavkaZalihe
|
||||
PoslednjeProdaje []StavkaProdajePregled
|
||||
FlashGreska string
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
CREATE TABLE IF NOT EXISTS dozvole (
|
||||
uloga TEXT NOT NULL,
|
||||
akcija TEXT NOT NULL,
|
||||
dozvoljeno INTEGER NOT NULL DEFAULT 1,
|
||||
PRIMARY KEY (uloga, akcija)
|
||||
);
|
||||
+87
-84
@@ -316,11 +316,75 @@ body {
|
||||
border-radius: 6px;
|
||||
padding: 4px 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
.btn-primarno-malo:hover { opacity: 0.85; }
|
||||
|
||||
/* sekundarno dugme (Odustani) */
|
||||
.btn-sekundarno {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background: transparent;
|
||||
color: var(--tekst-sporedni);
|
||||
border: 0.5px solid var(--ivica);
|
||||
border-radius: 8px;
|
||||
padding: 9px 20px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: background 0.2s, color 0.2s;
|
||||
}
|
||||
.btn-sekundarno:hover { background: var(--pozadina); color: var(--tekst-glavni); }
|
||||
|
||||
/* crveno dugme za brisanje u tabelama */
|
||||
.btn-obrisi-malo {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background: #dc2626;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 4px 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
.btn-obrisi-malo:hover { opacity: 0.8; }
|
||||
|
||||
/* nazad link na formama */
|
||||
.nazad-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
color: var(--tekst-sporedni);
|
||||
text-decoration: none;
|
||||
margin-bottom: 20px;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
.nazad-link:hover { color: var(--tekst-glavni); }
|
||||
|
||||
/* hover na redovima tabela — zamena za inline onmouseover/onmouseout */
|
||||
.red-tabele {
|
||||
border-bottom: 0.5px solid var(--ivica);
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.red-tabele:hover { background: var(--pozadina); }
|
||||
|
||||
/* naslov sekcije unutar forme */
|
||||
.sekcija-naslov {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--tekst-sporedni);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* avatar krug korisnika u topbaru */
|
||||
.avatar-korisnik {
|
||||
width: 32px;
|
||||
@@ -423,6 +487,7 @@ select {
|
||||
|
||||
/* mobilni ekrani */
|
||||
@media (max-width: 768px) {
|
||||
/* sidebar — drawer ponašanje */
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
@@ -433,60 +498,32 @@ select {
|
||||
transition: transform 0.28s cubic-bezier(.4,0,.2,1);
|
||||
width: 220px !important;
|
||||
}
|
||||
.sidebar.otvoren { transform: translateX(0); }
|
||||
.sidebar.otvoren .logo-zona { opacity: 1; width: 160px; pointer-events: auto; }
|
||||
.sidebar.otvoren .nav-oznaka { opacity: 1; }
|
||||
.sidebar.otvoren .nav-stavka span { opacity: 1; pointer-events: auto; }
|
||||
.nav-tooltip { display: none !important; }
|
||||
.glavni-sadrzaj { width: 100%; }
|
||||
|
||||
.sidebar.otvoren {
|
||||
transform: translateX(0);
|
||||
}
|
||||
/* topbar hamburger */
|
||||
#hamburger-topbar { display: flex !important; color: var(--tekst-glavni); }
|
||||
#hamburger-topbar:hover { background: var(--pozadina); }
|
||||
|
||||
/* na mobilnom logo zona uvek vidljiva kada je sidebar otvoren */
|
||||
.sidebar.otvoren .logo-zona {
|
||||
opacity: 1;
|
||||
width: 160px;
|
||||
pointer-events: auto;
|
||||
}
|
||||
/* teme */
|
||||
.topbar-teme { display: none; }
|
||||
.teme-grid { flex-direction: column !important; }
|
||||
|
||||
.sidebar.otvoren .nav-oznaka {
|
||||
opacity: 1;
|
||||
}
|
||||
/* forme */
|
||||
.forma-grid-2 { grid-template-columns: 1fr !important; }
|
||||
|
||||
.sidebar.otvoren .nav-stavka span {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* glavni sadržaj zauzima celu širinu */
|
||||
.glavni-sadrzaj {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* sakrij tooltip na mobilnom */
|
||||
.nav-tooltip {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* hamburger u topbaru — samo na mobilnom */
|
||||
@media (max-width: 768px) {
|
||||
#hamburger-topbar {
|
||||
display: flex !important;
|
||||
color: var(--tekst-glavni);
|
||||
}
|
||||
|
||||
#hamburger-topbar:hover {
|
||||
background: var(--pozadina);
|
||||
}
|
||||
}
|
||||
|
||||
/* tačkice za teme u topbaru — sakrij na mobilnom */
|
||||
@media (max-width: 768px) {
|
||||
.topbar-teme {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* teme u podešavanjima — jedna ispod druge na mobilnom */
|
||||
.teme-grid {
|
||||
flex-direction: column !important;
|
||||
}
|
||||
/* magacin traka */
|
||||
.magacin-traka { flex-direction: column; align-items: stretch; }
|
||||
.magacin-traka form { flex-direction: column; align-items: stretch; }
|
||||
.magacin-traka a { text-align: center; }
|
||||
.magacin-traka form input,
|
||||
.magacin-traka form select,
|
||||
.magacin-traka form button,
|
||||
.magacin-traka form label { width: 100%; justify-content: flex-start; }
|
||||
}
|
||||
|
||||
/* tačkice za teme */
|
||||
@@ -507,13 +544,6 @@ select {
|
||||
border-color: var(--tekst-glavni) !important;
|
||||
}
|
||||
|
||||
/* forme — responsive */
|
||||
@media (max-width: 768px) {
|
||||
.forma-grid-2 {
|
||||
grid-template-columns: 1fr !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* animacije */
|
||||
@keyframes fadeInUp {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
@@ -553,30 +583,3 @@ select {
|
||||
min-width: 200px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.magacin-traka {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.magacin-traka form {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.magacin-traka a {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* dodatna korekcija magacin trake */
|
||||
@media (max-width: 768px) {
|
||||
.magacin-traka form input,
|
||||
.magacin-traka form select,
|
||||
.magacin-traka form button,
|
||||
.magacin-traka form label {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,11 +75,13 @@
|
||||
<span class="nav-tooltip">Dobavljači</span>
|
||||
</a>
|
||||
|
||||
{{if index .Dozvole "izvestaj.pregled"}}
|
||||
<a href="/izvestaji" class="nav-stavka {{if eq .Stranica "izvestaji"}}aktivan{{end}}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>
|
||||
<span>Izveštaji</span>
|
||||
<span class="nav-tooltip">Izveštaji</span>
|
||||
</a>
|
||||
{{end}}
|
||||
|
||||
<div class="nav-separator"></div>
|
||||
<div class="nav-oznaka">Nalog</div>
|
||||
@@ -90,7 +92,7 @@
|
||||
<span class="nav-tooltip">Moj profil</span>
|
||||
</a>
|
||||
|
||||
{{if eq .KorisnikUloga "superadmin"}}
|
||||
{{if or (eq .KorisnikUloga "superadmin") (eq .KorisnikUloga "admin")}}
|
||||
<a href="/admin/korisnici" class="nav-stavka {{if eq .Stranica "admin"}}aktivan{{end}}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
|
||||
<span>Korisnici</span>
|
||||
@@ -98,14 +100,24 @@
|
||||
</a>
|
||||
{{end}}
|
||||
|
||||
{{if eq .KorisnikUloga "superadmin"}}
|
||||
<a href="/admin/dozvole" class="nav-stavka {{if eq .Stranica "dozvole"}}aktivan{{end}}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
|
||||
<span>Dozvole</span>
|
||||
<span class="nav-tooltip">Dozvole</span>
|
||||
</a>
|
||||
{{end}}
|
||||
|
||||
<div class="nav-separator"></div>
|
||||
<div class="nav-oznaka">Sistem</div>
|
||||
|
||||
{{if index .Dozvole "podesavanja.pregled"}}
|
||||
<a href="/podesavanja" class="nav-stavka {{if eq .Stranica "podesavanja"}}aktivan{{end}}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06-.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
|
||||
<span>Podešavanja</span>
|
||||
<span class="nav-tooltip">Podešavanja</span>
|
||||
</a>
|
||||
{{end}}
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-dno">
|
||||
|
||||
@@ -0,0 +1,358 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "naslov"}}Dozvole — NTech{{end}}
|
||||
|
||||
{{define "dodatni-css"}}
|
||||
<style>
|
||||
.dozvole-tabela tbody tr:nth-child(1) { animation-delay: 0.04s; }
|
||||
.dozvole-tabela tbody tr:nth-child(2) { animation-delay: 0.08s; }
|
||||
.dozvole-tabela tbody tr:nth-child(3) { animation-delay: 0.12s; }
|
||||
.dozvole-tabela tbody tr:nth-child(4) { animation-delay: 0.16s; }
|
||||
.dozvole-tabela tbody tr:nth-child(5) { animation-delay: 0.20s; }
|
||||
|
||||
.matrica-modul td {
|
||||
padding: 8px 16px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--tekst-sporedni);
|
||||
background: var(--pozadina);
|
||||
border-top: 0.5px solid var(--ivica);
|
||||
}
|
||||
|
||||
.matrica-checkbox { text-align: center; padding: 8px 16px; }
|
||||
.matrica-checkbox input[type=checkbox] { width: 16px; height: 16px; cursor: pointer; accent-color: var(--sb-akcent); }
|
||||
.matrica-checkbox input[type=checkbox]:disabled { cursor: default; opacity: 0.5; }
|
||||
</style>
|
||||
{{end}}
|
||||
|
||||
{{define "sadrzaj"}}
|
||||
<div style="display:flex;flex-direction:column;gap:20px;">
|
||||
|
||||
{{if .Sacuvano}}
|
||||
<div class="poruka-uspeh">Promene su uspešno sačuvane.</div>
|
||||
{{end}}
|
||||
{{if .Greska}}
|
||||
<div class="poruka-greska">Greška pri čuvanju promena.</div>
|
||||
{{end}}
|
||||
|
||||
<!-- matrica dozvola — editabilna forma -->
|
||||
<div class="kartica animiraj" style="padding:0;overflow:hidden;">
|
||||
<div style="padding:14px 16px;border-bottom:0.5px solid var(--ivica);">
|
||||
<span style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">Dozvole po ulogama</span>
|
||||
</div>
|
||||
<form method="POST" action="/admin/dozvole/sacuvaj">
|
||||
<div style="overflow-x:auto;">
|
||||
<table style="width:100%;border-collapse:collapse;">
|
||||
<thead>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<th style="padding:10px 16px;text-align:left;font-size:12px;font-weight:500;color:var(--tekst-sporedni);min-width:200px;">Akcija</th>
|
||||
<th style="padding:10px 16px;text-align:center;font-size:12px;font-weight:500;color:var(--tekst-sporedni);width:110px;">Radnik</th>
|
||||
<th style="padding:10px 16px;text-align:center;font-size:12px;font-weight:500;color:var(--tekst-sporedni);width:110px;">Admin</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
<!-- Magacin -->
|
||||
<tr class="matrica-modul"><td colspan="3">Magacin</td></tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:9px 16px;font-size:13px;color:var(--tekst-glavni);">Pregled artikala</td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="radnik__artikal.pregled" {{if index .DozvoleRadnik "artikal.pregled"}}checked{{end}}></td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="admin__artikal.pregled" {{if index .DozvoleAdmin "artikal.pregled"}}checked{{end}}></td>
|
||||
|
||||
</tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:9px 16px;font-size:13px;color:var(--tekst-glavni);">Dodavanje artikala</td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="radnik__artikal.dodaj" {{if index .DozvoleRadnik "artikal.dodaj"}}checked{{end}}></td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="admin__artikal.dodaj" {{if index .DozvoleAdmin "artikal.dodaj"}}checked{{end}}></td>
|
||||
|
||||
</tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:9px 16px;font-size:13px;color:var(--tekst-glavni);">Izmena artikala</td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="radnik__artikal.izmeni" {{if index .DozvoleRadnik "artikal.izmeni"}}checked{{end}}></td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="admin__artikal.izmeni" {{if index .DozvoleAdmin "artikal.izmeni"}}checked{{end}}></td>
|
||||
|
||||
</tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:9px 16px;font-size:13px;color:var(--tekst-glavni);">Brisanje artikala</td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="radnik__artikal.obrisi" {{if index .DozvoleRadnik "artikal.obrisi"}}checked{{end}}></td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="admin__artikal.obrisi" {{if index .DozvoleAdmin "artikal.obrisi"}}checked{{end}}></td>
|
||||
|
||||
</tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:9px 16px;font-size:13px;color:var(--tekst-glavni);">Premeštanje artikala</td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="radnik__artikal.premesti" {{if index .DozvoleRadnik "artikal.premesti"}}checked{{end}}></td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="admin__artikal.premesti" {{if index .DozvoleAdmin "artikal.premesti"}}checked{{end}}></td>
|
||||
|
||||
</tr>
|
||||
|
||||
<!-- Kategorije -->
|
||||
<tr class="matrica-modul"><td colspan="3">Kategorije</td></tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:9px 16px;font-size:13px;color:var(--tekst-glavni);">Pregled kategorija</td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="radnik__kategorija.pregled" {{if index .DozvoleRadnik "kategorija.pregled"}}checked{{end}}></td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="admin__kategorija.pregled" {{if index .DozvoleAdmin "kategorija.pregled"}}checked{{end}}></td>
|
||||
|
||||
</tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:9px 16px;font-size:13px;color:var(--tekst-glavni);">Dodavanje kategorija</td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="radnik__kategorija.dodaj" {{if index .DozvoleRadnik "kategorija.dodaj"}}checked{{end}}></td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="admin__kategorija.dodaj" {{if index .DozvoleAdmin "kategorija.dodaj"}}checked{{end}}></td>
|
||||
|
||||
</tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:9px 16px;font-size:13px;color:var(--tekst-glavni);">Izmena kategorija</td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="radnik__kategorija.izmeni" {{if index .DozvoleRadnik "kategorija.izmeni"}}checked{{end}}></td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="admin__kategorija.izmeni" {{if index .DozvoleAdmin "kategorija.izmeni"}}checked{{end}}></td>
|
||||
|
||||
</tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:9px 16px;font-size:13px;color:var(--tekst-glavni);">Brisanje kategorija</td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="radnik__kategorija.obrisi" {{if index .DozvoleRadnik "kategorija.obrisi"}}checked{{end}}></td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="admin__kategorija.obrisi" {{if index .DozvoleAdmin "kategorija.obrisi"}}checked{{end}}></td>
|
||||
|
||||
</tr>
|
||||
|
||||
<!-- Nabavke -->
|
||||
<tr class="matrica-modul"><td colspan="3">Nabavke</td></tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:9px 16px;font-size:13px;color:var(--tekst-glavni);">Pregled nabavki</td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="radnik__nabavka.pregled" {{if index .DozvoleRadnik "nabavka.pregled"}}checked{{end}}></td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="admin__nabavka.pregled" {{if index .DozvoleAdmin "nabavka.pregled"}}checked{{end}}></td>
|
||||
|
||||
</tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:9px 16px;font-size:13px;color:var(--tekst-glavni);">Dodavanje nabavki</td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="radnik__nabavka.dodaj" {{if index .DozvoleRadnik "nabavka.dodaj"}}checked{{end}}></td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="admin__nabavka.dodaj" {{if index .DozvoleAdmin "nabavka.dodaj"}}checked{{end}}></td>
|
||||
|
||||
</tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:9px 16px;font-size:13px;color:var(--tekst-glavni);">Brisanje nabavki</td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="radnik__nabavka.obrisi" {{if index .DozvoleRadnik "nabavka.obrisi"}}checked{{end}}></td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="admin__nabavka.obrisi" {{if index .DozvoleAdmin "nabavka.obrisi"}}checked{{end}}></td>
|
||||
|
||||
</tr>
|
||||
|
||||
<!-- Dobavljači -->
|
||||
<tr class="matrica-modul"><td colspan="3">Dobavljači</td></tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:9px 16px;font-size:13px;color:var(--tekst-glavni);">Pregled dobavljača</td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="radnik__dobavljac.pregled" {{if index .DozvoleRadnik "dobavljac.pregled"}}checked{{end}}></td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="admin__dobavljac.pregled" {{if index .DozvoleAdmin "dobavljac.pregled"}}checked{{end}}></td>
|
||||
|
||||
</tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:9px 16px;font-size:13px;color:var(--tekst-glavni);">Dodavanje dobavljača</td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="radnik__dobavljac.dodaj" {{if index .DozvoleRadnik "dobavljac.dodaj"}}checked{{end}}></td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="admin__dobavljac.dodaj" {{if index .DozvoleAdmin "dobavljac.dodaj"}}checked{{end}}></td>
|
||||
|
||||
</tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:9px 16px;font-size:13px;color:var(--tekst-glavni);">Izmena dobavljača</td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="radnik__dobavljac.izmeni" {{if index .DozvoleRadnik "dobavljac.izmeni"}}checked{{end}}></td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="admin__dobavljac.izmeni" {{if index .DozvoleAdmin "dobavljac.izmeni"}}checked{{end}}></td>
|
||||
|
||||
</tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:9px 16px;font-size:13px;color:var(--tekst-glavni);">Brisanje dobavljača</td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="radnik__dobavljac.obrisi" {{if index .DozvoleRadnik "dobavljac.obrisi"}}checked{{end}}></td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="admin__dobavljac.obrisi" {{if index .DozvoleAdmin "dobavljac.obrisi"}}checked{{end}}></td>
|
||||
|
||||
</tr>
|
||||
|
||||
<!-- Servis -->
|
||||
<tr class="matrica-modul"><td colspan="3">Servis</td></tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:9px 16px;font-size:13px;color:var(--tekst-glavni);">Pregled servisnih naloga</td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="radnik__servis.pregled" {{if index .DozvoleRadnik "servis.pregled"}}checked{{end}}></td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="admin__servis.pregled" {{if index .DozvoleAdmin "servis.pregled"}}checked{{end}}></td>
|
||||
|
||||
</tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:9px 16px;font-size:13px;color:var(--tekst-glavni);">Dodavanje servisnih naloga</td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="radnik__servis.dodaj" {{if index .DozvoleRadnik "servis.dodaj"}}checked{{end}}></td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="admin__servis.dodaj" {{if index .DozvoleAdmin "servis.dodaj"}}checked{{end}}></td>
|
||||
|
||||
</tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:9px 16px;font-size:13px;color:var(--tekst-glavni);">Izmena servisnih naloga</td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="radnik__servis.izmeni" {{if index .DozvoleRadnik "servis.izmeni"}}checked{{end}}></td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="admin__servis.izmeni" {{if index .DozvoleAdmin "servis.izmeni"}}checked{{end}}></td>
|
||||
|
||||
</tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:9px 16px;font-size:13px;color:var(--tekst-glavni);">Brisanje servisnih naloga</td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="radnik__servis.obrisi" {{if index .DozvoleRadnik "servis.obrisi"}}checked{{end}}></td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="admin__servis.obrisi" {{if index .DozvoleAdmin "servis.obrisi"}}checked{{end}}></td>
|
||||
|
||||
</tr>
|
||||
|
||||
<!-- Prodaja -->
|
||||
<tr class="matrica-modul"><td colspan="3">Prodaja</td></tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:9px 16px;font-size:13px;color:var(--tekst-glavni);">Pregled prodaje</td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="radnik__prodaja.pregled" {{if index .DozvoleRadnik "prodaja.pregled"}}checked{{end}}></td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="admin__prodaja.pregled" {{if index .DozvoleAdmin "prodaja.pregled"}}checked{{end}}></td>
|
||||
|
||||
</tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:9px 16px;font-size:13px;color:var(--tekst-glavni);">Dodavanje prodaje</td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="radnik__prodaja.dodaj" {{if index .DozvoleRadnik "prodaja.dodaj"}}checked{{end}}></td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="admin__prodaja.dodaj" {{if index .DozvoleAdmin "prodaja.dodaj"}}checked{{end}}></td>
|
||||
|
||||
</tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:9px 16px;font-size:13px;color:var(--tekst-glavni);">Brisanje prodaje</td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="radnik__prodaja.obrisi" {{if index .DozvoleRadnik "prodaja.obrisi"}}checked{{end}}></td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="admin__prodaja.obrisi" {{if index .DozvoleAdmin "prodaja.obrisi"}}checked{{end}}></td>
|
||||
|
||||
</tr>
|
||||
|
||||
<!-- Klijenti -->
|
||||
<tr class="matrica-modul"><td colspan="3">Klijenti</td></tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:9px 16px;font-size:13px;color:var(--tekst-glavni);">Pregled klijenata</td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="radnik__klijent.pregled" {{if index .DozvoleRadnik "klijent.pregled"}}checked{{end}}></td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="admin__klijent.pregled" {{if index .DozvoleAdmin "klijent.pregled"}}checked{{end}}></td>
|
||||
|
||||
</tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:9px 16px;font-size:13px;color:var(--tekst-glavni);">Dodavanje klijenata</td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="radnik__klijent.dodaj" {{if index .DozvoleRadnik "klijent.dodaj"}}checked{{end}}></td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="admin__klijent.dodaj" {{if index .DozvoleAdmin "klijent.dodaj"}}checked{{end}}></td>
|
||||
|
||||
</tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:9px 16px;font-size:13px;color:var(--tekst-glavni);">Izmena klijenata</td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="radnik__klijent.izmeni" {{if index .DozvoleRadnik "klijent.izmeni"}}checked{{end}}></td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="admin__klijent.izmeni" {{if index .DozvoleAdmin "klijent.izmeni"}}checked{{end}}></td>
|
||||
|
||||
</tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:9px 16px;font-size:13px;color:var(--tekst-glavni);">Brisanje klijenata</td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="radnik__klijent.obrisi" {{if index .DozvoleRadnik "klijent.obrisi"}}checked{{end}}></td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="admin__klijent.obrisi" {{if index .DozvoleAdmin "klijent.obrisi"}}checked{{end}}></td>
|
||||
|
||||
</tr>
|
||||
|
||||
<!-- Podsetnici -->
|
||||
<tr class="matrica-modul"><td colspan="3">Podsetnici</td></tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:9px 16px;font-size:13px;color:var(--tekst-glavni);">Pregled podsetnika</td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="radnik__podsetnik.pregled" {{if index .DozvoleRadnik "podsetnik.pregled"}}checked{{end}}></td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="admin__podsetnik.pregled" {{if index .DozvoleAdmin "podsetnik.pregled"}}checked{{end}}></td>
|
||||
|
||||
</tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:9px 16px;font-size:13px;color:var(--tekst-glavni);">Dodavanje podsetnika</td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="radnik__podsetnik.dodaj" {{if index .DozvoleRadnik "podsetnik.dodaj"}}checked{{end}}></td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="admin__podsetnik.dodaj" {{if index .DozvoleAdmin "podsetnik.dodaj"}}checked{{end}}></td>
|
||||
|
||||
</tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:9px 16px;font-size:13px;color:var(--tekst-glavni);">Izmena podsetnika</td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="radnik__podsetnik.izmeni" {{if index .DozvoleRadnik "podsetnik.izmeni"}}checked{{end}}></td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="admin__podsetnik.izmeni" {{if index .DozvoleAdmin "podsetnik.izmeni"}}checked{{end}}></td>
|
||||
|
||||
</tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:9px 16px;font-size:13px;color:var(--tekst-glavni);">Brisanje podsetnika</td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="radnik__podsetnik.obrisi" {{if index .DozvoleRadnik "podsetnik.obrisi"}}checked{{end}}></td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="admin__podsetnik.obrisi" {{if index .DozvoleAdmin "podsetnik.obrisi"}}checked{{end}}></td>
|
||||
|
||||
</tr>
|
||||
|
||||
<!-- Izveštaji -->
|
||||
<tr class="matrica-modul"><td colspan="3">Izveštaji</td></tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:9px 16px;font-size:13px;color:var(--tekst-glavni);">Pregled izveštaja</td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="radnik__izvestaj.pregled" {{if index .DozvoleRadnik "izvestaj.pregled"}}checked{{end}}></td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="admin__izvestaj.pregled" {{if index .DozvoleAdmin "izvestaj.pregled"}}checked{{end}}></td>
|
||||
|
||||
</tr>
|
||||
|
||||
<!-- Podešavanja -->
|
||||
<tr class="matrica-modul"><td colspan="3">Podešavanja</td></tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:9px 16px;font-size:13px;color:var(--tekst-glavni);">Pregled podešavanja</td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="radnik__podesavanja.pregled" {{if index .DozvoleRadnik "podesavanja.pregled"}}checked{{end}}></td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="admin__podesavanja.pregled" {{if index .DozvoleAdmin "podesavanja.pregled"}}checked{{end}}></td>
|
||||
|
||||
</tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:9px 16px;font-size:13px;color:var(--tekst-glavni);">Izmena podešavanja</td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="radnik__podesavanja.izmeni" {{if index .DozvoleRadnik "podesavanja.izmeni"}}checked{{end}}></td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="admin__podesavanja.izmeni" {{if index .DozvoleAdmin "podesavanja.izmeni"}}checked{{end}}></td>
|
||||
|
||||
</tr>
|
||||
|
||||
<!-- Korisnici -->
|
||||
<tr class="matrica-modul"><td colspan="3">Korisnici</td></tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:9px 16px;font-size:13px;color:var(--tekst-glavni);">Pregled korisnika</td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="radnik__korisnik.pregled" {{if index .DozvoleRadnik "korisnik.pregled"}}checked{{end}}></td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="admin__korisnik.pregled" {{if index .DozvoleAdmin "korisnik.pregled"}}checked{{end}}></td>
|
||||
|
||||
</tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:9px 16px;font-size:13px;color:var(--tekst-glavni);">Dodavanje korisnika</td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="radnik__korisnik.dodaj" {{if index .DozvoleRadnik "korisnik.dodaj"}}checked{{end}}></td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="admin__korisnik.dodaj" {{if index .DozvoleAdmin "korisnik.dodaj"}}checked{{end}}></td>
|
||||
|
||||
</tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:9px 16px;font-size:13px;color:var(--tekst-glavni);">Izmena korisnika</td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="radnik__korisnik.izmeni" {{if index .DozvoleRadnik "korisnik.izmeni"}}checked{{end}}></td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="admin__korisnik.izmeni" {{if index .DozvoleAdmin "korisnik.izmeni"}}checked{{end}}></td>
|
||||
|
||||
</tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:9px 16px;font-size:13px;color:var(--tekst-glavni);">Brisanje korisnika</td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="radnik__korisnik.obrisi" {{if index .DozvoleRadnik "korisnik.obrisi"}}checked{{end}}></td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="admin__korisnik.obrisi" {{if index .DozvoleAdmin "korisnik.obrisi"}}checked{{end}}></td>
|
||||
|
||||
</tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:9px 16px;font-size:13px;color:var(--tekst-glavni);">Promena uloge korisnika</td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="radnik__korisnik.uloga" {{if index .DozvoleRadnik "korisnik.uloga"}}checked{{end}}></td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="admin__korisnik.uloga" {{if index .DozvoleAdmin "korisnik.uloga"}}checked{{end}}></td>
|
||||
|
||||
</tr>
|
||||
|
||||
<!-- Backup -->
|
||||
<tr class="matrica-modul"><td colspan="3">Backup</td></tr>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:9px 16px;font-size:13px;color:var(--tekst-glavni);">Pregled rezervnih kopija</td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="radnik__backup.pregled" {{if index .DozvoleRadnik "backup.pregled"}}checked{{end}}></td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="admin__backup.pregled" {{if index .DozvoleAdmin "backup.pregled"}}checked{{end}}></td>
|
||||
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:9px 16px;font-size:13px;color:var(--tekst-glavni);">Pokretanje bekapa</td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="radnik__backup.pokreni" {{if index .DozvoleRadnik "backup.pokreni"}}checked{{end}}></td>
|
||||
<td class="matrica-checkbox"><input type="checkbox" name="admin__backup.pokreni" {{if index .DozvoleAdmin "backup.pokreni"}}checked{{end}}></td>
|
||||
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- dugmad za čuvanje i reset -->
|
||||
<div style="display:flex;justify-content:flex-end;gap:10px;padding:16px;border-top:0.5px solid var(--ivica);">
|
||||
<button type="submit" form="forma-reset" class="btn-sekundarno"
|
||||
data-potvrda="Resetovati sve dozvole na podrazumevane vrednosti? Ovo će poništiti sve prilagođene promene.">
|
||||
Reset na podrazumevano
|
||||
</button>
|
||||
<button type="submit" class="btn-primarno">Sačuvaj promene</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- zasebna forma za reset, bez vidljivih elemenata -->
|
||||
<form id="forma-reset" method="POST" action="/admin/dozvole/reset"></form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -4,10 +4,6 @@
|
||||
|
||||
{{define "dodatni-css"}}
|
||||
<style>
|
||||
@keyframes slideDown {
|
||||
from { opacity: 0; transform: translateY(-10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.poruka-animacija { animation: slideDown 0.3s ease forwards; }
|
||||
|
||||
.korisnici-tabela tbody tr:nth-child(1) { animation-delay: 0.04s; }
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
{{end}}
|
||||
|
||||
{{define "sadrzaj"}}
|
||||
{{if .FlashGreska}}
|
||||
<div class="poruka-greska animiraj" style="margin-bottom:16px;">{{.FlashGreska}}</div>
|
||||
{{end}}
|
||||
<div class="grid grid-cols-2 md:grid-cols-5 gap-3 mb-6">
|
||||
<!-- stat kartice -->
|
||||
<div class="kartica dash-stat animiraj">
|
||||
|
||||
@@ -4,9 +4,7 @@
|
||||
|
||||
{{define "dodatni-css"}}
|
||||
<style>
|
||||
.greska-animacija {
|
||||
animation: shake 0.4s ease;
|
||||
}
|
||||
.greska-animacija { animation: shake 0.4s ease; }
|
||||
</style>
|
||||
{{end}}
|
||||
|
||||
@@ -14,10 +12,8 @@
|
||||
<div style="width:100%;">
|
||||
|
||||
<!-- nazad dugme -->
|
||||
<a href="/dobavljaci"
|
||||
style="display:inline-flex;align-items:center;gap:6px;font-size:13px;color:var(--tekst-sporedni);text-decoration:none;margin-bottom:20px;transition:color 0.2s;"
|
||||
onmouseover="this.style.color='var(--tekst-glavni)'" onmouseout="this.style.color='var(--tekst-sporedni)'">
|
||||
<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"><polyline points="15 18 9 12 15 6"/></svg>
|
||||
<a href="/dobavljaci" class="nazad-link">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="15 18 9 12 15 6"/></svg>
|
||||
Nazad na dobavljače
|
||||
</a>
|
||||
|
||||
@@ -29,10 +25,7 @@
|
||||
</div>
|
||||
|
||||
{{if .Greska}}
|
||||
<div class="greska-animacija"
|
||||
style="background:var(--kartica);border:0.5px solid #dc2626;border-radius:8px;padding:10px 14px;margin-bottom:16px;font-size:13px;color:#dc2626;">
|
||||
{{.Greska}}
|
||||
</div>
|
||||
<div class="poruka-greska greska-animacija">{{.Greska}}</div>
|
||||
{{end}}
|
||||
|
||||
<form method="POST" action="{{if .Izmena}}/dobavljaci/izmeni/{{.Dobavljac.ID}}{{else}}/dobavljaci/novi{{end}}">
|
||||
@@ -82,14 +75,8 @@
|
||||
|
||||
<!-- dugmad -->
|
||||
<div style="display:flex;justify-content:flex-end;gap:10px;margin-top:6px;">
|
||||
<a href="/dobavljaci"
|
||||
style="padding:9px 20px;border:0.5px solid var(--ivica);border-radius:8px;font-size:14px;color:var(--tekst-sporedni);text-decoration:none;transition:background 0.2s;"
|
||||
onmouseover="this.style.background='var(--pozadina)'" onmouseout="this.style.background=''">
|
||||
Odustani
|
||||
</a>
|
||||
<button type="submit"
|
||||
style="padding:9px 20px;background:var(--sb-akcent);color:#fff;border:none;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;transition:opacity 0.2s;"
|
||||
onmouseover="this.style.opacity='0.85'" onmouseout="this.style.opacity='1'">
|
||||
<a href="/dobavljaci" class="btn-sekundarno">Odustani</a>
|
||||
<button type="submit" class="btn-primarno">
|
||||
{{if .Izmena}}Sačuvaj izmene{{else}}Dodaj dobavljača{{end}}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -4,14 +4,7 @@
|
||||
|
||||
{{define "dodatni-css"}}
|
||||
<style>
|
||||
@keyframes slideDown {
|
||||
from { opacity: 0; transform: translateY(-10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.poruka-animacija {
|
||||
animation: slideDown 0.3s ease forwards;
|
||||
}
|
||||
.poruka-animacija { animation: slideDown 0.3s ease forwards; }
|
||||
|
||||
/* stagger — svaki red se pojavljuje malo kasnije */
|
||||
.dobavljaci-tabela tbody tr:nth-child(1) { animation-delay: 0.04s; }
|
||||
@@ -56,11 +49,7 @@
|
||||
|
||||
<!-- gornja traka: dugme + pretraga -->
|
||||
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center;">
|
||||
<a href="/dobavljaci/novi"
|
||||
style="padding:8px 16px;background:var(--sb-akcent);color:#fff;border-radius:8px;font-size:14px;font-weight:500;text-decoration:none;white-space:nowrap;transition:opacity 0.2s;"
|
||||
onmouseover="this.style.opacity='0.85'" onmouseout="this.style.opacity='1'">
|
||||
+ Novi dobavljač
|
||||
</a>
|
||||
<a href="/dobavljaci/novi" class="btn-primarno">+ Novi dobavljač</a>
|
||||
<form method="GET" action="/dobavljaci" style="display:flex;gap:8px;flex:1;min-width:200px;">
|
||||
<input type="text" name="pretraga" value="{{.Pretraga}}"
|
||||
placeholder="Pretraži dobavljače..."
|
||||
@@ -86,9 +75,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Dobavljaci}}
|
||||
<tr class="animiraj" style="border-bottom:0.5px solid var(--ivica);transition:background 0.15s;"
|
||||
onmouseover="this.style.background='var(--pozadina)'"
|
||||
onmouseout="this.style.background=''">
|
||||
<tr class="animiraj red-tabele">
|
||||
<td style="padding:12px 16px;font-size:14px;font-weight:500;color:var(--tekst-glavni);">{{.Naziv}}</td>
|
||||
<td style="padding:12px 16px;font-size:13px;color:var(--tekst-sporedni);">
|
||||
{{if .KontaktOsoba}}{{.KontaktOsoba}}{{else}}—{{end}}
|
||||
@@ -104,14 +91,14 @@
|
||||
<a href="/dobavljaci/izmeni/{{.ID}}" class="btn-primarno-malo">
|
||||
Izmeni
|
||||
</a>
|
||||
{{if index $.Dozvole "dobavljac.obrisi"}}
|
||||
<form method="POST" action="/dobavljaci/obrisi/{{.ID}}" style="display:inline;">
|
||||
<button type="submit"
|
||||
onclick="return confirm('Da li ste sigurni da želite da obrišete dobavljača {{.Naziv}}?')"
|
||||
style="padding:4px 10px;background:#dc2626;color:#fff;border:none;border-radius:6px;font-size:12px;cursor:pointer;transition:opacity 0.2s;"
|
||||
onmouseover="this.style.opacity='0.8'" onmouseout="this.style.opacity='1'">
|
||||
<button type="submit" class="btn-obrisi-malo"
|
||||
data-potvrda="Da li ste sigurni da želite da obrišete dobavljača {{.Naziv}}?">
|
||||
Obriši
|
||||
</button>
|
||||
</form>
|
||||
{{end}}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -137,13 +124,14 @@
|
||||
<a href="/dobavljaci/izmeni/{{.ID}}" class="btn-primarno-malo">
|
||||
Izmeni
|
||||
</a>
|
||||
{{if index $.Dozvole "dobavljac.obrisi"}}
|
||||
<form method="POST" action="/dobavljaci/obrisi/{{.ID}}" style="display:inline;">
|
||||
<button type="submit"
|
||||
onclick="return confirm('Da li ste sigurni da želite da obrišete dobavljača {{.Naziv}}?')"
|
||||
style="padding:4px 10px;background:#dc2626;color:#fff;border:none;border-radius:6px;font-size:12px;cursor:pointer;">
|
||||
<button type="submit" class="btn-obrisi-malo"
|
||||
data-potvrda="Da li ste sigurni da želite da obrišete dobavljača {{.Naziv}}?">
|
||||
Obriši
|
||||
</button>
|
||||
</form>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;flex-direction:column;gap:6px;">
|
||||
|
||||
@@ -4,10 +4,6 @@
|
||||
|
||||
{{define "dodatni-css"}}
|
||||
<style>
|
||||
@keyframes slideDown {
|
||||
from { opacity: 0; transform: translateY(-10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.poruka-animacija { animation: slideDown 0.3s ease forwards; }
|
||||
|
||||
.kat-forma-kartica {
|
||||
@@ -41,6 +37,7 @@
|
||||
Nazad na magacin
|
||||
</a>
|
||||
|
||||
{{if index $.Dozvole "kategorija.dodaj"}}
|
||||
<!-- forma za novu kategoriju -->
|
||||
<div class="kartica kat-forma-kartica animiraj">
|
||||
<div style="margin-bottom:16px;padding-bottom:12px;border-bottom:0.5px solid var(--ivica);">
|
||||
@@ -69,6 +66,7 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<!-- lista kategorija -->
|
||||
<div class="kartica" style="padding:0;overflow:hidden;">
|
||||
@@ -83,11 +81,12 @@
|
||||
<div style="font-size:12px;color:var(--tekst-sporedni);margin-top:2px;">{{.Opis}}</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<a href="/magacin/kategorije/obrisi/{{.ID}}"
|
||||
onclick="return confirm('Da li ste sigurni da želite da obrišete ovu kategoriju?')"
|
||||
style="padding:4px 10px;background:#dc2626;color:#fff;border-radius:6px;font-size:12px;text-decoration:none;white-space:nowrap;">
|
||||
{{if index $.Dozvole "kategorija.obrisi"}}
|
||||
<a href="/magacin/kategorije/obrisi/{{.ID}}" class="btn-obrisi-malo"
|
||||
data-potvrda="Da li ste sigurni da želite da obrišete ovu kategoriju?">
|
||||
Obriši
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
<div style="padding:24px;text-align:center;font-size:14px;color:var(--tekst-sporedni);">
|
||||
|
||||
@@ -4,13 +4,7 @@
|
||||
|
||||
{{define "dodatni-css"}}
|
||||
<style>
|
||||
.greska-animacija {
|
||||
animation: shake 0.4s ease;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.forma-grid-2 { grid-template-columns: 1fr !important; }
|
||||
}
|
||||
.greska-animacija { animation: shake 0.4s ease; }
|
||||
</style>
|
||||
{{end}}
|
||||
|
||||
@@ -18,10 +12,8 @@
|
||||
<div style="width:100%;">
|
||||
|
||||
<!-- nazad dugme -->
|
||||
<a href="/klijenti"
|
||||
style="display:inline-flex;align-items:center;gap:6px;font-size:13px;color:var(--tekst-sporedni);text-decoration:none;margin-bottom:20px;transition:color 0.2s;"
|
||||
onmouseover="this.style.color='var(--tekst-glavni)'" onmouseout="this.style.color='var(--tekst-sporedni)'">
|
||||
<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"><polyline points="15 18 9 12 15 6"/></svg>
|
||||
<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>
|
||||
Nazad na klijente
|
||||
</a>
|
||||
|
||||
@@ -33,10 +25,7 @@
|
||||
</div>
|
||||
|
||||
{{if .Greska}}
|
||||
<div class="greska-animacija"
|
||||
style="background:var(--kartica);border:0.5px solid #dc2626;border-radius:8px;padding:10px 14px;margin-bottom:16px;font-size:13px;color:#dc2626;">
|
||||
{{.Greska}}
|
||||
</div>
|
||||
<div class="poruka-greska greska-animacija">{{.Greska}}</div>
|
||||
{{end}}
|
||||
|
||||
<form method="POST" action="{{if .Izmena}}/klijenti/izmeni/{{.Klijent.ID}}{{else}}/klijenti/novi{{end}}">
|
||||
@@ -120,14 +109,8 @@
|
||||
|
||||
<!-- dugmad -->
|
||||
<div style="display:flex;justify-content:flex-end;gap:10px;margin-top:6px;">
|
||||
<a href="/klijenti"
|
||||
style="padding:9px 20px;border:0.5px solid var(--ivica);border-radius:8px;font-size:14px;color:var(--tekst-sporedni);text-decoration:none;transition:background 0.2s;"
|
||||
onmouseover="this.style.background='var(--pozadina)'" onmouseout="this.style.background=''">
|
||||
Odustani
|
||||
</a>
|
||||
<button type="submit"
|
||||
style="padding:9px 20px;background:var(--sb-akcent);color:#fff;border:none;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;transition:opacity 0.2s;"
|
||||
onmouseover="this.style.opacity='0.85'" onmouseout="this.style.opacity='1'">
|
||||
<a href="/klijenti" class="btn-sekundarno">Odustani</a>
|
||||
<button type="submit" class="btn-primarno">
|
||||
{{if .Izmena}}Sačuvaj izmene{{else}}Dodaj klijenta{{end}}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -4,14 +4,7 @@
|
||||
|
||||
{{define "dodatni-css"}}
|
||||
<style>
|
||||
@keyframes slideDown {
|
||||
from { opacity: 0; transform: translateY(-10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.poruka-animacija {
|
||||
animation: slideDown 0.3s ease forwards;
|
||||
}
|
||||
.poruka-animacija { animation: slideDown 0.3s ease forwards; }
|
||||
|
||||
.klijenti-tabela tbody tr:nth-child(1) { animation-delay: 0.04s; }
|
||||
.klijenti-tabela tbody tr:nth-child(2) { animation-delay: 0.08s; }
|
||||
@@ -55,11 +48,7 @@
|
||||
|
||||
<!-- gornja traka: dugme + pretraga -->
|
||||
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center;">
|
||||
<a href="/klijenti/novi"
|
||||
style="padding:8px 16px;background:var(--sb-akcent);color:#fff;border-radius:8px;font-size:14px;font-weight:500;text-decoration:none;white-space:nowrap;transition:opacity 0.2s;"
|
||||
onmouseover="this.style.opacity='0.85'" onmouseout="this.style.opacity='1'">
|
||||
+ Novi klijent
|
||||
</a>
|
||||
<a href="/klijenti/novi" class="btn-primarno">+ Novi klijent</a>
|
||||
<form method="GET" action="/klijenti" style="display:flex;gap:8px;flex:1;min-width:200px;">
|
||||
<input type="text" name="pretraga" value="{{.Pretraga}}"
|
||||
placeholder="Pretraži po imenu ili nazivu firme..."
|
||||
@@ -85,9 +74,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Klijenti}}
|
||||
<tr class="animiraj" style="border-bottom:0.5px solid var(--ivica);transition:background 0.15s;"
|
||||
onmouseover="this.style.background='var(--pozadina)'"
|
||||
onmouseout="this.style.background=''">
|
||||
<tr class="animiraj red-tabele">
|
||||
<td style="padding:12px 16px;">
|
||||
{{if .NazivFirme}}
|
||||
<div style="font-size:14px;font-weight:500;color:var(--tekst-glavni);">{{.NazivFirme}}</div>
|
||||
@@ -112,14 +99,14 @@
|
||||
<a href="/klijenti/izmeni/{{.ID}}" class="btn-primarno-malo">
|
||||
Izmeni
|
||||
</a>
|
||||
{{if index $.Dozvole "klijent.obrisi"}}
|
||||
<form method="POST" action="/klijenti/obrisi/{{.ID}}" style="display:inline;">
|
||||
<button type="submit"
|
||||
onclick="return confirm('Da li ste sigurni da želite da obrišete klijenta?')"
|
||||
style="padding:4px 10px;background:#dc2626;color:#fff;border:none;border-radius:6px;font-size:12px;cursor:pointer;transition:opacity 0.2s;"
|
||||
onmouseover="this.style.opacity='0.8'" onmouseout="this.style.opacity='1'">
|
||||
<button type="submit" class="btn-obrisi-malo"
|
||||
data-potvrda="Da li ste sigurni da želite da obrišete klijenta?">
|
||||
Obriši
|
||||
</button>
|
||||
</form>
|
||||
{{end}}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -154,13 +141,14 @@
|
||||
<a href="/klijenti/izmeni/{{.ID}}" class="btn-primarno-malo">
|
||||
Izmeni
|
||||
</a>
|
||||
{{if index $.Dozvole "klijent.obrisi"}}
|
||||
<form method="POST" action="/klijenti/obrisi/{{.ID}}" style="display:inline;">
|
||||
<button type="submit"
|
||||
onclick="return confirm('Da li ste sigurni da želite da obrišete klijenta?')"
|
||||
style="padding:4px 10px;background:#dc2626;color:#fff;border:none;border-radius:6px;font-size:12px;cursor:pointer;">
|
||||
<button type="submit" class="btn-obrisi-malo"
|
||||
data-potvrda="Da li ste sigurni da želite da obrišete klijenta?">
|
||||
Obriši
|
||||
</button>
|
||||
</form>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;flex-direction:column;gap:6px;">
|
||||
|
||||
@@ -14,6 +14,23 @@
|
||||
.magacin-tabela tbody tr:nth-child(8) { animation-delay: 0.32s; }
|
||||
.magacin-tabela tbody tr:nth-child(9) { animation-delay: 0.36s; }
|
||||
.magacin-tabela tbody tr:nth-child(10) { animation-delay: 0.40s; }
|
||||
|
||||
.magacin-kartice {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.magacin-kartica:nth-child(1) { animation-delay: 0.04s; }
|
||||
.magacin-kartica:nth-child(2) { animation-delay: 0.10s; }
|
||||
.magacin-kartica:nth-child(3) { animation-delay: 0.16s; }
|
||||
.magacin-kartica:nth-child(4) { animation-delay: 0.22s; }
|
||||
.magacin-kartica:nth-child(5) { animation-delay: 0.28s; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.magacin-tabela { display: none; }
|
||||
.magacin-kartice { display: flex; }
|
||||
}
|
||||
</style>
|
||||
{{end}}
|
||||
|
||||
@@ -29,13 +46,8 @@
|
||||
|
||||
<!-- dugmad -->
|
||||
<div style="display:flex;gap:10px;flex-wrap:wrap;">
|
||||
<a href="/magacin/novi"
|
||||
style="padding:8px 16px;background:var(--sb-akcent);color:#fff;border-radius:8px;font-size:14px;font-weight:500;text-decoration:none;white-space:nowrap;">
|
||||
+ Novi artikal
|
||||
</a>
|
||||
<a href="/magacin/kategorije" class="btn-primarno">
|
||||
Kategorije
|
||||
</a>
|
||||
<a href="/magacin/novi" class="btn-primarno">+ Novi artikal</a>
|
||||
<a href="/magacin/kategorije" class="btn-primarno">Kategorije</a>
|
||||
</div>
|
||||
|
||||
<!-- pretraga i filteri -->
|
||||
@@ -76,9 +88,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Artikli}}
|
||||
<tr class="animiraj" style="border-bottom:0.5px solid var(--ivica);transition:background 0.15s;"
|
||||
onmouseover="this.style.background='var(--pozadina)'"
|
||||
onmouseout="this.style.background=''">
|
||||
<tr class="animiraj red-tabele">
|
||||
<td style="padding:12px 16px;font-size:14px;color:var(--tekst-glavni);">{{.Naziv}}</td>
|
||||
<td style="padding:12px 16px;font-size:13px;color:var(--tekst-sporedni);">
|
||||
{{if .KategorijaNaziv}}{{.KategorijaNaziv}}{{else}}—{{end}}
|
||||
@@ -97,11 +107,12 @@
|
||||
<a href="/magacin/izmeni/{{.ID}}" class="btn-primarno-malo">
|
||||
Izmeni
|
||||
</a>
|
||||
<a href="/magacin/obrisi/{{.ID}}"
|
||||
onclick="return confirm('Da li ste sigurni da želite da obrišete ovaj artikal?')"
|
||||
style="padding:4px 10px;background:#dc2626;color:#fff;border-radius:6px;font-size:12px;text-decoration:none;">
|
||||
{{if index $.Dozvole "artikal.obrisi"}}
|
||||
<a href="/magacin/obrisi/{{.ID}}" class="btn-obrisi-malo"
|
||||
data-potvrda="Da li ste sigurni da želite da obrišete ovaj artikal?">
|
||||
Obriši
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -117,5 +128,48 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- mobilne kartice -->
|
||||
<div class="magacin-kartice">
|
||||
{{range .Artikli}}
|
||||
<div class="kartica magacin-kartica animiraj">
|
||||
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:12px;margin-bottom:10px;">
|
||||
<div>
|
||||
<div style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">{{.Naziv}}</div>
|
||||
{{if .KategorijaNaziv}}
|
||||
<div style="font-size:12px;color:var(--tekst-sporedni);margin-top:2px;">{{.KategorijaNaziv}}</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;flex-shrink:0;">
|
||||
<a href="/magacin/izmeni/{{.ID}}" class="btn-primarno-malo">Izmeni</a>
|
||||
{{if index $.Dozvole "artikal.obrisi"}}
|
||||
<a href="/magacin/obrisi/{{.ID}}" class="btn-obrisi-malo"
|
||||
data-potvrda="Da li ste sigurni da želite da obrišete ovaj artikal?">
|
||||
Obriši
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:10px;">
|
||||
<div style="font-size:13px;color:var(--tekst-sporedni);">
|
||||
<span style="color:var(--tekst-glavni);font-weight:500;">Količina:</span>
|
||||
<span style="font-weight:500;color:{{if .KriticnaZaliha}}#dc2626{{else}}#16a34a{{end}};">{{.Kolicina}}</span>
|
||||
</div>
|
||||
<div style="font-size:13px;color:var(--tekst-sporedni);">
|
||||
<span style="color:var(--tekst-glavni);font-weight:500;">Cena:</span> {{printf "%.0f" .ProdajnaCena}} din
|
||||
</div>
|
||||
{{if .Lokacija}}
|
||||
<div style="font-size:13px;color:var(--tekst-sporedni);">
|
||||
<span style="color:var(--tekst-glavni);font-weight:500;">Lokacija:</span> {{.Lokacija}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div style="padding:32px;text-align:center;font-size:14px;color:var(--tekst-sporedni);">
|
||||
Nema artikala. <a href="/magacin/novi" style="color:var(--sb-akcent);">Dodaj prvi artikal.</a>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
@@ -2,12 +2,18 @@
|
||||
|
||||
{{define "naslov"}}{{if .Izmena}}Izmeni artikal{{else}}Novi artikal{{end}} — NTech{{end}}
|
||||
|
||||
{{define "dodatni-css"}}
|
||||
<style>
|
||||
.greska-animacija { animation: shake 0.4s ease; }
|
||||
</style>
|
||||
{{end}}
|
||||
|
||||
{{define "sadrzaj"}}
|
||||
<div style="width:100%;">
|
||||
|
||||
<!-- nazad dugme -->
|
||||
<a href="/magacin" style="display:inline-flex;align-items:center;gap:6px;font-size:13px;color:var(--tekst-sporedni);text-decoration:none;margin-bottom:20px;">
|
||||
<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"><polyline points="15 18 9 12 15 6"/></svg>
|
||||
<a href="/magacin" class="nazad-link">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="15 18 9 12 15 6"/></svg>
|
||||
Nazad na magacin
|
||||
</a>
|
||||
|
||||
@@ -19,9 +25,7 @@
|
||||
</div>
|
||||
|
||||
{{if .Greska}}
|
||||
<div style="background:var(--kartica);border:0.5px solid #dc2626;border-radius:8px;padding:10px 14px;margin-bottom:16px;font-size:13px;color:#dc2626;">
|
||||
{{.Greska}}
|
||||
</div>
|
||||
<div class="poruka-greska greska-animacija">{{.Greska}}</div>
|
||||
{{end}}
|
||||
|
||||
<form method="POST" action="{{if .Izmena}}/magacin/izmeni/{{.Artikal.ID}}{{else}}/magacin/novi{{end}}">
|
||||
@@ -91,12 +95,8 @@
|
||||
|
||||
<!-- dugmad -->
|
||||
<div style="display:flex;justify-content:flex-end;gap:10px;margin-top:6px;">
|
||||
<a href="/magacin"
|
||||
style="padding:9px 20px;border:0.5px solid var(--ivica);border-radius:8px;font-size:14px;color:var(--tekst-sporedni);text-decoration:none;">
|
||||
Odustani
|
||||
</a>
|
||||
<button type="submit"
|
||||
style="padding:9px 20px;background:var(--sb-akcent);color:#fff;border:none;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;">
|
||||
<a href="/magacin" class="btn-sekundarno">Odustani</a>
|
||||
<button type="submit" class="btn-primarno">
|
||||
{{if .Izmena}}Sačuvaj izmene{{else}}Dodaj artikal{{end}}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -31,10 +31,8 @@
|
||||
<div style="display:flex;flex-direction:column;gap:16px;">
|
||||
|
||||
<!-- nazad dugme -->
|
||||
<a href="/nabavke"
|
||||
style="display:inline-flex;align-items:center;gap:6px;font-size:13px;color:var(--tekst-sporedni);text-decoration:none;transition:color 0.2s;"
|
||||
onmouseover="this.style.color='var(--tekst-glavni)'" onmouseout="this.style.color='var(--tekst-sporedni)'">
|
||||
<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"><polyline points="15 18 9 12 15 6"/></svg>
|
||||
<a href="/nabavke" class="nazad-link">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="15 18 9 12 15 6"/></svg>
|
||||
Nazad na nabavke
|
||||
</a>
|
||||
|
||||
@@ -86,9 +84,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Stavke}}
|
||||
<tr class="animiraj" style="border-bottom:0.5px solid var(--ivica);transition:background 0.15s;"
|
||||
onmouseover="this.style.background='var(--pozadina)'"
|
||||
onmouseout="this.style.background=''">
|
||||
<tr class="animiraj red-tabele">
|
||||
<td style="padding:10px 16px;font-size:14px;color:var(--tekst-glavni);">{{.ArtikalNaziv}}</td>
|
||||
<td style="padding:10px 16px;text-align:center;font-size:14px;color:var(--tekst-glavni);">{{.Kolicina}}</td>
|
||||
<td style="padding:10px 16px;text-align:right;font-size:14px;color:var(--tekst-sporedni);">{{printf "%.2f" .CenaPoKomadu}} din</td>
|
||||
@@ -152,10 +148,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<form method="POST" action="/nabavke/obrisi/{{.Nabavka.ID}}">
|
||||
<button type="submit"
|
||||
onclick="return confirm('Da li ste sigurni da želite da obrišete ovu nabavku?\n\nBrisanje ne vraća količine artikala u magacin.')"
|
||||
style="padding:9px 20px;background:#dc2626;color:#fff;border:none;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;white-space:nowrap;transition:opacity 0.2s;"
|
||||
onmouseover="this.style.opacity='0.85'" onmouseout="this.style.opacity='1'">
|
||||
<button type="submit" class="btn-primarno" style="background:#dc2626;"
|
||||
data-potvrda="Da li ste sigurni da želite da obrišete ovu nabavku? Brisanje ne vraća količine artikala u magacin.">
|
||||
Obriši nabavku
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
params.append('naziv', this.modalNaziv.trim());
|
||||
if (this.modalKategorijaID) params.append('kategorija_id', this.modalKategorijaID);
|
||||
if (this.modalCena) params.append('prodajna_cena', this.modalCena);
|
||||
params.append('_csrf', document.querySelector('meta[name="csrf-token"]')?.content || '');
|
||||
params.append('_csrf', document.querySelector('meta[name=csrf-token]')?.content || '');
|
||||
|
||||
try {
|
||||
const odgovor = await fetch('/magacin/novi', {
|
||||
@@ -113,20 +113,15 @@
|
||||
}">
|
||||
|
||||
<!-- nazad dugme -->
|
||||
<a href="/nabavke"
|
||||
style="display:inline-flex;align-items:center;gap:6px;font-size:13px;color:var(--tekst-sporedni);text-decoration:none;margin-bottom:20px;transition:color 0.2s;"
|
||||
onmouseover="this.style.color='var(--tekst-glavni)'" onmouseout="this.style.color='var(--tekst-sporedni)'">
|
||||
<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"><polyline points="15 18 9 12 15 6"/></svg>
|
||||
<a href="/nabavke" class="nazad-link">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="15 18 9 12 15 6"/></svg>
|
||||
Nazad na nabavke
|
||||
</a>
|
||||
|
||||
<form method="POST" action="/nabavke/nova">
|
||||
|
||||
{{if .Greska}}
|
||||
<div class="greska-animacija"
|
||||
style="background:var(--kartica);border:0.5px solid #dc2626;border-radius:8px;padding:10px 14px;margin-bottom:16px;font-size:13px;color:#dc2626;">
|
||||
{{.Greska}}
|
||||
</div>
|
||||
<div class="poruka-greska greska-animacija">{{.Greska}}</div>
|
||||
{{end}}
|
||||
|
||||
<!-- zaglavlje nabavke -->
|
||||
@@ -275,16 +270,8 @@
|
||||
|
||||
<!-- dugmad forme -->
|
||||
<div style="display:flex;justify-content:flex-end;gap:10px;">
|
||||
<a href="/nabavke"
|
||||
style="padding:9px 20px;border:0.5px solid var(--ivica);border-radius:8px;font-size:14px;color:var(--tekst-sporedni);text-decoration:none;transition:background 0.2s;"
|
||||
onmouseover="this.style.background='var(--pozadina)'" onmouseout="this.style.background=''">
|
||||
Odustani
|
||||
</a>
|
||||
<button type="submit"
|
||||
style="padding:9px 20px;background:var(--sb-akcent);color:#fff;border:none;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;transition:opacity 0.2s;"
|
||||
onmouseover="this.style.opacity='0.85'" onmouseout="this.style.opacity='1'">
|
||||
Sačuvaj nabavku
|
||||
</button>
|
||||
<a href="/nabavke" class="btn-sekundarno">Odustani</a>
|
||||
<button type="submit" class="btn-primarno">Sačuvaj nabavku</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
@@ -301,14 +288,11 @@
|
||||
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:20px;padding-bottom:14px;border-bottom:0.5px solid var(--ivica);">
|
||||
<span style="font-size:16px;font-weight:500;color:var(--tekst-glavni);">Novi artikal</span>
|
||||
<button type="button" @click="zatvoriModal()"
|
||||
style="background:none;border:none;cursor:pointer;color:var(--tekst-sporedni);font-size:20px;line-height:1;padding:2px 6px;border-radius:4px;transition:background 0.15s;"
|
||||
onmouseover="this.style.background='var(--pozadina)'" onmouseout="this.style.background='none'">×</button>
|
||||
<button type="button" @click="zatvoriModal()" aria-label="Zatvori"
|
||||
style="background:none;border:none;cursor:pointer;color:var(--tekst-sporedni);font-size:20px;line-height:1;padding:2px 6px;border-radius:4px;transition:background 0.15s;">×</button>
|
||||
</div>
|
||||
|
||||
<div x-show="modalGreska" class="greska-animacija"
|
||||
style="background:var(--kartica);border:0.5px solid #dc2626;border-radius:8px;padding:10px 14px;margin-bottom:14px;font-size:13px;color:#dc2626;"
|
||||
x-text="modalGreska"></div>
|
||||
<div x-show="modalGreska" class="poruka-greska greska-animacija" x-text="modalGreska"></div>
|
||||
|
||||
<div style="display:flex;flex-direction:column;gap:14px;">
|
||||
<div>
|
||||
@@ -340,15 +324,9 @@
|
||||
</div>
|
||||
|
||||
<div style="display:flex;justify-content:flex-end;gap:10px;margin-top:20px;padding-top:14px;border-top:0.5px solid var(--ivica);">
|
||||
<button type="button" @click="zatvoriModal()"
|
||||
style="padding:9px 20px;border:0.5px solid var(--ivica);border-radius:8px;font-size:14px;color:var(--tekst-sporedni);background:none;cursor:pointer;transition:background 0.2s;"
|
||||
onmouseover="this.style.background='var(--pozadina)'" onmouseout="this.style.background='none'">
|
||||
Odustani
|
||||
</button>
|
||||
<button type="button" @click="sacuvajArtikal()" :disabled="modalUcitavanje"
|
||||
style="padding:9px 20px;background:var(--sb-akcent);color:#fff;border:none;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;transition:opacity 0.2s;"
|
||||
:style="modalUcitavanje ? 'opacity:0.6;cursor:not-allowed' : ''"
|
||||
@mouseover="if(!modalUcitavanje) $el.style.opacity='0.85'" @mouseout="$el.style.opacity='1'">
|
||||
<button type="button" @click="zatvoriModal()" class="btn-sekundarno">Odustani</button>
|
||||
<button type="button" @click="sacuvajArtikal()" :disabled="modalUcitavanje" class="btn-primarno"
|
||||
:style="modalUcitavanje ? 'opacity:0.6;cursor:not-allowed' : ''">
|
||||
<span x-text="modalUcitavanje ? 'Čuvanje...' : 'Dodaj artikal'"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -4,14 +4,7 @@
|
||||
|
||||
{{define "dodatni-css"}}
|
||||
<style>
|
||||
@keyframes slideDown {
|
||||
from { opacity: 0; transform: translateY(-10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.poruka-animacija {
|
||||
animation: slideDown 0.3s ease forwards;
|
||||
}
|
||||
.poruka-animacija { animation: slideDown 0.3s ease forwards; }
|
||||
|
||||
.nabavke-tabela tbody tr:nth-child(1) { animation-delay: 0.04s; }
|
||||
.nabavke-tabela tbody tr:nth-child(2) { animation-delay: 0.08s; }
|
||||
@@ -55,11 +48,7 @@
|
||||
|
||||
<!-- dugme nova nabavka -->
|
||||
<div>
|
||||
<a href="/nabavke/nova"
|
||||
style="display:inline-block;padding:8px 16px;background:var(--sb-akcent);color:#fff;border-radius:8px;font-size:14px;font-weight:500;text-decoration:none;transition:opacity 0.2s;"
|
||||
onmouseover="this.style.opacity='0.85'" onmouseout="this.style.opacity='1'">
|
||||
+ Nova nabavka
|
||||
</a>
|
||||
<a href="/nabavke/nova" class="btn-primarno">+ Nova nabavka</a>
|
||||
</div>
|
||||
|
||||
<!-- desktop tabela -->
|
||||
@@ -77,9 +66,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Nabavke}}
|
||||
<tr class="animiraj" style="border-bottom:0.5px solid var(--ivica);transition:background 0.15s;"
|
||||
onmouseover="this.style.background='var(--pozadina)'"
|
||||
onmouseout="this.style.background=''">
|
||||
<tr class="animiraj red-tabele">
|
||||
<td style="padding:12px 16px;font-size:13px;color:var(--tekst-sporedni);white-space:nowrap;">
|
||||
{{.Datum.Format "02.01.2006."}}
|
||||
</td>
|
||||
@@ -97,14 +84,14 @@
|
||||
<a href="/nabavke/{{.ID}}" class="btn-primarno-malo">
|
||||
Detalji
|
||||
</a>
|
||||
{{if index $.Dozvole "nabavka.obrisi"}}
|
||||
<form method="POST" action="/nabavke/obrisi/{{.ID}}" style="display:inline;">
|
||||
<button type="submit"
|
||||
onclick="return confirm('Da li ste sigurni?\n\nBrisanje nabavke ne vraća količine artikala u magacin.')"
|
||||
style="padding:4px 10px;background:#dc2626;color:#fff;border:none;border-radius:6px;font-size:12px;cursor:pointer;transition:opacity 0.2s;"
|
||||
onmouseover="this.style.opacity='0.8'" onmouseout="this.style.opacity='1'">
|
||||
<button type="submit" class="btn-obrisi-malo"
|
||||
data-potvrda="Da li ste sigurni? Brisanje nabavke ne vraća količine artikala u magacin.">
|
||||
Obriši
|
||||
</button>
|
||||
</form>
|
||||
{{end}}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -144,13 +131,14 @@
|
||||
<a href="/nabavke/{{.ID}}" class="btn-primarno-malo" style="flex:1;justify-content:center;">
|
||||
Detalji
|
||||
</a>
|
||||
{{if index $.Dozvole "nabavka.obrisi"}}
|
||||
<form method="POST" action="/nabavke/obrisi/{{.ID}}" style="flex:1;">
|
||||
<button type="submit"
|
||||
onclick="return confirm('Da li ste sigurni?\n\nBrisanje nabavke ne vraća količine artikala u magacin.')"
|
||||
style="width:100%;padding:7px;background:#dc2626;color:#fff;border:none;border-radius:6px;font-size:13px;cursor:pointer;">
|
||||
<button type="submit" class="btn-obrisi-malo" style="width:100%;justify-content:center;"
|
||||
data-potvrda="Da li ste sigurni? Brisanje nabavke ne vraća količine artikala u magacin.">
|
||||
Obriši
|
||||
</button>
|
||||
</form>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
|
||||
@@ -4,14 +4,7 @@
|
||||
|
||||
{{define "dodatni-css"}}
|
||||
<style>
|
||||
@keyframes slideDown {
|
||||
from { opacity: 0; transform: translateY(-10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.poruka-animacija {
|
||||
animation: slideDown 0.3s ease forwards;
|
||||
}
|
||||
.poruka-animacija { animation: slideDown 0.3s ease forwards; }
|
||||
|
||||
.pod-tabela tbody tr:nth-child(1) { animation-delay: 0.04s; }
|
||||
.pod-tabela tbody tr:nth-child(2) { animation-delay: 0.08s; }
|
||||
@@ -59,11 +52,7 @@
|
||||
|
||||
<!-- gornja traka -->
|
||||
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center;">
|
||||
<a href="/podsetnici/novi"
|
||||
style="padding:8px 16px;background:var(--sb-akcent);color:#fff;border-radius:8px;font-size:14px;font-weight:500;text-decoration:none;white-space:nowrap;transition:opacity 0.2s;"
|
||||
onmouseover="this.style.opacity='0.85'" onmouseout="this.style.opacity='1'">
|
||||
+ Novi podsetnik
|
||||
</a>
|
||||
<a href="/podsetnici/novi" class="btn-primarno">+ Novi podsetnik</a>
|
||||
<form method="GET" action="/podsetnici" style="display:flex;gap:8px;align-items:center;flex:1;min-width:200px;flex-wrap:wrap;">
|
||||
<label style="display:flex;align-items:center;gap:6px;font-size:14px;color:var(--tekst-sporedni);cursor:pointer;white-space:nowrap;">
|
||||
<input type="checkbox" name="samo_aktivni" value="1" {{if .SamoAktivni}}checked{{end}}
|
||||
@@ -88,10 +77,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Podsetnici}}
|
||||
<tr class="animiraj {{if .JePrekoracen}}red-prekoracen{{end}}"
|
||||
style="border-bottom:0.5px solid var(--ivica);transition:background 0.15s;"
|
||||
onmouseover="this.style.background='var(--pozadina)'"
|
||||
onmouseout="this.style.background=''">
|
||||
<tr class="animiraj red-tabele {{if .JePrekoracen}}red-prekoracen{{end}}">
|
||||
<td style="padding:12px 16px;">
|
||||
<div style="font-size:14px;font-weight:500;color:var(--tekst-glavni);">{{.Naslov}}</div>
|
||||
{{if .Napomena}}
|
||||
@@ -112,23 +98,15 @@
|
||||
</td>
|
||||
<td style="padding:12px 16px;text-align:center;">
|
||||
<div style="display:flex;align-items:center;justify-content:center;gap:8px;">
|
||||
<a href="/podsetnici/izmeni/{{.ID}}"
|
||||
style="padding:4px 10px;background:var(--sb-aktivan);color:var(--tekst-jak);border-radius:6px;font-size:12px;text-decoration:none;transition:opacity 0.2s;"
|
||||
onmouseover="this.style.opacity='0.8'" onmouseout="this.style.opacity='1'">
|
||||
Izmeni
|
||||
</a>
|
||||
<a href="/podsetnici/izmeni/{{.ID}}" class="btn-primarno-malo">Izmeni</a>
|
||||
<form method="POST" action="/podsetnici/zavrseno/{{.ID}}" style="display:inline;">
|
||||
<button type="submit"
|
||||
style="padding:4px 10px;background:var(--sb-aktivan);color:var(--tekst-jak);border:none;border-radius:6px;font-size:12px;cursor:pointer;transition:opacity 0.2s;"
|
||||
onmouseover="this.style.opacity='0.8'" onmouseout="this.style.opacity='1'">
|
||||
<button type="submit" class="btn-primarno-malo">
|
||||
{{if .Zavrseno}}Aktiviraj{{else}}Završi{{end}}
|
||||
</button>
|
||||
</form>
|
||||
<form method="POST" action="/podsetnici/obrisi/{{.ID}}" style="display:inline;">
|
||||
<button type="submit"
|
||||
onclick="return confirm('Da li ste sigurni da želite da obrišete podsetnik?')"
|
||||
style="padding:4px 10px;background:#dc2626;color:#fff;border:none;border-radius:6px;font-size:12px;cursor:pointer;transition:opacity 0.2s;"
|
||||
onmouseover="this.style.opacity='0.8'" onmouseout="this.style.opacity='1'">
|
||||
<button type="submit" class="btn-obrisi-malo"
|
||||
data-potvrda="Da li ste sigurni da želite da obrišete podsetnik?">
|
||||
Obriši
|
||||
</button>
|
||||
</form>
|
||||
@@ -172,20 +150,15 @@
|
||||
{{.DatumPodsecanja.Format "02.01.2006."}}
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap;">
|
||||
<a href="/podsetnici/izmeni/{{.ID}}"
|
||||
style="padding:6px 14px;background:var(--sb-aktivan);color:var(--tekst-jak);border-radius:6px;font-size:13px;text-decoration:none;">
|
||||
Izmeni
|
||||
</a>
|
||||
<a href="/podsetnici/izmeni/{{.ID}}" class="btn-primarno-malo">Izmeni</a>
|
||||
<form method="POST" action="/podsetnici/zavrseno/{{.ID}}" style="display:inline;">
|
||||
<button type="submit"
|
||||
style="padding:6px 14px;background:var(--sb-aktivan);color:var(--tekst-jak);border:none;border-radius:6px;font-size:13px;cursor:pointer;">
|
||||
<button type="submit" class="btn-primarno-malo">
|
||||
{{if .Zavrseno}}Aktiviraj{{else}}Završi{{end}}
|
||||
</button>
|
||||
</form>
|
||||
<form method="POST" action="/podsetnici/obrisi/{{.ID}}" style="display:inline;">
|
||||
<button type="submit"
|
||||
onclick="return confirm('Da li ste sigurni da želite da obrišete podsetnik?')"
|
||||
style="padding:6px 14px;background:#dc2626;color:#fff;border:none;border-radius:6px;font-size:13px;cursor:pointer;">
|
||||
<button type="submit" class="btn-obrisi-malo"
|
||||
data-potvrda="Da li ste sigurni da želite da obrišete podsetnik?">
|
||||
Obriši
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -13,10 +13,8 @@
|
||||
{{define "sadrzaj"}}
|
||||
<div style="width:100%;">
|
||||
|
||||
<a href="/podsetnici"
|
||||
style="display:inline-flex;align-items:center;gap:6px;font-size:13px;color:var(--tekst-sporedni);text-decoration:none;margin-bottom:20px;transition:color 0.2s;"
|
||||
onmouseover="this.style.color='var(--tekst-glavni)'" onmouseout="this.style.color='var(--tekst-sporedni)'">
|
||||
<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"><polyline points="15 18 9 12 15 6"/></svg>
|
||||
<a href="/podsetnici" class="nazad-link">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="15 18 9 12 15 6"/></svg>
|
||||
Nazad na podsetnik
|
||||
</a>
|
||||
|
||||
@@ -28,10 +26,7 @@
|
||||
</div>
|
||||
|
||||
{{if .Greska}}
|
||||
<div class="greska-animacija"
|
||||
style="background:var(--kartica);border:0.5px solid var(--greska);border-radius:8px;padding:10px 14px;margin-bottom:16px;font-size:13px;color:var(--greska);">
|
||||
{{.Greska}}
|
||||
</div>
|
||||
<div class="poruka-greska greska-animacija">{{.Greska}}</div>
|
||||
{{end}}
|
||||
|
||||
<form method="POST" action="{{if .Izmena}}/podsetnici/izmeni/{{.Podsetnik.ID}}{{else}}/podsetnici/novi{{end}}">
|
||||
@@ -63,14 +58,8 @@
|
||||
</div>
|
||||
|
||||
<div style="display:flex;justify-content:flex-end;gap:10px;margin-top:4px;">
|
||||
<a href="/podsetnici"
|
||||
style="padding:9px 20px;border:0.5px solid var(--ivica);border-radius:8px;font-size:14px;color:var(--tekst-sporedni);text-decoration:none;transition:background 0.2s;"
|
||||
onmouseover="this.style.background='var(--pozadina)'" onmouseout="this.style.background=''">
|
||||
Odustani
|
||||
</a>
|
||||
<button type="submit"
|
||||
style="padding:9px 20px;background:var(--sb-akcent);color:#fff;border:none;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;transition:opacity 0.2s;"
|
||||
onmouseover="this.style.opacity='0.85'" onmouseout="this.style.opacity='1'">
|
||||
<a href="/podsetnici" class="btn-sekundarno">Odustani</a>
|
||||
<button type="submit" class="btn-primarno">
|
||||
{{if .Izmena}}Sačuvaj izmene{{else}}Dodaj podsetnik{{end}}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -4,11 +4,6 @@
|
||||
|
||||
{{define "dodatni-css"}}
|
||||
<style>
|
||||
@keyframes slideDown {
|
||||
from { opacity: 0; transform: translateY(-10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.poruka-animacija { animation: slideDown 0.3s ease forwards; }
|
||||
|
||||
.prodaja-tabela tbody tr:nth-child(1) { animation-delay: 0.04s; }
|
||||
@@ -57,24 +52,12 @@
|
||||
<input type="text" name="pretraga" value="{{.Pretraga}}"
|
||||
placeholder="Pretraži po broju naloga..."
|
||||
style="flex:1;">
|
||||
<button type="submit"
|
||||
style="padding:8px 14px;background:var(--kartica);border:0.5px solid var(--ivica);border-radius:8px;font-size:13px;color:var(--tekst-sporedni);cursor:pointer;white-space:nowrap;transition:background 0.2s;"
|
||||
onmouseover="this.style.background='var(--pozadina)'" onmouseout="this.style.background='var(--kartica)'">
|
||||
Pretraži
|
||||
</button>
|
||||
<button type="submit" class="btn-sekundarno" style="white-space:nowrap;">Pretraži</button>
|
||||
{{if .Pretraga}}
|
||||
<a href="/prodaja"
|
||||
style="padding:8px 14px;border:0.5px solid var(--ivica);border-radius:8px;font-size:13px;color:var(--tekst-sporedni);text-decoration:none;white-space:nowrap;transition:background 0.2s;"
|
||||
onmouseover="this.style.background='var(--pozadina)'" onmouseout="this.style.background=''">
|
||||
✕ Resetuj
|
||||
</a>
|
||||
<a href="/prodaja" class="btn-sekundarno" style="white-space:nowrap;">✕ Resetuj</a>
|
||||
{{end}}
|
||||
</form>
|
||||
<a href="/prodaja/nova"
|
||||
style="padding:8px 16px;background:var(--sb-akcent);color:#fff;border-radius:8px;font-size:14px;font-weight:500;text-decoration:none;white-space:nowrap;transition:opacity 0.2s;"
|
||||
onmouseover="this.style.opacity='0.85'" onmouseout="this.style.opacity='1'">
|
||||
+ Nova prodaja
|
||||
</a>
|
||||
<a href="/prodaja/nova" class="btn-primarno">+ Nova prodaja</a>
|
||||
</div>
|
||||
|
||||
<!-- desktop tabela -->
|
||||
@@ -92,9 +75,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Nalozi}}
|
||||
<tr class="animiraj" style="border-bottom:0.5px solid var(--ivica);transition:background 0.15s;"
|
||||
onmouseover="this.style.background='var(--pozadina)'"
|
||||
onmouseout="this.style.background=''">
|
||||
<tr class="animiraj red-tabele">
|
||||
<td style="padding:12px 16px;font-size:13px;font-family:monospace;color:var(--tekst-glavni);">
|
||||
{{.BrojNaloga}}
|
||||
</td>
|
||||
|
||||
@@ -20,10 +20,8 @@
|
||||
{{end}}
|
||||
|
||||
<!-- nazad dugme -->
|
||||
<a href="/prodaja"
|
||||
style="display:inline-flex;align-items:center;gap:6px;font-size:13px;color:var(--tekst-sporedni);text-decoration:none;transition:color 0.2s;"
|
||||
onmouseover="this.style.color='var(--tekst-glavni)'" onmouseout="this.style.color='var(--tekst-sporedni)'">
|
||||
<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"><polyline points="15 18 9 12 15 6"/></svg>
|
||||
<a href="/prodaja" class="nazad-link">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="15 18 9 12 15 6"/></svg>
|
||||
Nazad na prodaju
|
||||
</a>
|
||||
|
||||
@@ -33,10 +31,8 @@
|
||||
<span style="font-size:20px;font-weight:600;color:var(--tekst-glavni);font-family:monospace;">
|
||||
{{.Nalog.BrojNaloga}}
|
||||
</span>
|
||||
<a href="/prodaja/{{.Nalog.ID}}/stampa" target="_blank"
|
||||
style="display:inline-flex;align-items:center;gap:6px;padding:8px 16px;background:var(--kartica);border:0.5px solid var(--ivica);border-radius:8px;font-size:13px;color:var(--tekst-sporedni);text-decoration:none;transition:background 0.2s;"
|
||||
onmouseover="this.style.background='var(--pozadina)'" onmouseout="this.style.background='var(--kartica)'">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"/><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect x="6" y="14" width="12" height="8"/></svg>
|
||||
<a href="/prodaja/{{.Nalog.ID}}/stampa" target="_blank" class="btn-sekundarno" style="gap:6px;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="6 9 6 2 18 2 18 9"/><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect x="6" y="14" width="12" height="8"/></svg>
|
||||
Štampaj
|
||||
</a>
|
||||
</div>
|
||||
@@ -99,6 +95,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{if index $.Dozvole "prodaja.obrisi"}}
|
||||
<!-- zona za brisanje -->
|
||||
<div class="kartica detalji-kartica animiraj" style="border-color:#dc262633;">
|
||||
<div style="display:flex;align-items:flex-start;gap:12px;flex-wrap:wrap;">
|
||||
@@ -109,15 +106,14 @@
|
||||
</div>
|
||||
</div>
|
||||
<form method="POST" action="/prodaja/obrisi/{{.Nalog.ID}}">
|
||||
<button type="submit"
|
||||
onclick="return confirm('Da li ste sigurni da želite da obrišete nalog {{.Nalog.BrojNaloga}}?\n\nKoličine artikala biće vraćene na stanje.')"
|
||||
style="padding:9px 20px;background:#dc2626;color:#fff;border:none;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;white-space:nowrap;transition:opacity 0.2s;"
|
||||
onmouseover="this.style.opacity='0.85'" onmouseout="this.style.opacity='1'">
|
||||
<button type="submit" class="btn-primarno" style="background:#dc2626;"
|
||||
data-potvrda="Da li ste sigurni da želite da obrišete nalog {{.Nalog.BrojNaloga}}? Količine artikala biće vraćene na stanje.">
|
||||
Obriši nalog
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
@@ -68,20 +68,15 @@
|
||||
}">
|
||||
|
||||
<!-- nazad dugme -->
|
||||
<a href="/prodaja"
|
||||
style="display:inline-flex;align-items:center;gap:6px;font-size:13px;color:var(--tekst-sporedni);text-decoration:none;margin-bottom:20px;transition:color 0.2s;"
|
||||
onmouseover="this.style.color='var(--tekst-glavni)'" onmouseout="this.style.color='var(--tekst-sporedni)'">
|
||||
<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"><polyline points="15 18 9 12 15 6"/></svg>
|
||||
<a href="/prodaja" class="nazad-link">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="15 18 9 12 15 6"/></svg>
|
||||
Nazad na prodaju
|
||||
</a>
|
||||
|
||||
<form method="POST" action="/prodaja/nova">
|
||||
|
||||
{{if .Greska}}
|
||||
<div class="greska-animacija"
|
||||
style="background:var(--kartica);border:0.5px solid #dc2626;border-radius:8px;padding:10px 14px;margin-bottom:16px;font-size:13px;color:#dc2626;">
|
||||
{{.Greska}}
|
||||
</div>
|
||||
<div class="poruka-greska greska-animacija">{{.Greska}}</div>
|
||||
{{end}}
|
||||
|
||||
<!-- zaglavlje prodaje -->
|
||||
@@ -246,17 +241,10 @@
|
||||
|
||||
<!-- dugmad forme -->
|
||||
<div style="display:flex;justify-content:flex-end;gap:10px;">
|
||||
<a href="/prodaja"
|
||||
style="padding:9px 20px;border:0.5px solid var(--ivica);border-radius:8px;font-size:14px;color:var(--tekst-sporedni);text-decoration:none;transition:background 0.2s;"
|
||||
onmouseover="this.style.background='var(--pozadina)'" onmouseout="this.style.background=''">
|
||||
Odustani
|
||||
</a>
|
||||
<button type="submit"
|
||||
<a href="/prodaja" class="btn-sekundarno">Odustani</a>
|
||||
<button type="submit" class="btn-primarno"
|
||||
:disabled="imaPrekoracenja()"
|
||||
:style="imaPrekoracenja() ? 'opacity:0.4;cursor:not-allowed;' : ''"
|
||||
style="padding:9px 20px;background:var(--sb-akcent);color:#fff;border:none;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;transition:opacity 0.2s;"
|
||||
@mouseover="if(!imaPrekoracenja()) $el.style.opacity='0.85'"
|
||||
@mouseout="$el.style.opacity=imaPrekoracenja()?'0.4':'1'">
|
||||
:style="imaPrekoracenja() ? 'opacity:0.4;cursor:not-allowed;' : ''">
|
||||
Sačuvaj prodaju
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -4,11 +4,6 @@
|
||||
|
||||
{{define "dodatni-css"}}
|
||||
<style>
|
||||
@keyframes slideDown {
|
||||
from { opacity: 0; transform: translateY(-10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.poruka-animacija { animation: slideDown 0.3s ease forwards; }
|
||||
|
||||
.servis-tabela tbody tr:nth-child(1) { animation-delay: 0.04s; }
|
||||
@@ -78,14 +73,9 @@
|
||||
<div class="poruka-uspeh poruka-animacija">Nalog je uspešno obrisan.</div>
|
||||
{{end}}
|
||||
|
||||
<!-- gornja traka: dugme + pretraga + filter statusa -->
|
||||
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center;">
|
||||
<a href="/servis/novi"
|
||||
style="padding:8px 16px;background:var(--sb-akcent);color:#fff;border-radius:8px;font-size:14px;font-weight:500;text-decoration:none;white-space:nowrap;transition:opacity 0.2s;"
|
||||
onmouseover="this.style.opacity='0.85'" onmouseout="this.style.opacity='1'">
|
||||
+ Novi nalog
|
||||
</a>
|
||||
<form method="GET" action="/servis" style="display:flex;gap:8px;flex:1;flex-wrap:wrap;min-width:200px;">
|
||||
<!-- gornja traka: pretraga + dugme nova, ispod toga dugme traži -->
|
||||
<form method="GET" action="/servis" style="display:flex;flex-direction:column;gap:8px;">
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;">
|
||||
<input type="text" name="pretraga" value="{{.Pretraga}}"
|
||||
placeholder="Pretraži po broju naloga ili uređaju..."
|
||||
style="flex:1;min-width:160px;">
|
||||
@@ -95,11 +85,12 @@
|
||||
<option value="{{.}}" {{if eq . $.FilterStatus}}selected{{end}}>{{.}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
<button type="submit" class="btn-primarno">
|
||||
Traži
|
||||
</button>
|
||||
</form>
|
||||
<a href="/servis/novi" class="btn-primarno" style="white-space:nowrap;flex-shrink:0;">+ Novi nalog</a>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class="btn-primarno" style="white-space:nowrap;">Traži</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- desktop tabela -->
|
||||
<div class="kartica servis-tabela animiraj" style="padding:0;overflow:hidden;">
|
||||
@@ -117,14 +108,10 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Nalozi}}
|
||||
<tr class="animiraj" style="border-bottom:0.5px solid var(--ivica);transition:background 0.15s;"
|
||||
onmouseover="this.style.background='var(--pozadina)'"
|
||||
onmouseout="this.style.background=''">
|
||||
<tr class="animiraj red-tabele">
|
||||
<td style="padding:12px 16px;">
|
||||
<a href="/servis/{{.ID}}"
|
||||
style="font-size:14px;font-weight:500;color:var(--sb-akcent);text-decoration:none;font-family:monospace;"
|
||||
onmouseover="this.style.textDecoration='underline'"
|
||||
onmouseout="this.style.textDecoration='none'">
|
||||
style="font-size:14px;font-weight:500;color:var(--sb-akcent);text-decoration:none;font-family:monospace;">
|
||||
{{.BrojNaloga}}
|
||||
</a>
|
||||
</td>
|
||||
@@ -143,14 +130,14 @@
|
||||
<a href="/servis/izmeni/{{.ID}}" class="btn-primarno-malo">
|
||||
Izmeni
|
||||
</a>
|
||||
{{if index $.Dozvole "servis.obrisi"}}
|
||||
<form method="POST" action="/servis/obrisi/{{.ID}}" style="display:inline;">
|
||||
<button type="submit"
|
||||
onclick="return confirm('Da li ste sigurni da želite da obrišete nalog {{.BrojNaloga}}?')"
|
||||
style="padding:4px 10px;background:#dc2626;color:#fff;border:none;border-radius:6px;font-size:12px;cursor:pointer;transition:opacity 0.2s;"
|
||||
onmouseover="this.style.opacity='0.8'" onmouseout="this.style.opacity='1'">
|
||||
<button type="submit" class="btn-obrisi-malo"
|
||||
data-potvrda="Da li ste sigurni da želite da obrišete nalog {{.BrojNaloga}}?">
|
||||
Obriši
|
||||
</button>
|
||||
</form>
|
||||
{{end}}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -196,13 +183,14 @@
|
||||
<a href="/servis/izmeni/{{.ID}}" class="btn-primarno-malo">
|
||||
Izmeni
|
||||
</a>
|
||||
{{if index $.Dozvole "servis.obrisi"}}
|
||||
<form method="POST" action="/servis/obrisi/{{.ID}}" style="display:inline;">
|
||||
<button type="submit"
|
||||
onclick="return confirm('Da li ste sigurni da želite da obrišete nalog {{.BrojNaloga}}?')"
|
||||
style="padding:6px 14px;background:#dc2626;color:#fff;border:none;border-radius:6px;font-size:13px;cursor:pointer;">
|
||||
<button type="submit" class="btn-obrisi-malo"
|
||||
data-potvrda="Da li ste sigurni da želite da obrišete nalog {{.BrojNaloga}}?">
|
||||
Obriši
|
||||
</button>
|
||||
</form>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
|
||||
@@ -49,10 +49,8 @@
|
||||
{{end}}
|
||||
|
||||
<!-- nazad dugme -->
|
||||
<a href="/servis"
|
||||
style="display:inline-flex;align-items:center;gap:6px;font-size:13px;color:var(--tekst-sporedni);text-decoration:none;transition:color 0.2s;"
|
||||
onmouseover="this.style.color='var(--tekst-glavni)'" onmouseout="this.style.color='var(--tekst-sporedni)'">
|
||||
<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"><polyline points="15 18 9 12 15 6"/></svg>
|
||||
<a href="/servis" class="nazad-link">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="15 18 9 12 15 6"/></svg>
|
||||
Nazad na servis
|
||||
</a>
|
||||
|
||||
@@ -171,10 +169,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<form method="POST" action="/servis/obrisi/{{.Nalog.ID}}">
|
||||
<button type="submit"
|
||||
onclick="return confirm('Da li ste sigurni da želite da obrišete nalog {{.Nalog.BrojNaloga}}?')"
|
||||
style="padding:9px 20px;background:#dc2626;color:#fff;border:none;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;white-space:nowrap;transition:opacity 0.2s;"
|
||||
onmouseover="this.style.opacity='0.85'" onmouseout="this.style.opacity='1'">
|
||||
<button type="submit" class="btn-primarno" style="background:#dc2626;"
|
||||
data-potvrda="Da li ste sigurni da želite da obrišete nalog {{.Nalog.BrojNaloga}}?">
|
||||
Obriši nalog
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -6,17 +6,7 @@
|
||||
<style>
|
||||
.greska-animacija { animation: shake 0.4s ease; }
|
||||
|
||||
.sekcija-naslov {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--tekst-sporedni);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.forma-grid-2 { grid-template-columns: 1fr !important; }
|
||||
.forma-grid-4 { grid-template-columns: 1fr 1fr !important; }
|
||||
}
|
||||
</style>
|
||||
@@ -26,10 +16,8 @@
|
||||
<div style="width:100%;">
|
||||
|
||||
<!-- nazad dugme -->
|
||||
<a href="/servis"
|
||||
style="display:inline-flex;align-items:center;gap:6px;font-size:13px;color:var(--tekst-sporedni);text-decoration:none;margin-bottom:20px;transition:color 0.2s;"
|
||||
onmouseover="this.style.color='var(--tekst-glavni)'" onmouseout="this.style.color='var(--tekst-sporedni)'">
|
||||
<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"><polyline points="15 18 9 12 15 6"/></svg>
|
||||
<a href="/servis" class="nazad-link">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="15 18 9 12 15 6"/></svg>
|
||||
Nazad na servis
|
||||
</a>
|
||||
|
||||
@@ -44,10 +32,7 @@
|
||||
</div>
|
||||
|
||||
{{if .Greska}}
|
||||
<div class="greska-animacija"
|
||||
style="background:var(--kartica);border:0.5px solid #dc2626;border-radius:8px;padding:10px 14px;margin-bottom:16px;font-size:13px;color:#dc2626;">
|
||||
{{.Greska}}
|
||||
</div>
|
||||
<div class="poruka-greska greska-animacija">{{.Greska}}</div>
|
||||
{{end}}
|
||||
|
||||
<form method="POST" action="{{if .Izmena}}/servis/izmeni/{{.Nalog.ID}}{{else}}/servis/novi{{end}}">
|
||||
@@ -164,14 +149,8 @@
|
||||
|
||||
<!-- dugmad -->
|
||||
<div style="display:flex;justify-content:flex-end;gap:10px;margin-top:6px;">
|
||||
<a href="/servis"
|
||||
style="padding:9px 20px;border:0.5px solid var(--ivica);border-radius:8px;font-size:14px;color:var(--tekst-sporedni);text-decoration:none;transition:background 0.2s;"
|
||||
onmouseover="this.style.background='var(--pozadina)'" onmouseout="this.style.background=''">
|
||||
Odustani
|
||||
</a>
|
||||
<button type="submit"
|
||||
style="padding:9px 20px;background:var(--sb-akcent);color:#fff;border:none;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;transition:opacity 0.2s;"
|
||||
onmouseover="this.style.opacity='0.85'" onmouseout="this.style.opacity='1'">
|
||||
<a href="/servis" class="btn-sekundarno">Odustani</a>
|
||||
<button type="submit" class="btn-primarno">
|
||||
{{if .Izmena}}Sačuvaj izmene{{else}}Sačuvaj nalog{{end}}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -102,6 +102,15 @@
|
||||
i.type = 'hidden'; i.name = '_csrf'; i.value = m.content;
|
||||
f.appendChild(i);
|
||||
});
|
||||
|
||||
// data-potvrda: sigurna alternativa za onclick=confirm() na dugmadima i linkovima
|
||||
// Vrednost atributa je poruka koja se prikazuje korisniku. Go template je HTML-escape-uje,
|
||||
// JS čita originalnu vrednost — nema problema sa specijalnim karakterima u imenima.
|
||||
document.querySelectorAll('[data-potvrda]').forEach(function(el) {
|
||||
el.addEventListener('click', function(e) {
|
||||
if (!confirm(el.getAttribute('data-potvrda'))) e.preventDefault();
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user