Servis: garantni list, radovi na otpremnici, selektivno prihvatanje predloga po stavkama

This commit is contained in:
2026-06-25 01:39:56 +02:00
parent d960a5b67b
commit 16cdee0ee1
9 changed files with 404 additions and 18 deletions
+2
View File
@@ -241,6 +241,7 @@ func main() {
r.Get("/status/{token}", h.ServisJavniStatus)
r.Post("/status/{token}/prihvati", h.ServisJavniPrihvati)
r.Post("/status/{token}/odbij", h.ServisJavniOdbij)
r.Post("/status/{token}/odluka-odabrano", h.ServisJavniOdlukaOdabrano)
// zaštićene rute — zahtevaju prijavljenog korisnika
r.Group(func(r chi.Router) {
@@ -375,6 +376,7 @@ func main() {
r.With(doz("servis.izmeni")).Post("/servis/{id}/uradjeno", h.SacuvajUradjeno)
r.With(doz("servis.izmeni")).Post("/servis/{id}/cena-dijagnostike", h.AzurirajCenaDijagnostike)
r.With(doz("servis.izmeni")).Post("/servis/{id}/odbij-popravku", h.OdbijPopravku)
r.With(doz("servis.izmeni")).Post("/servis/{id}/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}/delovi", h.DodajDeloNalogu)
r.With(doz("servis.izmeni")).Post("/servis/{id}/delovi/{deo_id}/obrisi", h.ObrisiDeloNaloga)
+6
View File
@@ -232,6 +232,9 @@ type ServisniDeloviRepository interface {
PrihvatiPredlozene(ctx context.Context, nalogID int64) error
// ObrisiPredlozene briše sve predložene delove sa naloga (klijent odbio predlog)
ObrisiPredlozene(ctx context.Context, nalogID int64) error
// 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
@@ -255,6 +258,9 @@ type ServisniRadoviRepository interface {
PrihvatiPredlozene(ctx context.Context, nalogID int64) error
// ObrisiPredlozene briše sve predložene radove sa naloga (klijent odbio predlog)
ObrisiPredlozene(ctx context.Context, nalogID int64) error
// 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
+68
View File
@@ -315,6 +315,74 @@ func (r *ServisniDeloviRepo) ObrisiPredlozene(ctx context.Context, nalogID int64
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)
func (r *ServisniDeloviRepo) DohvatiArtikalID(ctx context.Context, deoID int64) (int64, error) {
var artikalID int64
+48
View File
@@ -89,6 +89,54 @@ func (r *ServisniRadoviRepo) ObrisiPredlozene(ctx context.Context, nalogID int64
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
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 {
+111
View File
@@ -1347,6 +1347,7 @@ func (h *Handler) StampaRadnogNaloga(w http.ResponseWriter, r *http.Request) {
// PodaciOtpremnice su podaci za otpremnicu pri preuzimanju uređaja
type PodaciOtpremnice struct {
Nalog model.ServisniNalog
Radovi []model.ServisniRad
ServisniDelovi []model.ServisniDeoSaArtiklom
UkupnoDelovi float64
PreostaloSve float64
@@ -1379,6 +1380,12 @@ func (h *Handler) StampaOtpremnice(w http.ResponseWriter, r *http.Request) {
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)
@@ -1452,6 +1459,7 @@ func (h *Handler) StampaOtpremnice(w http.ResponseWriter, r *http.Request) {
h.renderujStandalone(w, "servis_otpremnica", PodaciOtpremnice{
Nalog: *nalog,
Radovi: radovi,
ServisniDelovi: delovi,
UkupnoDelovi: ukupnoDelovi,
PreostaloSve: preostaloSve,
@@ -2359,6 +2367,109 @@ func (h *Handler) ServisJavniPrihvati(w http.ResponseWriter, r *http.Request) {
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
func (h *Handler) ObrisiKomentarKlijenta(w http.ResponseWriter, r *http.Request) {
_, ok := h.zahtevajDozvolu(w, r, "servis.izmeni")
+37 -1
View File
@@ -718,7 +718,43 @@
{{end}}
</div>{{/* kraj stavke-blok */}}
<!-- prihvati/odbij odabrane predlozene stavke (interna forma) -->
{{if and (index .Dozvole "servis.izmeni") (or .PredlozeniRadovi .PredlozeniDelovi)}}
<div 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">
<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}} &times; {{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}} &times; {{.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>
{{end}}
<!-- ukupno: zbir usluga (radova) i ugrađenih delova -->
<div id="nalog-ukupno" class="kartica detalji-kartica animiraj">
@@ -164,6 +164,33 @@
<div class="polje-tekst">{{if .Nalog.OpisKvara}}{{.Nalog.OpisKvara}}{{else}}<span class="prazno"></span>{{end}}</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 -->
{{if .ServisniDelovi}}
<div class="odeljak">
+79 -1
View File
@@ -54,14 +54,18 @@
/* napomena na dnu */
.dno-info { margin-top: 20px; font-size: 11px; color: #888; text-align: center; }
@page { size: A4; margin: 12mm 14mm 18mm; }
@media print {
body { font-size: 12px; }
.strana { padding: 16px 20px; max-width: 100%; }
.dugme-stampa { display: none !important; }
@page { size: A4; margin: 12mm 14mm; }
/* uslovi servisa (sa potpisima) idu na novu stranicu */
.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:hover { background: #333; }
</style>
@@ -202,5 +206,79 @@
<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>
</html>
+26 -16
View File
@@ -261,18 +261,33 @@
Serviser predlaže sledeće radove i delove. Pogledajte i javite nam da li ste saglasni.
</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}}
<div class="red">
<span class="red-labela">{{.Naziv}}{{if gt .Kolicina 1.0}} &times;{{.Kolicina}}{{end}}</span>
<span class="red-vrednost">{{dinari .Ukupno}} din</span>
</div>
<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 style="display:flex;align-items:center;gap:10px;flex:1;">
<input type="checkbox" name="rad_id" value="{{.ID}}" checked
style="width:18px;height:18px;accent-color:#f59e0b;flex-shrink:0;">
<span style="font-size:13px;color:#e2e8f0;">{{.Naziv}}{{if gt .Kolicina 1.0}} &times; {{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}}
{{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}}
<div class="red">
<span class="red-labela">{{.ArtikalNaziv}}{{if gt .Kolicina 1}} &times;{{.Kolicina}}{{end}}</span>
<span class="red-vrednost">{{dinari .Ukupno}} din</span>
</div>
<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 style="display:flex;align-items:center;gap:10px;flex:1;">
<input type="checkbox" name="artikal_id" value="{{.ArtikalID}}" checked
style="width:18px;height:18px;accent-color:#f59e0b;flex-shrink:0;">
<span style="font-size:13px;color:#e2e8f0;">{{.ArtikalNaziv}}{{if gt .Kolicina 1}} &times; {{.Kolicina}}{{end}}</span>
</span>
<span style="font-size:13px;font-weight:600;color:#fbbf24;white-space:nowrap;">{{dinari .Ukupno}} din</span>
</label>
{{end}}
{{end}}
<div class="cena-blok predlog-ukupno" style="margin-top:12px;display:flex;justify-content:space-between;align-items:center;">
@@ -280,14 +295,9 @@
<span class="cena-vrednost">{{dinari .UkupnoPredlog}} din</span>
</div>
<!-- komentar i dugmad -->
<form method="POST" style="margin-top:16px;">
<textarea name="komentar" placeholder="Poruka serviseru (opciono)..." rows="2"
style="width:100%;padding:10px 12px;border:0.5px solid #334155;border-radius:8px;background:#0f172a;color:#e2e8f0;font-size:13px;font-family:inherit;resize:vertical;margin-bottom:10px;box-sizing:border-box;"></textarea>
<div style="display:flex;gap:10px;">
<button type="submit" formaction="/status/{{.Nalog.JavniToken}}/prihvati" class="dugme-prihvati" style="flex:1;">Prihvatam predlog</button>
<button type="submit" formaction="/status/{{.Nalog.JavniToken}}/odbij" class="dugme-odbij" style="flex:1;">Ne prihvatam</button>
</div>
<textarea name="komentar" placeholder="Poruka serviseru (opciono)..." rows="2"
style="width:100%;padding:10px 12px;border:0.5px solid #334155;border-radius:8px;background:#0f172a;color:#e2e8f0;font-size:13px;font-family:inherit;resize:vertical;margin:14px 0 10px;box-sizing:border-box;"></textarea>
<button type="submit" class="dugme-prihvati" style="width:100%;">Potvrdi odabrano</button>
</form>
</div>
{{end}}