Servis forma, animacije, hover efekti i pojačane senke
Servis: - Nova polja: ostecenja, pin_uredjaja, pribor (migracija 051) - Default garancija iz podešavanja, svič "Bez garancije" u formi - Podešavanja → Servis: konfigurabilan rok garancije (migracija 052) - Default datum prijema = danas; datum_prijema se eksplicitno upisuje - Sidebar link za Servis podešavanja PDV/Nivelacije: - Default raspon datuma = početak/kraj tekućeg meseca (KIR, KPR, Nivelacije) - Dodata class="tabela" na tabele bez klase (KIR, KPR, Obračun, Nivelacije) Animacije (Moj profil → Tema): - Korisnik bira vrstu animacije: bez, fadeInUp, fadeIn, scaleIn, slideLeft - Čuva se po korisniku u korisnici.lokalna_animacija (migracija 053) - CSS [data-animacija] radi na body (globalno) i na preview wrapperima (izolovano) - Preview animacije izolovan: data-animacija na #anim-preview-wrap, ne na body - Mobilne kartice se animiraju kad korisnik odabere stil (podrazumevano ne) - Animacija primenjena direktno na .tabela tbody tr (bez potrebe za .animiraj) Hover efekti (Moj profil → Tema): - Opcije: podrazumevano, bez, podizanje, svetlost, zoom, boja - Čuva se po korisniku u korisnici.lokalni_hover (migracija 054) - CSS [data-hover] radi izolovano; preview menja samo #hover-preview-wrap - Pojačane senke u oba teme (--senka i nova --senka-hover promenljiva) - Transition dodat za transform i background na karticama Grafikon (Izveštaji): toggle zamenjen globalnim .toggl/.toggl-klizac svičom
This commit is contained in:
@@ -46,6 +46,8 @@ type podaciProfilTema struct {
|
||||
LokalnaPozadinaBlur string
|
||||
LokalnaPozadinaBlurPozadine string
|
||||
LokalnaPozadinaGlassOpacity string
|
||||
LokalnaAnimacija string
|
||||
LokalniHover string
|
||||
}
|
||||
|
||||
// AdminKorisnici prikazuje listu korisnika
|
||||
|
||||
@@ -211,6 +211,10 @@ func (h *Handler) popuniPodaciStranice(r *http.Request, podesavanja map[string]s
|
||||
ps.AppPozadinaGlassOpacity = "10"
|
||||
}
|
||||
}
|
||||
if korisnik != nil {
|
||||
ps.LokalnaAnimacija = korisnik.LokalnaAnimacija
|
||||
ps.LokalniHover = korisnik.LokalniHover
|
||||
}
|
||||
|
||||
return ps
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ var saSidebar = []string{
|
||||
"klijenti", "klijent_forma",
|
||||
"magacin", "magacin_forma",
|
||||
"nabavke", "nabavka_forma", "nabavka_detalji",
|
||||
"podesavanja", "podesavanja_opste", "podesavanja_izgled", "podesavanja_sistem",
|
||||
"podesavanja", "podesavanja_opste", "podesavanja_izgled", "podesavanja_sistem", "podesavanja_servis",
|
||||
"pdv_stope",
|
||||
"pdv_kir", "pdv_kir_forma",
|
||||
"pdv_kpr", "pdv_kpr_forma",
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"ntech/internal/db/sqlite"
|
||||
"ntech/internal/middleware"
|
||||
@@ -34,6 +35,18 @@ func (h *Handler) Nivelacije(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
odStr := r.URL.Query().Get("od")
|
||||
doStr := r.URL.Query().Get("do")
|
||||
// podrazumevano: tekući mesec (od prvog do poslednjeg dana)
|
||||
if odStr == "" || doStr == "" {
|
||||
sada := time.Now()
|
||||
prvi := time.Date(sada.Year(), sada.Month(), 1, 0, 0, 0, 0, sada.Location())
|
||||
poslednji := prvi.AddDate(0, 1, -1)
|
||||
if odStr == "" {
|
||||
odStr = prvi.Format("2006-01-02")
|
||||
}
|
||||
if doStr == "" {
|
||||
doStr = poslednji.Format("2006-01-02")
|
||||
}
|
||||
}
|
||||
zapisi, err := h.NivelacijaRepo.Lista(r.Context(), parsiraDatumOpcionalno(odStr), parsiraDatumOpcionalno(doStr))
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri učitavanju nivelacija", http.StatusInternalServerError)
|
||||
|
||||
@@ -61,6 +61,18 @@ func (h *Handler) PdvKir(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
odStr := r.URL.Query().Get("od")
|
||||
doStr := r.URL.Query().Get("do")
|
||||
// podrazumevano: tekući mesec (od prvog do poslednjeg dana)
|
||||
if odStr == "" || doStr == "" {
|
||||
sada := time.Now()
|
||||
prvi := time.Date(sada.Year(), sada.Month(), 1, 0, 0, 0, 0, sada.Location())
|
||||
poslednji := prvi.AddDate(0, 1, -1)
|
||||
if odStr == "" {
|
||||
odStr = prvi.Format("2006-01-02")
|
||||
}
|
||||
if doStr == "" {
|
||||
doStr = poslednji.Format("2006-01-02")
|
||||
}
|
||||
}
|
||||
zapisi, err := h.PdvKirRepo.Lista(r.Context(), parsiraDatumOpcionalno(odStr), parsiraDatumOpcionalno(doStr))
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri učitavanju knjige izdatih računa", http.StatusInternalServerError)
|
||||
|
||||
@@ -42,6 +42,18 @@ func (h *Handler) PdvKpr(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
odStr := r.URL.Query().Get("od")
|
||||
doStr := r.URL.Query().Get("do")
|
||||
// podrazumevano: tekući mesec (od prvog do poslednjeg dana)
|
||||
if odStr == "" || doStr == "" {
|
||||
sada := time.Now()
|
||||
prvi := time.Date(sada.Year(), sada.Month(), 1, 0, 0, 0, 0, sada.Location())
|
||||
poslednji := prvi.AddDate(0, 1, -1)
|
||||
if odStr == "" {
|
||||
odStr = prvi.Format("2006-01-02")
|
||||
}
|
||||
if doStr == "" {
|
||||
doStr = poslednji.Format("2006-01-02")
|
||||
}
|
||||
}
|
||||
zapisi, err := h.PdvKprRepo.Lista(r.Context(), parsiraDatumOpcionalno(odStr), parsiraDatumOpcionalno(doStr))
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri učitavanju knjige primljenih računa", http.StatusInternalServerError)
|
||||
|
||||
@@ -43,6 +43,7 @@ type PodaciPodesavanja struct {
|
||||
BackupIntervalSati string
|
||||
BackupBrojKopija string
|
||||
KalkulacijaMarza string
|
||||
ServisGarancijaMeseci string
|
||||
LoginPozadina string
|
||||
LoginPozadinaOpacity string
|
||||
LoginPozadinaBlurPozadine string
|
||||
@@ -314,6 +315,20 @@ func (h *Handler) SacuvajPodesavanja(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// podrazumevani rok garancije za servis (meseci, 0–120)
|
||||
if v := strings.TrimSpace(r.FormValue("servis_garancija_meseci")); v != "" {
|
||||
n, err := strconv.Atoi(v)
|
||||
if err != nil || n < 0 || n > 120 {
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Rok garancije mora biti broj između 0 i 120 meseci.")
|
||||
http.Redirect(w, r, sledeci, http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "servis_garancija_meseci", strconv.Itoa(n)); err != nil {
|
||||
http.Error(w, "Greška pri čuvanju podešavanja", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
http.Redirect(w, r, sledeci+"?sacuvano=1", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
@@ -438,6 +453,20 @@ func (h *Handler) UkloniLogo(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/admin/podesavanja/opste?sacuvano=1", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// PodesavanjaServis renderuje stranicu sa podešavanjima servisnog modula
|
||||
func (h *Handler) PodesavanjaServis(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := h.zahtevajDozvolu(w, r, "podesavanja.pregled"); !ok {
|
||||
return
|
||||
}
|
||||
podaci, err := h.napuniPodaciPodesavanja(r, "Podešavanja — Servis")
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
podaci.Stranica = "podesavanja-servis"
|
||||
h.renderujTemplate(w, "podesavanja_servis", podaci)
|
||||
}
|
||||
|
||||
// generisiImeUploada vraća slučajno hex ime (16 bajtova) sa datom ekstenzijom
|
||||
func generisiImeUploada(ext string) (string, error) {
|
||||
buf := make([]byte, 16)
|
||||
@@ -669,6 +698,7 @@ func (h *Handler) napuniPodaciPodesavanja(r *http.Request, naslov string) (Podac
|
||||
BackupIntervalSati: vrednostIliDefault(podesavanja, "backup_interval_sati", "24"),
|
||||
BackupBrojKopija: vrednostIliDefault(podesavanja, "backup_broj_kopija", "7"),
|
||||
KalkulacijaMarza: vrednostIliDefault(podesavanja, "kalkulacija_marza", "20"),
|
||||
ServisGarancijaMeseci: vrednostIliDefault(podesavanja, "servis_garancija_meseci", "2"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,8 @@ func (h *Handler) ProfilTema(w http.ResponseWriter, r *http.Request) {
|
||||
LokalnaPozadinaBlur: svezi.LokalnaPozadinaBlur,
|
||||
LokalnaPozadinaBlurPozadine: svezi.LokalnaPozadinaBlurPozadine,
|
||||
LokalnaPozadinaGlassOpacity: svezi.LokalnaPozadinaGlassOpacity,
|
||||
LokalnaAnimacija: svezi.LokalnaAnimacija,
|
||||
LokalniHover: svezi.LokalniHover,
|
||||
}
|
||||
if podaci.LokalnaPozadinaOpacity == "" {
|
||||
podaci.LokalnaPozadinaOpacity = "50"
|
||||
@@ -293,6 +295,84 @@ func (h *Handler) SacuvajLokalnuTemu(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/admin/profil", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// SacuvajLokalnuAnimaciju čuva korisnikovu preferencu animacije tabela
|
||||
func (h *Handler) SacuvajLokalnuAnimaciju(w http.ResponseWriter, r *http.Request) {
|
||||
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||
if k == nil {
|
||||
http.Redirect(w, r, "/prijava", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "tema.lokalno") {
|
||||
http.Error(w, "Pristup odbijen", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if err := r.ParseForm(); err != nil {
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Greška. Pokušajte ponovo.")
|
||||
http.Redirect(w, r, "/admin/profil/tema", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
animacija := r.FormValue("lokalna_animacija")
|
||||
dozvoljene := map[string]bool{"": true, "bez": true, "fadeInUp": true, "fadeIn": true, "scaleIn": true, "slideLeft": true}
|
||||
if !dozvoljene[animacija] {
|
||||
animacija = ""
|
||||
}
|
||||
|
||||
if err := h.KorisniciRepo.SacuvajLokalnuAnimaciju(r.Context(), k.ID, animacija); err != nil {
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju. Pokušajte ponovo.")
|
||||
http.Redirect(w, r, "/admin/profil/tema", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Animacija je sačuvana.")
|
||||
if ref := r.Referer(); ref != "" {
|
||||
if u, err := url.Parse(ref); err == nil {
|
||||
http.Redirect(w, r, u.RequestURI(), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
}
|
||||
http.Redirect(w, r, "/admin/profil/tema", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// SacuvajLokalniHover čuva korisnikovu preferencu hover efekta na karticama
|
||||
func (h *Handler) SacuvajLokalniHover(w http.ResponseWriter, r *http.Request) {
|
||||
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||
if k == nil {
|
||||
http.Redirect(w, r, "/prijava", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "tema.lokalno") {
|
||||
http.Error(w, "Pristup odbijen", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if err := r.ParseForm(); err != nil {
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Greška. Pokušajte ponovo.")
|
||||
http.Redirect(w, r, "/admin/profil/tema", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
hover := r.FormValue("lokalni_hover")
|
||||
dozvoljeni := map[string]bool{"": true, "bez": true, "podizanje": true, "svetlost": true, "zoom": true, "boja": true}
|
||||
if !dozvoljeni[hover] {
|
||||
hover = ""
|
||||
}
|
||||
|
||||
if err := h.KorisniciRepo.SacuvajLokalniHover(r.Context(), k.ID, hover); err != nil {
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri čuvanju. Pokušajte ponovo.")
|
||||
http.Redirect(w, r, "/admin/profil/tema", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Hover efekat je sačuvan.")
|
||||
if ref := r.Referer(); ref != "" {
|
||||
if u, err := url.Parse(ref); err == nil {
|
||||
http.Redirect(w, r, u.RequestURI(), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
}
|
||||
http.Redirect(w, r, "/admin/profil/tema", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// ProfilOtpremiAvatar prima upload lične avatar slike korisnika
|
||||
func (h *Handler) ProfilOtpremiAvatar(w http.ResponseWriter, r *http.Request) {
|
||||
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||
|
||||
@@ -110,9 +110,15 @@ func (h *Handler) NoviNalog(w http.ResponseWriter, r *http.Request) {
|
||||
ps := h.popuniPodaciStranice(r, podesavanja)
|
||||
ps.Stranica = "servis"
|
||||
ps.NaslovStranice = "Novi nalog"
|
||||
noviNalog := model.ServisniNalog{
|
||||
BrojNaloga: brojNaloga,
|
||||
Status: model.StatusPrimljeno,
|
||||
DatumPrijema: time.Now(),
|
||||
}
|
||||
noviNalog.GarancijaDo = defaultGarancija(noviNalog.DatumPrijema, podesavanja)
|
||||
h.renderujFormuNaloga(w, PodaciFormeNaloga{
|
||||
PodaciStranice: ps,
|
||||
Nalog: model.ServisniNalog{BrojNaloga: brojNaloga, Status: model.StatusPrimljeno},
|
||||
Nalog: noviNalog,
|
||||
Klijenti: klijenti,
|
||||
Tehnicari: tehnicari,
|
||||
SviStatusi: model.SviStatusi,
|
||||
@@ -206,6 +212,9 @@ func (h *Handler) IzmeniNalog(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if nalog.GarancijaDo == nil {
|
||||
nalog.GarancijaDo = defaultGarancija(nalog.DatumPrijema, podesavanja)
|
||||
}
|
||||
ps := h.popuniPodaciStranice(r, podesavanja)
|
||||
ps.Stranica = "servis"
|
||||
ps.NaslovStranice = "Izmeni nalog"
|
||||
@@ -460,6 +469,17 @@ func parseFormuNaloga(r *http.Request) (model.ServisniNalog, string) {
|
||||
OpisKvara: opisKvara,
|
||||
Status: r.FormValue("status"),
|
||||
Napomena: strings.TrimSpace(r.FormValue("napomena")),
|
||||
Ostecenja: strings.TrimSpace(r.FormValue("ostecenja")),
|
||||
PinUredjaja: strings.TrimSpace(r.FormValue("pin_uredjaja")),
|
||||
Pribor: strings.TrimSpace(r.FormValue("pribor")),
|
||||
DatumPrijema: time.Now(),
|
||||
}
|
||||
|
||||
// datum prijema — korisnik može da unese drugi datum (npr. retroaktivno)
|
||||
if dp := strings.TrimSpace(r.FormValue("datum_prijema")); dp != "" {
|
||||
if t, err := time.Parse("2006-01-02", dp); err == nil {
|
||||
nalog.DatumPrijema = t
|
||||
}
|
||||
}
|
||||
|
||||
if nalog.Status == "" {
|
||||
@@ -493,16 +513,29 @@ func parseFormuNaloga(r *http.Request) (model.ServisniNalog, string) {
|
||||
}
|
||||
}
|
||||
|
||||
// opcioni datum garancije
|
||||
if gd := strings.TrimSpace(r.FormValue("garancija_do")); gd != "" {
|
||||
if t, err := time.Parse("2006-01-02", gd); err == nil {
|
||||
nalog.GarancijaDo = &t
|
||||
// opcioni datum garancije — preskačemo ako je korisnik označio "bez garancije"
|
||||
if r.FormValue("bez_garancije") != "1" {
|
||||
if gd := strings.TrimSpace(r.FormValue("garancija_do")); gd != "" {
|
||||
if t, err := time.Parse("2006-01-02", gd); err == nil {
|
||||
nalog.GarancijaDo = &t
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nalog, ""
|
||||
}
|
||||
|
||||
// defaultGarancija vraća datum garancije na osnovu datuma prijema i podešavanja;
|
||||
// vraća nil ako je rok 0 ili podešavanje nedostaje
|
||||
func defaultGarancija(datumPrijema time.Time, podesavanja map[string]string) *time.Time {
|
||||
meseci, err := strconv.Atoi(vrednostIliDefault(podesavanja, "servis_garancija_meseci", "2"))
|
||||
if err != nil || meseci <= 0 {
|
||||
return nil
|
||||
}
|
||||
t := datumPrijema.AddDate(0, meseci, 0)
|
||||
return &t
|
||||
}
|
||||
|
||||
// parseOpcionuCenu pretvara string u *float64 — prazno polje ili neispravna vrednost vraća nil
|
||||
func parseOpcionuCenu(s string) *float64 {
|
||||
s = strings.TrimSpace(s)
|
||||
|
||||
Reference in New Issue
Block a user