Servis: dodata štampa servisnog naloga

- Nova stranica /servis/{id}/stampa — print-friendly A4 dokument
- Prikazuje: zaglavlje firme, broj naloga, status, klijent, tehničar,
  uređaj sa opisom kvara i pribором, ugrađene delove sa ukupnim iznosom,
  cene usluge i prostor za potpise
- Dugme Štampaj nalog dodata na stranicu detalja naloga
This commit is contained in:
2026-06-19 19:46:05 +02:00
parent 695bb3e617
commit 8048834f87
5 changed files with 335 additions and 4 deletions
+1
View File
@@ -337,6 +337,7 @@ func main() {
r.With(doz("servis.izmeni")).Post("/servis/izmeni/{id}", h.SacuvajIzmenaNaloga) r.With(doz("servis.izmeni")).Post("/servis/izmeni/{id}", h.SacuvajIzmenaNaloga)
r.With(doz("servis.obrisi")).Post("/servis/obrisi/{id}", h.ObrisiNalog) r.With(doz("servis.obrisi")).Post("/servis/obrisi/{id}", h.ObrisiNalog)
r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "servis.pregled")).Get("/servis/{id}", h.DetaljiNaloga) r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "servis.pregled")).Get("/servis/{id}", h.DetaljiNaloga)
r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "servis.pregled")).Get("/servis/{id}/stampa", h.StampaServisa)
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.Get("/izvestaji", h.Izvestaji) r.Get("/izvestaji", h.Izvestaji)
+1 -1
View File
@@ -38,7 +38,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", "prijava", "setup", "totp_provera", "prodaja_stampa", "servis_stampa",
} }
// sablonskeFunkcije su pomoćne funkcije dostupne u svim šablonima. // sablonskeFunkcije su pomoćne funkcije dostupne u svim šablonima.
+79
View File
@@ -553,3 +553,82 @@ func parseOpcionuCenu(s string) *float64 {
func (h *Handler) renderujFormuNaloga(w http.ResponseWriter, podaci PodaciFormeNaloga) { func (h *Handler) renderujFormuNaloga(w http.ResponseWriter, podaci PodaciFormeNaloga) {
h.renderujTemplate(w, "servis_forma", podaci) h.renderujTemplate(w, "servis_forma", podaci)
} }
// PodaciStampeServisa su podaci za print-friendly prikaz servisnog naloga
type PodaciStampeServisa struct {
Nalog model.ServisniNalog
ServisniDelovi []model.ServisniDeoSaArtiklom
UkupnoDelovi float64
KlijentNaziv string
TehnicarNaziv string
NazivFirme string
Podnazlov string
Adresa string
Telefon string
PIB string
}
// StampaServisa renderuje print-friendly stranicu za servisni nalog
func (h *Handler) StampaServisa(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
}
delovi, err := h.ServisniDeloviRepo.DohvatiZaNalog(r.Context(), id)
if err != nil {
http.Error(w, "Greška pri učitavanju delova", http.StatusInternalServerError)
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
}
klijentNaziv := ""
if nalog.KlijentID != nil {
klijent, err := h.KlijentiRepo.DohvatiID(r.Context(), *nalog.KlijentID)
if err == nil {
if klijent.NazivFirme != "" {
klijentNaziv = klijent.NazivFirme
} else {
klijentNaziv = strings.TrimSpace(klijent.Ime + " " + klijent.Prezime)
}
}
}
tehnicarNaziv := ""
if nalog.TehnicarID != nil {
tehnicar, err := h.KorisniciRepo.DohvatiPoID(r.Context(), *nalog.TehnicarID)
if err == nil {
tehnicarNaziv = tehnicar.KorisnickoIme
}
}
var ukupnoDelovi float64
for _, d := range delovi {
ukupnoDelovi += d.Ukupno()
}
h.renderujStandalone(w, "servis_stampa", PodaciStampeServisa{
Nalog: *nalog,
ServisniDelovi: delovi,
UkupnoDelovi: ukupnoDelovi,
KlijentNaziv: klijentNaziv,
TehnicarNaziv: tehnicarNaziv,
NazivFirme: podesavanja["naziv_firme"],
Podnazlov: podesavanja["podnazlov"],
Adresa: podesavanja["adresa"],
Telefon: podesavanja["telefon"],
PIB: podesavanja["pib"],
})
}
+8 -3
View File
@@ -45,9 +45,14 @@
</span> </span>
{{template "status-badge-detalji" .Nalog.Status}} {{template "status-badge-detalji" .Nalog.Status}}
</div> </div>
<a href="/servis/izmeni/{{.Nalog.ID}}" class="btn-primarno"> <div style="display:flex;gap:8px;flex-wrap:wrap;">
Izmeni nalog <a href="/servis/{{.Nalog.ID}}/stampa" target="_blank" class="btn-sekundarno">
</a> Štampaj nalog
</a>
<a href="/servis/izmeni/{{.Nalog.ID}}" class="btn-primarno">
Izmeni nalog
</a>
</div>
</div> </div>
<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>
+246
View File
@@ -0,0 +1,246 @@
<!DOCTYPE html>
<html lang="sr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Servisni nalog {{.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 */
.zaglavlje { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 28px; padding-bottom: 16px; border-bottom: 2px solid #111; }
.firma-naziv { font-size: 20px; font-weight: 700; }
.firma-info { font-size: 11px; color: #555; margin-top: 4px; line-height: 1.6; }
.nalog-broj { text-align: right; }
.nalog-broj-vrednost { font-size: 22px; font-weight: 700; font-family: monospace; }
.nalog-naslov { font-size: 11px; color: #555; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; }
/* odeljci */
.odeljak { margin-bottom: 20px; }
.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; }
/* grid sa podacima */
.podaci-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; }
.podaci-grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
.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; }
.polje-tekst { font-size: 13px; line-height: 1.6; white-space: pre-wrap; }
/* status bedž */
.status { display: inline-block; padding: 2px 10px; border-radius: 20px; font-size: 11px; font-weight: 600; border: 1.5px solid #111; }
/* tabela delova */
.tabela { width: 100%; border-collapse: collapse; margin-top: 8px; }
.tabela th { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.4px; color: #555; padding: 6px 8px; border-bottom: 1.5px solid #111; text-align: left; }
.tabela th.desno { text-align: right; }
.tabela td { padding: 7px 8px; border-bottom: 0.5px solid #ddd; font-size: 13px; vertical-align: middle; }
.tabela td.desno { text-align: right; }
.tabela .ukupno-red td { font-weight: 600; border-top: 1.5px solid #111; border-bottom: none; }
/* cene */
.cene-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
.cena-blok { padding: 10px; border: 0.5px solid #ddd; border-radius: 6px; }
.cena-labela { font-size: 10px; color: #777; text-transform: uppercase; letter-spacing: 0.4px; margin-bottom: 4px; }
.cena-vrednost { font-size: 16px; font-weight: 700; }
.cena-konacna { border-color: #111; }
/* potpisi */
.potpisi { display: grid; grid-template-columns: 1fr 1fr; gap: 40px; margin-top: 32px; padding-top: 16px; border-top: 0.5px solid #ccc; }
.potpis-linija { border-top: 1px solid #888; margin-top: 40px; padding-top: 6px; font-size: 11px; color: #555; }
/* štampa */
@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; }
.prazno { color: #aaa; font-style: italic; }
</style>
</head>
<body>
<div class="strana">
<!-- zaglavlje firme i broj naloga -->
<div class="zaglavlje">
<div>
<div class="firma-naziv">{{if .NazivFirme}}{{.NazivFirme}}{{else}}— naziv firme —{{end}}</div>
<div class="firma-info">
{{if .Podnazlov}}{{.Podnazlov}}<br>{{end}}
{{if .Adresa}}{{.Adresa}}<br>{{end}}
{{if .Telefon}}Tel: {{.Telefon}}<br>{{end}}
{{if .PIB}}PIB: {{.PIB}}{{end}}
</div>
</div>
<div class="nalog-broj">
<div class="nalog-naslov">Servisni nalog</div>
<div class="nalog-broj-vrednost">{{.Nalog.BrojNaloga}}</div>
<div style="margin-top:6px;"><span class="status">{{.Nalog.Status}}</span></div>
</div>
</div>
<!-- datumi i klijent -->
<div class="odeljak">
<div class="odeljak-naslov">Osnovni podaci</div>
<div class="podaci-grid">
<div>
<div class="polje-labela">Datum prijema</div>
<div class="polje-vrednost">{{.Nalog.DatumPrijema.Format "02.01.2006. u 15:04"}}</div>
</div>
{{if .Nalog.DatumZavrsetka}}
<div>
<div class="polje-labela">Datum završetka</div>
<div class="polje-vrednost">{{.Nalog.DatumZavrsetka.Format "02.01.2006."}}</div>
</div>
{{end}}
{{if .Nalog.GarancijaDo}}
<div>
<div class="polje-labela">Garancija do</div>
<div class="polje-vrednost">{{.Nalog.GarancijaDo.Format "02.01.2006."}}</div>
</div>
{{end}}
<div>
<div class="polje-labela">Klijent</div>
<div class="polje-vrednost">{{if .KlijentNaziv}}{{.KlijentNaziv}}{{else}}<span class="prazno"></span>{{end}}</div>
</div>
{{if .TehnicarNaziv}}
<div>
<div class="polje-labela">Tehničar</div>
<div class="polje-vrednost">{{.TehnicarNaziv}}</div>
</div>
{{end}}
</div>
</div>
<!-- uređaj -->
<div class="odeljak">
<div class="odeljak-naslov">Uređaj</div>
<div class="podaci-grid" style="margin-bottom:12px;">
<div>
<div class="polje-labela">Naziv uređaja</div>
<div class="polje-vrednost">{{.Nalog.Uredjaj}}</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>
{{if .Nalog.PinUredjaja}}
<div>
<div class="polje-labela">PIN / lozinka</div>
<div class="polje-vrednost-mono">{{.Nalog.PinUredjaja}}</div>
</div>
{{end}}
</div>
<div style="margin-bottom:10px;">
<div class="polje-labela">Opis kvara</div>
<div class="polje-tekst">{{if .Nalog.OpisKvara}}{{.Nalog.OpisKvara}}{{else}}<span class="prazno"></span>{{end}}</div>
</div>
{{if or .Nalog.Ostecenja .Nalog.Pribor}}
<div class="podaci-grid-2">
{{if .Nalog.Ostecenja}}
<div>
<div class="polje-labela">Oštećenja pri prijemu</div>
<div class="polje-tekst">{{.Nalog.Ostecenja}}</div>
</div>
{{end}}
{{if .Nalog.Pribor}}
<div>
<div class="polje-labela">Pribor i oprema</div>
<div class="polje-tekst">{{.Nalog.Pribor}}</div>
</div>
{{end}}
</div>
{{end}}
</div>
<!-- ugrađeni delovi -->
{{if .ServisniDelovi}}
<div class="odeljak">
<div class="odeljak-naslov">Ugrađeni delovi</div>
<table class="tabela">
<thead>
<tr>
<th>Artikal</th>
<th class="desno" style="width:80px;">Kol.</th>
<th class="desno" style="width:130px;">Cena/kom</th>
<th class="desno" style="width:130px;">Ukupno</th>
</tr>
</thead>
<tbody>
{{range .ServisniDelovi}}
<tr>
<td>{{.ArtikalNaziv}}</td>
<td class="desno">{{.Kolicina}}</td>
<td class="desno">{{printf "%.2f" .CenaKomada}} din</td>
<td class="desno">{{printf "%.2f" .Ukupno}} din</td>
</tr>
{{end}}
<tr class="ukupno-red">
<td colspan="3">Ukupno delovi</td>
<td class="desno">{{printf "%.2f" .UkupnoDelovi}} din</td>
</tr>
</tbody>
</table>
</div>
{{end}}
<!-- cene -->
<div class="odeljak">
<div class="odeljak-naslov">Cene usluge</div>
<div class="cene-grid">
<div class="cena-blok">
<div class="cena-labela">Procena od</div>
<div class="cena-vrednost">{{if .Nalog.CenaOd}}{{.Nalog.CenaOdStr}} din{{else}}—{{end}}</div>
</div>
<div class="cena-blok">
<div class="cena-labela">Procena do</div>
<div class="cena-vrednost">{{if .Nalog.CenaDo}}{{.Nalog.CenaDoStr}} din{{else}}—{{end}}</div>
</div>
<div class="cena-blok">
<div class="cena-labela">Avans</div>
<div class="cena-vrednost">{{if .Nalog.Avans}}{{.Nalog.AvansStr}} din{{else}}—{{end}}</div>
</div>
<div class="cena-blok cena-konacna">
<div class="cena-labela">Konačna cena</div>
<div class="cena-vrednost">{{if .Nalog.CenaKonacna}}{{.Nalog.CenaKonacnaStr}} din{{else}}—{{end}}</div>
</div>
</div>
{{if .Nalog.PreostaloZaNaplatu}}
<div style="margin-top:10px;padding:10px 14px;background:#f0fdf4;border:1px solid #86efac;border-radius:6px;display:flex;justify-content:space-between;align-items:center;">
<span style="font-size:12px;color:#15803d;font-weight:500;">Preostalo za naplatu:</span>
<span style="font-size:18px;font-weight:700;color:#15803d;">{{.Nalog.PreostaloZaNaplatuStr}} din</span>
</div>
{{end}}
</div>
{{if .Nalog.Napomena}}
<div class="odeljak">
<div class="odeljak-naslov">Napomena</div>
<div class="polje-tekst" style="color:#555;">{{.Nalog.Napomena}}</div>
</div>
{{end}}
<!-- potpisi -->
<div class="potpisi">
<div>
<div class="potpis-linija">Predao klijent</div>
</div>
<div>
<div class="potpis-linija">Primio tehničar</div>
</div>
</div>
</div>
<button class="dugme-stampa" onclick="window.print()">Štampaj</button>
</body>
</html>