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}/predvidjen-datum", h.AzurirajPredvidjenDatum)
|
||||||
r.With(doz("servis.izmeni")).Post("/servis/{id}/tehnicar", h.AzurirajTehnicar)
|
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}/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", h.DodajDeloNalogu)
|
||||||
r.With(doz("servis.izmeni")).Post("/servis/{id}/delovi/{deo_id}/obrisi", h.ObrisiDeloNaloga)
|
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)
|
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
|
AzurirajPredvidjenDatum(ctx context.Context, id int64, predvidjenDatum *time.Time) error
|
||||||
AzurirajTehnicar(ctx context.Context, id int64, tehnicarID *int64) error
|
AzurirajTehnicar(ctx context.Context, id int64, tehnicarID *int64) error
|
||||||
AzurirajNapomenuKlijentu(ctx context.Context, id int64, tekst string) 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
|
Obrisi(ctx context.Context, id int64, korisnikID *int64) error
|
||||||
SledeciBroj(ctx context.Context) (string, 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.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.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.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
|
COALESCE(kp.naziv, '') AS klijent_naziv
|
||||||
FROM servisni_nalozi sn
|
FROM servisni_nalozi sn
|
||||||
LEFT JOIN klijent_prikaz kp ON kp.id = sn.klijent_id
|
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,
|
id, klijent_id, tehnicar_id, broj_naloga, uredjaj, serijski_broj,
|
||||||
opis_kvara, trazene_nadogradnje, status, cena_od, cena_do, cena_konacna,
|
opis_kvara, trazene_nadogradnje, status, cena_od, cena_do, cena_konacna,
|
||||||
avans, napomena, garancija_do, garancija_dana, datum_prijema, datum_zavrsetka, predvidjen_datum,
|
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)
|
FROM servisni_nalozi WHERE id = ?`, id)
|
||||||
|
|
||||||
var n model.ServisniNalog
|
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,
|
id, klijent_id, tehnicar_id, broj_naloga, uredjaj, serijski_broj,
|
||||||
opis_kvara, trazene_nadogradnje, status, cena_od, cena_do, cena_konacna,
|
opis_kvara, trazene_nadogradnje, status, cena_od, cena_do, cena_konacna,
|
||||||
avans, napomena, garancija_do, garancija_dana, datum_prijema, datum_zavrsetka, predvidjen_datum,
|
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)
|
FROM servisni_nalozi WHERE javni_token = ?`, token)
|
||||||
|
|
||||||
var n model.ServisniNalog
|
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
|
// AzurirajStatus menja samo status naloga; ako nalog prelazi u završno stanje
|
||||||
// i datum_zavrsetka još nije postavljen, automatski ga postavlja na danas.
|
// i datum_zavrsetka još nije postavljen, automatski ga postavlja na danas.
|
||||||
func (r *ServisRepo) AzurirajStatus(ctx context.Context, id int64, status string) error {
|
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
|
var upit string
|
||||||
if status == model.StatusZavrseno || status == model.StatusPreuzeto {
|
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'))
|
datum_zavrsetka = COALESCE(datum_zavrsetka, date('now', 'localtime'))
|
||||||
WHERE id = ?`
|
WHERE id = ?`
|
||||||
} else {
|
} 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)
|
_, err := r.db.ExecContext(ctx, upit, status, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -207,6 +209,19 @@ func (r *ServisRepo) AzurirajStatus(ctx context.Context, id int64, status string
|
|||||||
return nil
|
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.
|
// AzurirajGaranciju postavlja ili briše datum garancije na servisnom nalogu.
|
||||||
// garancijaDo == nil → bez garancije.
|
// garancijaDo == nil → bez garancije.
|
||||||
func (r *ServisRepo) AzurirajGaranciju(ctx context.Context, id int64, garancijaDo *time.Time) error {
|
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
|
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 po ID-u
|
||||||
// Obrisi briše servisni nalog i vraća ugrađene delove na stanje u magacinu (u transakciji)
|
// 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 {
|
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
|
// klijentNaziv je opcioni pokazivač, nil kada se čita bez JOIN-a
|
||||||
func scanNalog(scan func(...any) error, n *model.ServisniNalog, klijentNaziv *string) error {
|
func scanNalog(scan func(...any) error, n *model.ServisniNalog, klijentNaziv *string) error {
|
||||||
var klijentID, tehnicarID sql.NullInt64
|
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 cenaOd, cenaDo, cenaKonacna, avans sql.NullFloat64
|
||||||
var garancijaDo, datumZavrsetka, predvidjenDatum sql.NullTime
|
var garancijaDo, datumZavrsetka, predvidjenDatum sql.NullTime
|
||||||
var garancijaDana sql.NullInt64
|
var garancijaDana sql.NullInt64
|
||||||
|
var popravkaOdbijena sql.NullInt64
|
||||||
|
|
||||||
args := []any{
|
args := []any{
|
||||||
&n.ID, &klijentID, &tehnicarID, &n.BrojNaloga, &n.Uredjaj, &serijskiBroj,
|
&n.ID, &klijentID, &tehnicarID, &n.BrojNaloga, &n.Uredjaj, &serijskiBroj,
|
||||||
&n.OpisKvara, &n.TrazeneNadogradnje, &n.Status, &cenaOd, &cenaDo, &cenaKonacna,
|
&n.OpisKvara, &n.TrazeneNadogradnje, &n.Status, &cenaOd, &cenaDo, &cenaKonacna,
|
||||||
&avans, &napomena, &garancijaDo, &garancijaDana, &n.DatumPrijema, &datumZavrsetka, &predvidjenDatum,
|
&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 {
|
if klijentNaziv != nil {
|
||||||
@@ -403,6 +452,8 @@ func scanNalog(scan func(...any) error, n *model.ServisniNalog, klijentNaziv *st
|
|||||||
n.PinUredjaja = pinUredjaja.String
|
n.PinUredjaja = pinUredjaja.String
|
||||||
n.Pribor = pribor.String
|
n.Pribor = pribor.String
|
||||||
n.NapomenaKlijentu = napomenaKlijentu.String
|
n.NapomenaKlijentu = napomenaKlijentu.String
|
||||||
|
n.NalazDijagnostike = nalazDijagnostike.String
|
||||||
|
n.Uradjeno = uradjeno.String
|
||||||
if cenaOd.Valid {
|
if cenaOd.Valid {
|
||||||
v := cenaOd.Float64
|
v := cenaOd.Float64
|
||||||
n.CenaOd = &v
|
n.CenaOd = &v
|
||||||
@@ -435,6 +486,7 @@ func scanNalog(scan func(...any) error, n *model.ServisniNalog, klijentNaziv *st
|
|||||||
v := int(garancijaDana.Int64)
|
v := int(garancijaDana.Int64)
|
||||||
n.GarancijaDana = &v
|
n.GarancijaDana = &v
|
||||||
}
|
}
|
||||||
|
n.PopravkaOdbijena = popravkaOdbijena.Int64 != 0
|
||||||
n.JavniToken = javniToken.String
|
n.JavniToken = javniToken.String
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ type PodaciPodesavanja struct {
|
|||||||
BackupBrojKopija string
|
BackupBrojKopija string
|
||||||
KalkulacijaMarza string
|
KalkulacijaMarza string
|
||||||
ServisGarancijaDana string
|
ServisGarancijaDana string
|
||||||
|
ServisCenaDijagnostike string
|
||||||
PredracunRokDana string
|
PredracunRokDana string
|
||||||
PredvidjenRokDana string
|
PredvidjenRokDana string
|
||||||
ServisUslovi 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)
|
// rok važenja predračuna u danima (1–90)
|
||||||
if v := strings.TrimSpace(r.FormValue("predracun_rok_dana")); v != "" {
|
if v := strings.TrimSpace(r.FormValue("predracun_rok_dana")); v != "" {
|
||||||
n, err := strconv.Atoi(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"),
|
BackupBrojKopija: vrednostIliDefault(podesavanja, "backup_broj_kopija", "7"),
|
||||||
KalkulacijaMarza: vrednostIliDefault(podesavanja, "kalkulacija_marza", "20"),
|
KalkulacijaMarza: vrednostIliDefault(podesavanja, "kalkulacija_marza", "20"),
|
||||||
ServisGarancijaDana: vrednostIliDefault(podesavanja, "servis_garancija_dana", "60"),
|
ServisGarancijaDana: vrednostIliDefault(podesavanja, "servis_garancija_dana", "60"),
|
||||||
|
ServisCenaDijagnostike: vrednostIliDefault(podesavanja, "servis_cena_dijagnostike", "0"),
|
||||||
PredracunRokDana: vrednostIliDefault(podesavanja, "predracun_rok_dana", "7"),
|
PredracunRokDana: vrednostIliDefault(podesavanja, "predracun_rok_dana", "7"),
|
||||||
PredvidjenRokDana: vrednostIliDefault(podesavanja, "predvidjen_rok_dana", "15"),
|
PredvidjenRokDana: vrednostIliDefault(podesavanja, "predvidjen_rok_dana", "15"),
|
||||||
ServisUslovi: vrednostIliDefault(podesavanja, "servis_uslovi", podrazumevaniUsloviServisa),
|
ServisUslovi: vrednostIliDefault(podesavanja, "servis_uslovi", podrazumevaniUsloviServisa),
|
||||||
|
|||||||
+139
-2
@@ -66,6 +66,8 @@ type PodaciDetaljiNaloga struct {
|
|||||||
UkupnoSve float64
|
UkupnoSve float64
|
||||||
PreostaloSve float64
|
PreostaloSve float64
|
||||||
ZakljucanStatus bool // onemogući promenu statusa dok ima potraživanih delova
|
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
|
SviStatusi []string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -595,11 +597,23 @@ func (h *Handler) DetaljiNaloga(w http.ResponseWriter, r *http.Request) {
|
|||||||
for _, d := range delovi {
|
for _, d := range delovi {
|
||||||
ukupnoDelovi += d.Ukupno()
|
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 {
|
for _, rad := range radovi {
|
||||||
ukupnoRadovi += rad.Ukupno()
|
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
|
avans := 0.0
|
||||||
if nalog.Avans != nil {
|
if nalog.Avans != nil {
|
||||||
avans = *nalog.Avans
|
avans = *nalog.Avans
|
||||||
@@ -638,6 +652,8 @@ func (h *Handler) DetaljiNaloga(w http.ResponseWriter, r *http.Request) {
|
|||||||
UkupnoSve: ukupnoSve,
|
UkupnoSve: ukupnoSve,
|
||||||
PreostaloSve: preostaloSve,
|
PreostaloSve: preostaloSve,
|
||||||
ZakljucanStatus: zakljucanStatus,
|
ZakljucanStatus: zakljucanStatus,
|
||||||
|
CenaDijagnostikePredlog: podesavanja["servis_cena_dijagnostike"],
|
||||||
|
KorisceneUsluge: korisceneUsluge,
|
||||||
SviStatusi: model.SviStatusi,
|
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)
|
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
|
// PodaciJavnogStatusa su podaci za javnu status stranicu servisnog naloga
|
||||||
type PodaciJavnogStatusa struct {
|
type PodaciJavnogStatusa struct {
|
||||||
Nalog model.ServisniNalog
|
Nalog model.ServisniNalog
|
||||||
|
|||||||
@@ -50,6 +50,10 @@ type ServisniNalog struct {
|
|||||||
PinUredjaja string
|
PinUredjaja string
|
||||||
Pribor string
|
Pribor string
|
||||||
NapomenaKlijentu 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
|
JavniToken string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,6 +163,14 @@ func (n ServisniNalog) AvansStr() string {
|
|||||||
return fmt.Sprintf("%.2f", *n.Avans)
|
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.
|
// PreostaloZaNaplatu vraća razliku konacna_cena − avans, minimum 0.
|
||||||
// Vraća nil ako konačna cena nije uneta.
|
// Vraća nil ako konačna cena nije uneta.
|
||||||
func (n ServisniNalog) PreostaloZaNaplatu() *float64 {
|
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>
|
</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;">
|
<div style="max-width:280px;">
|
||||||
<label for="predracun_rok_dana" class="polje-labela">
|
<label for="predracun_rok_dana" class="polje-labela">
|
||||||
Rok važenja predračuna (dana)
|
Rok važenja predračuna (dana)
|
||||||
|
|||||||
@@ -47,8 +47,8 @@
|
|||||||
{{template "status-badge-detalji" .Nalog.Status}}
|
{{template "status-badge-detalji" .Nalog.Status}}
|
||||||
{{if eq .Nalog.Status "Primljeno"}}
|
{{if eq .Nalog.Status "Primljeno"}}
|
||||||
<!-- serviser preuzima uređaj u rad → status prelazi u „U dijagnostici";
|
<!-- serviser preuzima uređaj u rad → status prelazi u „U dijagnostici";
|
||||||
data-refresh osvežava samo zaglavlje (badge i ovo dugme se ponovo renderuju) -->
|
promena statusa menja kartice i dugmad, pa osvežavamo ceo sadržaj (#sadrzaj) -->
|
||||||
<form method="POST" action="/servis/{{.Nalog.ID}}/status" data-refresh="#nalog-zaglavlje" style="margin:0;">
|
<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="_csrf" value="{{.CsrfToken}}">
|
||||||
<input type="hidden" name="status" value="U dijagnostici">
|
<input type="hidden" name="status" value="U dijagnostici">
|
||||||
<button type="submit" class="btn-sekundarno" style="padding:3px 12px;font-size:12px;">
|
<button type="submit" class="btn-sekundarno" style="padding:3px 12px;font-size:12px;">
|
||||||
@@ -56,26 +56,72 @@
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
{{end}}
|
{{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>
|
</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">
|
<a href="/servis/{{.Nalog.ID}}/revers" target="_blank" class="btn-sekundarno">
|
||||||
Revers
|
Revers
|
||||||
</a>
|
</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">
|
<a href="/servis/{{.Nalog.ID}}/predracun" target="_blank" class="btn-sekundarno">
|
||||||
Predračun
|
Predračun
|
||||||
</a>
|
</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">
|
<a href="/servis/{{.Nalog.ID}}/radni-nalog" target="_blank" class="btn-sekundarno">
|
||||||
Radni nalog
|
Radni nalog
|
||||||
</a>
|
</a>
|
||||||
|
{{end}}
|
||||||
{{if or (eq .Nalog.Status "Završeno") (eq .Nalog.Status "Preuzeto")}}
|
{{if or (eq .Nalog.Status "Završeno") (eq .Nalog.Status "Preuzeto")}}
|
||||||
<a href="/servis/{{.Nalog.ID}}/otpremnica" target="_blank" class="btn-sekundarno">
|
<a href="/servis/{{.Nalog.ID}}/otpremnica" target="_blank" class="btn-sekundarno">
|
||||||
Otpremnica
|
Otpremnica
|
||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{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">
|
<a href="/servis/{{.Nalog.ID}}/nalepnica" target="_blank" class="btn-sekundarno">
|
||||||
Nalepnica
|
Nalepnica
|
||||||
</a>
|
</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">
|
<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
|
Izmeni nalog
|
||||||
</a>
|
</a>
|
||||||
@@ -244,6 +290,68 @@
|
|||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- napomena klijentu -->
|
||||||
<div class="kartica detalji-kartica animiraj" id="napomena-klijentu-kartica">
|
<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;">
|
<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}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{/* kad je popravka odbijena naplaćuje se samo dijagnostika — radovi i delovi se ne prikazuju */}}
|
||||||
|
{{if not .Nalog.PopravkaOdbijena}}
|
||||||
<!-- radovi (usluge) -->
|
<!-- 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;">
|
<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>
|
<span style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">Radovi (usluge)</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- forma za dodavanje rada iz cenovnika usluga -->
|
<!-- forma za dodavanje rada iz cenovnika usluga -->
|
||||||
{{if index .Dozvole "servis.izmeni"}}
|
{{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}}">
|
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
|
||||||
<div class="detalji-forma-rad">
|
<div class="detalji-forma-rad">
|
||||||
<div>
|
<div>
|
||||||
<label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">Usluga</label>
|
<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>
|
<select name="usluga_id" id="rad-usluga" style="width:100%;" required>
|
||||||
<option value="">— odaberi uslugu —</option>
|
<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>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<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:right;font-size:14px;font-weight:500;color:var(--tekst-glavni);">{{dinari .Ukupno}} din</td>
|
||||||
<td style="padding:9px 10px;text-align:center;">
|
<td style="padding:9px 10px;text-align:center;">
|
||||||
{{if index $.Dozvole "servis.izmeni"}}
|
{{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}}">
|
<input type="hidden" name="_csrf" value="{{$.CsrfToken}}">
|
||||||
<button type="submit"
|
<button type="submit"
|
||||||
data-potvrda="Ukloniti rad '{{.Naziv}}'?"
|
data-potvrda="Ukloniti rad '{{.Naziv}}'?"
|
||||||
@@ -353,7 +463,7 @@
|
|||||||
|
|
||||||
<!-- forma za dodavanje dela -->
|
<!-- forma za dodavanje dela -->
|
||||||
{{if index .Dozvole "servis.izmeni"}}
|
{{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}}">
|
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
|
||||||
<div style="margin-bottom:10px;">
|
<div style="margin-bottom:10px;">
|
||||||
<label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">Kategorija</label>
|
<label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">Kategorija</label>
|
||||||
@@ -414,7 +524,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td style="padding:9px 10px;text-align:center;">
|
<td style="padding:9px 10px;text-align:center;">
|
||||||
{{if and .ID (index $.Dozvole "servis.izmeni")}}
|
{{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}}">
|
<input type="hidden" name="_csrf" value="{{$.CsrfToken}}">
|
||||||
<button type="submit"
|
<button type="submit"
|
||||||
data-potvrda="Ukloniti deo '{{.ArtikalNaziv}}'?"
|
data-potvrda="Ukloniti deo '{{.ArtikalNaziv}}'?"
|
||||||
@@ -436,13 +546,21 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
<!-- ukupno: zbir usluga (radova) i ugrađenih delova -->
|
<!-- 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);">
|
<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>
|
<span style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">Ukupno</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="kolona" style="gap:10px;">
|
<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;">
|
<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-sporedni);">Usluge (rad)</span>
|
||||||
<span style="font-size:14px;color:var(--tekst-glavni);">{{dinari .UkupnoRadovi}} din</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-sporedni);">Ugrađeni delovi</span>
|
||||||
<span style="font-size:14px;color:var(--tekst-glavni);">{{dinari .UkupnoDelovi}} din</span>
|
<span style="font-size:14px;color:var(--tekst-glavni);">{{dinari .UkupnoDelovi}} din</span>
|
||||||
</div>
|
</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);">
|
<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: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>
|
<span style="font-size:18px;font-weight:600;color:var(--tekst-glavni);">{{dinari .UkupnoSve}} din</span>
|
||||||
@@ -459,6 +578,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- zona za brisanje -->
|
<!-- 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 class="kartica detalji-kartica animiraj" style="border-color:#dc262633;">
|
||||||
<div style="display:flex;align-items:flex-start;gap:12px;flex-wrap:wrap;">
|
<div style="display:flex;align-items:flex-start;gap:12px;flex-wrap:wrap;">
|
||||||
<div style="flex:1;min-width:200px;">
|
<div style="flex:1;min-width:200px;">
|
||||||
@@ -475,21 +596,22 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Radovi: pri izboru usluge iz <select>-a popuni predloženu cenu (data-cena).
|
// Radovi: pri izboru usluge iz <select>-a popuni predloženu cenu (data-cena).
|
||||||
(function () {
|
// Delegacija na document — kartica radova se osvežava AJAX-om (data-refresh),
|
||||||
var sel = document.getElementById('rad-usluga');
|
// 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');
|
var cena = document.getElementById('rad-cena');
|
||||||
if (!sel || !cena) return;
|
if (!cena) return;
|
||||||
sel.addEventListener('change', function () {
|
var opt = e.target.options[e.target.selectedIndex];
|
||||||
var opt = sel.options[sel.selectedIndex];
|
|
||||||
var c = parseFloat(opt.getAttribute('data-cena'));
|
var c = parseFloat(opt.getAttribute('data-cena'));
|
||||||
cena.value = (!isNaN(c) && c > 0) ? c.toFixed(2) : '0';
|
cena.value = (!isNaN(c) && c > 0) ? c.toFixed(2) : '0';
|
||||||
});
|
});
|
||||||
})();
|
|
||||||
|
|
||||||
// Delovi: kategorija filtrira <select> artikala; izborom artikla popunjavamo
|
// Delovi: kategorija filtrira <select> artikala; izborom artikla popunjavamo
|
||||||
// cenu i prikaz „U magacinu". Običan <select> ima native pretragu kucanjem
|
// cenu i prikaz „U magacinu". Običan <select> ima native pretragu kucanjem
|
||||||
|
|||||||
@@ -69,6 +69,10 @@
|
|||||||
.strana { padding: 16px 20px; max-width: 100%; }
|
.strana { padding: 16px 20px; max-width: 100%; }
|
||||||
.dugme-stampa { display: none !important; }
|
.dugme-stampa { display: none !important; }
|
||||||
@page { size: A4; margin: 12mm 14mm; }
|
@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 { 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; }
|
.dugme-stampa:hover { background: #333; }
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
.strana { max-width: 780px; margin: 0 auto; padding: 32px 40px; }
|
.strana { max-width: 780px; margin: 0 auto; padding: 32px 40px; }
|
||||||
|
|
||||||
.zaglavlje { display: flex; align-items: stretch; margin-bottom: 18px; gap: 12px; }
|
.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-naziv { font-size: 18px; font-weight: 700; }
|
||||||
.firma-info { font-size: 11px; color: #555; margin-top: 3px; line-height: 1.5; }
|
.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; }
|
.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; }
|
@page { size: A4; margin: 12mm 14mm; }
|
||||||
/* red tabele se ne sme prelomiti preko granice stranice */
|
/* red tabele se ne sme prelomiti preko granice stranice */
|
||||||
.tabela tr { break-inside: avoid; }
|
.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 */
|
/* zaglavlje tabele se ponavlja na svakoj stranici kad tabela pređe na novu */
|
||||||
.tabela thead { display: table-header-group; }
|
.tabela thead { display: table-header-group; }
|
||||||
/* rekapitulacija, klauzula i potpisi ostaju u jednom komadu */
|
/* od „Ukupno (procena)" do potpisa sve ostaje na jednoj strani — bez preloma */
|
||||||
.rekap, .klauzula-blok, .potpisi { break-inside: avoid; }
|
.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 { 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; }
|
.dugme-stampa:hover { background: #333; }
|
||||||
@@ -83,7 +86,19 @@
|
|||||||
{{if .PIB}}PIB: {{.PIB}}{{end}}{{if .MaticniBroj}}{{if .PIB}} · {{end}}MB: {{.MaticniBroj}}{{end}}
|
{{if .PIB}}PIB: {{.PIB}}{{end}}{{if .MaticniBroj}}{{if .PIB}} · {{end}}MB: {{.MaticniBroj}}{{end}}
|
||||||
</div>
|
</div>
|
||||||
</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="zg-okvir">
|
||||||
<div class="strana-tip">Klijent</div>
|
<div class="strana-tip">Klijent</div>
|
||||||
{{if .KlijentNaziv}}
|
{{if .KlijentNaziv}}
|
||||||
@@ -100,19 +115,6 @@
|
|||||||
<div class="strana-naziv prazno">— klijent nije naveden —</div>
|
<div class="strana-naziv prazno">— klijent nije naveden —</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- uređaj i traženo -->
|
<!-- uređaj i traženo -->
|
||||||
@@ -144,10 +146,6 @@
|
|||||||
<td class="desno">{{dinari .Ukupno}} din</td>
|
<td class="desno">{{dinari .Ukupno}} din</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{end}}
|
{{end}}
|
||||||
<tr class="ukupno-red">
|
|
||||||
<td colspan="3">Ukupno radovi</td>
|
|
||||||
<td class="desno">{{dinari .UkupnoRad}} din</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -175,10 +173,6 @@
|
|||||||
<td class="desno">{{dinari .Ukupno}} din</td>
|
<td class="desno">{{dinari .Ukupno}} din</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{end}}
|
{{end}}
|
||||||
<tr class="ukupno-red">
|
|
||||||
<td colspan="3">Ukupno delovi</td>
|
|
||||||
<td class="desno">{{dinari .UkupnoDelovi}} din</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -190,10 +184,13 @@
|
|||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{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">
|
<div class="rekap">
|
||||||
{{if .Radovi}}<div class="rekap-red"><span>Radovi</span><span>{{dinari .UkupnoRad}} 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"><span>Delovi i materijal</span><span>{{dinari .UkupnoDelovi}} 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">
|
||||||
<div class="rekap-ukupno-labela">Ukupno (procena):</div>
|
<div class="rekap-ukupno-labela">Ukupno (procena):</div>
|
||||||
<div class="rekap-ukupno-iznos">{{dinari .UkupnoSve}} din</div>
|
<div class="rekap-ukupno-iznos">{{dinari .UkupnoSve}} din</div>
|
||||||
@@ -217,6 +214,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</div><!-- /zavrsni-blok -->
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="dugme-stampa" onclick="window.print()">Štampaj</button>
|
<button class="dugme-stampa" onclick="window.print()">Štampaj</button>
|
||||||
|
|||||||
@@ -59,6 +59,10 @@
|
|||||||
.strana { padding: 16px 20px; max-width: 100%; }
|
.strana { padding: 16px 20px; max-width: 100%; }
|
||||||
.dugme-stampa { display: none !important; }
|
.dugme-stampa { display: none !important; }
|
||||||
@page { size: A4; margin: 12mm 14mm; }
|
@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 { 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; }
|
.dugme-stampa:hover { background: #333; }
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
/* zaglavlje: tri okvira (firma · klijent · dokument) */
|
/* zaglavlje: tri okvira (firma · klijent · dokument) */
|
||||||
.zaglavlje { display: flex; align-items: stretch; margin-bottom: 20px; gap: 12px; }
|
.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-naziv { font-size: 18px; font-weight: 700; }
|
||||||
.firma-info { font-size: 11px; color: #555; margin-top: 4px; line-height: 1.7; }
|
.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; }
|
.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}}
|
{{if .PIB}}PIB: {{.PIB}}{{end}}{{if .MaticniBroj}}{{if .PIB}} · {{end}}MB: {{.MaticniBroj}}{{end}}
|
||||||
</div>
|
</div>
|
||||||
</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="zg-okvir">
|
||||||
<div class="strana-tip">Klijent</div>
|
<div class="strana-tip">Klijent</div>
|
||||||
{{if .KlijentNaziv}}
|
{{if .KlijentNaziv}}
|
||||||
@@ -100,16 +110,6 @@
|
|||||||
<div class="strana-naziv prazno">— klijent nije naveden —</div>
|
<div class="strana-naziv prazno">— klijent nije naveden —</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- uređaj (QR skroz desno — klijent ga skenira da prati status) -->
|
<!-- uređaj (QR skroz desno — klijent ga skenira da prati status) -->
|
||||||
@@ -154,7 +154,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{{if .QRKod}}
|
{{if .QRKod}}
|
||||||
<div style="text-align:center;flex-shrink:0;">
|
<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;">
|
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 style="font-size:9px;color:#888;margin-top:3px;">Skenirajte za status</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -55,8 +55,8 @@
|
|||||||
/* red podataka */
|
/* 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 { 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:last-child { border-bottom: none; }
|
||||||
.red-labela { font-size: 12px; color: #888; flex-shrink: 0; }
|
.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; }
|
.red-vrednost { font-size: 13px; color: #111; font-weight: 500; text-align: right; flex-shrink: 0; white-space: nowrap; }
|
||||||
|
|
||||||
/* cena */
|
/* cena */
|
||||||
.cena-blok { background: #f9fafb; border-radius: 10px; padding: 14px 16px; margin-top: 4px; }
|
.cena-blok { background: #f9fafb; border-radius: 10px; padding: 14px 16px; margin-top: 4px; }
|
||||||
@@ -105,6 +105,8 @@
|
|||||||
{{$s := .Nalog.Status}}
|
{{$s := .Nalog.Status}}
|
||||||
{{if eq $s "Primljeno"}}
|
{{if eq $s "Primljeno"}}
|
||||||
<span class="status-bedz status-primljeno">Primljeno</span>
|
<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"}}
|
{{else if eq $s "Čeka delove"}}
|
||||||
<span class="status-bedz status-ceka">Čeka delove</span>
|
<span class="status-bedz status-ceka">Čeka delove</span>
|
||||||
{{else if eq $s "U popravci"}}
|
{{else if eq $s "U popravci"}}
|
||||||
@@ -153,6 +155,13 @@
|
|||||||
<span class="red-vrednost">{{garancijaTekst .Nalog.GarancijaDana}}</span>
|
<span class="red-vrednost">{{garancijaTekst .Nalog.GarancijaDana}}</span>
|
||||||
</div>
|
</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}}
|
{{if .Nalog.NapomenaKlijentu}}
|
||||||
<div style="margin-top:12px;">
|
<div style="margin-top:12px;">
|
||||||
<div class="red-labela" style="margin-bottom:6px;">Napomena</div>
|
<div class="red-labela" style="margin-bottom:6px;">Napomena</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user