Servis predlog delovi: lager integracija, brisanje predloženih delova, odluka klijenta (migracija 079)

This commit is contained in:
2026-06-24 19:39:30 +02:00
parent f61b3bd5ca
commit c8fa49967e
16 changed files with 599 additions and 180 deletions
+148 -61
View File
@@ -535,8 +535,11 @@ func (h *Handler) DetaljiNaloga(w http.ResponseWriter, r *http.Request) {
continue
}
for _, nalogID := range otkljucani {
if err := h.ServisRepo.AzurirajStatus(r.Context(), nalogID, model.StatusPrimljeno); err != nil {
slog.Error("self-heal reset statusa naloga nije uspeo", "nalog_id", nalogID, "error", err)
// reset samo ako je nalog čekao delove — ne sme da menja U dijagnostici, U popravci itd.
if tekuci, e := h.ServisRepo.DohvatiID(r.Context(), nalogID); e == nil && tekuci != nil && tekuci.Status == model.StatusCekaDelove {
if err := h.ServisRepo.AzurirajStatus(r.Context(), nalogID, model.StatusPrimljeno); err != nil {
slog.Error("self-heal reset statusa naloga nije uspeo", "nalog_id", nalogID, "error", err)
}
}
}
}
@@ -747,11 +750,13 @@ func (h *Handler) DodajDeloNalogu(w http.ResponseWriter, r *http.Request) {
imeArtikla = art.Naziv
}
// forma „Predloženi delovi" šalje predlozeno=1; forma „Ugrađeni delovi" ne šalje
predlozeno := r.FormValue("predlozeno") == "1"
// predlog ako status nije „Primljeno" (forma više ne određuje — server odlučuje)
nalog, _ := h.ServisRepo.DohvatiID(r.Context(), nalogID)
predlozeno := nalog != nil && nalog.Status != model.StatusPrimljeno
slog.Info("DODAJ_DEO_IN", "status", nalog.Status, "predlozeno", predlozeno, "kol", kolicina)
// atomično: ugradi ono što imamo (skida sa lagera, ne ide u minus), višak u potraživane
ugradjeno, nedostaje, err := h.ServisniDeloviRepo.UgradiIliPotrazuj(r.Context(), nalogID, artikalID, kolicina, cena, &k.ID, predlozeno)
slog.Info("DODAJ_DEO_OUT", "ugradjeno", ugradjeno, "nedostaje", nedostaje, "err", err)
if err != nil {
slog.Error("greška pri dodavanju dela", "error", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri dodavanju dela.")
@@ -759,8 +764,9 @@ func (h *Handler) DodajDeloNalogu(w http.ResponseWriter, r *http.Request) {
return
}
// ako nešto nedostaje, prebaci nalog u „Čeka delove" i obavesti korisnika
if nedostaje > 0 {
// ako nešto nedostaje kod ugrađenog dela, prebaci nalog u „Čeka delove".
// Za predložene delove ne menjamo status — to su samo predlozi, ne blokiraju rad.
if nedostaje > 0 && !predlozeno {
if e := h.ServisRepo.AzurirajStatus(r.Context(), nalogID, model.StatusCekaDelove); e != nil {
slog.Error("greška pri prebacivanju naloga u Čeka delove", "error", e)
}
@@ -775,6 +781,16 @@ func (h *Handler) DodajDeloNalogu(w http.ResponseWriter, r *http.Request) {
}
}
if nedostaje > 0 && predlozeno {
middleware.SetFlash(w, r, h.DB, "uspeh",
"Predlog sačuvan — "+strconv.Itoa(kolicina)+" kom. „"+imeArtikla+
"\" čeka odobrenje klijenta.")
// novi predlog poništava prethodnu odluku klijenta
if err := h.ServisRepo.ObrisiOdlukuKlijenta(r.Context(), nalogID); err != nil {
slog.Error("greška pri brisanju odluke klijenta", "error", err)
}
}
http.Redirect(w, r, "/servis/"+strconv.FormatInt(nalogID, 10)+"?sacuvano=1", http.StatusSeeOther)
}
@@ -795,26 +811,43 @@ func (h *Handler) deloviSaPotrazivanima(ctx context.Context, nalogID int64) ([]m
artikalID int64
predlozeno bool
}
potrMap := make(map[kljucDela]int)
type potrVrednost struct {
kolicina int
cena float64
}
potrMap := make(map[kljucDela]potrVrednost)
for _, p := range potrazivani {
potrMap[kljucDela{p.ArtikalID, p.Predlozeno}] += p.Kolicina
k := kljucDela{p.ArtikalID, p.Predlozeno}
v := potrMap[k]
v.kolicina += p.Kolicina
if v.cena == 0 && p.CenaKomada > 0 {
v.cena = p.CenaKomada
}
potrMap[k] = v
}
for i := range delovi {
k := kljucDela{delovi[i].ArtikalID, delovi[i].Predlozeno}
if kol, ok := potrMap[k]; ok {
delovi[i].Potrazivano = kol
if v, ok := potrMap[k]; ok {
delovi[i].Potrazivano = v.kolicina
delete(potrMap, k)
}
}
for k, kol := range potrMap {
for k, v := range potrMap {
naziv := ""
sifra := ""
if art, e := h.Artikli.DohvatiID(ctx, k.artikalID); e == nil && art != nil {
naziv = art.Naziv
sifra = art.Sifra
}
delovi = append(delovi, model.ServisniDeoSaArtiklom{
ArtikalNaziv: naziv,
ServisniDeo: model.ServisniDeo{ArtikalID: k.artikalID, Predlozeno: k.predlozeno},
Potrazivano: kol,
ArtikalSifra: sifra,
ServisniDeo: model.ServisniDeo{
ArtikalID: k.artikalID,
Kolicina: v.kolicina,
CenaKomada: v.cena,
Predlozeno: k.predlozeno,
},
})
}
return delovi, nil
@@ -853,6 +886,28 @@ func (h *Handler) ObrisiDeloNaloga(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/servis/"+strconv.FormatInt(nalogID, 10), http.StatusSeeOther)
}
// ObrisiPredlozeniDeoNaloga briše predlozene redove (predlozeno=1) za dati artikal na nalogu
func (h *Handler) ObrisiPredlozeniDeoNaloga(w http.ResponseWriter, r *http.Request) {
if _, ok := h.zahtevajDozvolu(w, r, "servis.izmeni"); !ok {
return
}
nalogID, err := parseID(chi.URLParam(r, "id"))
if err != nil {
http.Error(w, "Neispravan ID naloga", http.StatusBadRequest)
return
}
artikalID, err := parseID(chi.URLParam(r, "artikal_id"))
if err != nil {
http.Error(w, "Neispravan ID artikla", http.StatusBadRequest)
return
}
if err := h.ServisniPotrazivaniDeloviRepo.ObrisiPredlozeneZaArtikal(r.Context(), nalogID, artikalID); err != nil {
slog.Error("greška pri brisanju predloženog dela", "error", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri uklanjanju predloženog dela.")
}
http.Redirect(w, r, "/servis/"+strconv.FormatInt(nalogID, 10), http.StatusSeeOther)
}
// DodajRadNalogu dodaje stavku rada (uslugu) na nalog; naziv i cena se snapshot-uju
func (h *Handler) DodajRadNalogu(w http.ResponseWriter, r *http.Request) {
if _, ok := h.zahtevajDozvolu(w, r, "servis.izmeni"); !ok {
@@ -898,8 +953,9 @@ func (h *Handler) DodajRadNalogu(w http.ResponseWriter, r *http.Request) {
return
}
// forma „Predložene usluge" šalje predlozeno=1; forma „Radovi" ne šalje
predlozeno := r.FormValue("predlozeno") == "1"
// predlog ako status nije „Primljeno" (server odlučuje, ne forma)
nalog, _ := h.ServisRepo.DohvatiID(r.Context(), nalogID)
predlozeno := nalog != nil && nalog.Status != model.StatusPrimljeno
if _, err := h.ServisniRadoviRepo.Dodaj(r.Context(), nalogID, uslugaID, usluga.Naziv, kolicina, cena, predlozeno); err != nil {
slog.Error("greška pri dodavanju rada", "error", err)
@@ -908,6 +964,13 @@ func (h *Handler) DodajRadNalogu(w http.ResponseWriter, r *http.Request) {
return
}
// novi predlog poništava prethodnu odluku klijenta
if predlozeno {
if err := h.ServisRepo.ObrisiOdlukuKlijenta(r.Context(), nalogID); err != nil {
slog.Error("greška pri brisanju odluke klijenta", "error", err)
}
}
http.Redirect(w, r, "/servis/"+strconv.FormatInt(nalogID, 10)+"?sacuvano=1", http.StatusSeeOther)
}
@@ -1052,10 +1115,26 @@ func (h *Handler) mozdaKreirajKlijenta(ctx context.Context, r *http.Request, nal
tipIdent = "jmbg" // prazan unos — podrazumevani tip radi konzistentnosti baze
}
prezime := strings.TrimSpace(r.FormValue("prezime"))
telefon := strings.TrimSpace(r.FormValue("telefon"))
mestoLok := strings.TrimSpace(r.FormValue("mesto"))
// prvo proveri da li klijent već postoji u bazi
postojeci, err := h.KlijentiRepo.Pronadji(ctx, tip, ime, prezime, nazivFirme, jmbg, telefon, email, mestoLok)
if err != nil {
slog.Error("greška pri pretrazi klijenta", "error", err)
return "Došlo je do greške pri proveri klijenta. Pokušajte ponovo."
}
if postojeci != nil {
nalog.KlijentID = &postojeci.ID
return ""
}
// klijent ne postoji — kreiraj novog
klijent := model.Klijent{
Tip: tip,
Ime: ime,
Prezime: strings.TrimSpace(r.FormValue("prezime")),
Prezime: prezime,
JMBG: jmbg,
TipIdentifikacije: tipIdent,
NazivFirme: nazivFirme,
@@ -2008,24 +2087,24 @@ func (h *Handler) OdbijPopravku(w http.ResponseWriter, r *http.Request) {
// PodaciJavnogStatusa su podaci za javnu status stranicu servisnog naloga
type PodaciJavnogStatusa struct {
Nalog model.ServisniNalog
NazivFirme string
LogoPutanja string
Podnazlov string
Adresa string
Telefon string
PIB string
MaticniBroj string
Radovi []model.ServisniRad // ugrađeni/odobreni
ServisniDelovi []model.ServisniDeoSaArtiklom // ugrađeni/odobreni
UkupnoSve float64 // zbir ugrađenih (radovi + delovi)
PredlozeniRadovi []model.ServisniRad
PredlozeniDelovi []model.ServisniDeoSaArtiklom
UkupnoPredlog float64 // zbir predloženih (radovi + delovi)
ImaPredlog bool
UkupnoSaPredlogom float64 // ukupno ugrađeno + predloženo
Odgovoreno bool // klijent već odgovorio (nema predloga, nije „Primljeno")
SviStatusi []string
Nalog model.ServisniNalog
NazivFirme string
LogoPutanja string
Podnazlov string
Adresa string
Telefon string
PIB string
MaticniBroj string
Radovi []model.ServisniRad // ugrađeni/odobreni
ServisniDelovi []model.ServisniDeoSaArtiklom // ugrađeni/odobreni
UkupnoSve float64 // zbir ugrađenih (radovi + delovi)
PredlozeniRadovi []model.ServisniRad
PredlozeniDelovi []model.ServisniDeoSaArtiklom
UkupnoPredlog float64 // zbir predloženih (radovi + delovi)
ImaPredlog bool
UkupnoSaPredlogom float64 // ukupno ugrađeno + predloženo
Odgovoreno bool // klijent već odgovorio (nema predloga, nije „Primljeno")
SviStatusi []string
}
// ServisJavniStatus prikazuje javnu status stranicu — dostupna bez prijave putem QR koda
@@ -2077,24 +2156,24 @@ func (h *Handler) ServisJavniStatus(w http.ResponseWriter, r *http.Request) {
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
h.renderujStandalone(w, "servis_status_javni", PodaciJavnogStatusa{
Nalog: *nalog,
NazivFirme: podesavanja["naziv_firme"],
LogoPutanja: logoZaDokument(podesavanja),
Podnazlov: podesavanja["podnazlov"],
Adresa: podesavanja["adresa"],
Telefon: podesavanja["telefon"],
PIB: podesavanja["pib"],
MaticniBroj: podesavanja["maticni_broj"],
Radovi: ugrRadovi,
ServisniDelovi: ugrDelovi,
UkupnoSve: ukupnoSve,
PredlozeniRadovi: predRadovi,
PredlozeniDelovi: predDelovi,
UkupnoPredlog: ukupnoPredlog,
Nalog: *nalog,
NazivFirme: podesavanja["naziv_firme"],
LogoPutanja: logoZaDokument(podesavanja),
Podnazlov: podesavanja["podnazlov"],
Adresa: podesavanja["adresa"],
Telefon: podesavanja["telefon"],
PIB: podesavanja["pib"],
MaticniBroj: podesavanja["maticni_broj"],
Radovi: ugrRadovi,
ServisniDelovi: ugrDelovi,
UkupnoSve: ukupnoSve,
PredlozeniRadovi: predRadovi,
PredlozeniDelovi: predDelovi,
UkupnoPredlog: ukupnoPredlog,
ImaPredlog: len(predRadovi) > 0 || len(predDelovi) > 0,
UkupnoSaPredlogom: ukupnoSve + ukupnoPredlog,
Odgovoreno: !(len(predRadovi) > 0 || len(predDelovi) > 0) && nalog.Status != model.StatusPrimljeno,
SviStatusi: model.SviStatusi,
Odgovoreno: nalog.OdlukaKlijenta != "" || (!(len(predRadovi) > 0 || len(predDelovi) > 0) && nalog.Status != model.StatusPrimljeno),
SviStatusi: model.SviStatusi,
})
}
@@ -2136,6 +2215,12 @@ func (h *Handler) ServisJavniPrihvati(w http.ResponseWriter, r *http.Request) {
return
}
// odluka je već doneta — ne dozvoljavamo ponovno glasanje
if nalog.OdlukaKlijenta != "" {
http.Redirect(w, r, "/status/"+token, http.StatusSeeOther)
return
}
if err := h.ServisniRadoviRepo.PrihvatiPredlozene(r.Context(), nalog.ID); err != nil {
slog.Error("greška pri prihvatanju predloženih radova", "error", err, "nalog_id", nalog.ID)
http.Error(w, "Greška pri prihvatanju predloga.", http.StatusInternalServerError)
@@ -2147,11 +2232,9 @@ func (h *Handler) ServisJavniPrihvati(w http.ResponseWriter, r *http.Request) {
return
}
// sačuvaj komentar klijenta
if komentar := strings.TrimSpace(r.FormValue("komentar")); komentar != "" {
if err := h.ServisRepo.AzurirajKomentarKlijenta(r.Context(), nalog.ID, komentar); err != nil {
slog.Error("greška pri čuvanju komentara klijenta", "error", err, "nalog_id", nalog.ID)
}
komentar := strings.TrimSpace(r.FormValue("komentar"))
if err := h.ServisRepo.SacuvajOdlukuKlijenta(r.Context(), nalog.ID, "prihvaceno", komentar); err != nil {
slog.Error("greška pri čuvanju odluke klijenta", "error", err, "nalog_id", nalog.ID)
}
http.Redirect(w, r, "/status/"+token, http.StatusSeeOther)
@@ -2199,6 +2282,12 @@ func (h *Handler) ServisJavniOdbij(w http.ResponseWriter, r *http.Request) {
return
}
// odluka je već doneta — ne dozvoljavamo ponovno glasanje
if nalog.OdlukaKlijenta != "" {
http.Redirect(w, r, "/status/"+token, http.StatusSeeOther)
return
}
// brišemo predložene stavke — klijent ih je odbio
if err := h.ServisniRadoviRepo.ObrisiPredlozene(r.Context(), nalog.ID); err != nil {
slog.Error("greška pri brisanju predloženih radova", "error", err, "nalog_id", nalog.ID)
@@ -2207,11 +2296,9 @@ func (h *Handler) ServisJavniOdbij(w http.ResponseWriter, r *http.Request) {
slog.Error("greška pri brisanju predloženih delova", "error", err, "nalog_id", nalog.ID)
}
// sačuvaj komentar klijenta
if komentar := strings.TrimSpace(r.FormValue("komentar")); komentar != "" {
if err := h.ServisRepo.AzurirajKomentarKlijenta(r.Context(), nalog.ID, komentar); err != nil {
slog.Error("greška pri čuvanju komentara klijenta", "error", err, "nalog_id", nalog.ID)
}
komentar := strings.TrimSpace(r.FormValue("komentar"))
if err := h.ServisRepo.SacuvajOdlukuKlijenta(r.Context(), nalog.ID, "odbijeno", komentar); err != nil {
slog.Error("greška pri čuvanju odluke klijenta", "error", err, "nalog_id", nalog.ID)
}
http.Redirect(w, r, "/status/"+token, http.StatusSeeOther)