diff --git a/cmd/ntech/main.go b/cmd/ntech/main.go index 92185d2..00c9eef 100644 --- a/cmd/ntech/main.go +++ b/cmd/ntech/main.go @@ -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) diff --git a/go.mod b/go.mod index d385f3a..4d38521 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 01558cc..86ae5aa 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/db/repository.go b/internal/db/repository.go index f7d9893..92131f9 100644 --- a/internal/db/repository.go +++ b/internal/db/repository.go @@ -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 diff --git a/internal/db/sqlite/korisnici.go b/internal/db/sqlite/korisnici.go index 430e29c..01fb7fa 100644 --- a/internal/db/sqlite/korisnici.go +++ b/internal/db/sqlite/korisnici.go @@ -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 { diff --git a/internal/db/sqlite/servis.go b/internal/db/sqlite/servis.go index 4884e40..37a55bf 100644 --- a/internal/db/sqlite/servis.go +++ b/internal/db/sqlite/servis.go @@ -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 diff --git a/internal/handler/admin.go b/internal/handler/admin.go index 26e7fe1..af4c783 100644 --- a/internal/handler/admin.go +++ b/internal/handler/admin.go @@ -46,6 +46,8 @@ type podaciProfilTema struct { LokalnaPozadinaBlur string LokalnaPozadinaBlurPozadine string LokalnaPozadinaGlassOpacity string + LokalnaAnimacija string + LokalniHover string } // AdminKorisnici prikazuje listu korisnika diff --git a/internal/handler/handler.go b/internal/handler/handler.go index 717e218..8b5a35a 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -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 } diff --git a/internal/handler/kes.go b/internal/handler/kes.go index 43b72a9..d028f8c 100644 --- a/internal/handler/kes.go +++ b/internal/handler/kes.go @@ -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", diff --git a/internal/handler/nivelacija.go b/internal/handler/nivelacija.go index 00cc85f..2b35347 100644 --- a/internal/handler/nivelacija.go +++ b/internal/handler/nivelacija.go @@ -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) diff --git a/internal/handler/pdv_kir.go b/internal/handler/pdv_kir.go index dd8bace..73dd50b 100644 --- a/internal/handler/pdv_kir.go +++ b/internal/handler/pdv_kir.go @@ -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) diff --git a/internal/handler/pdv_kpr.go b/internal/handler/pdv_kpr.go index 9a17c97..956fb13 100644 --- a/internal/handler/pdv_kpr.go +++ b/internal/handler/pdv_kpr.go @@ -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) diff --git a/internal/handler/podesavanja.go b/internal/handler/podesavanja.go index 185cb37..e3be568 100644 --- a/internal/handler/podesavanja.go +++ b/internal/handler/podesavanja.go @@ -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, 0–120) + 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 } diff --git a/internal/handler/profil.go b/internal/handler/profil.go index be873fa..b00cdaa 100644 --- a/internal/handler/profil.go +++ b/internal/handler/profil.go @@ -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()) diff --git a/internal/handler/servis.go b/internal/handler/servis.go index fc200e1..6cfe5cb 100644 --- a/internal/handler/servis.go +++ b/internal/handler/servis.go @@ -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) diff --git a/internal/model/korisnik.go b/internal/model/korisnik.go index f4b606a..b572348 100644 --- a/internal/model/korisnik.go +++ b/internal/model/korisnik.go @@ -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 diff --git a/internal/model/servis.go b/internal/model/servis.go index 1cdb607..d34723e 100644 --- a/internal/model/servis.go +++ b/internal/model/servis.go @@ -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 diff --git a/internal/model/stranica.go b/internal/model/stranica.go index 3699fd3..b1fee7e 100644 --- a/internal/model/stranica.go +++ b/internal/model/stranica.go @@ -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 diff --git a/migrations/051_servis_prijem_detalji.sql b/migrations/051_servis_prijem_detalji.sql new file mode 100644 index 0000000..00dbca2 --- /dev/null +++ b/migrations/051_servis_prijem_detalji.sql @@ -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; diff --git a/migrations/052_servis_podesavanja.sql b/migrations/052_servis_podesavanja.sql new file mode 100644 index 0000000..1533fc8 --- /dev/null +++ b/migrations/052_servis_podesavanja.sql @@ -0,0 +1,2 @@ +INSERT OR IGNORE INTO podesavanja (kljuc, vrednost) VALUES + ('servis_garancija_meseci', '2'); diff --git a/migrations/053_lokalna_animacija.sql b/migrations/053_lokalna_animacija.sql new file mode 100644 index 0000000..5767fc6 --- /dev/null +++ b/migrations/053_lokalna_animacija.sql @@ -0,0 +1 @@ +ALTER TABLE korisnici ADD COLUMN lokalna_animacija TEXT; diff --git a/migrations/054_lokalni_hover.sql b/migrations/054_lokalni_hover.sql new file mode 100644 index 0000000..09fdf35 --- /dev/null +++ b/migrations/054_lokalni_hover.sql @@ -0,0 +1 @@ +ALTER TABLE korisnici ADD COLUMN lokalni_hover TEXT; diff --git a/web/static/css/main.css b/web/static/css/main.css index 2a6d542..1e72776 100644 --- a/web/static/css/main.css +++ b/web/static/css/main.css @@ -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 (