From f745f043c5313fa507b98490ba85fa24e7805394 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dalibor=20Markovi=C4=87?= Date: Tue, 23 Jun 2026 22:21:12 +0200 Subject: [PATCH] =?UTF-8?q?Servis:=20tok=20dijagnostike/popravke,=20kartic?= =?UTF-8?q?e=20Utvr=C4=91eni=20kvar=20i=20Ura=C4=91eno,=20cena=20dijagnost?= =?UTF-8?q?ike=20-=20Cena=20dijagnostike=20u=20Pode=C5=A1avanja=20?= =?UTF-8?q?=E2=86=92=20Servis;=20na=20nalogu=20se=20predla=C5=BEe=20iz=20p?= =?UTF-8?q?ode=C5=A1avanja=20-=20Odbijanje=20popravke:=20status=20Zavr?= =?UTF-8?q?=C5=A1eno=20+=20naplata=20samo=20dijagnostike=20(popravka=5Fodb?= =?UTF-8?q?ijena),=20sklanjaju=20se=20Radovi/Delovi,=20Ukupno=20=3D=20dija?= =?UTF-8?q?gnostika=20-=20Kartica=20Nalaz=20dijagnostike=20preimenovana=20?= =?UTF-8?q?u=20Utvr=C4=91eni=20kvar;=20editabilna=20samo=20u=20dijagnostic?= =?UTF-8?q?i,=20kasnije=20readonly=20-=20Nova=20kartica=20Ura=C4=91eno=20(?= =?UTF-8?q?polje=20uradjeno)=20=E2=80=94=20upis=20dok=20je=20U=20popravci/?= =?UTF-8?q?=C4=8Ceka=20delove,=20posle=20readonly=20-=20Dugmad=20statusa:?= =?UTF-8?q?=20Zavr=C5=A1i=20u=20U=20popravci;=20promena=20statusa=20osve?= =?UTF-8?q?=C5=BEava=20ceo=20sadr=C5=BEaj=20-=20Predra=C4=8Dun=20dostupan?= =?UTF-8?q?=20od=20Primljeno;=20radni=20nalog=20u=20U=20dijagnostici/?= =?UTF-8?q?=C4=8Ceka=20delove=20-=20Dropdown=20radova=20izostavlja=20ve?= =?UTF-8?q?=C4=87=20dodate=20usluge;=20kartica=20radova=20se=20osve=C5=BEa?= =?UTF-8?q?va=20pri=20izmeni=20-=20Javni=20status:=20badge=20U=20dijagnost?= =?UTF-8?q?ici,=20prikaz=20utvr=C4=91enog=20kvara,=20wordwrap=20naziva=20u?= =?UTF-8?q?=20tabeli=20-=20=C5=A0tampa:=20prelom=20izme=C4=91u=20stavki,?= =?UTF-8?q?=20ponavljanje=20zaglavlja,=20zavr=C5=A1ni=20blok=20bez=20prelo?= =?UTF-8?q?ma?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/ntech/main.go | 4 + internal/db/repository.go | 4 + internal/db/sqlite/servis.go | 66 +++++- internal/handler/podesavanja.go | 16 ++ internal/handler/servis.go | 211 +++++++++++++++--- internal/model/servis.go | 12 + migrations/073_servis_nalaz_dijagnostike.sql | 3 + migrations/074_servis_cena_dijagnostike.sql | 3 + migrations/075_servis_popravka_odbijena.sql | 3 + migrations/076_servis_uradjeno.sql | 3 + .../stranice/podesavanja_servis.html | 13 ++ web/templates/stranice/servis_detalji.html | 160 +++++++++++-- web/templates/stranice/servis_otpremnica.html | 4 + web/templates/stranice/servis_predracun.html | 55 +++-- .../stranice/servis_radni_nalog.html | 4 + web/templates/stranice/servis_revers.html | 26 +-- .../stranice/servis_status_javni.html | 13 +- 17 files changed, 494 insertions(+), 106 deletions(-) create mode 100644 migrations/073_servis_nalaz_dijagnostike.sql create mode 100644 migrations/074_servis_cena_dijagnostike.sql create mode 100644 migrations/075_servis_popravka_odbijena.sql create mode 100644 migrations/076_servis_uradjeno.sql diff --git a/cmd/ntech/main.go b/cmd/ntech/main.go index cf69870..e34c00b 100644 --- a/cmd/ntech/main.go +++ b/cmd/ntech/main.go @@ -366,6 +366,10 @@ func main() { r.With(doz("servis.izmeni")).Post("/servis/{id}/predvidjen-datum", h.AzurirajPredvidjenDatum) r.With(doz("servis.izmeni")).Post("/servis/{id}/tehnicar", h.AzurirajTehnicar) r.With(doz("servis.izmeni")).Post("/servis/{id}/napomena-klijentu", h.SacuvajNapomenuKlijentu) + r.With(doz("servis.izmeni")).Post("/servis/{id}/nalaz-dijagnostike", h.SacuvajNalazDijagnostike) + r.With(doz("servis.izmeni")).Post("/servis/{id}/uradjeno", h.SacuvajUradjeno) + r.With(doz("servis.izmeni")).Post("/servis/{id}/cena-dijagnostike", h.AzurirajCenaDijagnostike) + r.With(doz("servis.izmeni")).Post("/servis/{id}/odbij-popravku", h.OdbijPopravku) r.With(doz("servis.izmeni")).Post("/servis/{id}/delovi", h.DodajDeloNalogu) r.With(doz("servis.izmeni")).Post("/servis/{id}/delovi/{deo_id}/obrisi", h.ObrisiDeloNaloga) r.With(doz("servis.izmeni")).Post("/servis/{id}/radovi", h.DodajRadNalogu) diff --git a/internal/db/repository.go b/internal/db/repository.go index c245796..a2631c0 100644 --- a/internal/db/repository.go +++ b/internal/db/repository.go @@ -196,6 +196,10 @@ type ServisRepository interface { AzurirajPredvidjenDatum(ctx context.Context, id int64, predvidjenDatum *time.Time) error AzurirajTehnicar(ctx context.Context, id int64, tehnicarID *int64) error AzurirajNapomenuKlijentu(ctx context.Context, id int64, tekst string) error + AzurirajNalazDijagnostike(ctx context.Context, id int64, tekst string) error + AzurirajUradjeno(ctx context.Context, id int64, tekst string) error + AzurirajCenaDijagnostike(ctx context.Context, id int64, cena float64) error + OdbijPopravku(ctx context.Context, id int64, cena float64) error Obrisi(ctx context.Context, id int64, korisnikID *int64) error SledeciBroj(ctx context.Context) (string, error) } diff --git a/internal/db/sqlite/servis.go b/internal/db/sqlite/servis.go index e62cb09..3afd205 100644 --- a/internal/db/sqlite/servis.go +++ b/internal/db/sqlite/servis.go @@ -58,7 +58,7 @@ func (r *ServisRepo) Lista(ctx context.Context, pretraga, status string) ([]mode sn.id, sn.klijent_id, sn.tehnicar_id, sn.broj_naloga, sn.uredjaj, sn.serijski_broj, sn.opis_kvara, sn.trazene_nadogradnje, sn.status, sn.cena_od, sn.cena_do, sn.cena_konacna, sn.avans, sn.napomena, sn.garancija_do, sn.garancija_dana, sn.datum_prijema, sn.datum_zavrsetka, sn.predvidjen_datum, - sn.ostecenja, sn.pin_uredjaja, sn.pribor, sn.napomena_klijentu, sn.javni_token, + sn.ostecenja, sn.pin_uredjaja, sn.pribor, sn.napomena_klijentu, sn.nalaz_dijagnostike, sn.uradjeno, sn.cena_dijagnostike, sn.popravka_odbijena, sn.javni_token, COALESCE(kp.naziv, '') AS klijent_naziv FROM servisni_nalozi sn LEFT JOIN klijent_prikaz kp ON kp.id = sn.klijent_id @@ -105,7 +105,7 @@ func (r *ServisRepo) DohvatiID(ctx context.Context, id int64) (*model.ServisniNa id, klijent_id, tehnicar_id, broj_naloga, uredjaj, serijski_broj, opis_kvara, trazene_nadogradnje, status, cena_od, cena_do, cena_konacna, avans, napomena, garancija_do, garancija_dana, datum_prijema, datum_zavrsetka, predvidjen_datum, - ostecenja, pin_uredjaja, pribor, napomena_klijentu, javni_token + ostecenja, pin_uredjaja, pribor, napomena_klijentu, nalaz_dijagnostike, uradjeno, cena_dijagnostike, popravka_odbijena, javni_token FROM servisni_nalozi WHERE id = ?`, id) var n model.ServisniNalog @@ -157,7 +157,7 @@ func (r *ServisRepo) DohvatiJavniToken(ctx context.Context, token string) (*mode id, klijent_id, tehnicar_id, broj_naloga, uredjaj, serijski_broj, opis_kvara, trazene_nadogradnje, status, cena_od, cena_do, cena_konacna, avans, napomena, garancija_do, garancija_dana, datum_prijema, datum_zavrsetka, predvidjen_datum, - ostecenja, pin_uredjaja, pribor, napomena_klijentu, javni_token + ostecenja, pin_uredjaja, pribor, napomena_klijentu, nalaz_dijagnostike, uradjeno, cena_dijagnostike, popravka_odbijena, javni_token FROM servisni_nalozi WHERE javni_token = ?`, token) var n model.ServisniNalog @@ -192,13 +192,15 @@ func (r *ServisRepo) Izmeni(ctx context.Context, n *model.ServisniNalog) error { // AzurirajStatus menja samo status naloga; ako nalog prelazi u završno stanje // i datum_zavrsetka još nije postavljen, automatski ga postavlja na danas. func (r *ServisRepo) AzurirajStatus(ctx context.Context, id int64, status string) error { + // svaka „normalna" promena statusa poništava oznaku odbijene popravke + // (npr. serviser se predomisli i vrati nalog iz „Završeno" u dijagnostiku/popravku) var upit string if status == model.StatusZavrseno || status == model.StatusPreuzeto { - upit = `UPDATE servisni_nalozi SET status = ?, + upit = `UPDATE servisni_nalozi SET status = ?, popravka_odbijena = 0, datum_zavrsetka = COALESCE(datum_zavrsetka, date('now', 'localtime')) WHERE id = ?` } else { - upit = `UPDATE servisni_nalozi SET status = ? WHERE id = ?` + upit = `UPDATE servisni_nalozi SET status = ?, popravka_odbijena = 0 WHERE id = ?` } _, err := r.db.ExecContext(ctx, upit, status, id) if err != nil { @@ -207,6 +209,19 @@ func (r *ServisRepo) AzurirajStatus(ctx context.Context, id int64, status string return nil } +// OdbijPopravku označava da je klijent odbio popravku posle dijagnostike: +// nalog prelazi u „Završeno", upisuje se oznaka i cena dijagnostike (taksa za pregled). +func (r *ServisRepo) OdbijPopravku(ctx context.Context, id int64, cena float64) error { + _, err := r.db.ExecContext(ctx, `UPDATE servisni_nalozi + SET status = ?, popravka_odbijena = 1, cena_dijagnostike = ?, + datum_zavrsetka = COALESCE(datum_zavrsetka, date('now', 'localtime')) + WHERE id = ?`, model.StatusZavrseno, cena, id) + if err != nil { + return fmt.Errorf("ntech: ServisRepo.OdbijPopravku: %w", err) + } + return nil +} + // AzurirajGaranciju postavlja ili briše datum garancije na servisnom nalogu. // garancijaDo == nil → bez garancije. func (r *ServisRepo) AzurirajGaranciju(ctx context.Context, id int64, garancijaDo *time.Time) error { @@ -288,6 +303,39 @@ func (r *ServisRepo) AzurirajNapomenuKlijentu(ctx context.Context, id int64, tek return nil } +// AzurirajNalazDijagnostike postavlja tekst nalaza dijagnostike (dijagnoza kvara i predlog popravke) na nalogu +func (r *ServisRepo) AzurirajNalazDijagnostike(ctx context.Context, id int64, tekst string) error { + _, err := r.db.ExecContext(ctx, + "UPDATE servisni_nalozi SET nalaz_dijagnostike = ? WHERE id = ?", tekst, id, + ) + if err != nil { + return fmt.Errorf("ntech: ServisRepo.AzurirajNalazDijagnostike: %w", err) + } + return nil +} + +// AzurirajUradjeno čuva tekst „šta je urađeno" na servisnom nalogu +func (r *ServisRepo) AzurirajUradjeno(ctx context.Context, id int64, tekst string) error { + _, err := r.db.ExecContext(ctx, + "UPDATE servisni_nalozi SET uradjeno = ? WHERE id = ?", tekst, id, + ) + if err != nil { + return fmt.Errorf("ntech: ServisRepo.AzurirajUradjeno: %w", err) + } + return nil +} + +// AzurirajCenaDijagnostike postavlja cenu dijagnostike (taksa kad klijent ne prihvati popravku) +func (r *ServisRepo) AzurirajCenaDijagnostike(ctx context.Context, id int64, cena float64) error { + _, err := r.db.ExecContext(ctx, + "UPDATE servisni_nalozi SET cena_dijagnostike = ? WHERE id = ?", cena, id, + ) + if err != nil { + return fmt.Errorf("ntech: ServisRepo.AzurirajCenaDijagnostike: %w", err) + } + return nil +} + // Obrisi briše servisni nalog po ID-u // Obrisi briše servisni nalog i vraća ugrađene delove na stanje u magacinu (u transakciji) func (r *ServisRepo) Obrisi(ctx context.Context, id int64, korisnikID *int64) error { @@ -369,16 +417,17 @@ func (r *ServisRepo) Obrisi(ctx context.Context, id int64, korisnikID *int64) er // klijentNaziv je opcioni pokazivač, nil kada se čita bez JOIN-a func scanNalog(scan func(...any) error, n *model.ServisniNalog, klijentNaziv *string) error { var klijentID, tehnicarID sql.NullInt64 - var serijskiBroj, napomena, ostecenja, pinUredjaja, pribor, napomenaKlijentu, javniToken sql.NullString + var serijskiBroj, napomena, ostecenja, pinUredjaja, pribor, napomenaKlijentu, nalazDijagnostike, uradjeno, javniToken sql.NullString var cenaOd, cenaDo, cenaKonacna, avans sql.NullFloat64 var garancijaDo, datumZavrsetka, predvidjenDatum sql.NullTime var garancijaDana sql.NullInt64 + var popravkaOdbijena sql.NullInt64 args := []any{ &n.ID, &klijentID, &tehnicarID, &n.BrojNaloga, &n.Uredjaj, &serijskiBroj, &n.OpisKvara, &n.TrazeneNadogradnje, &n.Status, &cenaOd, &cenaDo, &cenaKonacna, &avans, &napomena, &garancijaDo, &garancijaDana, &n.DatumPrijema, &datumZavrsetka, &predvidjenDatum, - &ostecenja, &pinUredjaja, &pribor, &napomenaKlijentu, &javniToken, + &ostecenja, &pinUredjaja, &pribor, &napomenaKlijentu, &nalazDijagnostike, &uradjeno, &n.CenaDijagnostike, &popravkaOdbijena, &javniToken, } if klijentNaziv != nil { @@ -403,6 +452,8 @@ func scanNalog(scan func(...any) error, n *model.ServisniNalog, klijentNaziv *st n.PinUredjaja = pinUredjaja.String n.Pribor = pribor.String n.NapomenaKlijentu = napomenaKlijentu.String + n.NalazDijagnostike = nalazDijagnostike.String + n.Uradjeno = uradjeno.String if cenaOd.Valid { v := cenaOd.Float64 n.CenaOd = &v @@ -435,6 +486,7 @@ func scanNalog(scan func(...any) error, n *model.ServisniNalog, klijentNaziv *st v := int(garancijaDana.Int64) n.GarancijaDana = &v } + n.PopravkaOdbijena = popravkaOdbijena.Int64 != 0 n.JavniToken = javniToken.String return nil diff --git a/internal/handler/podesavanja.go b/internal/handler/podesavanja.go index 33363fc..ee9ccac 100644 --- a/internal/handler/podesavanja.go +++ b/internal/handler/podesavanja.go @@ -63,6 +63,7 @@ type PodaciPodesavanja struct { BackupBrojKopija string KalkulacijaMarza string ServisGarancijaDana string + ServisCenaDijagnostike string PredracunRokDana string PredvidjenRokDana string ServisUslovi string @@ -403,6 +404,20 @@ func (h *Handler) SacuvajPodesavanja(w http.ResponseWriter, r *http.Request) { } } + // podrazumevana cena dijagnostike (din, ≥ 0) + if v := strings.TrimSpace(r.FormValue("servis_cena_dijagnostike")); v != "" { + cena, err := strconv.ParseFloat(strings.ReplaceAll(v, ",", "."), 64) + if err != nil || cena < 0 { + middleware.SetFlash(w, r, h.DB, "greska", "Cena dijagnostike mora biti broj veći ili jednak 0.") + http.Redirect(w, r, sledeci, http.StatusSeeOther) + return + } + if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "servis_cena_dijagnostike", strconv.FormatFloat(cena, 'f', -1, 64)); err != nil { + http.Error(w, "Greška pri čuvanju podešavanja", http.StatusInternalServerError) + return + } + } + // rok važenja predračuna u danima (1–90) if v := strings.TrimSpace(r.FormValue("predracun_rok_dana")); v != "" { n, err := strconv.Atoi(v) @@ -831,6 +846,7 @@ func (h *Handler) napuniPodaciPodesavanja(r *http.Request, naslov string) (Podac BackupBrojKopija: vrednostIliDefault(podesavanja, "backup_broj_kopija", "7"), KalkulacijaMarza: vrednostIliDefault(podesavanja, "kalkulacija_marza", "20"), ServisGarancijaDana: vrednostIliDefault(podesavanja, "servis_garancija_dana", "60"), + ServisCenaDijagnostike: vrednostIliDefault(podesavanja, "servis_cena_dijagnostike", "0"), PredracunRokDana: vrednostIliDefault(podesavanja, "predracun_rok_dana", "7"), PredvidjenRokDana: vrednostIliDefault(podesavanja, "predvidjen_rok_dana", "15"), ServisUslovi: vrednostIliDefault(podesavanja, "servis_uslovi", podrazumevaniUsloviServisa), diff --git a/internal/handler/servis.go b/internal/handler/servis.go index 24a6a71..279e219 100644 --- a/internal/handler/servis.go +++ b/internal/handler/servis.go @@ -50,23 +50,25 @@ type PodaciFormeNaloga struct { // PodaciDetaljiNaloga su podaci za pregled jednog servisnog naloga type PodaciDetaljiNaloga struct { model.PodaciStranice - Nalog model.ServisniNalog - KlijentNaziv string - TehnicarNaziv string - Tehnicari []model.Korisnik - GarancijaDefault string // podrazumevana garancija (prijem + meseci), format 2006-01-02 - BezGarancije bool // u bazi nalog nema garanciju (GarancijaDo == NULL) - ServisniDelovi []model.ServisniDeoSaArtiklom - ServisniRadovi []model.ServisniRad - Artikli []model.ArtikalSaKategorijom - Usluge []model.Usluga - Sacuvano bool - UkupnoDelovi float64 - UkupnoRadovi float64 - UkupnoSve float64 - PreostaloSve float64 - ZakljucanStatus bool // onemogući promenu statusa dok ima potraživanih delova - SviStatusi []string + Nalog model.ServisniNalog + KlijentNaziv string + TehnicarNaziv string + Tehnicari []model.Korisnik + GarancijaDefault string // podrazumevana garancija (prijem + meseci), format 2006-01-02 + BezGarancije bool // u bazi nalog nema garanciju (GarancijaDo == NULL) + ServisniDelovi []model.ServisniDeoSaArtiklom + ServisniRadovi []model.ServisniRad + Artikli []model.ArtikalSaKategorijom + Usluge []model.Usluga + Sacuvano bool + UkupnoDelovi float64 + UkupnoRadovi float64 + UkupnoSve float64 + PreostaloSve float64 + ZakljucanStatus bool // onemogući promenu statusa dok ima potraživanih delova + CenaDijagnostikePredlog string // podrazumevana cena dijagnostike iz podešavanja (za prefill input-a) + KorisceneUsluge map[int64]bool // ID-evi usluga već dodatih na nalog — izostavljaju se iz dropdown-a + SviStatusi []string } // Servis renderuje listu servisnih naloga sa opcionom pretragom i filterom statusa @@ -595,11 +597,23 @@ func (h *Handler) DetaljiNaloga(w http.ResponseWriter, r *http.Request) { for _, d := range delovi { ukupnoDelovi += d.Ukupno() } + // skup već dodatih usluga — izostavljaju se iz dropdown-a da se ista usluga ne doda dvaput + korisceneUsluge := make(map[int64]bool, len(radovi)) for _, rad := range radovi { ukupnoRadovi += rad.Ukupno() + if rad.UslugaID != 0 { + korisceneUsluge[rad.UslugaID] = true + } + } + // ukupno za klijenta zavisi od ishoda dijagnostike: + // – klijent odbio popravku → naplaćuje se samo cena dijagnostike + // – inače (popravka u toku/gotova) → radovi + delovi, dijagnostika se ne gleda + var ukupnoSve float64 + if nalog.PopravkaOdbijena { + ukupnoSve = nalog.CenaDijagnostike + } else { + ukupnoSve = ukupnoRadovi + ukupnoDelovi } - // cena rada = zbir radova (usluga); ukupno za klijenta = radovi + delovi - ukupnoSve := ukupnoRadovi + ukupnoDelovi avans := 0.0 if nalog.Avans != nil { avans = *nalog.Avans @@ -621,24 +635,26 @@ func (h *Handler) DetaljiNaloga(w http.ResponseWriter, r *http.Request) { ps.Stranica = "servis" ps.NaslovStranice = "Detalji naloga" podaci := PodaciDetaljiNaloga{ - PodaciStranice: ps, - Nalog: *nalog, - KlijentNaziv: klijentNaziv, - TehnicarNaziv: tehnicarNaziv, - Tehnicari: tehnicari, - GarancijaDefault: garancijaDefault, - BezGarancije: bezGarancije, - ServisniDelovi: delovi, - ServisniRadovi: radovi, - Artikli: artikli, - Usluge: usluge, - Sacuvano: r.URL.Query().Get("sacuvano") == "1", - UkupnoDelovi: ukupnoDelovi, - UkupnoRadovi: ukupnoRadovi, - UkupnoSve: ukupnoSve, - PreostaloSve: preostaloSve, - ZakljucanStatus: zakljucanStatus, - SviStatusi: model.SviStatusi, + PodaciStranice: ps, + Nalog: *nalog, + KlijentNaziv: klijentNaziv, + TehnicarNaziv: tehnicarNaziv, + Tehnicari: tehnicari, + GarancijaDefault: garancijaDefault, + BezGarancije: bezGarancije, + ServisniDelovi: delovi, + ServisniRadovi: radovi, + Artikli: artikli, + Usluge: usluge, + Sacuvano: r.URL.Query().Get("sacuvano") == "1", + UkupnoDelovi: ukupnoDelovi, + UkupnoRadovi: ukupnoRadovi, + UkupnoSve: ukupnoSve, + PreostaloSve: preostaloSve, + ZakljucanStatus: zakljucanStatus, + CenaDijagnostikePredlog: podesavanja["servis_cena_dijagnostike"], + KorisceneUsluge: korisceneUsluge, + SviStatusi: model.SviStatusi, } h.renderujTemplate(w, "servis_detalji", podaci) @@ -1806,6 +1822,127 @@ func (h *Handler) SacuvajNapomenuKlijentu(w http.ResponseWriter, r *http.Request http.Redirect(w, r, "/servis/"+strconv.FormatInt(id, 10), http.StatusSeeOther) } +// SacuvajNalazDijagnostike obrađuje POST /servis/{id}/nalaz-dijagnostike i čuva +// dijagnozu koju je serviser utvrdio (rezultat dijagnostike, hrani predračun) +func (h *Handler) SacuvajNalazDijagnostike(w http.ResponseWriter, r *http.Request) { + if _, ok := h.zahtevajDozvolu(w, r, "servis.izmeni"); !ok { + return + } + id, err := parseID(chi.URLParam(r, "id")) + if err != nil { + http.Error(w, "Neispravan ID naloga", http.StatusBadRequest) + return + } + if err := r.ParseForm(); err != nil { + http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest) + return + } + tekst := strings.TrimSpace(r.FormValue("nalaz_dijagnostike")) + if err := h.ServisRepo.AzurirajNalazDijagnostike(r.Context(), id, tekst); err != nil { + slog.Error("greška pri ažuriranju nalaza dijagnostike", "id", id, "error", err) + http.Error(w, "Greška pri ažuriranju nalaza", http.StatusInternalServerError) + return + } + http.Redirect(w, r, "/servis/"+strconv.FormatInt(id, 10), http.StatusSeeOther) +} + +// SacuvajUradjeno obrađuje POST /servis/{id}/uradjeno i čuva tekst „šta je urađeno" +// (serviser upisuje izvršene radove tokom popravke) +func (h *Handler) SacuvajUradjeno(w http.ResponseWriter, r *http.Request) { + if _, ok := h.zahtevajDozvolu(w, r, "servis.izmeni"); !ok { + return + } + id, err := parseID(chi.URLParam(r, "id")) + if err != nil { + http.Error(w, "Neispravan ID naloga", http.StatusBadRequest) + return + } + if err := r.ParseForm(); err != nil { + http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest) + return + } + tekst := strings.TrimSpace(r.FormValue("uradjeno")) + if err := h.ServisRepo.AzurirajUradjeno(r.Context(), id, tekst); err != nil { + slog.Error("greška pri ažuriranju urađenog", "id", id, "error", err) + http.Error(w, "Greška pri čuvanju", http.StatusInternalServerError) + return + } + http.Redirect(w, r, "/servis/"+strconv.FormatInt(id, 10), http.StatusSeeOther) +} + +// AzurirajCenaDijagnostike čuva cenu dijagnostike (taksa kad klijent ne prihvati popravku); +// prazno polje ili neispravna vrednost se tretira kao 0 (ne naplaćuje se) +func (h *Handler) AzurirajCenaDijagnostike(w http.ResponseWriter, r *http.Request) { + if _, ok := h.zahtevajDozvolu(w, r, "servis.izmeni"); !ok { + return + } + id, err := parseID(chi.URLParam(r, "id")) + if err != nil { + http.Error(w, "Neispravan ID naloga", http.StatusBadRequest) + return + } + if err := r.ParseForm(); err != nil { + http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest) + return + } + cena := 0.0 + if s := strings.TrimSpace(r.FormValue("cena_dijagnostike")); s != "" { + v, e := strconv.ParseFloat(s, 64) + if e != nil || v < 0 { + http.Error(w, "Cena dijagnostike mora biti broj veći ili jednak nuli.", http.StatusBadRequest) + return + } + cena = v + } + if err := h.ServisRepo.AzurirajCenaDijagnostike(r.Context(), id, cena); err != nil { + slog.Error("greška pri ažuriranju cene dijagnostike", "id", id, "error", err) + http.Error(w, "Greška pri ažuriranju cene dijagnostike", http.StatusInternalServerError) + return + } + http.Redirect(w, r, "/servis/"+strconv.FormatInt(id, 10), http.StatusSeeOther) +} + +// OdbijPopravku obrađuje slučaj kad klijent posle dijagnostike ne prihvati popravku: +// nalog prelazi u „Završeno" i naplaćuje se samo cena dijagnostike. Cena se uzima iz +// forme, a ako je prazna koristi se podrazumevana iz podešavanja (Podešavanja → Servis). +func (h *Handler) OdbijPopravku(w http.ResponseWriter, r *http.Request) { + if _, ok := h.zahtevajDozvolu(w, r, "servis.izmeni"); !ok { + return + } + id, err := parseID(chi.URLParam(r, "id")) + if err != nil { + http.Error(w, "Neispravan ID naloga", http.StatusBadRequest) + return + } + if err := r.ParseForm(); err != nil { + http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest) + return + } + + cenaStr := strings.TrimSpace(r.FormValue("cena_dijagnostike")) + if cenaStr == "" { + // nema unete cene na nalogu → uzmi podrazumevanu iz podešavanja + podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB) + cenaStr = strings.TrimSpace(podesavanja["servis_cena_dijagnostike"]) + } + cena := 0.0 + if cenaStr != "" { + v, e := strconv.ParseFloat(strings.ReplaceAll(cenaStr, ",", "."), 64) + if e != nil || v < 0 { + http.Error(w, "Cena dijagnostike mora biti broj veći ili jednak nuli.", http.StatusBadRequest) + return + } + cena = v + } + + if err := h.ServisRepo.OdbijPopravku(r.Context(), id, cena); err != nil { + slog.Error("greška pri odbijanju popravke", "id", id, "error", err) + http.Error(w, "Greška pri promeni statusa", http.StatusInternalServerError) + return + } + http.Redirect(w, r, "/servis/"+strconv.FormatInt(id, 10)+"?sacuvano=1", http.StatusSeeOther) +} + // PodaciJavnogStatusa su podaci za javnu status stranicu servisnog naloga type PodaciJavnogStatusa struct { Nalog model.ServisniNalog diff --git a/internal/model/servis.go b/internal/model/servis.go index 084e817..a550a29 100644 --- a/internal/model/servis.go +++ b/internal/model/servis.go @@ -50,6 +50,10 @@ type ServisniNalog struct { PinUredjaja string Pribor string NapomenaKlijentu string + NalazDijagnostike string // šta je serviser utvrdio pregledom (dijagnoza + predlog popravke) + Uradjeno string // šta je serviser stvarno uradio tokom popravke + CenaDijagnostike float64 // taksa kad klijent ne prihvati popravku; 0 = ne naplaćuje se + PopravkaOdbijena bool // klijent odbio popravku posle dijagnostike → naplaćuje se samo dijagnostika JavniToken string } @@ -159,6 +163,14 @@ func (n ServisniNalog) AvansStr() string { return fmt.Sprintf("%.2f", *n.Avans) } +// CenaDijagnostikeStr vraća cenu dijagnostike za , ili prazan string kad je 0 +func (n ServisniNalog) CenaDijagnostikeStr() string { + if n.CenaDijagnostike == 0 { + return "" + } + return fmt.Sprintf("%.2f", n.CenaDijagnostike) +} + // PreostaloZaNaplatu vraća razliku konacna_cena − avans, minimum 0. // Vraća nil ako konačna cena nije uneta. func (n ServisniNalog) PreostaloZaNaplatu() *float64 { diff --git a/migrations/073_servis_nalaz_dijagnostike.sql b/migrations/073_servis_nalaz_dijagnostike.sql new file mode 100644 index 0000000..a840e6f --- /dev/null +++ b/migrations/073_servis_nalaz_dijagnostike.sql @@ -0,0 +1,3 @@ +-- nalaz dijagnostike: šta je serviser utvrdio pregledom uređaja (dijagnoza kvara +-- i predlog popravke), odvojeno od „opis kvara" koji je ono što klijent prijavi +ALTER TABLE servisni_nalozi ADD COLUMN nalaz_dijagnostike TEXT NOT NULL DEFAULT ''; diff --git a/migrations/074_servis_cena_dijagnostike.sql b/migrations/074_servis_cena_dijagnostike.sql new file mode 100644 index 0000000..65dbe29 --- /dev/null +++ b/migrations/074_servis_cena_dijagnostike.sql @@ -0,0 +1,3 @@ +-- cena dijagnostike: taksa koja se naplaćuje kada klijent ne prihvati popravku +-- posle dijagnostike; ulazi u ukupno za naplatu samo kada je veća od 0 +ALTER TABLE servisni_nalozi ADD COLUMN cena_dijagnostike REAL NOT NULL DEFAULT 0; diff --git a/migrations/075_servis_popravka_odbijena.sql b/migrations/075_servis_popravka_odbijena.sql new file mode 100644 index 0000000..656c2a0 --- /dev/null +++ b/migrations/075_servis_popravka_odbijena.sql @@ -0,0 +1,3 @@ +-- popravka_odbijena: označava nalog kod kog je klijent posle dijagnostike odbio popravku +-- (nalog ide u „Završeno", naplaćuje se samo dijagnostika, radovi i delovi se ne gledaju) +ALTER TABLE servisni_nalozi ADD COLUMN popravka_odbijena INTEGER NOT NULL DEFAULT 0; diff --git a/migrations/076_servis_uradjeno.sql b/migrations/076_servis_uradjeno.sql new file mode 100644 index 0000000..8bb95e9 --- /dev/null +++ b/migrations/076_servis_uradjeno.sql @@ -0,0 +1,3 @@ +-- uradjeno: serviser upisuje šta je stvarno urađeno na uređaju tokom popravke +-- (zaseban tekst od „nalaz_dijagnostike" koji opisuje utvrđeni kvar i predlog) +ALTER TABLE servisni_nalozi ADD COLUMN uradjeno TEXT NOT NULL DEFAULT ''; diff --git a/web/templates/stranice/podesavanja_servis.html b/web/templates/stranice/podesavanja_servis.html index 907575a..bf706e7 100644 --- a/web/templates/stranice/podesavanja_servis.html +++ b/web/templates/stranice/podesavanja_servis.html @@ -34,6 +34,19 @@ +
+ + +
+ Podrazumevana taksa za pregled/dijagnostiku. Predlaže se na nalogu pri dijagnostici; + naplaćuje se samo ako klijent ne prihvati popravku. 0 znači bez naplate. +
+
+
-
+
+ {{/* Revers je potvrda o prijemu — primaran u „Primljeno"; kasnije ostaje samo diskretan reprint */}} + {{if eq .Nalog.Status "Primljeno"}} Revers + {{else}} + + Revers (reprint) + + {{end}} + {{/* Predračun je ponuda klijentu sa procenom radova/delova — dostupan od prijema + (dijagnostika je opcioni korak; klijent često traži procenu odmah) */}} Predračun + {{/* Radni nalog je interni papir serviseru — šta treba uraditi; „U dijagnostici" i „Čeka delove" */}} + {{if or (eq .Nalog.Status "U dijagnostici") (eq .Nalog.Status "Čeka delove")}} Radni nalog + {{end}} {{if or (eq .Nalog.Status "Završeno") (eq .Nalog.Status "Preuzeto")}} Otpremnica {{end}} + {{/* Nalepnica je primarna pri prijemu (lepi se na uređaj); kasnije ostaje samo diskretan reprint */}} + {{if eq .Nalog.Status "Primljeno"}} Nalepnica + {{else}} + + Nalepnica (reprint) + + {{end}} Izmeni nalog @@ -244,6 +290,68 @@
+ + {{if ne .Nalog.Status "Primljeno"}} +
+
+ Utvrđeni kvar +
+ {{/* kvar i cena dijagnostike se unose samo dok je nalog „U dijagnostici"; kasnije su readonly */}} + {{if and (index .Dozvole "servis.izmeni") (eq .Nalog.Status "U dijagnostici")}} + +
+ + +
+ +
+ + + {{$cenaDij := .Nalog.CenaDijagnostikeStr}}{{if not $cenaDij}}{{$cenaDij = .CenaDijagnostikePredlog}}{{end}} + +
Predlog iz podešavanja; naplaćuje se samo ako klijent ne prihvati popravku.
+
+ {{else}} +
+ {{if .Nalog.NalazDijagnostike}}{{.Nalog.NalazDijagnostike}}{{else}}Nalaz još nije unet.{{end}} +
+ {{end}} +
+ {{end}} + + + {{if or (eq .Nalog.Status "U popravci") (eq .Nalog.Status "Čeka delove") (eq .Nalog.Status "Završeno") (eq .Nalog.Status "Preuzeto")}} +
+
+ Urađeno +
+ {{/* upisuje se dok traje popravka („U popravci" / „Čeka delove"); kasnije readonly */}} + {{if and (index .Dozvole "servis.izmeni") (or (eq .Nalog.Status "U popravci") (eq .Nalog.Status "Čeka delove"))}} + +
+ + +
+ {{else}} +
+ {{if .Nalog.Uradjeno}}{{.Nalog.Uradjeno}}{{else}}Još nije uneto šta je urađeno.{{end}} +
+ {{end}} +
+ {{end}} +
@@ -266,22 +374,24 @@ {{end}}
+ {{/* kad je popravka odbijena naplaćuje se samo dijagnostika — radovi i delovi se ne prikazuju */}} + {{if not .Nalog.PopravkaOdbijena}} -
+
Radovi (usluge)
{{if index .Dozvole "servis.izmeni"}} -
+
@@ -322,7 +432,7 @@ {{dinari .Ukupno}} din {{if index $.Dozvole "servis.izmeni"}} - +
+ {{end}} -
+
Ukupno
+ {{if .Nalog.PopravkaOdbijena}} + {{/* klijent odbio popravku — naplaćuje se samo dijagnostika */}} +
+ Dijagnostika + {{dinari .Nalog.CenaDijagnostike}} din +
+ {{else}}
Usluge (rad) {{dinari .UkupnoRadovi}} din @@ -451,6 +569,7 @@ Ugrađeni delovi {{dinari .UkupnoDelovi}} din
+ {{end}}
Ukupno {{dinari .UkupnoSve}} din @@ -459,6 +578,8 @@
+ {{/* Brisanje je moguće samo dok je „Primljeno" (greška pri unosu); čim uđe u proceduru naplaćuje se dijagnostika */}} + {{if eq .Nalog.Status "Primljeno"}}
@@ -475,21 +596,22 @@
+ {{end}}