diff --git a/cmd/ntech/main.go b/cmd/ntech/main.go index 117cbb5..cf69870 100644 --- a/cmd/ntech/main.go +++ b/cmd/ntech/main.go @@ -357,6 +357,8 @@ func main() { r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "servis.pregled")).Get("/servis/{id}", h.DetaljiNaloga) r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "servis.pregled")).Get("/servis/{id}/radni-nalog", h.StampaRadnogNaloga) r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "servis.pregled")).Get("/servis/{id}/otpremnica", h.StampaOtpremnice) + r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "servis.pregled")).Get("/servis/{id}/revers", h.StampaReversa) + r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "servis.pregled")).Get("/servis/{id}/predracun", h.StampaPredracuna) r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "servis.pregled")).Get("/servis/{id}/nalepnica", h.StampaNalepnice) r.With(doz("servis.izmeni")).Post("/servis/{id}/status", h.PromeniStatus) r.With(doz("servis.izmeni")).Post("/servis/{id}/garancija", h.AzurirajGaranciju) diff --git a/internal/handler/kes.go b/internal/handler/kes.go index 662ae09..1e52367 100644 --- a/internal/handler/kes.go +++ b/internal/handler/kes.go @@ -43,7 +43,7 @@ var saSidebar = []string{ // standalone su šabloni bez base layouta var standaloneIme = []string{ - "prijava", "setup", "totp_provera", "prodaja_stampa", "servis_radni_nalog", "servis_otpremnica", "servis_nalepnica", "servis_status_javni", + "prijava", "setup", "totp_provera", "prodaja_stampa", "servis_radni_nalog", "servis_otpremnica", "servis_revers", "servis_predracun", "servis_nalepnica", "servis_status_javni", } // sablonskeFunkcije su pomoćne funkcije dostupne u svim šablonima. diff --git a/internal/handler/podesavanja.go b/internal/handler/podesavanja.go index 00e3b13..7959a71 100644 --- a/internal/handler/podesavanja.go +++ b/internal/handler/podesavanja.go @@ -20,6 +20,19 @@ import ( "ntech/internal/model" ) +// podrazumevaniUsloviServisa je podrazumevani tekst uslova servisa koji se +// štampa na reversu; korisnik ga može izmeniti u Podešavanja → Servis. +const podrazumevaniUsloviServisa = `1. Servis ne odgovara za podatke na uređaju. Klijent je dužan da sam napravi rezervnu kopiju podataka pre predaje. +2. Uređaj se preuzima uz ovaj revers i ličnu kartu. Bez reversa uređaj se ne izdaje. +3. Rok za podizanje uređaja je 30 dana od obaveštenja o završetku. Po isteku roka servis ne odgovara za uređaj. +4. Ako klijent odustane od popravke, naplaćuje se dijagnostika prema cenovniku. +5. Garancija na izvršeni servis važi za zamenjene delove i obavljeni rad, ne i za nove kvarove. +6. Potpisom klijent potvrđuje da je saglasan sa navedenim uslovima.` + +// podrazumevanaKlauzulaPredracuna je podrazumevana napomena koja se štampa na +// dnu predračuna; korisnik je može izmeniti u Podešavanja → Servis. +const podrazumevanaKlauzulaPredracuna = `Procena je data na osnovu zahteva klijenta i nije fiskalni dokument. Ako se tokom rada utvrdi dodatni kvar, servis kontaktira klijenta radi saglasnosti pre nastavka radova i izmene cene.` + // PodaciPodesavanja su podaci za stranicu podešavanja type PodaciPodesavanja struct { model.PodaciStranice @@ -52,6 +65,8 @@ type PodaciPodesavanja struct { ServisGarancijaDana string PredracunRokDana string PredvidjenRokDana string + ServisUslovi string + ServisKlauzulaPredracuna string LoginPozadina string LoginPozadinaOpacity string LoginPozadinaBlurPozadine string @@ -407,6 +422,25 @@ func (h *Handler) SacuvajPodesavanja(w http.ResponseWriter, r *http.Request) { } } + // uslovi servisa (slobodan tekst, štampa se na reversu); čuva se uvek, + // pa korisnik može i da skrati ili isprazni tekst + if _, ima := r.Form["servis_uslovi"]; ima { + uslovi := strings.TrimSpace(r.FormValue("servis_uslovi")) + if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "servis_uslovi", uslovi); err != nil { + http.Error(w, "Greška pri čuvanju podešavanja", http.StatusInternalServerError) + return + } + } + + // klauzula na predračunu (slobodan tekst); čuva se uvek + if _, ima := r.Form["servis_klauzula_predracuna"]; ima { + klauzula := strings.TrimSpace(r.FormValue("servis_klauzula_predracuna")) + if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "servis_klauzula_predracuna", klauzula); err != nil { + http.Error(w, "Greška pri čuvanju podešavanja", http.StatusInternalServerError) + return + } + } + http.Redirect(w, r, sledeci+"?sacuvano=1", http.StatusSeeOther) } @@ -782,6 +816,8 @@ func (h *Handler) napuniPodaciPodesavanja(r *http.Request, naslov string) (Podac ServisGarancijaDana: vrednostIliDefault(podesavanja, "servis_garancija_dana", "60"), PredracunRokDana: vrednostIliDefault(podesavanja, "predracun_rok_dana", "7"), PredvidjenRokDana: vrednostIliDefault(podesavanja, "predvidjen_rok_dana", "15"), + ServisUslovi: vrednostIliDefault(podesavanja, "servis_uslovi", podrazumevaniUsloviServisa), + ServisKlauzulaPredracuna: vrednostIliDefault(podesavanja, "servis_klauzula_predracuna", podrazumevanaKlauzulaPredracuna), }, nil } diff --git a/internal/handler/servis.go b/internal/handler/servis.go index 8bc2c15..d02506e 100644 --- a/internal/handler/servis.go +++ b/internal/handler/servis.go @@ -1284,6 +1284,209 @@ func (h *Handler) StampaOtpremnice(w http.ResponseWriter, r *http.Request) { }) } +// PodaciReversa su podaci za revers — potvrdu o prijemu uređaja na servis. +// Dokument se izdaje pri prijemu, pa nema cena. +type PodaciReversa struct { + Nalog model.ServisniNalog + Klijent *model.Klijent + KlijentNaziv string + KlijentOznakaIdent string // labela identifikacije ("JMBG" / "Br. lične karte") + KlijentBrojIdent string // vrednost identifikacionog broja + TehnicarNaziv string + QRKod string // base64 QR ka javnoj status strani naloga + Uslovi string // uslovi servisa iz podešavanja (servis_uslovi) + NazivFirme string + Podnazlov string + Adresa string + Telefon string + PIB string +} + +// StampaReversa renderuje revers (potvrdu o prijemu uređaja na servis) +func (h *Handler) StampaReversa(w http.ResponseWriter, r *http.Request) { + id, err := parseID(chi.URLParam(r, "id")) + if err != nil { + http.Error(w, "Neispravan ID naloga", http.StatusBadRequest) + return + } + + nalog, err := h.ServisRepo.DohvatiID(r.Context(), id) + if err != nil { + http.Error(w, "Nalog nije pronađen", http.StatusNotFound) + return + } + + podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB) + if err != nil { + http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError) + return + } + + var klijent *model.Klijent + klijentNaziv := "" + klijentOznaka := "" + klijentBroj := "" + if nalog.KlijentID != nil { + k, err := h.KlijentiRepo.DohvatiID(r.Context(), *nalog.KlijentID) + if err == nil { + klijent = k + klijentNaziv = k.PunoIme() + if k.JMBG != "" { + klijentOznaka = k.OznakaIdentifikacije() + klijentBroj = k.JMBG + } + } + } + + tehnicarNaziv := "" + if nalog.TehnicarID != nil { + tehnicar, err := h.KorisniciRepo.DohvatiPoID(r.Context(), *nalog.TehnicarID) + if err == nil { + tehnicarNaziv = tehnicar.KorisnickoIme + } + } + + nalogURL := qrNalogURL(r, nalog.JavniToken) + var qrKod string + if png, err := qrcode.Encode(nalogURL, qrcode.Medium, 160); err == nil { + qrKod = base64.StdEncoding.EncodeToString(png) + } + + h.renderujStandalone(w, "servis_revers", PodaciReversa{ + Nalog: *nalog, + Klijent: klijent, + KlijentNaziv: klijentNaziv, + KlijentOznakaIdent: klijentOznaka, + KlijentBrojIdent: klijentBroj, + TehnicarNaziv: tehnicarNaziv, + QRKod: qrKod, + Uslovi: vrednostIliDefault(podesavanja, "servis_uslovi", podrazumevaniUsloviServisa), + NazivFirme: podesavanja["naziv_firme"], + Podnazlov: podesavanja["podnazlov"], + Adresa: podesavanja["adresa"], + Telefon: podesavanja["telefon"], + PIB: podesavanja["pib"], + }) +} + +// PodaciPredracuna su podaci za predračun — procenu cene za tražene radove i delove, +// koju klijent odobrava pre popravke. Gradi se iz stavki radova i delova na nalogu. +type PodaciPredracuna struct { + Nalog model.ServisniNalog + Radovi []model.ServisniRad + UkupnoRad float64 + ServisniDelovi []model.ServisniDeoSaArtiklom + UkupnoDelovi float64 + UkupnoSve float64 + DatumIzdavanja time.Time + VaziDo time.Time + Klauzula string + QRKod string + Klijent *model.Klijent + KlijentNaziv string + TehnicarNaziv string + NazivFirme string + Podnazlov string + Adresa string + Telefon string + PIB string +} + +// StampaPredracuna renderuje predračun (procenu cene) na osnovu traženih radova i delova +func (h *Handler) StampaPredracuna(w http.ResponseWriter, r *http.Request) { + id, err := parseID(chi.URLParam(r, "id")) + if err != nil { + http.Error(w, "Neispravan ID naloga", http.StatusBadRequest) + return + } + + nalog, err := h.ServisRepo.DohvatiID(r.Context(), id) + if err != nil { + http.Error(w, "Nalog nije pronađen", http.StatusNotFound) + return + } + + radovi, err := h.ServisniRadoviRepo.DohvatiZaNalog(r.Context(), id) + if err != nil { + http.Error(w, "Greška pri učitavanju radova", http.StatusInternalServerError) + return + } + + delovi, err := h.deloviSaPotrazivanima(r.Context(), id) + if err != nil { + http.Error(w, "Greška pri učitavanju delova", http.StatusInternalServerError) + return + } + + podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB) + if err != nil { + http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError) + return + } + + var klijent *model.Klijent + klijentNaziv := "" + if nalog.KlijentID != nil { + k, err := h.KlijentiRepo.DohvatiID(r.Context(), *nalog.KlijentID) + if err == nil { + klijent = k + klijentNaziv = k.PunoIme() + } + } + + tehnicarNaziv := "" + if nalog.TehnicarID != nil { + tehnicar, err := h.KorisniciRepo.DohvatiPoID(r.Context(), *nalog.TehnicarID) + if err == nil { + tehnicarNaziv = tehnicar.KorisnickoIme + } + } + + var ukupnoRad float64 + for _, rd := range radovi { + ukupnoRad += rd.Ukupno() + } + var ukupnoDelovi float64 + for _, d := range delovi { + ukupnoDelovi += d.Ukupno() + } + + // rok važenja iz podešavanja (default 7 dana) + rok := 7 + if v, err := strconv.Atoi(podesavanja["predracun_rok_dana"]); err == nil && v > 0 { + rok = v + } + datumIzdavanja := time.Now() + vaziDo := datumIzdavanja.AddDate(0, 0, rok) + + nalogURL := qrNalogURL(r, nalog.JavniToken) + var qrKod string + if png, err := qrcode.Encode(nalogURL, qrcode.Medium, 160); err == nil { + qrKod = base64.StdEncoding.EncodeToString(png) + } + + h.renderujStandalone(w, "servis_predracun", PodaciPredracuna{ + Nalog: *nalog, + Radovi: radovi, + UkupnoRad: ukupnoRad, + ServisniDelovi: delovi, + UkupnoDelovi: ukupnoDelovi, + UkupnoSve: ukupnoRad + ukupnoDelovi, + DatumIzdavanja: datumIzdavanja, + VaziDo: vaziDo, + Klauzula: vrednostIliDefault(podesavanja, "servis_klauzula_predracuna", podrazumevanaKlauzulaPredracuna), + QRKod: qrKod, + Klijent: klijent, + KlijentNaziv: klijentNaziv, + TehnicarNaziv: tehnicarNaziv, + NazivFirme: podesavanja["naziv_firme"], + Podnazlov: podesavanja["podnazlov"], + Adresa: podesavanja["adresa"], + Telefon: podesavanja["telefon"], + PIB: podesavanja["pib"], + }) +} + // PodaciNalepnice su podaci za malu nalepnicu (70×40 mm) koja se lepi na uređaj type PodaciNalepnice struct { Nalog model.ServisniNalog diff --git a/internal/model/servis.go b/internal/model/servis.go index b84dee0..084e817 100644 --- a/internal/model/servis.go +++ b/internal/model/servis.go @@ -7,16 +7,18 @@ import ( // Statusi servisnog naloga const ( - StatusPrimljeno = "Primljeno" - StatusCekaDelove = "Čeka delove" - StatusUPopravci = "U popravci" - StatusZavrseno = "Završeno" - StatusPreuzeto = "Preuzeto" + StatusPrimljeno = "Primljeno" + StatusUDijagnostici = "U dijagnostici" + StatusCekaDelove = "Čeka delove" + StatusUPopravci = "U popravci" + StatusZavrseno = "Završeno" + StatusPreuzeto = "Preuzeto" ) // SviStatusi je uređena lista statusa za prikaz u dropdownu var SviStatusi = []string{ StatusPrimljeno, + StatusUDijagnostici, StatusCekaDelove, StatusUPopravci, StatusZavrseno, diff --git a/web/templates/stranice/podesavanja_servis.html b/web/templates/stranice/podesavanja_servis.html index d67e334..e5c0129 100644 --- a/web/templates/stranice/podesavanja_servis.html +++ b/web/templates/stranice/podesavanja_servis.html @@ -9,11 +9,11 @@