Servis: logo na dokumentima, jedinstveno 3-kolonsko zaglavlje, podesiva QR adresa, javni status

- Logo na dokumentima ako je ukljucen prikaz loga; helper logoZaDokument
- Jedinstveno zaglavlje (revers/predracun/otpremnica/radni nalog): firma | klijent | barkod+broj+datumi+QR; dodati MB i barkod
- Revers: QR u sekciju Uredjaj desno, bez JMBG, Primio bez imena servisera
- Podesavanja Opste: auto-snimanje sve 3 sekcije (marker _sekcija_firma za checkbox loga)
- Podesiva adresa za QR (qr_bazni_url) za pristup sa telefona / Caddy HTTPS
- Javni status: token se gasi po preuzimanju; predvidjeni datum zavrsetka, garancija i napomena iz baze; nova kartica Radovi i delovi sa procenom
This commit is contained in:
2026-06-23 14:38:26 +02:00
parent 51febc5d41
commit a8d5b0e9b4
9 changed files with 335 additions and 291 deletions
+22 -4
View File
@@ -67,6 +67,7 @@ type PodaciPodesavanja struct {
PredvidjenRokDana string
ServisUslovi string
ServisKlauzulaPredracuna string
QrBazniUrl string
LoginPozadina string
LoginPozadinaOpacity string
LoginPozadinaBlurPozadine string
@@ -280,10 +281,17 @@ func (h *Handler) SacuvajPodesavanja(w http.ResponseWriter, r *http.Request) {
return
}
// checkbox-i: šalju vrednost samo kada su čekirani, pa ih uvek eksplicitno čitamo
topbarLogoSlika := "0"
if r.FormValue("topbar_logo_slika") == "1" {
topbarLogoSlika = "1"
// checkbox „topbar_logo_slika" živi u Firma sekciji. Pošto se sekcije auto-snimaju
// odvojeno, čitamo ga SAMO kad je poslata Firma sekcija (marker `_sekcija_firma`),
// inače ostaje "" pa ga petlja ispod preskoči i ne dira postojeću vrednost.
// (Checkbox koji nije čekiran se ne šalje, pa bi inače snimanje druge sekcije
// ugasilo logo u topbaru.)
topbarLogoSlika := ""
if _, jeFirmaSekcija := r.Form["_sekcija_firma"]; jeFirmaSekcija {
topbarLogoSlika = "0"
if r.FormValue("topbar_logo_slika") == "1" {
topbarLogoSlika = "1"
}
}
// matični broj — 8 cifara; prazno je dozvoljeno (polje nije obavezno)
@@ -441,6 +449,15 @@ func (h *Handler) SacuvajPodesavanja(w http.ResponseWriter, r *http.Request) {
}
}
// bazna adresa za QR kod (npr. http://192.168.1.25:3000); prazno → koristi se host iz zahteva
if _, ima := r.Form["qr_bazni_url"]; ima {
bazni := strings.TrimSpace(r.FormValue("qr_bazni_url"))
if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "qr_bazni_url", bazni); err != nil {
http.Error(w, "Greška pri čuvanju podešavanja", http.StatusInternalServerError)
return
}
}
http.Redirect(w, r, sledeci+"?sacuvano=1", http.StatusSeeOther)
}
@@ -818,6 +835,7 @@ func (h *Handler) napuniPodaciPodesavanja(r *http.Request, naslov string) (Podac
PredvidjenRokDana: vrednostIliDefault(podesavanja, "predvidjen_rok_dana", "15"),
ServisUslovi: vrednostIliDefault(podesavanja, "servis_uslovi", podrazumevaniUsloviServisa),
ServisKlauzulaPredracuna: vrednostIliDefault(podesavanja, "servis_klauzula_predracuna", podrazumevanaKlauzulaPredracuna),
QrBazniUrl: podesavanja["qr_bazni_url"],
}, nil
}
+83 -16
View File
@@ -1106,6 +1106,15 @@ func (h *Handler) renderujFormuNaloga(w http.ResponseWriter, podaci PodaciFormeN
}
// PodaciRadnogNaloga su podaci za interni radni list servisera (bez cena)
// logoZaDokument vraća putanju loga za štampu na dokumentima — samo ako je u
// Podešavanja → Opšte uključeno „Prikaži logo" (topbar_logo_slika). Inače prazno.
func logoZaDokument(podesavanja map[string]string) string {
if podesavanja["topbar_logo_slika"] == "1" {
return podesavanja["logo_putanja"]
}
return ""
}
type PodaciRadnogNaloga struct {
Nalog model.ServisniNalog
ServisniDelovi []model.ServisniDeoSaArtiklom
@@ -1113,6 +1122,7 @@ type PodaciRadnogNaloga struct {
KlijentTelefon string
TehnicarNaziv string
NazivFirme string
LogoPutanja string
Barkod string // base64 PNG Code128 barkoda broja naloga (za skener)
}
@@ -1171,6 +1181,7 @@ func (h *Handler) StampaRadnogNaloga(w http.ResponseWriter, r *http.Request) {
KlijentTelefon: klijentTelefon,
TehnicarNaziv: tehnicarNaziv,
NazivFirme: podesavanja["naziv_firme"],
LogoPutanja: logoZaDokument(podesavanja),
Barkod: barkodNaloga(nalog.BrojNaloga),
})
}
@@ -1187,10 +1198,13 @@ type PodaciOtpremnice struct {
KlijentNaziv string
TehnicarNaziv string
NazivFirme string
LogoPutanja string
Podnazlov string
Adresa string
Telefon string
PIB string
MaticniBroj string
Barkod string // base64 PNG Code128 barkoda broja naloga
}
// StampaOtpremnice renderuje otpremnicu pri preuzimanju uređaja od strane klijenta
@@ -1260,7 +1274,7 @@ func (h *Handler) StampaOtpremnice(w http.ResponseWriter, r *http.Request) {
}
}
nalogURL := qrNalogURL(r, nalog.JavniToken)
nalogURL := qrNalogURL(r, nalog.JavniToken, podesavanja["qr_bazni_url"])
var qrKodOtpr string
if png, err := qrcode.Encode(nalogURL, qrcode.Medium, 160); err == nil {
qrKodOtpr = base64.StdEncoding.EncodeToString(png)
@@ -1277,10 +1291,13 @@ func (h *Handler) StampaOtpremnice(w http.ResponseWriter, r *http.Request) {
KlijentNaziv: klijentNaziv,
TehnicarNaziv: tehnicarNaziv,
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),
})
}
@@ -1296,10 +1313,13 @@ type PodaciReversa struct {
QRKod string // base64 QR ka javnoj status strani naloga
Uslovi string // uslovi servisa iz podešavanja (servis_uslovi)
NazivFirme string
LogoPutanja string
Podnazlov string
Adresa string
Telefon string
PIB string
MaticniBroj string
Barkod string // base64 PNG Code128 barkoda broja naloga
}
// StampaReversa renderuje revers (potvrdu o prijemu uređaja na servis)
@@ -1346,7 +1366,7 @@ func (h *Handler) StampaReversa(w http.ResponseWriter, r *http.Request) {
}
}
nalogURL := qrNalogURL(r, nalog.JavniToken)
nalogURL := qrNalogURL(r, nalog.JavniToken, podesavanja["qr_bazni_url"])
var qrKod string
if png, err := qrcode.Encode(nalogURL, qrcode.Medium, 160); err == nil {
qrKod = base64.StdEncoding.EncodeToString(png)
@@ -1362,10 +1382,13 @@ func (h *Handler) StampaReversa(w http.ResponseWriter, r *http.Request) {
QRKod: qrKod,
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),
})
}
@@ -1386,10 +1409,13 @@ type PodaciPredracuna struct {
KlijentNaziv string
TehnicarNaziv string
NazivFirme string
LogoPutanja string
Podnazlov string
Adresa string
Telefon string
PIB string
MaticniBroj string
Barkod string // base64 PNG Code128 barkoda broja naloga
}
// StampaPredracuna renderuje predračun (procenu cene) na osnovu traženih radova i delova
@@ -1459,7 +1485,7 @@ func (h *Handler) StampaPredracuna(w http.ResponseWriter, r *http.Request) {
datumIzdavanja := time.Now()
vaziDo := datumIzdavanja.AddDate(0, 0, rok)
nalogURL := qrNalogURL(r, nalog.JavniToken)
nalogURL := qrNalogURL(r, nalog.JavniToken, podesavanja["qr_bazni_url"])
var qrKod string
if png, err := qrcode.Encode(nalogURL, qrcode.Medium, 160); err == nil {
qrKod = base64.StdEncoding.EncodeToString(png)
@@ -1480,10 +1506,13 @@ func (h *Handler) StampaPredracuna(w http.ResponseWriter, r *http.Request) {
KlijentNaziv: klijentNaziv,
TehnicarNaziv: tehnicarNaziv,
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),
})
}
@@ -1779,11 +1808,20 @@ func (h *Handler) SacuvajNapomenuKlijentu(w http.ResponseWriter, r *http.Request
// PodaciJavnogStatusa su podaci za javnu status stranicu servisnog naloga
type PodaciJavnogStatusa struct {
Nalog model.ServisniNalog
NazivFirme string
Telefon string
Adresa string
SviStatusi []string
Nalog model.ServisniNalog
NazivFirme string
LogoPutanja string
Podnazlov string
Adresa string
Telefon string
PIB string
MaticniBroj string
Radovi []model.ServisniRad
UkupnoRad float64
ServisniDelovi []model.ServisniDeoSaArtiklom
UkupnoDelovi float64
UkupnoSve float64
SviStatusi []string
}
// ServisJavniStatus prikazuje javnu status stranicu — dostupna bez prijave putem QR koda
@@ -1800,21 +1838,50 @@ func (h *Handler) ServisJavniStatus(w http.ResponseWriter, r *http.Request) {
return
}
// javni link se „gasi" čim klijent preuzme uređaj — posle preuzimanja status više nije dostupan
if nalog.Status == model.StatusPreuzeto {
http.NotFound(w, r)
return
}
radovi, _ := h.ServisniRadoviRepo.DohvatiZaNalog(r.Context(), nalog.ID)
delovi, _ := h.deloviSaPotrazivanima(r.Context(), nalog.ID)
var ukupnoRad, ukupnoDelovi float64
for _, rd := range radovi {
ukupnoRad += rd.Ukupno()
}
for _, d := range delovi {
ukupnoDelovi += d.Ukupno()
}
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
h.renderujStandalone(w, "servis_status_javni", PodaciJavnogStatusa{
Nalog: *nalog,
NazivFirme: podesavanja["naziv_firme"],
Telefon: podesavanja["telefon"],
Adresa: podesavanja["adresa"],
SviStatusi: model.SviStatusi,
Nalog: *nalog,
NazivFirme: podesavanja["naziv_firme"],
LogoPutanja: logoZaDokument(podesavanja),
Podnazlov: podesavanja["podnazlov"],
Adresa: podesavanja["adresa"],
Telefon: podesavanja["telefon"],
PIB: podesavanja["pib"],
MaticniBroj: podesavanja["maticni_broj"],
Radovi: radovi,
UkupnoRad: ukupnoRad,
ServisniDelovi: delovi,
UkupnoDelovi: ukupnoDelovi,
UkupnoSve: ukupnoRad + ukupnoDelovi,
SviStatusi: model.SviStatusi,
})
}
// qrNalogURL konstruiše URL za QR kod vodeći računa o reverse proxy-ju.
// Ako aplikacija radi iza nginx/Caddy/Traefik koji prekida TLS, r.TLS je nil,
// ali X-Forwarded-Proto header sadrži stvarnu šemu.
func qrNalogURL(r *http.Request, token string) string {
// Ako je u podešavanjima zadata fiksna adresa (bazniURL, npr. http://192.168.1.25:3000),
// koristi se ona — korisno da QR radi sa telefona kad se programu pristupa preko „localhost".
// Inače se host i šema čitaju iz zahteva (X-Forwarded-Proto pokriva nginx/Caddy/Traefik).
func qrNalogURL(r *http.Request, token, bazniURL string) string {
if b := strings.TrimRight(strings.TrimSpace(bazniURL), "/"); b != "" {
return b + "/status/" + token
}
scheme := "http"
if r.TLS != nil {
scheme = "https"