Compare commits
13 Commits
880456a5ba
...
1068bb12e0
| Author | SHA1 | Date | |
|---|---|---|---|
|
1068bb12e0
|
|||
|
8c0e9d50a0
|
|||
|
6f0ad3f29c
|
|||
|
070f9384cf
|
|||
|
fa717208c5
|
|||
|
8855b5b84f
|
|||
|
45e4863ebb
|
|||
|
8e1cf67618
|
|||
|
fd35408da7
|
|||
|
755f56f87a
|
|||
|
4047f035da
|
|||
|
2937acfcc1
|
|||
|
f7a5d2673b
|
@@ -239,6 +239,7 @@ func main() {
|
|||||||
r.Get("/setup", h.PrikazSetupa)
|
r.Get("/setup", h.PrikazSetupa)
|
||||||
r.Post("/setup", h.SacuvajSetup)
|
r.Post("/setup", h.SacuvajSetup)
|
||||||
r.Get("/odjava", h.Odjava)
|
r.Get("/odjava", h.Odjava)
|
||||||
|
r.Get("/status/{token}", h.ServisJavniStatus)
|
||||||
|
|
||||||
// zaštićene rute — zahtevaju prijavljenog korisnika
|
// zaštićene rute — zahtevaju prijavljenog korisnika
|
||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
@@ -393,6 +394,7 @@ func main() {
|
|||||||
r.With(doz("tema.lokalno")).Post("/profil/tema", h.SacuvajLokalnuTemu)
|
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/animacija", h.SacuvajLokalnuAnimaciju)
|
||||||
r.With(doz("tema.lokalno")).Post("/profil/hover", h.SacuvajLokalniHover)
|
r.With(doz("tema.lokalno")).Post("/profil/hover", h.SacuvajLokalniHover)
|
||||||
|
r.With(doz("tema.lokalno")).Post("/profil/brzina-animacije", h.SacuvajLokalnuBrzinuAnimacije)
|
||||||
r.Get("/profil/tema", h.ProfilTema)
|
r.Get("/profil/tema", h.ProfilTema)
|
||||||
r.With(doz("tema.lokalno")).Post("/profil/pozadina", h.ProfilOtpremiPozadinu)
|
r.With(doz("tema.lokalno")).Post("/profil/pozadina", h.ProfilOtpremiPozadinu)
|
||||||
r.With(doz("tema.lokalno")).Post("/profil/pozadina/ukloni", h.ProfilUkloniPozadinu)
|
r.With(doz("tema.lokalno")).Post("/profil/pozadina/ukloni", h.ProfilUkloniPozadinu)
|
||||||
|
|||||||
@@ -27,6 +27,17 @@ func ProveriLozinku(hash, lozinka string) bool {
|
|||||||
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(lozinka)) == nil
|
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(lozinka)) == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// dummyHash je bcrypt heš fiksne vrednosti, izračunat jednom pri pokretanju.
|
||||||
|
// Koristi ga IzjednaciVremeProvere kada korisnik ne postoji.
|
||||||
|
var dummyHash, _ = bcrypt.GenerateFromPassword([]byte("ntech-dummy-lozinka"), bcryptCost)
|
||||||
|
|
||||||
|
// IzjednaciVremeProvere izvršava bcrypt poređenje protiv fiksnog heša da bi vreme
|
||||||
|
// odgovora bilo isto kao kod postojećeg korisnika sa pogrešnom lozinkom —
|
||||||
|
// time se sprečava enumeracija korisničkih imena merenjem vremena odgovora.
|
||||||
|
func IzjednaciVremeProvere(lozinka string) {
|
||||||
|
_ = bcrypt.CompareHashAndPassword(dummyHash, []byte(lozinka))
|
||||||
|
}
|
||||||
|
|
||||||
// GenerisiToken generiše nasumičan UUID token za sesiju
|
// GenerisiToken generiše nasumičan UUID token za sesiju
|
||||||
func GenerisiToken() string {
|
func GenerisiToken() string {
|
||||||
return uuid.New().String()
|
return uuid.New().String()
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ type KlijentRepository interface {
|
|||||||
type ServisRepository interface {
|
type ServisRepository interface {
|
||||||
Lista(ctx context.Context, pretraga, status string) ([]model.ServisniNalogSaKlijentom, error)
|
Lista(ctx context.Context, pretraga, status string) ([]model.ServisniNalogSaKlijentom, error)
|
||||||
DohvatiID(ctx context.Context, id int64) (*model.ServisniNalog, error)
|
DohvatiID(ctx context.Context, id int64) (*model.ServisniNalog, error)
|
||||||
|
DohvatiJavniToken(ctx context.Context, token string) (*model.ServisniNalog, error)
|
||||||
Kreiraj(ctx context.Context, n *model.ServisniNalog) (int64, error)
|
Kreiraj(ctx context.Context, n *model.ServisniNalog) (int64, error)
|
||||||
Izmeni(ctx context.Context, n *model.ServisniNalog) error
|
Izmeni(ctx context.Context, n *model.ServisniNalog) error
|
||||||
AzurirajStatus(ctx context.Context, id int64, status string) error
|
AzurirajStatus(ctx context.Context, id int64, status string) error
|
||||||
@@ -156,6 +157,7 @@ type KorisniciRepository interface {
|
|||||||
SacuvajLokalnuPozadinu(ctx context.Context, id int64, pozadina, opacity, blur, blurPozadine, glassOpacity string) error
|
SacuvajLokalnuPozadinu(ctx context.Context, id int64, pozadina, opacity, blur, blurPozadine, glassOpacity string) error
|
||||||
SacuvajLokalnuAnimaciju(ctx context.Context, id int64, animacija string) error
|
SacuvajLokalnuAnimaciju(ctx context.Context, id int64, animacija string) error
|
||||||
SacuvajLokalniHover(ctx context.Context, id int64, hover string) error
|
SacuvajLokalniHover(ctx context.Context, id int64, hover string) error
|
||||||
|
SacuvajLokalnuBrzinuAnimacije(ctx context.Context, id int64, brzina string) error
|
||||||
SacuvajAvatar(ctx context.Context, id int64, putanja string) error
|
SacuvajAvatar(ctx context.Context, id int64, putanja string) error
|
||||||
PostojiIjedan(ctx context.Context) (bool, error)
|
PostojiIjedan(ctx context.Context) (bool, error)
|
||||||
Obrisi(ctx context.Context, id int64) error
|
Obrisi(ctx context.Context, id int64) error
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ type korisnikOpcije struct {
|
|||||||
avatarPutanja sql.NullString
|
avatarPutanja sql.NullString
|
||||||
lokalnaAnimacija sql.NullString
|
lokalnaAnimacija sql.NullString
|
||||||
lokalniHover sql.NullString
|
lokalniHover sql.NullString
|
||||||
|
lokalnaBrzinaAnimacije sql.NullString
|
||||||
}
|
}
|
||||||
|
|
||||||
// dodeliOpcijeKorisnika prenosi vrednosti iz korisnikOpcije na model.Korisnik
|
// dodeliOpcijeKorisnika prenosi vrednosti iz korisnikOpcije na model.Korisnik
|
||||||
@@ -48,6 +49,7 @@ func dodeliOpcijeKorisnika(k *model.Korisnik, o korisnikOpcije) {
|
|||||||
k.AvatarPutanja = o.avatarPutanja.String
|
k.AvatarPutanja = o.avatarPutanja.String
|
||||||
k.LokalnaAnimacija = o.lokalnaAnimacija.String
|
k.LokalnaAnimacija = o.lokalnaAnimacija.String
|
||||||
k.LokalniHover = o.lokalniHover.String
|
k.LokalniHover = o.lokalniHover.String
|
||||||
|
k.LokalnaBrzinaAnimacije = o.lokalnaBrzinaAnimacije.String
|
||||||
}
|
}
|
||||||
|
|
||||||
// skeniraiKorisnika čita jedan red iz baze i popunjava model.Korisnik
|
// skeniraiKorisnika čita jedan red iz baze i popunjava model.Korisnik
|
||||||
@@ -59,7 +61,7 @@ func skeniraiKorisnika(row interface{ Scan(...any) error }) (*model.Korisnik, er
|
|||||||
&o.lokalnaTema, &o.koristiLokalnuTemu, &o.datumKreiranja,
|
&o.lokalnaTema, &o.koristiLokalnuTemu, &o.datumKreiranja,
|
||||||
&o.lokalnaPozadina, &o.lokalnaPozadinaOpacity, &o.lokalnaPozadinaBlur,
|
&o.lokalnaPozadina, &o.lokalnaPozadinaOpacity, &o.lokalnaPozadinaBlur,
|
||||||
&o.lokalnaPozadinaBlurPozadine, &o.lokalnaPozadinaGlassOpacity, &o.avatarPutanja,
|
&o.lokalnaPozadinaBlurPozadine, &o.lokalnaPozadinaGlassOpacity, &o.avatarPutanja,
|
||||||
&o.lokalnaAnimacija, &o.lokalniHover,
|
&o.lokalnaAnimacija, &o.lokalniHover, &o.lokalnaBrzinaAnimacije,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -104,7 +106,8 @@ func (r *sqliteKorisniciRepo) DohvatiPoImenu(ctx context.Context, korisnickoIme
|
|||||||
COALESCE(lokalna_pozadina, ''), COALESCE(lokalna_pozadina_opacity, '50'),
|
COALESCE(lokalna_pozadina, ''), COALESCE(lokalna_pozadina_opacity, '50'),
|
||||||
COALESCE(lokalna_pozadina_blur, '12'), COALESCE(lokalna_pozadina_blur_pozadine, '0'),
|
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, '')
|
COALESCE(lokalna_animacija, ''), COALESCE(lokalni_hover, ''),
|
||||||
|
COALESCE(lokalna_brzina_animacije, '')
|
||||||
FROM korisnici WHERE korisnicko_ime = ?`, korisnickoIme)
|
FROM korisnici WHERE korisnicko_ime = ?`, korisnickoIme)
|
||||||
k, err := skeniraiKorisnika(row)
|
k, err := skeniraiKorisnika(row)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -121,7 +124,8 @@ func (r *sqliteKorisniciRepo) DohvatiPoID(ctx context.Context, id int64) (*model
|
|||||||
COALESCE(lokalna_pozadina, ''), COALESCE(lokalna_pozadina_opacity, '50'),
|
COALESCE(lokalna_pozadina, ''), COALESCE(lokalna_pozadina_opacity, '50'),
|
||||||
COALESCE(lokalna_pozadina_blur, '12'), COALESCE(lokalna_pozadina_blur_pozadine, '0'),
|
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, '')
|
COALESCE(lokalna_animacija, ''), COALESCE(lokalni_hover, ''),
|
||||||
|
COALESCE(lokalna_brzina_animacije, '')
|
||||||
FROM korisnici WHERE id = ?`, id)
|
FROM korisnici WHERE id = ?`, id)
|
||||||
k, err := skeniraiKorisnika(row)
|
k, err := skeniraiKorisnika(row)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -138,7 +142,8 @@ func (r *sqliteKorisniciRepo) Lista(ctx context.Context) ([]model.Korisnik, erro
|
|||||||
COALESCE(lokalna_pozadina, ''), COALESCE(lokalna_pozadina_opacity, '50'),
|
COALESCE(lokalna_pozadina, ''), COALESCE(lokalna_pozadina_opacity, '50'),
|
||||||
COALESCE(lokalna_pozadina_blur, '12'), COALESCE(lokalna_pozadina_blur_pozadine, '0'),
|
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, '')
|
COALESCE(lokalna_animacija, ''), COALESCE(lokalni_hover, ''),
|
||||||
|
COALESCE(lokalna_brzina_animacije, '')
|
||||||
FROM korisnici ORDER BY datum_kreiranja ASC`)
|
FROM korisnici ORDER BY datum_kreiranja ASC`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("ntech: korisnici.Lista: %w", err)
|
return nil, fmt.Errorf("ntech: korisnici.Lista: %w", err)
|
||||||
@@ -153,7 +158,7 @@ func (r *sqliteKorisniciRepo) Lista(ctx context.Context) ([]model.Korisnik, erro
|
|||||||
&o.lokalnaTema, &o.koristiLokalnuTemu, &o.datumKreiranja,
|
&o.lokalnaTema, &o.koristiLokalnuTemu, &o.datumKreiranja,
|
||||||
&o.lokalnaPozadina, &o.lokalnaPozadinaOpacity, &o.lokalnaPozadinaBlur,
|
&o.lokalnaPozadina, &o.lokalnaPozadinaOpacity, &o.lokalnaPozadinaBlur,
|
||||||
&o.lokalnaPozadinaBlurPozadine, &o.lokalnaPozadinaGlassOpacity, &o.avatarPutanja,
|
&o.lokalnaPozadinaBlurPozadine, &o.lokalnaPozadinaGlassOpacity, &o.avatarPutanja,
|
||||||
&o.lokalnaAnimacija, &o.lokalniHover,
|
&o.lokalnaAnimacija, &o.lokalniHover, &o.lokalnaBrzinaAnimacije,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, fmt.Errorf("ntech: korisnici.Lista: %w", err)
|
return nil, fmt.Errorf("ntech: korisnici.Lista: %w", err)
|
||||||
}
|
}
|
||||||
@@ -229,6 +234,19 @@ func (r *sqliteKorisniciRepo) SacuvajLokalniHover(ctx context.Context, id int64,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *sqliteKorisniciRepo) SacuvajLokalnuBrzinuAnimacije(ctx context.Context, id int64, brzina string) error {
|
||||||
|
var val any
|
||||||
|
if brzina != "" {
|
||||||
|
val = brzina
|
||||||
|
}
|
||||||
|
_, err := r.db.ExecContext(ctx,
|
||||||
|
`UPDATE korisnici SET lokalna_brzina_animacije = ? WHERE id = ?`, val, id)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ntech: korisnici.SacuvajLokalnuBrzinuAnimacije: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *sqliteKorisniciRepo) AzurirajUlogu(ctx context.Context, id int64, uloga string) error {
|
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)
|
_, err := r.db.ExecContext(ctx, `UPDATE korisnici SET uloga = ? WHERE id = ?`, uloga, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -2,13 +2,24 @@ package sqlite
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"ntech/internal/model"
|
"ntech/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// generisiJavniToken kreira 32-znakovni hex token za javni URL
|
||||||
|
func generisiJavniToken() (string, error) {
|
||||||
|
b := make([]byte, 16)
|
||||||
|
if _, err := rand.Read(b); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
// ServisRepo je SQLite implementacija ServisRepository interfejsa
|
// ServisRepo je SQLite implementacija ServisRepository interfejsa
|
||||||
type ServisRepo struct {
|
type ServisRepo struct {
|
||||||
db *sql.DB
|
db *sql.DB
|
||||||
@@ -43,7 +54,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.id, sn.klijent_id, sn.tehnicar_id, sn.broj_naloga, sn.uredjaj, sn.serijski_broj,
|
||||||
sn.opis_kvara, sn.status, sn.cena_od, sn.cena_do, sn.cena_konacna,
|
sn.opis_kvara, sn.status, sn.cena_od, sn.cena_do, sn.cena_konacna,
|
||||||
sn.avans, sn.napomena, sn.garancija_do, sn.datum_prijema, sn.datum_zavrsetka,
|
sn.avans, sn.napomena, sn.garancija_do, sn.datum_prijema, sn.datum_zavrsetka,
|
||||||
sn.ostecenja, sn.pin_uredjaja, sn.pribor,
|
sn.ostecenja, sn.pin_uredjaja, sn.pribor, sn.javni_token,
|
||||||
COALESCE(kp.naziv, '') AS klijent_naziv
|
COALESCE(kp.naziv, '') AS klijent_naziv
|
||||||
FROM servisni_nalozi sn
|
FROM servisni_nalozi sn
|
||||||
LEFT JOIN klijent_prikaz kp ON kp.id = sn.klijent_id
|
LEFT JOIN klijent_prikaz kp ON kp.id = sn.klijent_id
|
||||||
@@ -90,7 +101,7 @@ func (r *ServisRepo) DohvatiID(ctx context.Context, id int64) (*model.ServisniNa
|
|||||||
id, klijent_id, tehnicar_id, broj_naloga, uredjaj, serijski_broj,
|
id, klijent_id, tehnicar_id, broj_naloga, uredjaj, serijski_broj,
|
||||||
opis_kvara, status, cena_od, cena_do, cena_konacna,
|
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
|
ostecenja, pin_uredjaja, pribor, javni_token
|
||||||
FROM servisni_nalozi WHERE id = ?`, id)
|
FROM servisni_nalozi WHERE id = ?`, id)
|
||||||
|
|
||||||
var n model.ServisniNalog
|
var n model.ServisniNalog
|
||||||
@@ -102,21 +113,26 @@ func (r *ServisRepo) DohvatiID(ctx context.Context, id int64) (*model.ServisniNa
|
|||||||
return &n, nil
|
return &n, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kreiraj upisuje novi servisni nalog u bazu
|
// Kreiraj upisuje novi servisni nalog u bazu i generiše javni token
|
||||||
func (r *ServisRepo) Kreiraj(ctx context.Context, n *model.ServisniNalog) (int64, error) {
|
func (r *ServisRepo) Kreiraj(ctx context.Context, n *model.ServisniNalog) (int64, error) {
|
||||||
|
token, err := generisiJavniToken()
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("ntech: ServisRepo.Kreiraj: token: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
rezultat, err := r.db.ExecContext(ctx, `
|
rezultat, err := r.db.ExecContext(ctx, `
|
||||||
INSERT INTO servisni_nalozi
|
INSERT INTO servisni_nalozi
|
||||||
(klijent_id, tehnicar_id, broj_naloga, uredjaj, serijski_broj, opis_kvara,
|
(klijent_id, tehnicar_id, broj_naloga, uredjaj, serijski_broj, opis_kvara,
|
||||||
status, cena_od, cena_do, cena_konacna, avans, napomena, garancija_do, datum_zavrsetka,
|
status, cena_od, cena_do, cena_konacna, avans, napomena, garancija_do, datum_zavrsetka,
|
||||||
ostecenja, pin_uredjaja, pribor, datum_prijema)
|
ostecenja, pin_uredjaja, pribor, datum_prijema, javni_token)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
nullInt64(n.KlijentID), nullInt64(n.TehnicarID), n.BrojNaloga, n.Uredjaj,
|
nullInt64(n.KlijentID), nullInt64(n.TehnicarID), n.BrojNaloga, n.Uredjaj,
|
||||||
nullString(n.SerijskiBroj), n.OpisKvara, n.Status,
|
nullString(n.SerijskiBroj), n.OpisKvara, n.Status,
|
||||||
nullFloat64(n.CenaOd), nullFloat64(n.CenaDo), nullFloat64(n.CenaKonacna),
|
nullFloat64(n.CenaOd), nullFloat64(n.CenaDo), nullFloat64(n.CenaKonacna),
|
||||||
nullFloat64(n.Avans), nullString(n.Napomena),
|
nullFloat64(n.Avans), nullString(n.Napomena),
|
||||||
nullTime(n.GarancijaDo), nullTime(n.DatumZavrsetka),
|
nullTime(n.GarancijaDo), nullTime(n.DatumZavrsetka),
|
||||||
nullString(n.Ostecenja), nullString(n.PinUredjaja), nullString(n.Pribor),
|
nullString(n.Ostecenja), nullString(n.PinUredjaja), nullString(n.Pribor),
|
||||||
n.DatumPrijema,
|
n.DatumPrijema, token,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("ntech: ServisRepo.Kreiraj: %w", err)
|
return 0, fmt.Errorf("ntech: ServisRepo.Kreiraj: %w", err)
|
||||||
@@ -130,6 +146,23 @@ func (r *ServisRepo) Kreiraj(ctx context.Context, n *model.ServisniNalog) (int64
|
|||||||
return id, nil
|
return id, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DohvatiJavniToken vraća servisni nalog po javnom tokenu — bez autentifikacije
|
||||||
|
func (r *ServisRepo) DohvatiJavniToken(ctx context.Context, token string) (*model.ServisniNalog, error) {
|
||||||
|
red := r.db.QueryRowContext(ctx, `
|
||||||
|
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,
|
||||||
|
ostecenja, pin_uredjaja, pribor, javni_token
|
||||||
|
FROM servisni_nalozi WHERE javni_token = ?`, token)
|
||||||
|
|
||||||
|
var n model.ServisniNalog
|
||||||
|
if err := scanNalog(red.Scan, &n, nil); err != nil {
|
||||||
|
return nil, fmt.Errorf("ntech: ServisRepo.DohvatiJavniToken: %w", err)
|
||||||
|
}
|
||||||
|
return &n, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Izmeni ažurira postojeći servisni nalog — broj_naloga i datum_prijema se ne menjaju
|
// Izmeni ažurira postojeći servisni nalog — broj_naloga i datum_prijema se ne menjaju
|
||||||
func (r *ServisRepo) Izmeni(ctx context.Context, n *model.ServisniNalog) error {
|
func (r *ServisRepo) Izmeni(ctx context.Context, n *model.ServisniNalog) error {
|
||||||
_, err := r.db.ExecContext(ctx, `
|
_, err := r.db.ExecContext(ctx, `
|
||||||
@@ -184,7 +217,7 @@ func (r *ServisRepo) Obrisi(ctx context.Context, id int64) error {
|
|||||||
// klijentNaziv je opcioni pokazivač, nil kada se čita bez JOIN-a
|
// klijentNaziv je opcioni pokazivač, nil kada se čita bez JOIN-a
|
||||||
func scanNalog(scan func(...any) error, n *model.ServisniNalog, klijentNaziv *string) error {
|
func scanNalog(scan func(...any) error, n *model.ServisniNalog, klijentNaziv *string) error {
|
||||||
var klijentID, tehnicarID sql.NullInt64
|
var klijentID, tehnicarID sql.NullInt64
|
||||||
var serijskiBroj, napomena, ostecenja, pinUredjaja, pribor sql.NullString
|
var serijskiBroj, napomena, ostecenja, pinUredjaja, pribor, javniToken sql.NullString
|
||||||
var cenaOd, cenaDo, cenaKonacna, avans sql.NullFloat64
|
var cenaOd, cenaDo, cenaKonacna, avans sql.NullFloat64
|
||||||
var garancijaDo, datumZavrsetka sql.NullTime
|
var garancijaDo, datumZavrsetka sql.NullTime
|
||||||
|
|
||||||
@@ -192,7 +225,7 @@ func scanNalog(scan func(...any) error, n *model.ServisniNalog, klijentNaziv *st
|
|||||||
&n.ID, &klijentID, &tehnicarID, &n.BrojNaloga, &n.Uredjaj, &serijskiBroj,
|
&n.ID, &klijentID, &tehnicarID, &n.BrojNaloga, &n.Uredjaj, &serijskiBroj,
|
||||||
&n.OpisKvara, &n.Status, &cenaOd, &cenaDo, &cenaKonacna,
|
&n.OpisKvara, &n.Status, &cenaOd, &cenaDo, &cenaKonacna,
|
||||||
&avans, &napomena, &garancijaDo, &n.DatumPrijema, &datumZavrsetka,
|
&avans, &napomena, &garancijaDo, &n.DatumPrijema, &datumZavrsetka,
|
||||||
&ostecenja, &pinUredjaja, &pribor,
|
&ostecenja, &pinUredjaja, &pribor, &javniToken,
|
||||||
}
|
}
|
||||||
|
|
||||||
if klijentNaziv != nil {
|
if klijentNaziv != nil {
|
||||||
@@ -240,6 +273,7 @@ func scanNalog(scan func(...any) error, n *model.ServisniNalog, klijentNaziv *st
|
|||||||
v := datumZavrsetka.Time
|
v := datumZavrsetka.Time
|
||||||
n.DatumZavrsetka = &v
|
n.DatumZavrsetka = &v
|
||||||
}
|
}
|
||||||
|
n.JavniToken = javniToken.String
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,8 +126,7 @@ func (h *Handler) AdminSacuvajKorisnika(w http.ResponseWriter, r *http.Request)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Korisnik je uspešno kreiran.")
|
http.Redirect(w, r, "/admin/korisnici?sacuvano=1", http.StatusSeeOther)
|
||||||
http.Redirect(w, r, "/admin/korisnici", http.StatusSeeOther)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AdminToggleAktivan menja aktivan status korisnika
|
// AdminToggleAktivan menja aktivan status korisnika
|
||||||
@@ -172,8 +171,7 @@ func (h *Handler) AdminToggleAktivan(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Promene su uspešno sačuvane.")
|
http.Redirect(w, r, "/admin/korisnici?sacuvano=1", http.StatusSeeOther)
|
||||||
http.Redirect(w, r, "/admin/korisnici", http.StatusSeeOther)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AdminPromeniUlogu menja ulogu korisnika
|
// AdminPromeniUlogu menja ulogu korisnika
|
||||||
@@ -234,8 +232,7 @@ func (h *Handler) AdminPromeniUlogu(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Promene su uspešno sačuvane.")
|
http.Redirect(w, r, "/admin/korisnici?sacuvano=1", http.StatusSeeOther)
|
||||||
http.Redirect(w, r, "/admin/korisnici", http.StatusSeeOther)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AdminObrisiKorisnika briše korisnika sa ulogom radnik
|
// AdminObrisiKorisnika briše korisnika sa ulogom radnik
|
||||||
@@ -279,8 +276,7 @@ func (h *Handler) AdminObrisiKorisnika(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Korisnik je obrisan.")
|
http.Redirect(w, r, "/admin/korisnici?sacuvano=1", http.StatusSeeOther)
|
||||||
http.Redirect(w, r, "/admin/korisnici", http.StatusSeeOther)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AdminProfil prikazuje stranicu profila
|
// AdminProfil prikazuje stranicu profila
|
||||||
@@ -406,8 +402,7 @@ func (h *Handler) AdminPromeniLozinku(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Lozinka je uspešno promenjena.")
|
http.Redirect(w, r, "/admin/profil?sacuvano=1", http.StatusSeeOther)
|
||||||
http.Redirect(w, r, "/admin/profil", http.StatusSeeOther)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AdminTotpPokreni generiše TOTP tajnu i prikazuje QR kod
|
// AdminTotpPokreni generiše TOTP tajnu i prikazuje QR kod
|
||||||
@@ -529,8 +524,7 @@ func (h *Handler) AdminTotpDeaktivacija(w http.ResponseWriter, r *http.Request)
|
|||||||
// isključenjem 2FA brišemo i rezervne kodove
|
// isključenjem 2FA brišemo i rezervne kodove
|
||||||
_ = h.RezervniKodoviRepo.Obrisi(r.Context(), k.ID)
|
_ = h.RezervniKodoviRepo.Obrisi(r.Context(), k.ID)
|
||||||
|
|
||||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Dvostepena verifikacija je isključena.")
|
http.Redirect(w, r, "/admin/profil?sacuvano=1", http.StatusSeeOther)
|
||||||
http.Redirect(w, r, "/admin/profil", http.StatusSeeOther)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AdminLoginIstorija prikazuje evidenciju prijava za datog korisnika
|
// AdminLoginIstorija prikazuje evidenciju prijava za datog korisnika
|
||||||
@@ -679,8 +673,7 @@ func (h *Handler) AdminDozvolePromeniUlogu(w http.ResponseWriter, r *http.Reques
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Promene su uspešno sačuvane.")
|
http.Redirect(w, r, "/admin/dozvole?sacuvano=1", http.StatusSeeOther)
|
||||||
http.Redirect(w, r, "/admin/dozvole", http.StatusSeeOther)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AdminDozvoleSacuvaj prima POST i čuva promene dozvola za radnik i admin uloge
|
// AdminDozvoleSacuvaj prima POST i čuva promene dozvola za radnik i admin uloge
|
||||||
@@ -711,8 +704,7 @@ func (h *Handler) AdminDozvoleSacuvaj(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Promene su uspešno sačuvane.")
|
http.Redirect(w, r, "/admin/dozvole?sacuvano=1", http.StatusSeeOther)
|
||||||
http.Redirect(w, r, "/admin/dozvole", http.StatusSeeOther)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AdminDozvoleReset vraća sve dozvole na podrazumevane vrednosti — samo superadmin
|
// AdminDozvoleReset vraća sve dozvole na podrazumevane vrednosti — samo superadmin
|
||||||
@@ -727,6 +719,5 @@ func (h *Handler) AdminDozvoleReset(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Redirect(w, r, "/admin/dozvole", http.StatusSeeOther)
|
http.Redirect(w, r, "/admin/dozvole", http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Dozvole su vraćene na podrazumevane vrednosti.")
|
http.Redirect(w, r, "/admin/dozvole?sacuvano=1", http.StatusSeeOther)
|
||||||
http.Redirect(w, r, "/admin/dozvole", http.StatusSeeOther)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -215,6 +215,7 @@ func (h *Handler) popuniPodaciStranice(r *http.Request, podesavanja map[string]s
|
|||||||
if korisnik != nil {
|
if korisnik != nil {
|
||||||
ps.LokalnaAnimacija = korisnik.LokalnaAnimacija
|
ps.LokalnaAnimacija = korisnik.LokalnaAnimacija
|
||||||
ps.LokalniHover = korisnik.LokalniHover
|
ps.LokalniHover = korisnik.LokalniHover
|
||||||
|
ps.LokalnaBrzinaAnimacije = korisnik.LokalnaBrzinaAnimacije
|
||||||
}
|
}
|
||||||
|
|
||||||
return ps
|
return ps
|
||||||
|
|||||||
+24
-2
@@ -5,6 +5,7 @@ import (
|
|||||||
"html/template"
|
"html/template"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -38,13 +39,33 @@ var saSidebar = []string{
|
|||||||
|
|
||||||
// standalone su šabloni bez base layouta
|
// standalone su šabloni bez base layouta
|
||||||
var standaloneIme = []string{
|
var standaloneIme = []string{
|
||||||
"prijava", "setup", "totp_provera", "prodaja_stampa", "servis_stampa", "servis_otpremnica",
|
"prijava", "setup", "totp_provera", "prodaja_stampa", "servis_stampa", "servis_otpremnica", "servis_status_javni",
|
||||||
}
|
}
|
||||||
|
|
||||||
// sablonskeFunkcije su pomoćne funkcije dostupne u svim šablonima.
|
// sablonskeFunkcije su pomoćne funkcije dostupne u svim šablonima.
|
||||||
// dict gradi mapu iz parova ključ/vrednost — koristi se da se jednom partialu
|
// dict gradi mapu iz parova ključ/vrednost — koristi se da se jednom partialu
|
||||||
// prosledi više vrednosti (npr. {{template "x" (dict "ID" .ID "Lista" $.Lista)}}).
|
// prosledi više vrednosti (npr. {{template "x" (dict "ID" .ID "Lista" $.Lista)}}).
|
||||||
var sablonskeFunkcije = template.FuncMap{
|
var sablonskeFunkcije = template.FuncMap{
|
||||||
|
// formatBroj formatira float pointer kao ceo broj (zaokružen) — nil vraca ""
|
||||||
|
"formatBroj": func(v *float64) string {
|
||||||
|
if v == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d", int64(math.Round(*v)))
|
||||||
|
},
|
||||||
|
// statusPre vraća true ako je `a` pre `b` u redosledu statusa
|
||||||
|
"statusPre": func(a, b string, statusi []string) bool {
|
||||||
|
ia, ib := -1, -1
|
||||||
|
for i, s := range statusi {
|
||||||
|
if s == a {
|
||||||
|
ia = i
|
||||||
|
}
|
||||||
|
if s == b {
|
||||||
|
ib = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ia >= 0 && ib >= 0 && ia < ib
|
||||||
|
},
|
||||||
"dict": func(parovi ...any) (map[string]any, error) {
|
"dict": func(parovi ...any) (map[string]any, error) {
|
||||||
if len(parovi)%2 != 0 {
|
if len(parovi)%2 != 0 {
|
||||||
return nil, fmt.Errorf("dict: neparan broj argumenata")
|
return nil, fmt.Errorf("dict: neparan broj argumenata")
|
||||||
@@ -77,7 +98,8 @@ func KreirajKes(fsys fs.FS) (map[string]*template.Template, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, ime := range standaloneIme {
|
for _, ime := range standaloneIme {
|
||||||
t, err := template.ParseFS(fsys, "web/templates/stranice/"+ime+".html")
|
// ime+".html" mora biti ime roota da bi Execute() pronašlo sadržaj fajla
|
||||||
|
t, err := template.New(ime+".html").Funcs(sablonskeFunkcije).ParseFS(fsys, "web/templates/stranice/"+ime+".html")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("kes: %s: %w", ime, err)
|
return nil, fmt.Errorf("kes: %s: %w", ime, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,10 +93,11 @@ func (h *Handler) PromeniCenuArtikla(w http.ResponseWriter, r *http.Request) {
|
|||||||
switch {
|
switch {
|
||||||
case errors.Is(err, sqlite.ErrArtikalNePostoji):
|
case errors.Is(err, sqlite.ErrArtikalNePostoji):
|
||||||
middleware.SetFlash(w, r, h.DB, "greska", "Artikal nije pronađen.")
|
middleware.SetFlash(w, r, h.DB, "greska", "Artikal nije pronađen.")
|
||||||
|
http.Redirect(w, r, "/magacin", http.StatusSeeOther)
|
||||||
case err != nil:
|
case err != nil:
|
||||||
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri promeni cene.")
|
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri promeni cene.")
|
||||||
|
http.Redirect(w, r, "/magacin", http.StatusSeeOther)
|
||||||
default:
|
default:
|
||||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Prodajna cena je izmenjena.")
|
http.Redirect(w, r, "/magacin?sacuvano=1", http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
http.Redirect(w, r, "/magacin", http.StatusSeeOther)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -169,8 +169,7 @@ func (h *Handler) SacuvajPdvKir(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, "Greška pri čuvanju zapisa", http.StatusInternalServerError)
|
http.Error(w, "Greška pri čuvanju zapisa", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Izlazni račun je dodat u KIR.")
|
http.Redirect(w, r, "/pdv/kir?sacuvano=1", http.StatusSeeOther)
|
||||||
http.Redirect(w, r, "/pdv/kir", http.StatusSeeOther)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ObrisiPdvKir briše zapis iz KIR
|
// ObrisiPdvKir briše zapis iz KIR
|
||||||
@@ -187,6 +186,5 @@ func (h *Handler) ObrisiPdvKir(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, "Greška pri brisanju zapisa", http.StatusInternalServerError)
|
http.Error(w, "Greška pri brisanju zapisa", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Zapis je obrisan iz KIR.")
|
http.Redirect(w, r, "/pdv/kir?obrisan=1", http.StatusSeeOther)
|
||||||
http.Redirect(w, r, "/pdv/kir", http.StatusSeeOther)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,8 +154,7 @@ func (h *Handler) SacuvajPdvKpr(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, "Greška pri čuvanju zapisa", http.StatusInternalServerError)
|
http.Error(w, "Greška pri čuvanju zapisa", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Ulazni račun je dodat u KPR.")
|
http.Redirect(w, r, "/pdv/kpr?sacuvano=1", http.StatusSeeOther)
|
||||||
http.Redirect(w, r, "/pdv/kpr", http.StatusSeeOther)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ObrisiPdvKpr briše zapis iz KPR
|
// ObrisiPdvKpr briše zapis iz KPR
|
||||||
@@ -172,6 +171,5 @@ func (h *Handler) ObrisiPdvKpr(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, "Greška pri brisanju zapisa", http.StatusInternalServerError)
|
http.Error(w, "Greška pri brisanju zapisa", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Zapis je obrisan iz KPR.")
|
http.Redirect(w, r, "/pdv/kpr?obrisan=1", http.StatusSeeOther)
|
||||||
http.Redirect(w, r, "/pdv/kpr", http.StatusSeeOther)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,8 +99,7 @@ func (h *Handler) DodajPdvStopu(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, "Greška pri čuvanju PDV stope", http.StatusInternalServerError)
|
http.Error(w, "Greška pri čuvanju PDV stope", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
middleware.SetFlash(w, r, h.DB, "uspeh", "PDV stopa je dodata.")
|
http.Redirect(w, r, "/admin/podesavanja/kalkulacija-pdv?sacuvano=1", http.StatusSeeOther)
|
||||||
http.Redirect(w, r, "/admin/podesavanja/kalkulacija-pdv", http.StatusSeeOther)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// IzmeniPdvStopu prima POST i menja postojeću stopu
|
// IzmeniPdvStopu prima POST i menja postojeću stopu
|
||||||
@@ -128,8 +127,7 @@ func (h *Handler) IzmeniPdvStopu(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, "Greška pri izmeni PDV stope", http.StatusInternalServerError)
|
http.Error(w, "Greška pri izmeni PDV stope", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
middleware.SetFlash(w, r, h.DB, "uspeh", "PDV stopa je izmenjena.")
|
http.Redirect(w, r, "/admin/podesavanja/kalkulacija-pdv?sacuvano=1", http.StatusSeeOther)
|
||||||
http.Redirect(w, r, "/admin/podesavanja/kalkulacija-pdv", http.StatusSeeOther)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PromeniAktivnostPdvStope arhivira ili vraća stopu u upotrebu (toggle, bez brisanja)
|
// PromeniAktivnostPdvStope arhivira ili vraća stopu u upotrebu (toggle, bez brisanja)
|
||||||
@@ -151,10 +149,5 @@ func (h *Handler) PromeniAktivnostPdvStope(w http.ResponseWriter, r *http.Reques
|
|||||||
http.Error(w, "Greška pri promeni statusa PDV stope", http.StatusInternalServerError)
|
http.Error(w, "Greška pri promeni statusa PDV stope", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
poruka := "PDV stopa je arhivirana."
|
http.Redirect(w, r, "/admin/podesavanja/kalkulacija-pdv?sacuvano=1", http.StatusSeeOther)
|
||||||
if !postojeca.Aktivna {
|
|
||||||
poruka = "PDV stopa je vraćena u upotrebu."
|
|
||||||
}
|
|
||||||
middleware.SetFlash(w, r, h.DB, "uspeh", poruka)
|
|
||||||
http.Redirect(w, r, "/admin/podesavanja/kalkulacija-pdv", http.StatusSeeOther)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,13 +23,13 @@ import (
|
|||||||
// PodaciPodesavanja su podaci za stranicu podešavanja
|
// PodaciPodesavanja su podaci za stranicu podešavanja
|
||||||
type PodaciPodesavanja struct {
|
type PodaciPodesavanja struct {
|
||||||
model.PodaciStranice
|
model.PodaciStranice
|
||||||
NazivFirme string
|
NazivFirme string
|
||||||
Podnazlov string
|
Podnazlov string
|
||||||
Adresa string
|
Adresa string
|
||||||
Telefon string
|
Telefon string
|
||||||
PIB string
|
PIB string
|
||||||
LogoPutanja string
|
LogoPutanja string
|
||||||
TopbarLogoSlika bool
|
TopbarLogoSlika bool
|
||||||
// profil firme — pravni/poreski status (Faza 0); određuje koji se zakonski moduli pale
|
// profil firme — pravni/poreski status (Faza 0); određuje koji se zakonski moduli pale
|
||||||
FirmaPravniOblik string
|
FirmaPravniOblik string
|
||||||
FirmaPdvObveznik string
|
FirmaPdvObveznik string
|
||||||
@@ -266,8 +266,11 @@ func (h *Handler) SacuvajPodesavanja(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// whitelist dozvoljenih redirekcija — vrednost uvek dolazi iz mape, nikad od korisnika
|
// whitelist dozvoljenih redirekcija — vrednost uvek dolazi iz mape, nikad od korisnika
|
||||||
dozvoljeniSledeci := map[string]string{
|
dozvoljeniSledeci := map[string]string{
|
||||||
"/admin/podesavanja/opste": "/admin/podesavanja/opste",
|
"/admin/podesavanja/opste": "/admin/podesavanja/opste",
|
||||||
"/podesavanja": "/podesavanja",
|
"/admin/podesavanja/sistem": "/admin/podesavanja/sistem",
|
||||||
|
"/admin/podesavanja/servis": "/admin/podesavanja/servis",
|
||||||
|
"/admin/podesavanja/kalkulacija-pdv": "/admin/podesavanja/kalkulacija-pdv",
|
||||||
|
"/podesavanja": "/podesavanja",
|
||||||
}
|
}
|
||||||
sledeci := "/podesavanja"
|
sledeci := "/podesavanja"
|
||||||
if v, ok := dozvoljeniSledeci[r.FormValue("_next")]; ok {
|
if v, ok := dozvoljeniSledeci[r.FormValue("_next")]; ok {
|
||||||
@@ -359,20 +362,20 @@ func (h *Handler) OtpremiLogo(w http.ResponseWriter, r *http.Request) {
|
|||||||
// ograničavamo telo zahteva na 2MB + malo za zaglavlja forme
|
// ograničavamo telo zahteva na 2MB + malo za zaglavlja forme
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, 2<<20+4096)
|
r.Body = http.MaxBytesReader(w, r.Body, 2<<20+4096)
|
||||||
if err := r.ParseMultipartForm(2 << 20); err != nil {
|
if err := r.ParseMultipartForm(2 << 20); err != nil {
|
||||||
http.Redirect(w, r, "/podesavanja?logo_greska=Fajl+je+prevelik+%28maksimum+2+MB%29", http.StatusSeeOther)
|
http.Redirect(w, r, "/admin/podesavanja/opste?logo_greska=Fajl+je+prevelik+%28maksimum+2+MB%29", http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fajl, zaglavlje, err := r.FormFile("logo")
|
fajl, zaglavlje, err := r.FormFile("logo")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Redirect(w, r, "/podesavanja?logo_greska=Nije+odabran+fajl", http.StatusSeeOther)
|
http.Redirect(w, r, "/admin/podesavanja/opste?logo_greska=Nije+odabran+fajl", http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer fajl.Close()
|
defer fajl.Close()
|
||||||
|
|
||||||
// eksplicitna provera veličine (zaglavlje.Size je postavljeno od strane browsera)
|
// eksplicitna provera veličine (zaglavlje.Size je postavljeno od strane browsera)
|
||||||
if zaglavlje.Size > 2<<20 {
|
if zaglavlje.Size > 2<<20 {
|
||||||
http.Redirect(w, r, "/podesavanja?logo_greska=Fajl+je+prevelik+%28maksimum+2+MB%29", http.StatusSeeOther)
|
http.Redirect(w, r, "/admin/podesavanja/opste?logo_greska=Fajl+je+prevelik+%28maksimum+2+MB%29", http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -386,7 +389,7 @@ func (h *Handler) OtpremiLogo(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
ocekivaniMime, ok := dozvoljenoExt[ext]
|
ocekivaniMime, ok := dozvoljenoExt[ext]
|
||||||
if !ok {
|
if !ok {
|
||||||
http.Redirect(w, r, "/podesavanja?logo_greska=Dozvoljeni+formati+su+PNG%2C+JPG+i+SVG", http.StatusSeeOther)
|
http.Redirect(w, r, "/admin/podesavanja/opste?logo_greska=Dozvoljeni+formati+su+PNG%2C+JPG+i+SVG", http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -396,12 +399,12 @@ func (h *Handler) OtpremiLogo(w http.ResponseWriter, r *http.Request) {
|
|||||||
n, _ := fajl.Read(buf)
|
n, _ := fajl.Read(buf)
|
||||||
stvarniMime := http.DetectContentType(buf[:n])
|
stvarniMime := http.DetectContentType(buf[:n])
|
||||||
if !strings.HasPrefix(stvarniMime, ocekivaniMime) {
|
if !strings.HasPrefix(stvarniMime, ocekivaniMime) {
|
||||||
http.Redirect(w, r, "/podesavanja?logo_greska=Sadržaj+fajla+ne+odgovara+odabranoj+ekstenziji", http.StatusSeeOther)
|
http.Redirect(w, r, "/admin/podesavanja/opste?logo_greska=Sadržaj+fajla+ne+odgovara+odabranoj+ekstenziji", http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// vraćamo kursor na početak
|
// vraćamo kursor na početak
|
||||||
if _, err := fajl.Seek(0, io.SeekStart); err != nil {
|
if _, err := fajl.Seek(0, io.SeekStart); err != nil {
|
||||||
http.Redirect(w, r, "/podesavanja?logo_greska=Greška+pri+obradi+fajla", http.StatusSeeOther)
|
http.Redirect(w, r, "/admin/podesavanja/opste?logo_greska=Greška+pri+obradi+fajla", http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -416,14 +419,14 @@ func (h *Handler) OtpremiLogo(w http.ResponseWriter, r *http.Request) {
|
|||||||
dst, err := os.Create(odrediste)
|
dst, err := os.Create(odrediste)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("upload loga: ne mogu kreirati fajl", "error", err)
|
slog.Error("upload loga: ne mogu kreirati fajl", "error", err)
|
||||||
http.Redirect(w, r, "/podesavanja?logo_greska=Greška+pri+čuvanju+fajla", http.StatusSeeOther)
|
http.Redirect(w, r, "/admin/podesavanja/opste?logo_greska=Greška+pri+čuvanju+fajla", http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer dst.Close()
|
defer dst.Close()
|
||||||
|
|
||||||
if _, err := io.Copy(dst, fajl); err != nil {
|
if _, err := io.Copy(dst, fajl); err != nil {
|
||||||
slog.Error("upload loga: greška pri kopiranju", "error", err)
|
slog.Error("upload loga: greška pri kopiranju", "error", err)
|
||||||
http.Redirect(w, r, "/podesavanja?logo_greska=Greška+pri+čuvanju+fajla", http.StatusSeeOther)
|
http.Redirect(w, r, "/admin/podesavanja/opste?logo_greska=Greška+pri+čuvanju+fajla", http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -431,11 +434,11 @@ func (h *Handler) OtpremiLogo(w http.ResponseWriter, r *http.Request) {
|
|||||||
putanja := fmt.Sprintf("/static/uploads/logo%s?v=%d", ext, time.Now().Unix())
|
putanja := fmt.Sprintf("/static/uploads/logo%s?v=%d", ext, time.Now().Unix())
|
||||||
if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "logo_putanja", putanja); err != nil {
|
if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "logo_putanja", putanja); err != nil {
|
||||||
slog.Error("upload loga: greška pri čuvanju putanje", "error", err)
|
slog.Error("upload loga: greška pri čuvanju putanje", "error", err)
|
||||||
http.Redirect(w, r, "/podesavanja?logo_greska=Greška+pri+čuvanju+podešavanja", http.StatusSeeOther)
|
http.Redirect(w, r, "/admin/podesavanja/opste?logo_greska=Greška+pri+čuvanju+podešavanja", http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
http.Redirect(w, r, "/podesavanja?sacuvano=1", http.StatusSeeOther)
|
http.Redirect(w, r, "/admin/podesavanja/opste?sacuvano=1", http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UkloniLogo briše logo fajl i čisti putanju iz podešavanja
|
// UkloniLogo briše logo fajl i čisti putanju iz podešavanja
|
||||||
@@ -536,7 +539,7 @@ func (h *Handler) OtpremiLoginPozadinu(w http.ResponseWriter, r *http.Request) {
|
|||||||
staraPodesavanja, _ := ntechsqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
staraPodesavanja, _ := ntechsqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||||
if stara := staraPodesavanja["login_pozadina"]; stara != "" {
|
if stara := staraPodesavanja["login_pozadina"]; stara != "" {
|
||||||
// putanja u bazi je oblika /static/uploads/ime.ext?v=..., izvlačimo samo ime fajla
|
// putanja u bazi je oblika /static/uploads/ime.ext?v=..., izvlačimo samo ime fajla
|
||||||
deoBezverzije := strings.Split(stara, "?")[0]
|
deoBezverzije, _, _ := strings.Cut(stara, "?")
|
||||||
staroIme := filepath.Base(deoBezverzije)
|
staroIme := filepath.Base(deoBezverzije)
|
||||||
os.Remove(filepath.Join("web/static/uploads", staroIme))
|
os.Remove(filepath.Join("web/static/uploads", staroIme))
|
||||||
}
|
}
|
||||||
@@ -587,7 +590,7 @@ func (h *Handler) UkloniLoginPozadinu(w http.ResponseWriter, r *http.Request) {
|
|||||||
podesavanja, err := ntechsqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
podesavanja, err := ntechsqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if stara := podesavanja["login_pozadina"]; stara != "" {
|
if stara := podesavanja["login_pozadina"]; stara != "" {
|
||||||
deoBezverzije := strings.Split(stara, "?")[0]
|
deoBezverzije, _, _ := strings.Cut(stara, "?")
|
||||||
staroIme := filepath.Base(deoBezverzije)
|
staroIme := filepath.Base(deoBezverzije)
|
||||||
os.Remove(filepath.Join("web/static/uploads", staroIme))
|
os.Remove(filepath.Join("web/static/uploads", staroIme))
|
||||||
}
|
}
|
||||||
@@ -600,8 +603,7 @@ func (h *Handler) UkloniLoginPozadinu(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Pozadinska slika je uklonjena.")
|
http.Redirect(w, r, "/admin/podesavanja/izgled?sacuvano=1", http.StatusSeeOther)
|
||||||
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SacuvajLoginPozadinaStilove čuva vrednosti zamućenja i prozirnosti pozadine login stranice
|
// SacuvajLoginPozadinaStilove čuva vrednosti zamućenja i prozirnosti pozadine login stranice
|
||||||
@@ -659,8 +661,7 @@ func (h *Handler) SacuvajLoginPozadinaStilove(w http.ResponseWriter, r *http.Req
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Izgled pozadine prijave je sačuvan.")
|
http.Redirect(w, r, "/admin/podesavanja/izgled?sacuvano=1", http.StatusSeeOther)
|
||||||
http.Redirect(w, r, "/admin/podesavanja/izgled", http.StatusSeeOther)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// napuniPodaciPodesavanja učitava sva podešavanja i kreira strukturu za template
|
// napuniPodaciPodesavanja učitava sva podešavanja i kreira strukturu za template
|
||||||
|
|||||||
@@ -98,6 +98,8 @@ func (h *Handler) Prijava(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
korisnik, err := h.KorisniciRepo.DohvatiPoImenu(r.Context(), korisnickoIme)
|
korisnik, err := h.KorisniciRepo.DohvatiPoImenu(r.Context(), korisnickoIme)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
// izjednačavamo vreme odgovora sa slučajem postojećeg korisnika (anti-enumeracija)
|
||||||
|
auth.IzjednaciVremeProvere(lozinka)
|
||||||
_ = h.PokusajiRepo.Zabeleži(r.Context(), ip, korisnickoIme, false)
|
_ = h.PokusajiRepo.Zabeleži(r.Context(), ip, korisnickoIme, false)
|
||||||
_ = h.LoginIstorijsaRepo.Zabeleži(r.Context(), nil, ip, r.UserAgent(), "korisnik_ne_postoji", false)
|
_ = h.LoginIstorijsaRepo.Zabeleži(r.Context(), nil, ip, r.UserAgent(), "korisnik_ne_postoji", false)
|
||||||
auth.LogNeuspehPrijave(ip, korisnickoIme, "wrong_user")
|
auth.LogNeuspehPrijave(ip, korisnickoIme, "wrong_user")
|
||||||
|
|||||||
@@ -432,8 +432,7 @@ func (h *Handler) StornoProdaje(w http.ResponseWriter, r *http.Request) {
|
|||||||
if err := h.PdvKirRepo.ObrisiPoIzvoru(r.Context(), "prodaja", id); err != nil {
|
if err := h.PdvKirRepo.ObrisiPoIzvoru(r.Context(), "prodaja", id); err != nil {
|
||||||
slog.Error("brisanje vezanog KIR zapisa nije uspelo", "prodaja_id", id, "error", err)
|
slog.Error("brisanje vezanog KIR zapisa nije uspelo", "prodaja_id", id, "error", err)
|
||||||
}
|
}
|
||||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Prodajni nalog je storniran.")
|
http.Redirect(w, r, "/prodaja/"+strconv.FormatInt(id, 10)+"?sacuvano=1", http.StatusSeeOther)
|
||||||
http.Redirect(w, r, "/prodaja/"+strconv.FormatInt(id, 10), http.StatusSeeOther)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// renderujFormuProdaje renderuje HTML šablon forme za unos nove prodaje
|
// renderujFormuProdaje renderuje HTML šablon forme za unos nove prodaje
|
||||||
|
|||||||
+67
-35
@@ -15,6 +15,22 @@ import (
|
|||||||
"ntech/internal/middleware"
|
"ntech/internal/middleware"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// sacuvanoReferer vraća URL za redirect posle uspešnog čuvanja:
|
||||||
|
// koristi Referer zaglavlje (samo putanja, nikad spoljni sajt) i dodaje ?sacuvano=1.
|
||||||
|
// AJAX handler u browseru detektuje sacuvano=1 i prikazuje toast bez reloada.
|
||||||
|
func sacuvanoReferer(r *http.Request, rezervna string) string {
|
||||||
|
if ref := r.Referer(); ref != "" {
|
||||||
|
if u, err := url.Parse(ref); err == nil {
|
||||||
|
putanja := u.RequestURI()
|
||||||
|
if strings.Contains(putanja, "?") {
|
||||||
|
return putanja + "&sacuvano=1"
|
||||||
|
}
|
||||||
|
return putanja + "?sacuvano=1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rezervna + "?sacuvano=1"
|
||||||
|
}
|
||||||
|
|
||||||
// ProfilTema prikazuje stranicu lične teme i pozadine
|
// ProfilTema prikazuje stranicu lične teme i pozadine
|
||||||
func (h *Handler) ProfilTema(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) ProfilTema(w http.ResponseWriter, r *http.Request) {
|
||||||
k := middleware.KorisnikIzKonteksta(r.Context())
|
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||||
@@ -177,8 +193,7 @@ func (h *Handler) ProfilOtpremiPozadinu(w http.ResponseWriter, r *http.Request)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Pozadinska slika je uspešno otpremljena.")
|
http.Redirect(w, r, "/profil/tema?sacuvano=1", http.StatusSeeOther)
|
||||||
http.Redirect(w, r, "/profil/tema", http.StatusSeeOther)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProfilUkloniPozadinu briše ličnu pozadinsku sliku korisnika
|
// ProfilUkloniPozadinu briše ličnu pozadinsku sliku korisnika
|
||||||
@@ -202,8 +217,7 @@ func (h *Handler) ProfilUkloniPozadinu(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Pozadinska slika je uklonjena.")
|
http.Redirect(w, r, "/profil/tema?sacuvano=1", http.StatusSeeOther)
|
||||||
http.Redirect(w, r, "/profil/tema", http.StatusSeeOther)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProfilSacuvajPozadinuStilove čuva opacity i blur lične pozadine
|
// ProfilSacuvajPozadinuStilove čuva opacity i blur lične pozadine
|
||||||
@@ -250,8 +264,7 @@ func (h *Handler) ProfilSacuvajPozadinuStilove(w http.ResponseWriter, r *http.Re
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Podešavanja su sačuvana.")
|
http.Redirect(w, r, "/profil/tema?sacuvano=1", http.StatusSeeOther)
|
||||||
http.Redirect(w, r, "/profil/tema", http.StatusSeeOther)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SacuvajLokalnuTemu čuva korisnikovu lokalnu temu
|
// SacuvajLokalnuTemu čuva korisnikovu lokalnu temu
|
||||||
@@ -284,15 +297,7 @@ func (h *Handler) SacuvajLokalnuTemu(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Tema je sačuvana.")
|
http.Redirect(w, r, sacuvanoReferer(r, "/admin/profil"), http.StatusSeeOther)
|
||||||
// koristimo samo putanju iz Referer zaglavlja — nikad ceo URL jer može biti spoljni sajt
|
|
||||||
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", http.StatusSeeOther)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SacuvajLokalnuAnimaciju čuva korisnikovu preferencu animacije tabela
|
// SacuvajLokalnuAnimaciju čuva korisnikovu preferencu animacije tabela
|
||||||
@@ -313,25 +318,29 @@ func (h *Handler) SacuvajLokalnuAnimaciju(w http.ResponseWriter, r *http.Request
|
|||||||
}
|
}
|
||||||
|
|
||||||
animacija := r.FormValue("lokalna_animacija")
|
animacija := r.FormValue("lokalna_animacija")
|
||||||
dozvoljene := map[string]bool{"": true, "bez": true, "fadeInUp": true, "fadeIn": true, "scaleIn": true, "slideLeft": true}
|
dozvoljene := map[string]bool{"": true, "bez": true, "fadeInUp": true, "fadeIn": true, "blurIn": true, "slideLeft": true}
|
||||||
if !dozvoljene[animacija] {
|
if !dozvoljene[animacija] {
|
||||||
animacija = ""
|
animacija = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
brzina := r.FormValue("lokalna_brzina_animacije")
|
||||||
|
dozvoljenieBrzine := map[string]bool{"": true, "0.1": true, "0.2": true, "0.3": true, "0.4": true, "0.5": true, "0.6": true, "0.7": true, "0.8": true}
|
||||||
|
if !dozvoljenieBrzine[brzina] {
|
||||||
|
brzina = ""
|
||||||
|
}
|
||||||
|
|
||||||
if err := h.KorisniciRepo.SacuvajLokalnuAnimaciju(r.Context(), k.ID, animacija); err != nil {
|
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.")
|
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju. Pokušajte ponovo.")
|
||||||
http.Redirect(w, r, "/admin/profil/tema", http.StatusSeeOther)
|
http.Redirect(w, r, "/admin/profil/tema", http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if err := h.KorisniciRepo.SacuvajLokalnuBrzinuAnimacije(r.Context(), k.ID, brzina); err != nil {
|
||||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Animacija je sačuvana.")
|
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju. Pokušajte ponovo.")
|
||||||
if ref := r.Referer(); ref != "" {
|
http.Redirect(w, r, "/admin/profil/tema", http.StatusSeeOther)
|
||||||
if u, err := url.Parse(ref); err == nil {
|
return
|
||||||
http.Redirect(w, r, u.RequestURI(), http.StatusSeeOther)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
http.Redirect(w, r, "/admin/profil/tema", http.StatusSeeOther)
|
|
||||||
|
http.Redirect(w, r, sacuvanoReferer(r, "/admin/profil/tema"), http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SacuvajLokalniHover čuva korisnikovu preferencu hover efekta na karticama
|
// SacuvajLokalniHover čuva korisnikovu preferencu hover efekta na karticama
|
||||||
@@ -363,14 +372,39 @@ func (h *Handler) SacuvajLokalniHover(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Hover efekat je sačuvan.")
|
http.Redirect(w, r, sacuvanoReferer(r, "/admin/profil/tema"), http.StatusSeeOther)
|
||||||
if ref := r.Referer(); ref != "" {
|
}
|
||||||
if u, err := url.Parse(ref); err == nil {
|
|
||||||
http.Redirect(w, r, u.RequestURI(), http.StatusSeeOther)
|
// SacuvajLokalnuBrzinuAnimacije čuva korisnikovu preferencu brzine animacije tabela
|
||||||
return
|
func (h *Handler) SacuvajLokalnuBrzinuAnimacije(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||||
|
if k == nil {
|
||||||
|
http.Redirect(w, r, "/prijava", http.StatusSeeOther)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
http.Redirect(w, r, "/admin/profil/tema", http.StatusSeeOther)
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
brzina := r.FormValue("lokalna_brzina_animacije")
|
||||||
|
dozvoljene := map[string]bool{"": true, "0.2": true, "0.4": true, "0.6": true, "0.8": true, "1.0": true, "1.2": true, "1.5": true}
|
||||||
|
if !dozvoljene[brzina] {
|
||||||
|
brzina = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.KorisniciRepo.SacuvajLokalnuBrzinuAnimacije(r.Context(), k.ID, brzina); 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
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Redirect(w, r, sacuvanoReferer(r, "/admin/profil/tema"), http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProfilOtpremiAvatar prima upload lične avatar slike korisnika
|
// ProfilOtpremiAvatar prima upload lične avatar slike korisnika
|
||||||
@@ -464,8 +498,7 @@ func (h *Handler) ProfilOtpremiAvatar(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Avatar je sačuvan.")
|
http.Redirect(w, r, "/profil/tema?sacuvano=1", http.StatusSeeOther)
|
||||||
http.Redirect(w, r, "/profil/tema", http.StatusSeeOther)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProfilUkloniAvatar briše ličnu avatar sliku korisnika
|
// ProfilUkloniAvatar briše ličnu avatar sliku korisnika
|
||||||
@@ -489,6 +522,5 @@ func (h *Handler) ProfilUkloniAvatar(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Avatar je uklonjen.")
|
http.Redirect(w, r, "/profil/tema?sacuvano=1", http.StatusSeeOther)
|
||||||
http.Redirect(w, r, "/profil/tema", http.StatusSeeOther)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -444,8 +444,7 @@ func (h *Handler) DodajDeloNalogu(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Deo je dodat.")
|
http.Redirect(w, r, "/servis/"+strconv.FormatInt(nalogID, 10)+"?sacuvano=1", http.StatusSeeOther)
|
||||||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(nalogID, 10), http.StatusSeeOther)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ObrisiDeloNaloga prima POST zahtev i uklanja deo iz servisnog naloga
|
// ObrisiDeloNaloga prima POST zahtev i uklanja deo iz servisnog naloga
|
||||||
@@ -470,10 +469,7 @@ func (h *Handler) ObrisiDeloNaloga(w http.ResponseWriter, r *http.Request) {
|
|||||||
if err := h.ServisniDeloviRepo.Obrisi(r.Context(), deoID, &k.ID); err != nil {
|
if err := h.ServisniDeloviRepo.Obrisi(r.Context(), deoID, &k.ID); err != nil {
|
||||||
slog.Error("greška pri brisanju dela", "error", err)
|
slog.Error("greška pri brisanju dela", "error", err)
|
||||||
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri uklanjanju dela.")
|
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri uklanjanju dela.")
|
||||||
} else {
|
|
||||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Deo je uklonjen.")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(nalogID, 10), http.StatusSeeOther)
|
http.Redirect(w, r, "/servis/"+strconv.FormatInt(nalogID, 10), http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -647,12 +643,12 @@ func (h *Handler) StampaServisa(w http.ResponseWriter, r *http.Request) {
|
|||||||
ukupnoDelovi += d.Ukupno()
|
ukupnoDelovi += d.Ukupno()
|
||||||
}
|
}
|
||||||
|
|
||||||
// QR kod sadrži URL naloga — isti host kao što korisnik koristi
|
// QR kod vodi na javnu status stranicu — dostupnu bez prijave
|
||||||
scheme := "http"
|
scheme := "http"
|
||||||
if r.TLS != nil {
|
if r.TLS != nil {
|
||||||
scheme = "https"
|
scheme = "https"
|
||||||
}
|
}
|
||||||
nalogURL := scheme + "://" + r.Host + "/servis/" + strconv.FormatInt(id, 10)
|
nalogURL := scheme + "://" + r.Host + "/status/" + nalog.JavniToken
|
||||||
var qrKod string
|
var qrKod string
|
||||||
if png, err := qrcode.Encode(nalogURL, qrcode.Medium, 160); err == nil {
|
if png, err := qrcode.Encode(nalogURL, qrcode.Medium, 160); err == nil {
|
||||||
qrKod = base64.StdEncoding.EncodeToString(png)
|
qrKod = base64.StdEncoding.EncodeToString(png)
|
||||||
@@ -762,7 +758,7 @@ func (h *Handler) StampaOtpremnice(w http.ResponseWriter, r *http.Request) {
|
|||||||
if r.TLS != nil {
|
if r.TLS != nil {
|
||||||
nalogURL += "s"
|
nalogURL += "s"
|
||||||
}
|
}
|
||||||
nalogURL += "://" + r.Host + "/servis/" + strconv.FormatInt(id, 10)
|
nalogURL += "://" + r.Host + "/status/" + nalog.JavniToken
|
||||||
var qrKodOtpr string
|
var qrKodOtpr string
|
||||||
if png, err := qrcode.Encode(nalogURL, qrcode.Medium, 160); err == nil {
|
if png, err := qrcode.Encode(nalogURL, qrcode.Medium, 160); err == nil {
|
||||||
qrKodOtpr = base64.StdEncoding.EncodeToString(png)
|
qrKodOtpr = base64.StdEncoding.EncodeToString(png)
|
||||||
@@ -813,3 +809,37 @@ func (h *Handler) PromeniStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(id, 10)+"?sacuvano=1", http.StatusSeeOther)
|
http.Redirect(w, r, "/servis/"+strconv.FormatInt(id, 10)+"?sacuvano=1", http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PodaciJavnogStatusa su podaci za javnu status stranicu servisnog naloga
|
||||||
|
type PodaciJavnogStatusa struct {
|
||||||
|
Nalog model.ServisniNalog
|
||||||
|
NazivFirme string
|
||||||
|
Telefon string
|
||||||
|
Adresa string
|
||||||
|
SviStatusi []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServisJavniStatus prikazuje javnu status stranicu — dostupna bez prijave putem QR koda
|
||||||
|
func (h *Handler) ServisJavniStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
|
token := chi.URLParam(r, "token")
|
||||||
|
if token == "" {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nalog, err := h.ServisRepo.DohvatiJavniToken(r.Context(), token)
|
||||||
|
if err != nil {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||||
|
|
||||||
|
h.renderujStandalone(w, "servis_status_javni", PodaciJavnogStatusa{
|
||||||
|
Nalog: *nalog,
|
||||||
|
NazivFirme: podesavanja["naziv_firme"],
|
||||||
|
Telefon: podesavanja["telefon"],
|
||||||
|
Adresa: podesavanja["adresa"],
|
||||||
|
SviStatusi: model.SviStatusi,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,10 +6,17 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const csrfKolacic = "ntech_csrf"
|
const csrfKolacic = "ntech_csrf"
|
||||||
|
|
||||||
|
// maxTeloUpload je gornja granica veličine tela multipart zahteva (upload fajlova).
|
||||||
|
// Postavlja se u middleware-u pre parsiranja jer čitanje _csrf polja već parsira
|
||||||
|
// telo — bez ovog limita pojedinačni handleri ne mogu da ga efektivno ograniče.
|
||||||
|
// Najveći legitiman upload je 5 MB (avatar, pozadina); ostatak je rezerva za form overhead.
|
||||||
|
const maxTeloUpload = 6 << 20
|
||||||
|
|
||||||
type csrfKljucTip struct{}
|
type csrfKljucTip struct{}
|
||||||
|
|
||||||
var csrfKljuc = csrfKljucTip{}
|
var csrfKljuc = csrfKljucTip{}
|
||||||
@@ -52,6 +59,11 @@ func CsrfMiddleware(next http.Handler) http.Handler {
|
|||||||
// validiramo na svim mutabilnim HTTP metodama
|
// validiramo na svim mutabilnim HTTP metodama
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete:
|
case http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete:
|
||||||
|
// ograničavamo veličinu tela PRE parsiranja — čitanje _csrf polja parsira
|
||||||
|
// celo telo, pa je ovo jedino mesto gde limit za upload stvarno deluje
|
||||||
|
if strings.HasPrefix(r.Header.Get("Content-Type"), "multipart/form-data") {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxTeloUpload)
|
||||||
|
}
|
||||||
// čitamo token iz tela forme ili zaglavlja (za AJAX)
|
// čitamo token iz tela forme ili zaglavlja (za AJAX)
|
||||||
submitted := r.FormValue("_csrf")
|
submitted := r.FormValue("_csrf")
|
||||||
if submitted == "" {
|
if submitted == "" {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ type Korisnik struct {
|
|||||||
AvatarPutanja string
|
AvatarPutanja string
|
||||||
LokalnaAnimacija string // "" | "fadeInUp" | "fadeIn" | "scaleIn" | "slideLeft"
|
LokalnaAnimacija string // "" | "fadeInUp" | "fadeIn" | "scaleIn" | "slideLeft"
|
||||||
LokalniHover string // "" | "bez" | "podizanje" | "svetlost"
|
LokalniHover string // "" | "bez" | "podizanje" | "svetlost"
|
||||||
|
LokalnaBrzinaAnimacije string // "" | "0.2" | "0.4" | "0.6" | "0.8" | "1.2" (sekunde)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sesija predstavlja aktivnu sesiju prijavljenog korisnika
|
// Sesija predstavlja aktivnu sesiju prijavljenog korisnika
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ type ServisniNalog struct {
|
|||||||
Ostecenja string
|
Ostecenja string
|
||||||
PinUredjaja string
|
PinUredjaja string
|
||||||
Pribor string
|
Pribor string
|
||||||
|
JavniToken string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServisniDeo predstavlja jedan artikal ugrađen u servisni nalog
|
// ServisniDeo predstavlja jedan artikal ugrađen u servisni nalog
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ type PodaciStranice struct {
|
|||||||
AppPozadinaGlassOpacity string // vrednost 0-80 (% zatamnjivanje glass elemenata) — samo za ličnu pozadinu
|
AppPozadinaGlassOpacity string // vrednost 0-80 (% zatamnjivanje glass elemenata) — samo za ličnu pozadinu
|
||||||
LokalnaAnimacija string // "" | "fadeInUp" | "fadeIn" | "scaleIn" | "slideLeft"
|
LokalnaAnimacija string // "" | "fadeInUp" | "fadeIn" | "scaleIn" | "slideLeft"
|
||||||
LokalniHover string // "" | "bez" | "podizanje" | "svetlost"
|
LokalniHover string // "" | "bez" | "podizanje" | "svetlost"
|
||||||
|
LokalnaBrzinaAnimacije string // "" | "0.2" | "0.4" | ... | "1.5" (sekunde)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PodaciDashboarda su podaci specifični za dashboard stranicu
|
// PodaciDashboarda su podaci specifični za dashboard stranicu
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE korisnici ADD COLUMN lokalna_brzina_animacije TEXT NOT NULL DEFAULT '';
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE servisni_nalozi ADD COLUMN javni_token TEXT;
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_servisni_nalozi_javni_token ON servisni_nalozi(javni_token);
|
||||||
+27
-15
@@ -5,6 +5,16 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root { --anim-trajanje: 0.4s; }
|
||||||
|
[data-brzina-animacije="0.1"] { --anim-trajanje: 0.1s; }
|
||||||
|
[data-brzina-animacije="0.2"] { --anim-trajanje: 0.2s; }
|
||||||
|
[data-brzina-animacije="0.3"] { --anim-trajanje: 0.3s; }
|
||||||
|
[data-brzina-animacije="0.4"] { --anim-trajanje: 0.4s; }
|
||||||
|
[data-brzina-animacije="0.5"] { --anim-trajanje: 0.5s; }
|
||||||
|
[data-brzina-animacije="0.6"] { --anim-trajanje: 0.6s; }
|
||||||
|
[data-brzina-animacije="0.7"] { --anim-trajanje: 0.7s; }
|
||||||
|
[data-brzina-animacije="0.8"] { --anim-trajanje: 0.8s; }
|
||||||
|
|
||||||
html {
|
html {
|
||||||
background: var(--pozadina);
|
background: var(--pozadina);
|
||||||
}
|
}
|
||||||
@@ -324,6 +334,8 @@ body {
|
|||||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||||
}
|
}
|
||||||
.topbar-logo { height: 34px; width: auto; border-radius: 6px; flex-shrink: 0; }
|
.topbar-logo { height: 34px; width: auto; border-radius: 6px; flex-shrink: 0; }
|
||||||
|
/* sakrivanje preko klase (ne inline display) da mobilni @media koji gasi logo ostane nadređen */
|
||||||
|
.topbar-logo.skriven { display: none; }
|
||||||
|
|
||||||
/* sadržaj stranice */
|
/* sadržaj stranice */
|
||||||
.sadrzaj {
|
.sadrzaj {
|
||||||
@@ -793,8 +805,8 @@ select {
|
|||||||
animation: toastIn 0.3s ease forwards;
|
animation: toastIn 0.3s ease forwards;
|
||||||
max-width: 340px;
|
max-width: 340px;
|
||||||
}
|
}
|
||||||
.toast-greska { background: rgba(207, 87, 87, 0.12); color: var(--greska); border: 0.5px solid var(--greska); }
|
.toast-greska { background: #fef2f2; color: #dc2626; border: 0.5px solid #fecaca; }
|
||||||
.toast-uspeh { background: var(--poruka-uspeh-bg); color: var(--poruka-uspeh-boja); border: 0.5px solid var(--poruka-uspeh-boja); }
|
.toast-uspeh { background: #f0fdf4; color: #15803d; border: 0.5px solid #86efac; }
|
||||||
.toast.nestaje { animation: toastOut 0.3s ease forwards; }
|
.toast.nestaje { animation: toastOut 0.3s ease forwards; }
|
||||||
@keyframes toastIn {
|
@keyframes toastIn {
|
||||||
from { opacity: 0; transform: translateY(12px); }
|
from { opacity: 0; transform: translateY(12px); }
|
||||||
@@ -1011,7 +1023,7 @@ select {
|
|||||||
|
|
||||||
/* animacije */
|
/* animacije */
|
||||||
@keyframes fadeInUp {
|
@keyframes fadeInUp {
|
||||||
from { opacity: 0; transform: translateY(10px); }
|
from { opacity: 0; transform: translateY(16px); }
|
||||||
to { opacity: 1; transform: translateY(0); }
|
to { opacity: 1; transform: translateY(0); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1020,13 +1032,13 @@ select {
|
|||||||
to { opacity: 1; }
|
to { opacity: 1; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes scaleIn {
|
@keyframes blurIn {
|
||||||
from { opacity: 0; transform: scale(0.95); }
|
from { opacity: 0; filter: blur(8px); }
|
||||||
to { opacity: 1; transform: scale(1); }
|
to { opacity: 1; filter: blur(0); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes slideLeft {
|
@keyframes slideLeft {
|
||||||
from { opacity: 0; transform: translateX(-12px); }
|
from { opacity: 0; transform: translateX(-18px); }
|
||||||
to { opacity: 1; transform: translateX(0); }
|
to { opacity: 1; transform: translateX(0); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1050,7 +1062,7 @@ select {
|
|||||||
/* backwards (ne both): drži početni frame tokom stagger-delay-a da nema treperenja,
|
/* backwards (ne both): drži početni frame tokom stagger-delay-a da nema treperenja,
|
||||||
ali NE zaključava krajnji transform — pa .kartica:hover lift radi i na .animiraj karticama */
|
ali NE zaključava krajnji transform — pa .kartica:hover lift radi i na .animiraj karticama */
|
||||||
.animiraj {
|
.animiraj {
|
||||||
animation: fadeInUp 0.2s ease backwards;
|
animation: fadeInUp var(--anim-trajanje) ease backwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* kartice ne animiramo po defaultu — animacija je samo na redovima tabela */
|
/* kartice ne animiramo po defaultu — animacija je samo na redovima tabela */
|
||||||
@@ -1066,26 +1078,26 @@ select {
|
|||||||
|
|
||||||
[data-animacija="fadeInUp"] .animiraj,
|
[data-animacija="fadeInUp"] .animiraj,
|
||||||
[data-animacija="fadeInUp"] .tabela tbody tr,
|
[data-animacija="fadeInUp"] .tabela tbody tr,
|
||||||
[data-animacija="fadeInUp"] .kartica.animiraj { animation: fadeInUp 0.2s ease backwards; }
|
[data-animacija="fadeInUp"] .kartica.animiraj { animation: fadeInUp var(--anim-trajanje) ease backwards; }
|
||||||
|
|
||||||
[data-animacija="fadeIn"] .animiraj,
|
[data-animacija="fadeIn"] .animiraj,
|
||||||
[data-animacija="fadeIn"] .tabela tbody tr,
|
[data-animacija="fadeIn"] .tabela tbody tr,
|
||||||
[data-animacija="fadeIn"] .kartica.animiraj { animation: fadeIn 0.2s ease backwards; }
|
[data-animacija="fadeIn"] .kartica.animiraj { animation: fadeIn var(--anim-trajanje) ease backwards; }
|
||||||
|
|
||||||
[data-animacija="scaleIn"] .animiraj,
|
[data-animacija="blurIn"] .animiraj,
|
||||||
[data-animacija="scaleIn"] .tabela tbody tr,
|
[data-animacija="blurIn"] .tabela tbody tr,
|
||||||
[data-animacija="scaleIn"] .kartica.animiraj { animation: scaleIn 0.2s ease backwards; }
|
[data-animacija="blurIn"] .kartica.animiraj { animation: blurIn var(--anim-trajanje) ease backwards; }
|
||||||
|
|
||||||
[data-animacija="slideLeft"] .animiraj,
|
[data-animacija="slideLeft"] .animiraj,
|
||||||
[data-animacija="slideLeft"] .tabela tbody tr,
|
[data-animacija="slideLeft"] .tabela tbody tr,
|
||||||
[data-animacija="slideLeft"] .kartica.animiraj { animation: slideLeft 0.2s ease backwards; }
|
[data-animacija="slideLeft"] .kartica.animiraj { animation: slideLeft var(--anim-trajanje) ease backwards; }
|
||||||
|
|
||||||
/* Stepenasta (stagger) animacija redova u svim listama — JEDNO mesto za sve tabele.
|
/* Stepenasta (stagger) animacija redova u svim listama — JEDNO mesto za sve tabele.
|
||||||
Animacija se primenjuje direktno na <tr> u .tabela, bez potrebe za .animiraj klasom.
|
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
|
Mora biti u main.css: HTMX navigacija (hx-select) odbacuje <head>, pa
|
||||||
stilovi iz page <style> blokova ne bi važili tokom navigacije. */
|
stilovi iz page <style> blokova ne bi važili tokom navigacije. */
|
||||||
.tabela tbody tr {
|
.tabela tbody tr {
|
||||||
animation: fadeInUp 0.2s ease backwards;
|
animation: fadeInUp var(--anim-trajanje) ease backwards;
|
||||||
}
|
}
|
||||||
.tabela tbody tr:nth-child(1) { animation-delay: 0.04s; }
|
.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(2) { animation-delay: 0.08s; }
|
||||||
|
|||||||
@@ -22,3 +22,6 @@
|
|||||||
--poruka-uspeh-bg: rgba(93, 184, 118, 0.15);
|
--poruka-uspeh-bg: rgba(93, 184, 118, 0.15);
|
||||||
--poruka-uspeh-boja: #5db876;
|
--poruka-uspeh-boja: #5db876;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toast-uspeh { background: #1a2e22; color: #5db876; border: 0.5px solid #2d5a3d; }
|
||||||
|
.toast-greska { background: #2e1a1a; color: #e07070; border: 0.5px solid #5a2d2d; }
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{{if and .TopbarLogoSlika .LogoPutanja}}
|
{{if .LogoPutanja}}
|
||||||
<img src="{{.LogoPutanja}}" alt="Logo" class="topbar-logo">
|
<img src="{{.LogoPutanja}}" alt="Logo" class="topbar-logo{{if not .TopbarLogoSlika}} skriven{{end}}">
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<span class="topbar-naslov">{{.NaslovStranice}}</span>
|
<span class="topbar-naslov">{{.NaslovStranice}}</span>
|
||||||
|
|||||||
@@ -83,24 +83,23 @@
|
|||||||
|
|
||||||
<form method="POST" action="/profil/animacija">
|
<form method="POST" action="/profil/animacija">
|
||||||
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
|
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
|
||||||
|
<input type="hidden" name="lokalna_brzina_animacije" id="brzina-hidden" value="{{if .LokalnaBrzinaAnimacije}}{{.LokalnaBrzinaAnimacije}}{{else}}0.4{{end}}">
|
||||||
<div class="kolona" style="gap:16px;">
|
<div class="kolona" style="gap:16px;">
|
||||||
<div style="max-width:300px;">
|
<div style="max-width:300px;">
|
||||||
<label class="polje-labela" for="anim-select">Vrsta animacije pri učitavanju</label>
|
<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 -->
|
<select id="anim-select" name="lokalna_animacija" style="width:100%;" onchange="animPreview(this.value)">
|
||||||
<option value="" {{if eq .LokalnaAnimacija ""}}selected{{end}}>Podrazumevano (klizanje gore)</option>
|
<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="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="fadeInUp" {{if eq .LokalnaAnimacija "fadeInUp"}}selected{{end}}>Klizanje gore</option>
|
||||||
<option value="fadeIn" {{if eq .LokalnaAnimacija "fadeIn"}}selected{{end}}>Pojavljivanje (fadeIn)</option>
|
<option value="fadeIn" {{if eq .LokalnaAnimacija "fadeIn"}}selected{{end}}>Pojavljivanje</option>
|
||||||
<option value="scaleIn" {{if eq .LokalnaAnimacija "scaleIn"}}selected{{end}}>Zumiranje (scaleIn)</option>
|
<option value="blurIn" {{if eq .LokalnaAnimacija "blurIn"}}selected{{end}}>Zamagljivanje</option>
|
||||||
<option value="slideLeft" {{if eq .LokalnaAnimacija "slideLeft"}}selected{{end}}>Klizanje s leva (slideLeft)</option>
|
<option value="slideLeft" {{if eq .LokalnaAnimacija "slideLeft"}}selected{{end}}>Klizanje s leva</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- preview -->
|
<!-- preview -->
|
||||||
<div>
|
<div>
|
||||||
<div class="pomocni-tekst" style="margin-bottom:8px;">Pregled:</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;">
|
<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;">
|
<table style="width:100%;border-collapse:collapse;">
|
||||||
<tbody id="anim-preview-body">
|
<tbody id="anim-preview-body">
|
||||||
@@ -116,6 +115,20 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- slider brzine unutar iste forme -->
|
||||||
|
<div style="max-width:340px;">
|
||||||
|
<label class="polje-labela" for="brzina-slider">Trajanje efekta: <span id="brzina-labela">{{if .LokalnaBrzinaAnimacije}}{{.LokalnaBrzinaAnimacije}}s{{else}}0.4s{{end}}</span></label>
|
||||||
|
<input type="range" id="brzina-slider"
|
||||||
|
min="0.1" max="0.8" step="0.1"
|
||||||
|
value="{{if .LokalnaBrzinaAnimacije}}{{.LokalnaBrzinaAnimacije}}{{else}}0.4{{end}}"
|
||||||
|
style="width:100%;margin-top:8px;accent-color:var(--sb-akcent);"
|
||||||
|
oninput="brzinaPromena(this.value)">
|
||||||
|
<div style="display:flex;justify-content:space-between;margin-top:4px;">
|
||||||
|
<span style="font-size:11px;color:var(--tekst-sporedni);">0.1s (brže)</span>
|
||||||
|
<span style="font-size:11px;color:var(--tekst-sporedni);">0.8s (sporije)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<button type="submit" class="btn-primarno">Sačuvaj</button>
|
<button type="submit" class="btn-primarno">Sačuvaj</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -376,7 +389,7 @@ var animVrednosti = {
|
|||||||
'bez': 'bez',
|
'bez': 'bez',
|
||||||
'fadeInUp': 'fadeInUp',
|
'fadeInUp': 'fadeInUp',
|
||||||
'fadeIn': 'fadeIn',
|
'fadeIn': 'fadeIn',
|
||||||
'scaleIn': 'scaleIn',
|
'blurIn': 'blurIn',
|
||||||
'slideLeft': 'slideLeft'
|
'slideLeft': 'slideLeft'
|
||||||
};
|
};
|
||||||
// postavlja data-animacija na PREVIEW wrapper (ne na body), pa CSS
|
// postavlja data-animacija na PREVIEW wrapper (ne na body), pa CSS
|
||||||
@@ -385,10 +398,13 @@ function animPreview(val) {
|
|||||||
var wrap = document.getElementById('anim-preview-wrap');
|
var wrap = document.getElementById('anim-preview-wrap');
|
||||||
if (!wrap) return;
|
if (!wrap) return;
|
||||||
var anim = animVrednosti[val] || 'fadeInUp';
|
var anim = animVrednosti[val] || 'fadeInUp';
|
||||||
// restartuj animaciju: skini atribut, izazovi reflow, pa ga vrati
|
// skini data-animacija i klasu animiraj sa redova da bi se animacija resetovala
|
||||||
wrap.removeAttribute('data-animacija');
|
wrap.removeAttribute('data-animacija');
|
||||||
void wrap.offsetHeight; // reflow da bi se animacija ponovo okinula
|
var redovi = wrap.querySelectorAll('.animiraj');
|
||||||
|
redovi.forEach(function(r) { r.classList.remove('animiraj'); });
|
||||||
|
void wrap.offsetHeight; // reflow — bez ovoga browser spoji promene i ne restartuje
|
||||||
wrap.setAttribute('data-animacija', anim);
|
wrap.setAttribute('data-animacija', anim);
|
||||||
|
redovi.forEach(function(r) { r.classList.add('animiraj'); });
|
||||||
}
|
}
|
||||||
// "Ponovi pregled" — ponovo okida animaciju za trenutno izabrani stil
|
// "Ponovi pregled" — ponovo okida animaciju za trenutno izabrani stil
|
||||||
function animPreviewPonovi() {
|
function animPreviewPonovi() {
|
||||||
@@ -399,5 +415,17 @@ function animPreviewPonovi() {
|
|||||||
var sel = document.getElementById('anim-select');
|
var sel = document.getElementById('anim-select');
|
||||||
if (sel) animPreview(sel.value);
|
if (sel) animPreview(sel.value);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
function brzinaPromena(val) {
|
||||||
|
var v = parseFloat(val).toFixed(1);
|
||||||
|
document.getElementById('brzina-hidden').value = v;
|
||||||
|
document.getElementById('brzina-labela').textContent = v + 's';
|
||||||
|
document.body.dataset.brzinaAnimacije = v;
|
||||||
|
var wrap = document.getElementById('anim-preview-wrap');
|
||||||
|
if (wrap) wrap.style.setProperty('--anim-trajanje', v + 's');
|
||||||
|
// ponovi preview da korisnik odmah vidi novu brzinu
|
||||||
|
var sel = document.getElementById('anim-select');
|
||||||
|
if (sel) animPreview(sel.value);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@@ -57,12 +57,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex;gap:8px;flex-wrap:wrap;">
|
<div style="display:flex;gap:8px;flex-wrap:wrap;">
|
||||||
|
{{if not (or (eq .Nalog.Status "Završeno") (eq .Nalog.Status "Preuzeto"))}}
|
||||||
<a href="/servis/{{.Nalog.ID}}/stampa" target="_blank" class="btn-sekundarno">
|
<a href="/servis/{{.Nalog.ID}}/stampa" target="_blank" class="btn-sekundarno">
|
||||||
Radni nalog
|
Radni nalog
|
||||||
</a>
|
</a>
|
||||||
|
{{end}}
|
||||||
|
{{if or (eq .Nalog.Status "Završeno") (eq .Nalog.Status "Preuzeto")}}
|
||||||
<a href="/servis/{{.Nalog.ID}}/otpremnica" target="_blank" class="btn-sekundarno">
|
<a href="/servis/{{.Nalog.ID}}/otpremnica" target="_blank" class="btn-sekundarno">
|
||||||
Otpremnica
|
Otpremnica
|
||||||
</a>
|
</a>
|
||||||
|
{{end}}
|
||||||
<a href="/servis/izmeni/{{.Nalog.ID}}" class="btn-primarno">
|
<a href="/servis/izmeni/{{.Nalog.ID}}" class="btn-primarno">
|
||||||
Izmeni nalog
|
Izmeni nalog
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -0,0 +1,208 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="sr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Status popravke — {{.Nalog.BrojNaloga}}</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif; font-size: 14px; color: #111; background: #f5f5f7; min-height: 100vh; }
|
||||||
|
|
||||||
|
.omotac { max-width: 480px; margin: 0 auto; padding: 24px 16px 48px; }
|
||||||
|
|
||||||
|
/* zaglavlje firme */
|
||||||
|
.zaglavlje { text-align: center; margin-bottom: 24px; }
|
||||||
|
.firma-naziv { font-size: 18px; font-weight: 700; color: #111; }
|
||||||
|
.firma-info { font-size: 12px; color: #666; margin-top: 4px; line-height: 1.6; }
|
||||||
|
|
||||||
|
/* kartica naloga */
|
||||||
|
.kartica { background: #fff; border-radius: 16px; padding: 20px; margin-bottom: 16px; box-shadow: 0 1px 4px rgba(0,0,0,0.08); }
|
||||||
|
.kartica-naslov { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.6px; color: #999; margin-bottom: 12px; }
|
||||||
|
|
||||||
|
.uredjaj { font-size: 22px; font-weight: 700; color: #111; line-height: 1.2; }
|
||||||
|
.broj-naloga { font-size: 13px; color: #888; margin-top: 4px; font-family: monospace; }
|
||||||
|
|
||||||
|
/* status bedž */
|
||||||
|
.status-bedz {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 6px 14px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
.status-primljeno { background: #e8f0fe; color: #1a56db; }
|
||||||
|
.status-dijagnostika { background: #fef3c7; color: #b45309; }
|
||||||
|
.status-ceka { background: #fee2e2; color: #dc2626; }
|
||||||
|
.status-popravka { background: #fef3c7; color: #b45309; }
|
||||||
|
.status-zavrseno { background: #d1fae5; color: #059669; }
|
||||||
|
.status-preuzeto { background: #f3f4f6; color: #374151; }
|
||||||
|
|
||||||
|
/* napredak */
|
||||||
|
.napredak { margin: 16px 0 4px; }
|
||||||
|
.napredak-labela { font-size: 11px; color: #999; margin-bottom: 8px; }
|
||||||
|
.koraci { display: flex; gap: 4px; }
|
||||||
|
.korak {
|
||||||
|
flex: 1;
|
||||||
|
height: 5px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: #e5e7eb;
|
||||||
|
}
|
||||||
|
.korak.aktivan { background: #10b981; }
|
||||||
|
.korak.zavrseno { background: #10b981; }
|
||||||
|
.korak.tekuci { background: #f59e0b; }
|
||||||
|
|
||||||
|
/* red podataka */
|
||||||
|
.red { display: flex; justify-content: space-between; align-items: flex-start; padding: 10px 0; border-bottom: 0.5px solid #f0f0f0; gap: 12px; }
|
||||||
|
.red:last-child { border-bottom: none; }
|
||||||
|
.red-labela { font-size: 12px; color: #888; flex-shrink: 0; }
|
||||||
|
.red-vrednost { font-size: 13px; color: #111; font-weight: 500; text-align: right; }
|
||||||
|
|
||||||
|
/* cena */
|
||||||
|
.cena-blok { background: #f9fafb; border-radius: 10px; padding: 14px 16px; margin-top: 4px; }
|
||||||
|
.cena-od-do { display: flex; gap: 8px; align-items: center; }
|
||||||
|
.cena-od-do .cena-vrednost { font-size: 20px; font-weight: 700; color: #111; }
|
||||||
|
.cena-od-do .cena-sep { color: #bbb; }
|
||||||
|
.cena-labela { font-size: 11px; color: #888; margin-top: 2px; }
|
||||||
|
|
||||||
|
/* kontakt */
|
||||||
|
.kontakt { background: #fff; border-radius: 16px; padding: 18px 20px; box-shadow: 0 1px 4px rgba(0,0,0,0.08); }
|
||||||
|
.kontakt-naslov { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.6px; color: #999; margin-bottom: 12px; }
|
||||||
|
.kontakt-tel { font-size: 18px; font-weight: 700; color: #1a56db; text-decoration: none; display: block; }
|
||||||
|
.kontakt-adresa { font-size: 13px; color: #666; margin-top: 6px; line-height: 1.5; }
|
||||||
|
|
||||||
|
/* napomena za klijenta */
|
||||||
|
.napomena-blok { background: #fffbeb; border-radius: 10px; padding: 12px 14px; margin-top: 4px; font-size: 13px; color: #555; line-height: 1.5; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="omotac">
|
||||||
|
|
||||||
|
<!-- zaglavlje firme -->
|
||||||
|
<div class="zaglavlje">
|
||||||
|
<div class="firma-naziv">{{if .NazivFirme}}{{.NazivFirme}}{{else}}Servis{{end}}</div>
|
||||||
|
{{if .Adresa}}<div class="firma-info">{{.Adresa}}</div>{{end}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- kartica: uređaj i status -->
|
||||||
|
<div class="kartica">
|
||||||
|
<div class="kartica-naslov">Vaš uređaj</div>
|
||||||
|
<div class="uredjaj">{{.Nalog.Uredjaj}}</div>
|
||||||
|
<div class="broj-naloga">Nalog: {{.Nalog.BrojNaloga}}</div>
|
||||||
|
|
||||||
|
{{$s := .Nalog.Status}}
|
||||||
|
{{if eq $s "Primljeno"}}
|
||||||
|
<span class="status-bedz status-primljeno">Primljeno</span>
|
||||||
|
{{else if eq $s "U dijagnostici"}}
|
||||||
|
<span class="status-bedz status-dijagnostika">U dijagnostici</span>
|
||||||
|
{{else if eq $s "Čeka delove"}}
|
||||||
|
<span class="status-bedz status-ceka">Čeka delove</span>
|
||||||
|
{{else if eq $s "U popravci"}}
|
||||||
|
<span class="status-bedz status-popravka">U popravci</span>
|
||||||
|
{{else if eq $s "Završeno"}}
|
||||||
|
<span class="status-bedz status-zavrseno">Završeno — možete preuzeti</span>
|
||||||
|
{{else if eq $s "Preuzeto"}}
|
||||||
|
<span class="status-bedz status-preuzeto">Preuzeto</span>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<!-- traka napretka -->
|
||||||
|
<div class="napredak">
|
||||||
|
<div class="napredak-labela">Napredak popravke</div>
|
||||||
|
<div class="koraci">
|
||||||
|
{{range $i, $korak := .SviStatusi}}
|
||||||
|
{{if eq $korak $s}}
|
||||||
|
<div class="korak tekuci"></div>
|
||||||
|
{{else if statusPre $korak $s $.SviStatusi}}
|
||||||
|
<div class="korak zavrseno"></div>
|
||||||
|
{{else}}
|
||||||
|
<div class="korak"></div>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- kartica: detalji -->
|
||||||
|
<div class="kartica">
|
||||||
|
<div class="kartica-naslov">Detalji</div>
|
||||||
|
|
||||||
|
<div class="red">
|
||||||
|
<span class="red-labela">Datum prijema</span>
|
||||||
|
<span class="red-vrednost">{{.Nalog.DatumPrijema.Format "02.01.2006."}}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{if .Nalog.DatumZavrsetka}}
|
||||||
|
<div class="red">
|
||||||
|
<span class="red-labela">Datum završetka</span>
|
||||||
|
<span class="red-vrednost">{{.Nalog.DatumZavrsetka.Format "02.01.2006."}}</span>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if .Nalog.SerijskiBroj}}
|
||||||
|
<div class="red">
|
||||||
|
<span class="red-labela">Serijski broj</span>
|
||||||
|
<span class="red-vrednost">{{.Nalog.SerijskiBroj}}</span>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if .Nalog.OpisKvara}}
|
||||||
|
<div class="red">
|
||||||
|
<span class="red-labela">Opis kvara</span>
|
||||||
|
<span class="red-vrednost">{{.Nalog.OpisKvara}}</span>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if .Nalog.GarancijaDo}}
|
||||||
|
<div class="red">
|
||||||
|
<span class="red-labela">Garancija do</span>
|
||||||
|
<span class="red-vrednost">{{.Nalog.GarancijaDo.Format "02.01.2006."}}</span>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<!-- procenjena cena -->
|
||||||
|
{{if or .Nalog.CenaOd .Nalog.CenaDo .Nalog.CenaKonacna}}
|
||||||
|
<div style="margin-top:12px;">
|
||||||
|
<div class="red-labela" style="margin-bottom:8px;">Procena cene</div>
|
||||||
|
<div class="cena-blok">
|
||||||
|
{{if .Nalog.CenaKonacna}}
|
||||||
|
<div class="cena-od-do">
|
||||||
|
<span class="cena-vrednost">{{formatBroj .Nalog.CenaKonacna}} din</span>
|
||||||
|
</div>
|
||||||
|
<div class="cena-labela">Konačna cena popravke</div>
|
||||||
|
{{else if and .Nalog.CenaOd .Nalog.CenaDo}}
|
||||||
|
<div class="cena-od-do">
|
||||||
|
<span class="cena-vrednost">{{formatBroj .Nalog.CenaOd}}</span>
|
||||||
|
<span class="cena-sep">–</span>
|
||||||
|
<span class="cena-vrednost">{{formatBroj .Nalog.CenaDo}} din</span>
|
||||||
|
</div>
|
||||||
|
<div class="cena-labela">Procenjeni raspon cene</div>
|
||||||
|
{{else if .Nalog.CenaOd}}
|
||||||
|
<div class="cena-od-do">
|
||||||
|
<span class="cena-vrednost">od {{formatBroj .Nalog.CenaOd}} din</span>
|
||||||
|
</div>
|
||||||
|
<div class="cena-labela">Procenjena cena od</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if .Nalog.Napomena}}
|
||||||
|
<div style="margin-top:12px;">
|
||||||
|
<div class="red-labela" style="margin-bottom:6px;">Napomena</div>
|
||||||
|
<div class="napomena-blok">{{.Nalog.Napomena}}</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- kontakt -->
|
||||||
|
{{if .Telefon}}
|
||||||
|
<div class="kontakt">
|
||||||
|
<div class="kontakt-naslov">Kontakt</div>
|
||||||
|
<a href="tel:{{.Telefon}}" class="kontakt-tel">{{.Telefon}}</a>
|
||||||
|
{{if .Adresa}}<div class="kontakt-adresa">{{.Adresa}}</div>{{end}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -28,6 +28,14 @@
|
|||||||
|
|
||||||
{{if .AppPozadina}}
|
{{if .AppPozadina}}
|
||||||
<style>
|
<style>
|
||||||
|
:root {
|
||||||
|
--app-blur-bg: {{.AppPozadinaBlurPozadine}}px;
|
||||||
|
--app-blur-bg-inset: {{if ne .AppPozadinaBlurPozadine "0"}}-20px{{else}}0{{end}};
|
||||||
|
--app-overlay: {{.AppPozadinaOpacity}}%;
|
||||||
|
--app-blur: {{.AppPozadinaBlur}}px;
|
||||||
|
--app-glass-sb: {{if .AppPozadinaGlassOpacity}}{{.AppPozadinaGlassOpacity}}%{{else}}30%{{end}};
|
||||||
|
--app-glass-el: {{if .AppPozadinaGlassOpacity}}{{.AppPozadinaGlassOpacity}}%{{else}}8%{{end}};
|
||||||
|
}
|
||||||
html {
|
html {
|
||||||
background: url('{{.AppPozadina}}') center/cover fixed;
|
background: url('{{.AppPozadina}}') center/cover fixed;
|
||||||
background-color: #1f2228;
|
background-color: #1f2228;
|
||||||
@@ -36,9 +44,9 @@
|
|||||||
body::before {
|
body::before {
|
||||||
content: '';
|
content: '';
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: {{if ne .AppPozadinaBlurPozadine "0"}}-20px{{else}}0{{end}};
|
inset: var(--app-blur-bg-inset);
|
||||||
background: url('{{.AppPozadina}}') center/cover;
|
background: url('{{.AppPozadina}}') center/cover;
|
||||||
filter: blur({{.AppPozadinaBlurPozadine}}px);
|
filter: blur(var(--app-blur-bg));
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
@@ -46,18 +54,18 @@
|
|||||||
content: '';
|
content: '';
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background: rgba(0,0,0,{{.AppPozadinaOpacity}}%);
|
background: rgba(0,0,0,var(--app-overlay));
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
.raspored { position: relative; z-index: 2; }
|
.raspored { position: relative; z-index: 2; }
|
||||||
.sidebar { background: rgba(0,0,0,{{if .AppPozadinaGlassOpacity}}{{.AppPozadinaGlassOpacity}}%{{else}}0.3{{end}}) !important; backdrop-filter: blur({{.AppPozadinaBlur}}px); -webkit-backdrop-filter: blur({{.AppPozadinaBlur}}px); border-right: 1px solid rgba(255,255,255,0.12) !important; }
|
.sidebar { background: rgba(0,0,0,var(--app-glass-sb)) !important; backdrop-filter: blur(var(--app-blur)); -webkit-backdrop-filter: blur(var(--app-blur)); border-right: 1px solid rgba(255,255,255,0.12) !important; }
|
||||||
.sidebar .nav-stavka, .sidebar .logo-naziv, .sidebar .logo-podnazlov { text-shadow: 0 1px 3px rgba(0,0,0,0.8); color: rgba(255,255,255,0.95) !important; }
|
.sidebar .nav-stavka, .sidebar .logo-naziv, .sidebar .logo-podnazlov { text-shadow: 0 1px 3px rgba(0,0,0,0.8); color: rgba(255,255,255,0.95) !important; }
|
||||||
.sidebar .nav-stavka svg { color: rgba(255,255,255,0.95) !important; stroke: rgba(255,255,255,0.95) !important; }
|
.sidebar .nav-stavka svg { color: rgba(255,255,255,0.95) !important; stroke: rgba(255,255,255,0.95) !important; }
|
||||||
.sidebar .nav-oznaka { text-shadow: 0 1px 3px rgba(0,0,0,0.8); color: rgba(255,255,255,0.7) !important; }
|
.sidebar .nav-oznaka { text-shadow: 0 1px 3px rgba(0,0,0,0.8); color: rgba(255,255,255,0.7) !important; }
|
||||||
.topbar { background: rgba(0,0,0,{{if .AppPozadinaGlassOpacity}}{{.AppPozadinaGlassOpacity}}%{{else}}0.08{{end}}) !important; backdrop-filter: blur({{.AppPozadinaBlur}}px); -webkit-backdrop-filter: blur({{.AppPozadinaBlur}}px); border-bottom: 1px solid rgba(255,255,255,0.12) !important; }
|
.topbar { background: rgba(0,0,0,var(--app-glass-el)) !important; backdrop-filter: blur(var(--app-blur)); -webkit-backdrop-filter: blur(var(--app-blur)); border-bottom: 1px solid rgba(255,255,255,0.12) !important; }
|
||||||
.topbar-naslov { color: rgba(255,255,255,0.95) !important; text-shadow: 0 1px 2px rgba(0,0,0,0.9), 0 0 6px rgba(0,0,0,0.55); }
|
.topbar-naslov { color: rgba(255,255,255,0.95) !important; text-shadow: 0 1px 2px rgba(0,0,0,0.9), 0 0 6px rgba(0,0,0,0.55); }
|
||||||
.kartica, .premesti-modal { background: rgba(0,0,0,{{if .AppPozadinaGlassOpacity}}{{.AppPozadinaGlassOpacity}}%{{else}}0.08{{end}}) !important; backdrop-filter: blur({{.AppPozadinaBlur}}px); -webkit-backdrop-filter: blur({{.AppPozadinaBlur}}px); border: 1px solid rgba(255,255,255,0.12) !important; }
|
.kartica, .premesti-modal { background: rgba(0,0,0,var(--app-glass-el)) !important; backdrop-filter: blur(var(--app-blur)); -webkit-backdrop-filter: blur(var(--app-blur)); border: 1px solid rgba(255,255,255,0.12) !important; }
|
||||||
body:not([data-hover]) .kartica:hover { background: rgba(0,0,0,0.38) !important; }
|
body:not([data-hover]) .kartica:hover { background: rgba(0,0,0,0.38) !important; }
|
||||||
body:not([data-hover]) .kartica:hover .dash-ikona { filter: brightness(0.55); }
|
body:not([data-hover]) .kartica:hover .dash-ikona { filter: brightness(0.55); }
|
||||||
.kartica p, .kartica span, .kartica h1, .kartica h2, .kartica h3, .kartica h4, .kartica label, .kartica td, .kartica th, .kartica li, .kartica a { color: rgba(255,255,255,0.95) !important; text-shadow: 0 1px 2px rgba(0,0,0,0.9), 0 0 6px rgba(0,0,0,0.55); }
|
.kartica p, .kartica span, .kartica h1, .kartica h2, .kartica h3, .kartica h4, .kartica label, .kartica td, .kartica th, .kartica li, .kartica a { color: rgba(255,255,255,0.95) !important; text-shadow: 0 1px 2px rgba(0,0,0,0.9), 0 0 6px rgba(0,0,0,0.55); }
|
||||||
@@ -75,7 +83,7 @@
|
|||||||
dobijaju sopstvenu staklenu podlogu da se vide bez obzira na sliku ispod */
|
dobijaju sopstvenu staklenu podlogu da se vide bez obzira na sliku ispod */
|
||||||
.nazad-link, .btn-sekundarno, .btn-obrisi-ghost, .cek-filter {
|
.nazad-link, .btn-sekundarno, .btn-obrisi-ghost, .cek-filter {
|
||||||
background: rgba(0,0,0,0.28) !important;
|
background: rgba(0,0,0,0.28) !important;
|
||||||
backdrop-filter: blur({{.AppPozadinaBlur}}px); -webkit-backdrop-filter: blur({{.AppPozadinaBlur}}px);
|
backdrop-filter: blur(var(--app-blur)); -webkit-backdrop-filter: blur(var(--app-blur));
|
||||||
border: 1px solid rgba(255,255,255,0.18) !important;
|
border: 1px solid rgba(255,255,255,0.18) !important;
|
||||||
color: rgba(255,255,255,0.95) !important; text-shadow: 0 1px 3px rgba(0,0,0,0.8);
|
color: rgba(255,255,255,0.95) !important; text-shadow: 0 1px 3px rgba(0,0,0,0.8);
|
||||||
}
|
}
|
||||||
@@ -110,7 +118,7 @@
|
|||||||
</script>
|
</script>
|
||||||
{{end}}
|
{{end}}
|
||||||
</head>
|
</head>
|
||||||
<body{{if .LokalnaAnimacija}} data-animacija="{{.LokalnaAnimacija}}"{{end}}{{if .LokalniHover}} data-hover="{{.LokalniHover}}"{{end}}>
|
<body{{if .LokalnaAnimacija}} data-animacija="{{.LokalnaAnimacija}}"{{end}}{{if .LokalniHover}} data-hover="{{.LokalniHover}}"{{end}}{{if .LokalnaBrzinaAnimacije}} data-brzina-animacije="{{.LokalnaBrzinaAnimacije}}"{{end}}>
|
||||||
<div class="raspored">
|
<div class="raspored">
|
||||||
<div class="sidebar-overlay" id="sidebar-overlay"></div>
|
<div class="sidebar-overlay" id="sidebar-overlay"></div>
|
||||||
{{template "sidebar" .}}
|
{{template "sidebar" .}}
|
||||||
@@ -307,6 +315,8 @@
|
|||||||
|
|
||||||
function ntechInicijalizuj() {
|
function ntechInicijalizuj() {
|
||||||
ntechKonvertujPoruke();
|
ntechKonvertujPoruke();
|
||||||
|
|
||||||
|
|
||||||
var m = document.querySelector('meta[name="csrf-token"]');
|
var m = document.querySelector('meta[name="csrf-token"]');
|
||||||
if (m && m.content) {
|
if (m && m.content) {
|
||||||
document.querySelectorAll('form[method="POST"],form[method="post"]').forEach(function(f) {
|
document.querySelectorAll('form[method="POST"],form[method="post"]').forEach(function(f) {
|
||||||
@@ -316,6 +326,16 @@
|
|||||||
f.appendChild(i);
|
f.appendChild(i);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// toast pri učitavanju stranice ako URL sadrži ?sacuvano=1 (cross-page redirect)
|
||||||
|
if (!window._ntechSacuvanoPrikazano && location.search.indexOf('sacuvano=1') !== -1) {
|
||||||
|
window._ntechSacuvanoPrikazano = true;
|
||||||
|
window.ntechToast('Sačuvano', 'uspeh');
|
||||||
|
var _p = new URLSearchParams(location.search);
|
||||||
|
_p.delete('sacuvano');
|
||||||
|
var _q = _p.toString() ? '?' + _p.toString() : '';
|
||||||
|
history.replaceState(null, '', location.pathname + _q + location.hash);
|
||||||
|
}
|
||||||
|
|
||||||
document.querySelectorAll('[data-potvrda]').forEach(function(el) {
|
document.querySelectorAll('[data-potvrda]').forEach(function(el) {
|
||||||
if (el._potvrda) return;
|
if (el._potvrda) return;
|
||||||
el._potvrda = true;
|
el._potvrda = true;
|
||||||
@@ -323,14 +343,87 @@
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
window._ntechPotvrdi(el.getAttribute('data-potvrda')).then(function(ok) {
|
window._ntechPotvrdi(el.getAttribute('data-potvrda')).then(function(ok) {
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
// dugme unutar forme — submit forme
|
|
||||||
var forma = el.closest('form');
|
var forma = el.closest('form');
|
||||||
if (forma) { forma.submit(); return; }
|
if (forma) { forma.submit(); return; }
|
||||||
// link
|
|
||||||
if (el.href) { window.location.href = el.href; }
|
if (el.href) { window.location.href = el.href; }
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// AJAX submit — forme koje serveru vraćaju ?sacuvano= ostaju na stranici (scroll se ne gubi)
|
||||||
|
document.querySelectorAll('form[method="POST"],form[method="post"]').forEach(function(f) {
|
||||||
|
if (f._ajaxSave) return;
|
||||||
|
f._ajaxSave = true;
|
||||||
|
f.addEventListener('submit', function(e) {
|
||||||
|
// file upload i forme sa data-full-reload šalju se normalno
|
||||||
|
if (f.enctype === 'multipart/form-data' || f.hasAttribute('data-full-reload')) return;
|
||||||
|
e.preventDefault();
|
||||||
|
// prikazujemo dugme kao zauzeto
|
||||||
|
var btn = f.querySelector('[type="submit"]');
|
||||||
|
if (btn) btn.disabled = true;
|
||||||
|
// URLSearchParams šalje kao application/x-www-form-urlencoded
|
||||||
|
// što Go-ov r.FormValue() (i CSRF middleware) može da pročita
|
||||||
|
fetch(f.action || location.href, {
|
||||||
|
method: 'POST',
|
||||||
|
body: new URLSearchParams(new FormData(f)),
|
||||||
|
redirect: 'follow'
|
||||||
|
}).then(function(res) {
|
||||||
|
var finUrl = new URL(res.url);
|
||||||
|
var isStiPath = finUrl.pathname === location.pathname;
|
||||||
|
var imaSacuvano = finUrl.search.indexOf('sacuvano') !== -1;
|
||||||
|
if (isStiPath && imaSacuvano) {
|
||||||
|
// uspeh na istoj stranici — prikaži toast, ostani
|
||||||
|
window.ntechToast('Sačuvano', 'uspeh');
|
||||||
|
if (btn) btn.disabled = false;
|
||||||
|
// odmah primeni podešavanja koja menjaju globalne atribute body-ja
|
||||||
|
var anim = f.querySelector('[name="lokalna_animacija"]');
|
||||||
|
if (anim) {
|
||||||
|
if (anim.value) document.body.dataset.animacija = anim.value;
|
||||||
|
else delete document.body.dataset.animacija;
|
||||||
|
}
|
||||||
|
var hov = f.querySelector('[name="lokalni_hover"]');
|
||||||
|
if (hov) {
|
||||||
|
if (hov.value) document.body.dataset.hover = hov.value;
|
||||||
|
else delete document.body.dataset.hover;
|
||||||
|
}
|
||||||
|
var brzina = f.querySelector('[name="lokalna_brzina_animacije"]');
|
||||||
|
if (brzina) {
|
||||||
|
if (brzina.value) document.body.dataset.brzinaAnimacije = brzina.value;
|
||||||
|
else delete document.body.dataset.brzinaAnimacije;
|
||||||
|
}
|
||||||
|
// toggle „Prikaži logo u gornjoj traci" — pokaži/sakrij logo u topbaru bez reloda
|
||||||
|
var logoToggle = f.querySelector('[name="topbar_logo_slika"]');
|
||||||
|
if (logoToggle) {
|
||||||
|
var logoImg = document.querySelector('.topbar-logo');
|
||||||
|
if (logoImg) logoImg.classList.toggle('skriven', !logoToggle.checked);
|
||||||
|
}
|
||||||
|
// posle čuvanja stilova lične pozadine ažuriraj CSS custom properties
|
||||||
|
var bgBlur = f.querySelector('[name="lokalna_pozadina_blur"]');
|
||||||
|
if (bgBlur) {
|
||||||
|
var r = document.documentElement;
|
||||||
|
var bgBlurBg = parseInt(f.querySelector('[name="lokalna_pozadina_blur_pozadine"]').value) || 0;
|
||||||
|
var bgOp = parseInt(f.querySelector('[name="lokalna_pozadina_opacity"]').value) || 0;
|
||||||
|
var bgGlass = parseInt(f.querySelector('[name="lokalna_pozadina_glass_opacity"]').value) || 0;
|
||||||
|
var bgBlurV = parseInt(bgBlur.value) || 0;
|
||||||
|
r.style.setProperty('--app-blur-bg', bgBlurBg + 'px');
|
||||||
|
r.style.setProperty('--app-blur-bg-inset', bgBlurBg > 0 ? '-20px' : '0');
|
||||||
|
r.style.setProperty('--app-overlay', bgOp + '%');
|
||||||
|
r.style.setProperty('--app-blur', bgBlurV + 'px');
|
||||||
|
r.style.setProperty('--app-glass-sb', bgGlass + '%');
|
||||||
|
r.style.setProperty('--app-glass-el', bgGlass + '%');
|
||||||
|
}
|
||||||
|
// promena teme zahteva reload (menja se ceo CSS fajl)
|
||||||
|
if (f.querySelector('[name="lokalna_tema"]')) location.reload();
|
||||||
|
} else {
|
||||||
|
// redirect na drugu stranicu ili bez sacuvano — navigiraj normalno
|
||||||
|
location.href = res.url;
|
||||||
|
}
|
||||||
|
}).catch(function() {
|
||||||
|
// mrežna greška — pošalji formu normalno
|
||||||
|
f.submit();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (!window._ntechCsrfDodato) {
|
if (!window._ntechCsrfDodato) {
|
||||||
window._ntechCsrfDodato = true;
|
window._ntechCsrfDodato = true;
|
||||||
|
|||||||
Reference in New Issue
Block a user