Servis: servisni_log tabela — praćenje datuma događaja za eskalacioni list

This commit is contained in:
2026-06-30 22:57:01 +02:00
parent c971b5d075
commit 18f242d73f
7 changed files with 117 additions and 0 deletions
+6
View File
@@ -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)
+50
View File
@@ -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()
}
+3
View File
@@ -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.
+22
View File
@@ -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)
}
+10
View File
@@ -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
+10
View File
@@ -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);
@@ -126,11 +126,19 @@
<td><strong>{{.Nalog.DatumPrijema.Format "02.01.2006."}}</strong> <span class="badge-auto">auto</span></td>
<td></td>
</tr>
{{with index .LogDogadjaji "status:U dijagnostici"}}
<tr class="auto">
<td>Početak dijagnostike</td>
<td><strong>{{.Format "02.01.2006."}}</strong> <span class="badge-auto">auto</span></td>
<td></td>
</tr>
{{else}}
<tr>
<td>Početak dijagnostike</td>
<td><span class="linija-unos"></span></td>
<td></td>
</tr>
{{end}}
{{if .Nalog.PredvidjenDatum}}
<tr class="auto">
<td>Predviđeni datum završetka</td>
@@ -190,11 +198,19 @@
<td><span class="linija-unos"></span></td>
<td></td>
</tr>
{{with index .LogDogadjaji "status:Preuzeto"}}
<tr class="auto">
<td>Preuzimanje uređaja</td>
<td><strong>{{.Format "02.01.2006."}}</strong> <span class="badge-auto">auto</span></td>
<td></td>
</tr>
{{else}}
<tr>
<td>Preuzimanje uređaja</td>
<td><span class="linija-unos"></span></td>
<td></td>
</tr>
{{end}}
{{if .RokPodizanja}}
<tr class="auto rok-upozorenje">
<td><strong>Rok za podizanje</strong></td>