Servis: garancija u formi izmene, kartica Ukupno, povraćaj delova pri brisanju

- Forma izmene: polje Garancija do sa kvačicom Bez garancije, pored Avansa; veći razmak između kartica
- Detalji: kartica Ukupno (usluge + ugrađeni delovi) pre zone za brisanje
- Brisanje naloga vraća ugrađene delove na stanje u magacin (transakcija), čisti potraživane delove
This commit is contained in:
2026-06-22 17:20:45 +02:00
parent 5973cd02d7
commit 20b161a89d
5 changed files with 127 additions and 17 deletions
+1 -1
View File
@@ -192,7 +192,7 @@ type ServisRepository interface {
Izmeni(ctx context.Context, n *model.ServisniNalog) error
AzurirajStatus(ctx context.Context, id int64, status string) error
AzurirajGaranciju(ctx context.Context, id int64, garancijaDo *time.Time) error
Obrisi(ctx context.Context, id int64) error
Obrisi(ctx context.Context, id int64, korisnikID *int64) error
SledeciBroj(ctx context.Context) (string, error)
}
+70 -3
View File
@@ -230,10 +230,77 @@ func (r *ServisRepo) AzurirajGaranciju(ctx context.Context, id int64, garancijaD
}
// Obrisi briše servisni nalog po ID-u
func (r *ServisRepo) Obrisi(ctx context.Context, id int64) error {
_, err := r.db.ExecContext(ctx, "DELETE FROM servisni_nalozi WHERE id = ?", id)
// Obrisi briše servisni nalog i vraća ugrađene delove na stanje u magacinu (u transakciji)
func (r *ServisRepo) Obrisi(ctx context.Context, id int64, korisnikID *int64) error {
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("ntech: ServisRepo.Obrisi: %w", err)
return fmt.Errorf("ntech: ServisRepo.Obrisi: begin tx: %w", err)
}
defer tx.Rollback()
// pokupi ugrađene delove pre brisanja (CASCADE bi ih obrisao bez povraćaja)
redovi, err := tx.QueryContext(ctx,
"SELECT artikal_id, kolicina FROM servisni_delovi WHERE nalog_id = ?", id)
if err != nil {
return fmt.Errorf("ntech: ServisRepo.Obrisi: dohvati delove: %w", err)
}
type povrat struct {
artikalID int64
kolicina int
}
var delovi []povrat
for redovi.Next() {
var p povrat
if err := redovi.Scan(&p.artikalID, &p.kolicina); err != nil {
redovi.Close()
return fmt.Errorf("ntech: ServisRepo.Obrisi: scan dela: %w", err)
}
delovi = append(delovi, p)
}
redovi.Close()
for _, p := range delovi {
// usluge i troškovi nemaju stanje na lageru — vraćamo samo proizvodima
var stanjePre int
var tip string
err := tx.QueryRowContext(ctx,
"SELECT kolicina, tip FROM artikli WHERE id = ?", p.artikalID,
).Scan(&stanjePre, &tip)
if err != nil {
return fmt.Errorf("ntech: ServisRepo.Obrisi: dohvati stanje: %w", err)
}
if !(tip == model.TipProizvod || tip == "") {
continue
}
stanjePosle := stanjePre + p.kolicina
_, err = tx.ExecContext(ctx,
"UPDATE artikli SET kolicina = ? WHERE id = ?", stanjePosle, p.artikalID,
)
if err != nil {
return fmt.Errorf("ntech: ServisRepo.Obrisi: vrati stanje: %w", err)
}
err = zabeleziMagacinPromenu(ctx, tx, p.artikalID, model.PromenaPovracaj,
p.kolicina, stanjePre, stanjePosle, id, korisnikID, "brisanje servisnog naloga")
if err != nil {
return fmt.Errorf("ntech: ServisRepo.Obrisi: magacin: %w", err)
}
}
// potraživani delovi nemaju ON DELETE CASCADE — ručno ih čistimo da brisanje ne padne na FK
if _, err := tx.ExecContext(ctx,
"DELETE FROM servisni_potrazivani_delovi WHERE nalog_id = ?", id); err != nil {
return fmt.Errorf("ntech: ServisRepo.Obrisi: potraživani: %w", err)
}
// CASCADE briše servisni_delovi i servisni_radovi
if _, err := tx.ExecContext(ctx, "DELETE FROM servisni_nalozi WHERE id = ?", id); err != nil {
return fmt.Errorf("ntech: ServisRepo.Obrisi: delete: %w", err)
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("ntech: ServisRepo.Obrisi: commit: %w", err)
}
return nil
+8 -6
View File
@@ -327,16 +327,16 @@ func (h *Handler) SacuvajIzmenaNaloga(w http.ResponseWriter, r *http.Request) {
nalog.ID = id
// forma izmene više ne šalje status, datume, cene ni klijenta — očuvaj postojeće
// vrednosti (status se menja u detaljima, cena rada se računa iz radova, garancija
// i datum završetka se postavljaju kasnije). Inače bi prazna polja pregazila bazu.
// forma izmene više ne šalje status, datume (osim garancije), cene ni klijenta —
// očuvaj postojeće vrednosti (status se menja u detaljima, cena rada se računa iz
// radova, datum završetka se postavlja kasnije). Garancija se sada uređuje u formi,
// pa je čuvamo iz parseFormuNaloga. Inače bi prazna polja pregazila bazu.
if stari, e := h.ServisRepo.DohvatiID(r.Context(), id); e == nil && stari != nil {
nalog.Status = stari.Status
nalog.KlijentID = stari.KlijentID
nalog.CenaOd = stari.CenaOd
nalog.CenaDo = stari.CenaDo
nalog.CenaKonacna = stari.CenaKonacna
nalog.GarancijaDo = stari.GarancijaDo
nalog.DatumZavrsetka = stari.DatumZavrsetka
}
@@ -385,7 +385,8 @@ func (h *Handler) SacuvajIzmenaNaloga(w http.ResponseWriter, r *http.Request) {
// ObrisiNalog prima POST zahtev i briše servisni nalog po ID-u
func (h *Handler) ObrisiNalog(w http.ResponseWriter, r *http.Request) {
if _, ok := h.zahtevajDozvolu(w, r, "servis.obrisi"); !ok {
k, ok := h.zahtevajDozvolu(w, r, "servis.obrisi")
if !ok {
return
}
id, err := parseID(chi.URLParam(r, "id"))
@@ -394,7 +395,8 @@ func (h *Handler) ObrisiNalog(w http.ResponseWriter, r *http.Request) {
return
}
if err := h.ServisRepo.Obrisi(r.Context(), id); err != nil {
if err := h.ServisRepo.Obrisi(r.Context(), id, &k.ID); err != nil {
slog.Error("greška pri brisanju naloga", "id", id, "error", err)
http.Error(w, "Greška pri brisanju naloga", http.StatusInternalServerError)
return
}
@@ -333,6 +333,27 @@
</div>
</div>
<!-- ukupno: zbir usluga (radova) i ugrađenih delova -->
<div class="kartica detalji-kartica animiraj">
<div style="margin-bottom:16px;padding-bottom:14px;border-bottom:0.5px solid var(--ivica);">
<span style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">Ukupno</span>
</div>
<div class="kolona" style="gap:10px;">
<div style="display:flex;justify-content:space-between;align-items:center;">
<span style="font-size:14px;color:var(--tekst-sporedni);">Usluge (rad)</span>
<span style="font-size:14px;color:var(--tekst-glavni);">{{dinari .UkupnoRadovi}} din</span>
</div>
<div style="display:flex;justify-content:space-between;align-items:center;">
<span style="font-size:14px;color:var(--tekst-sporedni);">Ugrađeni delovi</span>
<span style="font-size:14px;color:var(--tekst-glavni);">{{dinari .UkupnoDelovi}} din</span>
</div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:6px;padding-top:12px;border-top:0.5px solid var(--ivica);">
<span style="font-size:16px;font-weight:600;color:var(--tekst-glavni);">Ukupno</span>
<span style="font-size:18px;font-weight:600;color:var(--tekst-glavni);">{{dinari .UkupnoSve}} din</span>
</div>
</div>
</div>
<!-- zona za brisanje -->
<div class="kartica detalji-kartica animiraj" style="border-color:#dc262633;">
<div style="display:flex;align-items:flex-start;gap:12px;flex-wrap:wrap;">
+27 -7
View File
@@ -29,7 +29,7 @@
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
<!-- ===================== KARTICA: UREĐAJ ===================== -->
<div class="kartica forma-kartica animiraj">
<div class="kartica forma-kartica animiraj" style="margin-bottom:20px;">
<div style="display:flex;justify-content:space-between;align-items:center;margin:-16px -16px 20px -16px;padding:12px 16px;background:var(--pozadina);border-radius:12px 12px 0 0;flex-wrap:wrap;gap:10px;">
<span style="font-size:15px;font-weight:600;color:var(--tekst-glavni);">
{{if .Izmena}}Izmeni nalog{{else}}Novi nalog{{end}}
@@ -91,7 +91,7 @@
<!-- ===================== KARTICA: TEHNIČAR (izmena) / KLIJENT I TEHNIČAR (novi) ===================== -->
{{if .Izmena}}
<!-- pri izmeni klijent se ne menja (vezan pri prijemu) — ostaje samo tehničar -->
<div class="kartica forma-kartica animiraj">
<div class="kartica forma-kartica animiraj" style="margin-bottom:20px;">
<div class="sekcija-naslov">Tehničar</div>
<div>
<label class="polje-labela">Tehničar</label>
@@ -104,7 +104,7 @@
</div>
</div>
{{else}}
<div class="kartica forma-kartica animiraj" x-data="klijentNalog">
<div class="kartica forma-kartica animiraj" x-data="klijentNalog" style="margin-bottom:20px;">
<div class="sekcija-naslov">Klijent</div>
<div class="kolona" style="gap:12px;">
@@ -259,18 +259,38 @@
</div>
{{end}}
<!-- ===================== KARTICA: AVANS I NAPOMENA ===================== -->
<div class="kartica forma-kartica animiraj">
<div class="sekcija-naslov">Avans i napomena</div>
<!-- ===================== KARTICA: AVANS, GARANCIJA I NAPOMENA ===================== -->
<div class="kartica forma-kartica animiraj" style="margin-bottom:20px;">
<div class="sekcija-naslov">{{if .Izmena}}Avans, garancija i napomena{{else}}Avans i napomena{{end}}</div>
<!-- cena rada se računa iz radova (usluga) u detaljima naloga; ovde samo avans -->
<div class="forma-grid-2" style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
<div class="forma-grid-2" style="display:grid;grid-template-columns:1fr 1fr;gap:12px;align-items:start;">
<div>
<label class="polje-labela">{{if .Izmena}}Avans{{else}}Avans / kapara{{end}}</label>
<input type="number" name="avans" min="0" step="0.01"
value="{{if .Nalog.Avans}}{{.Nalog.Avans}}{{end}}"
placeholder="0" style="width:100%;">
</div>
{{if .Izmena}}
<!-- garancija — uređuje se ovde; „Bez garancije" isključuje datum -->
<div x-data="{ bez: {{if .Nalog.GarancijaDo}}false{{else}}true{{end}} }">
<label class="polje-labela">Garancija do</label>
<div style="display:flex;align-items:center;gap:10px;">
<input type="date" name="garancija_do"
value="{{if .Nalog.GarancijaDo}}{{.Nalog.GarancijaDo.Format "2006-01-02"}}{{end}}"
:disabled="bez" :style="bez ? 'opacity:.5;' : ''"
style="flex:1;min-width:0;">
<label class="tip-opcija" :class="bez ? 'tip-opcija-akt' : ''"
style="white-space:nowrap;">
<input type="checkbox" x-model="bez" style="margin:0;width:auto;">
Bez garancije
</label>
</div>
<!-- skriveni flag: server preskače datum kada je 1 -->
<input type="hidden" name="bez_garancije" :value="bez ? '1' : '0'">
</div>
{{end}}
</div>
<div style="margin-top:16px;">
<label class="polje-labela">Napomena</label>
<textarea name="napomena" rows="2"