Servis: tok dijagnostike/popravke, kartice Utvrđeni kvar i Urađeno, cena dijagnostike

- Cena dijagnostike u Podešavanja → Servis; na nalogu se predlaže iz podešavanja
- Odbijanje popravke: status Završeno + naplata samo dijagnostike (popravka_odbijena),
sklanjaju se Radovi/Delovi, Ukupno = dijagnostika
- Kartica Nalaz dijagnostike preimenovana u Utvrđeni kvar; editabilna samo u dijagnostici, kasnije readonly
- Nova kartica Urađeno (polje uradjeno) — upis dok je U popravci/Čeka delove, posle readonly
- Dugmad statusa: Završi u U popravci; promena statusa osvežava ceo sadržaj
- Predračun dostupan od Primljeno; radni nalog u U dijagnostici/Čeka delove
- Dropdown radova izostavlja već dodate usluge; kartica radova se osvežava pri izmeni
- Javni status: badge U dijagnostici, prikaz utvrđenog kvara, wordwrap naziva u tabeli
- Štampa: prelom između stavki, ponavljanje zaglavlja, završni blok bez preloma
This commit is contained in:
2026-06-23 22:21:12 +02:00
parent a8d5b0e9b4
commit f745f043c5
17 changed files with 494 additions and 106 deletions
+174 -37
View File
@@ -50,23 +50,25 @@ type PodaciFormeNaloga struct {
// PodaciDetaljiNaloga su podaci za pregled jednog servisnog naloga
type PodaciDetaljiNaloga struct {
model.PodaciStranice
Nalog model.ServisniNalog
KlijentNaziv string
TehnicarNaziv string
Tehnicari []model.Korisnik
GarancijaDefault string // podrazumevana garancija (prijem + meseci), format 2006-01-02
BezGarancije bool // u bazi nalog nema garanciju (GarancijaDo == NULL)
ServisniDelovi []model.ServisniDeoSaArtiklom
ServisniRadovi []model.ServisniRad
Artikli []model.ArtikalSaKategorijom
Usluge []model.Usluga
Sacuvano bool
UkupnoDelovi float64
UkupnoRadovi float64
UkupnoSve float64
PreostaloSve float64
ZakljucanStatus bool // onemogući promenu statusa dok ima potraživanih delova
SviStatusi []string
Nalog model.ServisniNalog
KlijentNaziv string
TehnicarNaziv string
Tehnicari []model.Korisnik
GarancijaDefault string // podrazumevana garancija (prijem + meseci), format 2006-01-02
BezGarancije bool // u bazi nalog nema garanciju (GarancijaDo == NULL)
ServisniDelovi []model.ServisniDeoSaArtiklom
ServisniRadovi []model.ServisniRad
Artikli []model.ArtikalSaKategorijom
Usluge []model.Usluga
Sacuvano bool
UkupnoDelovi float64
UkupnoRadovi float64
UkupnoSve float64
PreostaloSve float64
ZakljucanStatus bool // onemogući promenu statusa dok ima potraživanih delova
CenaDijagnostikePredlog string // podrazumevana cena dijagnostike iz podešavanja (za prefill input-a)
KorisceneUsluge map[int64]bool // ID-evi usluga već dodatih na nalog — izostavljaju se iz dropdown-a
SviStatusi []string
}
// Servis renderuje listu servisnih naloga sa opcionom pretragom i filterom statusa
@@ -595,11 +597,23 @@ func (h *Handler) DetaljiNaloga(w http.ResponseWriter, r *http.Request) {
for _, d := range delovi {
ukupnoDelovi += d.Ukupno()
}
// skup već dodatih usluga — izostavljaju se iz dropdown-a da se ista usluga ne doda dvaput
korisceneUsluge := make(map[int64]bool, len(radovi))
for _, rad := range radovi {
ukupnoRadovi += rad.Ukupno()
if rad.UslugaID != 0 {
korisceneUsluge[rad.UslugaID] = true
}
}
// ukupno za klijenta zavisi od ishoda dijagnostike:
// klijent odbio popravku → naplaćuje se samo cena dijagnostike
// inače (popravka u toku/gotova) → radovi + delovi, dijagnostika se ne gleda
var ukupnoSve float64
if nalog.PopravkaOdbijena {
ukupnoSve = nalog.CenaDijagnostike
} else {
ukupnoSve = ukupnoRadovi + ukupnoDelovi
}
// cena rada = zbir radova (usluga); ukupno za klijenta = radovi + delovi
ukupnoSve := ukupnoRadovi + ukupnoDelovi
avans := 0.0
if nalog.Avans != nil {
avans = *nalog.Avans
@@ -621,24 +635,26 @@ func (h *Handler) DetaljiNaloga(w http.ResponseWriter, r *http.Request) {
ps.Stranica = "servis"
ps.NaslovStranice = "Detalji naloga"
podaci := PodaciDetaljiNaloga{
PodaciStranice: ps,
Nalog: *nalog,
KlijentNaziv: klijentNaziv,
TehnicarNaziv: tehnicarNaziv,
Tehnicari: tehnicari,
GarancijaDefault: garancijaDefault,
BezGarancije: bezGarancije,
ServisniDelovi: delovi,
ServisniRadovi: radovi,
Artikli: artikli,
Usluge: usluge,
Sacuvano: r.URL.Query().Get("sacuvano") == "1",
UkupnoDelovi: ukupnoDelovi,
UkupnoRadovi: ukupnoRadovi,
UkupnoSve: ukupnoSve,
PreostaloSve: preostaloSve,
ZakljucanStatus: zakljucanStatus,
SviStatusi: model.SviStatusi,
PodaciStranice: ps,
Nalog: *nalog,
KlijentNaziv: klijentNaziv,
TehnicarNaziv: tehnicarNaziv,
Tehnicari: tehnicari,
GarancijaDefault: garancijaDefault,
BezGarancije: bezGarancije,
ServisniDelovi: delovi,
ServisniRadovi: radovi,
Artikli: artikli,
Usluge: usluge,
Sacuvano: r.URL.Query().Get("sacuvano") == "1",
UkupnoDelovi: ukupnoDelovi,
UkupnoRadovi: ukupnoRadovi,
UkupnoSve: ukupnoSve,
PreostaloSve: preostaloSve,
ZakljucanStatus: zakljucanStatus,
CenaDijagnostikePredlog: podesavanja["servis_cena_dijagnostike"],
KorisceneUsluge: korisceneUsluge,
SviStatusi: model.SviStatusi,
}
h.renderujTemplate(w, "servis_detalji", podaci)
@@ -1806,6 +1822,127 @@ func (h *Handler) SacuvajNapomenuKlijentu(w http.ResponseWriter, r *http.Request
http.Redirect(w, r, "/servis/"+strconv.FormatInt(id, 10), http.StatusSeeOther)
}
// SacuvajNalazDijagnostike obrađuje POST /servis/{id}/nalaz-dijagnostike i čuva
// dijagnozu koju je serviser utvrdio (rezultat dijagnostike, hrani predračun)
func (h *Handler) SacuvajNalazDijagnostike(w http.ResponseWriter, r *http.Request) {
if _, ok := h.zahtevajDozvolu(w, r, "servis.izmeni"); !ok {
return
}
id, err := parseID(chi.URLParam(r, "id"))
if err != nil {
http.Error(w, "Neispravan ID naloga", http.StatusBadRequest)
return
}
if err := r.ParseForm(); err != nil {
http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest)
return
}
tekst := strings.TrimSpace(r.FormValue("nalaz_dijagnostike"))
if err := h.ServisRepo.AzurirajNalazDijagnostike(r.Context(), id, tekst); err != nil {
slog.Error("greška pri ažuriranju nalaza dijagnostike", "id", id, "error", err)
http.Error(w, "Greška pri ažuriranju nalaza", http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/servis/"+strconv.FormatInt(id, 10), http.StatusSeeOther)
}
// SacuvajUradjeno obrađuje POST /servis/{id}/uradjeno i čuva tekst „šta je urađeno"
// (serviser upisuje izvršene radove tokom popravke)
func (h *Handler) SacuvajUradjeno(w http.ResponseWriter, r *http.Request) {
if _, ok := h.zahtevajDozvolu(w, r, "servis.izmeni"); !ok {
return
}
id, err := parseID(chi.URLParam(r, "id"))
if err != nil {
http.Error(w, "Neispravan ID naloga", http.StatusBadRequest)
return
}
if err := r.ParseForm(); err != nil {
http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest)
return
}
tekst := strings.TrimSpace(r.FormValue("uradjeno"))
if err := h.ServisRepo.AzurirajUradjeno(r.Context(), id, tekst); err != nil {
slog.Error("greška pri ažuriranju urađenog", "id", id, "error", err)
http.Error(w, "Greška pri čuvanju", http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/servis/"+strconv.FormatInt(id, 10), http.StatusSeeOther)
}
// AzurirajCenaDijagnostike čuva cenu dijagnostike (taksa kad klijent ne prihvati popravku);
// prazno polje ili neispravna vrednost se tretira kao 0 (ne naplaćuje se)
func (h *Handler) AzurirajCenaDijagnostike(w http.ResponseWriter, r *http.Request) {
if _, ok := h.zahtevajDozvolu(w, r, "servis.izmeni"); !ok {
return
}
id, err := parseID(chi.URLParam(r, "id"))
if err != nil {
http.Error(w, "Neispravan ID naloga", http.StatusBadRequest)
return
}
if err := r.ParseForm(); err != nil {
http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest)
return
}
cena := 0.0
if s := strings.TrimSpace(r.FormValue("cena_dijagnostike")); s != "" {
v, e := strconv.ParseFloat(s, 64)
if e != nil || v < 0 {
http.Error(w, "Cena dijagnostike mora biti broj veći ili jednak nuli.", http.StatusBadRequest)
return
}
cena = v
}
if err := h.ServisRepo.AzurirajCenaDijagnostike(r.Context(), id, cena); err != nil {
slog.Error("greška pri ažuriranju cene dijagnostike", "id", id, "error", err)
http.Error(w, "Greška pri ažuriranju cene dijagnostike", http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/servis/"+strconv.FormatInt(id, 10), http.StatusSeeOther)
}
// OdbijPopravku obrađuje slučaj kad klijent posle dijagnostike ne prihvati popravku:
// nalog prelazi u „Završeno" i naplaćuje se samo cena dijagnostike. Cena se uzima iz
// forme, a ako je prazna koristi se podrazumevana iz podešavanja (Podešavanja → Servis).
func (h *Handler) OdbijPopravku(w http.ResponseWriter, r *http.Request) {
if _, ok := h.zahtevajDozvolu(w, r, "servis.izmeni"); !ok {
return
}
id, err := parseID(chi.URLParam(r, "id"))
if err != nil {
http.Error(w, "Neispravan ID naloga", http.StatusBadRequest)
return
}
if err := r.ParseForm(); err != nil {
http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest)
return
}
cenaStr := strings.TrimSpace(r.FormValue("cena_dijagnostike"))
if cenaStr == "" {
// nema unete cene na nalogu → uzmi podrazumevanu iz podešavanja
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
cenaStr = strings.TrimSpace(podesavanja["servis_cena_dijagnostike"])
}
cena := 0.0
if cenaStr != "" {
v, e := strconv.ParseFloat(strings.ReplaceAll(cenaStr, ",", "."), 64)
if e != nil || v < 0 {
http.Error(w, "Cena dijagnostike mora biti broj veći ili jednak nuli.", http.StatusBadRequest)
return
}
cena = v
}
if err := h.ServisRepo.OdbijPopravku(r.Context(), id, cena); err != nil {
slog.Error("greška pri odbijanju popravke", "id", id, "error", err)
http.Error(w, "Greška pri promeni statusa", http.StatusInternalServerError)
return
}
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