Compare commits

...

2 Commits

Author SHA1 Message Date
Dasko f29e76612e Servis forma, animacije, hover efekti i pojačane senke
Servis:
- Nova polja: ostecenja, pin_uredjaja, pribor (migracija 051)
- Default garancija iz podešavanja, svič "Bez garancije" u formi
- Podešavanja → Servis: konfigurabilan rok garancije (migracija 052)
- Default datum prijema = danas; datum_prijema se eksplicitno upisuje
- Sidebar link za Servis podešavanja

PDV/Nivelacije:
- Default raspon datuma = početak/kraj tekućeg meseca (KIR, KPR, Nivelacije)
- Dodata class="tabela" na tabele bez klase (KIR, KPR, Obračun, Nivelacije)

Animacije (Moj profil → Tema):
- Korisnik bira vrstu animacije: bez, fadeInUp, fadeIn, scaleIn, slideLeft
- Čuva se po korisniku u korisnici.lokalna_animacija (migracija 053)
- CSS [data-animacija] radi na body (globalno) i na preview wrapperima (izolovano)
- Preview animacije izolovan: data-animacija na #anim-preview-wrap, ne na body
- Mobilne kartice se animiraju kad korisnik odabere stil (podrazumevano ne)
- Animacija primenjena direktno na .tabela tbody tr (bez potrebe za .animiraj)

Hover efekti (Moj profil → Tema):
- Opcije: podrazumevano, bez, podizanje, svetlost, zoom, boja
- Čuva se po korisniku u korisnici.lokalni_hover (migracija 054)
- CSS [data-hover] radi izolovano; preview menja samo #hover-preview-wrap
- Pojačane senke u oba teme (--senka i nova --senka-hover promenljiva)
- Transition dodat za transform i background na karticama

