diff --git a/internal/db/repository.go b/internal/db/repository.go index eafcdbc..1dfd77e 100644 --- a/internal/db/repository.go +++ b/internal/db/repository.go @@ -386,6 +386,12 @@ type FiskalRepository interface { OznačiKaoStorniran(ctx context.Context, id int64) error } +// ServisniLogRepository definiše operacije nad log-om događaja servisnog naloga. +type ServisniLogRepository interface { + Kreiraj(ctx context.Context, nalogID int64, dogadjaj string, korisnikID *int64) error + DohvatiZaNalog(ctx context.Context, nalogID int64) ([]model.ServisniLog, error) +} + // KpoRepository definiše operacije nad knjigom o ostvarenom prometu. type KpoRepository interface { Lista(ctx context.Context, od, do time.Time) ([]model.KpoZapis, error) diff --git a/internal/db/sqlite/servisni_log.go b/internal/db/sqlite/servisni_log.go new file mode 100644 index 0000000..8d4644e --- /dev/null +++ b/internal/db/sqlite/servisni_log.go @@ -0,0 +1,50 @@ +package sqlite + +import ( + "context" + "database/sql" + + "ntech/internal/model" +) + +// ServisniLogRepo je SQLite implementacija db.ServisniLogRepository +type ServisniLogRepo struct { + db *sql.DB +} + +// NoviServisniLogRepo kreira repozitorijum log-a servisnih naloga +func NoviServisniLogRepo(baza *sql.DB) *ServisniLogRepo { + return &ServisniLogRepo{db: baza} +} + +// Kreiraj upisuje novi događaj u log servisnog naloga. +func (r *ServisniLogRepo) Kreiraj(ctx context.Context, nalogID int64, dogadjaj string, korisnikID *int64) error { + _, err := r.db.ExecContext(ctx, + `INSERT INTO servisni_log (nalog_id, dogadjaj, korisnik_id) VALUES (?, ?, ?)`, + nalogID, dogadjaj, korisnikID, + ) + return err +} + +// DohvatiZaNalog vraća sve log zapise za dati nalog, od najstarijeg ka najnovijem. +func (r *ServisniLogRepo) DohvatiZaNalog(ctx context.Context, nalogID int64) ([]model.ServisniLog, error) { + rows, err := r.db.QueryContext(ctx, + `SELECT id, nalog_id, dogadjaj, COALESCE(napomena,''), korisnik_id, datum + FROM servisni_log WHERE nalog_id = ? ORDER BY datum ASC`, + nalogID, + ) + if err != nil { + return nil, err + } + defer rows.Close() + + var lista []model.ServisniLog + for rows.Next() { + var l model.ServisniLog + if err := rows.Scan(&l.ID, &l.NalogID, &l.Dogadjaj, &l.Napomena, &l.KorisnikID, &l.Datum); err != nil { + return nil, err + } + lista = append(lista, l) + } + return lista, rows.Err() +} diff --git a/internal/handler/handler.go b/internal/handler/handler.go index aa81791..0be8629 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -47,6 +47,7 @@ type Handler struct { NivelacijaRepo db.NivelacijaRepository FiskalRepo db.FiskalRepository KpoRepo db.KpoRepository + ServisniLogRepo db.ServisniLogRepository Verzija string JelDemo bool AssetV string // verzija statičkih fajlova za cache-busting (postavlja se pri pokretanju) @@ -117,6 +118,7 @@ func Novi(baza *sql.DB, totpKljuc []byte) *Handler { NivelacijaRepo: sqlite.NoviNivelacijaRepo(baza), FiskalRepo: sqlite.NoviFiskalRepo(baza), KpoRepo: sqlite.NoviKpoRepo(baza), + ServisniLogRepo: sqlite.NoviServisniLogRepo(baza), } } @@ -152,6 +154,7 @@ func (h *Handler) reinicijalizujRepozitorijume(novaDB *sql.DB) { h.NivelacijaRepo = sqlite.NoviNivelacijaRepo(novaDB) h.FiskalRepo = sqlite.NoviFiskalRepo(novaDB) h.KpoRepo = sqlite.NoviKpoRepo(novaDB) + h.ServisniLogRepo = sqlite.NoviServisniLogRepo(novaDB) } // modulUkljucen vraća da li je zakonski modul (npr. „pdv") uključen za firmu prema profilu. diff --git a/internal/handler/servis.go b/internal/handler/servis.go index 8f5a3d0..3603d88 100644 --- a/internal/handler/servis.go +++ b/internal/handler/servis.go @@ -1702,6 +1702,8 @@ type PodaciEskalacionogLista struct { PIB string MaticniBroj string Barkod string + // LogDogadjaji mapa dogadjaj→vreme, npr. "status:U dijagnostici"→datum + LogDogadjaji map[string]time.Time } // StampaEskalacionogLista renderuje interni list praćenja rokova. @@ -1745,6 +1747,15 @@ func (h *Handler) StampaEskalacionogLista(w http.ResponseWriter, r *http.Request } } + logZapisi, _ := h.ServisniLogRepo.DohvatiZaNalog(r.Context(), id) + logDogadjaji := make(map[string]time.Time, len(logZapisi)) + for _, l := range logZapisi { + // čuvamo samo prvi zapis za svaki tip događaja + if _, postoji := logDogadjaji[l.Dogadjaj]; !postoji { + logDogadjaji[l.Dogadjaj] = l.Datum + } + } + h.renderujStandalone(w, "servis_eskalacioni_list", PodaciEskalacionogLista{ Nalog: *nalog, KlijentNaziv: klijentNaziv, @@ -1759,6 +1770,7 @@ func (h *Handler) StampaEskalacionogLista(w http.ResponseWriter, r *http.Request PIB: podesavanja["pib"], MaticniBroj: podesavanja["maticni_broj"], Barkod: barkodNaloga(nalog.BrojNaloga), + LogDogadjaji: logDogadjaji, }) } @@ -2123,6 +2135,13 @@ func (h *Handler) PromeniStatus(w http.ResponseWriter, r *http.Request) { http.Error(w, "Greška pri promeni statusa", http.StatusInternalServerError) return } + // log događaja — best-effort, greška se ignoriše + if k := middleware.KorisnikIzKonteksta(r.Context()); k != nil { + kid := k.ID + _ = h.ServisniLogRepo.Kreiraj(r.Context(), id, "status:"+noviStatus, &kid) + } else { + _ = h.ServisniLogRepo.Kreiraj(r.Context(), id, "status:"+noviStatus, nil) + } // pri prelasku u Preuzeto — auto-izračunaj cenu_konacnu ako nije uneta, pa sačuvaj naplatu i fiskalizuj if noviStatus == model.StatusPreuzeto { @@ -2934,6 +2953,7 @@ func (h *Handler) ServisJavniPrihvati(w http.ResponseWriter, r *http.Request) { if err := h.ServisRepo.SacuvajOdlukuKlijenta(r.Context(), nalog.ID, "prihvaceno", komentar); err != nil { slog.Error("greška pri čuvanju odluke klijenta", "error", err, "nalog_id", nalog.ID) } + _ = h.ServisniLogRepo.Kreiraj(r.Context(), nalog.ID, "odluka:prihvaceno", nil) http.Redirect(w, r, "/status/"+token, http.StatusSeeOther) } @@ -2990,6 +3010,7 @@ func (h *Handler) ServisJavniOdlukaOdabrano(w http.ResponseWriter, r *http.Reque 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) } + _ = h.ServisniLogRepo.Kreiraj(r.Context(), nalog.ID, "odluka:"+odluka, nil) http.Redirect(w, r, "/status/"+token, http.StatusSeeOther) } @@ -3101,6 +3122,7 @@ func (h *Handler) ServisJavniOdbij(w http.ResponseWriter, r *http.Request) { if err := h.ServisRepo.SacuvajOdlukuKlijenta(r.Context(), nalog.ID, "odbijeno", komentar); err != nil { slog.Error("greška pri čuvanju odluke klijenta", "error", err, "nalog_id", nalog.ID) } + _ = h.ServisniLogRepo.Kreiraj(r.Context(), nalog.ID, "odluka:odbijeno", nil) http.Redirect(w, r, "/status/"+token, http.StatusSeeOther) } diff --git a/internal/model/servis.go b/internal/model/servis.go index 299eccb..e9e78f8 100644 --- a/internal/model/servis.go +++ b/internal/model/servis.go @@ -62,6 +62,16 @@ type ServisniNalog struct { Naplaceno float64 // iznos koji je naplaćen pri preuzimanju } +// ServisniLog je jedan zapis u istoriji događaja servisnog naloga. +type ServisniLog struct { + ID int64 + NalogID int64 + Dogadjaj string // npr. "status:U dijagnostici", "odluka:prihvaceno" + Napomena string + KorisnikID *int64 + Datum time.Time +} + // ServisniDeo predstavlja jedan artikal ugrađen u servisni nalog type ServisniDeo struct { ID int64 diff --git a/migrations/095_servisni_log.sql b/migrations/095_servisni_log.sql new file mode 100644 index 0000000..c6d478e --- /dev/null +++ b/migrations/095_servisni_log.sql @@ -0,0 +1,10 @@ +CREATE TABLE IF NOT EXISTS servisni_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + nalog_id INTEGER NOT NULL REFERENCES servisni_nalozi(id) ON DELETE CASCADE, + dogadjaj TEXT NOT NULL, -- npr. "status:U dijagnostici", "odluka:prihvaceno", "poziv_klijenta" + napomena TEXT, + korisnik_id INTEGER REFERENCES korisnici(id), + datum DATETIME NOT NULL DEFAULT (datetime('now','localtime')) +); + +CREATE INDEX IF NOT EXISTS idx_servisni_log_nalog ON servisni_log(nalog_id); diff --git a/web/templates/stranice/servis_eskalacioni_list.html b/web/templates/stranice/servis_eskalacioni_list.html index e059ce5..922d278 100644 --- a/web/templates/stranice/servis_eskalacioni_list.html +++ b/web/templates/stranice/servis_eskalacioni_list.html @@ -126,11 +126,19 @@ {{.Nalog.DatumPrijema.Format "02.01.2006."}} auto + {{with index .LogDogadjaji "status:U dijagnostici"}} + + Početak dijagnostike + {{.Format "02.01.2006."}} auto + + + {{else}} Početak dijagnostike + {{end}} {{if .Nalog.PredvidjenDatum}} Predviđeni datum završetka @@ -190,11 +198,19 @@ + {{with index .LogDogadjaji "status:Preuzeto"}} + + Preuzimanje uređaja + {{.Format "02.01.2006."}} auto + + + {{else}} Preuzimanje uređaja + {{end}} {{if .RokPodizanja}} Rok za podizanje