Servis: tok dijagnostike/popravke, kartice Utvrđeni kvar i Urađeno, cena dijagnostike
- Cena dijagnostike u Podešavanja → Servis; na nalogu se predlaže iz podešavanja - Odbijanje popravke: status Završeno + naplata samo dijagnostike (popravka_odbijena), sklanjaju se Radovi/Delovi, Ukupno = dijagnostika - Kartica Nalaz dijagnostike preimenovana u Utvrđeni kvar; editabilna samo u dijagnostici, kasnije readonly - Nova kartica Urađeno (polje uradjeno) — upis dok je U popravci/Čeka delove, posle readonly - Dugmad statusa: Završi u U popravci; promena statusa osvežava ceo sadržaj - Predračun dostupan od Primljeno; radni nalog u U dijagnostici/Čeka delove - Dropdown radova izostavlja već dodate usluge; kartica radova se osvežava pri izmeni - Javni status: badge U dijagnostici, prikaz utvrđenog kvara, wordwrap naziva u tabeli - Štampa: prelom između stavki, ponavljanje zaglavlja, završni blok bez preloma
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
+139
-2
@@ -66,6 +66,8 @@ type PodaciDetaljiNaloga struct {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -638,6 +652,8 @@ func (h *Handler) DetaljiNaloga(w http.ResponseWriter, r *http.Request) {
|
||||
UkupnoSve: ukupnoSve,
|
||||
PreostaloSve: preostaloSve,
|
||||
ZakljucanStatus: zakljucanStatus,
|
||||
CenaDijagnostikePredlog: podesavanja["servis_cena_dijagnostike"],
|
||||
KorisceneUsluge: korisceneUsluge,
|
||||
SviStatusi: model.SviStatusi,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 <input>, 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 {
|
||||
|
||||
@@ -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 '';
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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 '';
|
||||
@@ -34,6 +34,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="max-width:280px;">
|
||||
<label for="servis_cena_dijagnostike" class="polje-labela">
|
||||
Cena dijagnostike (din)
|
||||
</label>
|
||||
<input type="number" id="servis_cena_dijagnostike" name="servis_cena_dijagnostike"
|
||||
min="0" step="0.01" value="{{.ServisCenaDijagnostike}}"
|
||||
style="width:100%;">
|
||||
<div class="pomocni-tekst" style="margin-top:6px;">
|
||||
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.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="max-width:280px;">
|
||||
<label for="predracun_rok_dana" class="polje-labela">
|
||||
Rok važenja predračuna (dana)
|
||||
|
||||
@@ -47,8 +47,8 @@
|
||||
{{template "status-badge-detalji" .Nalog.Status}}
|
||||
{{if eq .Nalog.Status "Primljeno"}}
|
||||
<!-- serviser preuzima uređaj u rad → status prelazi u „U dijagnostici";
|
||||
data-refresh osvežava samo zaglavlje (badge i ovo dugme se ponovo renderuju) -->
|
||||
<form method="POST" action="/servis/{{.Nalog.ID}}/status" data-refresh="#nalog-zaglavlje" style="margin:0;">
|
||||
promena statusa menja kartice i dugmad, pa osvežavamo ceo sadržaj (#sadrzaj) -->
|
||||
<form method="POST" action="/servis/{{.Nalog.ID}}/status" data-refresh="#sadrzaj" style="margin:0;">
|
||||
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
|
||||
<input type="hidden" name="status" value="U dijagnostici">
|
||||
<button type="submit" class="btn-sekundarno" style="padding:3px 12px;font-size:12px;">
|
||||
@@ -56,26 +56,72 @@
|
||||
</button>
|
||||
</form>
|
||||
{{end}}
|
||||
{{if eq .Nalog.Status "U dijagnostici"}}
|
||||
<!-- ishod dijagnostike: klijent odobri popravku (→ U popravci) ili ne prihvata (→ Završeno, naplata pregleda) -->
|
||||
<form method="POST" action="/servis/{{.Nalog.ID}}/status" data-refresh="#sadrzaj" style="margin:0;">
|
||||
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
|
||||
<input type="hidden" name="status" value="U popravci">
|
||||
<button type="submit" class="btn-sekundarno" style="padding:3px 12px;font-size:12px;">
|
||||
Odobri popravku
|
||||
</button>
|
||||
</form>
|
||||
<form method="POST" action="/servis/{{.Nalog.ID}}/odbij-popravku" style="margin:0;">
|
||||
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
|
||||
<button type="submit" class="btn-sekundarno" style="padding:3px 12px;font-size:12px;"
|
||||
data-potvrda="Klijent ne prihvata popravku? Nalog ide u „Završeno” i naplaćuje se samo dijagnostika.">
|
||||
Klijent ne prihvata
|
||||
</button>
|
||||
</form>
|
||||
{{end}}
|
||||
{{if eq .Nalog.Status "U popravci"}}
|
||||
<!-- popravka gotova → nalog prelazi u „Završeno" (na preuzimanje) -->
|
||||
<form method="POST" action="/servis/{{.Nalog.ID}}/status" data-refresh="#sadrzaj" style="margin:0;">
|
||||
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
|
||||
<input type="hidden" name="status" value="Završeno">
|
||||
<button type="submit" class="btn-sekundarno" style="padding:3px 12px;font-size:12px;">
|
||||
Završi
|
||||
</button>
|
||||
</form>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap;">
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;">
|
||||
{{/* Revers je potvrda o prijemu — primaran u „Primljeno"; kasnije ostaje samo diskretan reprint */}}
|
||||
{{if eq .Nalog.Status "Primljeno"}}
|
||||
<a href="/servis/{{.Nalog.ID}}/revers" target="_blank" class="btn-sekundarno">
|
||||
Revers
|
||||
</a>
|
||||
{{else}}
|
||||
<a href="/servis/{{.Nalog.ID}}/revers" target="_blank" style="font-size:12px;color:var(--tekst-sporedni);text-decoration:underline;align-self:center;">
|
||||
Revers (reprint)
|
||||
</a>
|
||||
{{end}}
|
||||
{{/* Predračun je ponuda klijentu sa procenom radova/delova — dostupan od prijema
|
||||
(dijagnostika je opcioni korak; klijent često traži procenu odmah) */}}
|
||||
<a href="/servis/{{.Nalog.ID}}/predracun" target="_blank" class="btn-sekundarno">
|
||||
Predračun
|
||||
</a>
|
||||
{{/* 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")}}
|
||||
<a href="/servis/{{.Nalog.ID}}/radni-nalog" target="_blank" class="btn-sekundarno">
|
||||
Radni nalog
|
||||
</a>
|
||||
{{end}}
|
||||
{{if or (eq .Nalog.Status "Završeno") (eq .Nalog.Status "Preuzeto")}}
|
||||
<a href="/servis/{{.Nalog.ID}}/otpremnica" target="_blank" class="btn-sekundarno">
|
||||
Otpremnica
|
||||
</a>
|
||||
{{end}}
|
||||
{{/* Nalepnica je primarna pri prijemu (lepi se na uređaj); kasnije ostaje samo diskretan reprint */}}
|
||||
{{if eq .Nalog.Status "Primljeno"}}
|
||||
<a href="/servis/{{.Nalog.ID}}/nalepnica" target="_blank" class="btn-sekundarno">
|
||||
Nalepnica
|
||||
</a>
|
||||
{{else}}
|
||||
<a href="/servis/{{.Nalog.ID}}/nalepnica" target="_blank" style="font-size:12px;color:var(--tekst-sporedni);text-decoration:underline;align-self:center;">
|
||||
Nalepnica (reprint)
|
||||
</a>
|
||||
{{end}}
|
||||
<a href="/servis/izmeni/{{.Nalog.ID}}" class="btn-primarno" hx-boost="true" hx-target="#glavni-sadrzaj" hx-select="#glavni-sadrzaj" hx-swap="outerHTML">
|
||||
Izmeni nalog
|
||||
</a>
|
||||
@@ -244,6 +290,68 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- nalaz dijagnostike: šta je serviser utvrdio (dijagnoza + predlog) — od „U dijagnostici" nadalje -->
|
||||
{{if ne .Nalog.Status "Primljeno"}}
|
||||
<div class="kartica detalji-kartica animiraj" id="nalaz-dijagnostike-kartica">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;padding-bottom:14px;border-bottom:0.5px solid var(--ivica);flex-wrap:wrap;gap:10px;">
|
||||
<span style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">Utvrđeni kvar</span>
|
||||
</div>
|
||||
{{/* 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")}}
|
||||
<!-- snima se kad textarea izgubi fokus (bez dugmeta); data-refresh osvežava samo ovu karticu -->
|
||||
<form method="POST" action="/servis/{{.Nalog.ID}}/nalaz-dijagnostike" data-refresh="#nalaz-dijagnostike-kartica">
|
||||
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
|
||||
<textarea name="nalaz_dijagnostike" rows="3"
|
||||
placeholder="Šta je serviser utvrdio pregledom — dijagnoza kvara i predlog popravke..."
|
||||
style="width:100%;resize:vertical;"
|
||||
data-poc="{{.Nalog.NalazDijagnostike}}"
|
||||
onblur="if(this.value!==this.dataset.poc){this.dataset.poc=this.value;(this.form.requestSubmit?this.form.requestSubmit():this.form.submit());}">{{.Nalog.NalazDijagnostike}}</textarea>
|
||||
</form>
|
||||
<!-- cena dijagnostike: naplaćuje se ako klijent ne prihvati popravku; snima se na promenu -->
|
||||
<form method="POST" action="/servis/{{.Nalog.ID}}/cena-dijagnostike" data-refresh="#nalaz-dijagnostike-kartica, #nalog-ukupno" style="margin-top:12px;">
|
||||
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
|
||||
<label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">Cena dijagnostike (din)</label>
|
||||
{{$cenaDij := .Nalog.CenaDijagnostikeStr}}{{if not $cenaDij}}{{$cenaDij = .CenaDijagnostikePredlog}}{{end}}
|
||||
<input type="number" name="cena_dijagnostike" min="0" step="0.01" style="max-width:200px;"
|
||||
placeholder="0"
|
||||
value="{{$cenaDij}}"
|
||||
data-poc="{{$cenaDij}}"
|
||||
onchange="if(this.value!==this.dataset.poc){this.dataset.poc=this.value;(this.form.requestSubmit?this.form.requestSubmit():this.form.submit());}">
|
||||
<div class="pomocni-tekst" style="margin-top:6px;">Predlog iz podešavanja; naplaćuje se samo ako klijent ne prihvati popravku.</div>
|
||||
</form>
|
||||
{{else}}
|
||||
<div style="font-size:13px;color:var(--tekst-sporedni);line-height:1.6;white-space:pre-wrap;">
|
||||
{{if .Nalog.NalazDijagnostike}}{{.Nalog.NalazDijagnostike}}{{else}}<span style="color:var(--tekst-sporedni);font-style:italic;">Nalaz još nije unet.</span>{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<!-- urađeno: šta je serviser stvarno uradio — od „U popravci" nadalje -->
|
||||
{{if or (eq .Nalog.Status "U popravci") (eq .Nalog.Status "Čeka delove") (eq .Nalog.Status "Završeno") (eq .Nalog.Status "Preuzeto")}}
|
||||
<div class="kartica detalji-kartica animiraj" id="uradjeno-kartica">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;padding-bottom:14px;border-bottom:0.5px solid var(--ivica);flex-wrap:wrap;gap:10px;">
|
||||
<span style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">Urađeno</span>
|
||||
</div>
|
||||
{{/* 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"))}}
|
||||
<!-- snima se kad textarea izgubi fokus (bez dugmeta); data-refresh osvežava samo ovu karticu -->
|
||||
<form method="POST" action="/servis/{{.Nalog.ID}}/uradjeno" data-refresh="#uradjeno-kartica">
|
||||
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
|
||||
<textarea name="uradjeno" rows="3"
|
||||
placeholder="Šta je serviser uradio na uređaju tokom popravke..."
|
||||
style="width:100%;resize:vertical;"
|
||||
data-poc="{{.Nalog.Uradjeno}}"
|
||||
onblur="if(this.value!==this.dataset.poc){this.dataset.poc=this.value;(this.form.requestSubmit?this.form.requestSubmit():this.form.submit());}">{{.Nalog.Uradjeno}}</textarea>
|
||||
</form>
|
||||
{{else}}
|
||||
<div style="font-size:13px;color:var(--tekst-sporedni);line-height:1.6;white-space:pre-wrap;">
|
||||
{{if .Nalog.Uradjeno}}{{.Nalog.Uradjeno}}{{else}}<span style="color:var(--tekst-sporedni);font-style:italic;">Još nije uneto šta je urađeno.</span>{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<!-- napomena klijentu -->
|
||||
<div class="kartica detalji-kartica animiraj" id="napomena-klijentu-kartica">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;padding-bottom:14px;border-bottom:0.5px solid var(--ivica);flex-wrap:wrap;gap:10px;">
|
||||
@@ -266,22 +374,24 @@
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{/* kad je popravka odbijena naplaćuje se samo dijagnostika — radovi i delovi se ne prikazuju */}}
|
||||
{{if not .Nalog.PopravkaOdbijena}}
|
||||
<!-- radovi (usluge) -->
|
||||
<div class="kartica detalji-kartica animiraj">
|
||||
<div class="kartica detalji-kartica animiraj" id="radovi-kartica">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;padding-bottom:14px;border-bottom:0.5px solid var(--ivica);flex-wrap:wrap;gap:10px;">
|
||||
<span style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">Radovi (usluge)</span>
|
||||
</div>
|
||||
|
||||
<!-- forma za dodavanje rada iz cenovnika usluga -->
|
||||
{{if index .Dozvole "servis.izmeni"}}
|
||||
<form method="POST" action="/servis/{{.Nalog.ID}}/radovi" style="margin-bottom:16px;" data-refresh="#radovi-spisak">
|
||||
<form method="POST" action="/servis/{{.Nalog.ID}}/radovi" style="margin-bottom:16px;" data-refresh="#radovi-kartica, #nalog-ukupno">
|
||||
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
|
||||
<div class="detalji-forma-rad">
|
||||
<div>
|
||||
<label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">Usluga</label>
|
||||
<select name="usluga_id" id="rad-usluga" style="width:100%;" required>
|
||||
<option value="">— odaberi uslugu —</option>
|
||||
{{range .Usluge}}<option value="{{.ID}}" data-cena="{{.Cena}}">{{.Naziv}}</option>{{end}}
|
||||
{{range .Usluge}}{{if not (index $.KorisceneUsluge .ID)}}<option value="{{.ID}}" data-cena="{{.Cena}}">{{.Naziv}}</option>{{end}}{{end}}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
@@ -322,7 +432,7 @@
|
||||
<td style="padding:9px 10px;text-align:right;font-size:14px;font-weight:500;color:var(--tekst-glavni);">{{dinari .Ukupno}} din</td>
|
||||
<td style="padding:9px 10px;text-align:center;">
|
||||
{{if index $.Dozvole "servis.izmeni"}}
|
||||
<form method="POST" action="/servis/{{$.Nalog.ID}}/radovi/{{.ID}}/obrisi" style="display:inline;" data-refresh="#radovi-spisak">
|
||||
<form method="POST" action="/servis/{{$.Nalog.ID}}/radovi/{{.ID}}/obrisi" style="display:inline;" data-refresh="#radovi-kartica, #nalog-ukupno">
|
||||
<input type="hidden" name="_csrf" value="{{$.CsrfToken}}">
|
||||
<button type="submit"
|
||||
data-potvrda="Ukloniti rad '{{.Naziv}}'?"
|
||||
@@ -353,7 +463,7 @@
|
||||
|
||||
<!-- forma za dodavanje dela -->
|
||||
{{if index .Dozvole "servis.izmeni"}}
|
||||
<form method="POST" action="/servis/{{.Nalog.ID}}/delovi" style="margin-bottom:16px;" data-refresh="#delovi-spisak, #nalog-zaglavlje">
|
||||
<form method="POST" action="/servis/{{.Nalog.ID}}/delovi" style="margin-bottom:16px;" data-refresh="#delovi-spisak, #nalog-zaglavlje, #nalog-ukupno">
|
||||
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
|
||||
<div style="margin-bottom:10px;">
|
||||
<label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">Kategorija</label>
|
||||
@@ -414,7 +524,7 @@
|
||||
</td>
|
||||
<td style="padding:9px 10px;text-align:center;">
|
||||
{{if and .ID (index $.Dozvole "servis.izmeni")}}
|
||||
<form method="POST" action="/servis/{{$.Nalog.ID}}/delovi/{{.ID}}/obrisi" style="display:inline;" data-refresh="#delovi-spisak, #nalog-zaglavlje">
|
||||
<form method="POST" action="/servis/{{$.Nalog.ID}}/delovi/{{.ID}}/obrisi" style="display:inline;" data-refresh="#delovi-spisak, #nalog-zaglavlje, #nalog-ukupno">
|
||||
<input type="hidden" name="_csrf" value="{{$.CsrfToken}}">
|
||||
<button type="submit"
|
||||
data-potvrda="Ukloniti deo '{{.ArtikalNaziv}}'?"
|
||||
@@ -436,13 +546,21 @@
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<!-- ukupno: zbir usluga (radova) i ugrađenih delova -->
|
||||
<div class="kartica detalji-kartica animiraj">
|
||||
<div id="nalog-ukupno" class="kartica detalji-kartica animiraj">
|
||||
<div style="margin-bottom:16px;padding-bottom:14px;border-bottom:0.5px solid var(--ivica);">
|
||||
<span style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">Ukupno</span>
|
||||
</div>
|
||||
<div class="kolona" style="gap:10px;">
|
||||
{{if .Nalog.PopravkaOdbijena}}
|
||||
{{/* klijent odbio popravku — naplaćuje se samo dijagnostika */}}
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;">
|
||||
<span style="font-size:14px;color:var(--tekst-sporedni);">Dijagnostika</span>
|
||||
<span style="font-size:14px;color:var(--tekst-glavni);">{{dinari .Nalog.CenaDijagnostike}} din</span>
|
||||
</div>
|
||||
{{else}}
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;">
|
||||
<span style="font-size:14px;color:var(--tekst-sporedni);">Usluge (rad)</span>
|
||||
<span style="font-size:14px;color:var(--tekst-glavni);">{{dinari .UkupnoRadovi}} din</span>
|
||||
@@ -451,6 +569,7 @@
|
||||
<span style="font-size:14px;color:var(--tekst-sporedni);">Ugrađeni delovi</span>
|
||||
<span style="font-size:14px;color:var(--tekst-glavni);">{{dinari .UkupnoDelovi}} din</span>
|
||||
</div>
|
||||
{{end}}
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:6px;padding-top:12px;border-top:0.5px solid var(--ivica);">
|
||||
<span style="font-size:16px;font-weight:600;color:var(--tekst-glavni);">Ukupno</span>
|
||||
<span style="font-size:18px;font-weight:600;color:var(--tekst-glavni);">{{dinari .UkupnoSve}} din</span>
|
||||
@@ -459,6 +578,8 @@
|
||||
</div>
|
||||
|
||||
<!-- zona za brisanje -->
|
||||
{{/* 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"}}
|
||||
<div class="kartica detalji-kartica animiraj" style="border-color:#dc262633;">
|
||||
<div style="display:flex;align-items:flex-start;gap:12px;flex-wrap:wrap;">
|
||||
<div style="flex:1;min-width:200px;">
|
||||
@@ -475,21 +596,22 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Radovi: pri izboru usluge iz <select>-a popuni predloženu cenu (data-cena).
|
||||
(function () {
|
||||
var sel = document.getElementById('rad-usluga');
|
||||
// Delegacija na document — kartica radova se osvežava AJAX-om (data-refresh),
|
||||
// pa listener vezan za sam <select> ne bi preživeo zamenu elementa.
|
||||
document.addEventListener('change', function (e) {
|
||||
if (!e.target || e.target.id !== 'rad-usluga') return;
|
||||
var cena = document.getElementById('rad-cena');
|
||||
if (!sel || !cena) return;
|
||||
sel.addEventListener('change', function () {
|
||||
var opt = sel.options[sel.selectedIndex];
|
||||
if (!cena) return;
|
||||
var opt = e.target.options[e.target.selectedIndex];
|
||||
var c = parseFloat(opt.getAttribute('data-cena'));
|
||||
cena.value = (!isNaN(c) && c > 0) ? c.toFixed(2) : '0';
|
||||
});
|
||||
})();
|
||||
});
|
||||
|
||||
// Delovi: kategorija filtrira <select> artikala; izborom artikla popunjavamo
|
||||
// cenu i prikaz „U magacinu". Običan <select> ima native pretragu kucanjem
|
||||
|
||||
@@ -69,6 +69,10 @@
|
||||
.strana { padding: 16px 20px; max-width: 100%; }
|
||||
.dugme-stampa { display: none !important; }
|
||||
@page { size: A4; margin: 12mm 14mm; }
|
||||
/* red tabele se ne sme prelomiti preko granice stranice — prelom pada između stavki */
|
||||
.tabela tr { break-inside: avoid; }
|
||||
/* zaglavlje tabele se ponavlja na svakoj stranici kad tabela pređe na novu */
|
||||
.tabela thead { display: table-header-group; }
|
||||
}
|
||||
.dugme-stampa { position: fixed; bottom: 24px; right: 24px; background: #111; color: #fff; border: none; padding: 12px 22px; border-radius: 8px; font-size: 14px; font-weight: 500; cursor: pointer; box-shadow: 0 4px 12px rgba(0,0,0,0.2); }
|
||||
.dugme-stampa:hover { background: #333; }
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
.strana { max-width: 780px; margin: 0 auto; padding: 32px 40px; }
|
||||
|
||||
.zaglavlje { display: flex; align-items: stretch; margin-bottom: 18px; gap: 12px; }
|
||||
.zg-okvir { flex: 1; padding: 12px 14px; border: 0.5px solid #ddd; border-radius: 6px; }
|
||||
.zg-okvir { flex: 1; padding: 12px 14px; border: 1.5px solid #bbb; border-radius: 12px; }
|
||||
.firma-naziv { font-size: 18px; font-weight: 700; }
|
||||
.firma-info { font-size: 11px; color: #555; margin-top: 3px; line-height: 1.5; }
|
||||
.dok-tip { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 1px; color: #555; margin-bottom: 4px; }
|
||||
@@ -55,10 +55,13 @@
|
||||
@page { size: A4; margin: 12mm 14mm; }
|
||||
/* red tabele se ne sme prelomiti preko granice stranice */
|
||||
.tabela tr { break-inside: avoid; }
|
||||
/* „Ukupno radovi/delovi" se ne sme odvojiti od stavki iznad — bez preloma pre njega */
|
||||
.tabela .ukupno-red { break-before: avoid; }
|
||||
.tabela .ukupno-red td { break-before: avoid; }
|
||||
/* zaglavlje tabele se ponavlja na svakoj stranici kad tabela pređe na novu */
|
||||
.tabela thead { display: table-header-group; }
|
||||
/* rekapitulacija, klauzula i potpisi ostaju u jednom komadu */
|
||||
.rekap, .klauzula-blok, .potpisi { break-inside: avoid; }
|
||||
/* od „Ukupno (procena)" do potpisa sve ostaje na jednoj strani — bez preloma */
|
||||
.zavrsni-blok { break-inside: avoid; }
|
||||
}
|
||||
.dugme-stampa { position: fixed; bottom: 24px; right: 24px; background: #111; color: #fff; border: none; padding: 12px 22px; border-radius: 8px; font-size: 14px; font-weight: 500; cursor: pointer; box-shadow: 0 4px 12px rgba(0,0,0,0.2); }
|
||||
.dugme-stampa:hover { background: #333; }
|
||||
@@ -83,7 +86,19 @@
|
||||
{{if .PIB}}PIB: {{.PIB}}{{end}}{{if .MaticniBroj}}{{if .PIB}} · {{end}}MB: {{.MaticniBroj}}{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<!-- SREDINA: klijent -->
|
||||
<!-- SREDINA: datumi (levo) + QR (skroz desno) -->
|
||||
<div class="zg-okvir" style="flex:1.3;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px;">
|
||||
<div>
|
||||
<div class="dok-tip">Predračun (procena)</div>
|
||||
<div class="dok-broj">{{.Nalog.BrojNaloga}}</div>
|
||||
<div class="dok-datum">Datum: {{.DatumIzdavanja.Format "02.01.2006."}}</div>
|
||||
<div class="dok-datum">Važi do: {{.VaziDo.Format "02.01.2006."}}</div>
|
||||
</div>
|
||||
{{if .QRKod}}<img src="data:image/png;base64,{{.QRKod}}" width="92" height="92" alt="QR {{.Nalog.BrojNaloga}}" style="image-rendering:pixelated;border:1px solid #ddd;border-radius:4px;flex-shrink:0;">{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<!-- DESNO: klijent -->
|
||||
<div class="zg-okvir">
|
||||
<div class="strana-tip">Klijent</div>
|
||||
{{if .KlijentNaziv}}
|
||||
@@ -100,19 +115,6 @@
|
||||
<div class="strana-naziv prazno">— klijent nije naveden —</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<!-- DESNO: barkod na vrhu, ispod datumi (levo) + QR (skroz desno) -->
|
||||
<div class="zg-okvir" style="flex:1.3;">
|
||||
{{if .Barkod}}<img src="data:image/png;base64,{{.Barkod}}" alt="Barkod {{.Nalog.BrojNaloga}}" style="display:block;width:100%;max-height:46px;object-fit:contain;margin-bottom:10px;">{{end}}
|
||||
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px;">
|
||||
<div>
|
||||
<div class="dok-tip">Predračun (procena)</div>
|
||||
<div class="dok-broj">{{.Nalog.BrojNaloga}}</div>
|
||||
<div class="dok-datum">Datum: {{.DatumIzdavanja.Format "02.01.2006."}}</div>
|
||||
<div class="dok-datum">Važi do: {{.VaziDo.Format "02.01.2006."}}</div>
|
||||
</div>
|
||||
{{if .QRKod}}<img src="data:image/png;base64,{{.QRKod}}" width="64" height="64" alt="QR {{.Nalog.BrojNaloga}}" style="image-rendering:pixelated;border:1px solid #ddd;border-radius:4px;flex-shrink:0;">{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- uređaj i traženo -->
|
||||
@@ -144,10 +146,6 @@
|
||||
<td class="desno">{{dinari .Ukupno}} din</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
<tr class="ukupno-red">
|
||||
<td colspan="3">Ukupno radovi</td>
|
||||
<td class="desno">{{dinari .UkupnoRad}} din</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -175,10 +173,6 @@
|
||||
<td class="desno">{{dinari .Ukupno}} din</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
<tr class="ukupno-red">
|
||||
<td colspan="3">Ukupno delovi</td>
|
||||
<td class="desno">{{dinari .UkupnoDelovi}} din</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -190,10 +184,13 @@
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<!-- rekapitulacija -->
|
||||
<!-- završni blok: od rekapitulacije do potpisa — pri štampi ostaje u jednom komadu -->
|
||||
<div class="zavrsni-blok">
|
||||
|
||||
<!-- rekapitulacija: ponavlja sume po sekciji (korisno kad tabele pređu na prethodnu stranu) -->
|
||||
<div class="rekap">
|
||||
{{if .Radovi}}<div class="rekap-red"><span>Radovi</span><span>{{dinari .UkupnoRad}} din</span></div>{{end}}
|
||||
{{if .ServisniDelovi}}<div class="rekap-red"><span>Delovi i materijal</span><span>{{dinari .UkupnoDelovi}} din</span></div>{{end}}
|
||||
{{if .Radovi}}<div class="rekap-red" style="font-weight:600;"><span>Ukupno radovi</span><span>{{dinari .UkupnoRad}} din</span></div>{{end}}
|
||||
{{if .ServisniDelovi}}<div class="rekap-red" style="font-weight:600;"><span>Ukupno delovi</span><span>{{dinari .UkupnoDelovi}} din</span></div>{{end}}
|
||||
<div class="rekap-ukupno">
|
||||
<div class="rekap-ukupno-labela">Ukupno (procena):</div>
|
||||
<div class="rekap-ukupno-iznos">{{dinari .UkupnoSve}} din</div>
|
||||
@@ -217,6 +214,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /zavrsni-blok -->
|
||||
|
||||
</div>
|
||||
|
||||
<button class="dugme-stampa" onclick="window.print()">Štampaj</button>
|
||||
|
||||
@@ -59,6 +59,10 @@
|
||||
.strana { padding: 16px 20px; max-width: 100%; }
|
||||
.dugme-stampa { display: none !important; }
|
||||
@page { size: A4; margin: 12mm 14mm; }
|
||||
/* red tabele se ne sme prelomiti preko granice stranice — prelom pada između stavki */
|
||||
.tabela tr { break-inside: avoid; }
|
||||
/* zaglavlje tabele se ponavlja na svakoj stranici kad tabela pređe na novu */
|
||||
.tabela thead { display: table-header-group; }
|
||||
}
|
||||
.dugme-stampa { position: fixed; bottom: 24px; right: 24px; background: #111; color: #fff; border: none; padding: 12px 22px; border-radius: 8px; font-size: 14px; font-weight: 500; cursor: pointer; box-shadow: 0 4px 12px rgba(0,0,0,0.2); }
|
||||
.dugme-stampa:hover { background: #333; }
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
/* zaglavlje: tri okvira (firma · klijent · dokument) */
|
||||
.zaglavlje { display: flex; align-items: stretch; margin-bottom: 20px; gap: 12px; }
|
||||
.zg-okvir { flex: 1; padding: 12px 14px; border: 0.5px solid #ddd; border-radius: 6px; }
|
||||
.zg-okvir { flex: 1; padding: 12px 14px; border: 1.5px solid #bbb; border-radius: 12px; }
|
||||
.firma-naziv { font-size: 18px; font-weight: 700; }
|
||||
.firma-info { font-size: 11px; color: #555; margin-top: 4px; line-height: 1.7; }
|
||||
.dok-tip { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 1px; color: #555; margin-bottom: 4px; }
|
||||
@@ -83,7 +83,17 @@
|
||||
{{if .PIB}}PIB: {{.PIB}}{{end}}{{if .MaticniBroj}}{{if .PIB}} · {{end}}MB: {{.MaticniBroj}}{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<!-- SREDINA: klijent -->
|
||||
<!-- SREDINA: barkod na vrhu, ispod dokument i datumi (QR je u sekciji „Uređaj na servisu") -->
|
||||
<div class="zg-okvir" style="flex:1.3;">
|
||||
{{if .Barkod}}<img src="data:image/png;base64,{{.Barkod}}" alt="Barkod {{.Nalog.BrojNaloga}}" style="display:block;width:100%;max-height:46px;object-fit:contain;margin-bottom:10px;">{{end}}
|
||||
<div class="dok-tip">Revers — potvrda o prijemu</div>
|
||||
<div class="dok-broj">{{.Nalog.BrojNaloga}}</div>
|
||||
<div class="dok-datum">Datum prijema: {{.Nalog.DatumPrijema.Format "02.01.2006."}}</div>
|
||||
{{if .Nalog.PredvidjenDatum}}
|
||||
<div class="dok-datum">Predviđen rok: {{.Nalog.PredvidjenDatum.Format "02.01.2006."}}</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<!-- DESNO: klijent -->
|
||||
<div class="zg-okvir">
|
||||
<div class="strana-tip">Klijent</div>
|
||||
{{if .KlijentNaziv}}
|
||||
@@ -100,16 +110,6 @@
|
||||
<div class="strana-naziv prazno">— klijent nije naveden —</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<!-- DESNO: barkod na vrhu, ispod dokument i datumi (QR je u sekciji „Uređaj na servisu") -->
|
||||
<div class="zg-okvir" style="flex:1.3;">
|
||||
{{if .Barkod}}<img src="data:image/png;base64,{{.Barkod}}" alt="Barkod {{.Nalog.BrojNaloga}}" style="display:block;width:100%;max-height:46px;object-fit:contain;margin-bottom:10px;">{{end}}
|
||||
<div class="dok-tip">Revers — potvrda o prijemu</div>
|
||||
<div class="dok-broj">{{.Nalog.BrojNaloga}}</div>
|
||||
<div class="dok-datum">Datum prijema: {{.Nalog.DatumPrijema.Format "02.01.2006."}}</div>
|
||||
{{if .Nalog.PredvidjenDatum}}
|
||||
<div class="dok-datum">Predviđen rok: {{.Nalog.PredvidjenDatum.Format "02.01.2006."}}</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- uređaj (QR skroz desno — klijent ga skenira da prati status) -->
|
||||
@@ -154,7 +154,7 @@
|
||||
</div>
|
||||
{{if .QRKod}}
|
||||
<div style="text-align:center;flex-shrink:0;">
|
||||
<img src="data:image/png;base64,{{.QRKod}}" width="96" height="96" alt="QR {{.Nalog.BrojNaloga}}"
|
||||
<img src="data:image/png;base64,{{.QRKod}}" width="128" height="128" alt="QR {{.Nalog.BrojNaloga}}"
|
||||
style="image-rendering:pixelated;border:1px solid #ddd;border-radius:4px;">
|
||||
<div style="font-size:9px;color:#888;margin-top:3px;">Skenirajte za status</div>
|
||||
</div>
|
||||
|
||||
@@ -55,8 +55,8 @@
|
||||
/* red podataka */
|
||||
.red { display: flex; justify-content: space-between; align-items: flex-start; padding: 10px 0; border-bottom: 0.5px solid #f0f0f0; gap: 12px; }
|
||||
.red:last-child { border-bottom: none; }
|
||||
.red-labela { font-size: 12px; color: #888; flex-shrink: 0; }
|
||||
.red-vrednost { font-size: 13px; color: #111; font-weight: 500; text-align: right; }
|
||||
.red-labela { font-size: 12px; color: #888; min-width: 0; overflow-wrap: anywhere; }
|
||||
.red-vrednost { font-size: 13px; color: #111; font-weight: 500; text-align: right; flex-shrink: 0; white-space: nowrap; }
|
||||
|
||||
/* cena */
|
||||
.cena-blok { background: #f9fafb; border-radius: 10px; padding: 14px 16px; margin-top: 4px; }
|
||||
@@ -105,6 +105,8 @@
|
||||
{{$s := .Nalog.Status}}
|
||||
{{if eq $s "Primljeno"}}
|
||||
<span class="status-bedz status-primljeno">Primljeno</span>
|
||||
{{else if eq $s "U dijagnostici"}}
|
||||
<span class="status-bedz status-dijagnostika">U dijagnostici</span>
|
||||
{{else if eq $s "Čeka delove"}}
|
||||
<span class="status-bedz status-ceka">Čeka delove</span>
|
||||
{{else if eq $s "U popravci"}}
|
||||
@@ -153,6 +155,13 @@
|
||||
<span class="red-vrednost">{{garancijaTekst .Nalog.GarancijaDana}}</span>
|
||||
</div>
|
||||
|
||||
{{if .Nalog.NalazDijagnostike}}
|
||||
<div style="margin-top:12px;">
|
||||
<div class="red-labela" style="margin-bottom:6px;">Utvrđeno na dijagnostici</div>
|
||||
<div class="napomena-blok">{{.Nalog.NalazDijagnostike}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .Nalog.NapomenaKlijentu}}
|
||||
<div style="margin-top:12px;">
|
||||
<div class="red-labela" style="margin-bottom:6px;">Napomena</div>
|
||||
|
||||
Reference in New Issue
Block a user