Grafikon (Izveštaji): toggle zamenjen globalnim .toggl/.toggl-klizac svičom
2026-06-18 02:21:06 +02:00
Dasko b417ff6d02 Verzija na login ekranu; konsolidacija build skripti
- Prijava: prosleđuje h.Verzija šablonu i prikazuje je ispod forme
- start.sh: objedinjena skripta (verzija, okruženje, platforma, UPX, build, Docker push)
- build.sh: obrisano (zamenjeno sa start.sh)
2026-06-16 22:20:47 +02:00
39 changed files with 709 additions and 161 deletions
-108
View File
@@ -1,108 +0,0 @@
#!/bin/bash
set -e
# ──────────────────────────────────────────────
# Verzija
# ──────────────────────────────────────────────
read -p "Verzija (npr. 0.1.1): " VERZIJA
VERZIJA=${VERZIJA:-"0.0.1"}
# ──────────────────────────────────────────────
# Okruženje
# ──────────────────────────────────────────────
echo ""
echo "Okruženje:"
echo " 1) production"
echo " 2) development"
read -p "Izbor [1/2, podrazumevano 1]: " OKR_IZBOR
OKR_IZBOR=${OKR_IZBOR:-1}
if [ "$OKR_IZBOR" = "2" ]; then
OKRUZENJE="development"
LDFLAGS="-X main.Verzija=dev-${VERZIJA}"
NAZIV="ntech-dev"
else
OKRUZENJE="production"
LDFLAGS="-X main.Verzija=${VERZIJA} -s -w"
NAZIV="ntech"
fi
# ──────────────────────────────────────────────
# Ciljni OS
# ──────────────────────────────────────────────
echo ""
echo "Ciljni OS:"
echo " 1) Linux (amd64)"
echo " 2) Windows (amd64)"
read -p "Izbor [1/2, podrazumevano 1]: " OS_IZBOR
OS_IZBOR=${OS_IZBOR:-1}
if [ "$OS_IZBOR" = "2" ]; then
GOOS_VAL="windows"
NAZIV="${NAZIV}.exe"
else
GOOS_VAL="linux"
fi
# ──────────────────────────────────────────────
# UPX kompresija
# ──────────────────────────────────────────────
echo ""
UPX_DOSTUPAN=false
if command -v upx &>/dev/null; then
UPX_DOSTUPAN=true
read -p "Kompresovati UPX-om? [d/N]: " UPX_IZBOR
else
echo "UPX nije instaliran — kompresija preskočena."
UPX_IZBOR="n"
fi
# ──────────────────────────────────────────────
# Sažetak pre builda
# ──────────────────────────────────────────────
echo ""
echo "──────────────────────────────────────────"
echo " Okruženje : ${OKRUZENJE}"
echo " Verzija : ${VERZIJA}"
echo " OS : ${GOOS_VAL}/amd64"
echo " Izlaz : ${NAZIV}"
if [ "$UPX_DOSTUPAN" = true ] && [[ "$UPX_IZBOR" =~ ^[dDyY] ]]; then
echo " UPX : da"
else
echo " UPX : ne"
fi
echo "──────────────────────────────────────────"
echo ""
read -p "Pokrenuti build? [D/n]: " POTVRDA
POTVRDA=${POTVRDA:-"d"}
if [[ ! "$POTVRDA" =~ ^[dDyY] ]]; then
echo "Build otkazan."
exit 0
fi
# ──────────────────────────────────────────────
# Build
# ──────────────────────────────────────────────
echo ""
echo "Buildovanje..."
CGO_ENABLED=0 GOARCH=amd64 GOOS=${GOOS_VAL} go build \
-ldflags "${LDFLAGS}" \
-o "${NAZIV}" \
./cmd/ntech
echo "Build završen: ${NAZIV}"
ls -lh "${NAZIV}"
# ──────────────────────────────────────────────
# UPX
# ──────────────────────────────────────────────
if [ "$UPX_DOSTUPAN" = true ] && [[ "$UPX_IZBOR" =~ ^[dDyY] ]]; then
echo ""
echo "Kompresovanje sa UPX..."
upx --best "${NAZIV}"
echo "Nakon kompresije:"
ls -lh "${NAZIV}"
fi
echo ""
echo "Gotovo."
+3
View File
@@ -247,6 +247,7 @@ func main() {
r.Get("/admin/podesavanja/opste", h.PodesavanjaOpste)
r.Get("/admin/podesavanja/izgled", h.PodesavanjaIzgled)
r.Get("/admin/podesavanja/sistem", h.PodesavanjaSistem)
r.Get("/admin/podesavanja/servis", h.PodesavanjaServis)
r.Get("/admin/podesavanja/kalkulacija-pdv", h.PdvStope)
r.With(doz("podesavanja.izmeni")).Post("/podesavanja/pdv-stope/dodaj", h.DodajPdvStopu)
r.With(doz("podesavanja.izmeni")).Post("/podesavanja/pdv-stope/{id}/izmeni", h.IzmeniPdvStopu)
@@ -356,6 +357,8 @@ func main() {
r.Post("/admin/profil/totp/deaktiviraj", h.AdminTotpDeaktivacija)
r.Post("/admin/profil/totp/kodovi", h.AdminTotpRegenerisiKodove)
r.With(doz("tema.lokalno")).Post("/profil/tema", h.SacuvajLokalnuTemu)
r.With(doz("tema.lokalno")).Post("/profil/animacija", h.SacuvajLokalnuAnimaciju)
r.With(doz("tema.lokalno")).Post("/profil/hover", h.SacuvajLokalniHover)
r.Get("/profil/tema", h.ProfilTema)
r.With(doz("tema.lokalno")).Post("/profil/pozadina", h.ProfilOtpremiPozadinu)
r.With(doz("tema.lokalno")).Post("/profil/pozadina/ukloni", h.ProfilUkloniPozadinu)
+15 -1
View File
@@ -12,12 +12,26 @@ require (
)
require (
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
github.com/boombuler/barcode v1.1.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/f-amaral/go-async v0.3.0 // indirect
github.com/hhrutter/lzw v1.0.0 // indirect
github.com/hhrutter/pkcs7 v0.2.0 // indirect
github.com/hhrutter/tiff v1.0.2 // indirect
github.com/johnfercher/go-tree v1.1.0 // indirect
github.com/johnfercher/maroto/v2 v2.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.21 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/pdfcpu/pdfcpu v0.11.1 // indirect
github.com/phpdave11/gofpdf v1.4.3 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
golang.org/x/image v0.37.0 // indirect
golang.org/x/sys v0.45.0 // indirect
golang.org/x/text v0.37.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
modernc.org/libc v1.72.3 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
+41
View File
@@ -1,9 +1,17 @@
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo=
github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/f-amaral/go-async v0.3.0 h1:h4kLsX7aKfdWaHvV0lf+/EE3OIeCzyeDYJDb/vDZUyg=
github.com/f-amaral/go-async v0.3.0/go.mod h1:Hz5Qr6DAWpbTTUjytnrg1WIsDgS7NtOei5y8SipYS7U=
github.com/go-chi/chi/v5 v5.3.0 h1:halUjDxhshgXHMrao5bB8eNBXo/rnzwr8m5m36glehM=
github.com/go-chi/chi/v5 v5.3.0/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
@@ -12,23 +20,50 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hhrutter/lzw v1.0.0 h1:laL89Llp86W3rRs83LvKbwYRx6INE8gDn0XNb1oXtm0=
github.com/hhrutter/lzw v1.0.0/go.mod h1:2HC6DJSn/n6iAZfgM3Pg+cP1KxeWc3ezG8bBqW5+WEo=
github.com/hhrutter/pkcs7 v0.2.0 h1:i4HN2XMbGQpZRnKBLsUwO3dSckzgX142TNqY/KfXg+I=
github.com/hhrutter/pkcs7 v0.2.0/go.mod h1:aEzKz0+ZAlz7YaEMY47jDHL14hVWD6iXt0AgqgAvWgE=
github.com/hhrutter/tiff v1.0.2 h1:7H3FQQpKu/i5WaSChoD1nnJbGx4MxU5TlNqqpxw55z8=
github.com/hhrutter/tiff v1.0.2/go.mod h1:pcOeuK5loFUE7Y/WnzGw20YxUdnqjY1P0Jlcieb/cCw=
github.com/johnfercher/go-tree v1.1.0 h1:L0Fs5jLR1uA2e/CwfHjNdO/Lt4IGQ46QgxarAC1yeXs=
github.com/johnfercher/go-tree v1.1.0/go.mod h1:DUO6QkXIFh1K7jeGBIkLCZaeUgnkdQAsB64FDSoHswg=
github.com/johnfercher/maroto/v2 v2.4.0 h1:Nc/jA2RCZvNZESrQj41HJOgtkwmerSHd5FUbP4dRrIE=
github.com/johnfercher/maroto/v2 v2.4.0/go.mod h1:Nnxa3g4f+vzdx/u/dUgx/52HnrCOCt5QBPSdeSlkFZQ=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w=
github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pdfcpu/pdfcpu v0.11.1 h1:htHBSkGH5jMKWC6e0sihBFbcKZ8vG1M67c8/dJxhjas=
github.com/pdfcpu/pdfcpu v0.11.1/go.mod h1:pP3aGga7pRvwFWAm9WwFvo+V68DfANi9kxSQYioNYcw=
github.com/phpdave11/gofpdf v1.4.3 h1:M/zHvS8FO3zh9tUd2RCOPEjyuVcs281FCyF22Qlz/IA=
github.com/phpdave11/gofpdf v1.4.3/go.mod h1:MAwzoUIgD3J55u0rxIG2eu37c+XWhBtXSpPAhnQXf/o=
github.com/phpdave11/gofpdi v1.0.15/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.37.0 h1:ZiRjArKI8GwxZOoEtUfhrBtaCN+4b/7709dlT6SSnQA=
golang.org/x/image v0.37.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
@@ -36,8 +71,14 @@ golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
modernc.org/cc/v4 v4.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY=
modernc.org/cc/v4 v4.28.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
modernc.org/ccgo/v4 v4.34.0 h1:yRLPFZieg532OT4rp4JFNIVcquwalMX26G95WQDqwCQ=
+2
View File
@@ -149,6 +149,8 @@ type KorisniciRepository interface {
SacuvajTotpTajnu(ctx context.Context, id int64, tajna string) error
SacuvajLokalnuTemu(ctx context.Context, id int64, lokalnaTema string, koristi bool) error
SacuvajLokalnuPozadinu(ctx context.Context, id int64, pozadina, opacity, blur, blurPozadine, glassOpacity string) error
SacuvajLokalnuAnimaciju(ctx context.Context, id int64, animacija string) error
SacuvajLokalniHover(ctx context.Context, id int64, hover string) error
SacuvajAvatar(ctx context.Context, id int64, putanja string) error
PostojiIjedan(ctx context.Context) (bool, error)
Obrisi(ctx context.Context, id int64) error
+38 -3
View File
@@ -30,6 +30,8 @@ type korisnikOpcije struct {
lokalnaPozadinaBlurPozadine sql.NullString
lokalnaPozadinaGlassOpacity sql.NullString
avatarPutanja sql.NullString
lokalnaAnimacija sql.NullString
lokalniHover sql.NullString
}
// dodeliOpcijeKorisnika prenosi vrednosti iz korisnikOpcije na model.Korisnik
@@ -44,6 +46,8 @@ func dodeliOpcijeKorisnika(k *model.Korisnik, o korisnikOpcije) {
k.LokalnaPozadinaBlurPozadine = o.lokalnaPozadinaBlurPozadine.String
k.LokalnaPozadinaGlassOpacity = o.lokalnaPozadinaGlassOpacity.String
k.AvatarPutanja = o.avatarPutanja.String
k.LokalnaAnimacija = o.lokalnaAnimacija.String
k.LokalniHover = o.lokalniHover.String
}
// skeniraiKorisnika čita jedan red iz baze i popunjava model.Korisnik
@@ -55,6 +59,7 @@ func skeniraiKorisnika(row interface{ Scan(...any) error }) (*model.Korisnik, er
&o.lokalnaTema, &o.koristiLokalnuTemu, &o.datumKreiranja,
&o.lokalnaPozadina, &o.lokalnaPozadinaOpacity, &o.lokalnaPozadinaBlur,
&o.lokalnaPozadinaBlurPozadine, &o.lokalnaPozadinaGlassOpacity, &o.avatarPutanja,
&o.lokalnaAnimacija, &o.lokalniHover,
); err != nil {
return nil, err
}
@@ -98,7 +103,8 @@ func (r *sqliteKorisniciRepo) DohvatiPoImenu(ctx context.Context, korisnickoIme
COALESCE(lokalna_tema, ''), koristi_lokalnu_temu, datum_kreiranja,
COALESCE(lokalna_pozadina, ''), COALESCE(lokalna_pozadina_opacity, '50'),
COALESCE(lokalna_pozadina_blur, '12'), COALESCE(lokalna_pozadina_blur_pozadine, '0'),
COALESCE(lokalna_pozadina_glass_opacity, '10'), COALESCE(avatar_putanja, '')
COALESCE(lokalna_pozadina_glass_opacity, '10'), COALESCE(avatar_putanja, ''),
COALESCE(lokalna_animacija, ''), COALESCE(lokalni_hover, '')
FROM korisnici WHERE korisnicko_ime = ?`, korisnickoIme)
k, err := skeniraiKorisnika(row)
if err != nil {
@@ -114,7 +120,8 @@ func (r *sqliteKorisniciRepo) DohvatiPoID(ctx context.Context, id int64) (*model
COALESCE(lokalna_tema, ''), koristi_lokalnu_temu, datum_kreiranja,
COALESCE(lokalna_pozadina, ''), COALESCE(lokalna_pozadina_opacity, '50'),
COALESCE(lokalna_pozadina_blur, '12'), COALESCE(lokalna_pozadina_blur_pozadine, '0'),
COALESCE(lokalna_pozadina_glass_opacity, '10'), COALESCE(avatar_putanja, '')
COALESCE(lokalna_pozadina_glass_opacity, '10'), COALESCE(avatar_putanja, ''),
COALESCE(lokalna_animacija, ''), COALESCE(lokalni_hover, '')
FROM korisnici WHERE id = ?`, id)
k, err := skeniraiKorisnika(row)
if err != nil {
@@ -130,7 +137,8 @@ func (r *sqliteKorisniciRepo) Lista(ctx context.Context) ([]model.Korisnik, erro
COALESCE(lokalna_tema, ''), koristi_lokalnu_temu, datum_kreiranja,
COALESCE(lokalna_pozadina, ''), COALESCE(lokalna_pozadina_opacity, '50'),
COALESCE(lokalna_pozadina_blur, '12'), COALESCE(lokalna_pozadina_blur_pozadine, '0'),
COALESCE(lokalna_pozadina_glass_opacity, '10'), COALESCE(avatar_putanja, '')
COALESCE(lokalna_pozadina_glass_opacity, '10'), COALESCE(avatar_putanja, ''),
COALESCE(lokalna_animacija, ''), COALESCE(lokalni_hover, '')
FROM korisnici ORDER BY datum_kreiranja ASC`)
if err != nil {
return nil, fmt.Errorf("ntech: korisnici.Lista: %w", err)
@@ -145,6 +153,7 @@ func (r *sqliteKorisniciRepo) Lista(ctx context.Context) ([]model.Korisnik, erro
&o.lokalnaTema, &o.koristiLokalnuTemu, &o.datumKreiranja,
&o.lokalnaPozadina, &o.lokalnaPozadinaOpacity, &o.lokalnaPozadinaBlur,
&o.lokalnaPozadinaBlurPozadine, &o.lokalnaPozadinaGlassOpacity, &o.avatarPutanja,
&o.lokalnaAnimacija, &o.lokalniHover,
); err != nil {
return nil, fmt.Errorf("ntech: korisnici.Lista: %w", err)
}
@@ -194,6 +203,32 @@ func (r *sqliteKorisniciRepo) SacuvajLokalnuTemu(ctx context.Context, id int64,
return nil
}
func (r *sqliteKorisniciRepo) SacuvajLokalnuAnimaciju(ctx context.Context, id int64, animacija string) error {
var val any
if animacija != "" {
val = animacija
}
_, err := r.db.ExecContext(ctx,
`UPDATE korisnici SET lokalna_animacija = ? WHERE id = ?`, val, id)
if err != nil {
return fmt.Errorf("ntech: korisnici.SacuvajLokalnuAnimaciju: %w", err)
}
return nil
}
func (r *sqliteKorisniciRepo) SacuvajLokalniHover(ctx context.Context, id int64, hover string) error {
var val any
if hover != "" {
val = hover
}
_, err := r.db.ExecContext(ctx,
`UPDATE korisnici SET lokalni_hover = ? WHERE id = ?`, val, id)
if err != nil {
return fmt.Errorf("ntech: korisnici.SacuvajLokalniHover: %w", err)
}
return nil
}
func (r *sqliteKorisniciRepo) AzurirajUlogu(ctx context.Context, id int64, uloga string) error {
_, err := r.db.ExecContext(ctx, `UPDATE korisnici SET uloga = ? WHERE id = ?`, uloga, id)
if err != nil {
+16 -5
View File
@@ -43,6 +43,7 @@ func (r *ServisRepo) Lista(ctx context.Context, pretraga, status string) ([]mode
sn.id, sn.klijent_id, sn.tehnicar_id, sn.broj_naloga, sn.uredjaj, sn.serijski_broj,
sn.opis_kvara, sn.status, sn.cena_od, sn.cena_do, sn.cena_konacna,
sn.avans, sn.napomena, sn.garancija_do, sn.datum_prijema, sn.datum_zavrsetka,
sn.ostecenja, sn.pin_uredjaja, sn.pribor,
COALESCE(kp.naziv, '') AS klijent_naziv
FROM servisni_nalozi sn
LEFT JOIN klijent_prikaz kp ON kp.id = sn.klijent_id
@@ -88,7 +89,8 @@ func (r *ServisRepo) DohvatiID(ctx context.Context, id int64) (*model.ServisniNa
SELECT
id, klijent_id, tehnicar_id, broj_naloga, uredjaj, serijski_broj,
opis_kvara, status, cena_od, cena_do, cena_konacna,
avans, napomena, garancija_do, datum_prijema, datum_zavrsetka
avans, napomena, garancija_do, datum_prijema, datum_zavrsetka,
ostecenja, pin_uredjaja, pribor
FROM servisni_nalozi WHERE id = ?`, id)
var n model.ServisniNalog
@@ -105,13 +107,16 @@ func (r *ServisRepo) Kreiraj(ctx context.Context, n *model.ServisniNalog) (int64
rezultat, err := r.db.ExecContext(ctx, `
INSERT INTO servisni_nalozi
(klijent_id, tehnicar_id, broj_naloga, uredjaj, serijski_broj, opis_kvara,
status, cena_od, cena_do, cena_konacna, avans, napomena, garancija_do, datum_zavrsetka)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
status, cena_od, cena_do, cena_konacna, avans, napomena, garancija_do, datum_zavrsetka,
ostecenja, pin_uredjaja, pribor, datum_prijema)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
nullInt64(n.KlijentID), nullInt64(n.TehnicarID), n.BrojNaloga, n.Uredjaj,
nullString(n.SerijskiBroj), n.OpisKvara, n.Status,
nullFloat64(n.CenaOd), nullFloat64(n.CenaDo), nullFloat64(n.CenaKonacna),
nullFloat64(n.Avans), nullString(n.Napomena),
nullTime(n.GarancijaDo), nullTime(n.DatumZavrsetka),
nullString(n.Ostecenja), nullString(n.PinUredjaja), nullString(n.Pribor),
n.DatumPrijema,
)
if err != nil {
return 0, fmt.Errorf("ntech: ServisRepo.Kreiraj: %w", err)
@@ -131,11 +136,13 @@ func (r *ServisRepo) Izmeni(ctx context.Context, n *model.ServisniNalog) error {
UPDATE servisni_nalozi SET
klijent_id = ?, tehnicar_id = ?, uredjaj = ?, serijski_broj = ?, opis_kvara = ?,
status = ?, cena_od = ?, cena_do = ?, cena_konacna = ?,
avans = ?, napomena = ?, garancija_do = ?, datum_zavrsetka = ?
avans = ?, napomena = ?, garancija_do = ?, datum_zavrsetka = ?,
ostecenja = ?, pin_uredjaja = ?, pribor = ?
WHERE id = ?`,
nullInt64(n.KlijentID), nullInt64(n.TehnicarID), n.Uredjaj, nullString(n.SerijskiBroj), n.OpisKvara,
n.Status, nullFloat64(n.CenaOd), nullFloat64(n.CenaDo), nullFloat64(n.CenaKonacna),
nullFloat64(n.Avans), nullString(n.Napomena), nullTime(n.GarancijaDo), nullTime(n.DatumZavrsetka),
nullString(n.Ostecenja), nullString(n.PinUredjaja), nullString(n.Pribor),
n.ID,
)
if err != nil {
@@ -159,7 +166,7 @@ func (r *ServisRepo) Obrisi(ctx context.Context, id int64) error {
// klijentNaziv je opcioni pokazivač, nil kada se čita bez JOIN-a
func scanNalog(scan func(...any) error, n *model.ServisniNalog, klijentNaziv *string) error {
var klijentID, tehnicarID sql.NullInt64
var serijskiBroj, napomena sql.NullString
var serijskiBroj, napomena, ostecenja, pinUredjaja, pribor sql.NullString
var cenaOd, cenaDo, cenaKonacna, avans sql.NullFloat64
var garancijaDo, datumZavrsetka sql.NullTime
@@ -167,6 +174,7 @@ func scanNalog(scan func(...any) error, n *model.ServisniNalog, klijentNaziv *st
&n.ID, &klijentID, &tehnicarID, &n.BrojNaloga, &n.Uredjaj, &serijskiBroj,
&n.OpisKvara, &n.Status, &cenaOd, &cenaDo, &cenaKonacna,
&avans, &napomena, &garancijaDo, &n.DatumPrijema, &datumZavrsetka,
&ostecenja, &pinUredjaja, &pribor,
}
if klijentNaziv != nil {
@@ -187,6 +195,9 @@ func scanNalog(scan func(...any) error, n *model.ServisniNalog, klijentNaziv *st
}
n.SerijskiBroj = serijskiBroj.String
n.Napomena = napomena.String
n.Ostecenja = ostecenja.String
n.PinUredjaja = pinUredjaja.String
n.Pribor = pribor.String
if cenaOd.Valid {
v := cenaOd.Float64
n.CenaOd = &v
+2
View File
@@ -46,6 +46,8 @@ type podaciProfilTema struct {
LokalnaPozadinaBlur string
LokalnaPozadinaBlurPozadine string
LokalnaPozadinaGlassOpacity string
LokalnaAnimacija string
LokalniHover string
}
// AdminKorisnici prikazuje listu korisnika
+4
View File
@@ -211,6 +211,10 @@ func (h *Handler) popuniPodaciStranice(r *http.Request, podesavanja map[string]s
ps.AppPozadinaGlassOpacity = "10"
}
}
if korisnik != nil {
ps.LokalnaAnimacija = korisnik.LokalnaAnimacija
ps.LokalniHover = korisnik.LokalniHover
}
return ps
}
+1 -1
View File
@@ -24,7 +24,7 @@ var saSidebar = []string{
"klijenti", "klijent_forma",
"magacin", "magacin_forma",
"nabavke", "nabavka_forma", "nabavka_detalji",
"podesavanja", "podesavanja_opste", "podesavanja_izgled", "podesavanja_sistem",
"podesavanja", "podesavanja_opste", "podesavanja_izgled", "podesavanja_sistem", "podesavanja_servis",
"pdv_stope",
"pdv_kir", "pdv_kir_forma",
"pdv_kpr", "pdv_kpr_forma",
+13
View File
@@ -5,6 +5,7 @@ import (
"net/http"
"strconv"
"strings"
"time"
"ntech/internal/db/sqlite"
"ntech/internal/middleware"
@@ -34,6 +35,18 @@ func (h *Handler) Nivelacije(w http.ResponseWriter, r *http.Request) {
odStr := r.URL.Query().Get("od")
doStr := r.URL.Query().Get("do")
// podrazumevano: tekući mesec (od prvog do poslednjeg dana)
if odStr == "" || doStr == "" {
sada := time.Now()
prvi := time.Date(sada.Year(), sada.Month(), 1, 0, 0, 0, 0, sada.Location())
poslednji := prvi.AddDate(0, 1, -1)
if odStr == "" {
odStr = prvi.Format("2006-01-02")
}
if doStr == "" {
doStr = poslednji.Format("2006-01-02")
}
}
zapisi, err := h.NivelacijaRepo.Lista(r.Context(), parsiraDatumOpcionalno(odStr), parsiraDatumOpcionalno(doStr))
if err != nil {
http.Error(w, "Greška pri učitavanju nivelacija", http.StatusInternalServerError)
+12
View File
@@ -61,6 +61,18 @@ func (h *Handler) PdvKir(w http.ResponseWriter, r *http.Request) {
odStr := r.URL.Query().Get("od")
doStr := r.URL.Query().Get("do")
// podrazumevano: tekući mesec (od prvog do poslednjeg dana)
if odStr == "" || doStr == "" {
sada := time.Now()
prvi := time.Date(sada.Year(), sada.Month(), 1, 0, 0, 0, 0, sada.Location())
poslednji := prvi.AddDate(0, 1, -1)
if odStr == "" {
odStr = prvi.Format("2006-01-02")
}
if doStr == "" {
doStr = poslednji.Format("2006-01-02")
}
}
zapisi, err := h.PdvKirRepo.Lista(r.Context(), parsiraDatumOpcionalno(odStr), parsiraDatumOpcionalno(doStr))
if err != nil {
http.Error(w, "Greška pri učitavanju knjige izdatih računa", http.StatusInternalServerError)
+12
View File
@@ -42,6 +42,18 @@ func (h *Handler) PdvKpr(w http.ResponseWriter, r *http.Request) {
odStr := r.URL.Query().Get("od")
doStr := r.URL.Query().Get("do")
// podrazumevano: tekući mesec (od prvog do poslednjeg dana)
if odStr == "" || doStr == "" {
sada := time.Now()
prvi := time.Date(sada.Year(), sada.Month(), 1, 0, 0, 0, 0, sada.Location())
poslednji := prvi.AddDate(0, 1, -1)
if odStr == "" {
odStr = prvi.Format("2006-01-02")
}
if doStr == "" {
doStr = poslednji.Format("2006-01-02")
}
}
zapisi, err := h.PdvKprRepo.Lista(r.Context(), parsiraDatumOpcionalno(odStr), parsiraDatumOpcionalno(doStr))
if err != nil {
http.Error(w, "Greška pri učitavanju knjige primljenih računa", http.StatusInternalServerError)
+30
View File
@@ -43,6 +43,7 @@ type PodaciPodesavanja struct {
BackupIntervalSati string
BackupBrojKopija string
KalkulacijaMarza string
ServisGarancijaMeseci string
LoginPozadina string
LoginPozadinaOpacity string
LoginPozadinaBlurPozadine string
@@ -314,6 +315,20 @@ func (h *Handler) SacuvajPodesavanja(w http.ResponseWriter, r *http.Request) {
}
}
// podrazumevani rok garancije za servis (meseci, 0120)
if v := strings.TrimSpace(r.FormValue("servis_garancija_meseci")); v != "" {
n, err := strconv.Atoi(v)
if err != nil || n < 0 || n > 120 {
middleware.SetFlash(w, r, h.DB, "greska", "Rok garancije mora biti broj između 0 i 120 meseci.")
http.Redirect(w, r, sledeci, http.StatusSeeOther)
return
}
if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "servis_garancija_meseci", strconv.Itoa(n)); err != nil {
http.Error(w, "Greška pri čuvanju podešavanja", http.StatusInternalServerError)
return
}
}
http.Redirect(w, r, sledeci+"?sacuvano=1", http.StatusSeeOther)
}
@@ -438,6 +453,20 @@ func (h *Handler) UkloniLogo(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/admin/podesavanja/opste?sacuvano=1", http.StatusSeeOther)
}
// PodesavanjaServis renderuje stranicu sa podešavanjima servisnog modula
func (h *Handler) PodesavanjaServis(w http.ResponseWriter, r *http.Request) {
if _, ok := h.zahtevajDozvolu(w, r, "podesavanja.pregled"); !ok {
return
}
podaci, err := h.napuniPodaciPodesavanja(r, "Podešavanja — Servis")
if err != nil {
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
return
}
podaci.Stranica = "podesavanja-servis"
h.renderujTemplate(w, "podesavanja_servis", podaci)
}
// generisiImeUploada vraća slučajno hex ime (16 bajtova) sa datom ekstenzijom
func generisiImeUploada(ext string) (string, error) {
buf := make([]byte, 16)
@@ -669,6 +698,7 @@ func (h *Handler) napuniPodaciPodesavanja(r *http.Request, naslov string) (Podac
BackupIntervalSati: vrednostIliDefault(podesavanja, "backup_interval_sati", "24"),
BackupBrojKopija: vrednostIliDefault(podesavanja, "backup_broj_kopija", "7"),
KalkulacijaMarza: vrednostIliDefault(podesavanja, "kalkulacija_marza", "20"),
ServisGarancijaMeseci: vrednostIliDefault(podesavanja, "servis_garancija_meseci", "2"),
}, nil
}
+1
View File
@@ -63,6 +63,7 @@ func (h *Handler) PrikazPrijave(w http.ResponseWriter, r *http.Request) {
"LoginPozadinaBlurPozadine": loginBlurPozadine,
"LoginPozadinaBlurKartice": loginBlurKartice,
"LoginPozadinaZatamnjenjeKartice": loginZatamnjenjeKartice,
"Verzija": h.Verzija,
})
}
+80
View File
@@ -48,6 +48,8 @@ func (h *Handler) ProfilTema(w http.ResponseWriter, r *http.Request) {
LokalnaPozadinaBlur: svezi.LokalnaPozadinaBlur,
LokalnaPozadinaBlurPozadine: svezi.LokalnaPozadinaBlurPozadine,
LokalnaPozadinaGlassOpacity: svezi.LokalnaPozadinaGlassOpacity,
LokalnaAnimacija: svezi.LokalnaAnimacija,
LokalniHover: svezi.LokalniHover,
}
if podaci.LokalnaPozadinaOpacity == "" {
podaci.LokalnaPozadinaOpacity = "50"
@@ -293,6 +295,84 @@ func (h *Handler) SacuvajLokalnuTemu(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/admin/profil", http.StatusSeeOther)
}
// SacuvajLokalnuAnimaciju čuva korisnikovu preferencu animacije tabela
func (h *Handler) SacuvajLokalnuAnimaciju(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if k == nil {
http.Redirect(w, r, "/prijava", http.StatusSeeOther)
return
}
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "tema.lokalno") {
http.Error(w, "Pristup odbijen", http.StatusForbidden)
return
}
if err := r.ParseForm(); err != nil {
middleware.SetFlash(w, r, h.DB, "greska", "Greška. Pokušajte ponovo.")
http.Redirect(w, r, "/admin/profil/tema", http.StatusSeeOther)
return
}
animacija := r.FormValue("lokalna_animacija")
dozvoljene := map[string]bool{"": true, "bez": true, "fadeInUp": true, "fadeIn": true, "scaleIn": true, "slideLeft": true}
if !dozvoljene[animacija] {
animacija = ""
}
if err := h.KorisniciRepo.SacuvajLokalnuAnimaciju(r.Context(), k.ID, animacija); err != nil {
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju. Pokušajte ponovo.")
http.Redirect(w, r, "/admin/profil/tema", http.StatusSeeOther)
return
}
middleware.SetFlash(w, r, h.DB, "uspeh", "Animacija je sačuvana.")
if ref := r.Referer(); ref != "" {
if u, err := url.Parse(ref); err == nil {
http.Redirect(w, r, u.RequestURI(), http.StatusSeeOther)
return
}
}
http.Redirect(w, r, "/admin/profil/tema", http.StatusSeeOther)
}
// SacuvajLokalniHover čuva korisnikovu preferencu hover efekta na karticama
func (h *Handler) SacuvajLokalniHover(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
if k == nil {
http.Redirect(w, r, "/prijava", http.StatusSeeOther)
return
}
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "tema.lokalno") {
http.Error(w, "Pristup odbijen", http.StatusForbidden)
return
}
if err := r.ParseForm(); err != nil {
middleware.SetFlash(w, r, h.DB, "greska", "Greška. Pokušajte ponovo.")
http.Redirect(w, r, "/admin/profil/tema", http.StatusSeeOther)
return
}
hover := r.FormValue("lokalni_hover")
dozvoljeni := map[string]bool{"": true, "bez": true, "podizanje": true, "svetlost": true, "zoom": true, "boja": true}
if !dozvoljeni[hover] {
hover = ""
}
if err := h.KorisniciRepo.SacuvajLokalniHover(r.Context(), k.ID, hover); err != nil {
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju. Pokušajte ponovo.")
http.Redirect(w, r, "/admin/profil/tema", http.StatusSeeOther)
return
}
middleware.SetFlash(w, r, h.DB, "uspeh", "Hover efekat je sačuvan.")
if ref := r.Referer(); ref != "" {
if u, err := url.Parse(ref); err == nil {
http.Redirect(w, r, u.RequestURI(), http.StatusSeeOther)
return
}
}
http.Redirect(w, r, "/admin/profil/tema", http.StatusSeeOther)
}
// ProfilOtpremiAvatar prima upload lične avatar slike korisnika
func (h *Handler) ProfilOtpremiAvatar(w http.ResponseWriter, r *http.Request) {
k := middleware.KorisnikIzKonteksta(r.Context())
+38 -5
View File
@@ -110,9 +110,15 @@ func (h *Handler) NoviNalog(w http.ResponseWriter, r *http.Request) {
ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "servis"
ps.NaslovStranice = "Novi nalog"
noviNalog := model.ServisniNalog{
BrojNaloga: brojNaloga,
Status: model.StatusPrimljeno,
DatumPrijema: time.Now(),
}
noviNalog.GarancijaDo = defaultGarancija(noviNalog.DatumPrijema, podesavanja)
h.renderujFormuNaloga(w, PodaciFormeNaloga{
PodaciStranice: ps,
Nalog: model.ServisniNalog{BrojNaloga: brojNaloga, Status: model.StatusPrimljeno},
Nalog: noviNalog,
Klijenti: klijenti,
Tehnicari: tehnicari,
SviStatusi: model.SviStatusi,
@@ -206,6 +212,9 @@ func (h *Handler) IzmeniNalog(w http.ResponseWriter, r *http.Request) {
return
}
if nalog.GarancijaDo == nil {
nalog.GarancijaDo = defaultGarancija(nalog.DatumPrijema, podesavanja)
}
ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "servis"
ps.NaslovStranice = "Izmeni nalog"
@@ -460,6 +469,17 @@ func parseFormuNaloga(r *http.Request) (model.ServisniNalog, string) {
OpisKvara: opisKvara,
Status: r.FormValue("status"),
Napomena: strings.TrimSpace(r.FormValue("napomena")),
Ostecenja: strings.TrimSpace(r.FormValue("ostecenja")),
PinUredjaja: strings.TrimSpace(r.FormValue("pin_uredjaja")),
Pribor: strings.TrimSpace(r.FormValue("pribor")),
DatumPrijema: time.Now(),
}
// datum prijema — korisnik može da unese drugi datum (npr. retroaktivno)
if dp := strings.TrimSpace(r.FormValue("datum_prijema")); dp != "" {
if t, err := time.Parse("2006-01-02", dp); err == nil {
nalog.DatumPrijema = t
}
}
if nalog.Status == "" {
@@ -493,16 +513,29 @@ func parseFormuNaloga(r *http.Request) (model.ServisniNalog, string) {
}
}
// opcioni datum garancije
if gd := strings.TrimSpace(r.FormValue("garancija_do")); gd != "" {
if t, err := time.Parse("2006-01-02", gd); err == nil {
nalog.GarancijaDo = &t
// opcioni datum garancije — preskačemo ako je korisnik označio "bez garancije"
if r.FormValue("bez_garancije") != "1" {
if gd := strings.TrimSpace(r.FormValue("garancija_do")); gd != "" {
if t, err := time.Parse("2006-01-02", gd); err == nil {
nalog.GarancijaDo = &t
}
}
}
return nalog, ""
}
// defaultGarancija vraća datum garancije na osnovu datuma prijema i podešavanja;
// vraća nil ako je rok 0 ili podešavanje nedostaje
func defaultGarancija(datumPrijema time.Time, podesavanja map[string]string) *time.Time {
meseci, err := strconv.Atoi(vrednostIliDefault(podesavanja, "servis_garancija_meseci", "2"))
if err != nil || meseci <= 0 {
return nil
}
t := datumPrijema.AddDate(0, meseci, 0)
return &t
}
// parseOpcionuCenu pretvara string u *float64 — prazno polje ili neispravna vrednost vraća nil
func parseOpcionuCenu(s string) *float64 {
s = strings.TrimSpace(s)
+2
View File
@@ -19,6 +19,8 @@ type Korisnik struct {
LokalnaPozadinaBlurPozadine string
LokalnaPozadinaGlassOpacity string
AvatarPutanja string
LokalnaAnimacija string // "" | "fadeInUp" | "fadeIn" | "scaleIn" | "slideLeft"
LokalniHover string // "" | "bez" | "podizanje" | "svetlost"
}
// Sesija predstavlja aktivnu sesiju prijavljenog korisnika
+3
View File
@@ -43,6 +43,9 @@ type ServisniNalog struct {
GarancijaDo *time.Time
DatumPrijema time.Time
DatumZavrsetka *time.Time
Ostecenja string
PinUredjaja string
Pribor string
}
// ServisniDeo predstavlja jedan artikal ugrađen u servisni nalog
+2
View File
@@ -53,6 +53,8 @@ type PodaciStranice struct {
AppPozadinaBlur string // vrednost 0-20 (px backdrop-filter blur na elementima)
AppPozadinaBlurPozadine string // vrednost 0-20 (px filter blur na pozadinskoj slici)
AppPozadinaGlassOpacity string // vrednost 0-80 (% zatamnjivanje glass elemenata) — samo za ličnu pozadinu
LokalnaAnimacija string // "" | "fadeInUp" | "fadeIn" | "scaleIn" | "slideLeft"
LokalniHover string // "" | "bez" | "podizanje" | "svetlost"
}
// PodaciDashboarda su podaci specifični za dashboard stranicu
+3
View File
@@ -0,0 +1,3 @@
ALTER TABLE servisni_nalozi ADD COLUMN ostecenja TEXT;
ALTER TABLE servisni_nalozi ADD COLUMN pin_uredjaja TEXT;
ALTER TABLE servisni_nalozi ADD COLUMN pribor TEXT;
+2
View File
@@ -0,0 +1,2 @@
INSERT OR IGNORE INTO podesavanja (kljuc, vrednost) VALUES
('servis_garancija_meseci', '2');
+1
View File
@@ -0,0 +1 @@
ALTER TABLE korisnici ADD COLUMN lokalna_animacija TEXT;
+1
View File
@@ -0,0 +1 @@
ALTER TABLE korisnici ADD COLUMN lokalni_hover TEXT;
+83 -7
View File
@@ -339,12 +339,42 @@ body {
border-radius: 12px;
padding: 16px;
box-shadow: var(--senka);
transition: transform 0.25s cubic-bezier(.4,0,.2,1), box-shadow 0.25s;
transition: box-shadow 0.25s, border-color 0.25s, transform 0.25s, background 0.25s;
}
.kartica:hover {
transform: scale(1.02);
box-shadow: var(--senka-hover);
border-color: var(--ivica-jaka);
}
/* hover varijante po korisničkoj preferenciji.
Selektor je [data-hover] (ne body[data-hover]) da bi isti CSS
radio i globalno (body) i lokalno na preview wrapperima. */
[data-hover="bez"] .kartica:hover {
box-shadow: var(--senka);
border-color: var(--ivica);
transform: none;
}
[data-hover="podizanje"] .kartica:hover {
box-shadow: var(--senka-hover);
border-color: var(--ivica-jaka);
transform: translateY(-3px);
}
[data-hover="svetlost"] .kartica:hover {
box-shadow: 0 0 0 1.5px var(--sb-akcent), 0 0 20px color-mix(in srgb, var(--sb-akcent) 35%, transparent);
border-color: var(--sb-akcent);
transform: none;
}
[data-hover="zoom"] .kartica:hover {
box-shadow: var(--senka-hover);
border-color: var(--ivica-jaka);
transform: scale(1.02);
}
[data-hover="boja"] .kartica:hover {
box-shadow: var(--senka-hover);
border-color: var(--ivica-jaka);
background: color-mix(in srgb, var(--sb-akcent) 6%, var(--kartica));
transform: none;
}
/* modal za premeštanje artikla koristi promenljive teme, pa prati svetlu/tamnu;
@@ -592,7 +622,9 @@ body {
color: var(--tekst-sporedni);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 10px;
padding-bottom: 8px;
margin-bottom: 12px;
border-bottom: 0.5px solid var(--ivica);
}
/* avatar krug korisnika u topbaru */
@@ -980,6 +1012,21 @@ select {
to { opacity: 1; transform: translateY(0); }
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes scaleIn {
from { opacity: 0; transform: scale(0.95); }
to { opacity: 1; transform: scale(1); }
}
@keyframes slideLeft {
from { opacity: 0; transform: translateX(-12px); }
to { opacity: 1; transform: translateX(0); }
}
@keyframes slideDown {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
@@ -1003,11 +1050,40 @@ select {
animation: fadeInUp 0.2s ease backwards;
}
/* kartice ne animiramo po defaultu — animacija je samo na redovima tabela */
.kartica.animiraj {
animation: none;
}
/* korisnikova preferencija animacije: body[data-animacija] nadjačava podrazumevano.
Kad korisnik odabere stil, animiraju se i redovi tabela i mobilne kartice. */
[data-animacija="bez"] .animiraj,
[data-animacija="bez"] .tabela tbody tr,
[data-animacija="bez"] .kartica.animiraj { animation: none; }
[data-animacija="fadeInUp"] .animiraj,
[data-animacija="fadeInUp"] .tabela tbody tr,
[data-animacija="fadeInUp"] .kartica.animiraj { animation: fadeInUp 0.2s ease backwards; }
[data-animacija="fadeIn"] .animiraj,
[data-animacija="fadeIn"] .tabela tbody tr,
[data-animacija="fadeIn"] .kartica.animiraj { animation: fadeIn 0.2s ease backwards; }
[data-animacija="scaleIn"] .animiraj,
[data-animacija="scaleIn"] .tabela tbody tr,
[data-animacija="scaleIn"] .kartica.animiraj { animation: scaleIn 0.2s ease backwards; }
[data-animacija="slideLeft"] .animiraj,
[data-animacija="slideLeft"] .tabela tbody tr,
[data-animacija="slideLeft"] .kartica.animiraj { animation: slideLeft 0.2s ease backwards; }
/* Stepenasta (stagger) animacija redova u svim listama JEDNO mesto za sve tabele.
Pogađa samo redove koji se animiraju (<tr class="animiraj">); ostali ostaju bez
delay-a. Mora biti u main.css: HTMX navigacija (hx-select) odbacuje <head>, pa
stilovi iz page <style> blokova ne bi važili tokom navigacije.
Da promeniš animaciju redova svuda menjaš samo ovde. */
Animacija se primenjuje direktno na <tr> u .tabela, bez potrebe za .animiraj klasom.
Mora biti u main.css: HTMX navigacija (hx-select) odbacuje <head>, pa
stilovi iz page <style> blokova ne bi važili tokom navigacije. */
.tabela tbody tr {
animation: fadeInUp 0.2s ease backwards;
}
.tabela tbody tr:nth-child(1) { animation-delay: 0.04s; }
.tabela tbody tr:nth-child(2) { animation-delay: 0.08s; }
.tabela tbody tr:nth-child(3) { animation-delay: 0.12s; }
+3 -1
View File
@@ -8,6 +8,7 @@
--tekst-sporedni: #57606a;
--tekst-jak: #1f2328;
--ivica: #d0d7de;
--ivica-jaka: #9ba5b0;
--sb-akcent: #1f6feb;
--sb-akcent-hover: #388bfd;
--sb-aktivan: #ebeef1;
@@ -15,7 +16,8 @@
--upozorenje: #9a6700;
--greska: #cf222e;
--info: #0969da;
--senka: 0 4px 12px rgba(0, 0, 0, 0.08);
--senka: 0 4px 16px rgba(0, 0, 0, 0.12), 0 1px 4px rgba(0, 0, 0, 0.08);
--senka-hover: 0 8px 28px rgba(0, 0, 0, 0.18), 0 2px 8px rgba(0, 0, 0, 0.10);
--sidebar-pozadina: #ffffff;
--poruka-uspeh-bg: rgba(26, 127, 55, 0.1);
--poruka-uspeh-boja: #1a7f37;
+3 -1
View File
@@ -8,6 +8,7 @@
--tekst-sporedni: #a0a8b5;
--tekst-jak: #f0f3f6;
--ivica: #353a44;
--ivica-jaka: #5a6070;
--sb-akcent: #4684d6;
--sb-akcent-hover: #5a96e6;
--sb-aktivan: #2a2f37;
@@ -15,7 +16,8 @@
--upozorenje: #e1b04a;
--greska: #cf5757;
--info: #5cb1d6;
--senka: 0 4px 12px rgba(0, 0, 0, 0.4);
--senka: 0 4px 16px rgba(0, 0, 0, 0.55), 0 1px 4px rgba(0, 0, 0, 0.35);
--senka-hover: 0 8px 28px rgba(0, 0, 0, 0.7), 0 2px 8px rgba(0, 0, 0, 0.45);
--sidebar-pozadina: #1a1c20;
--poruka-uspeh-bg: rgba(93, 184, 118, 0.15);
--poruka-uspeh-boja: #5db876;
+8 -3
View File
@@ -187,19 +187,19 @@
{{if index .Dozvole "podesavanja.pregled"}}
<div>
<button type="button" data-podmeni-dugme
class="nav-stavka {{if or (eq .Stranica "podesavanja") (eq .Stranica "podesavanja-opste") (eq .Stranica "podesavanja-izgled") (eq .Stranica "podesavanja-sistem") (eq .Stranica "podesavanja-kalkulacija-pdv") (eq .Stranica "dozvole")}}aktivan{{end}}"
class="nav-stavka {{if or (eq .Stranica "podesavanja") (eq .Stranica "podesavanja-opste") (eq .Stranica "podesavanja-izgled") (eq .Stranica "podesavanja-sistem") (eq .Stranica "podesavanja-kalkulacija-pdv") (eq .Stranica "podesavanja-servis") (eq .Stranica "dozvole")}}aktivan{{end}}"
style="width:100%;background:none;border:none;cursor:pointer;">
<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-strelica">
<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"
style="transition:transform 0.2s;transform:{{if or (eq .Stranica "podesavanja") (eq .Stranica "podesavanja-opste") (eq .Stranica "podesavanja-izgled") (eq .Stranica "podesavanja-sistem") (eq .Stranica "podesavanja-kalkulacija-pdv") (eq .Stranica "dozvole")}}rotate(180deg){{else}}rotate(0deg){{end}}">
style="transition:transform 0.2s;transform:{{if or (eq .Stranica "podesavanja") (eq .Stranica "podesavanja-opste") (eq .Stranica "podesavanja-izgled") (eq .Stranica "podesavanja-sistem") (eq .Stranica "podesavanja-kalkulacija-pdv") (eq .Stranica "podesavanja-servis") (eq .Stranica "dozvole")}}rotate(180deg){{else}}rotate(0deg){{end}}">
<polyline points="6 9 12 15 18 9"/>
</svg>
</span>
<span class="nav-tooltip">Podešavanja</span>
</button>
<div class="nav-podmeni {{if or (eq .Stranica "podesavanja") (eq .Stranica "podesavanja-opste") (eq .Stranica "podesavanja-izgled") (eq .Stranica "podesavanja-sistem") (eq .Stranica "podesavanja-kalkulacija-pdv") (eq .Stranica "dozvole")}}otvoren{{end}}">
<div class="nav-podmeni {{if or (eq .Stranica "podesavanja") (eq .Stranica "podesavanja-opste") (eq .Stranica "podesavanja-izgled") (eq .Stranica "podesavanja-sistem") (eq .Stranica "podesavanja-kalkulacija-pdv") (eq .Stranica "podesavanja-servis") (eq .Stranica "dozvole")}}otvoren{{end}}">
<a href="/admin/podesavanja/opste" class="nav-stavka nav-podstavka {{if eq .Stranica "podesavanja-opste"}}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="M3 21h18"/><path d="M5 21V7a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v14"/><path d="M9 21v-5h6v5"/><path d="M9 9h.01M15 9h.01M9 13h.01M15 13h.01"/></svg>
<span>Opšte</span>
@@ -220,6 +220,11 @@
<span>Kalkulacija i PDV</span>
<span class="nav-tooltip">Kalkulacija i PDV</span>
</a>
<a href="/admin/podesavanja/servis" class="nav-stavka nav-podstavka {{if eq .Stranica "podesavanja-servis"}}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="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>
<span>Servis</span>
<span class="nav-tooltip">Servis</span>
</a>
{{if or (eq .KorisnikUloga "superadmin") (eq .KorisnikUloga "admin")}}
<a href="/admin/dozvole" class="nav-stavka nav-podstavka {{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>
+2 -8
View File
@@ -6,12 +6,6 @@
<style>
.izv-naslov { font-size: 13px; font-weight: 500; color: var(--tekst-sporedni); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 14px; }
.toggle-red { display: flex; align-items: center; gap: 10px; margin-bottom: 16px; }
.toggle-switch { position: relative; display: inline-block; width: 40px; height: 22px; flex-shrink: 0; }
.toggle-switch input { opacity: 0; width: 0; height: 0; }
.toggle-slider { position: absolute; inset: 0; background: var(--ivica); border-radius: 22px; cursor: pointer; transition: background 0.2s; }
.toggle-slider::before { content: ''; position: absolute; width: 16px; height: 16px; left: 3px; top: 3px; background: #fff; border-radius: 50%; transition: transform 0.2s; }
.toggle-switch input:checked + .toggle-slider { background: var(--sb-akcent); }
.toggle-switch input:checked + .toggle-slider::before { transform: translateX(18px); }
.rang-broj { display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; border-radius: 50%; background: var(--pozadina); font-size: 11px; font-weight: 600; color: var(--tekst-sporedni); flex-shrink: 0; }
.rang-broj.zlatni { background: #fef3c7; color: #92400e; }
.badge-dana { display: inline-block; padding: 2px 8px; border-radius: 20px; font-size: 11px; font-weight: 500; white-space: nowrap; }
@@ -34,9 +28,9 @@
<div class="izv-naslov" style="margin-bottom:0;">Mesečni prihod — poslednjih 12 meseci</div>
<div class="toggle-red" style="margin-bottom:0;">
<span class="pomocni-tekst">Grafikon</span>
<label class="toggle-switch">
<label class="toggl">
<input type="checkbox" id="toggle-grafikon" checked>
<span class="toggle-slider"></span>
<span class="toggl-klizac"></span>
</label>
</div>
</div>
+1 -1
View File
@@ -24,7 +24,7 @@
<!-- istorija nivelacija -->
<div class="kartica animiraj" style="padding:0;overflow:hidden;">
<div style="overflow-x:auto;">
<table style="width:100%;border-collapse:collapse;font-size:13px;min-width:760px;">
<table class="tabela" style="width:100%;border-collapse:collapse;font-size:13px;min-width:760px;">
<thead>
<tr style="text-align:left;color:var(--tekst-sporedni);border-bottom:0.5px solid var(--ivica);">
<th style="padding:10px 12px;">Datum</th>
+1 -1
View File
@@ -27,7 +27,7 @@
<!-- knjiga -->
<div class="kartica animiraj" style="padding:0;overflow:hidden;">
<div style="overflow-x:auto;">
<table style="width:100%;border-collapse:collapse;font-size:13px;min-width:900px;">
<table class="tabela" style="width:100%;border-collapse:collapse;font-size:13px;min-width:900px;">
<thead>
<tr style="text-align:left;color:var(--tekst-sporedni);border-bottom:0.5px solid var(--ivica);">
<th style="padding:10px 12px;">Datum prometa</th>
+1 -1
View File
@@ -27,7 +27,7 @@
<!-- knjiga -->
<div class="kartica animiraj" style="padding:0;overflow:hidden;">
<div style="overflow-x:auto;">
<table style="width:100%;border-collapse:collapse;font-size:13px;min-width:980px;">
<table class="tabela" style="width:100%;border-collapse:collapse;font-size:13px;min-width:980px;">
<thead>
<tr style="text-align:left;color:var(--tekst-sporedni);border-bottom:0.5px solid var(--ivica);">
<th style="padding:10px 12px;">Datum prometa</th>
+2 -2
View File
@@ -23,7 +23,7 @@
<!-- obračun po stopama -->
<div class="kartica animiraj" style="padding:0;overflow:hidden;margin-bottom:16px;">
<div style="overflow-x:auto;">
<table style="width:100%;border-collapse:collapse;font-size:13px;min-width:560px;">
<table class="tabela" style="width:100%;border-collapse:collapse;font-size:13px;min-width:560px;">
<thead>
<tr style="text-align:left;color:var(--tekst-sporedni);border-bottom:0.5px solid var(--ivica);">
<th style="padding:10px 12px;"></th>
@@ -110,7 +110,7 @@
</div>
</div>
<div style="overflow-x:auto;margin-top:12px;">
<table style="width:100%;border-collapse:collapse;font-size:13px;min-width:560px;">
<table class="tabela" style="width:100%;border-collapse:collapse;font-size:13px;min-width:560px;">
<thead>
<tr style="text-align:left;color:var(--tekst-sporedni);border-bottom:0.5px solid var(--ivica);">
<th style="padding:10px 12px;width:48px;">Polje</th>
@@ -0,0 +1,45 @@
{{template "base" .}}
{{define "naslov"}}Podešavanja — Servis — NTech{{end}}
{{define "sadrzaj"}}
<div class="stranica-stack" style="width:100%;max-width:100%;">
{{if .Sacuvano}}
<div class="poruka-uspeh">Podešavanja su uspešno sačuvana.</div>
{{end}}
<form method="POST" action="/podesavanja/sacuvaj">
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
<input type="hidden" name="_next" value="/admin/podesavanja/servis">
<div class="kartica animiraj">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px;padding-bottom:12px;border-bottom:0.5px solid var(--ivica);">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--sb-akcent)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>
<span style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">Servis</span>
</div>
<div class="kolona" style="gap:16px;">
<div style="max-width:280px;">
<label for="servis_garancija_meseci" class="polje-labela">
Podrazumevani rok garancije (meseci)
</label>
<input type="number" id="servis_garancija_meseci" name="servis_garancija_meseci"
min="0" max="120" value="{{.ServisGarancijaMeseci}}"
style="width:100%;">
<div class="pomocni-tekst" style="margin-top:6px;">
Automatski se upisuje pri prijemu uređaja. Vrednost 0 znači bez garancije.
</div>
</div>
</div>
<div style="display:flex;justify-content:flex-end;margin-top:20px;">
<button type="submit" class="btn-primarno">Sačuvaj</button>
</div>
</div>
</form>
</div>
{{end}}
+1
View File
@@ -65,6 +65,7 @@
</div>
<button type="submit" class="dugme">Prijavi se</button>
</form>
<p style="text-align:center;font-size:12px;color:#4b5563;margin-top:16px;">Verzija: {{.Verzija}}</p>
</div>
{{if .LoginPozadina}}
+145
View File
@@ -74,6 +74,104 @@
{{end}}
</div>
<!-- kartica: animacije -->
<div class="kartica animiraj" style="margin-bottom:16px;">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px;padding-bottom:12px;border-bottom:0.5px solid var(--ivica);">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--sb-akcent)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12s2.545-5 7-5c4.454 0 7 5 7 5s-2.546 5-7 5c-4.455 0-7-5-7-5z"/><circle cx="12" cy="12" r="3"/></svg>
<span style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">Animacije tabela</span>
</div>
<form method="POST" action="/profil/animacija">
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
<div class="kolona" style="gap:16px;">
<div style="max-width:300px;">
<label class="polje-labela" for="anim-select">Vrsta animacije pri učitavanju</label>
<select id="anim-select" name="lokalna_animacija" style="width:100%;" onchange="animPreview(this.value)"><!-- preview se primenjuje na wrapper, ne na body -->
<option value="" {{if eq .LokalnaAnimacija ""}}selected{{end}}>Podrazumevano (klizanje gore)</option>
<option value="bez" {{if eq .LokalnaAnimacija "bez"}}selected{{end}}>Bez animacije</option>
<option value="fadeInUp" {{if eq .LokalnaAnimacija "fadeInUp"}}selected{{end}}>Klizanje gore (fadeInUp)</option>
<option value="fadeIn" {{if eq .LokalnaAnimacija "fadeIn"}}selected{{end}}>Pojavljivanje (fadeIn)</option>
<option value="scaleIn" {{if eq .LokalnaAnimacija "scaleIn"}}selected{{end}}>Zumiranje (scaleIn)</option>
<option value="slideLeft" {{if eq .LokalnaAnimacija "slideLeft"}}selected{{end}}>Klizanje s leva (slideLeft)</option>
</select>
</div>
<!-- preview -->
<div>
<div class="pomocni-tekst" style="margin-bottom:8px;">Pregled:</div>
<!-- preview wrapper nosi data-animacija atribut; isti CSS [data-animacija] .animiraj
koji radi globalno na body radi i ovde, ali izolovano — pogađa SAMO redove unutar wrappera -->
<div id="anim-preview-wrap" style="border:0.5px solid var(--ivica);border-radius:8px;overflow:hidden;max-width:340px;">
<table style="width:100%;border-collapse:collapse;">
<tbody id="anim-preview-body">
<tr class="animiraj" style="border-bottom:0.5px solid var(--ivica);"><td style="padding:8px 12px;font-size:13px;color:var(--tekst-glavni);">Laptop HP 840</td><td style="padding:8px 12px;text-align:right;font-size:13px;color:var(--tekst-sporedni);">120.000 din</td></tr>
<tr class="animiraj" style="border-bottom:0.5px solid var(--ivica);animation-delay:0.08s;"><td style="padding:8px 12px;font-size:13px;color:var(--tekst-glavni);">iPhone 14 Pro</td><td style="padding:8px 12px;text-align:right;font-size:13px;color:var(--tekst-sporedni);">95.000 din</td></tr>
<tr class="animiraj" style="animation-delay:0.16s;"><td style="padding:8px 12px;font-size:13px;color:var(--tekst-glavni);">Samsung S24</td><td style="padding:8px 12px;text-align:right;font-size:13px;color:var(--tekst-sporedni);">80.000 din</td></tr>
</tbody>
</table>
</div>
<button type="button" onclick="animPreviewPonovi()"
style="margin-top:8px;padding:6px 14px;background:var(--pozadina);border:0.5px solid var(--ivica);border-radius:8px;font-size:12px;color:var(--tekst-sporedni);cursor:pointer;">
Ponovi pregled
</button>
</div>
<div>
<button type="submit" class="btn-primarno">Sačuvaj</button>
</div>
</div>
</form>
</div>
<!-- kartica: hover efekat -->
<div class="kartica animiraj" style="margin-bottom:16px;">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px;padding-bottom:12px;border-bottom:0.5px solid var(--ivica);">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--sb-akcent)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 3l14 9-7 1-4 7z"/></svg>
<span style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">Hover efekat kartica</span>
</div>
<form method="POST" action="/profil/hover">
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
<div class="kolona" style="gap:16px;">
<div style="max-width:300px;">
<label class="polje-labela" for="hover-select">Efekat pri prelasku mišem</label>
<select id="hover-select" name="lokalni_hover" style="width:100%;" onchange="hoverPreview(this.value)">
<option value="" {{if eq .LokalniHover ""}}selected{{end}}>Podrazumevano (senka + ivica)</option>
<option value="bez" {{if eq .LokalniHover "bez"}}selected{{end}}>Bez efekta</option>
<option value="podizanje" {{if eq .LokalniHover "podizanje"}}selected{{end}}>Podizanje (lift)</option>
<option value="svetlost" {{if eq .LokalniHover "svetlost"}}selected{{end}}>Svetlost ivice (akcent)</option>
<option value="zoom" {{if eq .LokalniHover "zoom"}}selected{{end}}>Zoom (uvećanje)</option>
<option value="boja" {{if eq .LokalniHover "boja"}}selected{{end}}>Boja pozadine (akcent)</option>
</select>
</div>
<!-- preview -->
<div>
<div class="pomocni-tekst" style="margin-bottom:8px;">Pregled — pređi mišem:</div>
<div id="hover-preview-wrap" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:10px;max-width:380px;">
<div id="hprev1" class="kartica" style="padding:12px;cursor:default;min-height:64px;display:flex;flex-direction:column;gap:4px;">
<div style="font-size:11px;color:var(--tekst-sporedni);">Artikli</div>
<div style="font-size:18px;font-weight:600;color:var(--tekst-glavni);">1.284</div>
</div>
<div id="hprev2" class="kartica" style="padding:12px;cursor:default;min-height:64px;display:flex;flex-direction:column;gap:4px;">
<div style="font-size:11px;color:var(--tekst-sporedni);">Servis</div>
<div style="font-size:18px;font-weight:600;color:var(--tekst-glavni);">47</div>
</div>
<div id="hprev3" class="kartica" style="padding:12px;cursor:default;min-height:64px;display:flex;flex-direction:column;gap:4px;">
<div style="font-size:11px;color:var(--tekst-sporedni);">Prihod</div>
<div style="font-size:18px;font-weight:600;color:var(--tekst-glavni);">+12%</div>
</div>
</div>
<div class="pomocni-tekst" style="margin-top:6px;font-size:11px;">Efekat se primenjuje odmah pri odabiru.</div>
</div>
<div>
<button type="submit" class="btn-primarno">Sačuvaj</button>
</div>
</div>
</form>
</div>
<!-- kartica: moj avatar -->
<div class="kartica animiraj" style="margin-bottom:16px;">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px;padding-bottom:12px;border-bottom:0.5px solid var(--ivica);">
@@ -255,4 +353,51 @@
{{end}}
</div>
<script>
function hoverPreview(val) {
var wrap = document.getElementById('hover-preview-wrap');
if (!wrap) return;
if (val === '') {
wrap.removeAttribute('data-hover');
} else {
wrap.setAttribute('data-hover', val);
}
}
// inicijalizuj preview na osnovu trenutnog odabira pri učitavanju
(function() {
var sel = document.getElementById('hover-select');
if (sel) hoverPreview(sel.value);
})();
// mapiranje vrednosti dropdowna na vrednost data-animacija atributa.
// prazna vrednost ("Podrazumevano") odgovara fadeInUp — istom podrazumevanom stilu.
var animVrednosti = {
'': 'fadeInUp',
'bez': 'bez',
'fadeInUp': 'fadeInUp',
'fadeIn': 'fadeIn',
'scaleIn': 'scaleIn',
'slideLeft': 'slideLeft'
};
// postavlja data-animacija na PREVIEW wrapper (ne na body), pa CSS
// [data-animacija] .animiraj radi izolovano — samo nad redovima preview tabele.
function animPreview(val) {
var wrap = document.getElementById('anim-preview-wrap');
if (!wrap) return;
var anim = animVrednosti[val] || 'fadeInUp';
// restartuj animaciju: skini atribut, izazovi reflow, pa ga vrati
wrap.removeAttribute('data-animacija');
void wrap.offsetHeight; // reflow da bi se animacija ponovo okinula
wrap.setAttribute('data-animacija', anim);
}
// "Ponovi pregled" — ponovo okida animaciju za trenutno izabrani stil
function animPreviewPonovi() {
animPreview(document.getElementById('anim-select').value);
}
// inicijalizuj preview na osnovu sačuvane preferencije pri učitavanju stranice
(function() {
var sel = document.getElementById('anim-select');
if (sel) animPreview(sel.value);
})();
</script>
{{end}}
@@ -103,6 +103,28 @@
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:6px;">Opis kvara</div>
<div style="font-size:14px;color:var(--tekst-glavni);line-height:1.6;white-space:pre-wrap;">{{.Nalog.OpisKvara}}</div>
</div>
{{if or .Nalog.Ostecenja .Nalog.PinUredjaja .Nalog.Pribor}}
<div style="display:grid;grid-template-columns:repeat(auto-fit, minmax(160px, 1fr));gap:16px;">
{{if .Nalog.Ostecenja}}
<div>
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:4px;">Oštećenja pri prijemu</div>
<div style="font-size:14px;color:var(--tekst-glavni);">{{.Nalog.Ostecenja}}</div>
</div>
{{end}}
{{if .Nalog.PinUredjaja}}
<div>
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:4px;">PIN / lozinka uređaja</div>
<div style="font-size:14px;color:var(--tekst-glavni);font-family:monospace;">{{.Nalog.PinUredjaja}}</div>
</div>
{{end}}
{{if .Nalog.Pribor}}
<div>
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:4px;">Pribor i oprema</div>
<div style="font-size:14px;color:var(--tekst-glavni);">{{.Nalog.Pribor}}</div>
</div>
{{end}}
</div>
{{end}}
{{if .Nalog.Napomena}}
<div>
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:6px;">Napomena</div>
+69 -12
View File
@@ -12,11 +12,11 @@
</a>
<div class="kartica forma-kartica animiraj">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:20px;padding-bottom:14px;border-bottom:0.5px solid var(--ivica);flex-wrap:wrap;gap:10px;">
<span style="font-size:16px;font-weight:500;color:var(--tekst-glavni);">
<div style="display:flex;justify-content:space-between;align-items:center;margin:-16px -16px 20px -16px;padding:12px 16px;background:var(--pozadina);border-radius:12px 12px 0 0;flex-wrap:wrap;gap:10px;">
<span style="font-size:15px;font-weight:600;color:var(--tekst-glavni);">
{{if .Izmena}}Izmeni nalog{{else}}Novi nalog{{end}}
</span>
<span style="font-size:14px;font-weight:500;color:var(--tekst-sporedni);font-family:monospace;">
<span style="font-size:13px;font-weight:500;color:var(--tekst-sporedni);font-family:monospace;">
{{.Nalog.BrojNaloga}}
</span>
</div>
@@ -60,6 +60,26 @@
placeholder="Opišite problem koji je klijent prijavio..."
style="width:100%;resize:vertical;">{{.Nalog.OpisKvara}}</textarea>
</div>
<div class="forma-grid-2" style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
<div>
<label class="polje-labela">Oštećenja pri prijemu</label>
<input type="text" name="ostecenja" value="{{.Nalog.Ostecenja}}"
placeholder="npr. ogrebotina na ekranu"
style="width:100%;">
</div>
<div>
<label class="polje-labela">PIN / lozinka uređaja</label>
<input type="text" name="pin_uredjaja" value="{{.Nalog.PinUredjaja}}"
placeholder="npr. 1234"
style="width:100%;">
</div>
</div>
<div>
<label class="polje-labela">Pribor i oprema</label>
<input type="text" name="pribor" value="{{.Nalog.Pribor}}"
placeholder="npr. punjač, torbica, miš"
style="width:100%;">
</div>
</div>
</div>
@@ -97,8 +117,8 @@
<!-- status i datumi -->
<div>
<div class="sekcija-naslov">Status i datumi</div>
<div class="forma-grid-4" style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:12px;">
<div style="grid-column:span 2;">
<div class="kolona" style="gap:12px;">
<div>
<label class="polje-labela">Status naloga</label>
<select name="status" style="width:100%;">
{{range .SviStatusi}}
@@ -106,17 +126,38 @@
{{end}}
</select>
</div>
<div>
<label class="polje-labela">Datum završetka</label>
<input type="date" name="datum_zavrsetka"
value="{{if .Nalog.DatumZavrsetka}}{{.Nalog.DatumZavrsetka.Format "2006-01-02"}}{{end}}"
style="width:100%;">
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:12px;">
<div>
<label class="polje-labela">Datum prijema</label>
<input type="date" name="datum_prijema"
value="{{.Nalog.DatumPrijema.Format "2006-01-02"}}"
style="width:100%;">
</div>
<div>
<label class="polje-labela">Datum završetka</label>
<input type="date" name="datum_zavrsetka"
value="{{if .Nalog.DatumZavrsetka}}{{.Nalog.DatumZavrsetka.Format "2006-01-02"}}{{end}}"
style="width:100%;">
</div>
</div>
<div>
<label class="polje-labela">Garancija do</label>
<input type="date" name="garancija_do"
<input type="date" name="garancija_do" id="garancija_do_input"
value="{{if .Nalog.GarancijaDo}}{{.Nalog.GarancijaDo.Format "2006-01-02"}}{{end}}"
style="width:100%;">
{{if not .Nalog.GarancijaDo}}disabled{{end}}
style="width:100%;{{if not .Nalog.GarancijaDo}}opacity:0.4;{{end}}">
</div>
<!-- svič bez garancije -->
<div style="display:flex;align-items:center;gap:10px;">
<label class="toggl" style="flex-shrink:0;">
<input type="checkbox" id="bez_garancije_chk"
{{if not .Nalog.GarancijaDo}}checked{{end}}
onchange="toggleGarancija(this)">
<span class="toggl-klizac"></span>
</label>
<input type="hidden" name="bez_garancije" id="bez_garancije_val"
value="{{if not .Nalog.GarancijaDo}}1{{else}}0{{end}}">
<span style="font-size:13px;color:var(--tekst-sporedni);">Bez garancije</span>
</div>
</div>
</div>
@@ -172,4 +213,20 @@
</form>
</div>
</div>
<script>
function toggleGarancija(chk) {
var input = document.getElementById('garancija_do_input');
var hidden = document.getElementById('bez_garancije_val');
if (chk.checked) {
input.value = '';
input.disabled = true;
input.style.opacity = '0.4';
hidden.value = '1';
} else {
input.disabled = false;
input.style.opacity = '1';
hidden.value = '0';
}
}
</script>
{{end}}
+1 -1
View File
@@ -106,7 +106,7 @@
</script>
{{end}}
</head>
<body>
<body{{if .LokalnaAnimacija}} data-animacija="{{.LokalnaAnimacija}}"{{end}}{{if .LokalniHover}} data-hover="{{.LokalniHover}}"{{end}}>
<div class="raspored">
<div class="sidebar-overlay" id="sidebar-overlay"></div>
{{template "sidebar" .}}