Garancija se sad čuva kao broj dana (GarancijaDana), a konkretan datum (GarancijaDo) se računa tek kad nalog pređe u Završeno/Preuzeto — na osnovu stvarnog datuma završetka, ne datuma prijema. Ranije se datum računao odmah pri prijemu naloga, pa je garancija krenula da teče pre nego što je popravka uopšte gotova.
3141 lines
105 KiB
Go
3141 lines
105 KiB
Go
package handler
|
||
|
||
import (
|
||
"bytes"
|
||
"context"
|
||
"encoding/base64"
|
||
"encoding/json"
|
||
"fmt"
|
||
"image/png"
|
||
"log/slog"
|
||
"net/http"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"ntech/internal/config"
|
||
appdbPkg "ntech/internal/db"
|
||
"ntech/internal/db/sqlite"
|
||
"ntech/internal/fiskal"
|
||
"ntech/internal/middleware"
|
||
"ntech/internal/model"
|
||
|
||
"github.com/boombuler/barcode"
|
||
"github.com/boombuler/barcode/code128"
|
||
"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
|
||
NemaFiskalnog map[int64]bool // ID-evi naloga sa statusom Preuzeto koji nemaju fiskalni račun
|
||
}
|
||
|
||
// 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
|
||
GarancijaDefault string // podrazumevana garancija (prijem + meseci), format 2006-01-02
|
||
BezGarancije bool // u bazi nalog nema garanciju (GarancijaDo == NULL)
|
||
}
|
||
|
||
// 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 // ugrađeni/traženi (predlozeno=false)
|
||
ServisniRadovi []model.ServisniRad // ugrađeni/traženi (predlozeno=false)
|
||
PredlozeniDelovi []model.ServisniDeoSaArtiklom // serviser predložio posle dijagnostike
|
||
PredlozeniRadovi []model.ServisniRad // serviser predložio posle dijagnostike
|
||
ImaPredlog bool // postoji bar jedna predložena stavka
|
||
ZakljucaniUgradjeni bool // ugrađene stavke zaključane (status nije „Primljeno")
|
||
Artikli []model.ArtikalSaKategorijom
|
||
Usluge []model.Usluga
|
||
Sacuvano bool
|
||
UkupnoDelovi float64
|
||
UkupnoRadovi float64
|
||
UkupnoDeloviSaPdv float64
|
||
UkupnoRadoviSaPdv float64
|
||
UkupnoSve float64
|
||
UkupnoSveSaPdv float64
|
||
Avans float64
|
||
PreostaloSve float64
|
||
PreostaloSveSaPdv 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
|
||
FiskalniRacun *model.FiskalniRacun // nil ako nije fiskalizovano
|
||
RokPodizanja *time.Time // DatumZavrsetka + 30 dana; nil dok nije završeno
|
||
FiskalGreska bool // true: fiskalizacija aktivna, status Preuzeto, ali nema fiskalnog računa
|
||
}
|
||
|
||
// 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"
|
||
var nemaFiskalnog map[int64]bool
|
||
if h.modulUkljucen(r.Context(), config.ModulFiskalizacija) {
|
||
nemaFiskalnog, _ = h.FiskalRepo.ServisiBezFiskalnog(r.Context())
|
||
}
|
||
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",
|
||
NemaFiskalnog: nemaFiskalnog,
|
||
}
|
||
|
||
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"
|
||
var nemaFiskalnog map[int64]bool
|
||
if h.modulUkljucen(r.Context(), config.ModulFiskalizacija) {
|
||
nemaFiskalnog, _ = h.FiskalRepo.ServisiBezFiskalnog(r.Context())
|
||
}
|
||
podaci := PodaciServisa{
|
||
PodaciStranice: ps,
|
||
Nalozi: nalozi,
|
||
Pretraga: pretraga,
|
||
SviStatusi: model.SviStatusi,
|
||
NemaFiskalnog: nemaFiskalnog,
|
||
}
|
||
|
||
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 servisera", http.StatusInternalServerError)
|
||
return
|
||
}
|
||
|
||
ps := h.popuniPodaciStranice(r, podesavanja)
|
||
ps.Stranica = "servis"
|
||
ps.NaslovStranice = "Novi nalog"
|
||
dana := defaultGarancijaDana(podesavanja)
|
||
noviNalog := model.ServisniNalog{
|
||
BrojNaloga: brojNaloga,
|
||
Status: model.StatusPrimljeno,
|
||
DatumPrijema: time.Now(),
|
||
}
|
||
noviNalog.GarancijaDana = &dana
|
||
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
|
||
}
|
||
|
||
// forma novog naloga ne sadrži polje garancije — primeni podrazumevanu
|
||
// garanciju iz podešavanja, osim ako je korisnik izričito izabrao „bez garancije"
|
||
if nalog.GarancijaDana == nil && r.FormValue("bez_garancije") != "1" {
|
||
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||
dana := defaultGarancijaDana(podesavanja)
|
||
nalog.GarancijaDana = &dana
|
||
}
|
||
|
||
// garancija ne sme da bude pre datuma prijema
|
||
if nalog.GarancijaDo != nil && garancijaPrePrijema(*nalog.GarancijaDo, nalog.DatumPrijema) {
|
||
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: "Datum garancije ne može biti pre datuma prijema.",
|
||
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 servisera", http.StatusInternalServerError)
|
||
return
|
||
}
|
||
|
||
garancijaDefault := strconv.Itoa(defaultGarancijaDana(podesavanja))
|
||
// stanje „bez garancije" čitamo iz baze pre nego što popunimo default za prikaz
|
||
bezGarancije := nalog.GarancijaDana == nil || *nalog.GarancijaDana <= 0
|
||
if nalog.GarancijaDana == nil {
|
||
dana := defaultGarancijaDana(podesavanja)
|
||
nalog.GarancijaDana = &dana
|
||
}
|
||
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,
|
||
GarancijaDefault: garancijaDefault,
|
||
BezGarancije: bezGarancije,
|
||
})
|
||
}
|
||
|
||
// 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
|
||
}
|
||
|
||
// auto-snimanje (forma se snima u realnom vremenu): umesto redirecta vraćamo 204,
|
||
// a greške kao kratak tekst sa statusom — JS na stranici to prikaže korisniku
|
||
autosave := r.Header.Get("X-Autosave") == "1"
|
||
|
||
nalog, greska := parseFormuNaloga(r)
|
||
if greska != "" {
|
||
if autosave {
|
||
http.Error(w, greska, http.StatusBadRequest)
|
||
return
|
||
}
|
||
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 (osim garancije), cene ni klijenta —
|
||
// očuvaj postojeće vrednosti (status se menja u detaljima, cena rada se računa iz
|
||
// radova, datum završetka se postavlja kasnije). Garancija se sada uređuje u formi,
|
||
// pa je čuvamo iz parseFormuNaloga. 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.DatumZavrsetka = stari.DatumZavrsetka
|
||
nalog.PredvidjenDatum = stari.PredvidjenDatum
|
||
nalog.DatumPrijema = stari.DatumPrijema
|
||
nalog.GarancijaDo = stari.GarancijaDo // garancija se uređuje u detaljima, ne u formi
|
||
nalog.GarancijaDana = stari.GarancijaDana // isto — trajanje garancije se ne dira u formi
|
||
}
|
||
|
||
// garancija ne sme da bude pre datuma prijema
|
||
if nalog.GarancijaDo != nil && garancijaPrePrijema(*nalog.GarancijaDo, nalog.DatumPrijema) {
|
||
greska := "Datum garancije ne može biti pre datuma prijema."
|
||
if autosave {
|
||
http.Error(w, greska, http.StatusBadRequest)
|
||
return
|
||
}
|
||
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
|
||
}
|
||
|
||
// ako nije izabran postojeći klijent, eventualno kreiraj novog iz polja forme
|
||
if greska := h.mozdaKreirajKlijenta(r.Context(), r, &nalog); greska != "" {
|
||
if autosave {
|
||
http.Error(w, greska, http.StatusBadRequest)
|
||
return
|
||
}
|
||
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)
|
||
if autosave {
|
||
http.Error(w, "Došlo je do greške pri čuvanju.", http.StatusInternalServerError)
|
||
return
|
||
}
|
||
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
|
||
}
|
||
|
||
if autosave {
|
||
w.WriteHeader(http.StatusNoContent)
|
||
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) {
|
||
k, ok := h.zahtevajDozvolu(w, r, "servis.obrisi")
|
||
if !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, &k.ID); err != nil {
|
||
slog.Error("greška pri brisanju naloga", "id", id, "error", err)
|
||
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.
|
||
// Samo predlozeno=false redovi blokiraju nalog — predloženi se ignorišu.
|
||
if potrazivaniSvi, e := h.ServisniPotrazivaniDeloviRepo.DohvatiZaNalog(r.Context(), id); e == nil {
|
||
var potrazivani []model.ServisniPotrazivaniDeo
|
||
for _, p := range potrazivaniSvi {
|
||
if !p.Predlozeno {
|
||
potrazivani = append(potrazivani, p)
|
||
}
|
||
}
|
||
if len(potrazivani) == 0 && nalog.Status == model.StatusCekaDelove {
|
||
// nema predlozeno=0 redova koji blokiraju — resetuj status odmah
|
||
if err := h.ServisRepo.AzurirajStatus(r.Context(), id, model.StatusPrimljeno); err != nil {
|
||
slog.Error("self-heal reset statusa (nema predlozeno=0) nije uspeo", "nalog_id", id, "error", err)
|
||
}
|
||
} else if 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 {
|
||
// 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)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// 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
|
||
}
|
||
|
||
// predviđen datum popravke: ako postoji ručni override (iz baze) koristi se,
|
||
// inače izvedeni default (datum prijema + rok iz podešavanja)
|
||
if nalog.PredvidjenDatum == nil {
|
||
nalog.PredvidjenDatum = defaultPredvidjenDatum(nalog.DatumPrijema, podesavanja)
|
||
}
|
||
|
||
// podrazumevano trajanje garancije u danima (dugme „Podrazumevano") i stanje „bez garancije" iz baze
|
||
garancijaDefault := strconv.Itoa(defaultGarancijaDana(podesavanja))
|
||
bezGarancije := nalog.GarancijaDana == nil || *nalog.GarancijaDana <= 0
|
||
|
||
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
|
||
}
|
||
}
|
||
|
||
// lista servisera za izbor u popup-u „Promeni servisera"
|
||
tehnicari, err := h.KorisniciRepo.Lista(r.Context())
|
||
if err != nil {
|
||
slog.Error("greška pri učitavanju servisera", "error", err)
|
||
}
|
||
|
||
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)
|
||
}
|
||
|
||
// razdvajamo na ugrađene/tražene (predlozeno=false) i predložene (predlozeno=true)
|
||
var ugradjeniDelovi, predlozeniDelovi []model.ServisniDeoSaArtiklom
|
||
for _, d := range delovi {
|
||
if d.Predlozeno {
|
||
predlozeniDelovi = append(predlozeniDelovi, d)
|
||
} else {
|
||
ugradjeniDelovi = append(ugradjeniDelovi, d)
|
||
}
|
||
}
|
||
var ugradjeniRadovi, predlozeniRadovi []model.ServisniRad
|
||
for _, rad := range radovi {
|
||
if rad.Predlozeno {
|
||
predlozeniRadovi = append(predlozeniRadovi, rad)
|
||
} else {
|
||
ugradjeniRadovi = append(ugradjeniRadovi, rad)
|
||
}
|
||
}
|
||
|
||
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
|
||
var ukupnoDeloviSaPdv, ukupnoRadoviSaPdv float64
|
||
for _, d := range delovi {
|
||
ukupnoDelovi += d.Ukupno()
|
||
ukupnoDeloviSaPdv += d.CenaSaPdv * float64(d.Kolicina)
|
||
}
|
||
// 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()
|
||
ukupnoRadoviSaPdv += rad.CenaSaPdv * rad.Kolicina
|
||
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
|
||
}
|
||
// ukupno sa PDV-om —suma CenaSaPdv za radove i delove
|
||
var ukupnoSveSaPdv float64
|
||
for _, rad := range radovi {
|
||
ukupnoSveSaPdv += rad.CenaSaPdv * rad.Kolicina
|
||
}
|
||
for _, d := range delovi {
|
||
ukupnoSveSaPdv += d.CenaSaPdv * float64(d.Kolicina)
|
||
}
|
||
avans := 0.0
|
||
if nalog.Avans != nil {
|
||
avans = *nalog.Avans
|
||
}
|
||
preostaloSve := ukupnoSve - avans
|
||
if preostaloSve < 0 {
|
||
preostaloSve = 0
|
||
}
|
||
preostaloSveSaPdv := ukupnoSveSaPdv - avans
|
||
if preostaloSveSaPdv < 0 {
|
||
preostaloSveSaPdv = 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,
|
||
Tehnicari: tehnicari,
|
||
GarancijaDefault: garancijaDefault,
|
||
BezGarancije: bezGarancije,
|
||
ServisniDelovi: ugradjeniDelovi,
|
||
ServisniRadovi: ugradjeniRadovi,
|
||
PredlozeniDelovi: predlozeniDelovi,
|
||
PredlozeniRadovi: predlozeniRadovi,
|
||
ImaPredlog: len(predlozeniDelovi) > 0 || len(predlozeniRadovi) > 0,
|
||
ZakljucaniUgradjeni: jePredlogStatus(nalog.Status),
|
||
Artikli: artikli,
|
||
Usluge: usluge,
|
||
Sacuvano: r.URL.Query().Get("sacuvano") == "1",
|
||
UkupnoDelovi: ukupnoDelovi,
|
||
UkupnoRadovi: ukupnoRadovi,
|
||
UkupnoDeloviSaPdv: ukupnoDeloviSaPdv,
|
||
UkupnoRadoviSaPdv: ukupnoRadoviSaPdv,
|
||
UkupnoSve: ukupnoSve,
|
||
UkupnoSveSaPdv: ukupnoSveSaPdv,
|
||
Avans: avans,
|
||
PreostaloSve: preostaloSve,
|
||
PreostaloSveSaPdv: preostaloSveSaPdv,
|
||
ZakljucanStatus: zakljucanStatus,
|
||
CenaDijagnostikePredlog: podesavanja["servis_cena_dijagnostike"],
|
||
KorisceneUsluge: korisceneUsluge,
|
||
SviStatusi: model.SviStatusi,
|
||
RokPodizanja: rokPodizanja(nalog.DatumZavrsetka),
|
||
}
|
||
|
||
// učitaj fiskalni račun ako postoji (za prikaz u detaljima)
|
||
if fr, err := h.FiskalRepo.DohvatiPoServisu(r.Context(), id); fr != nil {
|
||
podaci.FiskalniRacun = fr
|
||
} else if err != nil {
|
||
slog.Error("greška pri učitavanju fiskalnog računa za servis", "servis_id", id, "error", err)
|
||
}
|
||
// fiskalna greška: modul aktivan, nalog preuzet, ali nema fiskalnog računa
|
||
if h.modulUkljucen(r.Context(), config.ModulFiskalizacija) &&
|
||
nalog.Status == model.StatusPreuzeto &&
|
||
podaci.FiskalniRacun == nil {
|
||
podaci.FiskalGreska = true
|
||
}
|
||
|
||
h.renderujTemplate(w, "servis_detalji", podaci)
|
||
}
|
||
|
||
// jePredlogStatus vraća true ako se stavka dodata u datom statusu računa kao
|
||
// predlog servisera (sve posle prijema); dok je nalog „Primljeno" stavke su
|
||
// deo zahteva klijenta (ugrađene/tražene).
|
||
func jePredlogStatus(status string) bool {
|
||
return status != "" && status != model.StatusPrimljeno
|
||
}
|
||
|
||
// rokPodizanja vraća datum završetka + 30 dana; nil ako završetak nije postavljen.
|
||
func rokPodizanja(datumZavrsetka *time.Time) *time.Time {
|
||
if datumZavrsetka == nil {
|
||
return nil
|
||
}
|
||
t := datumZavrsetka.AddDate(0, 0, 30)
|
||
return &t
|
||
}
|
||
|
||
// 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
|
||
}
|
||
|
||
// 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.")
|
||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(nalogID, 10), http.StatusSeeOther)
|
||
return
|
||
}
|
||
|
||
// 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)
|
||
}
|
||
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\".")
|
||
}
|
||
}
|
||
|
||
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)
|
||
}
|
||
|
||
// 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
|
||
}
|
||
// ključ uključuje predlozeno: isti artikal može biti i ugrađen i predložen,
|
||
// pa se potraživane količine ne smeju mešati između te dve grupe
|
||
type kljucDela struct {
|
||
artikalID int64
|
||
predlozeno bool
|
||
}
|
||
type potrVrednost struct {
|
||
kolicina int
|
||
cena float64
|
||
}
|
||
potrMap := make(map[kljucDela]potrVrednost)
|
||
for _, p := range potrazivani {
|
||
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 v, ok := potrMap[k]; ok {
|
||
delovi[i].Potrazivano = v.kolicina
|
||
delete(potrMap, k)
|
||
}
|
||
}
|
||
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,
|
||
ArtikalSifra: sifra,
|
||
ServisniDeo: model.ServisniDeo{
|
||
ArtikalID: k.artikalID,
|
||
Kolicina: v.kolicina,
|
||
CenaKomada: v.cena,
|
||
Predlozeno: k.predlozeno,
|
||
},
|
||
})
|
||
}
|
||
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)
|
||
}
|
||
|
||
// 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 {
|
||
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
|
||
}
|
||
|
||
// 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)
|
||
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri dodavanju rada.")
|
||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(nalogID, 10), http.StatusSeeOther)
|
||
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)
|
||
}
|
||
|
||
// 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,
|
||
TrazeneNadogradnje: strings.TrimSpace(r.FormValue("trazene_nadogradnje")),
|
||
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 serviser
|
||
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
|
||
}
|
||
|
||
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: 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 ""
|
||
}
|
||
|
||
// defaultPredvidjenDatum računa predviđen rok popravke kao datum prijema + broj
|
||
// dana iz podešavanja (predvidjen_rok_dana, podrazumevano 15)
|
||
func defaultPredvidjenDatum(datumPrijema time.Time, podesavanja map[string]string) *time.Time {
|
||
dana, err := strconv.Atoi(vrednostIliDefault(podesavanja, "predvidjen_rok_dana", "15"))
|
||
if err != nil || dana <= 0 {
|
||
return nil
|
||
}
|
||
t := datumPrijema.AddDate(0, 0, dana)
|
||
return &t
|
||
}
|
||
|
||
// srpskiPlural vraća odgovarajući oblik reči za dati broj (1 / 2–4 / 5+).
|
||
func srpskiPlural(n int, jedan, par, mnogo string) string {
|
||
if n < 0 {
|
||
n = -n
|
||
}
|
||
d, dd := n%10, n%100
|
||
switch {
|
||
case d == 1 && dd != 11:
|
||
return jedan
|
||
case d >= 2 && d <= 4 && (dd < 12 || dd > 14):
|
||
return par
|
||
default:
|
||
return mnogo
|
||
}
|
||
}
|
||
|
||
// garancijaUTekst formatira trajanje garancije (u danima) na srpskom:
|
||
// 20 → „20 dana", 45 → „1 mesec i 15 dana", 60 → „2 meseca".
|
||
func garancijaUTekst(dana int) string {
|
||
if dana <= 0 {
|
||
return "bez garancije"
|
||
}
|
||
m := dana / 30
|
||
d := dana % 30
|
||
mesecRec := srpskiPlural(m, "mesec", "meseca", "meseci")
|
||
danRec := srpskiPlural(d, "dan", "dana", "dana")
|
||
switch {
|
||
case m > 0 && d > 0:
|
||
return fmt.Sprintf("%d %s i %d %s", m, mesecRec, d, danRec)
|
||
case m > 0:
|
||
return fmt.Sprintf("%d %s", m, mesecRec)
|
||
default:
|
||
return fmt.Sprintf("%d %s", d, danRec)
|
||
}
|
||
}
|
||
|
||
// garancijaTekst je šablonski helper: trajanje garancije u danima → tekst, ili „—" za prazno.
|
||
func garancijaTekst(dana *int) string {
|
||
if dana == nil || *dana <= 0 {
|
||
return "—"
|
||
}
|
||
return garancijaUTekst(*dana)
|
||
}
|
||
|
||
// defaultGarancijaDana vraća podrazumevano trajanje garancije u danima iz podešavanja
|
||
// (servis_garancija_dana, podrazumevano 60).
|
||
func defaultGarancijaDana(podesavanja map[string]string) int {
|
||
dana, err := strconv.Atoi(vrednostIliDefault(podesavanja, "servis_garancija_dana", "60"))
|
||
if err != nil || dana < 0 {
|
||
return 60
|
||
}
|
||
return dana
|
||
}
|
||
|
||
// garancijaPrePrijema vraća true ako je datum garancije raniji od datuma prijema (po danu).
|
||
// Garancija ne sme da bude pre prijema uređaja.
|
||
func garancijaPrePrijema(garancija, prijem time.Time) bool {
|
||
g := time.Date(garancija.Year(), garancija.Month(), garancija.Day(), 0, 0, 0, 0, time.UTC)
|
||
p := time.Date(prijem.Year(), prijem.Month(), prijem.Day(), 0, 0, 0, 0, time.UTC)
|
||
return g.Before(p)
|
||
}
|
||
|
||
// 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)
|
||
}
|
||
|
||
// PodaciRadnogNaloga su podaci za interni radni list servisera (bez cena)
|
||
// logoZaDokument vraća putanju loga za štampu na dokumentima — samo ako je u
|
||
// Podešavanja → Opšte uključeno „Prikaži logo" (topbar_logo_slika). Inače prazno.
|
||
func logoZaDokument(podesavanja map[string]string) string {
|
||
if podesavanja["topbar_logo_slika"] == "1" {
|
||
return podesavanja["logo_putanja"]
|
||
}
|
||
return ""
|
||
}
|
||
|
||
type PodaciRadnogNaloga struct {
|
||
Nalog model.ServisniNalog
|
||
Radovi []model.ServisniRad
|
||
ServisniDelovi []model.ServisniDeoSaArtiklom
|
||
Klijent *model.Klijent
|
||
KlijentNaziv string
|
||
TehnicarNaziv string
|
||
NazivFirme string
|
||
LogoPutanja string
|
||
Barkod string // base64 PNG Code128 barkoda broja naloga (za skener)
|
||
}
|
||
|
||
// StampaRadnogNaloga renderuje interni radni list koji serviser preuzima uz uređaj
|
||
func (h *Handler) StampaRadnogNaloga(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
|
||
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
|
||
}
|
||
}
|
||
|
||
h.renderujStandalone(w, "servis_radni_nalog", PodaciRadnogNaloga{
|
||
Nalog: *nalog,
|
||
Radovi: radovi,
|
||
ServisniDelovi: delovi,
|
||
Klijent: klijent,
|
||
KlijentNaziv: klijentNaziv,
|
||
TehnicarNaziv: tehnicarNaziv,
|
||
NazivFirme: podesavanja["naziv_firme"],
|
||
LogoPutanja: logoZaDokument(podesavanja),
|
||
Barkod: barkodNaloga(nalog.BrojNaloga),
|
||
})
|
||
}
|
||
|
||
// PodaciOtpremnice su podaci za otpremnicu pri preuzimanju uređaja
|
||
type PodaciOtpremnice struct {
|
||
Nalog model.ServisniNalog
|
||
Radovi []model.ServisniRad
|
||
ServisniDelovi []model.ServisniDeoSaArtiklom
|
||
UkupnoDelovi float64
|
||
UkupnoDeloviSaPdv float64
|
||
UkupnoSve float64
|
||
UkupnoSveSaPdv float64
|
||
PreostaloSve float64
|
||
PreostaloSveSaPdv float64
|
||
ImaAvans bool
|
||
QRKod string
|
||
Klijent *model.Klijent
|
||
KlijentNaziv string
|
||
TehnicarNaziv string
|
||
Moduli map[string]bool
|
||
NazivFirme string
|
||
LogoPutanja string
|
||
Podnazlov string
|
||
Adresa string
|
||
Telefon string
|
||
PIB string
|
||
MaticniBroj string
|
||
Barkod string // base64 PNG Code128 barkoda broja naloga
|
||
}
|
||
|
||
// 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
|
||
}
|
||
|
||
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
|
||
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
|
||
var ukupnoDeloviSaPdv float64
|
||
for _, d := range delovi {
|
||
ukupnoDelovi += d.Ukupno()
|
||
ukupnoDeloviSaPdv += d.CenaSaPdv * float64(d.Kolicina)
|
||
}
|
||
var ukupnoSve, ukupnoSveSaPdv, preostaloSve, preostaloSveSaPdv float64
|
||
var imaAvans bool
|
||
if nalog.PopravkaOdbijena {
|
||
// klijent odbio popravku — naplaćuje se samo dijagnostika
|
||
ukupnoSve = nalog.CenaDijagnostike
|
||
ukupnoSveSaPdv = nalog.CenaDijagnostike
|
||
avans := 0.0
|
||
if nalog.Avans != nil && *nalog.Avans > 0 {
|
||
avans = *nalog.Avans
|
||
imaAvans = true
|
||
}
|
||
preostaloSve = ukupnoSve - avans
|
||
if preostaloSve < 0 {
|
||
preostaloSve = 0
|
||
}
|
||
preostaloSveSaPdv = preostaloSve
|
||
} else if nalog.CenaKonacna != nil {
|
||
ukupnoSve = *nalog.CenaKonacna + ukupnoDelovi
|
||
ukupnoSveSaPdv = *nalog.CenaKonacna + ukupnoDeloviSaPdv
|
||
avans := 0.0
|
||
if nalog.Avans != nil && *nalog.Avans > 0 {
|
||
avans = *nalog.Avans
|
||
imaAvans = true
|
||
}
|
||
preostaloSve = ukupnoSve - avans
|
||
if preostaloSve < 0 {
|
||
preostaloSve = 0
|
||
}
|
||
preostaloSveSaPdv = ukupnoSveSaPdv - avans
|
||
if preostaloSveSaPdv < 0 {
|
||
preostaloSveSaPdv = 0
|
||
}
|
||
}
|
||
|
||
moduli := config.SviModuli(podesavanja)
|
||
|
||
nalogURL := qrNalogURL(r, nalog.JavniToken, podesavanja["qr_bazni_url"])
|
||
var qrKodOtpr string
|
||
if png, err := qrcode.Encode(nalogURL, qrcode.Medium, 160); err == nil {
|
||
qrKodOtpr = base64.StdEncoding.EncodeToString(png)
|
||
}
|
||
|
||
// GarancijaDo se računa od završetka, ne od prijema
|
||
if nalog.GarancijaDana == nil || *nalog.GarancijaDana <= 0 {
|
||
nalog.GarancijaDo = nil
|
||
} else {
|
||
baza := time.Now()
|
||
if nalog.DatumZavrsetka != nil {
|
||
baza = *nalog.DatumZavrsetka
|
||
}
|
||
t := baza.AddDate(0, 0, *nalog.GarancijaDana)
|
||
nalog.GarancijaDo = &t
|
||
}
|
||
|
||
h.renderujStandalone(w, "servis_otpremnica", PodaciOtpremnice{
|
||
Nalog: *nalog,
|
||
Radovi: radovi,
|
||
ServisniDelovi: delovi,
|
||
UkupnoDelovi: ukupnoDelovi,
|
||
UkupnoDeloviSaPdv: ukupnoDeloviSaPdv,
|
||
UkupnoSve: ukupnoSve,
|
||
UkupnoSveSaPdv: ukupnoSveSaPdv,
|
||
PreostaloSve: preostaloSve,
|
||
PreostaloSveSaPdv: preostaloSveSaPdv,
|
||
ImaAvans: imaAvans,
|
||
QRKod: qrKodOtpr,
|
||
Klijent: klijent,
|
||
KlijentNaziv: klijentNaziv,
|
||
TehnicarNaziv: tehnicarNaziv,
|
||
Moduli: moduli,
|
||
NazivFirme: podesavanja["naziv_firme"],
|
||
LogoPutanja: logoZaDokument(podesavanja),
|
||
Podnazlov: podesavanja["podnazlov"],
|
||
Adresa: podesavanja["adresa"],
|
||
Telefon: podesavanja["telefon"],
|
||
PIB: podesavanja["pib"],
|
||
MaticniBroj: podesavanja["maticni_broj"],
|
||
Barkod: barkodNaloga(nalog.BrojNaloga),
|
||
})
|
||
}
|
||
|
||
// PodaciGarantnog su podaci za garantni list koji se predaje klijentu pri preuzimanju.
|
||
type PodaciGarantnog struct {
|
||
Nalog model.ServisniNalog
|
||
GarancijaDo *time.Time // izračunato od DatumZavrsetka + GarancijaDana
|
||
GarancijaTekst string // npr. "60 dana"
|
||
Radovi []model.ServisniRad
|
||
ServisniDelovi []model.ServisniDeoSaArtiklom
|
||
Klijent *model.Klijent
|
||
KlijentNaziv string
|
||
TehnicarNaziv string
|
||
Uslovi string
|
||
Moduli map[string]bool
|
||
NazivFirme string
|
||
LogoPutanja string
|
||
Podnazlov string
|
||
Adresa string
|
||
Telefon string
|
||
PIB string
|
||
MaticniBroj string
|
||
Barkod string
|
||
}
|
||
|
||
// StampaGarantnog renderuje garantni list (dostupno od statusa Završeno, samo ako ima garancija)
|
||
func (h *Handler) StampaGarantnog(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
|
||
}
|
||
|
||
if nalog.GarancijaDana == nil || *nalog.GarancijaDana <= 0 {
|
||
http.Error(w, "Nalog nema definisanu garanciju", http.StatusBadRequest)
|
||
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
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
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
|
||
}
|
||
}
|
||
|
||
baza := time.Now()
|
||
if nalog.DatumZavrsetka != nil {
|
||
baza = *nalog.DatumZavrsetka
|
||
}
|
||
garanDo := baza.AddDate(0, 0, *nalog.GarancijaDana)
|
||
|
||
h.renderujStandalone(w, "servis_garantni_list", PodaciGarantnog{
|
||
Nalog: *nalog,
|
||
GarancijaDo: &garanDo,
|
||
GarancijaTekst: garancijaUTekst(*nalog.GarancijaDana),
|
||
Radovi: radovi,
|
||
ServisniDelovi: delovi,
|
||
Klijent: klijent,
|
||
KlijentNaziv: klijentNaziv,
|
||
TehnicarNaziv: tehnicarNaziv,
|
||
Uslovi: vrednostIliDefault(podesavanja, "servis_uslovi", podrazumevaniUsloviServisa),
|
||
Moduli: config.SviModuli(podesavanja),
|
||
NazivFirme: podesavanja["naziv_firme"],
|
||
LogoPutanja: logoZaDokument(podesavanja),
|
||
Podnazlov: podesavanja["podnazlov"],
|
||
Adresa: podesavanja["adresa"],
|
||
Telefon: podesavanja["telefon"],
|
||
PIB: podesavanja["pib"],
|
||
MaticniBroj: podesavanja["maticni_broj"],
|
||
Barkod: barkodNaloga(nalog.BrojNaloga),
|
||
})
|
||
}
|
||
|
||
// PodaciEskalacionogLista su podaci za interni list praćenja rokova naloga.
|
||
type PodaciEskalacionogLista struct {
|
||
Nalog model.ServisniNalog
|
||
KlijentNaziv string
|
||
Klijent *model.Klijent
|
||
TehnicarNaziv string
|
||
RokPodizanja *time.Time
|
||
NazivFirme string
|
||
LogoPutanja string
|
||
Podnazlov string
|
||
Adresa string
|
||
Telefon string
|
||
PIB string
|
||
MaticniBroj string
|
||
Barkod string
|
||
// LogDogadjaji mapa dogadjaj→vreme, npr. "status:U dijagnostici"→datum
|
||
LogDogadjaji map[string]time.Time
|
||
}
|
||
|
||
// StampaEskalacionogLista renderuje interni list praćenja rokova.
|
||
func (h *Handler) StampaEskalacionogLista(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 := ""
|
||
if nalog.KlijentID != nil {
|
||
k, e := h.KlijentiRepo.DohvatiID(r.Context(), *nalog.KlijentID)
|
||
if e == nil {
|
||
klijent = k
|
||
if k.NazivFirme != "" {
|
||
klijentNaziv = k.NazivFirme
|
||
} else {
|
||
klijentNaziv = strings.TrimSpace(k.Ime + " " + k.Prezime)
|
||
}
|
||
}
|
||
}
|
||
|
||
tehnicarNaziv := ""
|
||
if nalog.TehnicarID != nil {
|
||
if t, e := h.KorisniciRepo.DohvatiPoID(r.Context(), *nalog.TehnicarID); e == nil {
|
||
tehnicarNaziv = t.KorisnickoIme
|
||
}
|
||
}
|
||
|
||
logZapisi, _ := h.ServisniLogRepo.DohvatiZaNalog(r.Context(), id)
|
||
logDogadjaji := make(map[string]time.Time, len(logZapisi))
|
||
for _, l := range logZapisi {
|
||
// čuvamo samo prvi zapis za svaki tip događaja
|
||
if _, postoji := logDogadjaji[l.Dogadjaj]; !postoji {
|
||
logDogadjaji[l.Dogadjaj] = l.Datum
|
||
}
|
||
}
|
||
|
||
h.renderujStandalone(w, "servis_eskalacioni_list", PodaciEskalacionogLista{
|
||
Nalog: *nalog,
|
||
KlijentNaziv: klijentNaziv,
|
||
Klijent: klijent,
|
||
TehnicarNaziv: tehnicarNaziv,
|
||
RokPodizanja: rokPodizanja(nalog.DatumZavrsetka),
|
||
NazivFirme: podesavanja["naziv_firme"],
|
||
LogoPutanja: logoZaDokument(podesavanja),
|
||
Podnazlov: podesavanja["podnazlov"],
|
||
Adresa: podesavanja["adresa"],
|
||
Telefon: podesavanja["telefon"],
|
||
PIB: podesavanja["pib"],
|
||
MaticniBroj: podesavanja["maticni_broj"],
|
||
Barkod: barkodNaloga(nalog.BrojNaloga),
|
||
LogDogadjaji: logDogadjaji,
|
||
})
|
||
}
|
||
|
||
// 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
|
||
LogoPutanja string
|
||
Podnazlov string
|
||
Adresa string
|
||
Telefon string
|
||
PIB string
|
||
MaticniBroj string
|
||
Barkod string // base64 PNG Code128 barkoda broja naloga
|
||
}
|
||
|
||
// 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, podesavanja["qr_bazni_url"])
|
||
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"],
|
||
LogoPutanja: logoZaDokument(podesavanja),
|
||
Podnazlov: podesavanja["podnazlov"],
|
||
Adresa: podesavanja["adresa"],
|
||
Telefon: podesavanja["telefon"],
|
||
PIB: podesavanja["pib"],
|
||
MaticniBroj: podesavanja["maticni_broj"],
|
||
Barkod: barkodNaloga(nalog.BrojNaloga),
|
||
})
|
||
}
|
||
|
||
// 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
|
||
UkupnoRadSaPdv float64
|
||
ServisniDelovi []model.ServisniDeoSaArtiklom
|
||
UkupnoDelovi float64
|
||
UkupnoDeloviSaPdv float64
|
||
UkupnoSve float64
|
||
UkupnoSveSaPdv float64
|
||
DatumIzdavanja time.Time
|
||
VaziDo time.Time
|
||
Klauzula string
|
||
QRKod string
|
||
Klijent *model.Klijent
|
||
KlijentNaziv string
|
||
TehnicarNaziv string
|
||
Moduli map[string]bool
|
||
NazivFirme string
|
||
LogoPutanja string
|
||
Podnazlov string
|
||
Adresa string
|
||
Telefon string
|
||
PIB string
|
||
MaticniBroj string
|
||
Barkod string // base64 PNG Code128 barkoda broja naloga
|
||
}
|
||
|
||
// 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
|
||
var ukupnoRadSaPdv float64
|
||
for _, rd := range radovi {
|
||
ukupnoRad += rd.Ukupno()
|
||
ukupnoRadSaPdv += rd.CenaSaPdv * rd.Kolicina
|
||
}
|
||
var ukupnoDelovi float64
|
||
var ukupnoDeloviSaPdv float64
|
||
for _, d := range delovi {
|
||
ukupnoDelovi += d.Ukupno()
|
||
ukupnoDeloviSaPdv += d.CenaSaPdv * float64(d.Kolicina)
|
||
}
|
||
|
||
ukupnoSve := ukupnoRad + ukupnoDelovi
|
||
ukupnoSveSaPdv := ukupnoRadSaPdv + ukupnoDeloviSaPdv
|
||
moduli := config.SviModuli(podesavanja)
|
||
|
||
// 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, podesavanja["qr_bazni_url"])
|
||
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,
|
||
UkupnoRadSaPdv: ukupnoRadSaPdv,
|
||
ServisniDelovi: delovi,
|
||
UkupnoDelovi: ukupnoDelovi,
|
||
UkupnoDeloviSaPdv: ukupnoDeloviSaPdv,
|
||
UkupnoSve: ukupnoSve,
|
||
UkupnoSveSaPdv: ukupnoSveSaPdv,
|
||
DatumIzdavanja: datumIzdavanja,
|
||
VaziDo: vaziDo,
|
||
Klauzula: vrednostIliDefault(podesavanja, "servis_klauzula_predracuna", podrazumevanaKlauzulaPredracuna),
|
||
QRKod: qrKod,
|
||
Klijent: klijent,
|
||
KlijentNaziv: klijentNaziv,
|
||
TehnicarNaziv: tehnicarNaziv,
|
||
Moduli: moduli,
|
||
NazivFirme: podesavanja["naziv_firme"],
|
||
LogoPutanja: logoZaDokument(podesavanja),
|
||
Podnazlov: podesavanja["podnazlov"],
|
||
Adresa: podesavanja["adresa"],
|
||
Telefon: podesavanja["telefon"],
|
||
PIB: podesavanja["pib"],
|
||
MaticniBroj: podesavanja["maticni_broj"],
|
||
Barkod: barkodNaloga(nalog.BrojNaloga),
|
||
})
|
||
}
|
||
|
||
// PodaciNalepnice su podaci za malu nalepnicu (70×40 mm) koja se lepi na uređaj
|
||
type PodaciNalepnice struct {
|
||
Nalog model.ServisniNalog
|
||
KlijentNaziv string
|
||
Telefon string // sirov telefon klijenta (formatira se u šablonu)
|
||
DatumPrijema string // kratki format, npr. "20.06."
|
||
PredvidjenRok string // kratki format predviđenog datuma; prazno ako nije postavljen
|
||
Barkod string // base64 PNG Code128 barkoda sa brojem naloga (za laserski skener)
|
||
}
|
||
|
||
// barkodNaloga generiše Code128 barkod broja naloga kao base64 PNG; prazan string ako ne uspe
|
||
func barkodNaloga(broj string) string {
|
||
kod, err := code128.Encode(broj)
|
||
if err != nil {
|
||
return ""
|
||
}
|
||
// razvuci na fiksnu širinu/visinu radi oštrog otiska na nalepnici
|
||
skaliran, err := barcode.Scale(kod, 480, 90)
|
||
if err != nil {
|
||
return ""
|
||
}
|
||
var buf bytes.Buffer
|
||
if err := png.Encode(&buf, skaliran); err != nil {
|
||
return ""
|
||
}
|
||
return base64.StdEncoding.EncodeToString(buf.Bytes())
|
||
}
|
||
|
||
// StampaNalepnice renderuje malu nalepnicu sa brojem naloga, klijentom, uređajem i QR kodom
|
||
func (h *Handler) StampaNalepnice(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
|
||
}
|
||
|
||
klijentNaziv := ""
|
||
telefon := ""
|
||
if nalog.KlijentID != nil {
|
||
k, err := h.KlijentiRepo.DohvatiID(r.Context(), *nalog.KlijentID)
|
||
if err == nil {
|
||
if k.NazivFirme != "" {
|
||
klijentNaziv = k.NazivFirme
|
||
} else {
|
||
klijentNaziv = strings.TrimSpace(k.Ime + " " + k.Prezime)
|
||
}
|
||
telefon = k.Telefon
|
||
}
|
||
}
|
||
|
||
// kratki datumi za nalepnicu (npr. "20.06.")
|
||
datumPrijema := nalog.DatumPrijema.Format("02.01.")
|
||
predvidjenRok := ""
|
||
if nalog.PredvidjenDatum != nil {
|
||
predvidjenRok = nalog.PredvidjenDatum.Format("02.01.")
|
||
}
|
||
|
||
h.renderujStandalone(w, "servis_nalepnica", PodaciNalepnice{
|
||
Nalog: *nalog,
|
||
KlijentNaziv: klijentNaziv,
|
||
Telefon: telefon,
|
||
DatumPrijema: datumPrijema,
|
||
PredvidjenRok: predvidjenRok,
|
||
Barkod: barkodNaloga(nalog.BrojNaloga),
|
||
})
|
||
}
|
||
|
||
// 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 (predlozeno=false) —
|
||
// predloženi delovi (predlozeno=true) ne blokiraju nalog
|
||
if e == nil && trenutni.Status == model.StatusCekaDelove && noviStatus != model.StatusCekaDelove {
|
||
sviPotrazivani, err := h.ServisniPotrazivaniDeloviRepo.DohvatiZaNalog(r.Context(), id)
|
||
var blokirajuci int
|
||
if err == nil {
|
||
for _, p := range sviPotrazivani {
|
||
if !p.Predlozeno {
|
||
blokirajuci++
|
||
}
|
||
}
|
||
}
|
||
if blokirajuci > 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
|
||
}
|
||
// log događaja — best-effort, greška se ignoriše
|
||
if k := middleware.KorisnikIzKonteksta(r.Context()); k != nil {
|
||
kid := k.ID
|
||
_ = h.ServisniLogRepo.Kreiraj(r.Context(), id, "status:"+noviStatus, &kid)
|
||
} else {
|
||
_ = h.ServisniLogRepo.Kreiraj(r.Context(), id, "status:"+noviStatus, nil)
|
||
}
|
||
|
||
// pri prelasku u Završeno ili Preuzeto — auto-izračunaj garancija_do ako je postavljeno trajanje
|
||
if noviStatus == model.StatusZavrseno || noviStatus == model.StatusPreuzeto {
|
||
nalog, _ := h.ServisRepo.DohvatiID(r.Context(), id)
|
||
if nalog != nil && nalog.GarancijaDana != nil && *nalog.GarancijaDana > 0 && nalog.GarancijaDo == nil {
|
||
if nalog.DatumZavrsetka != nil {
|
||
garancijaDo := nalog.DatumZavrsetka.AddDate(0, 0, *nalog.GarancijaDana)
|
||
_ = h.ServisRepo.AzurirajGaranciju(r.Context(), id, &garancijaDo)
|
||
}
|
||
}
|
||
}
|
||
|
||
// pri prelasku u Preuzeto — auto-izračunaj cenu_konacnu ako nije uneta, pa sačuvaj naplatu i fiskalizuj
|
||
if noviStatus == model.StatusPreuzeto {
|
||
nalog, _ := h.ServisRepo.DohvatiID(r.Context(), id)
|
||
if nalog != nil && nalog.CenaKonacna == nil {
|
||
// auto-izračunaj: dijagnostika + radovi (delovi se računaju posebno)
|
||
radovi, _ := h.ServisniRadoviRepo.DohvatiZaNalog(r.Context(), id)
|
||
ukupno := nalog.CenaDijagnostike
|
||
for _, rad := range radovi {
|
||
ukupno += rad.Ukupno()
|
||
}
|
||
h.ServisRepo.AzurirajCenuKonacnu(r.Context(), id, ukupno)
|
||
nalog.CenaKonacna = &ukupno
|
||
}
|
||
nacin := strings.TrimSpace(r.FormValue("nacin_placanja"))
|
||
if nacin == "" {
|
||
nacin = "Gotovina"
|
||
}
|
||
iznosStr := strings.TrimSpace(r.FormValue("naplaceno"))
|
||
iznos := 0.0
|
||
if iznosStr != "" {
|
||
if v, e := strconv.ParseFloat(iznosStr, 64); e == nil && v > 0 {
|
||
iznos = v
|
||
}
|
||
}
|
||
if iznos == 0 {
|
||
// izračunaj iz za naplatu ako nije prosleđen
|
||
nalog, _ := h.ServisRepo.DohvatiID(r.Context(), id)
|
||
if nalog != nil {
|
||
iznos = nalog.CenaDijagnostike
|
||
if !nalog.PopravkaOdbijena && nalog.CenaKonacna != nil {
|
||
delovi, _ := h.ServisniDeloviRepo.DohvatiZaNalog(r.Context(), id)
|
||
ukupnoDelovi := 0.0
|
||
for _, d := range delovi {
|
||
ukupnoDelovi += d.Ukupno()
|
||
}
|
||
iznos = *nalog.CenaKonacna + ukupnoDelovi
|
||
}
|
||
if nalog.Avans != nil {
|
||
iznos -= *nalog.Avans
|
||
}
|
||
if iznos < 0 {
|
||
iznos = 0
|
||
}
|
||
}
|
||
}
|
||
if err := h.ServisRepo.SacuvajNaplatu(r.Context(), id, nacin, iznos); err != nil {
|
||
slog.Error("greška pri čuvanju naplate servisa", "id", id, "error", err)
|
||
middleware.SetFlash(w, r, h.DB, "greska", "Naplata nije sačuvana. Pokušajte ponovo.")
|
||
}
|
||
|
||
// fiskalizacija servisa — ako je modul uključen
|
||
if h.modulUkljucen(r.Context(), config.ModulFiskalizacija) {
|
||
klijent := h.fiskalKlijent()
|
||
if klijent != nil && iznos > 0 {
|
||
h.fiskalizujServis(r.Context(), id, klijent, nacin, iznos)
|
||
}
|
||
}
|
||
|
||
// automatski upis u KIR ako je firma PDV obveznik i servis je na klijenta (B2B/identifikovan
|
||
// kupac) — maloprodaja bez klijenta ide samo preko fiskalizacije, isto kao u Prodaji.
|
||
if h.modulUkljucen(r.Context(), "pdv") {
|
||
nalog, _ := h.ServisRepo.DohvatiID(r.Context(), id)
|
||
if nalog != nil && !nalog.PopravkaOdbijena && nalog.KlijentID != nil {
|
||
kupacNaziv, kupacPib, kupacMesto := "", "", ""
|
||
if k, e := h.KlijentiRepo.DohvatiID(r.Context(), *nalog.KlijentID); e == nil {
|
||
kupacNaziv = k.PunoIme()
|
||
kupacPib = k.PIB
|
||
if k.Tip != "pravno" {
|
||
kupacPib = k.JMBG
|
||
}
|
||
kupacMesto = k.Mesto
|
||
}
|
||
radovi, _ := h.ServisniRadoviRepo.DohvatiZaNalog(r.Context(), id)
|
||
delovi, _ := h.ServisniDeloviRepo.DohvatiZaNalog(r.Context(), id)
|
||
|
||
kir := model.PdvKir{
|
||
DatumPrometa: time.Now(),
|
||
DatumKnjizenja: time.Now(),
|
||
BrojDokumenta: nalog.BrojNaloga,
|
||
KupacNaziv: kupacNaziv,
|
||
KupacPib: kupacPib,
|
||
KupacMesto: kupacMesto,
|
||
Izvor: "servis",
|
||
IzvorID: &id,
|
||
}
|
||
// cena_komada rada/dela je NETO; PDV se dodaje naviše po stvarnoj stopi
|
||
for _, r := range radovi {
|
||
if r.Predlozeno {
|
||
continue
|
||
}
|
||
kir.DodajNeto(r.Ukupno(), r.PdvStopa)
|
||
}
|
||
for _, d := range delovi {
|
||
if d.Predlozeno {
|
||
continue
|
||
}
|
||
kir.DodajNeto(d.Ukupno(), d.PdvStopa)
|
||
}
|
||
if kir.Ukupno > 0 {
|
||
if _, e := h.PdvKirRepo.Kreiraj(r.Context(), &kir); e != nil {
|
||
slog.Error("auto-upis u KIR za servis nije uspeo", "servis_id", id, "error", e)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// auto-KPO za servisni nalog
|
||
if h.modulUkljucen(r.Context(), "kpo") && iznos > 0 {
|
||
nalogKpo, _ := h.ServisRepo.DohvatiID(r.Context(), id)
|
||
if nalogKpo != nil {
|
||
kpoZ := model.KpoZapis{
|
||
DatumPrometa: time.Now(),
|
||
BrojDokumenta: nalogKpo.BrojNaloga,
|
||
Opis: fmt.Sprintf("Servis %s", nalogKpo.BrojNaloga),
|
||
Prihod: iznos,
|
||
NacinPlacanja: nacin,
|
||
Izvor: "servis",
|
||
IzvorID: &id,
|
||
}
|
||
if _, e := h.KpoRepo.Kreiraj(r.Context(), &kpoZ); e != nil {
|
||
slog.Error("auto-upis u KPO za servis nije uspeo", "servis_id", id, "error", e)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(id, 10)+"?sacuvano=1", http.StatusSeeOther)
|
||
}
|
||
|
||
// fiskalizujServis šalje fiskalni zahtev za servisni nalog pri preuzimanju.
|
||
// Best-effort: greške se loguju, ne zaustavljaju promenu statusa.
|
||
func (h *Handler) fiskalizujServis(ctx context.Context, servisID int64, klijent *fiskal.Klijent, nacinPlacanja string, iznos float64) {
|
||
nalog, err := h.ServisRepo.DohvatiID(ctx, servisID)
|
||
if err != nil {
|
||
slog.Error("fiskalizujServis: nije pronađen nalog", "id", servisID, "error", err)
|
||
return
|
||
}
|
||
|
||
radovi, err := h.ServisniRadoviRepo.DohvatiZaNalog(ctx, servisID)
|
||
if err != nil {
|
||
slog.Error("fiskalizujServis: greška pri dohvatanju radova", "id", servisID, "error", err)
|
||
return
|
||
}
|
||
delovi, err := h.deloviSaPotrazivanima(ctx, servisID)
|
||
if err != nil {
|
||
slog.Error("fiskalizujServis: greška pri dohvatanju delova", "id", servisID, "error", err)
|
||
return
|
||
}
|
||
|
||
// napravi stavke za fiskalni račun
|
||
items := make([]fiskal.InvoiceItem, 0)
|
||
for _, r := range radovi {
|
||
if r.Predlozeno {
|
||
continue
|
||
}
|
||
items = append(items, fiskal.InvoiceItem{
|
||
Name: r.Naziv,
|
||
Labels: []string{fiskal.OznakaPDV(r.PdvStopa)},
|
||
TotalAmount: fiskal.BrutoCena(r.Ukupno(), r.PdvStopa),
|
||
UnitPrice: fiskal.BrutoCena(r.CenaKomada, r.PdvStopa),
|
||
Quantity: r.Kolicina,
|
||
})
|
||
}
|
||
for _, d := range delovi {
|
||
if d.Predlozeno {
|
||
continue
|
||
}
|
||
items = append(items, fiskal.InvoiceItem{
|
||
Name: d.ArtikalNaziv,
|
||
Labels: []string{fiskal.OznakaPDV(d.PdvStopa)},
|
||
TotalAmount: fiskal.BrutoCena(d.Ukupno(), d.PdvStopa),
|
||
UnitPrice: fiskal.BrutoCena(d.CenaKomada, d.PdvStopa),
|
||
Quantity: float64(d.Kolicina),
|
||
})
|
||
}
|
||
if len(items) == 0 {
|
||
slog.Warn("fiskalizujServis: nema stavki za fiskalni račun, zahtev odbačen", "id", servisID)
|
||
return
|
||
}
|
||
|
||
pib := ""
|
||
if nalog.KlijentID != nil {
|
||
if k, e := h.KlijentiRepo.DohvatiID(ctx, *nalog.KlijentID); e == nil {
|
||
pib = k.PIB
|
||
}
|
||
}
|
||
|
||
kasir := h.imeKasira(ctx)
|
||
|
||
zahtev := fiskal.InvoiceRequest{
|
||
InvoiceRequest: fiskal.InvoiceRequestBody{
|
||
InvoiceType: "Normal",
|
||
TransactionType: "Sale",
|
||
Payment: []fiskal.PaymentItem{
|
||
{Amount: iznos, PaymentType: fiskal.TipPlacanja(nacinPlacanja)},
|
||
},
|
||
Items: items,
|
||
Cashier: kasir,
|
||
},
|
||
}
|
||
if pib != "" {
|
||
zahtev.InvoiceRequest.BuyerID = "10:" + pib
|
||
}
|
||
|
||
odgovor, errFisk := klijent.IzdajRacun(ctx, zahtev)
|
||
if errFisk != nil {
|
||
slog.Error("fiskalizacija servisa nije uspela", "servis_id", servisID, "error", errFisk)
|
||
return
|
||
}
|
||
|
||
// sačuvaj fiskalni račun (kao i za prodaju, ali bez veze prodaja_id — koristimo negativan ID)
|
||
poreskeJSON, _ := json.Marshal(odgovor.TaxItems)
|
||
siroviJSON, _ := json.Marshal(odgovor)
|
||
fr := &model.FiskalniRacun{
|
||
ServisID: servisID,
|
||
TipRacuna: "Normal",
|
||
TipTransakcije: "Sale",
|
||
PfrBroj: odgovor.InvoiceNumber,
|
||
PfrVreme: odgovor.SdcDateTime,
|
||
Brojac: odgovor.InvoiceCounter,
|
||
EkstenzijaBrojaca: odgovor.InvoiceCounterExtension,
|
||
UrlVerifikacija: odgovor.VerificationURL,
|
||
QRKod: odgovor.VerificationQRCode,
|
||
PoreskeStavke: string(poreskeJSON),
|
||
UkupnoZaNaplatu: odgovor.TotalAmount,
|
||
UkupanPorez: odgovor.TotalTax,
|
||
SiroviOdgovor: string(siroviJSON),
|
||
Potpisao: odgovor.SignedBy,
|
||
Zatrazio: odgovor.RequestedBy,
|
||
Poruka: odgovor.Messages,
|
||
}
|
||
if _, err := h.FiskalRepo.Kreiraj(ctx, fr); err != nil {
|
||
slog.Error("greška pri čuvanju fiskalnog računa za servis", "servis_id", servisID, "error", err)
|
||
}
|
||
}
|
||
|
||
// RetryFiskalizacija pokušava ponovo da izda fiskalni račun za nalog čija je
|
||
// prethodna fiskalizacija pala. Bezbedno za ponovni poziv — proverava da račun
|
||
// još uvek ne postoji pre slanja zahteva ka ESIR/PFR.
|
||
func (h *Handler) RetryFiskalizacija(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
|
||
}
|
||
|
||
// provjeri da fiskal još nije izdat (zaštita od duplikata)
|
||
if fr, _ := h.FiskalRepo.DohvatiPoServisu(r.Context(), id); fr != nil {
|
||
middleware.SetFlash(w, r, h.DB, "uspeh", "Fiskalni račun već postoji.")
|
||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(id, 10), http.StatusSeeOther)
|
||
return
|
||
}
|
||
|
||
nalog, err := h.ServisRepo.DohvatiID(r.Context(), id)
|
||
if err != nil || nalog.Status != model.StatusPreuzeto {
|
||
http.Error(w, "Nalog nije u statusu Preuzeto", http.StatusBadRequest)
|
||
return
|
||
}
|
||
if nalog.Naplaceno <= 0 {
|
||
middleware.SetFlash(w, r, h.DB, "greska", "Iznos naplate je 0 — fiskalizacija nije moguća.")
|
||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(id, 10), http.StatusSeeOther)
|
||
return
|
||
}
|
||
|
||
klijent := h.fiskalKlijent()
|
||
if klijent == nil {
|
||
middleware.SetFlash(w, r, h.DB, "greska", "Fiskalni servis nije dostupan. Proverite vezu sa ESIR/PFR.")
|
||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(id, 10), http.StatusSeeOther)
|
||
return
|
||
}
|
||
|
||
h.fiskalizujServis(r.Context(), id, klijent, nalog.NacinPlacanja, nalog.Naplaceno)
|
||
|
||
// provjeri da li je uspelo
|
||
if fr, _ := h.FiskalRepo.DohvatiPoServisu(r.Context(), id); fr != nil {
|
||
middleware.SetFlash(w, r, h.DB, "uspeh", "Fiskalni račun izdat.")
|
||
} else {
|
||
middleware.SetFlash(w, r, h.DB, "greska", "Fiskalizacija nije uspela. Proverite vezu sa ESIR/PFR i pokušajte ponovo.")
|
||
}
|
||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(id, 10), http.StatusSeeOther)
|
||
}
|
||
|
||
func (h *Handler) StampaFiskalnog(w http.ResponseWriter, r *http.Request) {
|
||
id, err := parseID(chi.URLParam(r, "id"))
|
||
if err != nil {
|
||
http.Error(w, "Neispravan ID", http.StatusBadRequest)
|
||
return
|
||
}
|
||
fr, err := h.FiskalRepo.DohvatiPoServisu(r.Context(), id)
|
||
if err != nil || fr == nil {
|
||
http.Error(w, "Fiskalni račun nije pronađen", http.StatusNotFound)
|
||
return
|
||
}
|
||
nalog, _ := h.ServisRepo.DohvatiID(r.Context(), id)
|
||
journal := ""
|
||
if fr.SiroviOdgovor != "" {
|
||
var raw map[string]any
|
||
if json.Unmarshal([]byte(fr.SiroviOdgovor), &raw) == nil {
|
||
if j, ok := raw["journal"].(string); ok {
|
||
journal = j
|
||
}
|
||
}
|
||
}
|
||
if nalog != nil {
|
||
journal += "\n\n--- NTech servisni nalog: " + nalog.BrojNaloga + " ---\n"
|
||
}
|
||
|
||
// Podeli journal na deo pre i posle QR placeholder-a
|
||
const qrPlaceholder = "{{{{QR-KOD}}}}"
|
||
pre, post, hasQR := strings.Cut(journal, qrPlaceholder)
|
||
|
||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||
fmt.Fprint(w, `<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Fiskalni račun</title>
|
||
<style>
|
||
*{box-sizing:border-box;}
|
||
body{font-family:monospace;font-size:12px;padding:20px;max-width:max-content;margin:0 auto;}
|
||
pre{white-space:pre;margin:0;padding:0;font-family:inherit;font-size:inherit;display:block;}
|
||
@media print{body{font-size:11px;padding:10px;}}
|
||
</style></head><body>`)
|
||
|
||
fmt.Fprint(w, `<pre>`)
|
||
fmt.Fprint(w, pre)
|
||
fmt.Fprint(w, `</pre>`)
|
||
|
||
if hasQR && fr.QRKod != "" {
|
||
fmt.Fprintf(w, `<div style="margin:10px 0;"><img src="data:image/png;base64,%s" style="display:block;margin:0 auto;width:72mm;height:72mm;"></div>`, fr.QRKod)
|
||
}
|
||
|
||
fmt.Fprint(w, `<pre>`)
|
||
fmt.Fprint(w, post)
|
||
fmt.Fprint(w, `</pre>`)
|
||
|
||
fmt.Fprint(w, `<p style="margin-top:16px;"><button onclick="window.print()" style="padding:8px 16px;">Štampaj</button></p></body></html>`)
|
||
}
|
||
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
|
||
}
|
||
// ista logika kao u formi „Izmeni nalog" (parseFormuNaloga): popunjen datum se čuva,
|
||
// prazno ili „bez garancije" → bez garancije. Podrazumevanu vrednost postavlja dugme
|
||
// „Podrazumevano" eksplicitno (popuni input), pa ovde nema posebne default grane.
|
||
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
|
||
}
|
||
}
|
||
// garancija ne sme da bude pre datuma prijema
|
||
if garancijaDo != nil {
|
||
if nalog, e := h.ServisRepo.DohvatiID(r.Context(), id); e == nil && nalog != nil && garancijaPrePrijema(*garancijaDo, nalog.DatumPrijema) {
|
||
http.Error(w, "Datum garancije ne može biti pre datuma prijema.", http.StatusBadRequest)
|
||
return
|
||
}
|
||
}
|
||
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)
|
||
}
|
||
|
||
// AzurirajGarancijaDana postavlja trajanje garancije u danima (od završetka radova);
|
||
// „bez garancije" ili prazno/0 → bez garancije.
|
||
func (h *Handler) AzurirajGarancijaDana(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 dana *int
|
||
if r.FormValue("bez_garancije") != "1" {
|
||
if s := strings.TrimSpace(r.FormValue("garancija_dana")); s != "" {
|
||
n, err := strconv.Atoi(s)
|
||
if err != nil || n < 0 {
|
||
http.Error(w, "Garancija mora biti ceo broj dana.", http.StatusBadRequest)
|
||
return
|
||
}
|
||
if n > 0 {
|
||
dana = &n
|
||
}
|
||
}
|
||
}
|
||
if err := h.ServisRepo.AzurirajGarancijaDana(r.Context(), id, dana); err != nil {
|
||
slog.Error("greška pri ažuriranju garancije (dana)", "id", id, "error", err)
|
||
http.Error(w, "Greška pri ažuriranju garancije", http.StatusInternalServerError)
|
||
return
|
||
}
|
||
|
||
// ako je nalog već završen/preuzet, odmah preračunaj i garancija_do
|
||
if dana != nil && *dana > 0 {
|
||
nalog, _ := h.ServisRepo.DohvatiID(r.Context(), id)
|
||
if nalog != nil && (nalog.Status == model.StatusZavrseno || nalog.Status == model.StatusPreuzeto) {
|
||
if nalog.DatumZavrsetka != nil {
|
||
garancijaDo := nalog.DatumZavrsetka.AddDate(0, 0, *dana)
|
||
_ = h.ServisRepo.AzurirajGaranciju(r.Context(), id, &garancijaDo)
|
||
}
|
||
}
|
||
}
|
||
|
||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(id, 10), http.StatusSeeOther)
|
||
}
|
||
|
||
// AzurirajPredvidjenDatum postavlja ručni override predviđenog datuma popravke;
|
||
// prazno polje vraća nalog na izvedeni default (prijem + rok iz podešavanja).
|
||
func (h *Handler) AzurirajPredvidjenDatum(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 predvidjenDatum *time.Time
|
||
if pd := strings.TrimSpace(r.FormValue("predvidjen_datum")); pd != "" {
|
||
t, err := time.Parse("2006-01-02", pd)
|
||
if err != nil {
|
||
http.Error(w, "Neispravan predviđen datum", http.StatusBadRequest)
|
||
return
|
||
}
|
||
predvidjenDatum = &t
|
||
}
|
||
if err := h.ServisRepo.AzurirajPredvidjenDatum(r.Context(), id, predvidjenDatum); err != nil {
|
||
slog.Error("greška pri ažuriranju predviđenog datuma", "id", id, "error", err)
|
||
http.Error(w, "Greška pri ažuriranju predviđenog datuma", http.StatusInternalServerError)
|
||
return
|
||
}
|
||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(id, 10), http.StatusSeeOther)
|
||
}
|
||
|
||
// AzurirajTehnicar menja dodeljenog servisera na nalogu; prazna vrednost → nedodeljen.
|
||
func (h *Handler) AzurirajTehnicar(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 tehnicarID *int64
|
||
if t := strings.TrimSpace(r.FormValue("tehnicar_id")); t != "" {
|
||
v, err := strconv.ParseInt(t, 10, 64)
|
||
if err != nil {
|
||
http.Error(w, "Neispravan serviser", http.StatusBadRequest)
|
||
return
|
||
}
|
||
tehnicarID = &v
|
||
}
|
||
if err := h.ServisRepo.AzurirajTehnicar(r.Context(), id, tehnicarID); err != nil {
|
||
slog.Error("greška pri ažuriranju servisera", "id", id, "error", err)
|
||
http.Error(w, "Greška pri ažuriranju servisera", http.StatusInternalServerError)
|
||
return
|
||
}
|
||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(id, 10), http.StatusSeeOther)
|
||
}
|
||
|
||
// SacuvajNapomenuKlijentu ažurira napomenu namenjenu klijentu na nalogu
|
||
func (h *Handler) SacuvajNapomenuKlijentu(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("napomena_klijentu"))
|
||
if err := h.ServisRepo.AzurirajNapomenuKlijentu(r.Context(), id, tekst); err != nil {
|
||
slog.Error("greška pri ažuriranju napomene klijentu", "id", id, "error", err)
|
||
http.Error(w, "Greška pri ažuriranju napomene", http.StatusInternalServerError)
|
||
return
|
||
}
|
||
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
|
||
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)
|
||
UkupnoSveSaPdv float64
|
||
PredlozeniRadovi []model.ServisniRad
|
||
PredlozeniDelovi []model.ServisniDeoSaArtiklom
|
||
UkupnoPredlog float64 // zbir predloženih (radovi + delovi)
|
||
UkupnoPredlogSaPdv float64
|
||
ImaPredlog bool
|
||
UkupnoSaPredlogom float64 // ukupno ugrađeno + predloženo
|
||
UkupnoSaPredlogomSaPdv float64
|
||
Odgovoreno bool // klijent vec odgovorio (nema predloga, nije "Primljeno")
|
||
RokPodizanja *time.Time
|
||
Moduli map[string]bool
|
||
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
|
||
}
|
||
|
||
// javni link se „gasi" čim klijent preuzme uređaj — posle preuzimanja status više nije dostupan
|
||
if nalog.Status == model.StatusPreuzeto {
|
||
http.NotFound(w, r)
|
||
return
|
||
}
|
||
|
||
radovi, _ := h.ServisniRadoviRepo.DohvatiZaNalog(r.Context(), nalog.ID)
|
||
delovi, _ := h.deloviSaPotrazivanima(r.Context(), nalog.ID)
|
||
|
||
// razdvajamo ugrađene/odobrene od predloženih (čekaju odluku klijenta)
|
||
var ugrRadovi, predRadovi []model.ServisniRad
|
||
var ukupnoSve, ukupnoSveSaPdv, ukupnoPredlog, ukupnoPredlogSaPdv float64
|
||
for _, rd := range radovi {
|
||
if rd.Predlozeno {
|
||
predRadovi = append(predRadovi, rd)
|
||
ukupnoPredlog += rd.Ukupno()
|
||
ukupnoPredlogSaPdv += rd.UkupnoSaPdv()
|
||
} else {
|
||
ugrRadovi = append(ugrRadovi, rd)
|
||
ukupnoSve += rd.Ukupno()
|
||
ukupnoSveSaPdv += rd.UkupnoSaPdv()
|
||
}
|
||
}
|
||
var ugrDelovi, predDelovi []model.ServisniDeoSaArtiklom
|
||
for _, d := range delovi {
|
||
if d.Predlozeno {
|
||
predDelovi = append(predDelovi, d)
|
||
ukupnoPredlog += d.Ukupno()
|
||
ukupnoPredlogSaPdv += d.UkupnoSaPdv()
|
||
} else {
|
||
ugrDelovi = append(ugrDelovi, d)
|
||
ukupnoSve += d.Ukupno()
|
||
ukupnoSveSaPdv += d.UkupnoSaPdv()
|
||
}
|
||
}
|
||
|
||
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||
moduli := config.SviModuli(podesavanja)
|
||
|
||
// kad je klijent odbio popravku, ne prikazujemo radove i delove
|
||
if nalog.PopravkaOdbijena {
|
||
ugrRadovi = nil
|
||
ugrDelovi = nil
|
||
predRadovi = nil
|
||
predDelovi = nil
|
||
ukupnoSve = 0
|
||
ukupnoSveSaPdv = 0
|
||
ukupnoPredlog = 0
|
||
ukupnoPredlogSaPdv = 0
|
||
}
|
||
|
||
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,
|
||
UkupnoSveSaPdv: ukupnoSveSaPdv,
|
||
PredlozeniRadovi: predRadovi,
|
||
PredlozeniDelovi: predDelovi,
|
||
UkupnoPredlog: ukupnoPredlog,
|
||
UkupnoPredlogSaPdv: ukupnoPredlogSaPdv,
|
||
ImaPredlog: len(predRadovi) > 0 || len(predDelovi) > 0,
|
||
UkupnoSaPredlogom: ukupnoSve + ukupnoPredlog,
|
||
UkupnoSaPredlogomSaPdv: ukupnoSveSaPdv + ukupnoPredlogSaPdv,
|
||
Odgovoreno: nalog.OdlukaKlijenta != "" || (!(len(predRadovi) > 0 || len(predDelovi) > 0) && nalog.Status != model.StatusPrimljeno),
|
||
RokPodizanja: rokPodizanja(nalog.DatumZavrsetka),
|
||
Moduli: moduli,
|
||
SviStatusi: model.SviStatusi,
|
||
})
|
||
}
|
||
|
||
// qrNalogURL konstruiše URL za QR kod vodeći računa o reverse proxy-ju.
|
||
// Ako je u podešavanjima zadata fiksna adresa (bazniURL, npr. http://192.168.1.25:3000),
|
||
// koristi se ona — korisno da QR radi sa telefona kad se programu pristupa preko „localhost".
|
||
// Inače se host i šema čitaju iz zahteva (X-Forwarded-Proto pokriva nginx/Caddy/Traefik).
|
||
func qrNalogURL(r *http.Request, token, bazniURL string) string {
|
||
if b := strings.TrimRight(strings.TrimSpace(bazniURL), "/"); b != "" {
|
||
return b + "/status/" + token
|
||
}
|
||
scheme := "http"
|
||
if r.TLS != nil {
|
||
scheme = "https"
|
||
}
|
||
if r.Header.Get("X-Forwarded-Proto") == "https" {
|
||
scheme = "https"
|
||
}
|
||
return scheme + "://" + r.Host + "/status/" + token
|
||
}
|
||
|
||
// ServisJavniPrihvati obrađuje klijentovo prihvatanje predloženih stavki.
|
||
// Sve predložene stavke (radovi i delovi) prelaze u ugrađene (predlozeno 1→0).
|
||
func (h *Handler) ServisJavniPrihvati(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
|
||
}
|
||
|
||
if nalog.Status == model.StatusPreuzeto {
|
||
http.NotFound(w, r)
|
||
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)
|
||
return
|
||
}
|
||
if err := h.ServisniDeloviRepo.PrihvatiPredlozene(r.Context(), nalog.ID); err != nil {
|
||
slog.Error("greška pri prihvatanju predloženih delova", "error", err, "nalog_id", nalog.ID)
|
||
http.Error(w, "Greška pri prihvatanju predloga.", http.StatusInternalServerError)
|
||
return
|
||
}
|
||
|
||
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)
|
||
}
|
||
_ = h.ServisniLogRepo.Kreiraj(r.Context(), nalog.ID, "odluka:prihvaceno", nil)
|
||
|
||
http.Redirect(w, r, "/status/"+token, http.StatusSeeOther)
|
||
}
|
||
|
||
// ServisJavniOdlukaOdabrano obrađuje selektivno odobrenje predloga sa javne status stranice.
|
||
// Klijent čekira pojedinačne stavke; prihvaćene idu u ugrađene, ostale se brišu.
|
||
func (h *Handler) ServisJavniOdlukaOdabrano(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
|
||
}
|
||
|
||
if nalog.Status == model.StatusPreuzeto {
|
||
http.NotFound(w, r)
|
||
return
|
||
}
|
||
|
||
if nalog.OdlukaKlijenta != "" {
|
||
http.Redirect(w, r, "/status/"+token, http.StatusSeeOther)
|
||
return
|
||
}
|
||
|
||
if err := r.ParseForm(); err != nil {
|
||
http.Error(w, "Neispravan zahtev.", http.StatusBadRequest)
|
||
return
|
||
}
|
||
|
||
radIDs := parsInt64s(r.Form["rad_id"])
|
||
artikalIDs := parsInt64s(r.Form["artikal_id"])
|
||
|
||
if err := h.ServisniRadoviRepo.PrihvatiOdabrane(r.Context(), nalog.ID, radIDs); err != nil {
|
||
slog.Error("greška pri selektivnom prihvatanju radova", "error", err, "nalog_id", nalog.ID)
|
||
http.Error(w, "Greška pri obradi predloga.", http.StatusInternalServerError)
|
||
return
|
||
}
|
||
if err := h.ServisniDeloviRepo.PrihvatiOdabranePoArtiklu(r.Context(), nalog.ID, artikalIDs); err != nil {
|
||
slog.Error("greška pri selektivnom prihvatanju delova", "error", err, "nalog_id", nalog.ID)
|
||
http.Error(w, "Greška pri obradi predloga.", http.StatusInternalServerError)
|
||
return
|
||
}
|
||
|
||
komentar := strings.TrimSpace(r.FormValue("komentar"))
|
||
odluka := "prihvaceno"
|
||
if len(radIDs) == 0 && len(artikalIDs) == 0 {
|
||
odluka = "odbijeno"
|
||
}
|
||
if err := h.ServisRepo.SacuvajOdlukuKlijenta(r.Context(), nalog.ID, odluka, komentar); err != nil {
|
||
slog.Error("greška pri čuvanju odluke klijenta", "error", err, "nalog_id", nalog.ID)
|
||
}
|
||
_ = h.ServisniLogRepo.Kreiraj(r.Context(), nalog.ID, "odluka:"+odluka, nil)
|
||
|
||
http.Redirect(w, r, "/status/"+token, http.StatusSeeOther)
|
||
}
|
||
|
||
// PrihvatiOdabraniPredlog je interni handler za selektivno prihvatanje predloga
|
||
// (serviser štiklira stavke u detaljima naloga i klika "Prihvati odabrano").
|
||
func (h *Handler) PrihvatiOdabraniPredlog(w http.ResponseWriter, r *http.Request) {
|
||
_, 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, "Neispravan zahtev.", http.StatusBadRequest)
|
||
return
|
||
}
|
||
|
||
radIDs := parsInt64s(r.Form["rad_id"])
|
||
artikalIDs := parsInt64s(r.Form["artikal_id"])
|
||
|
||
if err := h.ServisniRadoviRepo.PrihvatiOdabrane(r.Context(), nalogID, radIDs); err != nil {
|
||
slog.Error("greška pri selektivnom prihvatanju radova", "error", err, "nalog_id", nalogID)
|
||
http.Error(w, "Greška pri obradi predloga.", http.StatusInternalServerError)
|
||
return
|
||
}
|
||
if err := h.ServisniDeloviRepo.PrihvatiOdabranePoArtiklu(r.Context(), nalogID, artikalIDs); err != nil {
|
||
slog.Error("greška pri selektivnom prihvatanju delova", "error", err, "nalog_id", nalogID)
|
||
http.Error(w, "Greška pri obradi predloga.", http.StatusInternalServerError)
|
||
return
|
||
}
|
||
|
||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(nalogID, 10), http.StatusSeeOther)
|
||
}
|
||
|
||
// parsInt64s konvertuje listu string vrednosti u []int64, preskačući neispravne.
|
||
func parsInt64s(vals []string) []int64 {
|
||
result := make([]int64, 0, len(vals))
|
||
for _, v := range vals {
|
||
if id, err := strconv.ParseInt(v, 10, 64); err == nil && id > 0 {
|
||
result = append(result, id)
|
||
}
|
||
}
|
||
return result
|
||
}
|
||
|
||
// ObrisiKomentarKlijenta briše poruku klijenta sa naloga
|
||
func (h *Handler) ObrisiKomentarKlijenta(w http.ResponseWriter, r *http.Request) {
|
||
_, ok := h.zahtevajDozvolu(w, r, "servis.izmeni")
|
||
if !ok {
|
||
return
|
||
}
|
||
|
||
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||
if err != nil {
|
||
http.NotFound(w, r)
|
||
return
|
||
}
|
||
|
||
if err := h.ServisRepo.AzurirajKomentarKlijenta(r.Context(), id, ""); err != nil {
|
||
slog.Error("greška pri brisanju komentara klijenta", "error", err, "nalog_id", id)
|
||
http.Error(w, "Greška pri brisanju komentara.", http.StatusInternalServerError)
|
||
return
|
||
}
|
||
|
||
http.Redirect(w, r, "/servis/"+chi.URLParam(r, "id"), http.StatusSeeOther)
|
||
}
|
||
|
||
// ServisJavniOdbij beleži da je klijent video predloge ali ih ne prihvata.
|
||
// Ne briše stavke — serviser ih ručno uklanja ako želi.
|
||
func (h *Handler) ServisJavniOdbij(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
|
||
}
|
||
|
||
if nalog.Status == model.StatusPreuzeto {
|
||
http.NotFound(w, r)
|
||
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)
|
||
}
|
||
if err := h.ServisniDeloviRepo.ObrisiPredlozene(r.Context(), nalog.ID); err != nil {
|
||
slog.Error("greška pri brisanju predloženih delova", "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)
|
||
}
|
||
_ = h.ServisniLogRepo.Kreiraj(r.Context(), nalog.ID, "odluka:odbijeno", nil)
|
||
|
||
http.Redirect(w, r, "/status/"+token, http.StatusSeeOther)
|
||
}
|