Izmeni nalog sada ima samo: Uređaj, Tehničar, Avans, Napomena. Uklonjene kartice Klijent i Status i datumi (status se menja u detaljima, klijent je vezan pri prijemu), procena od/do, cena rada (računa se iz radova) i garancija. Nazad/Odustani vode na detalje naloga. Handler čuva postojeće vrednosti (status, cene, klijent, garancija, datum završetka) da ih prazna forma ne pregazi.
1412 lines
44 KiB
Go
1412 lines
44 KiB
Go
package handler
|
||
|
||
import (
|
||
"context"
|
||
"encoding/base64"
|
||
"log/slog"
|
||
"net/http"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
appdbPkg "ntech/internal/db"
|
||
"ntech/internal/db/sqlite"
|
||
"ntech/internal/middleware"
|
||
"ntech/internal/model"
|
||
|
||
"github.com/go-chi/chi/v5"
|
||
qrcode "github.com/skip2/go-qrcode"
|
||
)
|
||
|
||
// PodaciServisa su podaci za stranicu sa listom servisnih naloga
|
||
type PodaciServisa struct {
|
||
model.PodaciStranice
|
||
Nalozi []model.ServisniNalogSaKlijentom
|
||
Pretraga string
|
||
FilterStatus string
|
||
SviStatusi []string
|
||
Sacuvano bool
|
||
Obrisan bool
|
||
}
|
||
|
||
// PodaciFormeNaloga su podaci za formu novog/izmenjenog servisnog naloga
|
||
type PodaciFormeNaloga struct {
|
||
model.PodaciStranice
|
||
Nalog model.ServisniNalog
|
||
Klijenti []model.Klijent
|
||
Tehnicari []model.Korisnik
|
||
SviStatusi []string
|
||
Greska string
|
||
Izmena bool
|
||
}
|
||
|
||
// PodaciDetaljiNaloga su podaci za pregled jednog servisnog naloga
|
||
type PodaciDetaljiNaloga struct {
|
||
model.PodaciStranice
|
||
Nalog model.ServisniNalog
|
||
KlijentNaziv string
|
||
TehnicarNaziv string
|
||
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
|
||
}
|
||
|
||
// Servis renderuje listu servisnih naloga sa opcionom pretragom i filterom statusa
|
||
func (h *Handler) Servis(w http.ResponseWriter, r *http.Request) {
|
||
podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||
if err != nil {
|
||
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
|
||
return
|
||
}
|
||
|
||
pretraga := r.URL.Query().Get("pretraga")
|
||
filterStatus := r.URL.Query().Get("status")
|
||
|
||
nalozi, err := h.ServisRepo.Lista(r.Context(), pretraga, filterStatus)
|
||
if err != nil {
|
||
http.Error(w, "Greška pri učitavanju naloga", http.StatusInternalServerError)
|
||
return
|
||
}
|
||
|
||
ps := h.popuniPodaciStranice(r, podesavanja)
|
||
ps.Stranica = "servis"
|
||
ps.NaslovStranice = "Servis"
|
||
podaci := PodaciServisa{
|
||
PodaciStranice: ps,
|
||
Nalozi: nalozi,
|
||
Pretraga: pretraga,
|
||
FilterStatus: filterStatus,
|
||
SviStatusi: model.SviStatusi,
|
||
Sacuvano: r.URL.Query().Get("sacuvano") == "1",
|
||
Obrisan: r.URL.Query().Get("obrisan") == "1",
|
||
}
|
||
|
||
h.renderujTemplate(w, "servis", podaci)
|
||
}
|
||
|
||
// ArhivaServisa prikazuje preuzete naloge — oni napuštaju kanban tablu i ovde se
|
||
// pretražuju, da tabla ostane pregledna i pokazuje samo aktivan rad
|
||
func (h *Handler) ArhivaServisa(w http.ResponseWriter, r *http.Request) {
|
||
podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||
if err != nil {
|
||
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
|
||
return
|
||
}
|
||
|
||
pretraga := r.URL.Query().Get("pretraga")
|
||
nalozi, err := h.ServisRepo.Lista(r.Context(), pretraga, model.StatusPreuzeto)
|
||
if err != nil {
|
||
http.Error(w, "Greška pri učitavanju naloga", http.StatusInternalServerError)
|
||
return
|
||
}
|
||
|
||
ps := h.popuniPodaciStranice(r, podesavanja)
|
||
ps.Stranica = "servis"
|
||
ps.NaslovStranice = "Arhiva servisa"
|
||
podaci := PodaciServisa{
|
||
PodaciStranice: ps,
|
||
Nalozi: nalozi,
|
||
Pretraga: pretraga,
|
||
SviStatusi: model.SviStatusi,
|
||
}
|
||
|
||
h.renderujTemplate(w, "servis_arhiva", podaci)
|
||
}
|
||
|
||
// NoviNalog generiše broj naloga i prikazuje praznu formu za unos
|
||
func (h *Handler) NoviNalog(w http.ResponseWriter, r *http.Request) {
|
||
podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||
if err != nil {
|
||
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
|
||
return
|
||
}
|
||
|
||
brojNaloga, err := h.ServisRepo.SledeciBroj(r.Context())
|
||
if err != nil {
|
||
http.Error(w, "Greška pri generisanju broja naloga", http.StatusInternalServerError)
|
||
return
|
||
}
|
||
|
||
klijenti, err := h.KlijentiRepo.Lista(r.Context(), "")
|
||
if err != nil {
|
||
http.Error(w, "Greška pri učitavanju klijenata", http.StatusInternalServerError)
|
||
return
|
||
}
|
||
|
||
tehnicari, err := h.KorisniciRepo.Lista(r.Context())
|
||
if err != nil {
|
||
http.Error(w, "Greška pri učitavanju tehničara", http.StatusInternalServerError)
|
||
return
|
||
}
|
||
|
||
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: noviNalog,
|
||
Klijenti: klijenti,
|
||
Tehnicari: tehnicari,
|
||
SviStatusi: model.SviStatusi,
|
||
Izmena: false,
|
||
})
|
||
}
|
||
|
||
// SacuvajNalog prima POST formu i upisuje novi servisni nalog u bazu
|
||
func (h *Handler) SacuvajNalog(w http.ResponseWriter, r *http.Request) {
|
||
if _, ok := h.zahtevajDozvolu(w, r, "servis.dodaj"); !ok {
|
||
return
|
||
}
|
||
if err := r.ParseForm(); err != nil {
|
||
http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest)
|
||
return
|
||
}
|
||
|
||
nalog, greska := parseFormuNaloga(r)
|
||
if greska != "" {
|
||
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||
klijenti, _ := h.KlijentiRepo.Lista(r.Context(), "")
|
||
tehnicari, _ := h.KorisniciRepo.Lista(r.Context())
|
||
ps := h.popuniPodaciStranice(r, podesavanja)
|
||
ps.Stranica = "servis"
|
||
ps.NaslovStranice = "Novi nalog"
|
||
h.renderujFormuNaloga(w, PodaciFormeNaloga{
|
||
PodaciStranice: ps,
|
||
Nalog: nalog,
|
||
Klijenti: klijenti,
|
||
Tehnicari: tehnicari,
|
||
SviStatusi: model.SviStatusi,
|
||
Greska: greska,
|
||
Izmena: false,
|
||
})
|
||
return
|
||
}
|
||
|
||
// ako nije izabran postojeći klijent, eventualno kreiraj novog iz polja forme
|
||
if greska := h.mozdaKreirajKlijenta(r.Context(), r, &nalog); greska != "" {
|
||
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||
klijenti, _ := h.KlijentiRepo.Lista(r.Context(), "")
|
||
tehnicari, _ := h.KorisniciRepo.Lista(r.Context())
|
||
ps := h.popuniPodaciStranice(r, podesavanja)
|
||
ps.Stranica = "servis"
|
||
ps.NaslovStranice = "Novi nalog"
|
||
h.renderujFormuNaloga(w, PodaciFormeNaloga{
|
||
PodaciStranice: ps,
|
||
Nalog: nalog,
|
||
Klijenti: klijenti,
|
||
Tehnicari: tehnicari,
|
||
SviStatusi: model.SviStatusi,
|
||
Greska: greska,
|
||
Izmena: false,
|
||
})
|
||
return
|
||
}
|
||
|
||
id, err := h.ServisRepo.Kreiraj(r.Context(), &nalog)
|
||
if err != nil {
|
||
slog.Error("greška pri čuvanju naloga", "error", err)
|
||
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||
klijenti, _ := h.KlijentiRepo.Lista(r.Context(), "")
|
||
tehnicari, _ := h.KorisniciRepo.Lista(r.Context())
|
||
ps := h.popuniPodaciStranice(r, podesavanja)
|
||
ps.Stranica = "servis"
|
||
ps.NaslovStranice = "Novi nalog"
|
||
h.renderujFormuNaloga(w, PodaciFormeNaloga{
|
||
PodaciStranice: ps,
|
||
Nalog: nalog,
|
||
Klijenti: klijenti,
|
||
Tehnicari: tehnicari,
|
||
SviStatusi: model.SviStatusi,
|
||
Greska: "Došlo je do greške pri čuvanju. Pokušajte ponovo.",
|
||
Izmena: false,
|
||
})
|
||
return
|
||
}
|
||
|
||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(id, 10)+"?sacuvano=1", http.StatusSeeOther)
|
||
}
|
||
|
||
// IzmeniNalog učitava servisni nalog po ID-u i prikazuje popunjenu formu
|
||
func (h *Handler) IzmeniNalog(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
|
||
}
|
||
|
||
klijenti, err := h.KlijentiRepo.Lista(r.Context(), "")
|
||
if err != nil {
|
||
http.Error(w, "Greška pri učitavanju klijenata", http.StatusInternalServerError)
|
||
return
|
||
}
|
||
|
||
tehnicari, err := h.KorisniciRepo.Lista(r.Context())
|
||
if err != nil {
|
||
http.Error(w, "Greška pri učitavanju tehničara", http.StatusInternalServerError)
|
||
return
|
||
}
|
||
|
||
if nalog.GarancijaDo == nil {
|
||
nalog.GarancijaDo = defaultGarancija(nalog.DatumPrijema, podesavanja)
|
||
}
|
||
ps := h.popuniPodaciStranice(r, podesavanja)
|
||
ps.Stranica = "servis"
|
||
ps.NaslovStranice = "Izmeni nalog"
|
||
h.renderujFormuNaloga(w, PodaciFormeNaloga{
|
||
PodaciStranice: ps,
|
||
Nalog: *nalog,
|
||
Klijenti: klijenti,
|
||
Tehnicari: tehnicari,
|
||
SviStatusi: model.SviStatusi,
|
||
Izmena: true,
|
||
})
|
||
}
|
||
|
||
// SacuvajIzmenaNaloga prima POST formu i ažurira postojeći servisni nalog
|
||
func (h *Handler) SacuvajIzmenaNaloga(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
|
||
}
|
||
|
||
nalog, greska := parseFormuNaloga(r)
|
||
if greska != "" {
|
||
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||
klijenti, _ := h.KlijentiRepo.Lista(r.Context(), "")
|
||
tehnicari, _ := h.KorisniciRepo.Lista(r.Context())
|
||
nalog.ID = id
|
||
ps := h.popuniPodaciStranice(r, podesavanja)
|
||
ps.Stranica = "servis"
|
||
ps.NaslovStranice = "Izmeni nalog"
|
||
h.renderujFormuNaloga(w, PodaciFormeNaloga{
|
||
PodaciStranice: ps,
|
||
Nalog: nalog,
|
||
Klijenti: klijenti,
|
||
Tehnicari: tehnicari,
|
||
SviStatusi: model.SviStatusi,
|
||
Greska: greska,
|
||
Izmena: true,
|
||
})
|
||
return
|
||
}
|
||
|
||
nalog.ID = id
|
||
|
||
// forma izmene više ne šalje status, datume, cene ni klijenta — očuvaj postojeće
|
||
// vrednosti (status se menja u detaljima, cena rada se računa iz radova, garancija
|
||
// i datum završetka se postavljaju kasnije). Inače bi prazna polja pregazila bazu.
|
||
if stari, e := h.ServisRepo.DohvatiID(r.Context(), id); e == nil && stari != nil {
|
||
nalog.Status = stari.Status
|
||
nalog.KlijentID = stari.KlijentID
|
||
nalog.CenaOd = stari.CenaOd
|
||
nalog.CenaDo = stari.CenaDo
|
||
nalog.CenaKonacna = stari.CenaKonacna
|
||
nalog.GarancijaDo = stari.GarancijaDo
|
||
nalog.DatumZavrsetka = stari.DatumZavrsetka
|
||
}
|
||
|
||
// ako nije izabran postojeći klijent, eventualno kreiraj novog iz polja forme
|
||
if greska := h.mozdaKreirajKlijenta(r.Context(), r, &nalog); greska != "" {
|
||
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||
klijenti, _ := h.KlijentiRepo.Lista(r.Context(), "")
|
||
tehnicari, _ := h.KorisniciRepo.Lista(r.Context())
|
||
ps := h.popuniPodaciStranice(r, podesavanja)
|
||
ps.Stranica = "servis"
|
||
ps.NaslovStranice = "Izmeni nalog"
|
||
h.renderujFormuNaloga(w, PodaciFormeNaloga{
|
||
PodaciStranice: ps,
|
||
Nalog: nalog,
|
||
Klijenti: klijenti,
|
||
Tehnicari: tehnicari,
|
||
SviStatusi: model.SviStatusi,
|
||
Greska: greska,
|
||
Izmena: true,
|
||
})
|
||
return
|
||
}
|
||
|
||
if err := h.ServisRepo.Izmeni(r.Context(), &nalog); err != nil {
|
||
slog.Error("greška pri čuvanju izmene naloga", "error", err)
|
||
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||
klijenti, _ := h.KlijentiRepo.Lista(r.Context(), "")
|
||
tehnicari, _ := h.KorisniciRepo.Lista(r.Context())
|
||
ps := h.popuniPodaciStranice(r, podesavanja)
|
||
ps.Stranica = "servis"
|
||
ps.NaslovStranice = "Izmeni nalog"
|
||
h.renderujFormuNaloga(w, PodaciFormeNaloga{
|
||
PodaciStranice: ps,
|
||
Nalog: nalog,
|
||
Klijenti: klijenti,
|
||
Tehnicari: tehnicari,
|
||
SviStatusi: model.SviStatusi,
|
||
Greska: "Došlo je do greške pri čuvanju. Pokušajte ponovo.",
|
||
Izmena: true,
|
||
})
|
||
return
|
||
}
|
||
|
||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(id, 10)+"?sacuvano=1", http.StatusSeeOther)
|
||
}
|
||
|
||
// ObrisiNalog prima POST zahtev i briše servisni nalog po ID-u
|
||
func (h *Handler) ObrisiNalog(w http.ResponseWriter, r *http.Request) {
|
||
if _, ok := h.zahtevajDozvolu(w, r, "servis.obrisi"); !ok {
|
||
return
|
||
}
|
||
id, err := parseID(chi.URLParam(r, "id"))
|
||
if err != nil {
|
||
http.Error(w, "Neispravan ID naloga", http.StatusBadRequest)
|
||
return
|
||
}
|
||
|
||
if err := h.ServisRepo.Obrisi(r.Context(), id); err != nil {
|
||
http.Error(w, "Greška pri brisanju naloga", http.StatusInternalServerError)
|
||
return
|
||
}
|
||
|
||
http.Redirect(w, r, "/servis?obrisan=1", http.StatusSeeOther)
|
||
}
|
||
|
||
// DetaljiNaloga prikazuje sve podatke jednog servisnog naloga sa ugrađenim delovima
|
||
func (h *Handler) DetaljiNaloga(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
|
||
}
|
||
|
||
// self-heal: ako u međuvremenu ima stanja na magacinu za potraživane artikle,
|
||
// povuci ih (delimično ili u celosti) i otključaj nalog ako je sve pokriveno.
|
||
// Hvata sve načine na koje stanje poraste, ne samo nabavku/izmenu/popis.
|
||
if potrazivani, e := h.ServisniPotrazivaniDeloviRepo.DohvatiZaNalog(r.Context(), id); e == nil && len(potrazivani) > 0 {
|
||
vidjeni := map[int64]bool{}
|
||
for _, p := range potrazivani {
|
||
if vidjeni[p.ArtikalID] {
|
||
continue
|
||
}
|
||
vidjeni[p.ArtikalID] = true
|
||
otkljucani, err := h.ServisniPotrazivaniDeloviRepo.ProveriIPocistiZaArtikal(r.Context(), p.ArtikalID)
|
||
if err != nil {
|
||
slog.Error("self-heal potraživanih delova nije uspeo", "artikal_id", p.ArtikalID, "error", err)
|
||
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)
|
||
}
|
||
}
|
||
}
|
||
// status naloga se možda promenio — ponovo ga učitaj za prikaz
|
||
if osvezen, e := h.ServisRepo.DohvatiID(r.Context(), id); e == nil && osvezen != nil {
|
||
nalog = osvezen
|
||
}
|
||
}
|
||
|
||
podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||
if err != nil {
|
||
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
|
||
return
|
||
}
|
||
|
||
klijentNaziv := ""
|
||
if nalog.KlijentID != nil {
|
||
klijent, err := h.KlijentiRepo.DohvatiID(r.Context(), *nalog.KlijentID)
|
||
if err == nil {
|
||
klijentNaziv = klijent.PunoIme()
|
||
}
|
||
}
|
||
|
||
tehnicarNaziv := ""
|
||
if nalog.TehnicarID != nil {
|
||
tehnicar, err := h.KorisniciRepo.DohvatiPoID(r.Context(), *nalog.TehnicarID)
|
||
if err == nil {
|
||
tehnicarNaziv = tehnicar.KorisnickoIme
|
||
}
|
||
}
|
||
|
||
delovi, err := h.deloviSaPotrazivanima(r.Context(), id)
|
||
if err != nil {
|
||
slog.Error("greška pri učitavanju delova", "error", err)
|
||
}
|
||
|
||
radovi, err := h.ServisniRadoviRepo.DohvatiZaNalog(r.Context(), id)
|
||
if err != nil {
|
||
slog.Error("greška pri učitavanju radova", "error", err)
|
||
}
|
||
|
||
appdb := appdbPkg.ArtikalFilter{}
|
||
artikli, err := h.Artikli.Lista(r.Context(), appdb)
|
||
if err != nil {
|
||
slog.Error("greška pri učitavanju artikala", "error", err)
|
||
}
|
||
|
||
usluge, err := h.UslugeRepo.Lista(r.Context(), appdbPkg.UslugaFilter{})
|
||
if err != nil {
|
||
slog.Error("greška pri učitavanju usluga", "error", err)
|
||
}
|
||
|
||
var ukupnoDelovi, ukupnoRadovi float64
|
||
for _, d := range delovi {
|
||
ukupnoDelovi += d.Ukupno()
|
||
}
|
||
for _, rad := range radovi {
|
||
ukupnoRadovi += rad.Ukupno()
|
||
}
|
||
// cena rada = zbir radova (usluga); ukupno za klijenta = radovi + delovi
|
||
ukupnoSve := ukupnoRadovi + ukupnoDelovi
|
||
avans := 0.0
|
||
if nalog.Avans != nil {
|
||
avans = *nalog.Avans
|
||
}
|
||
preostaloSve := ukupnoSve - avans
|
||
if preostaloSve < 0 {
|
||
preostaloSve = 0
|
||
}
|
||
|
||
zakljucanStatus := false
|
||
for _, d := range delovi {
|
||
if d.Potrazivano > 0 {
|
||
zakljucanStatus = true
|
||
break
|
||
}
|
||
}
|
||
|
||
ps := h.popuniPodaciStranice(r, podesavanja)
|
||
ps.Stranica = "servis"
|
||
ps.NaslovStranice = "Detalji naloga"
|
||
podaci := PodaciDetaljiNaloga{
|
||
PodaciStranice: ps,
|
||
Nalog: *nalog,
|
||
KlijentNaziv: klijentNaziv,
|
||
TehnicarNaziv: tehnicarNaziv,
|
||
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,
|
||
}
|
||
|
||
h.renderujTemplate(w, "servis_detalji", podaci)
|
||
}
|
||
|
||
// DodajDeloNalogu prima POST formu i dodaje artikal kao deo servisnog naloga
|
||
func (h *Handler) DodajDeloNalogu(w http.ResponseWriter, r *http.Request) {
|
||
k, ok := h.zahtevajDozvolu(w, r, "servis.izmeni")
|
||
if !ok {
|
||
return
|
||
}
|
||
|
||
nalogID, 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
|
||
}
|
||
|
||
artikalID, err := strconv.ParseInt(r.FormValue("artikal_id"), 10, 64)
|
||
if err != nil || artikalID <= 0 {
|
||
middleware.SetFlash(w, r, h.DB, "greska", "Neispravan artikal.")
|
||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(nalogID, 10), http.StatusSeeOther)
|
||
return
|
||
}
|
||
|
||
kolicina, err := strconv.Atoi(r.FormValue("kolicina"))
|
||
if err != nil || kolicina <= 0 {
|
||
middleware.SetFlash(w, r, h.DB, "greska", "Količina mora biti pozitivan broj.")
|
||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(nalogID, 10), http.StatusSeeOther)
|
||
return
|
||
}
|
||
|
||
cena, err := strconv.ParseFloat(r.FormValue("cena_komada"), 64)
|
||
if err != nil || cena < 0 {
|
||
middleware.SetFlash(w, r, h.DB, "greska", "Cena mora biti pozitivan broj.")
|
||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(nalogID, 10), http.StatusSeeOther)
|
||
return
|
||
}
|
||
|
||
// ime artikla za poruke korisniku
|
||
imeArtikla := ""
|
||
if art, e := h.Artikli.DohvatiID(r.Context(), artikalID); e == nil && art != nil {
|
||
imeArtikla = art.Naziv
|
||
}
|
||
|
||
// 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)
|
||
if err != nil {
|
||
slog.Error("greška pri dodavanju dela", "error", err)
|
||
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri dodavanju dela.")
|
||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(nalogID, 10), http.StatusSeeOther)
|
||
return
|
||
}
|
||
|
||
// ako nešto nedostaje, prebaci nalog u „Čeka delove" i obavesti korisnika
|
||
if nedostaje > 0 {
|
||
if e := h.ServisRepo.AzurirajStatus(r.Context(), nalogID, model.StatusCekaDelove); e != nil {
|
||
slog.Error("greška pri prebacivanju naloga u Čeka delove", "error", e)
|
||
}
|
||
if ugradjeno > 0 {
|
||
middleware.SetFlash(w, r, h.DB, "greska",
|
||
"Ugrađeno "+strconv.Itoa(ugradjeno)+" kom. artikla „"+imeArtikla+
|
||
"\", nedostaje još "+strconv.Itoa(nedostaje)+" — nalog je prebačen u „Čeka delove\".")
|
||
} else {
|
||
middleware.SetFlash(w, r, h.DB, "greska",
|
||
"Nema „"+imeArtikla+"\" na stanju — potrebno "+strconv.Itoa(nedostaje)+
|
||
" kom. Nalog je prebačen u „Čeka delove\".")
|
||
}
|
||
}
|
||
|
||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(nalogID, 10)+"?sacuvano=1", http.StatusSeeOther)
|
||
}
|
||
|
||
// deloviSaPotrazivanima vraća ugrađene delove spojene sa potraživanima (ista tabela).
|
||
// Potraživani delovi bez ugrađenog (stanje bilo 0) se dodaju sa Kolicina=0.
|
||
func (h *Handler) deloviSaPotrazivanima(ctx context.Context, nalogID int64) ([]model.ServisniDeoSaArtiklom, error) {
|
||
delovi, err := h.ServisniDeloviRepo.DohvatiZaNalog(ctx, nalogID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
potrazivani, err := h.ServisniPotrazivaniDeloviRepo.DohvatiZaNalog(ctx, nalogID)
|
||
if err != nil {
|
||
return delovi, nil // ne prekidamo ako potraživani nisu dostupni
|
||
}
|
||
potrMap := make(map[int64]int)
|
||
for _, p := range potrazivani {
|
||
potrMap[p.ArtikalID] += p.Kolicina
|
||
}
|
||
for i := range delovi {
|
||
if kol, ok := potrMap[delovi[i].ArtikalID]; ok {
|
||
delovi[i].Potrazivano = kol
|
||
delete(potrMap, delovi[i].ArtikalID)
|
||
}
|
||
}
|
||
for artikalID, kol := range potrMap {
|
||
naziv := ""
|
||
if art, e := h.Artikli.DohvatiID(ctx, artikalID); e == nil && art != nil {
|
||
naziv = art.Naziv
|
||
}
|
||
delovi = append(delovi, model.ServisniDeoSaArtiklom{
|
||
ArtikalNaziv: naziv,
|
||
ServisniDeo: model.ServisniDeo{ArtikalID: artikalID},
|
||
Potrazivano: kol,
|
||
})
|
||
}
|
||
return delovi, nil
|
||
}
|
||
|
||
// ObrisiDeloNaloga prima POST zahtev i uklanja deo iz servisnog naloga
|
||
func (h *Handler) ObrisiDeloNaloga(w http.ResponseWriter, r *http.Request) {
|
||
k, ok := h.zahtevajDozvolu(w, r, "servis.izmeni")
|
||
if !ok {
|
||
return
|
||
}
|
||
|
||
nalogID, err := parseID(chi.URLParam(r, "id"))
|
||
if err != nil {
|
||
http.Error(w, "Neispravan ID naloga", http.StatusBadRequest)
|
||
return
|
||
}
|
||
|
||
deoID, err := parseID(chi.URLParam(r, "deo_id"))
|
||
if err != nil {
|
||
http.Error(w, "Neispravan ID dela", http.StatusBadRequest)
|
||
return
|
||
}
|
||
|
||
// obriši i potraživane delove za isti artikal (inače ostaju blokirani u tabeli)
|
||
if artikalID, err := h.ServisniDeloviRepo.DohvatiArtikalID(r.Context(), deoID); err == nil {
|
||
if err := h.ServisniPotrazivaniDeloviRepo.ObrisiZaArtikal(r.Context(), nalogID, artikalID); err != nil {
|
||
slog.Error("greška pri brisanju potraživanih delova", "error", err)
|
||
}
|
||
}
|
||
|
||
if err := h.ServisniDeloviRepo.Obrisi(r.Context(), deoID, &k.ID); err != nil {
|
||
slog.Error("greška pri brisanju dela", "error", err)
|
||
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri uklanjanju 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 {
|
||
return
|
||
}
|
||
|
||
nalogID, 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
|
||
}
|
||
|
||
uslugaID, err := strconv.ParseInt(r.FormValue("usluga_id"), 10, 64)
|
||
if err != nil || uslugaID <= 0 {
|
||
middleware.SetFlash(w, r, h.DB, "greska", "Izaberite uslugu.")
|
||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(nalogID, 10), http.StatusSeeOther)
|
||
return
|
||
}
|
||
|
||
usluga, err := h.UslugeRepo.DohvatiID(r.Context(), uslugaID)
|
||
if err != nil {
|
||
middleware.SetFlash(w, r, h.DB, "greska", "Izabrana usluga nije pronađena.")
|
||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(nalogID, 10), http.StatusSeeOther)
|
||
return
|
||
}
|
||
|
||
kolicina, err := strconv.ParseFloat(r.FormValue("kolicina"), 64)
|
||
if err != nil || kolicina <= 0 {
|
||
middleware.SetFlash(w, r, h.DB, "greska", "Količina mora biti pozitivan broj.")
|
||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(nalogID, 10), http.StatusSeeOther)
|
||
return
|
||
}
|
||
|
||
cena, err := strconv.ParseFloat(r.FormValue("cena_komada"), 64)
|
||
if err != nil || cena < 0 {
|
||
middleware.SetFlash(w, r, h.DB, "greska", "Cena mora biti pozitivan broj.")
|
||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(nalogID, 10), http.StatusSeeOther)
|
||
return
|
||
}
|
||
|
||
if _, err := h.ServisniRadoviRepo.Dodaj(r.Context(), nalogID, uslugaID, usluga.Naziv, kolicina, cena); err != nil {
|
||
slog.Error("greška pri dodavanju rada", "error", err)
|
||
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri dodavanju rada.")
|
||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(nalogID, 10), http.StatusSeeOther)
|
||
return
|
||
}
|
||
|
||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(nalogID, 10)+"?sacuvano=1", http.StatusSeeOther)
|
||
}
|
||
|
||
// ObrisiRadNaloga uklanja stavku rada sa naloga
|
||
func (h *Handler) ObrisiRadNaloga(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
|
||
}
|
||
|
||
radID, err := parseID(chi.URLParam(r, "rad_id"))
|
||
if err != nil {
|
||
http.Error(w, "Neispravan ID rada", http.StatusBadRequest)
|
||
return
|
||
}
|
||
|
||
if err := h.ServisniRadoviRepo.Obrisi(r.Context(), radID); err != nil {
|
||
slog.Error("greška pri brisanju rada", "error", err)
|
||
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri uklanjanju rada.")
|
||
}
|
||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(nalogID, 10), http.StatusSeeOther)
|
||
}
|
||
|
||
// parseFormuNaloga čita i validira polja iz HTTP forme
|
||
func parseFormuNaloga(r *http.Request) (model.ServisniNalog, string) {
|
||
uredjaj := strings.TrimSpace(r.FormValue("uredjaj"))
|
||
if uredjaj == "" {
|
||
return model.ServisniNalog{}, "Naziv uređaja je obavezan."
|
||
}
|
||
|
||
opisKvara := strings.TrimSpace(r.FormValue("opis_kvara"))
|
||
if opisKvara == "" {
|
||
return model.ServisniNalog{}, "Opis kvara je obavezan."
|
||
}
|
||
|
||
nalog := model.ServisniNalog{
|
||
BrojNaloga: strings.TrimSpace(r.FormValue("broj_naloga")),
|
||
Uredjaj: uredjaj,
|
||
SerijskiBroj: strings.TrimSpace(r.FormValue("serijski_broj")),
|
||
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 == "" {
|
||
nalog.Status = model.StatusPrimljeno
|
||
}
|
||
|
||
// opcioni klijent
|
||
if kidStr := r.FormValue("klijent_id"); kidStr != "" {
|
||
if kid, err := strconv.ParseInt(kidStr, 10, 64); err == nil {
|
||
nalog.KlijentID = &kid
|
||
}
|
||
}
|
||
|
||
// opcioni tehničar
|
||
if tidStr := r.FormValue("tehnicar_id"); tidStr != "" {
|
||
if tid, err := strconv.ParseInt(tidStr, 10, 64); err == nil {
|
||
nalog.TehnicarID = &tid
|
||
}
|
||
}
|
||
|
||
// opcione cene — prazno polje ostaje nil
|
||
nalog.CenaOd = parseOpcionuCenu(r.FormValue("cena_od"))
|
||
nalog.CenaDo = parseOpcionuCenu(r.FormValue("cena_do"))
|
||
nalog.CenaKonacna = parseOpcionuCenu(r.FormValue("cena_konacna"))
|
||
nalog.Avans = parseOpcionuCenu(r.FormValue("avans"))
|
||
|
||
// opcioni datum završetka
|
||
if dz := strings.TrimSpace(r.FormValue("datum_zavrsetka")); dz != "" {
|
||
if t, err := time.Parse("2006-01-02", dz); err == nil {
|
||
nalog.DatumZavrsetka = &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, ""
|
||
}
|
||
|
||
// mozdaKreirajKlijenta kreira novog klijenta iz polja forme naloga kada nije
|
||
// izabran postojeći (klijent_id prazno), a uneto je ime (fizičko) ili naziv firme
|
||
// (pravno). Klijent je opcioni — ako nema dovoljno podataka, nalog ostaje bez njega.
|
||
// Vraća srpsku poruku o grešci za prikaz, ili prazan string ako je sve u redu.
|
||
func (h *Handler) mozdaKreirajKlijenta(ctx context.Context, r *http.Request, nalog *model.ServisniNalog) string {
|
||
if nalog.KlijentID != nil {
|
||
return "" // izabran postojeći klijent — ništa ne kreiramo
|
||
}
|
||
|
||
tip := r.FormValue("tip")
|
||
if tip != "fizicko" && tip != "pravno" {
|
||
tip = "fizicko"
|
||
}
|
||
ime := strings.TrimSpace(r.FormValue("ime"))
|
||
nazivFirme := strings.TrimSpace(r.FormValue("naziv_firme"))
|
||
|
||
// bez minimalnih podataka nalog ostaje bez klijenta
|
||
if tip == "fizicko" && ime == "" {
|
||
return ""
|
||
}
|
||
if tip == "pravno" && nazivFirme == "" {
|
||
return ""
|
||
}
|
||
|
||
email := strings.TrimSpace(r.FormValue("email"))
|
||
if email != "" && !strings.Contains(email, "@") {
|
||
return "Adresa e-pošte nije ispravna."
|
||
}
|
||
|
||
// tip identifikacionog broja se prepoznaje iz dužine unosa:
|
||
// 13 cifara → JMBG, 9 cifara → broj lične karte
|
||
jmbg := strings.TrimSpace(r.FormValue("jmbg"))
|
||
tipIdent, greska := odrediTipIdentifikacije(jmbg)
|
||
if greska != "" {
|
||
return greska
|
||
}
|
||
if tipIdent == "" {
|
||
tipIdent = "jmbg" // prazan unos — podrazumevani tip radi konzistentnosti baze
|
||
}
|
||
|
||
klijent := model.Klijent{
|
||
Tip: tip,
|
||
Ime: ime,
|
||
Prezime: strings.TrimSpace(r.FormValue("prezime")),
|
||
JMBG: jmbg,
|
||
TipIdentifikacije: tipIdent,
|
||
NazivFirme: nazivFirme,
|
||
PIB: strings.TrimSpace(r.FormValue("pib")),
|
||
Telefon: strings.TrimSpace(r.FormValue("telefon")),
|
||
Email: email,
|
||
Mesto: strings.TrimSpace(r.FormValue("mesto")),
|
||
}
|
||
|
||
id, err := h.KlijentiRepo.Kreiraj(ctx, &klijent)
|
||
if err != nil {
|
||
slog.Error("greška pri kreiranju klijenta iz naloga", "error", err)
|
||
return "Došlo je do greške pri čuvanju klijenta. Pokušajte ponovo."
|
||
}
|
||
nalog.KlijentID = &id
|
||
return ""
|
||
}
|
||
|
||
// 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)
|
||
if s == "" {
|
||
return nil
|
||
}
|
||
v, err := strconv.ParseFloat(s, 64)
|
||
if err != nil || v < 0 {
|
||
return nil
|
||
}
|
||
return &v
|
||
}
|
||
|
||
// renderujFormuNaloga renderuje HTML šablon forme za unos ili izmenu servisnog naloga
|
||
func (h *Handler) renderujFormuNaloga(w http.ResponseWriter, podaci PodaciFormeNaloga) {
|
||
h.renderujTemplate(w, "servis_forma", podaci)
|
||
}
|
||
|
||
// PodaciStampeServisa su podaci za print-friendly prikaz servisnog naloga
|
||
type PodaciStampeServisa struct {
|
||
Nalog model.ServisniNalog
|
||
ServisniDelovi []model.ServisniDeoSaArtiklom
|
||
UkupnoDelovi float64
|
||
KlijentNaziv string
|
||
TehnicarNaziv string
|
||
NazivFirme string
|
||
Podnazlov string
|
||
Adresa string
|
||
Telefon string
|
||
PIB string
|
||
QRKod string // base64 PNG QR koda sa URL-om naloga
|
||
}
|
||
|
||
// StampaServisa renderuje print-friendly stranicu za servisni nalog
|
||
func (h *Handler) StampaServisa(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
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
klijentNaziv := ""
|
||
if nalog.KlijentID != nil {
|
||
klijent, err := h.KlijentiRepo.DohvatiID(r.Context(), *nalog.KlijentID)
|
||
if err == nil {
|
||
if klijent.NazivFirme != "" {
|
||
klijentNaziv = klijent.NazivFirme
|
||
} else {
|
||
klijentNaziv = strings.TrimSpace(klijent.Ime + " " + klijent.Prezime)
|
||
}
|
||
}
|
||
}
|
||
|
||
tehnicarNaziv := ""
|
||
if nalog.TehnicarID != nil {
|
||
tehnicar, err := h.KorisniciRepo.DohvatiPoID(r.Context(), *nalog.TehnicarID)
|
||
if err == nil {
|
||
tehnicarNaziv = tehnicar.KorisnickoIme
|
||
}
|
||
}
|
||
|
||
var ukupnoDelovi float64
|
||
for _, d := range delovi {
|
||
ukupnoDelovi += d.Ukupno()
|
||
}
|
||
|
||
// QR kod vodi na javnu status stranicu — dostupnu bez prijave
|
||
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_stampa", PodaciStampeServisa{
|
||
Nalog: *nalog,
|
||
ServisniDelovi: delovi,
|
||
UkupnoDelovi: ukupnoDelovi,
|
||
KlijentNaziv: klijentNaziv,
|
||
TehnicarNaziv: tehnicarNaziv,
|
||
NazivFirme: podesavanja["naziv_firme"],
|
||
Podnazlov: podesavanja["podnazlov"],
|
||
Adresa: podesavanja["adresa"],
|
||
Telefon: podesavanja["telefon"],
|
||
PIB: podesavanja["pib"],
|
||
QRKod: qrKod,
|
||
})
|
||
}
|
||
|
||
// PodaciOtpremnice su podaci za otpremnicu pri preuzimanju uređaja
|
||
type PodaciOtpremnice struct {
|
||
Nalog model.ServisniNalog
|
||
ServisniDelovi []model.ServisniDeoSaArtiklom
|
||
UkupnoDelovi float64
|
||
PreostaloSve float64
|
||
ImaAvans bool
|
||
QRKod string
|
||
Klijent *model.Klijent
|
||
KlijentNaziv string
|
||
TehnicarNaziv string
|
||
NazivFirme string
|
||
Podnazlov string
|
||
Adresa string
|
||
Telefon string
|
||
PIB string
|
||
}
|
||
|
||
// StampaOtpremnice renderuje otpremnicu pri preuzimanju uređaja od strane klijenta
|
||
func (h *Handler) StampaOtpremnice(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
|
||
}
|
||
|
||
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
|
||
if k.NazivFirme != "" {
|
||
klijentNaziv = k.NazivFirme
|
||
} else {
|
||
klijentNaziv = strings.TrimSpace(k.Ime + " " + k.Prezime)
|
||
}
|
||
}
|
||
}
|
||
|
||
tehnicarNaziv := ""
|
||
if nalog.TehnicarID != nil {
|
||
tehnicar, err := h.KorisniciRepo.DohvatiPoID(r.Context(), *nalog.TehnicarID)
|
||
if err == nil {
|
||
tehnicarNaziv = tehnicar.KorisnickoIme
|
||
}
|
||
}
|
||
|
||
var ukupnoDelovi float64
|
||
for _, d := range delovi {
|
||
ukupnoDelovi += d.Ukupno()
|
||
}
|
||
var preostaloSve float64
|
||
var imaAvans bool
|
||
if nalog.CenaKonacna != nil {
|
||
ukupnoSve := *nalog.CenaKonacna + ukupnoDelovi
|
||
avans := 0.0
|
||
if nalog.Avans != nil && *nalog.Avans > 0 {
|
||
avans = *nalog.Avans
|
||
imaAvans = true
|
||
}
|
||
preostaloSve = ukupnoSve - avans
|
||
if preostaloSve < 0 {
|
||
preostaloSve = 0
|
||
}
|
||
}
|
||
|
||
nalogURL := qrNalogURL(r, nalog.JavniToken)
|
||
var qrKodOtpr string
|
||
if png, err := qrcode.Encode(nalogURL, qrcode.Medium, 160); err == nil {
|
||
qrKodOtpr = base64.StdEncoding.EncodeToString(png)
|
||
}
|
||
|
||
h.renderujStandalone(w, "servis_otpremnica", PodaciOtpremnice{
|
||
Nalog: *nalog,
|
||
ServisniDelovi: delovi,
|
||
UkupnoDelovi: ukupnoDelovi,
|
||
PreostaloSve: preostaloSve,
|
||
ImaAvans: imaAvans,
|
||
QRKod: qrKodOtpr,
|
||
Klijent: klijent,
|
||
KlijentNaziv: klijentNaziv,
|
||
TehnicarNaziv: tehnicarNaziv,
|
||
NazivFirme: podesavanja["naziv_firme"],
|
||
Podnazlov: podesavanja["podnazlov"],
|
||
Adresa: podesavanja["adresa"],
|
||
Telefon: podesavanja["telefon"],
|
||
PIB: podesavanja["pib"],
|
||
})
|
||
}
|
||
|
||
// PodaciPredracuna su podaci za predračun/ponudu koja se šalje klijentu pre rada
|
||
type PodaciPredracuna struct {
|
||
Nalog model.ServisniNalog
|
||
ServisniDelovi []model.ServisniDeoSaArtiklom
|
||
UkupnoDelovi float64
|
||
|
||
ImaCenuRada bool // ima li nalog uopšte cenu rada (raspon ili fiksnu)
|
||
CenaRaspon bool // true → prikaži procenu Od–Do; false → fiksnu cenu
|
||
CenaRadaOd float64 // donja granica procene
|
||
CenaRadaDo float64 // gornja granica procene
|
||
CenaRada float64 // fiksna cena rada
|
||
|
||
UkupnoOd float64 // delovi + CenaRadaOd (kad je raspon)
|
||
UkupnoDo float64 // delovi + CenaRadaDo (kad je raspon)
|
||
Ukupno float64 // delovi + fiksna cena (ili samo delovi ako nema cene rada)
|
||
|
||
DatumIzdavanja time.Time
|
||
VaziDo time.Time
|
||
|
||
QRKod string
|
||
Klijent *model.Klijent
|
||
KlijentNaziv string
|
||
TehnicarNaziv string
|
||
NazivFirme string
|
||
Podnazlov string
|
||
Adresa string
|
||
Telefon string
|
||
PIB string
|
||
}
|
||
|
||
// StampaPredracuna renderuje predračun/ponudu koja se šalje klijentu kada se utvrdi kvar
|
||
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
|
||
}
|
||
|
||
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
|
||
if k.NazivFirme != "" {
|
||
klijentNaziv = k.NazivFirme
|
||
} else {
|
||
klijentNaziv = strings.TrimSpace(k.Ime + " " + k.Prezime)
|
||
}
|
||
}
|
||
}
|
||
|
||
tehnicarNaziv := ""
|
||
if nalog.TehnicarID != nil {
|
||
tehnicar, err := h.KorisniciRepo.DohvatiPoID(r.Context(), *nalog.TehnicarID)
|
||
if err == nil {
|
||
tehnicarNaziv = tehnicar.KorisnickoIme
|
||
}
|
||
}
|
||
|
||
var ukupnoDelovi float64
|
||
for _, d := range delovi {
|
||
ukupnoDelovi += d.Ukupno()
|
||
}
|
||
|
||
// cena rada: ako postoji procena raspona (Od i Do) prikaži je, inače padni na fiksnu cenu
|
||
var imaCenuRada, cenaRaspon bool
|
||
var cenaRadaOd, cenaRadaDo, cenaRada float64
|
||
var ukupnoOd, ukupnoDo, ukupno float64
|
||
switch {
|
||
case nalog.CenaOd != nil && nalog.CenaDo != nil:
|
||
imaCenuRada = true
|
||
cenaRaspon = true
|
||
cenaRadaOd = *nalog.CenaOd
|
||
cenaRadaDo = *nalog.CenaDo
|
||
ukupnoOd = ukupnoDelovi + cenaRadaOd
|
||
ukupnoDo = ukupnoDelovi + cenaRadaDo
|
||
case nalog.CenaKonacna != nil:
|
||
imaCenuRada = true
|
||
cenaRada = *nalog.CenaKonacna
|
||
ukupno = ukupnoDelovi + cenaRada
|
||
default:
|
||
ukupno = ukupnoDelovi
|
||
}
|
||
|
||
// 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)
|
||
|
||
// QR kod vodi na javnu status stranicu — dostupnu bez prijave
|
||
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,
|
||
ServisniDelovi: delovi,
|
||
UkupnoDelovi: ukupnoDelovi,
|
||
ImaCenuRada: imaCenuRada,
|
||
CenaRaspon: cenaRaspon,
|
||
CenaRadaOd: cenaRadaOd,
|
||
CenaRadaDo: cenaRadaDo,
|
||
CenaRada: cenaRada,
|
||
UkupnoOd: ukupnoOd,
|
||
UkupnoDo: ukupnoDo,
|
||
Ukupno: ukupno,
|
||
DatumIzdavanja: datumIzdavanja,
|
||
VaziDo: vaziDo,
|
||
QRKod: qrKod,
|
||
Klijent: klijent,
|
||
KlijentNaziv: klijentNaziv,
|
||
TehnicarNaziv: tehnicarNaziv,
|
||
NazivFirme: podesavanja["naziv_firme"],
|
||
Podnazlov: podesavanja["podnazlov"],
|
||
Adresa: podesavanja["adresa"],
|
||
Telefon: podesavanja["telefon"],
|
||
PIB: podesavanja["pib"],
|
||
})
|
||
}
|
||
|
||
// PromeniStatus obrađuje POST /servis/{id}/status i menja samo status naloga
|
||
func (h *Handler) PromeniStatus(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
|
||
}
|
||
if err := r.ParseForm(); err != nil {
|
||
http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest)
|
||
return
|
||
}
|
||
noviStatus := strings.TrimSpace(r.FormValue("status"))
|
||
dozvoljenStatusi := map[string]bool{}
|
||
for _, s := range model.SviStatusi {
|
||
dozvoljenStatusi[s] = true
|
||
}
|
||
if !dozvoljenStatusi[noviStatus] {
|
||
http.Error(w, "Nepoznat status", http.StatusBadRequest)
|
||
return
|
||
}
|
||
|
||
trenutni, e := h.ServisRepo.DohvatiID(r.Context(), id)
|
||
|
||
// „Čeka delove" je automatski status (postavlja se kad nedostaje deo) —
|
||
// ne može se izabrati ručno ni iz jednog drugog statusa
|
||
if noviStatus == model.StatusCekaDelove && e == nil && trenutni.Status != model.StatusCekaDelove {
|
||
middleware.SetFlash(w, r, h.DB, "greska",
|
||
"Status „Čeka delove\" se postavlja automatski kada nedostaje deo i ne može se izabrati ručno.")
|
||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(id, 10), http.StatusSeeOther)
|
||
return
|
||
}
|
||
|
||
// zabrana izlaska iz „Čeka delove" dok postoje potraživani delovi —
|
||
// prvo nabaviti delove, pa obrisati iz potraživanih (ili kroz nabavku)
|
||
if e == nil && trenutni.Status == model.StatusCekaDelove && noviStatus != model.StatusCekaDelove {
|
||
potrazivani, err := h.ServisniPotrazivaniDeloviRepo.DohvatiZaNalog(r.Context(), id)
|
||
if err == nil && len(potrazivani) > 0 {
|
||
middleware.SetFlash(w, r, h.DB, "greska",
|
||
"Nalog ne može da napusti „Čeka delove\" dok ima delova koji nedostaju. Obrišite ih iz tabele ili dopunite zalihe.")
|
||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(id, 10), http.StatusSeeOther)
|
||
return
|
||
}
|
||
}
|
||
|
||
if err := h.ServisRepo.AzurirajStatus(r.Context(), id, noviStatus); err != nil {
|
||
slog.Error("greška pri promeni statusa naloga", "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)
|
||
}
|
||
|
||
// AzurirajGaranciju ažurira datum garancije na servisnom nalogu
|
||
func (h *Handler) AzurirajGaranciju(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
|
||
}
|
||
var garancijaDo *time.Time
|
||
if r.FormValue("bez_garancije") != "1" {
|
||
if gd := strings.TrimSpace(r.FormValue("garancija_do")); gd != "" {
|
||
t, err := time.Parse("2006-01-02", gd)
|
||
if err != nil {
|
||
http.Error(w, "Neispravan datum garancije", http.StatusBadRequest)
|
||
return
|
||
}
|
||
garancijaDo = &t
|
||
} else {
|
||
// odštiklirano „bez garancije" bez izabranog datuma → vrati default
|
||
nalog, err := h.ServisRepo.DohvatiID(r.Context(), id)
|
||
if err != nil {
|
||
slog.Error("greška pri dohvatanju naloga za garanciju", "id", id, "error", err)
|
||
} else {
|
||
podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||
if err != nil {
|
||
slog.Error("greška pri učitavanju podešavanja za garanciju", "error", err)
|
||
} else {
|
||
garancijaDo = defaultGarancija(nalog.DatumPrijema, podesavanja)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if err := h.ServisRepo.AzurirajGaranciju(r.Context(), id, garancijaDo); err != nil {
|
||
slog.Error("greška pri ažuriranju garancije", "id", id, "error", err)
|
||
http.Error(w, "Greška pri ažuriranju garancije", http.StatusInternalServerError)
|
||
return
|
||
}
|
||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(id, 10), 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,
|
||
})
|
||
}
|
||
|
||
// qrNalogURL konstruiše URL za QR kod vodeći računa o reverse proxy-ju.
|
||
// Ako aplikacija radi iza nginx/Caddy/Traefik koji prekida TLS, r.TLS je nil,
|
||
// ali X-Forwarded-Proto header sadrži stvarnu šemu.
|
||
func qrNalogURL(r *http.Request, token string) string {
|
||
scheme := "http"
|
||
if r.TLS != nil {
|
||
scheme = "https"
|
||
}
|
||
if r.Header.Get("X-Forwarded-Proto") == "https" {
|
||
scheme = "https"
|
||
}
|
||
return scheme + "://" + r.Host + "/status/" + token
|
||
}
|