Compare commits
10
Commits
ce53e43d5e
...
7bb9d04fcb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7bb9d04fcb
|
||
|
|
d9eeec4c6a
|
||
|
|
47ae5e9347
|
||
|
|
29c9b1869d
|
||
|
|
17aa1c5878
|
||
|
|
be82e85323
|
||
|
|
f54a3e62ed
|
||
|
|
020ecb756c
|
||
|
|
15c44dd2d3
|
||
|
|
57c092ed8f
|
@@ -241,6 +241,7 @@ func main() {
|
|||||||
r.Get("/status/{token}", h.ServisJavniStatus)
|
r.Get("/status/{token}", h.ServisJavniStatus)
|
||||||
r.Post("/status/{token}/prihvati", h.ServisJavniPrihvati)
|
r.Post("/status/{token}/prihvati", h.ServisJavniPrihvati)
|
||||||
r.Post("/status/{token}/odbij", h.ServisJavniOdbij)
|
r.Post("/status/{token}/odbij", h.ServisJavniOdbij)
|
||||||
|
r.Post("/status/{token}/odluka-odabrano", h.ServisJavniOdlukaOdabrano)
|
||||||
|
|
||||||
// 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) {
|
||||||
@@ -364,6 +365,7 @@ func main() {
|
|||||||
r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "servis.pregled")).Get("/servis/{id}/revers", h.StampaReversa)
|
r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "servis.pregled")).Get("/servis/{id}/revers", h.StampaReversa)
|
||||||
r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "servis.pregled")).Get("/servis/{id}/predracun", h.StampaPredracuna)
|
r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "servis.pregled")).Get("/servis/{id}/predracun", h.StampaPredracuna)
|
||||||
r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "servis.pregled")).Get("/servis/{id}/nalepnica", h.StampaNalepnice)
|
r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "servis.pregled")).Get("/servis/{id}/nalepnica", h.StampaNalepnice)
|
||||||
|
r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "servis.pregled")).Get("/servis/{id}/garantni-list", h.StampaGarantnog)
|
||||||
r.With(doz("servis.izmeni")).Post("/servis/{id}/status", h.PromeniStatus)
|
r.With(doz("servis.izmeni")).Post("/servis/{id}/status", h.PromeniStatus)
|
||||||
r.With(doz("servis.izmeni")).Post("/servis/{id}/garancija", h.AzurirajGaranciju)
|
r.With(doz("servis.izmeni")).Post("/servis/{id}/garancija", h.AzurirajGaranciju)
|
||||||
r.With(doz("servis.izmeni")).Post("/servis/{id}/garancija-dana", h.AzurirajGarancijaDana)
|
r.With(doz("servis.izmeni")).Post("/servis/{id}/garancija-dana", h.AzurirajGarancijaDana)
|
||||||
@@ -374,6 +376,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}/prihvati-odabrano", h.PrihvatiOdabraniPredlog)
|
||||||
r.With(doz("servis.izmeni")).Post("/servis/{id}/obrisi-komentar-klijenta", h.ObrisiKomentarKlijenta)
|
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)
|
||||||
|
|||||||
@@ -232,6 +232,9 @@ type ServisniDeloviRepository interface {
|
|||||||
PrihvatiPredlozene(ctx context.Context, nalogID int64) error
|
PrihvatiPredlozene(ctx context.Context, nalogID int64) error
|
||||||
// ObrisiPredlozene briše sve predložene delove sa naloga (klijent odbio predlog)
|
// ObrisiPredlozene briše sve predložene delove sa naloga (klijent odbio predlog)
|
||||||
ObrisiPredlozene(ctx context.Context, nalogID int64) error
|
ObrisiPredlozene(ctx context.Context, nalogID int64) error
|
||||||
|
// PrihvatiOdabranePoArtiklu selektivno prihvata predlozene delove po artikal_id;
|
||||||
|
// prihvaćeni idu kroz normalnu ugradnju (skidaju sa lagera), ostali se odbacuju.
|
||||||
|
PrihvatiOdabranePoArtiklu(ctx context.Context, nalogID int64, artikalIDs []int64) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServisniPotrazivaniDeloviRepository definiše operacije nad delovima koji nedostaju
|
// ServisniPotrazivaniDeloviRepository definiše operacije nad delovima koji nedostaju
|
||||||
@@ -255,6 +258,9 @@ type ServisniRadoviRepository interface {
|
|||||||
PrihvatiPredlozene(ctx context.Context, nalogID int64) error
|
PrihvatiPredlozene(ctx context.Context, nalogID int64) error
|
||||||
// ObrisiPredlozene briše sve predložene radove sa naloga (klijent odbio predlog)
|
// ObrisiPredlozene briše sve predložene radove sa naloga (klijent odbio predlog)
|
||||||
ObrisiPredlozene(ctx context.Context, nalogID int64) error
|
ObrisiPredlozene(ctx context.Context, nalogID int64) error
|
||||||
|
// PrihvatiOdabrane selektivno prihvata predlozene radove po ID-u;
|
||||||
|
// prihvaćeni dobijaju predlozeno=0, ostali predlozeni se brišu.
|
||||||
|
PrihvatiOdabrane(ctx context.Context, nalogID int64, ids []int64) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// MagacinskePromeneRepository definiše operacije nad revizijskim tragom magacina
|
// MagacinskePromeneRepository definiše operacije nad revizijskim tragom magacina
|
||||||
|
|||||||
@@ -315,6 +315,74 @@ func (r *ServisniDeloviRepo) ObrisiPredlozene(ctx context.Context, nalogID int64
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PrihvatiOdabranePoArtiklu prihvata selektivno predložene delove: artikalIDs su prihvaćeni
|
||||||
|
// (idu kroz normalnu ugradnju/lager), svi ostali predlozeni se brišu bez povraćaja lagera.
|
||||||
|
func (r *ServisniDeloviRepo) PrihvatiOdabranePoArtiklu(ctx context.Context, nalogID int64, artikalIDs []int64) error {
|
||||||
|
tx, err := r.db.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ntech: ServisniDeloviRepo.PrihvatiOdabranePoArtiklu: begin tx: %w", err)
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
// Dohvati sve predložene potraživane
|
||||||
|
redovi, err := tx.QueryContext(ctx,
|
||||||
|
"SELECT id, artikal_id, kolicina, cena_komada FROM servisni_potrazivani_delovi WHERE nalog_id = ? AND predlozeno = 1",
|
||||||
|
nalogID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ntech: ServisniDeloviRepo.PrihvatiOdabranePoArtiklu: dohvati: %w", err)
|
||||||
|
}
|
||||||
|
type stavka struct {
|
||||||
|
id int64
|
||||||
|
artikalID int64
|
||||||
|
kolicina int
|
||||||
|
cena float64
|
||||||
|
}
|
||||||
|
var sve []stavka
|
||||||
|
for redovi.Next() {
|
||||||
|
var s stavka
|
||||||
|
if err := redovi.Scan(&s.id, &s.artikalID, &s.kolicina, &s.cena); err != nil {
|
||||||
|
redovi.Close()
|
||||||
|
return fmt.Errorf("ntech: ServisniDeloviRepo.PrihvatiOdabranePoArtiklu: scan: %w", err)
|
||||||
|
}
|
||||||
|
sve = append(sve, s)
|
||||||
|
}
|
||||||
|
redovi.Close()
|
||||||
|
if err := redovi.Err(); err != nil {
|
||||||
|
return fmt.Errorf("ntech: ServisniDeloviRepo.PrihvatiOdabranePoArtiklu: rows: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
prihvaceni := make(map[int64]bool, len(artikalIDs))
|
||||||
|
for _, id := range artikalIDs {
|
||||||
|
prihvaceni[id] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range sve {
|
||||||
|
if prihvaceni[s.artikalID] {
|
||||||
|
if _, _, err := ugradiIliPotrazujTx(ctx, tx, nalogID, s.artikalID, s.kolicina, s.cena, nil, false); err != nil {
|
||||||
|
return fmt.Errorf("ntech: ServisniDeloviRepo.PrihvatiOdabranePoArtiklu: ugradi: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Briši sve predlozene potraživane (prihvaćeni su prebačeni, odbijeni se odbacuju)
|
||||||
|
if _, err := tx.ExecContext(ctx,
|
||||||
|
"DELETE FROM servisni_potrazivani_delovi WHERE nalog_id = ? AND predlozeno = 1", nalogID,
|
||||||
|
); err != nil {
|
||||||
|
return fmt.Errorf("ntech: ServisniDeloviRepo.PrihvatiOdabranePoArtiklu: obrisi: %w", err)
|
||||||
|
}
|
||||||
|
if _, err := tx.ExecContext(ctx,
|
||||||
|
"DELETE FROM servisni_delovi WHERE nalog_id = ? AND predlozeno = 1", nalogID,
|
||||||
|
); err != nil {
|
||||||
|
return fmt.Errorf("ntech: ServisniDeloviRepo.PrihvatiOdabranePoArtiklu: obrisi delove: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return fmt.Errorf("ntech: ServisniDeloviRepo.PrihvatiOdabranePoArtiklu: 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
|
||||||
|
|||||||
@@ -89,6 +89,54 @@ func (r *ServisniRadoviRepo) ObrisiPredlozene(ctx context.Context, nalogID int64
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PrihvatiOdabrane prihvata selektivno predložene radove: ids su prihvaćeni (predlozeno→0),
|
||||||
|
// ostali predlozeni se brišu.
|
||||||
|
func (r *ServisniRadoviRepo) PrihvatiOdabrane(ctx context.Context, nalogID int64, ids []int64) error {
|
||||||
|
tx, err := r.db.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ntech: ServisniRadoviRepo.PrihvatiOdabrane: begin tx: %w", err)
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
prihvaceni := make(map[int64]bool, len(ids))
|
||||||
|
for _, id := range ids {
|
||||||
|
prihvaceni[id] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dohvati sve predlozene radove
|
||||||
|
redovi, err := tx.QueryContext(ctx, "SELECT id FROM servis_radovi WHERE nalog_id = ? AND predlozeno = 1", nalogID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ntech: ServisniRadoviRepo.PrihvatiOdabrane: dohvati: %w", err)
|
||||||
|
}
|
||||||
|
var sviIDs []int64
|
||||||
|
for redovi.Next() {
|
||||||
|
var id int64
|
||||||
|
if err := redovi.Scan(&id); err != nil {
|
||||||
|
redovi.Close()
|
||||||
|
return fmt.Errorf("ntech: ServisniRadoviRepo.PrihvatiOdabrane: scan: %w", err)
|
||||||
|
}
|
||||||
|
sviIDs = append(sviIDs, id)
|
||||||
|
}
|
||||||
|
redovi.Close()
|
||||||
|
|
||||||
|
for _, id := range sviIDs {
|
||||||
|
if prihvaceni[id] {
|
||||||
|
if _, err := tx.ExecContext(ctx, "UPDATE servis_radovi SET predlozeno = 0 WHERE id = ?", id); err != nil {
|
||||||
|
return fmt.Errorf("ntech: ServisniRadoviRepo.PrihvatiOdabrane: update: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if _, err := tx.ExecContext(ctx, "DELETE FROM servis_radovi WHERE id = ?", id); err != nil {
|
||||||
|
return fmt.Errorf("ntech: ServisniRadoviRepo.PrihvatiOdabrane: delete: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return fmt.Errorf("ntech: ServisniRadoviRepo.PrihvatiOdabrane: commit: %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 {
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ var saSidebar = []string{
|
|||||||
|
|
||||||
// standalone su šabloni bez base layouta
|
// standalone su šabloni bez base layouta
|
||||||
var standaloneIme = []string{
|
var standaloneIme = []string{
|
||||||
"prijava", "setup", "totp_provera", "prodaja_stampa", "servis_radni_nalog", "servis_otpremnica", "servis_revers", "servis_predracun", "servis_nalepnica", "servis_status_javni",
|
"prijava", "setup", "totp_provera", "prodaja_stampa", "servis_radni_nalog", "servis_otpremnica", "servis_revers", "servis_predracun", "servis_nalepnica", "servis_status_javni", "servis_garantni_list",
|
||||||
}
|
}
|
||||||
|
|
||||||
// sablonskeFunkcije su pomoćne funkcije dostupne u svim šablonima.
|
// sablonskeFunkcije su pomoćne funkcije dostupne u svim šablonima.
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ import (
|
|||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"html"
|
||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
@@ -922,11 +924,18 @@ func (h *Handler) TestFiskalizacije(w http.ResponseWriter, r *http.Request) {
|
|||||||
pfrURL = vrednostIliDefault(podesavanja, "pfr_url", "http://127.0.0.1:4566")
|
pfrURL = vrednostIliDefault(podesavanja, "pfr_url", "http://127.0.0.1:4566")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dozvoljavamo samo localhost/loopback da sprečimo SSRF
|
||||||
|
parsedURL, err := url.Parse(pfrURL)
|
||||||
|
if err != nil || (parsedURL.Hostname() != "127.0.0.1" && parsedURL.Hostname() != "localhost") {
|
||||||
|
http.Error(w, "Nevažeći PFR URL — dozvoljen samo localhost", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
klijent := &http.Client{Timeout: 5 * time.Second}
|
klijent := &http.Client{Timeout: 5 * time.Second}
|
||||||
resp, err := klijent.Get(pfrURL + "/api/status")
|
resp, err := klijent.Get(pfrURL + "/api/status")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
fmt.Fprintf(w, `<div class="fisk-status greska">✗ Nije dostupan — %s</div>`, err.Error())
|
fmt.Fprintf(w, `<div class="fisk-status greska">✗ Nije dostupan — %s</div>`, html.EscapeString(err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
@@ -946,7 +955,7 @@ func (h *Handler) TestFiskalizacije(w http.ResponseWriter, r *http.Request) {
|
|||||||
<span>Poslednji račun: %s</span>
|
<span>Poslednji račun: %s</span>
|
||||||
<span>Vreme: %s</span>
|
<span>Vreme: %s</span>
|
||||||
</div>
|
</div>
|
||||||
</div>`, pfrURL, tin, lastInvoice, sdcDateTime)
|
</div>`, html.EscapeString(pfrURL), html.EscapeString(tin), html.EscapeString(lastInvoice), html.EscapeString(sdcDateTime))
|
||||||
}
|
}
|
||||||
|
|
||||||
// PodesavanjaSistem renderuje stranicu sa sistemskim podešavanjima (backup)
|
// PodesavanjaSistem renderuje stranicu sa sistemskim podešavanjima (backup)
|
||||||
|
|||||||
@@ -1347,6 +1347,7 @@ func (h *Handler) StampaRadnogNaloga(w http.ResponseWriter, r *http.Request) {
|
|||||||
// PodaciOtpremnice su podaci za otpremnicu pri preuzimanju uređaja
|
// PodaciOtpremnice su podaci za otpremnicu pri preuzimanju uređaja
|
||||||
type PodaciOtpremnice struct {
|
type PodaciOtpremnice struct {
|
||||||
Nalog model.ServisniNalog
|
Nalog model.ServisniNalog
|
||||||
|
Radovi []model.ServisniRad
|
||||||
ServisniDelovi []model.ServisniDeoSaArtiklom
|
ServisniDelovi []model.ServisniDeoSaArtiklom
|
||||||
UkupnoDelovi float64
|
UkupnoDelovi float64
|
||||||
PreostaloSve float64
|
PreostaloSve float64
|
||||||
@@ -1379,6 +1380,12 @@ func (h *Handler) StampaOtpremnice(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)
|
||||||
@@ -1438,8 +1445,21 @@ func (h *Handler) StampaOtpremnice(w http.ResponseWriter, r *http.Request) {
|
|||||||
qrKodOtpr = base64.StdEncoding.EncodeToString(png)
|
qrKodOtpr = base64.StdEncoding.EncodeToString(png)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GarancijaDo se računa od završetka, ne od prijema
|
||||||
|
if nalog.GarancijaDana == nil || *nalog.GarancijaDana <= 0 {
|
||||||
|
nalog.GarancijaDo = nil
|
||||||
|
} else {
|
||||||
|
baza := time.Now()
|
||||||
|
if nalog.DatumZavrsetka != nil {
|
||||||
|
baza = *nalog.DatumZavrsetka
|
||||||
|
}
|
||||||
|
t := baza.AddDate(0, 0, *nalog.GarancijaDana)
|
||||||
|
nalog.GarancijaDo = &t
|
||||||
|
}
|
||||||
|
|
||||||
h.renderujStandalone(w, "servis_otpremnica", PodaciOtpremnice{
|
h.renderujStandalone(w, "servis_otpremnica", PodaciOtpremnice{
|
||||||
Nalog: *nalog,
|
Nalog: *nalog,
|
||||||
|
Radovi: radovi,
|
||||||
ServisniDelovi: delovi,
|
ServisniDelovi: delovi,
|
||||||
UkupnoDelovi: ukupnoDelovi,
|
UkupnoDelovi: ukupnoDelovi,
|
||||||
PreostaloSve: preostaloSve,
|
PreostaloSve: preostaloSve,
|
||||||
@@ -1459,6 +1479,113 @@ func (h *Handler) StampaOtpremnice(w http.ResponseWriter, r *http.Request) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PodaciGarantnog su podaci za garantni list koji se predaje klijentu pri preuzimanju.
|
||||||
|
type PodaciGarantnog struct {
|
||||||
|
Nalog model.ServisniNalog
|
||||||
|
GarancijaDo *time.Time // izračunato od DatumZavrsetka + GarancijaDana
|
||||||
|
GarancijaTekst string // npr. "60 dana"
|
||||||
|
Radovi []model.ServisniRad
|
||||||
|
ServisniDelovi []model.ServisniDeoSaArtiklom
|
||||||
|
Klijent *model.Klijent
|
||||||
|
KlijentNaziv string
|
||||||
|
TehnicarNaziv string
|
||||||
|
Uslovi string
|
||||||
|
NazivFirme string
|
||||||
|
LogoPutanja string
|
||||||
|
Podnazlov string
|
||||||
|
Adresa string
|
||||||
|
Telefon string
|
||||||
|
PIB string
|
||||||
|
MaticniBroj string
|
||||||
|
Barkod string
|
||||||
|
}
|
||||||
|
|
||||||
|
// StampaGarantnog renderuje garantni list (dostupno od statusa Završeno, samo ako ima garancija)
|
||||||
|
func (h *Handler) StampaGarantnog(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id, err := parseID(chi.URLParam(r, "id"))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Neispravan ID naloga", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nalog, err := h.ServisRepo.DohvatiID(r.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Nalog nije pronađen", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if nalog.GarancijaDana == nil || *nalog.GarancijaDana <= 0 {
|
||||||
|
http.Error(w, "Nalog nema definisanu garanciju", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
radovi, err := h.ServisniRadoviRepo.DohvatiZaNalog(r.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Greška pri učitavanju radova", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
delovi, err := h.deloviSaPotrazivanima(r.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Greška pri učitavanju delova", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var klijent *model.Klijent
|
||||||
|
klijentNaziv := ""
|
||||||
|
if nalog.KlijentID != nil {
|
||||||
|
k, err := h.KlijentiRepo.DohvatiID(r.Context(), *nalog.KlijentID)
|
||||||
|
if err == nil {
|
||||||
|
klijent = k
|
||||||
|
if k.NazivFirme != "" {
|
||||||
|
klijentNaziv = k.NazivFirme
|
||||||
|
} else {
|
||||||
|
klijentNaziv = strings.TrimSpace(k.Ime + " " + k.Prezime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tehnicarNaziv := ""
|
||||||
|
if nalog.TehnicarID != nil {
|
||||||
|
tehnicar, err := h.KorisniciRepo.DohvatiPoID(r.Context(), *nalog.TehnicarID)
|
||||||
|
if err == nil {
|
||||||
|
tehnicarNaziv = tehnicar.KorisnickoIme
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
baza := time.Now()
|
||||||
|
if nalog.DatumZavrsetka != nil {
|
||||||
|
baza = *nalog.DatumZavrsetka
|
||||||
|
}
|
||||||
|
garanDo := baza.AddDate(0, 0, *nalog.GarancijaDana)
|
||||||
|
|
||||||
|
h.renderujStandalone(w, "servis_garantni_list", PodaciGarantnog{
|
||||||
|
Nalog: *nalog,
|
||||||
|
GarancijaDo: &garanDo,
|
||||||
|
GarancijaTekst: garancijaUTekst(*nalog.GarancijaDana),
|
||||||
|
Radovi: radovi,
|
||||||
|
ServisniDelovi: delovi,
|
||||||
|
Klijent: klijent,
|
||||||
|
KlijentNaziv: klijentNaziv,
|
||||||
|
TehnicarNaziv: tehnicarNaziv,
|
||||||
|
Uslovi: vrednostIliDefault(podesavanja, "servis_uslovi", podrazumevaniUsloviServisa),
|
||||||
|
NazivFirme: podesavanja["naziv_firme"],
|
||||||
|
LogoPutanja: logoZaDokument(podesavanja),
|
||||||
|
Podnazlov: podesavanja["podnazlov"],
|
||||||
|
Adresa: podesavanja["adresa"],
|
||||||
|
Telefon: podesavanja["telefon"],
|
||||||
|
PIB: podesavanja["pib"],
|
||||||
|
MaticniBroj: podesavanja["maticni_broj"],
|
||||||
|
Barkod: barkodNaloga(nalog.BrojNaloga),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// PodaciReversa su podaci za revers — potvrdu o prijemu uređaja na servis.
|
// PodaciReversa su podaci za revers — potvrdu o prijemu uređaja na servis.
|
||||||
// Dokument se izdaje pri prijemu, pa nema cena.
|
// Dokument se izdaje pri prijemu, pa nema cena.
|
||||||
type PodaciReversa struct {
|
type PodaciReversa struct {
|
||||||
@@ -2240,6 +2367,109 @@ func (h *Handler) ServisJavniPrihvati(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Redirect(w, r, "/status/"+token, http.StatusSeeOther)
|
http.Redirect(w, r, "/status/"+token, http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ServisJavniOdlukaOdabrano obrađuje selektivno odobrenje predloga sa javne status stranice.
|
||||||
|
// Klijent čekira pojedinačne stavke; prihvaćene idu u ugrađene, ostale se brišu.
|
||||||
|
func (h *Handler) ServisJavniOdlukaOdabrano(w http.ResponseWriter, r *http.Request) {
|
||||||
|
token := chi.URLParam(r, "token")
|
||||||
|
if token == "" {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nalog, err := h.ServisRepo.DohvatiJavniToken(r.Context(), token)
|
||||||
|
if err != nil {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if nalog.Status == model.StatusPreuzeto {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if nalog.OdlukaKlijenta != "" {
|
||||||
|
http.Redirect(w, r, "/status/"+token, http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
http.Error(w, "Neispravan zahtev.", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
radIDs := parsInt64s(r.Form["rad_id"])
|
||||||
|
artikalIDs := parsInt64s(r.Form["artikal_id"])
|
||||||
|
|
||||||
|
if err := h.ServisniRadoviRepo.PrihvatiOdabrane(r.Context(), nalog.ID, radIDs); err != nil {
|
||||||
|
slog.Error("greška pri selektivnom prihvatanju radova", "error", err, "nalog_id", nalog.ID)
|
||||||
|
http.Error(w, "Greška pri obradi predloga.", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := h.ServisniDeloviRepo.PrihvatiOdabranePoArtiklu(r.Context(), nalog.ID, artikalIDs); err != nil {
|
||||||
|
slog.Error("greška pri selektivnom prihvatanju delova", "error", err, "nalog_id", nalog.ID)
|
||||||
|
http.Error(w, "Greška pri obradi predloga.", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
komentar := strings.TrimSpace(r.FormValue("komentar"))
|
||||||
|
odluka := "prihvaceno"
|
||||||
|
if len(radIDs) == 0 && len(artikalIDs) == 0 {
|
||||||
|
odluka = "odbijeno"
|
||||||
|
}
|
||||||
|
if err := h.ServisRepo.SacuvajOdlukuKlijenta(r.Context(), nalog.ID, odluka, komentar); err != nil {
|
||||||
|
slog.Error("greška pri čuvanju odluke klijenta", "error", err, "nalog_id", nalog.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Redirect(w, r, "/status/"+token, http.StatusSeeOther)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrihvatiOdabraniPredlog je interni handler za selektivno prihvatanje predloga
|
||||||
|
// (serviser štiklira stavke u detaljima naloga i klika "Prihvati odabrano").
|
||||||
|
func (h *Handler) PrihvatiOdabraniPredlog(w http.ResponseWriter, r *http.Request) {
|
||||||
|
_, ok := h.zahtevajDozvolu(w, r, "servis.izmeni")
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nalogID, err := parseID(chi.URLParam(r, "id"))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Neispravan ID naloga", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
http.Error(w, "Neispravan zahtev.", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
radIDs := parsInt64s(r.Form["rad_id"])
|
||||||
|
artikalIDs := parsInt64s(r.Form["artikal_id"])
|
||||||
|
|
||||||
|
if err := h.ServisniRadoviRepo.PrihvatiOdabrane(r.Context(), nalogID, radIDs); err != nil {
|
||||||
|
slog.Error("greška pri selektivnom prihvatanju radova", "error", err, "nalog_id", nalogID)
|
||||||
|
http.Error(w, "Greška pri obradi predloga.", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := h.ServisniDeloviRepo.PrihvatiOdabranePoArtiklu(r.Context(), nalogID, artikalIDs); err != nil {
|
||||||
|
slog.Error("greška pri selektivnom prihvatanju delova", "error", err, "nalog_id", nalogID)
|
||||||
|
http.Error(w, "Greška pri obradi predloga.", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Redirect(w, r, "/servis/"+strconv.FormatInt(nalogID, 10), http.StatusSeeOther)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parsInt64s konvertuje listu string vrednosti u []int64, preskačući neispravne.
|
||||||
|
func parsInt64s(vals []string) []int64 {
|
||||||
|
result := make([]int64, 0, len(vals))
|
||||||
|
for _, v := range vals {
|
||||||
|
if id, err := strconv.ParseInt(v, 10, 64); err == nil && id > 0 {
|
||||||
|
result = append(result, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
// ObrisiKomentarKlijenta briše poruku klijenta sa naloga
|
// ObrisiKomentarKlijenta briše poruku klijenta sa naloga
|
||||||
func (h *Handler) ObrisiKomentarKlijenta(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) ObrisiKomentarKlijenta(w http.ResponseWriter, r *http.Request) {
|
||||||
_, ok := h.zahtevajDozvolu(w, r, "servis.izmeni")
|
_, ok := h.zahtevajDozvolu(w, r, "servis.izmeni")
|
||||||
|
|||||||
@@ -11,6 +11,35 @@
|
|||||||
.detalji-kartica:nth-child(5) { animation-delay: 0.36s; }
|
.detalji-kartica:nth-child(5) { animation-delay: 0.36s; }
|
||||||
/* bez animacija na detaljima — smetaju pri dodavanju/brisanju stavki */
|
/* bez animacija na detaljima — smetaju pri dodavanju/brisanju stavki */
|
||||||
#sadrzaj .kartica.animiraj { animation: none; }
|
#sadrzaj .kartica.animiraj { animation: none; }
|
||||||
|
/* custom dropdown za usluge i artikle */
|
||||||
|
.cust-sel { position: relative; }
|
||||||
|
.cust-sel-btn {
|
||||||
|
display: flex; align-items: center; justify-content: space-between; gap: 8px;
|
||||||
|
height: 38px; padding: 0 10px; box-sizing: border-box; cursor: pointer;
|
||||||
|
border: 0.5px solid var(--ivica) !important; border-radius: 8px; font-size: 14px;
|
||||||
|
background: var(--kartica) !important; color: var(--tekst-glavni) !important;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.cust-sel-btn:focus { border-color: #3b82f6 !important; outline: none; }
|
||||||
|
.cust-sel-tekst { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.cust-sel-chevron { font-size: 10px; color: var(--tekst-sporedni); flex-shrink: 0; transition: transform 0.15s; }
|
||||||
|
.cust-sel.otvoren .cust-sel-chevron { transform: rotate(180deg); }
|
||||||
|
.cust-sel-lista {
|
||||||
|
display: none; position: absolute; top: calc(100% + 3px); left: 0; right: 0; z-index: 500;
|
||||||
|
background: var(--kartica); border: 0.5px solid var(--ivica); border-radius: 10px;
|
||||||
|
max-height: 260px; overflow-y: auto; box-shadow: 0 8px 30px rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
.cust-sel.otvoren .cust-sel-lista { display: block; }
|
||||||
|
.cso {
|
||||||
|
display: flex; align-items: center; justify-content: space-between; gap: 12px;
|
||||||
|
padding: 8px 12px; cursor: pointer; border-bottom: 0.5px solid var(--ivica); font-size: 13px;
|
||||||
|
}
|
||||||
|
.cso:last-child { border-bottom: none; }
|
||||||
|
.cso:hover, .cso.izabran { background: rgba(255,255,255,0.06); }
|
||||||
|
.cso-naziv { color: var(--tekst-glavni); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.cso-cena { color: #f59e0b; font-weight: 600; white-space: nowrap; flex-shrink: 0; }
|
||||||
|
.cso-prazno { color: var(--tekst-sporedni); }
|
||||||
|
.kartica.cust-aktivan { position: relative; z-index: 100; }
|
||||||
</style>
|
</style>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
@@ -67,10 +96,10 @@
|
|||||||
Odobri popravku
|
Odobri popravku
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<form method="POST" action="/servis/{{.Nalog.ID}}/odbij-popravku" style="margin:0;">
|
<form method="POST" action="/servis/{{.Nalog.ID}}/odbij-popravku" style="margin:0;" data-full-reload>
|
||||||
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
|
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
|
||||||
<button type="submit" class="btn-sekundarno" style="padding:3px 12px;font-size:12px;"
|
<button type="submit" class="btn-sekundarno" style="padding:3px 12px;font-size:12px;"
|
||||||
data-potvrda="Klijent ne prihvata popravku? Nalog ide u „Završeno” i naplaćuje se samo dijagnostika.">
|
data-potvrda="Klijent ne prihvata popravku? Nalog prelazi u Zavrseno, naplacuje se samo dijagnostika.">
|
||||||
Klijent ne prihvata
|
Klijent ne prihvata
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -149,6 +178,11 @@
|
|||||||
<a href="/servis/{{.Nalog.ID}}/otpremnica" target="_blank" class="btn-sekundarno">
|
<a href="/servis/{{.Nalog.ID}}/otpremnica" target="_blank" class="btn-sekundarno">
|
||||||
Otpremnica
|
Otpremnica
|
||||||
</a>
|
</a>
|
||||||
|
{{if not .BezGarancije}}
|
||||||
|
<a href="/servis/{{.Nalog.ID}}/garantni-list" target="_blank" class="btn-sekundarno">
|
||||||
|
Garantni list
|
||||||
|
</a>
|
||||||
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{/* Nalepnica je primarna pri prijemu (lepi se na uređaj); reprint dok traje; ne u „Završeno" */}}
|
{{/* Nalepnica je primarna pri prijemu (lepi se na uređaj); reprint dok traje; ne u „Završeno" */}}
|
||||||
{{if eq .Nalog.Status "Primljeno"}}
|
{{if eq .Nalog.Status "Primljeno"}}
|
||||||
@@ -456,16 +490,23 @@
|
|||||||
|
|
||||||
<!-- forma za dodavanje rada — u dijagnostici se automatski beleži kao predlog -->
|
<!-- forma za dodavanje rada — u dijagnostici se automatski beleži kao predlog -->
|
||||||
{{if and (index .Dozvole "servis.izmeni") (ne .Nalog.Status "U popravci")}}
|
{{if and (index .Dozvole "servis.izmeni") (ne .Nalog.Status "U popravci")}}
|
||||||
<form method="POST" action="/servis/{{.Nalog.ID}}/radovi" style="margin-bottom:16px;" data-refresh="#radovi-blok, #nalog-ukupno">
|
<form method="POST" action="/servis/{{.Nalog.ID}}/radovi" style="margin-bottom:16px;" data-refresh="#radovi-blok, #nalog-ukupno, #predlozeno-blok">
|
||||||
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
|
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
|
||||||
{{if .ZakljucaniUgradjeni}}<input type="hidden" name="predlozeno" value="1">{{end}}
|
{{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>
|
||||||
<select name="usluga_id" id="rad-usluga" style="width:100%;" required>
|
<div class="cust-sel" data-sel="rad-usluga">
|
||||||
<option value="">— odaberi uslugu —</option>
|
<div class="cust-sel-btn" tabindex="0">
|
||||||
{{range .Usluge}}{{if not (index $.KorisceneUsluge .ID)}}<option value="{{.ID}}" data-cena="{{.Cena}}">{{.Naziv}}</option>{{end}}{{end}}
|
<span class="cust-sel-tekst cso-prazno">— odaberi uslugu —</span>
|
||||||
</select>
|
<span class="cust-sel-chevron">▾</span>
|
||||||
|
</div>
|
||||||
|
<div class="cust-sel-lista">
|
||||||
|
<div class="cso" data-val="" data-cena="0"><span class="cso-prazno">— odaberi uslugu —</span></div>
|
||||||
|
{{range .Usluge}}{{if not (index $.KorisceneUsluge .ID)}}<div class="cso" data-val="{{.ID}}" data-cena="{{.Cena}}"><span class="cso-naziv">{{.Naziv}}</span><span class="cso-cena">{{dinari .Cena}} din</span></div>{{end}}{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input type="hidden" name="usluga_id" id="rad-usluga">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">Kol.</label>
|
<label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">Kol.</label>
|
||||||
@@ -507,7 +548,7 @@
|
|||||||
<td style="padding:9px 10px;text-align:right;font-size:14px;font-weight:500;color:var(--tekst-glavni);">{{dinari .Ukupno}} din</td>
|
<td style="padding:9px 10px;text-align:right;font-size:14px;font-weight:500;color:var(--tekst-glavni);">{{dinari .Ukupno}} din</td>
|
||||||
<td style="padding:9px 10px;text-align:center;">
|
<td style="padding:9px 10px;text-align:center;">
|
||||||
{{if and (not $.ZakljucaniUgradjeni) (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-blok, #nalog-ukupno">
|
<form method="POST" action="/servis/{{$.Nalog.ID}}/radovi/{{.ID}}/obrisi" style="display:inline;" data-refresh="#radovi-blok, #nalog-ukupno, #predlozeno-blok">
|
||||||
<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}}'?"
|
||||||
@@ -528,7 +569,7 @@
|
|||||||
<td style="padding:9px 10px;text-align:right;font-size:14px;font-weight:500;color:var(--tekst-glavni);">{{dinari .Ukupno}} din</td>
|
<td style="padding:9px 10px;text-align:right;font-size:14px;font-weight:500;color:var(--tekst-glavni);">{{dinari .Ukupno}} din</td>
|
||||||
<td style="padding:9px 10px;text-align:center;">
|
<td style="padding:9px 10px;text-align:center;">
|
||||||
{{if and (index $.Dozvole "servis.izmeni") (ne $.Nalog.Status "U popravci")}}
|
{{if and (index $.Dozvole "servis.izmeni") (ne $.Nalog.Status "U popravci")}}
|
||||||
<form method="POST" action="/servis/{{$.Nalog.ID}}/radovi/{{.ID}}/obrisi" style="display:inline;" data-refresh="#radovi-blok, #nalog-ukupno">
|
<form method="POST" action="/servis/{{$.Nalog.ID}}/radovi/{{.ID}}/obrisi" style="display:inline;" data-refresh="#radovi-blok, #nalog-ukupno, #predlozeno-blok">
|
||||||
<input type="hidden" name="_csrf" value="{{$.CsrfToken}}">
|
<input type="hidden" name="_csrf" value="{{$.CsrfToken}}">
|
||||||
<button type="submit"
|
<button type="submit"
|
||||||
data-potvrda="Ukloniti predloženi rad '{{.Naziv}}'?"
|
data-potvrda="Ukloniti predloženi rad '{{.Naziv}}'?"
|
||||||
@@ -566,7 +607,7 @@
|
|||||||
|
|
||||||
<!-- forma za dodavanje dela — u dijagnostici se automatski beleži kao predlog -->
|
<!-- forma za dodavanje dela — u dijagnostici se automatski beleži kao predlog -->
|
||||||
{{if and (index .Dozvole "servis.izmeni") (ne .Nalog.Status "U popravci")}}
|
{{if and (index .Dozvole "servis.izmeni") (ne .Nalog.Status "U popravci")}}
|
||||||
<form method="POST" action="/servis/{{.Nalog.ID}}/delovi" style="margin-bottom:16px;" data-refresh="#delovi-blok, #nalog-zaglavlje, #nalog-ukupno">
|
<form method="POST" action="/servis/{{.Nalog.ID}}/delovi" style="margin-bottom:16px;" data-refresh="#delovi-blok, #nalog-zaglavlje, #nalog-ukupno, #predlozeno-blok">
|
||||||
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
|
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
|
||||||
{{if .ZakljucaniUgradjeni}}<input type="hidden" name="predlozeno" value="1">{{end}}
|
{{if .ZakljucaniUgradjeni}}<input type="hidden" name="predlozeno" value="1">{{end}}
|
||||||
<div style="margin-bottom:10px;">
|
<div style="margin-bottom:10px;">
|
||||||
@@ -576,9 +617,14 @@
|
|||||||
<div class="detalji-forma-deo">
|
<div class="detalji-forma-deo">
|
||||||
<div>
|
<div>
|
||||||
<label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">Artikal</label>
|
<label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">Artikal</label>
|
||||||
<select name="artikal_id" id="deo-artikal" style="width:100%;" required>
|
<div class="cust-sel" id="deo-artikal-cust" data-sel="deo-artikal">
|
||||||
<option value="">— odaberi artikal —</option>
|
<div class="cust-sel-btn" tabindex="0">
|
||||||
</select>
|
<span class="cust-sel-tekst cso-prazno">— odaberi artikal —</span>
|
||||||
|
<span class="cust-sel-chevron">▾</span>
|
||||||
|
</div>
|
||||||
|
<div class="cust-sel-lista"></div>
|
||||||
|
</div>
|
||||||
|
<input type="hidden" name="artikal_id" id="deo-artikal">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">U magacinu</label>
|
<label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">U magacinu</label>
|
||||||
@@ -630,7 +676,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td style="padding:9px 10px;text-align:center;">
|
<td style="padding:9px 10px;text-align:center;">
|
||||||
{{if and (not $.ZakljucaniUgradjeni) (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-blok, #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, #predlozeno-blok">
|
||||||
<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}}'?"
|
||||||
@@ -658,14 +704,14 @@
|
|||||||
<td style="padding:9px 10px;text-align:center;">
|
<td style="padding:9px 10px;text-align:center;">
|
||||||
{{if and (index $.Dozvole "servis.izmeni") (ne $.Nalog.Status "U popravci")}}
|
{{if and (index $.Dozvole "servis.izmeni") (ne $.Nalog.Status "U popravci")}}
|
||||||
{{if .ID}}
|
{{if .ID}}
|
||||||
<form method="POST" action="/servis/{{$.Nalog.ID}}/delovi/{{.ID}}/obrisi" style="display:inline;" data-refresh="#delovi-blok, #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, #predlozeno-blok">
|
||||||
<input type="hidden" name="_csrf" value="{{$.CsrfToken}}">
|
<input type="hidden" name="_csrf" value="{{$.CsrfToken}}">
|
||||||
<button type="submit"
|
<button type="submit"
|
||||||
data-potvrda="Ukloniti predloženi deo '{{.ArtikalNaziv}}'?"
|
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>
|
style="background:none;border:none;cursor:pointer;color:#dc2626;font-size:16px;padding:2px 6px;border-radius:4px;">×</button>
|
||||||
</form>
|
</form>
|
||||||
{{else if .ArtikalID}}
|
{{else if .ArtikalID}}
|
||||||
<form method="POST" action="/servis/{{$.Nalog.ID}}/predlozeni-delovi/{{.ArtikalID}}/obrisi" style="display:inline;" data-refresh="#delovi-blok, #nalog-zaglavlje, #nalog-ukupno">
|
<form method="POST" action="/servis/{{$.Nalog.ID}}/predlozeni-delovi/{{.ArtikalID}}/obrisi" style="display:inline;" data-refresh="#delovi-blok, #nalog-zaglavlje, #nalog-ukupno, #predlozeno-blok">
|
||||||
<input type="hidden" name="_csrf" value="{{$.CsrfToken}}">
|
<input type="hidden" name="_csrf" value="{{$.CsrfToken}}">
|
||||||
<button type="submit"
|
<button type="submit"
|
||||||
data-potvrda="Ukloniti predloženi deo '{{.ArtikalNaziv}}'?"
|
data-potvrda="Ukloniti predloženi deo '{{.ArtikalNaziv}}'?"
|
||||||
@@ -687,33 +733,74 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>(function(){
|
<script>(function(){
|
||||||
|
window._dinFmt=function(v){var s=Math.round((v||0)*100);var dec=String(s%100).padStart(2,'0');var cel=String(Math.floor(s/100));var r='';for(var i=0;i<cel.length;i++){if(i>0&&(cel.length-i)%3===0)r+='.';r+=cel[i];}return r+','+dec;}
|
||||||
var k=document.getElementById('deo-kategorija');
|
var k=document.getElementById('deo-kategorija');
|
||||||
var a=document.getElementById('deo-artikal');
|
var a=document.getElementById('deo-artikal');
|
||||||
var u=document.getElementById('deo-umagacinu');
|
var u=document.getElementById('deo-umagacinu');
|
||||||
var l=document.getElementById('deo-kol');
|
var l=document.getElementById('deo-kol');
|
||||||
var c=document.getElementById('deo-cena');
|
var c=document.getElementById('deo-cena');
|
||||||
var st=0;
|
function osv(){if(!l||!a)return;var s=parseInt(a.getAttribute('data-stanje'),10);var st=isNaN(s)?0:s;if(u)u.textContent=a.value?st:'\u2014';if(l)l.classList.toggle('prekoraceno',!!a.value&&st===0)}
|
||||||
function osv(){if(!l||!a)return;var o=a.options[a.selectedIndex];var s=o?parseInt(o.getAttribute('data-stanje'),10):0;st=isNaN(s)?0:s;if(u)u.textContent=a.value?st:'—';if(l)l.classList.toggle('prekoraceno',a.value&&st===0)}
|
|
||||||
function pop(){
|
function pop(){
|
||||||
if(!a)return;
|
var cust=document.getElementById('deo-artikal-cust');
|
||||||
|
if(!cust)return;
|
||||||
|
var lista=cust.querySelector('.cust-sel-lista');
|
||||||
var ka=k?k.value:'';
|
var ka=k?k.value:'';
|
||||||
a.innerHTML='<option value="">— odaberi artikal —</option>';
|
var aa=window._ntechArtikli||[];
|
||||||
var aa=window._ntechArtikli||[];
|
var html='<div class="cso" data-val="" data-cena="0" data-stanje="0"><span class="cso-prazno">\u2014 odaberi artikal \u2014</span></div>';
|
||||||
aa.forEach(function(x){if(!ka||(x.KategorijaNaziv||'')===ka)a.innerHTML+='<option value="'+x.ID+'" data-cena="'+x.ProdajnaCena+'" data-stanje="'+(x.Kolicina||0)+'">'+x.Naziv+'</option>'})
|
aa.forEach(function(x){if(!ka||(x.KategorijaNaziv||'')===ka)html+='<div class="cso" data-val="'+x.ID+'" data-cena="'+x.ProdajnaCena+'" data-stanje="'+(x.Kolicina||0)+'"><span class="cso-naziv">'+x.Naziv+'</span><span class="cso-cena">'+window._dinFmt(x.ProdajnaCena)+' din</span></div>';});
|
||||||
}
|
if(lista)lista.innerHTML=html;
|
||||||
if(k){var aa2=window._ntechArtikli||[];var kgs=[];aa2.forEach(function(x){var n=x.KategorijaNaziv||'';if(n&&kgs.indexOf(n)===-1)kgs.push(n)})
|
}
|
||||||
k.innerHTML='<option value="">— sve kategorije —</option>'+kgs.map(function(x){return'<option value="'+x+'">'+x+'</option>'}).join('');k.onchange=pop}
|
if(k){var aa2=window._ntechArtikli||[];var kgs=[];aa2.forEach(function(x){var n=x.KategorijaNaziv||'';if(n&&kgs.indexOf(n)===-1)kgs.push(n)})
|
||||||
|
k.innerHTML='<option value="">\u2014 sve kategorije \u2014</option>'+kgs.map(function(x){return'<option value="'+x+'">'+x+'</option>'}).join('');k.onchange=pop}
|
||||||
pop();
|
pop();
|
||||||
if(a)a.onchange=function(){var o=a.options[a.selectedIndex];if(!o)return;var v=parseFloat(o.getAttribute('data-cena'));if(c)c.value=(!isNaN(v)&&v>0)?v.toFixed(2):'0';osv()}
|
if(a)a.addEventListener('change',function(){var v=parseFloat(a.getAttribute('data-cena'));if(c)c.value=(!isNaN(v)&&v>0)?v.toFixed(2):'0';osv()});
|
||||||
if(l)l.oninput=osv;
|
if(l)l.oninput=osv;
|
||||||
var fd=document.querySelector('form[action$=\"/delovi\"]');
|
var fd=document.querySelector('form[action$=\"/delovi\"]');
|
||||||
if(fd)fd.addEventListener('reset',function(){if(u){u.textContent='—';u.classList.remove('prekoraceno')}st=0;if(l)l.classList.remove('prekoraceno')})
|
if(fd)fd.addEventListener('reset',function(){if(u){u.textContent='\u2014';u.classList.remove('prekoraceno')}if(l)l.classList.remove('prekoraceno')})
|
||||||
})();</script>
|
})();</script>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>{{/* kraj stavke-blok */}}
|
</div>{{/* kraj stavke-blok */}}
|
||||||
|
|
||||||
|
<!-- prihvati/odbij odabrane predlozene stavke (interna forma) -->
|
||||||
|
{{if and (index .Dozvole "servis.izmeni") (or .PredlozeniRadovi .PredlozeniDelovi)}}
|
||||||
|
<div id="predlozeno-blok" class="kartica detalji-kartica animiraj" style="border-left:3px solid #f59e0b;">
|
||||||
|
<div style="margin-bottom:14px;padding-bottom:12px;border-bottom:0.5px solid var(--ivica);display:flex;justify-content:space-between;align-items:center;">
|
||||||
|
<span style="font-size:15px;font-weight:500;color:#f59e0b;">Predložene stavke</span>
|
||||||
|
<span style="font-size:12px;color:var(--tekst-sporedni);">Odštikliraj šta ne prihvataš</span>
|
||||||
|
</div>
|
||||||
|
<form method="POST" action="/servis/{{.Nalog.ID}}/prihvati-odabrano" data-refresh="#radovi-blok, #delovi-blok, #nalog-ukupno, #predlozeno-blok">
|
||||||
|
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
|
||||||
|
{{if .PredlozeniRadovi}}
|
||||||
|
<div style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.6px;color:var(--tekst-sporedni);margin-bottom:8px;">Radovi</div>
|
||||||
|
{{range .PredlozeniRadovi}}
|
||||||
|
<label style="display:flex;align-items:center;justify-content:space-between;gap:10px;padding:8px 0;border-bottom:0.5px solid var(--ivica);cursor:pointer;">
|
||||||
|
<span style="display:flex;align-items:center;gap:10px;flex:1;">
|
||||||
|
<input type="checkbox" name="rad_id" value="{{.ID}}" checked style="width:16px;height:16px;accent-color:#f59e0b;flex-shrink:0;">
|
||||||
|
<span style="font-size:13px;color:var(--tekst-glavni);">{{.Naziv}}{{if gt .Kolicina 1.0}} × {{printf "%.0f" .Kolicina}}{{end}}</span>
|
||||||
|
</span>
|
||||||
|
<span style="font-size:13px;font-weight:500;color:var(--tekst-glavni);white-space:nowrap;">{{dinari .Ukupno}} din</span>
|
||||||
|
</label>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
{{if .PredlozeniDelovi}}
|
||||||
|
<div style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.6px;color:var(--tekst-sporedni);margin:{{if .PredlozeniRadovi}}12px{{else}}0px{{end}} 0 8px;">Delovi</div>
|
||||||
|
{{range .PredlozeniDelovi}}
|
||||||
|
<label style="display:flex;align-items:center;justify-content:space-between;gap:10px;padding:8px 0;border-bottom:0.5px solid var(--ivica);cursor:pointer;">
|
||||||
|
<span style="display:flex;align-items:center;gap:10px;flex:1;">
|
||||||
|
<input type="checkbox" name="artikal_id" value="{{.ArtikalID}}" checked style="width:16px;height:16px;accent-color:#f59e0b;flex-shrink:0;">
|
||||||
|
<span style="font-size:13px;color:var(--tekst-glavni);">{{.ArtikalNaziv}}{{if gt .Kolicina 1}} × {{.Kolicina}}{{end}}</span>
|
||||||
|
</span>
|
||||||
|
<span style="font-size:13px;font-weight:500;color:var(--tekst-glavni);white-space:nowrap;">{{if .Kolicina}}{{dinari .Ukupno}} din{{else}}—{{end}}</span>
|
||||||
|
</label>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
<button type="submit" class="btn-primarno" style="width:100%;margin-top:14px;">Prihvati odabrano</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<div id="predlozeno-blok"></div>
|
||||||
|
{{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">
|
||||||
@@ -768,15 +855,63 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Radovi: pri izboru usluge iz <select>-a popuni predloženu cenu (data-cena).
|
// Custom dropdown: toggle, izbor opcije, zatvaranje klikom van.
|
||||||
// Delegacija na document — kartica radova se osvežava AJAX-om (data-refresh),
|
function _custZatvoriSve() {
|
||||||
// pa listener vezan za sam <select> ne bi preživeo zamenu elementa.
|
document.querySelectorAll('.cust-sel.otvoren').forEach(function (el) {
|
||||||
|
el.classList.remove('otvoren');
|
||||||
|
var k = el.closest('.kartica');
|
||||||
|
if (k) k.classList.remove('cust-aktivan');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
document.addEventListener('click', function (e) {
|
||||||
|
var btn = e.target.closest('.cust-sel-btn');
|
||||||
|
if (btn) {
|
||||||
|
var cust = btn.closest('.cust-sel');
|
||||||
|
var bio = cust.classList.contains('otvoren');
|
||||||
|
_custZatvoriSve();
|
||||||
|
if (!bio) {
|
||||||
|
cust.classList.add('otvoren');
|
||||||
|
var k = cust.closest('.kartica');
|
||||||
|
if (k) k.classList.add('cust-aktivan');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var opt = e.target.closest('.cso');
|
||||||
|
if (opt) {
|
||||||
|
var cust = opt.closest('.cust-sel');
|
||||||
|
var selId = cust.getAttribute('data-sel');
|
||||||
|
var inp = selId ? document.getElementById(selId) : null;
|
||||||
|
var val = opt.getAttribute('data-val') || '';
|
||||||
|
var btnEl = cust.querySelector('.cust-sel-btn');
|
||||||
|
var btnTekst = btnEl ? btnEl.querySelector('.cust-sel-tekst') : null;
|
||||||
|
var naziv = opt.querySelector('.cso-naziv');
|
||||||
|
var prazan = opt.querySelector('.cso-prazno');
|
||||||
|
if (btnTekst) {
|
||||||
|
btnTekst.textContent = naziv ? naziv.textContent : (prazan ? prazan.textContent : '');
|
||||||
|
btnTekst.className = 'cust-sel-tekst' + (val ? '' : ' cso-prazno');
|
||||||
|
}
|
||||||
|
cust.querySelectorAll('.cso').forEach(function (o) { o.classList.remove('izabran'); });
|
||||||
|
if (val) opt.classList.add('izabran');
|
||||||
|
if (inp) {
|
||||||
|
inp.value = val;
|
||||||
|
inp.setAttribute('data-cena', opt.getAttribute('data-cena') || '0');
|
||||||
|
inp.setAttribute('data-stanje', opt.getAttribute('data-stanje') || '0');
|
||||||
|
inp.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
}
|
||||||
|
cust.classList.remove('otvoren');
|
||||||
|
var k2 = cust.closest('.kartica');
|
||||||
|
if (k2) k2.classList.remove('cust-aktivan');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_custZatvoriSve();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Radovi: pri izboru usluge (custom dropdown → hidden input) popuni predloženu cenu.
|
||||||
document.addEventListener('change', function (e) {
|
document.addEventListener('change', function (e) {
|
||||||
if (!e.target || e.target.id !== 'rad-usluga') return;
|
if (!e.target || e.target.id !== 'rad-usluga') return;
|
||||||
var cena = document.getElementById('rad-cena');
|
var cena = document.getElementById('rad-cena');
|
||||||
if (!cena) return;
|
if (!cena) return;
|
||||||
var opt = e.target.options[e.target.selectedIndex];
|
var c = parseFloat(e.target.getAttribute('data-cena'));
|
||||||
var c = parseFloat(opt.getAttribute('data-cena'));
|
|
||||||
cena.value = (!isNaN(c) && c > 0) ? c.toFixed(2) : '0';
|
cena.value = (!isNaN(c) && c > 0) ? c.toFixed(2) : '0';
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -819,12 +954,16 @@ document.addEventListener('change', function (e) {
|
|||||||
function popuniArtikle() {
|
function popuniArtikle() {
|
||||||
ARTIKLI = window._ntechArtikli || [];
|
ARTIKLI = window._ntechArtikli || [];
|
||||||
var izabrana = kategSel ? kategSel.value : '';
|
var izabrana = kategSel ? kategSel.value : '';
|
||||||
artikalSel.innerHTML = '<option value="">— odaberi artikal —</option>' +
|
var cust = document.getElementById('deo-artikal-cust');
|
||||||
ARTIKLI.filter(function (a) { return !izabrana || (a.KategorijaNaziv || '') === izabrana; })
|
var lista = cust ? cust.querySelector('.cust-sel-lista') : null;
|
||||||
.map(function (a) {
|
var html = '<div class="cso" data-val="" data-cena="0" data-stanje="0"><span class="cso-prazno">— odaberi artikal —</span></div>';
|
||||||
return '<option value="' + a.ID + '" data-cena="' + a.ProdajnaCena +
|
ARTIKLI.filter(function (a) { return !izabrana || (a.KategorijaNaziv || '') === izabrana; })
|
||||||
'" data-stanje="' + a.Kolicina + '">' + a.Naziv + '</option>';
|
.forEach(function (a) {
|
||||||
}).join('');
|
html += '<div class="cso" data-val="' + a.ID + '" data-cena="' + a.ProdajnaCena +
|
||||||
|
'" data-stanje="' + (a.Kolicina || 0) + '"><span class="cso-naziv">' + a.Naziv +
|
||||||
|
'</span><span class="cso-cena">' + window._dinFmt(a.ProdajnaCena) + ' din</span></div>';
|
||||||
|
});
|
||||||
|
if (lista) lista.innerHTML = html;
|
||||||
if (umagacinu) umagacinu.textContent = '—';
|
if (umagacinu) umagacinu.textContent = '—';
|
||||||
}
|
}
|
||||||
window._ntechPopuniArtikle = popuniArtikle;
|
window._ntechPopuniArtikle = popuniArtikle;
|
||||||
@@ -832,10 +971,9 @@ document.addEventListener('change', function (e) {
|
|||||||
if (kategSel) kategSel.addEventListener('change', popuniArtikle);
|
if (kategSel) kategSel.addEventListener('change', popuniArtikle);
|
||||||
|
|
||||||
artikalSel.addEventListener('change', function () {
|
artikalSel.addEventListener('change', function () {
|
||||||
var opt = artikalSel.options[artikalSel.selectedIndex];
|
var c = parseFloat(artikalSel.getAttribute('data-cena'));
|
||||||
var c = parseFloat(opt.getAttribute('data-cena'));
|
|
||||||
if (cena) cena.value = (!isNaN(c) && c > 0) ? c.toFixed(2) : '0';
|
if (cena) cena.value = (!isNaN(c) && c > 0) ? c.toFixed(2) : '0';
|
||||||
var st = parseInt(opt.getAttribute('data-stanje'), 10);
|
var st = parseInt(artikalSel.getAttribute('data-stanje'), 10);
|
||||||
stanjeIzabrano = isNaN(st) ? 0 : st;
|
stanjeIzabrano = isNaN(st) ? 0 : st;
|
||||||
if (umagacinu) umagacinu.textContent = artikalSel.value ? stanjeIzabrano : '—';
|
if (umagacinu) umagacinu.textContent = artikalSel.value ? stanjeIzabrano : '—';
|
||||||
osveziPrekoracenje();
|
osveziPrekoracenje();
|
||||||
|
|||||||
@@ -0,0 +1,240 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="sr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Garantni list — {{.Nalog.BrojNaloga}}</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif; font-size: 13px; color: #111; background: #fff; }
|
||||||
|
|
||||||
|
.strana { max-width: 780px; margin: 0 auto; padding: 32px 40px; }
|
||||||
|
|
||||||
|
/* zaglavlje: tri okvira (firma · klijent · dokument) */
|
||||||
|
.zaglavlje { display: flex; align-items: stretch; margin-bottom: 20px; gap: 12px; }
|
||||||
|
.zg-okvir { flex: 1; padding: 12px 14px; border: 1.5px solid #bbb; border-radius: 12px; }
|
||||||
|
.firma-naziv { font-size: 18px; font-weight: 700; }
|
||||||
|
.firma-info { font-size: 11px; color: #555; margin-top: 4px; line-height: 1.7; }
|
||||||
|
.dok-tip { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 1px; color: #555; margin-bottom: 4px; }
|
||||||
|
.dok-broj { font-size: 18px; font-weight: 700; font-family: monospace; }
|
||||||
|
.dok-datum { font-size: 12px; color: #555; margin-top: 4px; }
|
||||||
|
|
||||||
|
/* blok garancije */
|
||||||
|
.garancija-blok { padding: 14px 18px; border: 2px solid #16a34a; border-radius: 10px; background: #f0fdf4; margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center; gap: 16px; }
|
||||||
|
.garancija-tekst { font-size: 13px; color: #15803d; font-weight: 500; }
|
||||||
|
.garancija-datum { font-size: 22px; font-weight: 800; color: #15803d; white-space: nowrap; }
|
||||||
|
.garancija-trajanje { font-size: 12px; color: #166534; margin-top: 3px; }
|
||||||
|
|
||||||
|
/* odeljci */
|
||||||
|
.odeljak { margin-bottom: 18px; }
|
||||||
|
.odeljak-naslov { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.8px; color: #555; margin-bottom: 8px; padding-bottom: 4px; border-bottom: 0.5px solid #ccc; }
|
||||||
|
|
||||||
|
/* podaci */
|
||||||
|
.podaci-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
|
||||||
|
.polje-labela { font-size: 10px; color: #777; text-transform: uppercase; letter-spacing: 0.4px; margin-bottom: 3px; }
|
||||||
|
.polje-vrednost { font-size: 13px; font-weight: 500; }
|
||||||
|
.polje-vrednost-mono { font-family: monospace; font-size: 13px; font-weight: 500; }
|
||||||
|
.polje-tekst { font-size: 13px; line-height: 1.6; white-space: pre-wrap; }
|
||||||
|
.prazno { color: #aaa; font-style: italic; }
|
||||||
|
|
||||||
|
/* tabela stavki */
|
||||||
|
.tabela { width: 100%; border-collapse: collapse; font-size: 12px; margin-top: 6px; }
|
||||||
|
.tabela th { text-align: left; padding: 6px 8px; font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px; color: #666; border-bottom: 1px solid #ddd; font-weight: 600; }
|
||||||
|
.tabela td { padding: 6px 8px; border-bottom: 0.5px solid #eee; vertical-align: top; }
|
||||||
|
.tabela td:last-child { text-align: right; white-space: nowrap; }
|
||||||
|
.tabela th:last-child { text-align: right; }
|
||||||
|
.prazna-poruka { font-size: 12px; color: #aaa; font-style: italic; padding: 8px 0; }
|
||||||
|
|
||||||
|
/* uslovi */
|
||||||
|
.uslovi-blok { font-size: 11px; color: #444; line-height: 1.7; white-space: pre-wrap; }
|
||||||
|
|
||||||
|
/* potpisi */
|
||||||
|
.potpisi { display: grid; grid-template-columns: 1fr 1fr; gap: 40px; margin-top: 36px; padding-top: 16px; border-top: 0.5px solid #ccc; }
|
||||||
|
.potpis-linija { border-top: 1px solid #888; margin-top: 44px; padding-top: 6px; font-size: 11px; color: #555; text-align: center; }
|
||||||
|
|
||||||
|
.dno-info { margin-top: 20px; font-size: 11px; color: #888; text-align: center; }
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
body { font-size: 12px; }
|
||||||
|
.strana { padding: 16px 20px; max-width: 100%; }
|
||||||
|
.dugme-stampa { display: none !important; }
|
||||||
|
@page { size: A4; margin: 12mm 14mm; }
|
||||||
|
}
|
||||||
|
.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; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="strana">
|
||||||
|
|
||||||
|
<!-- zaglavlje -->
|
||||||
|
<div class="zaglavlje">
|
||||||
|
<!-- LEVO: firma -->
|
||||||
|
<div class="zg-okvir">
|
||||||
|
{{if .LogoPutanja}}
|
||||||
|
<img src="{{.LogoPutanja}}" alt="Logo" style="max-height:54px;max-width:100%;object-fit:contain;display:block;margin-bottom:8px;">
|
||||||
|
{{else}}
|
||||||
|
<div class="firma-naziv">{{if .NazivFirme}}{{.NazivFirme}}{{else}}— naziv firme —{{end}}</div>
|
||||||
|
{{end}}
|
||||||
|
<div class="firma-info">
|
||||||
|
{{if .Podnazlov}}{{.Podnazlov}}<br>{{end}}
|
||||||
|
{{if .Adresa}}{{.Adresa}}<br>{{end}}
|
||||||
|
{{if .Telefon}}Tel: {{telefon .Telefon}}<br>{{end}}
|
||||||
|
{{if .PIB}}PIB: {{.PIB}}{{end}}{{if .MaticniBroj}}{{if .PIB}} · {{end}}MB: {{.MaticniBroj}}{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- SREDINA: barkod + dokument -->
|
||||||
|
<div class="zg-okvir" style="flex:1.3;">
|
||||||
|
{{if .Barkod}}<img src="data:image/png;base64,{{.Barkod}}" alt="Barkod {{.Nalog.BrojNaloga}}" style="display:block;width:100%;max-height:46px;object-fit:contain;margin-bottom:10px;">{{end}}
|
||||||
|
<div class="dok-tip">Garantni list</div>
|
||||||
|
<div class="dok-broj">{{.Nalog.BrojNaloga}}</div>
|
||||||
|
{{if .Nalog.DatumZavrsetka}}
|
||||||
|
<div class="dok-datum">Datum završetka: {{.Nalog.DatumZavrsetka.Format "02.01.2006."}}</div>
|
||||||
|
{{end}}
|
||||||
|
{{if .TehnicarNaziv}}
|
||||||
|
<div class="dok-datum">Serviser: {{.TehnicarNaziv}}</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
<!-- DESNO: klijent -->
|
||||||
|
<div class="zg-okvir">
|
||||||
|
<div class="polje-labela">Klijent</div>
|
||||||
|
{{if .KlijentNaziv}}
|
||||||
|
<div class="polje-vrednost" style="font-size:14px;font-weight:600;margin-top:2px;">{{.KlijentNaziv}}</div>
|
||||||
|
<div style="font-size:12px;color:#555;margin-top:4px;line-height:1.6;">
|
||||||
|
{{if .Klijent}}
|
||||||
|
{{if .Klijent.Telefon}}Tel: {{telefon .Klijent.Telefon}}<br>{{end}}
|
||||||
|
{{if .Klijent.Email}}{{.Klijent.Email}}<br>{{end}}
|
||||||
|
{{if .Klijent.Mesto}}{{.Klijent.Mesto}}{{end}}
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<div class="prazno" style="margin-top:4px;">— klijent nije naveden —</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- blok garancije -->
|
||||||
|
<div class="garancija-blok">
|
||||||
|
<div>
|
||||||
|
<div class="garancija-tekst">Garancija na izvršeni servis</div>
|
||||||
|
<div class="garancija-trajanje">Trajanje: {{.GarancijaTekst}}</div>
|
||||||
|
</div>
|
||||||
|
<div style="text-align:right;">
|
||||||
|
<div class="garancija-tekst">Važi do:</div>
|
||||||
|
<div class="garancija-datum">{{.GarancijaDo.Format "02.01.2006."}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- uređaj -->
|
||||||
|
<div class="odeljak">
|
||||||
|
<div class="odeljak-naslov">Uređaj</div>
|
||||||
|
<div class="podaci-grid">
|
||||||
|
<div>
|
||||||
|
<div class="polje-labela">Naziv uređaja</div>
|
||||||
|
<div class="polje-vrednost">{{if .Nalog.Uredjaj}}{{.Nalog.Uredjaj}}{{else}}<span class="prazno">—</span>{{end}}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="polje-labela">Serijski broj</div>
|
||||||
|
<div class="polje-vrednost-mono">{{if .Nalog.SerijskiBroj}}{{.Nalog.SerijskiBroj}}{{else}}<span class="prazno">—</span>{{end}}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="polje-labela">Datum prijema</div>
|
||||||
|
<div class="polje-vrednost">{{.Nalog.DatumPrijema.Format "02.01.2006."}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- urađeni radovi -->
|
||||||
|
<div class="odeljak">
|
||||||
|
<div class="odeljak-naslov">Urađeni radovi</div>
|
||||||
|
{{if .Radovi}}
|
||||||
|
<table class="tabela">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Opis rada</th>
|
||||||
|
<th>Kol.</th>
|
||||||
|
<th>Cena</th>
|
||||||
|
<th>Ukupno</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{range .Radovi}}
|
||||||
|
<tr>
|
||||||
|
<td>{{.Naziv}}</td>
|
||||||
|
<td style="text-align:right;">{{printf "%.0f" .Kolicina}}</td>
|
||||||
|
<td style="text-align:right;">{{dinari .CenaKomada}}</td>
|
||||||
|
<td>{{dinari .Ukupno}}</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{{else}}
|
||||||
|
<div class="prazna-poruka">Nisu evidentirani posebni radovi.</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- zamenjeni delovi -->
|
||||||
|
<div class="odeljak">
|
||||||
|
<div class="odeljak-naslov">Zamenjeni delovi / materijal</div>
|
||||||
|
{{if .ServisniDelovi}}
|
||||||
|
<table class="tabela">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Naziv</th>
|
||||||
|
<th>Šifra</th>
|
||||||
|
<th>Kol.</th>
|
||||||
|
<th>Cena</th>
|
||||||
|
<th>Ukupno</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{range .ServisniDelovi}}
|
||||||
|
{{if not .Predlozeno}}
|
||||||
|
<tr>
|
||||||
|
<td>{{.ArtikalNaziv}}</td>
|
||||||
|
<td style="font-family:monospace;">{{.ArtikalSifra}}</td>
|
||||||
|
<td style="text-align:right;">{{.Kolicina}}</td>
|
||||||
|
<td style="text-align:right;">{{dinari .CenaKomada}}</td>
|
||||||
|
<td>{{dinari .Ukupno}}</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{{else}}
|
||||||
|
<div class="prazna-poruka">Nisu evidentirani zamenjeni delovi.</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- napomena -->
|
||||||
|
{{if .Nalog.NapomenaKlijentu}}
|
||||||
|
<div class="odeljak">
|
||||||
|
<div class="odeljak-naslov">Napomena</div>
|
||||||
|
<div class="polje-tekst">{{.Nalog.NapomenaKlijentu}}</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<!-- uslovi garancije -->
|
||||||
|
{{if .Uslovi}}
|
||||||
|
<div class="odeljak">
|
||||||
|
<div class="odeljak-naslov">Uslovi garancije</div>
|
||||||
|
<div class="uslovi-blok">{{.Uslovi}}</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<!-- potpisi -->
|
||||||
|
<div class="potpisi">
|
||||||
|
<div>
|
||||||
|
<div class="potpis-linija">Preuzeo (klijent)</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="potpis-linija">Predao (servis)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dno-info">Garantni list važi uz predočenje ovog dokumenta i originalnog reversa.</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<button class="dugme-stampa" onclick="window.print()">Štampaj</button>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -164,6 +164,33 @@
|
|||||||
<div class="polje-tekst">{{if .Nalog.OpisKvara}}{{.Nalog.OpisKvara}}{{else}}<span class="prazno">—</span>{{end}}</div>
|
<div class="polje-tekst">{{if .Nalog.OpisKvara}}{{.Nalog.OpisKvara}}{{else}}<span class="prazno">—</span>{{end}}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- radovi -->
|
||||||
|
{{if .Radovi}}
|
||||||
|
<div class="odeljak">
|
||||||
|
<div class="odeljak-naslov">Izvršeni radovi</div>
|
||||||
|
<table class="tabela">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Opis rada</th>
|
||||||
|
<th class="desno" style="width:70px;">Kol.</th>
|
||||||
|
<th class="desno" style="width:120px;">Cena/kom</th>
|
||||||
|
<th class="desno" style="width:120px;">Ukupno</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{range .Radovi}}
|
||||||
|
<tr>
|
||||||
|
<td>{{.Naziv}}</td>
|
||||||
|
<td class="desno">{{printf "%.0f" .Kolicina}}</td>
|
||||||
|
<td class="desno">{{dinari .CenaKomada}} din</td>
|
||||||
|
<td class="desno">{{dinari .Ukupno}} din</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
<!-- ugrađeni delovi -->
|
<!-- ugrađeni delovi -->
|
||||||
{{if .ServisniDelovi}}
|
{{if .ServisniDelovi}}
|
||||||
<div class="odeljak">
|
<div class="odeljak">
|
||||||
|
|||||||
@@ -54,14 +54,18 @@
|
|||||||
/* napomena na dnu */
|
/* napomena na dnu */
|
||||||
.dno-info { margin-top: 20px; font-size: 11px; color: #888; text-align: center; }
|
.dno-info { margin-top: 20px; font-size: 11px; color: #888; text-align: center; }
|
||||||
|
|
||||||
|
@page { size: A4; margin: 12mm 14mm 18mm; }
|
||||||
@media print {
|
@media print {
|
||||||
body { font-size: 12px; }
|
body { font-size: 12px; }
|
||||||
.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; }
|
|
||||||
/* uslovi servisa (sa potpisima) idu na novu stranicu */
|
/* uslovi servisa (sa potpisima) idu na novu stranicu */
|
||||||
.uslovi-strana { break-before: page; }
|
.uslovi-strana { break-before: page; }
|
||||||
|
.stampa-broj-strane { display: block; text-align: center; font-size: 9px; color: #888; margin-top: 16px; }
|
||||||
|
.stampa-samo-ekran { display: none !important; }
|
||||||
}
|
}
|
||||||
|
.stampa-broj-strane { display: none; }
|
||||||
|
.stampa-samo-ekran { display: block; }
|
||||||
.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; }
|
||||||
</style>
|
</style>
|
||||||
@@ -202,5 +206,79 @@
|
|||||||
|
|
||||||
<button class="dugme-stampa" onclick="window.print()">Štampaj</button>
|
<button class="dugme-stampa" onclick="window.print()">Štampaj</button>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Brojevi strana pri štampi — radi u svim browser-ima
|
||||||
|
(function() {
|
||||||
|
const sadrzaj = document.querySelector('.strana');
|
||||||
|
if (!sadrzaj) return;
|
||||||
|
|
||||||
|
let originalniHTML = '';
|
||||||
|
// Visina štampane strane (A4 - margine) u px
|
||||||
|
const VISINA_STRANE_PX = (297 - 12 - 18) * 96 / 25.4;
|
||||||
|
|
||||||
|
function izmeriVisinu(el) {
|
||||||
|
const test = el.cloneNode(true);
|
||||||
|
test.style.cssText = 'position:absolute;visibility:hidden;width:' + sadrzaj.offsetWidth + 'px;max-width:none;';
|
||||||
|
document.body.appendChild(test);
|
||||||
|
const h = test.scrollHeight;
|
||||||
|
document.body.removeChild(test);
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('beforeprint', function() {
|
||||||
|
if (originalniHTML) return;
|
||||||
|
originalniHTML = sadrzaj.innerHTML;
|
||||||
|
|
||||||
|
const ukupnaVisina = izmeriVisinu(sadrzaj);
|
||||||
|
const brStrana = Math.max(1, Math.ceil(ukupnaVisina / VISINA_STRANE_PX));
|
||||||
|
if (brStrana <= 1) return;
|
||||||
|
|
||||||
|
const deca = Array.from(sadrzaj.children);
|
||||||
|
sadrzaj.innerHTML = '';
|
||||||
|
|
||||||
|
let stranaBr = 1;
|
||||||
|
let visinaTrenutne = 0;
|
||||||
|
let stranaDiv = napraviStranu(stranaBr, brStrana);
|
||||||
|
sadrzaj.appendChild(stranaDiv);
|
||||||
|
|
||||||
|
for (const el of deca) {
|
||||||
|
const h = izmeriVisinu(el);
|
||||||
|
if (visinaTrenutne + h > VISINA_STRANE_PX && visinaTrenutne > 200) {
|
||||||
|
dodajBrojac(stranaDiv, stranaBr, brStrana);
|
||||||
|
stranaBr++;
|
||||||
|
stranaDiv = napraviStranu(stranaBr, brStrana);
|
||||||
|
sadrzaj.appendChild(stranaDiv);
|
||||||
|
visinaTrenutne = 0;
|
||||||
|
}
|
||||||
|
stranaDiv.appendChild(el.cloneNode(true));
|
||||||
|
visinaTrenutne += h;
|
||||||
|
}
|
||||||
|
dodajBrojac(stranaDiv, stranaBr, brStrana);
|
||||||
|
});
|
||||||
|
|
||||||
|
function napraviStranu(br, ukupno) {
|
||||||
|
const d = document.createElement('div');
|
||||||
|
d.className = 'stampa-stranica';
|
||||||
|
d.style.cssText = 'min-height:' + (VISINA_STRANE_PX - 30) + 'px;';
|
||||||
|
if (br < ukupno) d.style.pageBreakAfter = 'always';
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dodajBrojac(stranaDiv, br, ukupno) {
|
||||||
|
const brojac = document.createElement('div');
|
||||||
|
brojac.className = 'stampa-broj-strane';
|
||||||
|
brojac.textContent = br + ' / ' + ukupno;
|
||||||
|
stranaDiv.appendChild(brojac);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('afterprint', function() {
|
||||||
|
if (originalniHTML) {
|
||||||
|
sadrzaj.innerHTML = originalniHTML;
|
||||||
|
originalniHTML = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -243,7 +243,7 @@
|
|||||||
{{if .ImaPredlog}}
|
{{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;">
|
<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-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>
|
<span class="cena-vrednost" id="sa-predlogom-zbir" data-base="{{printf "%.2f" .UkupnoSve}}" style="font-size:16px;font-weight:700;color:#fbbf24;">{{dinari .UkupnoSaPredlogom}} din</span>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
@@ -261,33 +261,43 @@
|
|||||||
Serviser predlaže sledeće radove i delove. Pogledajte i javite nam da li ste saglasni.
|
Serviser predlaže sledeće radove i delove. Pogledajte i javite nam da li ste saglasni.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<form method="POST" action="/status/{{.Nalog.JavniToken}}/odluka-odabrano" style="margin-top:4px;">
|
||||||
|
{{if .PredlozeniRadovi}}
|
||||||
|
<div style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.6px;color:#94a3b8;margin-bottom:6px;">Radovi</div>
|
||||||
{{range .PredlozeniRadovi}}
|
{{range .PredlozeniRadovi}}
|
||||||
<div class="red">
|
<label style="display:flex;align-items:center;justify-content:space-between;gap:10px;padding:8px 0;border-bottom:0.5px solid #1e293b;cursor:pointer;">
|
||||||
<span class="red-labela">{{.Naziv}}{{if gt .Kolicina 1.0}} ×{{.Kolicina}}{{end}}</span>
|
<span style="display:flex;align-items:center;gap:10px;flex:1;">
|
||||||
<span class="red-vrednost">{{dinari .Ukupno}} din</span>
|
<input type="checkbox" name="rad_id" value="{{.ID}}" checked data-iznos="{{printf "%.2f" .Ukupno}}"
|
||||||
</div>
|
style="width:18px;height:18px;accent-color:#f59e0b;flex-shrink:0;">
|
||||||
|
<span style="font-size:13px;color:#e2e8f0;">{{.Naziv}}{{if gt .Kolicina 1.0}} × {{printf "%.0f" .Kolicina}}{{end}}</span>
|
||||||
|
</span>
|
||||||
|
<span style="font-size:13px;font-weight:600;color:#fbbf24;white-space:nowrap;">{{dinari .Ukupno}} din</span>
|
||||||
|
</label>
|
||||||
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
{{if .PredlozeniDelovi}}
|
||||||
|
<div style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.6px;color:#94a3b8;margin:10px 0 6px;">Delovi i materijal</div>
|
||||||
{{range .PredlozeniDelovi}}
|
{{range .PredlozeniDelovi}}
|
||||||
<div class="red">
|
<label style="display:flex;align-items:center;justify-content:space-between;gap:10px;padding:8px 0;border-bottom:0.5px solid #1e293b;cursor:pointer;">
|
||||||
<span class="red-labela">{{.ArtikalNaziv}}{{if gt .Kolicina 1}} ×{{.Kolicina}}{{end}}</span>
|
<span style="display:flex;align-items:center;gap:10px;flex:1;">
|
||||||
<span class="red-vrednost">{{dinari .Ukupno}} din</span>
|
<input type="checkbox" name="artikal_id" value="{{.ArtikalID}}" checked data-iznos="{{printf "%.2f" .Ukupno}}"
|
||||||
</div>
|
style="width:18px;height:18px;accent-color:#f59e0b;flex-shrink:0;">
|
||||||
|
<span style="font-size:13px;color:#e2e8f0;">{{.ArtikalNaziv}}{{if gt .Kolicina 1}} × {{.Kolicina}}{{end}}</span>
|
||||||
|
</span>
|
||||||
|
<span style="font-size:13px;font-weight:600;color:#fbbf24;white-space:nowrap;">{{dinari .Ukupno}} din</span>
|
||||||
|
</label>
|
||||||
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<div class="cena-blok predlog-ukupno" style="margin-top:12px;display:flex;justify-content:space-between;align-items:center;">
|
<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-labela" style="margin:0;">Predlog ukupno</span>
|
||||||
<span class="cena-vrednost">{{dinari .UkupnoPredlog}} din</span>
|
<span class="cena-vrednost" id="predlog-zbir">{{dinari .UkupnoPredlog}} din</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- komentar i dugmad -->
|
<textarea name="komentar" placeholder="Poruka serviseru (opciono)..." rows="2"
|
||||||
<form method="POST" style="margin-top:16px;">
|
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:14px 0 10px;box-sizing:border-box;"></textarea>
|
||||||
<textarea name="komentar" placeholder="Poruka serviseru (opciono)..." rows="2"
|
<button type="submit" class="dugme-prihvati" style="width:100%;">Potvrdi odabrano</button>
|
||||||
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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -310,5 +320,37 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var zbir = document.getElementById('predlog-zbir');
|
||||||
|
if (!zbir) return;
|
||||||
|
var forma = zbir.closest('form');
|
||||||
|
if (!forma) return;
|
||||||
|
function fmt(v) {
|
||||||
|
var s = Math.round(v * 100);
|
||||||
|
var dec = String(s % 100).padStart(2, '0');
|
||||||
|
var cel = String(Math.floor(s / 100));
|
||||||
|
var r = '';
|
||||||
|
for (var i = 0; i < cel.length; i++) {
|
||||||
|
if (i > 0 && (cel.length - i) % 3 === 0) r += '.';
|
||||||
|
r += cel[i];
|
||||||
|
}
|
||||||
|
return r + ',' + dec + ' din';
|
||||||
|
}
|
||||||
|
var saPredlogom = document.getElementById('sa-predlogom-zbir');
|
||||||
|
var bazaUkupno = saPredlogom ? parseFloat(saPredlogom.getAttribute('data-base')) || 0 : 0;
|
||||||
|
function preracunaj() {
|
||||||
|
var suma = 0;
|
||||||
|
forma.querySelectorAll('input[type="checkbox"][data-iznos]').forEach(function (cb) {
|
||||||
|
if (cb.checked) suma += parseFloat(cb.getAttribute('data-iznos')) || 0;
|
||||||
|
});
|
||||||
|
zbir.textContent = fmt(suma);
|
||||||
|
if (saPredlogom) saPredlogom.textContent = fmt(bazaUkupno + suma);
|
||||||
|
}
|
||||||
|
forma.addEventListener('change', function (e) {
|
||||||
|
if (e.target && e.target.type === 'checkbox') preracunaj();
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user