Servis: garancija kao trajanje u danima + Tehničar → Serviser
- Garancija se u detaljima prikazuje kao trajanje na srpskom (npr. „1 mesec i 15 dana", „2 meseca", „20 dana"); helper garancijaTekst + srpska deklinacija - Nova kolona garancija_dana (trajanje od završetka); garancija_do ostaje za fazu Završeno - Modal: textbox „Trajanje garancije (dana)" umesto kalendara; Podrazumevano i Bez garancije - Nova ruta/handler AzurirajGarancijaDana; forma izmene čuva trajanje (ne briše) - Podešavanja → Servis: rok garancije prebačen iz meseci u dane (servis_garancija_dana, default 60) - „Garancija do" uklonjena iz forme Izmeni nalog (kod arhiviran u docs/garancija-buduca-logika.md); SacuvajIzmenaNaloga čuva garanciju iz baze da je auto-snimanje ne pregazi - Prikazni tekst „Tehničar" → „Serviser" svuda (identifikatori Tehnicar/tehnicar_id netaknuti)
This commit is contained in:
@@ -360,6 +360,7 @@ func main() {
|
||||
r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "servis.pregled")).Get("/servis/{id}/predracun", h.StampaPredracuna)
|
||||
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-dana", h.AzurirajGarancijaDana)
|
||||
r.With(doz("servis.izmeni")).Post("/servis/{id}/predvidjen-datum", h.AzurirajPredvidjenDatum)
|
||||
r.With(doz("servis.izmeni")).Post("/servis/{id}/tehnicar", h.AzurirajTehnicar)
|
||||
r.With(doz("servis.izmeni")).Post("/servis/{id}/napomena-klijentu", h.SacuvajNapomenuKlijentu)
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# Garancija — buduća logika (arhiva koda)
|
||||
|
||||
## Plan (zašto)
|
||||
|
||||
Trenutno se „Garancija do" unosi kao konkretan datum. To nema smisla dok je uređaj
|
||||
tek primljen (status **Primljeno**) — popravka još nije ni počela, pa ni datum
|
||||
garancije nije poznat.
|
||||
|
||||
**Buduća logika:**
|
||||
- Garancija se izražava kao **broj dana od datuma završetka** (podesivo, kao i sada
|
||||
preko `servis_garancija_meseci` / mogući novi `servis_garancija_dana`).
|
||||
- Dok je nalog u radu (Primljeno, U popravci…), klijentu se prikazuje samo
|
||||
**podrazumevana garancija** kao informacija (npr. „garancija: 12 meseci od završetka").
|
||||
- **Pravi datum garancije** (`garancija_do`) upisuje se tek kad nalog pređe u
|
||||
**Završeno** → `datum_zavrsetka + garantni period`.
|
||||
|
||||
Zbog toga je „Garancija do" uklonjena iz forme **Izmeni nalog** (vidi commit), a
|
||||
ovde je arhiviran kod da se kasnije ponovo upotrebi / prilagodi.
|
||||
|
||||
## Arhivirani kod — „Garancija do" blok iz `servis_forma.html` (Izmeni nalog)
|
||||
|
||||
```html
|
||||
{{if .Izmena}}
|
||||
<!-- garancija — isti izgled kao popup „Garancija do" u detaljima naloga;
|
||||
„Bez garancije" isključuje datum, „Podrazumevano" vraća prijem + meseci -->
|
||||
<div>
|
||||
<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"}}{{else}}{{.GarancijaDefault}}{{end}}"
|
||||
min="{{.Nalog.DatumPrijema.Format "2006-01-02"}}"
|
||||
{{if .BezGarancije}}disabled{{end}}
|
||||
style="flex:1;min-width:0;{{if .BezGarancije}}opacity:.5;{{end}}">
|
||||
<label class="tip-opcija{{if .BezGarancije}} tip-opcija-akt{{end}}" style="white-space:nowrap;">
|
||||
<input type="checkbox" name="bez_garancije" value="1"{{if .BezGarancije}} checked{{end}} style="margin:0;width:auto;"
|
||||
onchange="var d=this.closest('div').querySelector('[name=garancija_do]'); d.disabled=this.checked; d.style.opacity=this.checked?'.5':''; this.closest('label').classList.toggle('tip-opcija-akt',this.checked);">
|
||||
Bez garancije
|
||||
</label>
|
||||
</div>
|
||||
<button type="button" class="btn-sekundarno" style="margin-top:8px;"
|
||||
onclick="var f=this.closest('form'); var d=f.querySelector('[name=garancija_do]'); d.disabled=false; d.style.opacity=''; d.value='{{.GarancijaDefault}}'; var c=f.querySelector('[name=bez_garancije]'); c.checked=false; c.closest('label').classList.remove('tip-opcija-akt'); d.dispatchEvent(new Event('change',{bubbles:true}));">Podrazumevano</button>
|
||||
</div>
|
||||
{{end}}
|
||||
```
|
||||
|
||||
## Backend koji OSTAJE (i dalje koristi modal „Garancija do" u detaljima)
|
||||
|
||||
Nije uklonjeno — ovo su delovi koje treba znati pri ponovnoj upotrebi:
|
||||
|
||||
- `internal/handler/servis.go`:
|
||||
- `defaultGarancija(datumPrijema, podesavanja)` — prijem + `servis_garancija_meseci`.
|
||||
Za buduću logiku zameniti osnovicu `datum_zavrsetka` umesto `datum_prijema`.
|
||||
- `garancijaPrePrijema(garancija, prijem)` — validacija da garancija nije pre prijema.
|
||||
- `AzurirajGaranciju` (ruta `POST /servis/{id}/garancija`) — snima `garancija_do`,
|
||||
prazno / „bez garancije" → NULL.
|
||||
- U `parseFormuNaloga`: blok koji čita `garancija_do` + `bez_garancije`.
|
||||
- `BezGarancije` flag (čita NULL stanje iz baze) i `GarancijaDefault` (prijem + meseci)
|
||||
u `PodaciDetaljiNaloga` / `PodaciFormeNaloga`.
|
||||
- `internal/db/sqlite/servis.go`: `AzurirajGaranciju` repo metoda; kolona `garancija_do`.
|
||||
- Podešavanje: `servis_garancija_meseci` (Podešavanja → Servis).
|
||||
|
||||
## TODO kad se vraća
|
||||
|
||||
1. Dodati prikaz „podrazumevana garancija" (informativno) na nalogu u statusu Primljeno.
|
||||
2. Pri prelasku u **Završeno** automatski izračunati `garancija_do = datum_zavrsetka + period`.
|
||||
3. Razmotriti period u danima umesto/pored meseci.
|
||||
@@ -192,6 +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
|
||||
AzurirajGarancijaDana(ctx context.Context, id int64, dana *int) error
|
||||
AzurirajPredvidjenDatum(ctx context.Context, id int64, predvidjenDatum *time.Time) error
|
||||
AzurirajTehnicar(ctx context.Context, id int64, tehnicarID *int64) error
|
||||
AzurirajNapomenuKlijentu(ctx context.Context, id int64, tekst string) error
|
||||
|
||||
@@ -57,7 +57,7 @@ func (r *ServisRepo) Lista(ctx context.Context, pretraga, status string) ([]mode
|
||||
SELECT
|
||||
sn.id, sn.klijent_id, sn.tehnicar_id, sn.broj_naloga, sn.uredjaj, sn.serijski_broj,
|
||||
sn.opis_kvara, sn.trazene_nadogradnje, sn.status, sn.cena_od, sn.cena_do, sn.cena_konacna,
|
||||
sn.avans, sn.napomena, sn.garancija_do, sn.datum_prijema, sn.datum_zavrsetka, sn.predvidjen_datum,
|
||||
sn.avans, sn.napomena, sn.garancija_do, sn.garancija_dana, sn.datum_prijema, sn.datum_zavrsetka, sn.predvidjen_datum,
|
||||
sn.ostecenja, sn.pin_uredjaja, sn.pribor, sn.napomena_klijentu, sn.javni_token,
|
||||
COALESCE(kp.naziv, '') AS klijent_naziv
|
||||
FROM servisni_nalozi sn
|
||||
@@ -104,7 +104,7 @@ func (r *ServisRepo) DohvatiID(ctx context.Context, id int64) (*model.ServisniNa
|
||||
SELECT
|
||||
id, klijent_id, tehnicar_id, broj_naloga, uredjaj, serijski_broj,
|
||||
opis_kvara, trazene_nadogradnje, status, cena_od, cena_do, cena_konacna,
|
||||
avans, napomena, garancija_do, datum_prijema, datum_zavrsetka, predvidjen_datum,
|
||||
avans, napomena, garancija_do, garancija_dana, datum_prijema, datum_zavrsetka, predvidjen_datum,
|
||||
ostecenja, pin_uredjaja, pribor, napomena_klijentu, javni_token
|
||||
FROM servisni_nalozi WHERE id = ?`, id)
|
||||
|
||||
@@ -127,14 +127,14 @@ func (r *ServisRepo) Kreiraj(ctx context.Context, n *model.ServisniNalog) (int64
|
||||
rezultat, err := r.db.ExecContext(ctx, `
|
||||
INSERT INTO servisni_nalozi
|
||||
(klijent_id, tehnicar_id, broj_naloga, uredjaj, serijski_broj, opis_kvara, trazene_nadogradnje,
|
||||
status, cena_od, cena_do, cena_konacna, avans, napomena, garancija_do, datum_zavrsetka, predvidjen_datum,
|
||||
status, cena_od, cena_do, cena_konacna, avans, napomena, garancija_do, garancija_dana, datum_zavrsetka, predvidjen_datum,
|
||||
ostecenja, pin_uredjaja, pribor, datum_prijema, javni_token)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
nullInt64(n.KlijentID), nullInt64(n.TehnicarID), n.BrojNaloga, n.Uredjaj,
|
||||
nullString(n.SerijskiBroj), n.OpisKvara, n.TrazeneNadogradnje, n.Status,
|
||||
nullFloat64(n.CenaOd), nullFloat64(n.CenaDo), nullFloat64(n.CenaKonacna),
|
||||
nullFloat64(n.Avans), nullString(n.Napomena),
|
||||
nullTime(n.GarancijaDo), nullTime(n.DatumZavrsetka), nullTime(n.PredvidjenDatum),
|
||||
nullTime(n.GarancijaDo), nullInt(n.GarancijaDana), nullTime(n.DatumZavrsetka), nullTime(n.PredvidjenDatum),
|
||||
nullString(n.Ostecenja), nullString(n.PinUredjaja), nullString(n.Pribor),
|
||||
n.DatumPrijema, token,
|
||||
)
|
||||
@@ -156,7 +156,7 @@ func (r *ServisRepo) DohvatiJavniToken(ctx context.Context, token string) (*mode
|
||||
SELECT
|
||||
id, klijent_id, tehnicar_id, broj_naloga, uredjaj, serijski_broj,
|
||||
opis_kvara, trazene_nadogradnje, status, cena_od, cena_do, cena_konacna,
|
||||
avans, napomena, garancija_do, datum_prijema, datum_zavrsetka, predvidjen_datum,
|
||||
avans, napomena, garancija_do, garancija_dana, datum_prijema, datum_zavrsetka, predvidjen_datum,
|
||||
ostecenja, pin_uredjaja, pribor, napomena_klijentu, javni_token
|
||||
FROM servisni_nalozi WHERE javni_token = ?`, token)
|
||||
|
||||
@@ -173,12 +173,12 @@ func (r *ServisRepo) Izmeni(ctx context.Context, n *model.ServisniNalog) error {
|
||||
UPDATE servisni_nalozi SET
|
||||
klijent_id = ?, tehnicar_id = ?, uredjaj = ?, serijski_broj = ?, opis_kvara = ?, trazene_nadogradnje = ?,
|
||||
status = ?, cena_od = ?, cena_do = ?, cena_konacna = ?,
|
||||
avans = ?, napomena = ?, garancija_do = ?, datum_zavrsetka = ?, predvidjen_datum = ?,
|
||||
avans = ?, napomena = ?, garancija_do = ?, garancija_dana = ?, datum_zavrsetka = ?, predvidjen_datum = ?,
|
||||
ostecenja = ?, pin_uredjaja = ?, pribor = ?
|
||||
WHERE id = ?`,
|
||||
nullInt64(n.KlijentID), nullInt64(n.TehnicarID), n.Uredjaj, nullString(n.SerijskiBroj), n.OpisKvara, n.TrazeneNadogradnje,
|
||||
n.Status, nullFloat64(n.CenaOd), nullFloat64(n.CenaDo), nullFloat64(n.CenaKonacna),
|
||||
nullFloat64(n.Avans), nullString(n.Napomena), nullTime(n.GarancijaDo), nullTime(n.DatumZavrsetka), nullTime(n.PredvidjenDatum),
|
||||
nullFloat64(n.Avans), nullString(n.Napomena), nullTime(n.GarancijaDo), nullInt(n.GarancijaDana), nullTime(n.DatumZavrsetka), nullTime(n.PredvidjenDatum),
|
||||
nullString(n.Ostecenja), nullString(n.PinUredjaja), nullString(n.Pribor),
|
||||
n.ID,
|
||||
)
|
||||
@@ -229,6 +229,19 @@ func (r *ServisRepo) AzurirajGaranciju(ctx context.Context, id int64, garancijaD
|
||||
return nil
|
||||
}
|
||||
|
||||
// AzurirajGarancijaDana postavlja trajanje garancije u danima (od završetka radova).
|
||||
// dana == nil ili 0 → bez garancije.
|
||||
func (r *ServisRepo) AzurirajGarancijaDana(ctx context.Context, id int64, dana *int) error {
|
||||
_, err := r.db.ExecContext(ctx,
|
||||
"UPDATE servisni_nalozi SET garancija_dana = ? WHERE id = ?",
|
||||
nullInt(dana), id,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntech: ServisRepo.AzurirajGarancijaDana: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AzurirajPredvidjenDatum postavlja ili briše ručni override predviđenog datuma popravke.
|
||||
// predvidjenDatum == nil → vraća se na izvedeni default (prijem + rok iz podešavanja).
|
||||
func (r *ServisRepo) AzurirajPredvidjenDatum(ctx context.Context, id int64, predvidjenDatum *time.Time) error {
|
||||
@@ -251,7 +264,7 @@ func (r *ServisRepo) AzurirajPredvidjenDatum(ctx context.Context, id int64, pred
|
||||
return nil
|
||||
}
|
||||
|
||||
// AzurirajTehnicar postavlja ili uklanja dodeljenog tehničara na nalogu.
|
||||
// AzurirajTehnicar postavlja ili uklanja dodeljenog servisera na nalogu.
|
||||
// tehnicarID == nil → nedodeljen.
|
||||
func (r *ServisRepo) AzurirajTehnicar(ctx context.Context, id int64, tehnicarID *int64) error {
|
||||
_, err := r.db.ExecContext(ctx,
|
||||
@@ -359,11 +372,12 @@ func scanNalog(scan func(...any) error, n *model.ServisniNalog, klijentNaziv *st
|
||||
var serijskiBroj, napomena, ostecenja, pinUredjaja, pribor, napomenaKlijentu, javniToken sql.NullString
|
||||
var cenaOd, cenaDo, cenaKonacna, avans sql.NullFloat64
|
||||
var garancijaDo, datumZavrsetka, predvidjenDatum sql.NullTime
|
||||
var garancijaDana sql.NullInt64
|
||||
|
||||
args := []any{
|
||||
&n.ID, &klijentID, &tehnicarID, &n.BrojNaloga, &n.Uredjaj, &serijskiBroj,
|
||||
&n.OpisKvara, &n.TrazeneNadogradnje, &n.Status, &cenaOd, &cenaDo, &cenaKonacna,
|
||||
&avans, &napomena, &garancijaDo, &n.DatumPrijema, &datumZavrsetka, &predvidjenDatum,
|
||||
&avans, &napomena, &garancijaDo, &garancijaDana, &n.DatumPrijema, &datumZavrsetka, &predvidjenDatum,
|
||||
&ostecenja, &pinUredjaja, &pribor, &napomenaKlijentu, &javniToken,
|
||||
}
|
||||
|
||||
@@ -417,6 +431,10 @@ func scanNalog(scan func(...any) error, n *model.ServisniNalog, klijentNaziv *st
|
||||
v := predvidjenDatum.Time
|
||||
n.PredvidjenDatum = &v
|
||||
}
|
||||
if garancijaDana.Valid {
|
||||
v := int(garancijaDana.Int64)
|
||||
n.GarancijaDana = &v
|
||||
}
|
||||
n.JavniToken = javniToken.String
|
||||
|
||||
return nil
|
||||
|
||||
@@ -20,6 +20,15 @@ func nullInt64(v *int64) sql.NullInt64 {
|
||||
return sql.NullInt64{Int64: *v, Valid: true}
|
||||
}
|
||||
|
||||
// nullInt pretvara *int pokazivač u sql.NullInt64 —
|
||||
// koristi se za opciona celobrojna polja (npr. trajanje garancije u danima)
|
||||
func nullInt(v *int) sql.NullInt64 {
|
||||
if v == nil {
|
||||
return sql.NullInt64{}
|
||||
}
|
||||
return sql.NullInt64{Int64: int64(*v), Valid: true}
|
||||
}
|
||||
|
||||
// nullFloat64 pretvara *float64 pokazivač u sql.NullFloat64 —
|
||||
// koristi se za opciona numerička polja kao što su cene
|
||||
func nullFloat64(v *float64) sql.NullFloat64 {
|
||||
|
||||
@@ -68,6 +68,8 @@ var sablonskeFunkcije = template.FuncMap{
|
||||
},
|
||||
// telefon formatira srpski broj telefona radi lakšeg čitanja: "0641234567" → "064 123 4567"
|
||||
"telefon": formatirajTelefon,
|
||||
// garancijaTekst prikazuje trajanje garancije (dani) na srpskom: 45 → „1 mesec i 15 dana"
|
||||
"garancijaTekst": garancijaTekst,
|
||||
// json serijalizuje vrednost u JSON za ugradnju u <script> (npr. Alpine x-data);
|
||||
// template.JS sprečava da html/template dodatno escapuje rezultat
|
||||
"json": func(v any) (template.JS, error) {
|
||||
|
||||
@@ -49,7 +49,7 @@ type PodaciPodesavanja struct {
|
||||
BackupIntervalSati string
|
||||
BackupBrojKopija string
|
||||
KalkulacijaMarza string
|
||||
ServisGarancijaMeseci string
|
||||
ServisGarancijaDana string
|
||||
PredracunRokDana string
|
||||
PredvidjenRokDana string
|
||||
LoginPozadina string
|
||||
@@ -366,15 +366,15 @@ func (h *Handler) SacuvajPodesavanja(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// podrazumevani rok garancije za servis (meseci, 0–120)
|
||||
if v := strings.TrimSpace(r.FormValue("servis_garancija_meseci")); v != "" {
|
||||
// podrazumevana garancija za servis (dani, 0–3650)
|
||||
if v := strings.TrimSpace(r.FormValue("servis_garancija_dana")); v != "" {
|
||||
n, err := strconv.Atoi(v)
|
||||
if err != nil || n < 0 || n > 120 {
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Rok garancije mora biti broj između 0 i 120 meseci.")
|
||||
if err != nil || n < 0 || n > 3650 {
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Garancija mora biti broj između 0 i 3650 dana.")
|
||||
http.Redirect(w, r, sledeci, http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "servis_garancija_meseci", strconv.Itoa(n)); err != nil {
|
||||
if err := ntechsqlite.SacuvajPodesavanje(r.Context(), h.DB, "servis_garancija_dana", strconv.Itoa(n)); err != nil {
|
||||
http.Error(w, "Greška pri čuvanju podešavanja", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
@@ -779,7 +779,7 @@ func (h *Handler) napuniPodaciPodesavanja(r *http.Request, naslov string) (Podac
|
||||
BackupIntervalSati: vrednostIliDefault(podesavanja, "backup_interval_sati", "24"),
|
||||
BackupBrojKopija: vrednostIliDefault(podesavanja, "backup_broj_kopija", "7"),
|
||||
KalkulacijaMarza: vrednostIliDefault(podesavanja, "kalkulacija_marza", "20"),
|
||||
ServisGarancijaMeseci: vrednostIliDefault(podesavanja, "servis_garancija_meseci", "2"),
|
||||
ServisGarancijaDana: vrednostIliDefault(podesavanja, "servis_garancija_dana", "60"),
|
||||
PredracunRokDana: vrednostIliDefault(podesavanja, "predracun_rok_dana", "7"),
|
||||
PredvidjenRokDana: vrednostIliDefault(podesavanja, "predvidjen_rok_dana", "15"),
|
||||
}, nil
|
||||
|
||||
+105
-15
@@ -3,6 +3,7 @@ package handler
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@@ -148,7 +149,7 @@ func (h *Handler) NoviNalog(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
tehnicari, err := h.KorisniciRepo.Lista(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri učitavanju tehničara", http.StatusInternalServerError)
|
||||
http.Error(w, "Greška pri učitavanju servisera", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -293,7 +294,7 @@ func (h *Handler) IzmeniNalog(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
tehnicari, err := h.KorisniciRepo.Lista(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri učitavanju tehničara", http.StatusInternalServerError)
|
||||
http.Error(w, "Greška pri učitavanju servisera", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -381,6 +382,8 @@ func (h *Handler) SacuvajIzmenaNaloga(w http.ResponseWriter, r *http.Request) {
|
||||
nalog.DatumZavrsetka = stari.DatumZavrsetka
|
||||
nalog.PredvidjenDatum = stari.PredvidjenDatum
|
||||
nalog.DatumPrijema = stari.DatumPrijema
|
||||
nalog.GarancijaDo = stari.GarancijaDo // garancija se uređuje u detaljima, ne u formi
|
||||
nalog.GarancijaDana = stari.GarancijaDana // isto — trajanje garancije se ne dira u formi
|
||||
}
|
||||
|
||||
// garancija ne sme da bude pre datuma prijema
|
||||
@@ -537,12 +540,9 @@ func (h *Handler) DetaljiNaloga(w http.ResponseWriter, r *http.Request) {
|
||||
nalog.PredvidjenDatum = defaultPredvidjenDatum(nalog.DatumPrijema, podesavanja)
|
||||
}
|
||||
|
||||
// podrazumevana garancija (za dugme „Podrazumevano" u popup-u) i stanje „bez garancije" iz baze
|
||||
garancijaDefault := ""
|
||||
if d := defaultGarancija(nalog.DatumPrijema, podesavanja); d != nil {
|
||||
garancijaDefault = d.Format("2006-01-02")
|
||||
}
|
||||
bezGarancije := nalog.GarancijaDo == nil
|
||||
// podrazumevano trajanje garancije u danima (dugme „Podrazumevano") i stanje „bez garancije" iz baze
|
||||
garancijaDefault := strconv.Itoa(defaultGarancijaDana(podesavanja))
|
||||
bezGarancije := nalog.GarancijaDana == nil || *nalog.GarancijaDana <= 0
|
||||
|
||||
klijentNaziv := ""
|
||||
if nalog.KlijentID != nil {
|
||||
@@ -560,10 +560,10 @@ func (h *Handler) DetaljiNaloga(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// lista tehničara za izbor u popup-u „Promeni tehničara"
|
||||
// lista servisera za izbor u popup-u „Promeni servisera"
|
||||
tehnicari, err := h.KorisniciRepo.Lista(r.Context())
|
||||
if err != nil {
|
||||
slog.Error("greška pri učitavanju tehničara", "error", err)
|
||||
slog.Error("greška pri učitavanju servisera", "error", err)
|
||||
}
|
||||
|
||||
delovi, err := h.deloviSaPotrazivanima(r.Context(), id)
|
||||
@@ -905,7 +905,7 @@ func parseFormuNaloga(r *http.Request) (model.ServisniNalog, string) {
|
||||
}
|
||||
}
|
||||
|
||||
// opcioni tehničar
|
||||
// opcioni serviser
|
||||
if tidStr := r.FormValue("tehnicar_id"); tidStr != "" {
|
||||
if tid, err := strconv.ParseInt(tidStr, 10, 64); err == nil {
|
||||
nalog.TehnicarID = &tid
|
||||
@@ -1021,6 +1021,60 @@ func defaultPredvidjenDatum(datumPrijema time.Time, podesavanja map[string]strin
|
||||
return &t
|
||||
}
|
||||
|
||||
// srpskiPlural vraća odgovarajući oblik reči za dati broj (1 / 2–4 / 5+).
|
||||
func srpskiPlural(n int, jedan, par, mnogo string) string {
|
||||
if n < 0 {
|
||||
n = -n
|
||||
}
|
||||
d, dd := n%10, n%100
|
||||
switch {
|
||||
case d == 1 && dd != 11:
|
||||
return jedan
|
||||
case d >= 2 && d <= 4 && (dd < 12 || dd > 14):
|
||||
return par
|
||||
default:
|
||||
return mnogo
|
||||
}
|
||||
}
|
||||
|
||||
// garancijaUTekst formatira trajanje garancije (u danima) na srpskom:
|
||||
// 20 → „20 dana", 45 → „1 mesec i 15 dana", 60 → „2 meseca".
|
||||
func garancijaUTekst(dana int) string {
|
||||
if dana <= 0 {
|
||||
return "bez garancije"
|
||||
}
|
||||
m := dana / 30
|
||||
d := dana % 30
|
||||
mesecRec := srpskiPlural(m, "mesec", "meseca", "meseci")
|
||||
danRec := srpskiPlural(d, "dan", "dana", "dana")
|
||||
switch {
|
||||
case m > 0 && d > 0:
|
||||
return fmt.Sprintf("%d %s i %d %s", m, mesecRec, d, danRec)
|
||||
case m > 0:
|
||||
return fmt.Sprintf("%d %s", m, mesecRec)
|
||||
default:
|
||||
return fmt.Sprintf("%d %s", d, danRec)
|
||||
}
|
||||
}
|
||||
|
||||
// garancijaTekst je šablonski helper: trajanje garancije u danima → tekst, ili „—" za prazno.
|
||||
func garancijaTekst(dana *int) string {
|
||||
if dana == nil || *dana <= 0 {
|
||||
return "—"
|
||||
}
|
||||
return garancijaUTekst(*dana)
|
||||
}
|
||||
|
||||
// defaultGarancijaDana vraća podrazumevano trajanje garancije u danima iz podešavanja
|
||||
// (servis_garancija_dana, podrazumevano 60).
|
||||
func defaultGarancijaDana(podesavanja map[string]string) int {
|
||||
dana, err := strconv.Atoi(vrednostIliDefault(podesavanja, "servis_garancija_dana", "60"))
|
||||
if err != nil || dana < 0 {
|
||||
return 60
|
||||
}
|
||||
return dana
|
||||
}
|
||||
|
||||
// garancijaPrePrijema vraća true ako je datum garancije raniji od datuma prijema (po danu).
|
||||
// Garancija ne sme da bude pre prijema uređaja.
|
||||
func garancijaPrePrijema(garancija, prijem time.Time) bool {
|
||||
@@ -1483,6 +1537,42 @@ func (h *Handler) AzurirajGaranciju(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(id, 10), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// AzurirajGarancijaDana postavlja trajanje garancije u danima (od završetka radova);
|
||||
// „bez garancije" ili prazno/0 → bez garancije.
|
||||
func (h *Handler) AzurirajGarancijaDana(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := h.zahtevajDozvolu(w, r, "servis.izmeni"); !ok {
|
||||
return
|
||||
}
|
||||
id, 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, "Greška pri čitanju forme", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
var dana *int
|
||||
if r.FormValue("bez_garancije") != "1" {
|
||||
if s := strings.TrimSpace(r.FormValue("garancija_dana")); s != "" {
|
||||
n, err := strconv.Atoi(s)
|
||||
if err != nil || n < 0 {
|
||||
http.Error(w, "Garancija mora biti ceo broj dana.", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if n > 0 {
|
||||
dana = &n
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := h.ServisRepo.AzurirajGarancijaDana(r.Context(), id, dana); err != nil {
|
||||
slog.Error("greška pri ažuriranju garancije (dana)", "id", id, "error", err)
|
||||
http.Error(w, "Greška pri ažuriranju garancije", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(id, 10), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// AzurirajPredvidjenDatum postavlja ručni override predviđenog datuma popravke;
|
||||
// prazno polje vraća nalog na izvedeni default (prijem + rok iz podešavanja).
|
||||
func (h *Handler) AzurirajPredvidjenDatum(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -1515,7 +1605,7 @@ func (h *Handler) AzurirajPredvidjenDatum(w http.ResponseWriter, r *http.Request
|
||||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(id, 10), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// AzurirajTehnicar menja dodeljenog tehničara na nalogu; prazna vrednost → nedodeljen.
|
||||
// AzurirajTehnicar menja dodeljenog servisera na nalogu; prazna vrednost → nedodeljen.
|
||||
func (h *Handler) AzurirajTehnicar(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := h.zahtevajDozvolu(w, r, "servis.izmeni"); !ok {
|
||||
return
|
||||
@@ -1533,14 +1623,14 @@ func (h *Handler) AzurirajTehnicar(w http.ResponseWriter, r *http.Request) {
|
||||
if t := strings.TrimSpace(r.FormValue("tehnicar_id")); t != "" {
|
||||
v, err := strconv.ParseInt(t, 10, 64)
|
||||
if err != nil {
|
||||
http.Error(w, "Neispravan tehničar", http.StatusBadRequest)
|
||||
http.Error(w, "Neispravan serviser", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
tehnicarID = &v
|
||||
}
|
||||
if err := h.ServisRepo.AzurirajTehnicar(r.Context(), id, tehnicarID); err != nil {
|
||||
slog.Error("greška pri ažuriranju tehničara", "id", id, "error", err)
|
||||
http.Error(w, "Greška pri ažuriranju tehničara", http.StatusInternalServerError)
|
||||
slog.Error("greška pri ažuriranju servisera", "id", id, "error", err)
|
||||
http.Error(w, "Greška pri ažuriranju servisera", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(id, 10), http.StatusSeeOther)
|
||||
|
||||
@@ -40,6 +40,7 @@ type ServisniNalog struct {
|
||||
Avans *float64
|
||||
Napomena string
|
||||
GarancijaDo *time.Time
|
||||
GarancijaDana *int // trajanje garancije u danima (od završetka); nil/0 = bez garancije
|
||||
DatumPrijema time.Time
|
||||
PredvidjenDatum *time.Time
|
||||
DatumZavrsetka *time.Time
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
-- garancija_dana: trajanje garancije u danima (računato od datuma završetka radova).
|
||||
-- Unosi se dok je uređaj u radu; pravi datum (garancija_do) se popunjava kad nalog
|
||||
-- pređe u „Završeno" (datum_zavrsetka + garancija_dana). NULL ili 0 = bez garancije.
|
||||
ALTER TABLE servisni_nalozi ADD COLUMN garancija_dana INTEGER;
|
||||
@@ -22,14 +22,15 @@
|
||||
<div class="kolona" style="gap:16px;">
|
||||
|
||||
<div style="max-width:280px;">
|
||||
<label for="servis_garancija_meseci" class="polje-labela">
|
||||
Podrazumevani rok garancije (meseci)
|
||||
<label for="servis_garancija_dana" class="polje-labela">
|
||||
Podrazumevana garancija (dana)
|
||||
</label>
|
||||
<input type="number" id="servis_garancija_meseci" name="servis_garancija_meseci"
|
||||
min="0" max="120" value="{{.ServisGarancijaMeseci}}"
|
||||
<input type="number" id="servis_garancija_dana" name="servis_garancija_dana"
|
||||
min="0" max="3650" value="{{.ServisGarancijaDana}}"
|
||||
style="width:100%;">
|
||||
<div class="pomocni-tekst" style="margin-top:6px;">
|
||||
Automatski se upisuje pri prijemu uređaja. Vrednost 0 znači bez garancije.
|
||||
Trajanje garancije u danima (računato od završetka radova). Dugme „Podrazumevano"
|
||||
na nalogu koristi ovu vrednost. 0 znači bez garancije.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -98,35 +98,35 @@
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:4px;">Garancija do</div>
|
||||
<div style="font-size:14px;{{if .Nalog.GarancijaDo}}color:#16a34a;font-weight:500;{{else}}color:var(--tekst-sporedni);{{end}}">
|
||||
{{if .Nalog.GarancijaDo}}{{.Nalog.GarancijaDo.Format "02.01.2006."}}{{else}}—{{end}}
|
||||
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:4px;">Garancija</div>
|
||||
<div style="font-size:14px;{{if not .BezGarancije}}color:#16a34a;font-weight:500;{{else}}color:var(--tekst-sporedni);{{end}}">
|
||||
{{garancijaTekst .Nalog.GarancijaDana}}
|
||||
{{if index .Dozvole "servis.izmeni"}}
|
||||
<button type="button" class="izmeni-polje" onclick="ntechOtvoriModal(this)">Promeni</button>
|
||||
<dialog class="premesti-modal" onclick="if(event.target===this)this.close()">
|
||||
<form method="dialog" class="premesti-zaglavlje">
|
||||
<h3>Garancija do</h3>
|
||||
<h3>Garancija</h3>
|
||||
<button type="submit" class="premesti-zatvori" aria-label="Zatvori">×</button>
|
||||
</form>
|
||||
<form method="POST" action="/servis/{{.Nalog.ID}}/garancija" data-refresh="#nalog-zaglavlje" style="display:flex;flex-direction:column;gap:12px;padding:16px;">
|
||||
<form method="POST" action="/servis/{{.Nalog.ID}}/garancija-dana" data-refresh="#nalog-zaglavlje" style="display:flex;flex-direction:column;gap:12px;padding:16px;">
|
||||
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
|
||||
<div>
|
||||
<label class="polje-labela">Garancija do</label>
|
||||
<label class="polje-labela">Trajanje garancije (dana)</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"}}{{else}}{{.GarancijaDefault}}{{end}}"
|
||||
min="{{.Nalog.DatumPrijema.Format "2006-01-02"}}"
|
||||
<input type="number" name="garancija_dana" min="0" step="1"
|
||||
value="{{if .BezGarancije}}{{.GarancijaDefault}}{{else}}{{.Nalog.GarancijaDana}}{{end}}"
|
||||
{{if .BezGarancije}}disabled{{end}}
|
||||
style="flex:1;min-width:0;padding:8px 12px;border:0.5px solid var(--ivica);border-radius:8px;font-size:14px;background:var(--pozadina);color:var(--tekst-glavni);outline:none;{{if .BezGarancije}}opacity:.5;{{end}}">
|
||||
<label class="tip-opcija{{if .BezGarancije}} tip-opcija-akt{{end}}" style="white-space:nowrap;">
|
||||
<input type="checkbox" name="bez_garancije" value="1"{{if .BezGarancije}} checked{{end}} style="margin:0;width:auto;"
|
||||
onchange="var d=this.closest('div').querySelector('[name=garancija_do]'); d.disabled=this.checked; d.style.opacity=this.checked?'.5':''; this.closest('label').classList.toggle('tip-opcija-akt',this.checked);">
|
||||
onchange="var d=this.closest('div').querySelector('[name=garancija_dana]'); d.disabled=this.checked; d.style.opacity=this.checked?'.5':''; this.closest('label').classList.toggle('tip-opcija-akt',this.checked);">
|
||||
Bez garancije
|
||||
</label>
|
||||
</div>
|
||||
<div class="pomocni-tekst" style="margin-top:6px;">Računa se od datuma završetka radova.</div>
|
||||
</div>
|
||||
<button type="button" class="btn-sekundarno" style="align-self:flex-start;"
|
||||
onclick="var f=this.closest('form'); var d=f.querySelector('[name=garancija_do]'); d.disabled=false; d.style.opacity=''; d.value='{{.GarancijaDefault}}'; var c=f.querySelector('[name=bez_garancije]'); c.checked=false; c.closest('label').classList.remove('tip-opcija-akt'); f.requestSubmit();">Podrazumevano</button>
|
||||
onclick="var f=this.closest('form'); var d=f.querySelector('[name=garancija_dana]'); d.disabled=false; d.style.opacity=''; d.value='{{.GarancijaDefault}}'; var c=f.querySelector('[name=bez_garancije]'); c.checked=false; c.closest('label').classList.remove('tip-opcija-akt'); f.requestSubmit();">Podrazumevano</button>
|
||||
</form>
|
||||
</dialog>
|
||||
{{end}}
|
||||
@@ -139,20 +139,20 @@
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:4px;">Tehničar</div>
|
||||
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:4px;">Serviser</div>
|
||||
<div style="font-size:14px;font-weight:500;color:var(--tekst-glavni);">
|
||||
{{if .TehnicarNaziv}}{{.TehnicarNaziv}}{{else}}<span style="color:var(--tekst-sporedni);font-weight:normal;">— nedodeljen —</span>{{end}}
|
||||
{{if index .Dozvole "servis.izmeni"}}
|
||||
<button type="button" class="izmeni-polje" onclick="ntechOtvoriModal(this)">Promeni</button>
|
||||
<dialog class="premesti-modal" onclick="if(event.target===this)this.close()">
|
||||
<form method="dialog" class="premesti-zaglavlje">
|
||||
<h3>Tehničar</h3>
|
||||
<h3>Serviser</h3>
|
||||
<button type="submit" class="premesti-zatvori" aria-label="Zatvori">×</button>
|
||||
</form>
|
||||
<form method="POST" action="/servis/{{.Nalog.ID}}/tehnicar" data-refresh="#nalog-zaglavlje" style="display:flex;flex-direction:column;gap:12px;padding:16px;">
|
||||
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
|
||||
<div>
|
||||
<label class="polje-labela">Tehničar</label>
|
||||
<label class="polje-labela">Serviser</label>
|
||||
<select name="tehnicar_id"
|
||||
style="width:100%;padding:8px 12px;border:0.5px solid var(--ivica);border-radius:8px;font-size:14px;background:var(--pozadina);color:var(--tekst-glavni);outline:none;">
|
||||
<option value="">— nedodeljen —</option>
|
||||
@@ -553,7 +553,7 @@
|
||||
}
|
||||
})();
|
||||
|
||||
// Popup polja (predviđen datum, garancija, tehničar): nema dugmeta „Sačuvaj" —
|
||||
// Popup polja (predviđen datum, garancija, serviser): nema dugmeta „Sačuvaj" —
|
||||
// snima se na zatvaranje modala, i to samo ako je vrednost promenjena.
|
||||
// Početno stanje forme pamtimo pri otvaranju; na „close" upoređujemo i, ako se
|
||||
// razlikuje, šaljemo formu (ntech AJAX presretač osveži #nalog-zaglavlje).
|
||||
|
||||
@@ -100,11 +100,11 @@
|
||||
|
||||
<!-- ===================== KARTICA: TEHNIČAR (izmena) / KLIJENT I TEHNIČAR (novi) ===================== -->
|
||||
{{if .Izmena}}
|
||||
<!-- pri izmeni klijent se ne menja (vezan pri prijemu) — ostaje samo tehničar -->
|
||||
<!-- pri izmeni klijent se ne menja (vezan pri prijemu) — ostaje samo serviser -->
|
||||
<div class="kartica forma-kartica animiraj" style="margin-bottom:20px;">
|
||||
<div class="sekcija-naslov">Tehničar</div>
|
||||
<div class="sekcija-naslov">Serviser</div>
|
||||
<div>
|
||||
<label class="polje-labela">Tehničar</label>
|
||||
<label class="polje-labela">Serviser</label>
|
||||
<select name="tehnicar_id" style="width:100%;">
|
||||
<option value="">— nedodeljen —</option>
|
||||
{{range .Tehnicari}}
|
||||
@@ -252,9 +252,9 @@
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- tehničar -->
|
||||
<!-- serviser -->
|
||||
<div>
|
||||
<label class="polje-labela">Tehničar</label>
|
||||
<label class="polje-labela">Serviser</label>
|
||||
<select name="tehnicar_id" style="width:100%;">
|
||||
<option value="">— nedodeljen —</option>
|
||||
{{range .Tehnicari}}
|
||||
@@ -280,27 +280,6 @@
|
||||
value="{{if .Nalog.Avans}}{{.Nalog.Avans}}{{end}}"
|
||||
placeholder="0" style="width:100%;">
|
||||
</div>
|
||||
{{if .Izmena}}
|
||||
<!-- garancija — isti izgled kao popup „Garancija do" u detaljima naloga;
|
||||
„Bez garancije" isključuje datum, „Podrazumevano" vraća prijem + meseci -->
|
||||
<div>
|
||||
<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"}}{{else}}{{.GarancijaDefault}}{{end}}"
|
||||
min="{{.Nalog.DatumPrijema.Format "2006-01-02"}}"
|
||||
{{if .BezGarancije}}disabled{{end}}
|
||||
style="flex:1;min-width:0;{{if .BezGarancije}}opacity:.5;{{end}}">
|
||||
<label class="tip-opcija{{if .BezGarancije}} tip-opcija-akt{{end}}" style="white-space:nowrap;">
|
||||
<input type="checkbox" name="bez_garancije" value="1"{{if .BezGarancije}} checked{{end}} style="margin:0;width:auto;"
|
||||
onchange="var d=this.closest('div').querySelector('[name=garancija_do]'); d.disabled=this.checked; d.style.opacity=this.checked?'.5':''; this.closest('label').classList.toggle('tip-opcija-akt',this.checked);">
|
||||
Bez garancije
|
||||
</label>
|
||||
</div>
|
||||
<button type="button" class="btn-sekundarno" style="margin-top:8px;"
|
||||
onclick="var f=this.closest('form'); var d=f.querySelector('[name=garancija_do]'); d.disabled=false; d.style.opacity=''; d.value='{{.GarancijaDefault}}'; var c=f.querySelector('[name=bez_garancije]'); c.checked=false; c.closest('label').classList.remove('tip-opcija-akt'); d.dispatchEvent(new Event('change',{bubbles:true}));">Podrazumevano</button>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<div style="margin-top:16px;">
|
||||
|
||||
@@ -146,7 +146,7 @@
|
||||
</div>
|
||||
{{if .TehnicarNaziv}}
|
||||
<div>
|
||||
<div class="polje-labela">Tehničar</div>
|
||||
<div class="polje-labela">Serviser</div>
|
||||
<div class="polje-vrednost">{{.TehnicarNaziv}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -244,7 +244,7 @@
|
||||
<!-- potpisi -->
|
||||
<div class="potpisi">
|
||||
<div>
|
||||
<div class="potpis-linija">Predao (tehničar / firma)</div>
|
||||
<div class="potpis-linija">Predao (serviser / firma)</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="potpis-linija">Preuzeo (klijent)</div>
|
||||
|
||||
@@ -143,7 +143,7 @@
|
||||
</div>
|
||||
{{if .TehnicarNaziv}}
|
||||
<div>
|
||||
<div class="polje-labela">Tehničar</div>
|
||||
<div class="polje-labela">Serviser</div>
|
||||
<div class="polje-vrednost">{{.TehnicarNaziv}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
</div>
|
||||
{{if .TehnicarNaziv}}
|
||||
<div>
|
||||
<div class="polje-labela">Tehničar</div>
|
||||
<div class="polje-labela">Serviser</div>
|
||||
<div class="polje-vrednost">{{.TehnicarNaziv}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -249,7 +249,7 @@
|
||||
<div class="potpis-linija">Predao klijent</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="potpis-linija">Primio tehničar</div>
|
||||
<div class="potpis-linija">Primio serviser</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user