Javno odobravanje predloga: Prihvati/Odbij sa komentarom
- Dodati handleri za prihvatanje i odbijanje predloga sa mobilnog
- Prihvatanje: predložene stavke prelaze u ugrađene (predlozeno 1→0)
- Odbijanje: brišu se predložene stavke
- Komentar klijenta uz odluku (textarea na mobilnom, prikaz u detaljima)
- Oznaka na kanbanu za naloge sa predlozima (narandžasta tačka + leva ivica)
- Oznaka na kanbanu za poruku klijenta (💬 dugme, tap za prikaz)
- Spojene kartice Radovi/Predložene usluge i Delovi/Predloženi delovi
- Auto-detekcija predlozeno=1 kad status != Primljeno
- Obrisana Otključaj/Zaključaj dugmad
- Ukupno sa predlogom na javnoj status stranici
- CSRF middleware premešten u zaštićenu grupu
- Migracija 078: komentar_klijenta
- Repo metode: ObrisiPredlozene, AzurirajKomentarKlijenta
- Popravljeno: 405 na odbij, duple rute, razmak između kartica
This commit is contained in:
+4
-1
@@ -204,7 +204,6 @@ func main() {
|
|||||||
|
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
r.Use(ntechmw.BezbednostHeaders())
|
r.Use(ntechmw.BezbednostHeaders())
|
||||||
r.Use(ntechmw.CsrfMiddleware)
|
|
||||||
r.Use(middleware.Compress(5))
|
r.Use(middleware.Compress(5))
|
||||||
// deljeno zaključavanje baze za vreme zahteva — obnova backupa (VratiBackup)
|
// deljeno zaključavanje baze za vreme zahteva — obnova backupa (VratiBackup)
|
||||||
// čeka da svi zahtevi završe pre zamene konekcije (vidi handler.ZakljucajCitanje)
|
// čeka da svi zahtevi završe pre zamene konekcije (vidi handler.ZakljucajCitanje)
|
||||||
@@ -240,9 +239,12 @@ func main() {
|
|||||||
r.Post("/setup", h.SacuvajSetup)
|
r.Post("/setup", h.SacuvajSetup)
|
||||||
r.Get("/odjava", h.Odjava)
|
r.Get("/odjava", h.Odjava)
|
||||||
r.Get("/status/{token}", h.ServisJavniStatus)
|
r.Get("/status/{token}", h.ServisJavniStatus)
|
||||||
|
r.Post("/status/{token}/prihvati", h.ServisJavniPrihvati)
|
||||||
|
r.Post("/status/{token}/odbij", h.ServisJavniOdbij)
|
||||||
|
|
||||||
// zaštićene rute — zahtevaju prijavljenog korisnika
|
// zaštićene rute — zahtevaju prijavljenog korisnika
|
||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
|
r.Use(ntechmw.CsrfMiddleware)
|
||||||
r.Use(ntechmw.RequireAuth(db, totpKljuc))
|
r.Use(ntechmw.RequireAuth(db, totpKljuc))
|
||||||
|
|
||||||
// doz vraća middleware koji na ruteru zahteva datu dozvolu za mutirajuću
|
// doz vraća middleware koji na ruteru zahteva datu dozvolu za mutirajuću
|
||||||
@@ -370,6 +372,7 @@ func main() {
|
|||||||
r.With(doz("servis.izmeni")).Post("/servis/{id}/uradjeno", h.SacuvajUradjeno)
|
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}/cena-dijagnostike", h.AzurirajCenaDijagnostike)
|
||||||
r.With(doz("servis.izmeni")).Post("/servis/{id}/odbij-popravku", h.OdbijPopravku)
|
r.With(doz("servis.izmeni")).Post("/servis/{id}/odbij-popravku", h.OdbijPopravku)
|
||||||
|
r.With(doz("servis.izmeni")).Post("/servis/{id}/obrisi-komentar-klijenta", h.ObrisiKomentarKlijenta)
|
||||||
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)
|
||||||
|
|||||||
@@ -200,6 +200,7 @@ type ServisRepository interface {
|
|||||||
AzurirajUradjeno(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
|
AzurirajCenaDijagnostike(ctx context.Context, id int64, cena float64) error
|
||||||
OdbijPopravku(ctx context.Context, id int64, cena float64) error
|
OdbijPopravku(ctx context.Context, id int64, cena float64) error
|
||||||
|
AzurirajKomentarKlijenta(ctx context.Context, id int64, tekst string) 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)
|
||||||
}
|
}
|
||||||
@@ -221,9 +222,13 @@ type ServisniDeloviRepository interface {
|
|||||||
// UgradiIliPotrazuj atomično (jedna transakcija) čita stanje magacina, ugrađuje
|
// UgradiIliPotrazuj atomično (jedna transakcija) čita stanje magacina, ugrađuje
|
||||||
// ono što fizički ima (skida sa lagera, lager NE ide u minus), a višak beleži u
|
// ono što fizički ima (skida sa lagera, lager NE ide u minus), a višak beleži u
|
||||||
// potraživane delove. Vraća koliko je ugrađeno i koliko nedostaje.
|
// potraživane delove. Vraća koliko je ugrađeno i koliko nedostaje.
|
||||||
UgradiIliPotrazuj(ctx context.Context, nalogID, artikalID int64, kolicina int, cenaKomada float64, korisnikID *int64) (ugradjeno, nedostaje int, err error)
|
UgradiIliPotrazuj(ctx context.Context, nalogID, artikalID int64, kolicina int, cenaKomada float64, korisnikID *int64, predlozeno bool) (ugradjeno, nedostaje int, err error)
|
||||||
DohvatiArtikalID(ctx context.Context, deoID int64) (int64, error)
|
DohvatiArtikalID(ctx context.Context, deoID int64) (int64, error)
|
||||||
Obrisi(ctx context.Context, id int64, korisnikID *int64) error
|
Obrisi(ctx context.Context, id int64, korisnikID *int64) error
|
||||||
|
// PrihvatiPredlozene prebacuje predložene delove (i potraživane) naloga u ugrađene
|
||||||
|
PrihvatiPredlozene(ctx context.Context, nalogID int64) error
|
||||||
|
// ObrisiPredlozene briše sve predložene delove sa naloga (klijent odbio predlog)
|
||||||
|
ObrisiPredlozene(ctx context.Context, nalogID int64) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServisniPotrazivaniDeloviRepository definiše operacije nad delovima koji nedostaju
|
// ServisniPotrazivaniDeloviRepository definiše operacije nad delovima koji nedostaju
|
||||||
@@ -240,8 +245,12 @@ type ServisniPotrazivaniDeloviRepository interface {
|
|||||||
// ServisniRadoviRepository definiše operacije nad radovima (uslugama) na nalogu
|
// ServisniRadoviRepository definiše operacije nad radovima (uslugama) na nalogu
|
||||||
type ServisniRadoviRepository interface {
|
type ServisniRadoviRepository interface {
|
||||||
DohvatiZaNalog(ctx context.Context, nalogID int64) ([]model.ServisniRad, error)
|
DohvatiZaNalog(ctx context.Context, nalogID int64) ([]model.ServisniRad, error)
|
||||||
Dodaj(ctx context.Context, nalogID, uslugaID int64, naziv string, kolicina, cenaKomada float64) (int64, error)
|
Dodaj(ctx context.Context, nalogID, uslugaID int64, naziv string, kolicina, cenaKomada float64, predlozeno bool) (int64, error)
|
||||||
Obrisi(ctx context.Context, id int64) error
|
Obrisi(ctx context.Context, id int64) error
|
||||||
|
// PrihvatiPredlozene prebacuje predložene radove naloga u redovne
|
||||||
|
PrihvatiPredlozene(ctx context.Context, nalogID int64) error
|
||||||
|
// ObrisiPredlozene briše sve predložene radove sa naloga (klijent odbio predlog)
|
||||||
|
ObrisiPredlozene(ctx context.Context, nalogID int64) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// MagacinskePromeneRepository definiše operacije nad revizijskim tragom magacina
|
// MagacinskePromeneRepository definiše operacije nad revizijskim tragom magacina
|
||||||
|
|||||||
@@ -58,8 +58,10 @@ 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.nalaz_dijagnostike, sn.uradjeno, sn.cena_dijagnostike, sn.popravka_odbijena, 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, sn.komentar_klijenta,
|
||||||
COALESCE(kp.naziv, '') AS klijent_naziv
|
COALESCE(kp.naziv, '') AS klijent_naziv,
|
||||||
|
(EXISTS(SELECT 1 FROM servis_radovi sr WHERE sr.nalog_id = sn.id AND sr.predlozeno = 1)
|
||||||
|
OR EXISTS(SELECT 1 FROM servisni_delovi sd WHERE sd.nalog_id = sn.id AND sd.predlozeno = 1)) AS ima_predlog
|
||||||
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
|
||||||
WHERE 1=1`
|
WHERE 1=1`
|
||||||
@@ -88,7 +90,7 @@ func (r *ServisRepo) Lista(ctx context.Context, pretraga, status string) ([]mode
|
|||||||
var rezultat []model.ServisniNalogSaKlijentom
|
var rezultat []model.ServisniNalogSaKlijentom
|
||||||
for redovi.Next() {
|
for redovi.Next() {
|
||||||
var n model.ServisniNalogSaKlijentom
|
var n model.ServisniNalogSaKlijentom
|
||||||
err := scanNalog(redovi.Scan, &n.ServisniNalog, &n.KlijentNaziv)
|
err := scanNalog(redovi.Scan, &n.ServisniNalog, &n.KlijentNaziv, &n.ImaPredlog)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("ntech: ServisRepo.Lista: scan: %w", err)
|
return nil, fmt.Errorf("ntech: ServisRepo.Lista: scan: %w", err)
|
||||||
}
|
}
|
||||||
@@ -105,11 +107,11 @@ 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, nalaz_dijagnostike, uradjeno, cena_dijagnostike, popravka_odbijena, javni_token
|
ostecenja, pin_uredjaja, pribor, napomena_klijentu, nalaz_dijagnostike, uradjeno, cena_dijagnostike, popravka_odbijena, javni_token, komentar_klijenta
|
||||||
FROM servisni_nalozi WHERE id = ?`, id)
|
FROM servisni_nalozi WHERE id = ?`, id)
|
||||||
|
|
||||||
var n model.ServisniNalog
|
var n model.ServisniNalog
|
||||||
err := scanNalog(red.Scan, &n, nil)
|
err := scanNalog(red.Scan, &n, nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("ntech: ServisRepo.DohvatiID: %w", err)
|
return nil, fmt.Errorf("ntech: ServisRepo.DohvatiID: %w", err)
|
||||||
}
|
}
|
||||||
@@ -157,11 +159,11 @@ 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, nalaz_dijagnostike, uradjeno, cena_dijagnostike, popravka_odbijena, javni_token
|
ostecenja, pin_uredjaja, pribor, napomena_klijentu, nalaz_dijagnostike, uradjeno, cena_dijagnostike, popravka_odbijena, javni_token, komentar_klijenta
|
||||||
FROM servisni_nalozi WHERE javni_token = ?`, token)
|
FROM servisni_nalozi WHERE javni_token = ?`, token)
|
||||||
|
|
||||||
var n model.ServisniNalog
|
var n model.ServisniNalog
|
||||||
if err := scanNalog(red.Scan, &n, nil); err != nil {
|
if err := scanNalog(red.Scan, &n, nil, nil); err != nil {
|
||||||
return nil, fmt.Errorf("ntech: ServisRepo.DohvatiJavniToken: %w", err)
|
return nil, fmt.Errorf("ntech: ServisRepo.DohvatiJavniToken: %w", err)
|
||||||
}
|
}
|
||||||
return &n, nil
|
return &n, nil
|
||||||
@@ -336,6 +338,17 @@ func (r *ServisRepo) AzurirajCenaDijagnostike(ctx context.Context, id int64, cen
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AzurirajKomentarKlijenta čuva poruku klijenta uz prihvatanje/odbijanje predloga
|
||||||
|
func (r *ServisRepo) AzurirajKomentarKlijenta(ctx context.Context, id int64, tekst string) error {
|
||||||
|
_, err := r.db.ExecContext(ctx,
|
||||||
|
"UPDATE servisni_nalozi SET komentar_klijenta = ? WHERE id = ?", tekst, id,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ntech: ServisRepo.AzurirajKomentarKlijenta: %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 {
|
||||||
@@ -415,9 +428,9 @@ func (r *ServisRepo) Obrisi(ctx context.Context, id int64, korisnikID *int64) er
|
|||||||
|
|
||||||
// scanNalog čita redove iz upita u ServisniNalog struct —
|
// scanNalog čita redove iz upita u ServisniNalog struct —
|
||||||
// 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, imaPredlog *bool) error {
|
||||||
var klijentID, tehnicarID sql.NullInt64
|
var klijentID, tehnicarID sql.NullInt64
|
||||||
var serijskiBroj, napomena, ostecenja, pinUredjaja, pribor, napomenaKlijentu, nalazDijagnostike, uradjeno, javniToken sql.NullString
|
var serijskiBroj, napomena, ostecenja, pinUredjaja, pribor, napomenaKlijentu, nalazDijagnostike, uradjeno, javniToken, komentarKlijenta 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
|
||||||
@@ -427,12 +440,15 @@ func scanNalog(scan func(...any) error, n *model.ServisniNalog, klijentNaziv *st
|
|||||||
&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, &nalazDijagnostike, &uradjeno, &n.CenaDijagnostike, &popravkaOdbijena, &javniToken,
|
&ostecenja, &pinUredjaja, &pribor, &napomenaKlijentu, &nalazDijagnostike, &uradjeno, &n.CenaDijagnostike, &popravkaOdbijena, &javniToken, &komentarKlijenta,
|
||||||
}
|
}
|
||||||
|
|
||||||
if klijentNaziv != nil {
|
if klijentNaziv != nil {
|
||||||
args = append(args, klijentNaziv)
|
args = append(args, klijentNaziv)
|
||||||
}
|
}
|
||||||
|
if imaPredlog != nil {
|
||||||
|
args = append(args, imaPredlog)
|
||||||
|
}
|
||||||
|
|
||||||
if err := scan(args...); err != nil {
|
if err := scan(args...); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -488,6 +504,7 @@ func scanNalog(scan func(...any) error, n *model.ServisniNalog, klijentNaziv *st
|
|||||||
}
|
}
|
||||||
n.PopravkaOdbijena = popravkaOdbijena.Int64 != 0
|
n.PopravkaOdbijena = popravkaOdbijena.Int64 != 0
|
||||||
n.JavniToken = javniToken.String
|
n.JavniToken = javniToken.String
|
||||||
|
n.KomentarKlijenta = komentarKlijenta.String
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ func NoviServisniDeloviRepo(db *sql.DB) *ServisniDeloviRepo {
|
|||||||
func (r *ServisniDeloviRepo) DohvatiZaNalog(ctx context.Context, nalogID int64) ([]model.ServisniDeoSaArtiklom, error) {
|
func (r *ServisniDeloviRepo) DohvatiZaNalog(ctx context.Context, nalogID int64) ([]model.ServisniDeoSaArtiklom, error) {
|
||||||
redovi, err := r.db.QueryContext(ctx, `
|
redovi, err := r.db.QueryContext(ctx, `
|
||||||
SELECT sd.id, sd.nalog_id, sd.artikal_id, sd.kolicina, sd.cena_komada, sd.datum,
|
SELECT sd.id, sd.nalog_id, sd.artikal_id, sd.kolicina, sd.cena_komada, sd.datum,
|
||||||
a.naziv
|
sd.predlozeno, a.naziv
|
||||||
FROM servisni_delovi sd
|
FROM servisni_delovi sd
|
||||||
JOIN artikli a ON a.id = sd.artikal_id
|
JOIN artikli a ON a.id = sd.artikal_id
|
||||||
WHERE sd.nalog_id = ?
|
WHERE sd.nalog_id = ?
|
||||||
@@ -38,7 +38,7 @@ func (r *ServisniDeloviRepo) DohvatiZaNalog(ctx context.Context, nalogID int64)
|
|||||||
var d model.ServisniDeoSaArtiklom
|
var d model.ServisniDeoSaArtiklom
|
||||||
err := redovi.Scan(
|
err := redovi.Scan(
|
||||||
&d.ID, &d.NalogID, &d.ArtikalID, &d.Kolicina, &d.CenaKomada, &d.Datum,
|
&d.ID, &d.NalogID, &d.ArtikalID, &d.Kolicina, &d.CenaKomada, &d.Datum,
|
||||||
&d.ArtikalNaziv,
|
&d.Predlozeno, &d.ArtikalNaziv,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("ntech: ServisniDeloviRepo.DohvatiZaNalog: scan: %w", err)
|
return nil, fmt.Errorf("ntech: ServisniDeloviRepo.DohvatiZaNalog: scan: %w", err)
|
||||||
@@ -53,7 +53,11 @@ func (r *ServisniDeloviRepo) DohvatiZaNalog(ctx context.Context, nalogID int64)
|
|||||||
// stanju, a višak beleži u potraživane delove. Stanje se čita i koristi unutar iste
|
// stanju, a višak beleži u potraživane delove. Stanje se čita i koristi unutar iste
|
||||||
// transakcije pa lager NIKAD ne ide u minus (nema TOCTOU između čitanja i upisa).
|
// transakcije pa lager NIKAD ne ide u minus (nema TOCTOU između čitanja i upisa).
|
||||||
// Vraća koliko je ugrađeno i koliko nedostaje.
|
// Vraća koliko je ugrađeno i koliko nedostaje.
|
||||||
func (r *ServisniDeloviRepo) UgradiIliPotrazuj(ctx context.Context, nalogID, artikalID int64, kolicina int, cenaKomada float64, korisnikID *int64) (ugradjeno, nedostaje int, err error) {
|
func (r *ServisniDeloviRepo) UgradiIliPotrazuj(ctx context.Context, nalogID, artikalID int64, kolicina int, cenaKomada float64, korisnikID *int64, predlozeno bool) (ugradjeno, nedostaje int, err error) {
|
||||||
|
predInt := 0
|
||||||
|
if predlozeno {
|
||||||
|
predInt = 1
|
||||||
|
}
|
||||||
tx, err := r.db.BeginTx(ctx, nil)
|
tx, err := r.db.BeginTx(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, 0, fmt.Errorf("ntech: ServisniDeloviRepo.UgradiIliPotrazuj: begin tx: %w", err)
|
return 0, 0, fmt.Errorf("ntech: ServisniDeloviRepo.UgradiIliPotrazuj: begin tx: %w", err)
|
||||||
@@ -82,8 +86,8 @@ func (r *ServisniDeloviRepo) UgradiIliPotrazuj(ctx context.Context, nalogID, art
|
|||||||
var postojeciID int64
|
var postojeciID int64
|
||||||
var postojeciKol int
|
var postojeciKol int
|
||||||
errRed := tx.QueryRowContext(ctx,
|
errRed := tx.QueryRowContext(ctx,
|
||||||
"SELECT id, kolicina FROM servisni_delovi WHERE nalog_id = ? AND artikal_id = ?",
|
"SELECT id, kolicina FROM servisni_delovi WHERE nalog_id = ? AND artikal_id = ? AND predlozeno = ?",
|
||||||
nalogID, artikalID,
|
nalogID, artikalID, predInt,
|
||||||
).Scan(&postojeciID, &postojeciKol)
|
).Scan(&postojeciID, &postojeciKol)
|
||||||
if errRed == nil {
|
if errRed == nil {
|
||||||
if _, err = tx.ExecContext(ctx,
|
if _, err = tx.ExecContext(ctx,
|
||||||
@@ -94,9 +98,9 @@ func (r *ServisniDeloviRepo) UgradiIliPotrazuj(ctx context.Context, nalogID, art
|
|||||||
}
|
}
|
||||||
} else if errors.Is(errRed, sql.ErrNoRows) {
|
} else if errors.Is(errRed, sql.ErrNoRows) {
|
||||||
if _, err = tx.ExecContext(ctx, `
|
if _, err = tx.ExecContext(ctx, `
|
||||||
INSERT INTO servisni_delovi (nalog_id, artikal_id, kolicina, cena_komada)
|
INSERT INTO servisni_delovi (nalog_id, artikal_id, kolicina, cena_komada, predlozeno)
|
||||||
VALUES (?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?)`,
|
||||||
nalogID, artikalID, ugradjeno, cenaKomada,
|
nalogID, artikalID, ugradjeno, cenaKomada, predInt,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return 0, 0, fmt.Errorf("ntech: ServisniDeloviRepo.UgradiIliPotrazuj: insert: %w", err)
|
return 0, 0, fmt.Errorf("ntech: ServisniDeloviRepo.UgradiIliPotrazuj: insert: %w", err)
|
||||||
}
|
}
|
||||||
@@ -121,8 +125,8 @@ func (r *ServisniDeloviRepo) UgradiIliPotrazuj(ctx context.Context, nalogID, art
|
|||||||
var postojeciID int64
|
var postojeciID int64
|
||||||
var postojeciKol int
|
var postojeciKol int
|
||||||
errRed := tx.QueryRowContext(ctx,
|
errRed := tx.QueryRowContext(ctx,
|
||||||
"SELECT id, kolicina FROM servisni_potrazivani_delovi WHERE nalog_id = ? AND artikal_id = ?",
|
"SELECT id, kolicina FROM servisni_potrazivani_delovi WHERE nalog_id = ? AND artikal_id = ? AND predlozeno = ?",
|
||||||
nalogID, artikalID,
|
nalogID, artikalID, predInt,
|
||||||
).Scan(&postojeciID, &postojeciKol)
|
).Scan(&postojeciID, &postojeciKol)
|
||||||
if errRed == nil {
|
if errRed == nil {
|
||||||
if _, err = tx.ExecContext(ctx,
|
if _, err = tx.ExecContext(ctx,
|
||||||
@@ -133,8 +137,8 @@ func (r *ServisniDeloviRepo) UgradiIliPotrazuj(ctx context.Context, nalogID, art
|
|||||||
}
|
}
|
||||||
} else if errors.Is(errRed, sql.ErrNoRows) {
|
} else if errors.Is(errRed, sql.ErrNoRows) {
|
||||||
if _, err = tx.ExecContext(ctx,
|
if _, err = tx.ExecContext(ctx,
|
||||||
"INSERT INTO servisni_potrazivani_delovi (nalog_id, artikal_id, kolicina, cena_komada) VALUES (?, ?, ?, ?)",
|
"INSERT INTO servisni_potrazivani_delovi (nalog_id, artikal_id, kolicina, cena_komada, predlozeno) VALUES (?, ?, ?, ?, ?)",
|
||||||
nalogID, artikalID, nedostaje, cenaKomada,
|
nalogID, artikalID, nedostaje, cenaKomada, predInt,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return 0, 0, fmt.Errorf("ntech: ServisniDeloviRepo.UgradiIliPotrazuj: potraživani insert: %w", err)
|
return 0, 0, fmt.Errorf("ntech: ServisniDeloviRepo.UgradiIliPotrazuj: potraživani insert: %w", err)
|
||||||
}
|
}
|
||||||
@@ -201,6 +205,57 @@ func (r *ServisniDeloviRepo) Obrisi(ctx context.Context, id int64, korisnikID *i
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PrihvatiPredlozene prebacuje sve predložene delove (i potraživane) naloga u
|
||||||
|
// ugrađene (predlozeno → 0) — poziva se kad klijent prihvati predlog popravke.
|
||||||
|
func (r *ServisniDeloviRepo) PrihvatiPredlozene(ctx context.Context, nalogID int64) error {
|
||||||
|
tx, err := r.db.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ntech: ServisniDeloviRepo.PrihvatiPredlozene: begin tx: %w", err)
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
if _, err := tx.ExecContext(ctx,
|
||||||
|
"UPDATE servisni_delovi SET predlozeno = 0 WHERE nalog_id = ? AND predlozeno = 1", nalogID,
|
||||||
|
); err != nil {
|
||||||
|
return fmt.Errorf("ntech: ServisniDeloviRepo.PrihvatiPredlozene: delovi: %w", err)
|
||||||
|
}
|
||||||
|
if _, err := tx.ExecContext(ctx,
|
||||||
|
"UPDATE servisni_potrazivani_delovi SET predlozeno = 0 WHERE nalog_id = ? AND predlozeno = 1", nalogID,
|
||||||
|
); err != nil {
|
||||||
|
return fmt.Errorf("ntech: ServisniDeloviRepo.PrihvatiPredlozene: potraživani: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return fmt.Errorf("ntech: ServisniDeloviRepo.PrihvatiPredlozene: commit: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ObrisiPredlozene briše sve predložene delove (i potraživane) sa naloga
|
||||||
|
func (r *ServisniDeloviRepo) ObrisiPredlozene(ctx context.Context, nalogID int64) error {
|
||||||
|
tx, err := r.db.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ntech: ServisniDeloviRepo.ObrisiPredlozene: begin tx: %w", err)
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
if _, err := tx.ExecContext(ctx,
|
||||||
|
"DELETE FROM servisni_delovi WHERE nalog_id = ? AND predlozeno = 1", nalogID,
|
||||||
|
); err != nil {
|
||||||
|
return fmt.Errorf("ntech: ServisniDeloviRepo.ObrisiPredlozene: delovi: %w", err)
|
||||||
|
}
|
||||||
|
if _, err := tx.ExecContext(ctx,
|
||||||
|
"DELETE FROM servisni_potrazivani_delovi WHERE nalog_id = ? AND predlozeno = 1", nalogID,
|
||||||
|
); err != nil {
|
||||||
|
return fmt.Errorf("ntech: ServisniDeloviRepo.ObrisiPredlozene: potraživani: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return fmt.Errorf("ntech: ServisniDeloviRepo.ObrisiPredlozene: commit: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// DohvatiArtikalID vraća artikal_id za dati servisni deo (pre brisanja)
|
// DohvatiArtikalID vraća artikal_id za dati servisni deo (pre brisanja)
|
||||||
func (r *ServisniDeloviRepo) DohvatiArtikalID(ctx context.Context, deoID int64) (int64, error) {
|
func (r *ServisniDeloviRepo) DohvatiArtikalID(ctx context.Context, deoID int64) (int64, error) {
|
||||||
var artikalID int64
|
var artikalID int64
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ func NoviServisniPotrazivaniDeloviRepo(db *sql.DB) *ServisniPotrazivaniDeloviRep
|
|||||||
func (r *ServisniPotrazivaniDeloviRepo) DohvatiZaNalog(ctx context.Context, nalogID int64) ([]model.ServisniPotrazivaniDeo, error) {
|
func (r *ServisniPotrazivaniDeloviRepo) DohvatiZaNalog(ctx context.Context, nalogID int64) ([]model.ServisniPotrazivaniDeo, error) {
|
||||||
redovi, err := r.db.QueryContext(ctx, `
|
redovi, err := r.db.QueryContext(ctx, `
|
||||||
SELECT spd.id, spd.nalog_id, spd.artikal_id, spd.kolicina, spd.cena_komada, spd.datum,
|
SELECT spd.id, spd.nalog_id, spd.artikal_id, spd.kolicina, spd.cena_komada, spd.datum,
|
||||||
a.naziv
|
spd.predlozeno, a.naziv
|
||||||
FROM servisni_potrazivani_delovi spd
|
FROM servisni_potrazivani_delovi spd
|
||||||
JOIN artikli a ON a.id = spd.artikal_id
|
JOIN artikli a ON a.id = spd.artikal_id
|
||||||
WHERE spd.nalog_id = ?
|
WHERE spd.nalog_id = ?
|
||||||
@@ -36,7 +36,7 @@ func (r *ServisniPotrazivaniDeloviRepo) DohvatiZaNalog(ctx context.Context, nalo
|
|||||||
var rezultat []model.ServisniPotrazivaniDeo
|
var rezultat []model.ServisniPotrazivaniDeo
|
||||||
for redovi.Next() {
|
for redovi.Next() {
|
||||||
var d model.ServisniPotrazivaniDeo
|
var d model.ServisniPotrazivaniDeo
|
||||||
err := redovi.Scan(&d.ID, &d.NalogID, &d.ArtikalID, &d.Kolicina, &d.CenaKomada, &d.Datum, &d.ArtikalNaziv)
|
err := redovi.Scan(&d.ID, &d.NalogID, &d.ArtikalID, &d.Kolicina, &d.CenaKomada, &d.Datum, &d.Predlozeno, &d.ArtikalNaziv)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("ntech: ServisniPotrazivaniDeloviRepo.DohvatiZaNalog: scan: %w", err)
|
return nil, fmt.Errorf("ntech: ServisniPotrazivaniDeloviRepo.DohvatiZaNalog: scan: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ func TestPotrazivaniTok(t *testing.T) {
|
|||||||
potrRepo := NoviServisniPotrazivaniDeloviRepo(db)
|
potrRepo := NoviServisniPotrazivaniDeloviRepo(db)
|
||||||
|
|
||||||
// traži 5, na stanju 2 → ugradi 2, nedostaje 3
|
// traži 5, na stanju 2 → ugradi 2, nedostaje 3
|
||||||
ugradjeno, nedostaje, err := deloviRepo.UgradiIliPotrazuj(ctx, 1, 1, 5, 100, nil)
|
ugradjeno, nedostaje, err := deloviRepo.UgradiIliPotrazuj(ctx, 1, 1, 5, 100, nil, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("UgradiIliPotrazuj: %v", err)
|
t.Fatalf("UgradiIliPotrazuj: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ func NoviServisniRadoviRepo(baza *sql.DB) *ServisniRadoviRepo {
|
|||||||
// DohvatiZaNalog vraća sve radove (usluge) jednog naloga, redom unosa
|
// DohvatiZaNalog vraća sve radove (usluge) jednog naloga, redom unosa
|
||||||
func (r *ServisniRadoviRepo) DohvatiZaNalog(ctx context.Context, nalogID int64) ([]model.ServisniRad, error) {
|
func (r *ServisniRadoviRepo) DohvatiZaNalog(ctx context.Context, nalogID int64) ([]model.ServisniRad, error) {
|
||||||
redovi, err := r.db.QueryContext(ctx, `
|
redovi, err := r.db.QueryContext(ctx, `
|
||||||
SELECT id, nalog_id, COALESCE(usluga_id, 0), naziv, kolicina, cena_komada, datum
|
SELECT id, nalog_id, COALESCE(usluga_id, 0), naziv, kolicina, cena_komada, datum, predlozeno
|
||||||
FROM servis_radovi WHERE nalog_id = ? ORDER BY id`, nalogID)
|
FROM servis_radovi WHERE nalog_id = ? ORDER BY id`, nalogID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("ntech: ServisniRadoviRepo.DohvatiZaNalog: %w", err)
|
return nil, fmt.Errorf("ntech: ServisniRadoviRepo.DohvatiZaNalog: %w", err)
|
||||||
@@ -31,7 +31,7 @@ func (r *ServisniRadoviRepo) DohvatiZaNalog(ctx context.Context, nalogID int64)
|
|||||||
var radovi []model.ServisniRad
|
var radovi []model.ServisniRad
|
||||||
for redovi.Next() {
|
for redovi.Next() {
|
||||||
var rad model.ServisniRad
|
var rad model.ServisniRad
|
||||||
if err := redovi.Scan(&rad.ID, &rad.NalogID, &rad.UslugaID, &rad.Naziv, &rad.Kolicina, &rad.CenaKomada, &rad.Datum); err != nil {
|
if err := redovi.Scan(&rad.ID, &rad.NalogID, &rad.UslugaID, &rad.Naziv, &rad.Kolicina, &rad.CenaKomada, &rad.Datum, &rad.Predlozeno); err != nil {
|
||||||
return nil, fmt.Errorf("ntech: ServisniRadoviRepo.DohvatiZaNalog: %w", err)
|
return nil, fmt.Errorf("ntech: ServisniRadoviRepo.DohvatiZaNalog: %w", err)
|
||||||
}
|
}
|
||||||
radovi = append(radovi, rad)
|
radovi = append(radovi, rad)
|
||||||
@@ -39,16 +39,21 @@ func (r *ServisniRadoviRepo) DohvatiZaNalog(ctx context.Context, nalogID int64)
|
|||||||
return radovi, redovi.Err()
|
return radovi, redovi.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dodaj upisuje jednu stavku rada na nalog (radovi ne diraju lager)
|
// Dodaj upisuje jednu stavku rada na nalog (radovi ne diraju lager).
|
||||||
func (r *ServisniRadoviRepo) Dodaj(ctx context.Context, nalogID, uslugaID int64, naziv string, kolicina, cenaKomada float64) (int64, error) {
|
// predlozeno=true označava uslugu koju serviser predlaže posle dijagnostike.
|
||||||
|
func (r *ServisniRadoviRepo) Dodaj(ctx context.Context, nalogID, uslugaID int64, naziv string, kolicina, cenaKomada float64, predlozeno bool) (int64, error) {
|
||||||
var uid any
|
var uid any
|
||||||
if uslugaID > 0 {
|
if uslugaID > 0 {
|
||||||
uid = uslugaID
|
uid = uslugaID
|
||||||
}
|
}
|
||||||
|
predInt := 0
|
||||||
|
if predlozeno {
|
||||||
|
predInt = 1
|
||||||
|
}
|
||||||
rez, err := r.db.ExecContext(ctx, `
|
rez, err := r.db.ExecContext(ctx, `
|
||||||
INSERT INTO servis_radovi (nalog_id, usluga_id, naziv, kolicina, cena_komada)
|
INSERT INTO servis_radovi (nalog_id, usluga_id, naziv, kolicina, cena_komada, predlozeno)
|
||||||
VALUES (?, ?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||||
nalogID, uid, naziv, kolicina, cenaKomada,
|
nalogID, uid, naziv, kolicina, cenaKomada, predInt,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("ntech: ServisniRadoviRepo.Dodaj: %w", err)
|
return 0, fmt.Errorf("ntech: ServisniRadoviRepo.Dodaj: %w", err)
|
||||||
@@ -60,6 +65,27 @@ func (r *ServisniRadoviRepo) Dodaj(ctx context.Context, nalogID, uslugaID int64,
|
|||||||
return id, nil
|
return id, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PrihvatiPredlozene prebacuje sve predložene radove naloga u redovne (predlozeno → 0)
|
||||||
|
func (r *ServisniRadoviRepo) PrihvatiPredlozene(ctx context.Context, nalogID int64) error {
|
||||||
|
if _, err := r.db.ExecContext(ctx,
|
||||||
|
"UPDATE servis_radovi SET predlozeno = 0 WHERE nalog_id = ? AND predlozeno = 1", nalogID,
|
||||||
|
); err != nil {
|
||||||
|
return fmt.Errorf("ntech: ServisniRadoviRepo.PrihvatiPredlozene: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ObrisiPredlozene briše sve predložene radove sa naloga (klijent odbio predlog)
|
||||||
|
func (r *ServisniRadoviRepo) ObrisiPredlozene(ctx context.Context, nalogID int64) error {
|
||||||
|
_, err := r.db.ExecContext(ctx,
|
||||||
|
"DELETE FROM servis_radovi WHERE nalog_id = ? AND predlozeno = 1", nalogID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ntech: ServisniRadoviRepo.ObrisiPredlozene: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Obrisi uklanja jednu stavku rada sa naloga
|
// Obrisi uklanja jednu stavku rada sa naloga
|
||||||
func (r *ServisniRadoviRepo) Obrisi(ctx context.Context, id int64) error {
|
func (r *ServisniRadoviRepo) Obrisi(ctx context.Context, id int64) error {
|
||||||
if _, err := r.db.ExecContext(ctx, "DELETE FROM servis_radovi WHERE id = ?", id); err != nil {
|
if _, err := r.db.ExecContext(ctx, "DELETE FROM servis_radovi WHERE id = ?", id); err != nil {
|
||||||
|
|||||||
+240
-54
@@ -54,10 +54,14 @@ type PodaciDetaljiNaloga struct {
|
|||||||
KlijentNaziv string
|
KlijentNaziv string
|
||||||
TehnicarNaziv string
|
TehnicarNaziv string
|
||||||
Tehnicari []model.Korisnik
|
Tehnicari []model.Korisnik
|
||||||
GarancijaDefault string // podrazumevana garancija (prijem + meseci), format 2006-01-02
|
GarancijaDefault string // podrazumevana garancija (prijem + meseci), format 2006-01-02
|
||||||
BezGarancije bool // u bazi nalog nema garanciju (GarancijaDo == NULL)
|
BezGarancije bool // u bazi nalog nema garanciju (GarancijaDo == NULL)
|
||||||
ServisniDelovi []model.ServisniDeoSaArtiklom
|
ServisniDelovi []model.ServisniDeoSaArtiklom // ugrađeni/traženi (predlozeno=false)
|
||||||
ServisniRadovi []model.ServisniRad
|
ServisniRadovi []model.ServisniRad // ugrađeni/traženi (predlozeno=false)
|
||||||
|
PredlozeniDelovi []model.ServisniDeoSaArtiklom // serviser predložio posle dijagnostike
|
||||||
|
PredlozeniRadovi []model.ServisniRad // serviser predložio posle dijagnostike
|
||||||
|
ImaPredlog bool // postoji bar jedna predložena stavka
|
||||||
|
ZakljucaniUgradjeni bool // ugrađene stavke zaključane (status nije „Primljeno")
|
||||||
Artikli []model.ArtikalSaKategorijom
|
Artikli []model.ArtikalSaKategorijom
|
||||||
Usluge []model.Usluga
|
Usluge []model.Usluga
|
||||||
Sacuvano bool
|
Sacuvano bool
|
||||||
@@ -209,6 +213,13 @@ func (h *Handler) SacuvajNalog(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// forma novog naloga ne sadrži polje garancije — primeni podrazumevanu
|
||||||
|
// garanciju iz podešavanja, osim ako je korisnik izričito izabrao „bez garancije"
|
||||||
|
if nalog.GarancijaDo == nil && r.FormValue("bez_garancije") != "1" {
|
||||||
|
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||||
|
nalog.GarancijaDo = defaultGarancija(nalog.DatumPrijema, podesavanja)
|
||||||
|
}
|
||||||
|
|
||||||
// garancija ne sme da bude pre datuma prijema
|
// garancija ne sme da bude pre datuma prijema
|
||||||
if nalog.GarancijaDo != nil && garancijaPrePrijema(*nalog.GarancijaDo, nalog.DatumPrijema) {
|
if nalog.GarancijaDo != nil && garancijaPrePrijema(*nalog.GarancijaDo, nalog.DatumPrijema) {
|
||||||
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||||
@@ -583,6 +594,24 @@ func (h *Handler) DetaljiNaloga(w http.ResponseWriter, r *http.Request) {
|
|||||||
slog.Error("greška pri učitavanju radova", "error", err)
|
slog.Error("greška pri učitavanju radova", "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// razdvajamo na ugrađene/tražene (predlozeno=false) i predložene (predlozeno=true)
|
||||||
|
var ugradjeniDelovi, predlozeniDelovi []model.ServisniDeoSaArtiklom
|
||||||
|
for _, d := range delovi {
|
||||||
|
if d.Predlozeno {
|
||||||
|
predlozeniDelovi = append(predlozeniDelovi, d)
|
||||||
|
} else {
|
||||||
|
ugradjeniDelovi = append(ugradjeniDelovi, d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var ugradjeniRadovi, predlozeniRadovi []model.ServisniRad
|
||||||
|
for _, rad := range radovi {
|
||||||
|
if rad.Predlozeno {
|
||||||
|
predlozeniRadovi = append(predlozeniRadovi, rad)
|
||||||
|
} else {
|
||||||
|
ugradjeniRadovi = append(ugradjeniRadovi, rad)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
appdb := appdbPkg.ArtikalFilter{}
|
appdb := appdbPkg.ArtikalFilter{}
|
||||||
artikli, err := h.Artikli.Lista(r.Context(), appdb)
|
artikli, err := h.Artikli.Lista(r.Context(), appdb)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -643,8 +672,12 @@ func (h *Handler) DetaljiNaloga(w http.ResponseWriter, r *http.Request) {
|
|||||||
Tehnicari: tehnicari,
|
Tehnicari: tehnicari,
|
||||||
GarancijaDefault: garancijaDefault,
|
GarancijaDefault: garancijaDefault,
|
||||||
BezGarancije: bezGarancije,
|
BezGarancije: bezGarancije,
|
||||||
ServisniDelovi: delovi,
|
ServisniDelovi: ugradjeniDelovi,
|
||||||
ServisniRadovi: radovi,
|
ServisniRadovi: ugradjeniRadovi,
|
||||||
|
PredlozeniDelovi: predlozeniDelovi,
|
||||||
|
PredlozeniRadovi: predlozeniRadovi,
|
||||||
|
ImaPredlog: len(predlozeniDelovi) > 0 || len(predlozeniRadovi) > 0,
|
||||||
|
ZakljucaniUgradjeni: jePredlogStatus(nalog.Status),
|
||||||
Artikli: artikli,
|
Artikli: artikli,
|
||||||
Usluge: usluge,
|
Usluge: usluge,
|
||||||
Sacuvano: r.URL.Query().Get("sacuvano") == "1",
|
Sacuvano: r.URL.Query().Get("sacuvano") == "1",
|
||||||
@@ -662,6 +695,13 @@ func (h *Handler) DetaljiNaloga(w http.ResponseWriter, r *http.Request) {
|
|||||||
h.renderujTemplate(w, "servis_detalji", podaci)
|
h.renderujTemplate(w, "servis_detalji", podaci)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// jePredlogStatus vraća true ako se stavka dodata u datom statusu računa kao
|
||||||
|
// predlog servisera (sve posle prijema); dok je nalog „Primljeno" stavke su
|
||||||
|
// deo zahteva klijenta (ugrađene/tražene).
|
||||||
|
func jePredlogStatus(status string) bool {
|
||||||
|
return status != "" && status != model.StatusPrimljeno
|
||||||
|
}
|
||||||
|
|
||||||
// DodajDeloNalogu prima POST formu i dodaje artikal kao deo servisnog naloga
|
// DodajDeloNalogu prima POST formu i dodaje artikal kao deo servisnog naloga
|
||||||
func (h *Handler) DodajDeloNalogu(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) DodajDeloNalogu(w http.ResponseWriter, r *http.Request) {
|
||||||
k, ok := h.zahtevajDozvolu(w, r, "servis.izmeni")
|
k, ok := h.zahtevajDozvolu(w, r, "servis.izmeni")
|
||||||
@@ -707,8 +747,11 @@ func (h *Handler) DodajDeloNalogu(w http.ResponseWriter, r *http.Request) {
|
|||||||
imeArtikla = art.Naziv
|
imeArtikla = art.Naziv
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// forma „Predloženi delovi" šalje predlozeno=1; forma „Ugrađeni delovi" ne šalje
|
||||||
|
predlozeno := r.FormValue("predlozeno") == "1"
|
||||||
|
|
||||||
// atomično: ugradi ono što imamo (skida sa lagera, ne ide u minus), višak u potraživane
|
// atomično: ugradi ono što imamo (skida sa lagera, ne ide u minus), višak u potraživane
|
||||||
ugradjeno, nedostaje, err := h.ServisniDeloviRepo.UgradiIliPotrazuj(r.Context(), nalogID, artikalID, kolicina, cena, &k.ID)
|
ugradjeno, nedostaje, err := h.ServisniDeloviRepo.UgradiIliPotrazuj(r.Context(), nalogID, artikalID, kolicina, cena, &k.ID, predlozeno)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("greška pri dodavanju dela", "error", err)
|
slog.Error("greška pri dodavanju dela", "error", err)
|
||||||
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri dodavanju dela.")
|
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri dodavanju dela.")
|
||||||
@@ -746,24 +789,31 @@ func (h *Handler) deloviSaPotrazivanima(ctx context.Context, nalogID int64) ([]m
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return delovi, nil // ne prekidamo ako potraživani nisu dostupni
|
return delovi, nil // ne prekidamo ako potraživani nisu dostupni
|
||||||
}
|
}
|
||||||
potrMap := make(map[int64]int)
|
// ključ uključuje predlozeno: isti artikal može biti i ugrađen i predložen,
|
||||||
|
// pa se potraživane količine ne smeju mešati između te dve grupe
|
||||||
|
type kljucDela struct {
|
||||||
|
artikalID int64
|
||||||
|
predlozeno bool
|
||||||
|
}
|
||||||
|
potrMap := make(map[kljucDela]int)
|
||||||
for _, p := range potrazivani {
|
for _, p := range potrazivani {
|
||||||
potrMap[p.ArtikalID] += p.Kolicina
|
potrMap[kljucDela{p.ArtikalID, p.Predlozeno}] += p.Kolicina
|
||||||
}
|
}
|
||||||
for i := range delovi {
|
for i := range delovi {
|
||||||
if kol, ok := potrMap[delovi[i].ArtikalID]; ok {
|
k := kljucDela{delovi[i].ArtikalID, delovi[i].Predlozeno}
|
||||||
|
if kol, ok := potrMap[k]; ok {
|
||||||
delovi[i].Potrazivano = kol
|
delovi[i].Potrazivano = kol
|
||||||
delete(potrMap, delovi[i].ArtikalID)
|
delete(potrMap, k)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for artikalID, kol := range potrMap {
|
for k, kol := range potrMap {
|
||||||
naziv := ""
|
naziv := ""
|
||||||
if art, e := h.Artikli.DohvatiID(ctx, artikalID); e == nil && art != nil {
|
if art, e := h.Artikli.DohvatiID(ctx, k.artikalID); e == nil && art != nil {
|
||||||
naziv = art.Naziv
|
naziv = art.Naziv
|
||||||
}
|
}
|
||||||
delovi = append(delovi, model.ServisniDeoSaArtiklom{
|
delovi = append(delovi, model.ServisniDeoSaArtiklom{
|
||||||
ArtikalNaziv: naziv,
|
ArtikalNaziv: naziv,
|
||||||
ServisniDeo: model.ServisniDeo{ArtikalID: artikalID},
|
ServisniDeo: model.ServisniDeo{ArtikalID: k.artikalID, Predlozeno: k.predlozeno},
|
||||||
Potrazivano: kol,
|
Potrazivano: kol,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -848,7 +898,10 @@ func (h *Handler) DodajRadNalogu(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := h.ServisniRadoviRepo.Dodaj(r.Context(), nalogID, uslugaID, usluga.Naziv, kolicina, cena); err != nil {
|
// forma „Predložene usluge" šalje predlozeno=1; forma „Radovi" ne šalje
|
||||||
|
predlozeno := r.FormValue("predlozeno") == "1"
|
||||||
|
|
||||||
|
if _, err := h.ServisniRadoviRepo.Dodaj(r.Context(), nalogID, uslugaID, usluga.Naziv, kolicina, cena, predlozeno); err != nil {
|
||||||
slog.Error("greška pri dodavanju rada", "error", err)
|
slog.Error("greška pri dodavanju rada", "error", err)
|
||||||
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri dodavanju rada.")
|
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri dodavanju rada.")
|
||||||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(nalogID, 10), http.StatusSeeOther)
|
http.Redirect(w, r, "/servis/"+strconv.FormatInt(nalogID, 10), http.StatusSeeOther)
|
||||||
@@ -1135,9 +1188,10 @@ func logoZaDokument(podesavanja map[string]string) string {
|
|||||||
|
|
||||||
type PodaciRadnogNaloga struct {
|
type PodaciRadnogNaloga struct {
|
||||||
Nalog model.ServisniNalog
|
Nalog model.ServisniNalog
|
||||||
|
Radovi []model.ServisniRad
|
||||||
ServisniDelovi []model.ServisniDeoSaArtiklom
|
ServisniDelovi []model.ServisniDeoSaArtiklom
|
||||||
|
Klijent *model.Klijent
|
||||||
KlijentNaziv string
|
KlijentNaziv string
|
||||||
KlijentTelefon string
|
|
||||||
TehnicarNaziv string
|
TehnicarNaziv string
|
||||||
NazivFirme string
|
NazivFirme string
|
||||||
LogoPutanja string
|
LogoPutanja string
|
||||||
@@ -1158,6 +1212,12 @@ func (h *Handler) StampaRadnogNaloga(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
radovi, err := h.ServisniRadoviRepo.DohvatiZaNalog(r.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Greška pri učitavanju radova", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
delovi, err := h.deloviSaPotrazivanima(r.Context(), id)
|
delovi, err := h.deloviSaPotrazivanima(r.Context(), id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Greška pri učitavanju delova", http.StatusInternalServerError)
|
http.Error(w, "Greška pri učitavanju delova", http.StatusInternalServerError)
|
||||||
@@ -1170,17 +1230,17 @@ func (h *Handler) StampaRadnogNaloga(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var klijent *model.Klijent
|
||||||
klijentNaziv := ""
|
klijentNaziv := ""
|
||||||
klijentTelefon := ""
|
|
||||||
if nalog.KlijentID != nil {
|
if nalog.KlijentID != nil {
|
||||||
klijent, err := h.KlijentiRepo.DohvatiID(r.Context(), *nalog.KlijentID)
|
k, err := h.KlijentiRepo.DohvatiID(r.Context(), *nalog.KlijentID)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if klijent.NazivFirme != "" {
|
klijent = k
|
||||||
klijentNaziv = klijent.NazivFirme
|
if k.NazivFirme != "" {
|
||||||
|
klijentNaziv = k.NazivFirme
|
||||||
} else {
|
} else {
|
||||||
klijentNaziv = strings.TrimSpace(klijent.Ime + " " + klijent.Prezime)
|
klijentNaziv = strings.TrimSpace(k.Ime + " " + k.Prezime)
|
||||||
}
|
}
|
||||||
klijentTelefon = klijent.Telefon
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1194,9 +1254,10 @@ func (h *Handler) StampaRadnogNaloga(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
h.renderujStandalone(w, "servis_radni_nalog", PodaciRadnogNaloga{
|
h.renderujStandalone(w, "servis_radni_nalog", PodaciRadnogNaloga{
|
||||||
Nalog: *nalog,
|
Nalog: *nalog,
|
||||||
|
Radovi: radovi,
|
||||||
ServisniDelovi: delovi,
|
ServisniDelovi: delovi,
|
||||||
|
Klijent: klijent,
|
||||||
KlijentNaziv: klijentNaziv,
|
KlijentNaziv: klijentNaziv,
|
||||||
KlijentTelefon: klijentTelefon,
|
|
||||||
TehnicarNaziv: tehnicarNaziv,
|
TehnicarNaziv: tehnicarNaziv,
|
||||||
NazivFirme: podesavanja["naziv_firme"],
|
NazivFirme: podesavanja["naziv_firme"],
|
||||||
LogoPutanja: logoZaDokument(podesavanja),
|
LogoPutanja: logoZaDokument(podesavanja),
|
||||||
@@ -1947,20 +2008,24 @@ func (h *Handler) OdbijPopravku(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// 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
|
||||||
NazivFirme string
|
NazivFirme string
|
||||||
LogoPutanja string
|
LogoPutanja string
|
||||||
Podnazlov string
|
Podnazlov string
|
||||||
Adresa string
|
Adresa string
|
||||||
Telefon string
|
Telefon string
|
||||||
PIB string
|
PIB string
|
||||||
MaticniBroj string
|
MaticniBroj string
|
||||||
Radovi []model.ServisniRad
|
Radovi []model.ServisniRad // ugrađeni/odobreni
|
||||||
UkupnoRad float64
|
ServisniDelovi []model.ServisniDeoSaArtiklom // ugrađeni/odobreni
|
||||||
ServisniDelovi []model.ServisniDeoSaArtiklom
|
UkupnoSve float64 // zbir ugrađenih (radovi + delovi)
|
||||||
UkupnoDelovi float64
|
PredlozeniRadovi []model.ServisniRad
|
||||||
UkupnoSve float64
|
PredlozeniDelovi []model.ServisniDeoSaArtiklom
|
||||||
SviStatusi []string
|
UkupnoPredlog float64 // zbir predloženih (radovi + delovi)
|
||||||
|
ImaPredlog bool
|
||||||
|
UkupnoSaPredlogom float64 // ukupno ugrađeno + predloženo
|
||||||
|
Odgovoreno bool // klijent već odgovorio (nema predloga, nije „Primljeno")
|
||||||
|
SviStatusi []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServisJavniStatus prikazuje javnu status stranicu — dostupna bez prijave putem QR koda
|
// ServisJavniStatus prikazuje javnu status stranicu — dostupna bez prijave putem QR koda
|
||||||
@@ -1985,31 +2050,51 @@ func (h *Handler) ServisJavniStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
radovi, _ := h.ServisniRadoviRepo.DohvatiZaNalog(r.Context(), nalog.ID)
|
radovi, _ := h.ServisniRadoviRepo.DohvatiZaNalog(r.Context(), nalog.ID)
|
||||||
delovi, _ := h.deloviSaPotrazivanima(r.Context(), nalog.ID)
|
delovi, _ := h.deloviSaPotrazivanima(r.Context(), nalog.ID)
|
||||||
var ukupnoRad, ukupnoDelovi float64
|
|
||||||
|
// razdvajamo ugrađene/odobrene od predloženih (čekaju odluku klijenta)
|
||||||
|
var ugrRadovi, predRadovi []model.ServisniRad
|
||||||
|
var ukupnoSve, ukupnoPredlog float64
|
||||||
for _, rd := range radovi {
|
for _, rd := range radovi {
|
||||||
ukupnoRad += rd.Ukupno()
|
if rd.Predlozeno {
|
||||||
|
predRadovi = append(predRadovi, rd)
|
||||||
|
ukupnoPredlog += rd.Ukupno()
|
||||||
|
} else {
|
||||||
|
ugrRadovi = append(ugrRadovi, rd)
|
||||||
|
ukupnoSve += rd.Ukupno()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
var ugrDelovi, predDelovi []model.ServisniDeoSaArtiklom
|
||||||
for _, d := range delovi {
|
for _, d := range delovi {
|
||||||
ukupnoDelovi += d.Ukupno()
|
if d.Predlozeno {
|
||||||
|
predDelovi = append(predDelovi, d)
|
||||||
|
ukupnoPredlog += d.Ukupno()
|
||||||
|
} else {
|
||||||
|
ugrDelovi = append(ugrDelovi, d)
|
||||||
|
ukupnoSve += d.Ukupno()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||||
|
|
||||||
h.renderujStandalone(w, "servis_status_javni", PodaciJavnogStatusa{
|
h.renderujStandalone(w, "servis_status_javni", PodaciJavnogStatusa{
|
||||||
Nalog: *nalog,
|
Nalog: *nalog,
|
||||||
NazivFirme: podesavanja["naziv_firme"],
|
NazivFirme: podesavanja["naziv_firme"],
|
||||||
LogoPutanja: logoZaDokument(podesavanja),
|
LogoPutanja: logoZaDokument(podesavanja),
|
||||||
Podnazlov: podesavanja["podnazlov"],
|
Podnazlov: podesavanja["podnazlov"],
|
||||||
Adresa: podesavanja["adresa"],
|
Adresa: podesavanja["adresa"],
|
||||||
Telefon: podesavanja["telefon"],
|
Telefon: podesavanja["telefon"],
|
||||||
PIB: podesavanja["pib"],
|
PIB: podesavanja["pib"],
|
||||||
MaticniBroj: podesavanja["maticni_broj"],
|
MaticniBroj: podesavanja["maticni_broj"],
|
||||||
Radovi: radovi,
|
Radovi: ugrRadovi,
|
||||||
UkupnoRad: ukupnoRad,
|
ServisniDelovi: ugrDelovi,
|
||||||
ServisniDelovi: delovi,
|
UkupnoSve: ukupnoSve,
|
||||||
UkupnoDelovi: ukupnoDelovi,
|
PredlozeniRadovi: predRadovi,
|
||||||
UkupnoSve: ukupnoRad + ukupnoDelovi,
|
PredlozeniDelovi: predDelovi,
|
||||||
SviStatusi: model.SviStatusi,
|
UkupnoPredlog: ukupnoPredlog,
|
||||||
|
ImaPredlog: len(predRadovi) > 0 || len(predDelovi) > 0,
|
||||||
|
UkupnoSaPredlogom: ukupnoSve + ukupnoPredlog,
|
||||||
|
Odgovoreno: !(len(predRadovi) > 0 || len(predDelovi) > 0) && nalog.Status != model.StatusPrimljeno,
|
||||||
|
SviStatusi: model.SviStatusi,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2030,3 +2115,104 @@ func qrNalogURL(r *http.Request, token, bazniURL string) string {
|
|||||||
}
|
}
|
||||||
return scheme + "://" + r.Host + "/status/" + token
|
return scheme + "://" + r.Host + "/status/" + token
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ServisJavniPrihvati obrađuje klijentovo prihvatanje predloženih stavki.
|
||||||
|
// Sve predložene stavke (radovi i delovi) prelaze u ugrađene (predlozeno 1→0).
|
||||||
|
func (h *Handler) ServisJavniPrihvati(w http.ResponseWriter, r *http.Request) {
|
||||||
|
token := chi.URLParam(r, "token")
|
||||||
|
if token == "" {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nalog, err := h.ServisRepo.DohvatiJavniToken(r.Context(), token)
|
||||||
|
if err != nil {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if nalog.Status == model.StatusPreuzeto {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.ServisniRadoviRepo.PrihvatiPredlozene(r.Context(), nalog.ID); err != nil {
|
||||||
|
slog.Error("greška pri prihvatanju predloženih radova", "error", err, "nalog_id", nalog.ID)
|
||||||
|
http.Error(w, "Greška pri prihvatanju predloga.", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := h.ServisniDeloviRepo.PrihvatiPredlozene(r.Context(), nalog.ID); err != nil {
|
||||||
|
slog.Error("greška pri prihvatanju predloženih delova", "error", err, "nalog_id", nalog.ID)
|
||||||
|
http.Error(w, "Greška pri prihvatanju predloga.", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// sačuvaj komentar klijenta
|
||||||
|
if komentar := strings.TrimSpace(r.FormValue("komentar")); komentar != "" {
|
||||||
|
if err := h.ServisRepo.AzurirajKomentarKlijenta(r.Context(), nalog.ID, komentar); err != nil {
|
||||||
|
slog.Error("greška pri čuvanju komentara klijenta", "error", err, "nalog_id", nalog.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Redirect(w, r, "/status/"+token, http.StatusSeeOther)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ObrisiKomentarKlijenta briše poruku klijenta sa naloga
|
||||||
|
func (h *Handler) ObrisiKomentarKlijenta(w http.ResponseWriter, r *http.Request) {
|
||||||
|
_, ok := h.zahtevajDozvolu(w, r, "servis.izmeni")
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.ServisRepo.AzurirajKomentarKlijenta(r.Context(), id, ""); err != nil {
|
||||||
|
slog.Error("greška pri brisanju komentara klijenta", "error", err, "nalog_id", id)
|
||||||
|
http.Error(w, "Greška pri brisanju komentara.", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Redirect(w, r, "/servis/"+chi.URLParam(r, "id"), http.StatusSeeOther)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServisJavniOdbij beleži da je klijent video predloge ali ih ne prihvata.
|
||||||
|
// Ne briše stavke — serviser ih ručno uklanja ako želi.
|
||||||
|
func (h *Handler) ServisJavniOdbij(w http.ResponseWriter, r *http.Request) {
|
||||||
|
token := chi.URLParam(r, "token")
|
||||||
|
if token == "" {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nalog, err := h.ServisRepo.DohvatiJavniToken(r.Context(), token)
|
||||||
|
if err != nil {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if nalog.Status == model.StatusPreuzeto {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// brišemo predložene stavke — klijent ih je odbio
|
||||||
|
if err := h.ServisniRadoviRepo.ObrisiPredlozene(r.Context(), nalog.ID); err != nil {
|
||||||
|
slog.Error("greška pri brisanju predloženih radova", "error", err, "nalog_id", nalog.ID)
|
||||||
|
}
|
||||||
|
if err := h.ServisniDeloviRepo.ObrisiPredlozene(r.Context(), nalog.ID); err != nil {
|
||||||
|
slog.Error("greška pri brisanju predloženih delova", "error", err, "nalog_id", nalog.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sačuvaj komentar klijenta
|
||||||
|
if komentar := strings.TrimSpace(r.FormValue("komentar")); komentar != "" {
|
||||||
|
if err := h.ServisRepo.AzurirajKomentarKlijenta(r.Context(), nalog.ID, komentar); err != nil {
|
||||||
|
slog.Error("greška pri čuvanju komentara klijenta", "error", err, "nalog_id", nalog.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Redirect(w, r, "/status/"+token, http.StatusSeeOther)
|
||||||
|
}
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ type ServisniNalog struct {
|
|||||||
Uradjeno string // šta je serviser stvarno uradio tokom popravke
|
Uradjeno string // šta je serviser stvarno uradio tokom popravke
|
||||||
CenaDijagnostike float64 // taksa kad klijent ne prihvati popravku; 0 = ne naplaćuje se
|
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
|
PopravkaOdbijena bool // klijent odbio popravku posle dijagnostike → naplaćuje se samo dijagnostika
|
||||||
|
KomentarKlijenta string // poruka klijenta uz prihvatanje/odbijanje predloga
|
||||||
JavniToken string
|
JavniToken string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,6 +66,7 @@ type ServisniDeo struct {
|
|||||||
Kolicina int
|
Kolicina int
|
||||||
CenaKomada float64
|
CenaKomada float64
|
||||||
Datum time.Time
|
Datum time.Time
|
||||||
|
Predlozeno bool // true = serviser predložio posle dijagnostike; čeka odobrenje klijenta
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ukupno vraća ukupnu vrednost dela (kolicina × cena)
|
// Ukupno vraća ukupnu vrednost dela (kolicina × cena)
|
||||||
@@ -88,6 +90,7 @@ type ServisniPotrazivaniDeo struct {
|
|||||||
Kolicina int
|
Kolicina int
|
||||||
CenaKomada float64
|
CenaKomada float64
|
||||||
Datum time.Time
|
Datum time.Time
|
||||||
|
Predlozeno bool
|
||||||
// za prikaz:
|
// za prikaz:
|
||||||
ArtikalNaziv string
|
ArtikalNaziv string
|
||||||
}
|
}
|
||||||
@@ -102,6 +105,7 @@ type ServisniRad struct {
|
|||||||
Kolicina float64
|
Kolicina float64
|
||||||
CenaKomada float64
|
CenaKomada float64
|
||||||
Datum string
|
Datum string
|
||||||
|
Predlozeno bool // true = serviser predložio posle dijagnostike; čeka odobrenje klijenta
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ukupno vraća ukupnu vrednost rada (kolicina × cena)
|
// Ukupno vraća ukupnu vrednost rada (kolicina × cena)
|
||||||
@@ -113,6 +117,7 @@ func (r ServisniRad) Ukupno() float64 {
|
|||||||
type ServisniNalogSaKlijentom struct {
|
type ServisniNalogSaKlijentom struct {
|
||||||
ServisniNalog
|
ServisniNalog
|
||||||
KlijentNaziv string
|
KlijentNaziv string
|
||||||
|
ImaPredlog bool // ima predloženih stavki koje čekaju odobrenje klijenta
|
||||||
}
|
}
|
||||||
|
|
||||||
// KlijentIDVrednost vraća vrednost KlijentID pointera, ili 0 ako je nil
|
// KlijentIDVrednost vraća vrednost KlijentID pointera, ili 0 ako je nil
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
-- predlozeno: razdvaja stavke unete pri prijemu (zahtev klijenta, predlozeno=0)
|
||||||
|
-- od onih koje serviser predlaže posle dijagnostike (predlozeno=1).
|
||||||
|
-- Predložene stavke klijent vidi obojeno na status stranici i odobrava ih;
|
||||||
|
-- prihvatanjem prelaze u ugrađene (predlozeno → 0).
|
||||||
|
ALTER TABLE servisni_delovi ADD COLUMN predlozeno INTEGER NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE servis_radovi ADD COLUMN predlozeno INTEGER NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE servisni_potrazivani_delovi ADD COLUMN predlozeno INTEGER NOT NULL DEFAULT 0;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- komentar klijenta uz odluku o predlogu (prihvatanje/odbijanje)
|
||||||
|
ALTER TABLE servisni_nalozi ADD COLUMN komentar_klijenta TEXT DEFAULT '';
|
||||||
@@ -1308,6 +1308,9 @@ select {
|
|||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.12);
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.12);
|
||||||
}
|
}
|
||||||
|
.kanban-ceka-odobrenje {
|
||||||
|
border-left: 3px solid #f59e0b;
|
||||||
|
}
|
||||||
/* dugme uz SN broj — otvara internu napomenu */
|
/* dugme uz SN broj — otvara internu napomenu */
|
||||||
.napomena-omot {
|
.napomena-omot {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@@ -1365,6 +1368,13 @@ select {
|
|||||||
/* forme „Dodaj rad/deo" u detaljima naloga — grid u CSS (ne inline) da ga @media može nadjačati */
|
/* forme „Dodaj rad/deo" u detaljima naloga — grid u CSS (ne inline) da ga @media može nadjačati */
|
||||||
.detalji-forma-rad { display: grid; grid-template-columns: 1fr 80px 120px auto; gap: 10px; align-items: flex-end; }
|
.detalji-forma-rad { display: grid; grid-template-columns: 1fr 80px 120px auto; gap: 10px; align-items: flex-end; }
|
||||||
.detalji-forma-deo { display: grid; grid-template-columns: 1fr 70px 80px 120px auto; gap: 10px; align-items: flex-end; }
|
.detalji-forma-deo { display: grid; grid-template-columns: 1fr 70px 80px 120px auto; gap: 10px; align-items: flex-end; }
|
||||||
|
|
||||||
|
/* predlog servisera — kartica sa narandžastim akcentom i bedž „Predlog" */
|
||||||
|
.predlog-kartica { border-left: 3px solid #f59e0b; }
|
||||||
|
.predlog-bedz {
|
||||||
|
display: inline-block; padding: 2px 10px; border-radius: 20px;
|
||||||
|
font-size: 11px; font-weight: 600; background: #f59e0b; color: #fff;
|
||||||
|
}
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
/* na telefonu: dve kolone, dugme preko cele širine */
|
/* na telefonu: dve kolone, dugme preko cele širine */
|
||||||
.detalji-forma-rad,
|
.detalji-forma-rad,
|
||||||
|
|||||||
@@ -235,7 +235,7 @@ document.addEventListener('alpine:init', () => {
|
|||||||
const punoIme = ((k.Ime || '') + ' ' + (k.Prezime || '')).trim()
|
const punoIme = ((k.Ime || '') + ' ' + (k.Prezime || '')).trim()
|
||||||
const polja = (tip === 'pravno') ? [k.NazivFirme] : [punoIme, k.Ime, k.Prezime]
|
const polja = (tip === 'pravno') ? [k.NazivFirme] : [punoIme, k.Ime, k.Prezime]
|
||||||
return polja.some(p => (p || '').toLowerCase().startsWith(q))
|
return polja.some(p => (p || '').toLowerCase().startsWith(q))
|
||||||
}).slice(0, 8)
|
}).slice(0, 100)
|
||||||
},
|
},
|
||||||
jeTip(t) {
|
jeTip(t) {
|
||||||
return this.tip === t
|
return this.tip === t
|
||||||
|
|||||||
@@ -48,12 +48,11 @@
|
|||||||
<div class="kanban-kartice">
|
<div class="kanban-kartice">
|
||||||
{{range $.Nalozi}}
|
{{range $.Nalozi}}
|
||||||
{{if eq .Status $status}}
|
{{if eq .Status $status}}
|
||||||
<a hx-boost="true" hx-target="#glavni-sadrzaj" hx-select="#glavni-sadrzaj" hx-swap="outerHTML" href="/servis/{{.ID}}" class="kartica kanban-karta">
|
<a hx-boost="true" hx-target="#glavni-sadrzaj" hx-select="#glavni-sadrzaj" hx-swap="outerHTML" href="/servis/{{.ID}}" class="kartica kanban-karta{{if .ImaPredlog}} kanban-ceka-odobrenje{{end}}">
|
||||||
<div style="display:flex;align-items:center;gap:6px;">
|
<div style="display:flex;align-items:center;gap:6px;">
|
||||||
|
{{if .ImaPredlog}}<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:#f59e0b;flex-shrink:0;" title="Čeka odobrenje klijenta"></span>{{end}}
|
||||||
<span style="font-size:14px;font-weight:500;color:var(--sb-akcent);font-family:monospace;">{{.BrojNaloga}}</span>
|
<span style="font-size:14px;font-weight:500;color:var(--sb-akcent);font-family:monospace;">{{.BrojNaloga}}</span>
|
||||||
{{if .Napomena}}
|
{{if .Napomena}}
|
||||||
<!-- dugme uz SN broj otvara internu napomenu (hover na desktopu, tap→focus na mobilnom);
|
|
||||||
onclick sprečava da klik na dugme otvori sam nalog (kartica je hx-boost link) -->
|
|
||||||
<span class="napomena-omot">
|
<span class="napomena-omot">
|
||||||
<button type="button" class="napomena-dugme" aria-label="Interna napomena"
|
<button type="button" class="napomena-dugme" aria-label="Interna napomena"
|
||||||
onclick="event.preventDefault();event.stopPropagation();">
|
onclick="event.preventDefault();event.stopPropagation();">
|
||||||
@@ -68,6 +67,13 @@
|
|||||||
<div class="karta-napomena">{{.Napomena}}</div>
|
<div class="karta-napomena">{{.Napomena}}</div>
|
||||||
</span>
|
</span>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
{{if .KomentarKlijenta}}
|
||||||
|
<span class="napomena-omot">
|
||||||
|
<button type="button" class="napomena-dugme" aria-label="Poruka klijenta"
|
||||||
|
onclick="event.preventDefault();event.stopPropagation();">💬</button>
|
||||||
|
<div class="karta-napomena" style="border-color:#f59e0b;">{{.KomentarKlijenta}}</div>
|
||||||
|
</span>
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
<div style="font-size:13px;color:var(--tekst-glavni);margin-top:4px;">{{.Uredjaj}}</div>
|
<div style="font-size:13px;color:var(--tekst-glavni);margin-top:4px;">{{.Uredjaj}}</div>
|
||||||
{{if .KlijentNaziv}}
|
{{if .KlijentNaziv}}
|
||||||
|
|||||||
@@ -166,6 +166,24 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- poruka klijenta uz odluku o predlogu -->
|
||||||
|
{{if .Nalog.KomentarKlijenta}}
|
||||||
|
<div style="margin-bottom:16px;padding:14px 16px;background:rgba(245,158,11,0.08);border:1px solid rgba(245,158,11,0.3);border-radius:10px;">
|
||||||
|
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;">
|
||||||
|
{{if index .Dozvole "servis.izmeni"}}
|
||||||
|
<form method="POST" action="/servis/{{.Nalog.ID}}/obrisi-komentar-klijenta" style="margin:0;" data-refresh="#nalog-zaglavlje">
|
||||||
|
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
|
||||||
|
<button type="submit" data-potvrda="Obrisati poruku klijenta?" style="background:none;border:none;cursor:pointer;color:#94a3b8;font-size:16px;padding:0 2px;line-height:1;">×</button>
|
||||||
|
</form>
|
||||||
|
{{end}}
|
||||||
|
<span style="font-size:14px;">💬</span>
|
||||||
|
<span style="font-size:12px;font-weight:600;color:#f59e0b;text-transform:uppercase;letter-spacing:0.5px;flex:1;">Poruka klijenta</span>
|
||||||
|
</div>
|
||||||
|
<div style="font-size:14px;color:var(--tekst-glavni);line-height:1.5;white-space:pre-wrap;">{{.Nalog.KomentarKlijenta}}</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
<div style="display:grid;grid-template-columns:repeat(auto-fit, minmax(140px, 1fr));gap:16px;">
|
<div style="display:grid;grid-template-columns:repeat(auto-fit, minmax(140px, 1fr));gap:16px;">
|
||||||
<div>
|
<div>
|
||||||
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:4px;">Datum prijema</div>
|
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:4px;">Datum prijema</div>
|
||||||
@@ -426,16 +444,18 @@
|
|||||||
{{/* kartice radova/delova se ne prikazuju kad je popravka odbijena (samo dijagnostika)
|
{{/* kartice radova/delova se ne prikazuju kad je popravka odbijena (samo dijagnostika)
|
||||||
ni kad je nalog „Završeno"/„Preuzeto" (posao gotov — sume ostaju u kartici Ukupno) */}}
|
ni kad je nalog „Završeno"/„Preuzeto" (posao gotov — sume ostaju u kartici Ukupno) */}}
|
||||||
{{if and (not .Nalog.PopravkaOdbijena) (ne .Nalog.Status "Završeno") (ne .Nalog.Status "Preuzeto")}}
|
{{if and (not .Nalog.PopravkaOdbijena) (ne .Nalog.Status "Završeno") (ne .Nalog.Status "Preuzeto")}}
|
||||||
<!-- radovi (usluge) -->
|
<div id="radovi-blok">
|
||||||
|
<!-- radovi (usluge) — traženi pri prijemu -->
|
||||||
<div class="kartica detalji-kartica animiraj" id="radovi-kartica">
|
<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 — u dijagnostici se automatski beleži kao predlog -->
|
||||||
{{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-kartica, #nalog-ukupno">
|
<form method="POST" action="/servis/{{.Nalog.ID}}/radovi" style="margin-bottom:16px;" data-refresh="#radovi-blok, #nalog-ukupno">
|
||||||
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
|
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
|
||||||
|
{{if .ZakljucaniUgradjeni}}<input type="hidden" name="predlozeno" value="1">{{end}}
|
||||||
<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>
|
||||||
@@ -461,7 +481,7 @@
|
|||||||
|
|
||||||
<!-- lista radova -->
|
<!-- lista radova -->
|
||||||
<div id="radovi-spisak">
|
<div id="radovi-spisak">
|
||||||
{{if .ServisniRadovi}}
|
{{if or .ServisniRadovi .PredlozeniRadovi}}
|
||||||
<div class="tabela-skrol">
|
<div class="tabela-skrol">
|
||||||
<table class="tabela">
|
<table class="tabela">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -481,8 +501,8 @@
|
|||||||
<td style="padding:9px 10px;text-align:right;font-size:14px;color:var(--tekst-glavni);">{{dinari .CenaKomada}} din</td>
|
<td style="padding:9px 10px;text-align:right;font-size:14px;color:var(--tekst-glavni);">{{dinari .CenaKomada}} 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: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 and (not $.ZakljucaniUgradjeni) (index $.Dozvole "servis.izmeni")}}
|
||||||
<form method="POST" action="/servis/{{$.Nalog.ID}}/radovi/{{.ID}}/obrisi" style="display:inline;" data-refresh="#radovi-kartica, #nalog-ukupno">
|
<form method="POST" action="/servis/{{$.Nalog.ID}}/radovi/{{.ID}}/obrisi" style="display:inline;" data-refresh="#radovi-blok, #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}}'?"
|
||||||
@@ -494,6 +514,26 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
{{range .PredlozeniRadovi}}
|
||||||
|
<tr style="border-bottom:0.5px solid var(--ivica);background:rgba(245,158,11,0.08);">
|
||||||
|
<td style="padding:9px 10px;font-size:14px;color:var(--tekst-glavni);">{{.Naziv}} <span class="predlog-bedz" style="font-size:10px;padding:1px 7px;">Predlog</span></td>
|
||||||
|
<td style="padding:9px 10px;text-align:center;font-size:14px;color:var(--tekst-glavni);">{{.Kolicina}}</td>
|
||||||
|
<td style="padding:9px 10px;text-align:right;font-size:14px;color:var(--tekst-glavni);">{{dinari .CenaKomada}} 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;">
|
||||||
|
{{if index $.Dozvole "servis.izmeni"}}
|
||||||
|
<form method="POST" action="/servis/{{$.Nalog.ID}}/radovi/{{.ID}}/obrisi" style="display:inline;" data-refresh="#radovi-blok, #nalog-ukupno">
|
||||||
|
<input type="hidden" name="_csrf" value="{{$.CsrfToken}}">
|
||||||
|
<button type="submit"
|
||||||
|
data-potvrda="Ukloniti predloženi rad '{{.Naziv}}'?"
|
||||||
|
style="background:none;border:none;cursor:pointer;color:#dc2626;font-size:16px;padding:2px 6px;border-radius:4px;">
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{{end}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -504,17 +544,20 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- ugrađeni delovi -->
|
<!-- ugrađeni delovi -->
|
||||||
<div class="kartica detalji-kartica animiraj">
|
<div id="delovi-blok">
|
||||||
|
<div class="kartica detalji-kartica animiraj" id="delovi-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);">Ugrađeni delovi</span>
|
<span style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">Delovi</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- forma za dodavanje dela -->
|
<!-- forma za dodavanje dela — u dijagnostici se automatski beleži kao predlog -->
|
||||||
{{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, #nalog-ukupno">
|
<form method="POST" action="/servis/{{.Nalog.ID}}/delovi" style="margin-bottom:16px;" data-refresh="#delovi-blok, #nalog-zaglavlje, #nalog-ukupno">
|
||||||
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
|
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
|
||||||
|
{{if .ZakljucaniUgradjeni}}<input type="hidden" name="predlozeno" value="1">{{end}}
|
||||||
<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>
|
||||||
<select id="deo-kategorija" style="width:100%;"></select>
|
<select id="deo-kategorija" style="width:100%;"></select>
|
||||||
@@ -547,7 +590,7 @@
|
|||||||
|
|
||||||
<!-- lista delova -->
|
<!-- lista delova -->
|
||||||
<div id="delovi-spisak">
|
<div id="delovi-spisak">
|
||||||
{{if .ServisniDelovi}}
|
{{if or .ServisniDelovi .PredlozeniDelovi}}
|
||||||
<div class="tabela-skrol">
|
<div class="tabela-skrol">
|
||||||
<table class="tabela">
|
<table class="tabela">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -573,8 +616,8 @@
|
|||||||
{{if .Kolicina}}{{dinari .Ukupno}} din{{else}}—{{end}}
|
{{if .Kolicina}}{{dinari .Ukupno}} din{{else}}—{{end}}
|
||||||
</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 (not $.ZakljucaniUgradjeni) (index $.Dozvole "servis.izmeni")}}
|
||||||
<form method="POST" action="/servis/{{$.Nalog.ID}}/delovi/{{.ID}}/obrisi" style="display:inline;" data-refresh="#delovi-spisak, #nalog-zaglavlje, #nalog-ukupno">
|
<form method="POST" action="/servis/{{$.Nalog.ID}}/delovi/{{.ID}}/obrisi" style="display:inline;" data-refresh="#delovi-blok, #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}}'?"
|
||||||
@@ -586,18 +629,47 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
{{range .PredlozeniDelovi}}
|
||||||
|
<tr data-artikal-id="{{.ArtikalID}}" data-kolicina="{{.Kolicina}}" data-potrazivano="{{.Potrazivano}}" style="border-bottom:0.5px solid var(--ivica);background:rgba(245,158,11,0.08);{{if .Potrazivano}}background:rgba(220,38,38,0.06);{{end}}">
|
||||||
|
<td style="padding:9px 10px;font-size:14px;color:var(--tekst-glavni);">{{.ArtikalNaziv}} <span class="predlog-bedz" style="font-size:10px;padding:1px 7px;">Predlog</span></td>
|
||||||
|
<td style="padding:9px 10px;text-align:center;font-size:14px;{{if .Potrazivano}}color:#dc2626;font-weight:600;{{else}}color:var(--tekst-glavni);{{end}}">
|
||||||
|
{{.Kolicina}}{{if .Potrazivano}}<span style="font-weight:400;"> + {{.Potrazivano}} na čekanju</span>{{end}}
|
||||||
|
</td>
|
||||||
|
<td style="padding:9px 10px;text-align:right;font-size:14px;color:var(--tekst-glavni);">
|
||||||
|
{{if .Kolicina}}{{dinari .CenaKomada}} din{{else}}—{{end}}
|
||||||
|
</td>
|
||||||
|
<td style="padding:9px 10px;text-align:right;font-size:14px;font-weight:500;color:var(--tekst-glavni);">
|
||||||
|
{{if .Kolicina}}{{dinari .Ukupno}} din{{else}}—{{end}}
|
||||||
|
</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-blok, #nalog-zaglavlje, #nalog-ukupno">
|
||||||
|
<input type="hidden" name="_csrf" value="{{$.CsrfToken}}">
|
||||||
|
<button type="submit"
|
||||||
|
data-potvrda="Ukloniti predloženi deo '{{.ArtikalNaziv}}'?"
|
||||||
|
style="background:none;border:none;cursor:pointer;color:#dc2626;font-size:16px;padding:2px 6px;border-radius:4px;">
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{{end}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{{else}}
|
{{else}}
|
||||||
<div style="font-size:13px;color:var(--tekst-sporedni);text-align:center;padding:16px 0;">
|
<div style="font-size:13px;color:var(--tekst-sporedni);text-align:center;padding:16px 0;">
|
||||||
Nema ugrađenih delova za ovaj nalog.
|
Nema delova za ovaj nalog.
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- ukupno: zbir usluga (radova) i ugrađenih delova -->
|
<!-- ukupno: zbir usluga (radova) i ugrađenih delova -->
|
||||||
<div id="nalog-ukupno" 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);">
|
||||||
|
|||||||
@@ -122,8 +122,22 @@
|
|||||||
<div class="odeljak-naslov">Uređaj i traženi radovi</div>
|
<div class="odeljak-naslov">Uređaj i traženi radovi</div>
|
||||||
<div style="margin-bottom:6px;"><strong>Uređaj:</strong> {{if .Nalog.Uredjaj}}{{.Nalog.Uredjaj}}{{else}}<span class="prazno">—</span>{{end}}{{if .Nalog.SerijskiBroj}} · S/N: {{.Nalog.SerijskiBroj}}{{end}}</div>
|
<div style="margin-bottom:6px;"><strong>Uređaj:</strong> {{if .Nalog.Uredjaj}}{{.Nalog.Uredjaj}}{{else}}<span class="prazno">—</span>{{end}}{{if .Nalog.SerijskiBroj}} · S/N: {{.Nalog.SerijskiBroj}}{{end}}</div>
|
||||||
<div class="polje-tekst"><strong>Zahtev klijenta:</strong> {{if .Nalog.OpisKvara}}{{.Nalog.OpisKvara}}{{else}}<span class="prazno">—</span>{{end}}</div>
|
<div class="polje-tekst"><strong>Zahtev klijenta:</strong> {{if .Nalog.OpisKvara}}{{.Nalog.OpisKvara}}{{else}}<span class="prazno">—</span>{{end}}</div>
|
||||||
|
{{if .Nalog.TrazeneNadogradnje}}
|
||||||
|
<div class="polje-tekst" style="margin-top:6px;"><strong>Tražene nadogradnje:</strong> {{.Nalog.TrazeneNadogradnje}}</div>
|
||||||
|
{{end}}
|
||||||
|
{{if .Nalog.NapomenaKlijentu}}
|
||||||
|
<div class="polje-tekst" style="margin-top:6px;"><strong>Napomena klijentu:</strong> {{.Nalog.NapomenaKlijentu}}</div>
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- utvrđen kvar (rezultat dijagnostike) -->
|
||||||
|
{{if .Nalog.NalazDijagnostike}}
|
||||||
|
<div class="odeljak">
|
||||||
|
<div class="odeljak-naslov">Utvrđen kvar</div>
|
||||||
|
<div class="polje-tekst">{{.Nalog.NalazDijagnostike}}</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
<!-- radovi -->
|
<!-- radovi -->
|
||||||
{{if .Radovi}}
|
{{if .Radovi}}
|
||||||
<div class="odeljak">
|
<div class="odeljak">
|
||||||
|
|||||||
@@ -86,7 +86,14 @@
|
|||||||
<div class="zg-okvir" style="font-size:12px;line-height:1.6;">
|
<div class="zg-okvir" style="font-size:12px;line-height:1.6;">
|
||||||
<div class="polje-labela">Klijent</div>
|
<div class="polje-labela">Klijent</div>
|
||||||
<div class="polje-vrednost">{{if .KlijentNaziv}}{{.KlijentNaziv}}{{else}}<span class="prazno">—</span>{{end}}</div>
|
<div class="polje-vrednost">{{if .KlijentNaziv}}{{.KlijentNaziv}}{{else}}<span class="prazno">—</span>{{end}}</div>
|
||||||
{{if .KlijentTelefon}}<div style="color:#555;">Tel: {{telefon .KlijentTelefon}}</div>{{end}}
|
{{if .Klijent}}
|
||||||
|
<div style="color:#555;">
|
||||||
|
{{if .Klijent.Telefon}}Tel: {{telefon .Klijent.Telefon}}<br>{{end}}
|
||||||
|
{{if .Klijent.Email}}{{.Klijent.Email}}<br>{{end}}
|
||||||
|
{{if .Klijent.Mesto}}{{.Klijent.Mesto}}<br>{{end}}
|
||||||
|
{{if .Klijent.PIB}}PIB: {{.Klijent.PIB}}{{end}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
<!-- DESNO: barkod na vrhu, ispod broj + status + datumi + serviser -->
|
<!-- DESNO: barkod na vrhu, ispod broj + status + datumi + serviser -->
|
||||||
<div class="zg-okvir" style="flex:1.3;">
|
<div class="zg-okvir" style="flex:1.3;">
|
||||||
@@ -151,10 +158,49 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- utvrđen kvar (rezultat dijagnostike) -->
|
||||||
|
{{if .Nalog.NalazDijagnostike}}
|
||||||
|
<div class="odeljak">
|
||||||
|
<div class="odeljak-naslov">Utvrđeni kvar</div>
|
||||||
|
<div class="polje-tekst">{{.Nalog.NalazDijagnostike}}</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<!-- napomena klijentu -->
|
||||||
|
{{if .Nalog.NapomenaKlijentu}}
|
||||||
|
<div class="odeljak">
|
||||||
|
<div class="odeljak-naslov">Napomena klijentu</div>
|
||||||
|
<div class="polje-tekst">{{.Nalog.NapomenaKlijentu}}</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<!-- usluge koje serviser treba da uradi — bez cena -->
|
||||||
|
{{if .Radovi}}
|
||||||
|
<div class="odeljak">
|
||||||
|
<div class="odeljak-naslov">Usluge (radovi)</div>
|
||||||
|
<table class="tabela">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Usluga</th>
|
||||||
|
<th class="desno" style="width:90px;">Količina</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{range .Radovi}}
|
||||||
|
<tr>
|
||||||
|
<td>{{.Naziv}}</td>
|
||||||
|
<td class="desno">{{.Kolicina}}</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
<!-- rezervisani/ugrađeni delovi — bez cena -->
|
<!-- rezervisani/ugrađeni delovi — bez cena -->
|
||||||
{{if .ServisniDelovi}}
|
{{if .ServisniDelovi}}
|
||||||
<div class="odeljak">
|
<div class="odeljak">
|
||||||
<div class="odeljak-naslov">Delovi na nalogu</div>
|
<div class="odeljak-naslov">Delovi za ugradnju</div>
|
||||||
<table class="tabela">
|
<table class="tabela">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -174,17 +220,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<!-- prazna polja za servisera -->
|
|
||||||
<div class="odeljak">
|
|
||||||
<div class="upis-labela">Dijagnoza / utvrđen kvar</div>
|
|
||||||
<div class="upis-okvir upis-veliki"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="odeljak">
|
|
||||||
<div class="upis-labela">Urađen rad</div>
|
|
||||||
<div class="upis-okvir upis-veliki"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- vreme i potpis -->
|
<!-- vreme i potpis -->
|
||||||
<div class="zavrsetak">
|
<div class="zavrsetak">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -59,6 +59,8 @@
|
|||||||
.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; }
|
||||||
|
/* uslovi servisa (sa potpisima) idu na novu stranicu */
|
||||||
|
.uslovi-strana { break-before: page; }
|
||||||
}
|
}
|
||||||
.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; }
|
||||||
@@ -178,7 +180,7 @@
|
|||||||
|
|
||||||
<!-- uslovi servisa -->
|
<!-- uslovi servisa -->
|
||||||
{{if .Uslovi}}
|
{{if .Uslovi}}
|
||||||
<div class="odeljak">
|
<div class="odeljak uslovi-strana">
|
||||||
<div class="odeljak-naslov">Uslovi servisa</div>
|
<div class="odeljak-naslov">Uslovi servisa</div>
|
||||||
<div class="uslovi-blok">{{.Uslovi}}</div>
|
<div class="uslovi-blok">{{.Uslovi}}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,21 +6,21 @@
|
|||||||
<title>Status popravke — {{.Nalog.BrojNaloga}}</title>
|
<title>Status popravke — {{.Nalog.BrojNaloga}}</title>
|
||||||
<style>
|
<style>
|
||||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif; font-size: 14px; color: #111; background: #f5f5f7; min-height: 100vh; }
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif; font-size: 14px; color: #e2e8f0; background: #0f172a; min-height: 100vh; }
|
||||||
|
|
||||||
.omotac { max-width: 480px; margin: 0 auto; padding: 24px 16px 48px; }
|
.omotac { max-width: 480px; margin: 0 auto; padding: 24px 16px 48px; }
|
||||||
|
|
||||||
/* zaglavlje firme */
|
/* zaglavlje firme */
|
||||||
.zaglavlje { text-align: center; margin-bottom: 24px; }
|
.zaglavlje { text-align: center; margin-bottom: 24px; }
|
||||||
.firma-naziv { font-size: 18px; font-weight: 700; color: #111; }
|
.firma-naziv { font-size: 18px; font-weight: 700; color: #f1f5f9; }
|
||||||
.firma-info { font-size: 12px; color: #666; margin-top: 4px; line-height: 1.6; }
|
.firma-info { font-size: 12px; color: #94a3b8; margin-top: 4px; line-height: 1.6; }
|
||||||
|
|
||||||
/* kartica naloga */
|
/* kartica naloga */
|
||||||
.kartica { background: #fff; border-radius: 16px; padding: 20px; margin-bottom: 16px; box-shadow: 0 1px 4px rgba(0,0,0,0.08); }
|
.kartica { background: #1e293b; border-radius: 16px; padding: 20px; margin-bottom: 16px; box-shadow: 0 1px 4px rgba(0,0,0,0.35); }
|
||||||
.kartica-naslov { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.6px; color: #999; margin-bottom: 12px; }
|
.kartica-naslov { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.6px; color: #94a3b8; margin-bottom: 12px; }
|
||||||
|
|
||||||
.uredjaj { font-size: 22px; font-weight: 700; color: #111; line-height: 1.2; }
|
.uredjaj { font-size: 22px; font-weight: 700; color: #f1f5f9; line-height: 1.2; }
|
||||||
.broj-naloga { font-size: 13px; color: #888; margin-top: 4px; font-family: monospace; }
|
.broj-naloga { font-size: 13px; color: #94a3b8; margin-top: 4px; font-family: monospace; }
|
||||||
|
|
||||||
/* status bedž */
|
/* status bedž */
|
||||||
.status-bedz {
|
.status-bedz {
|
||||||
@@ -40,39 +40,63 @@
|
|||||||
|
|
||||||
/* napredak */
|
/* napredak */
|
||||||
.napredak { margin: 16px 0 4px; }
|
.napredak { margin: 16px 0 4px; }
|
||||||
.napredak-labela { font-size: 11px; color: #999; margin-bottom: 8px; }
|
.napredak-labela { font-size: 11px; color: #94a3b8; margin-bottom: 8px; }
|
||||||
.koraci { display: flex; gap: 4px; }
|
.koraci { display: flex; gap: 4px; }
|
||||||
.korak {
|
.korak {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 5px;
|
height: 5px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
background: #e5e7eb;
|
background: #334155;
|
||||||
}
|
}
|
||||||
.korak.aktivan { background: #10b981; }
|
.korak.aktivan { background: #10b981; }
|
||||||
.korak.zavrseno { background: #10b981; }
|
.korak.zavrseno { background: #10b981; }
|
||||||
.korak.tekuci { background: #f59e0b; }
|
.korak.tekuci { background: #f59e0b; }
|
||||||
|
|
||||||
/* 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 #334155; gap: 12px; }
|
||||||
.red:last-child { border-bottom: none; }
|
.red:last-child { border-bottom: none; }
|
||||||
.red-labela { font-size: 12px; color: #888; min-width: 0; overflow-wrap: anywhere; }
|
.red-labela { font-size: 12px; color: #94a3b8; 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; }
|
.red-vrednost { font-size: 13px; color: #f1f5f9; 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: #0f172a; border-radius: 10px; padding: 14px 16px; margin-top: 4px; }
|
||||||
.cena-od-do { display: flex; gap: 8px; align-items: center; }
|
.cena-od-do { display: flex; gap: 8px; align-items: center; }
|
||||||
.cena-od-do .cena-vrednost { font-size: 20px; font-weight: 700; color: #111; }
|
.cena-od-do .cena-vrednost { font-size: 20px; font-weight: 700; color: #f1f5f9; }
|
||||||
.cena-od-do .cena-sep { color: #bbb; }
|
.cena-od-do .cena-sep { color: #475569; }
|
||||||
.cena-labela { font-size: 11px; color: #888; margin-top: 2px; }
|
.cena-labela { font-size: 11px; color: #94a3b8; margin-top: 2px; }
|
||||||
|
.cena-vrednost { color: #f1f5f9; }
|
||||||
|
|
||||||
/* kontakt */
|
/* kontakt */
|
||||||
.kontakt { background: #fff; border-radius: 16px; padding: 18px 20px; box-shadow: 0 1px 4px rgba(0,0,0,0.08); }
|
.kontakt { background: #1e293b; border-radius: 16px; padding: 18px 20px; box-shadow: 0 1px 4px rgba(0,0,0,0.35); }
|
||||||
.kontakt-naslov { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.6px; color: #999; margin-bottom: 12px; }
|
.kontakt-naslov { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.6px; color: #94a3b8; margin-bottom: 12px; }
|
||||||
.kontakt-tel { font-size: 18px; font-weight: 700; color: #1a56db; text-decoration: none; display: block; }
|
.kontakt-tel { font-size: 18px; font-weight: 700; color: #60a5fa; text-decoration: none; display: block; }
|
||||||
.kontakt-adresa { font-size: 13px; color: #666; margin-top: 6px; line-height: 1.5; }
|
.kontakt-adresa { font-size: 13px; color: #94a3b8; margin-top: 6px; line-height: 1.5; }
|
||||||
|
|
||||||
/* napomena za klijenta */
|
/* napomena za klijenta */
|
||||||
.napomena-blok { background: #fffbeb; border-radius: 10px; padding: 12px 14px; margin-top: 4px; font-size: 13px; color: #555; line-height: 1.5; }
|
.napomena-blok { background: #0f172a; border-left: 3px solid #f59e0b; border-radius: 8px; padding: 12px 14px; margin-top: 4px; font-size: 13px; color: #cbd5e1; line-height: 1.5; white-space: pre-wrap; }
|
||||||
|
|
||||||
|
/* predlog popravke — istaknuta kartica koju klijent odobrava */
|
||||||
|
.predlog-kartica { border: 1px solid #f59e0b; box-shadow: 0 0 0 3px rgba(245,158,11,0.12); }
|
||||||
|
|
||||||
|
/* dugmad za odobravanje */
|
||||||
|
.dugme-prihvati {
|
||||||
|
width: 100%; padding: 12px 16px; border: none; border-radius: 10px;
|
||||||
|
font-size: 14px; font-weight: 600; cursor: pointer;
|
||||||
|
background: #10b981; color: #fff;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
.dugme-prihvati:hover { background: #059669; }
|
||||||
|
.dugme-odbij {
|
||||||
|
width: 100%; padding: 12px 16px; border: 1px solid #475569; border-radius: 10px;
|
||||||
|
font-size: 14px; font-weight: 500; cursor: pointer;
|
||||||
|
background: transparent; color: #94a3b8;
|
||||||
|
transition: background 0.2s, color 0.2s;
|
||||||
|
}
|
||||||
|
.dugme-odbij:hover { background: #334155; color: #e2e8f0; }
|
||||||
|
|
||||||
|
.predlog-bedz { display: inline-block; padding: 3px 10px; border-radius: 20px; font-size: 11px; font-weight: 600; background: #f59e0b; color: #1f1300; }
|
||||||
|
.predlog-ukupno .cena-labela { color: #fcd34d; }
|
||||||
|
.predlog-ukupno .cena-vrednost { color: #fcd34d; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -134,6 +158,34 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- kartica: zahtev klijenta (opis kvara, nadogradnje, napomena) -->
|
||||||
|
{{if or .Nalog.OpisKvara (or .Nalog.TrazeneNadogradnje .Nalog.NapomenaKlijentu)}}
|
||||||
|
<div class="kartica">
|
||||||
|
<div class="kartica-naslov">Vaš zahtev</div>
|
||||||
|
|
||||||
|
{{if .Nalog.OpisKvara}}
|
||||||
|
<div style="margin-bottom:12px;">
|
||||||
|
<div class="red-labela" style="margin-bottom:6px;">Opis kvara</div>
|
||||||
|
<div class="napomena-blok">{{.Nalog.OpisKvara}}</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if .Nalog.TrazeneNadogradnje}}
|
||||||
|
<div style="margin-bottom:12px;">
|
||||||
|
<div class="red-labela" style="margin-bottom:6px;">Tražene nadogradnje</div>
|
||||||
|
<div class="napomena-blok">{{.Nalog.TrazeneNadogradnje}}</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if .Nalog.NapomenaKlijentu}}
|
||||||
|
<div>
|
||||||
|
<div class="red-labela" style="margin-bottom:6px;">Napomena klijentu</div>
|
||||||
|
<div class="napomena-blok">{{.Nalog.NapomenaKlijentu}}</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
<!-- kartica: detalji -->
|
<!-- kartica: detalji -->
|
||||||
<div class="kartica">
|
<div class="kartica">
|
||||||
<div class="kartica-naslov">Detalji</div>
|
<div class="kartica-naslov">Detalji</div>
|
||||||
@@ -162,12 +214,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{if .Nalog.NapomenaKlijentu}}
|
|
||||||
<div style="margin-top:12px;">
|
|
||||||
<div class="red-labela" style="margin-bottom:6px;">Napomena</div>
|
|
||||||
<div class="napomena-blok">{{.Nalog.NapomenaKlijentu}}</div>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- kartica: radovi i delovi sa procenom -->
|
<!-- kartica: radovi i delovi sa procenom -->
|
||||||
@@ -193,6 +239,62 @@
|
|||||||
<span class="cena-labela" style="margin:0;">Ukupno (procena)</span>
|
<span class="cena-labela" style="margin:0;">Ukupno (procena)</span>
|
||||||
<span class="cena-vrednost">{{dinari .UkupnoSve}} din</span>
|
<span class="cena-vrednost">{{dinari .UkupnoSve}} din</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{if .ImaPredlog}}
|
||||||
|
<div class="cena-blok" style="margin-top:8px;padding:10px 14px;display:flex;justify-content:space-between;align-items:center;background:#0f172a;border:1px solid #f59e0b;border-radius:8px;">
|
||||||
|
<span class="cena-labela" style="margin:0;color:#fbbf24;">Sa predlogom</span>
|
||||||
|
<span class="cena-vrednost" style="font-size:16px;font-weight:700;color:#fbbf24;">{{dinari .UkupnoSaPredlogom}} din</span>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<!-- kartica: predlog popravke — serviser predložio posle dijagnostike -->
|
||||||
|
{{if .ImaPredlog}}
|
||||||
|
<div class="kartica predlog-kartica">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
|
||||||
|
<span class="kartica-naslov" style="margin:0;">Predlog popravke</span>
|
||||||
|
<span class="predlog-bedz">Predlog</span>
|
||||||
|
</div>
|
||||||
|
<div style="font-size:12px;color:#94a3b8;margin-bottom:10px;line-height:1.5;">
|
||||||
|
Serviser predlaže sledeće radove i delove. Pogledajte i javite nam da li ste saglasni.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{range .PredlozeniRadovi}}
|
||||||
|
<div class="red">
|
||||||
|
<span class="red-labela">{{.Naziv}}{{if gt .Kolicina 1.0}} ×{{.Kolicina}}{{end}}</span>
|
||||||
|
<span class="red-vrednost">{{dinari .Ukupno}} din</span>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{range .PredlozeniDelovi}}
|
||||||
|
<div class="red">
|
||||||
|
<span class="red-labela">{{.ArtikalNaziv}}{{if gt .Kolicina 1}} ×{{.Kolicina}}{{end}}</span>
|
||||||
|
<span class="red-vrednost">{{dinari .Ukupno}} din</span>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<div class="cena-blok predlog-ukupno" style="margin-top:12px;display:flex;justify-content:space-between;align-items:center;">
|
||||||
|
<span class="cena-labela" style="margin:0;">Predlog ukupno</span>
|
||||||
|
<span class="cena-vrednost">{{dinari .UkupnoPredlog}} din</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- komentar i dugmad -->
|
||||||
|
<form method="POST" style="margin-top:16px;">
|
||||||
|
<textarea name="komentar" placeholder="Poruka serviseru (opciono)..." rows="2"
|
||||||
|
style="width:100%;padding:10px 12px;border:0.5px solid #334155;border-radius:8px;background:#0f172a;color:#e2e8f0;font-size:13px;font-family:inherit;resize:vertical;margin-bottom:10px;box-sizing:border-box;"></textarea>
|
||||||
|
<div style="display:flex;gap:10px;">
|
||||||
|
<button type="submit" formaction="/status/{{.Nalog.JavniToken}}/prihvati" class="dugme-prihvati" style="flex:1;">Prihvatam predlog</button>
|
||||||
|
<button type="submit" formaction="/status/{{.Nalog.JavniToken}}/odbij" class="dugme-odbij" style="flex:1;">Ne prihvatam</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if .Odgovoreno}}
|
||||||
|
<div class="kartica" style="border-color:#10b981;text-align:center;">
|
||||||
|
<div style="font-size:16px;font-weight:600;color:#10b981;">Hvala na odgovoru!</div>
|
||||||
|
<div style="font-size:12px;color:#94a3b8;margin-top:6px;">Serviser je obavešten o vašoj odluci.</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user