Dashboard/izveštaji: ispravka obračuna prihoda + KIR servisa, testovi

- Prihod meseca: izbačeni stornirani nalozi, servis preko naplaceno+avans
- Servis KIR: neto cena + PDV po stvarnoj stopi (PdvKir.DodajNeto), bez hardkodovanih 20%
- TopKlijenti: samo preuzeti nalozi sa naplatom/avansom
- Izveštaji: mesečni ključevi sidreni na 1. u mesecu (bez preliva dana)
- BUG-04: konačna cena servisa bez dvostrukog računanja delova
- Dashboard: uklonjena kartica „Prihod ovog meseca"
- Default port 3000
- Dodati testovi: prihod/storno/delovi/nivelacija/KIR/mesečni ključevi
This commit is contained in:
2026-06-27 09:41:07 +02:00
parent 60e7debad5
commit 3475422cb9
17 changed files with 1385 additions and 76 deletions
-10
View File
@@ -36,7 +36,6 @@ func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) {
}
var brojArtikala, aktivniServisi, kriticnaZaliha, aktivniPodsetnici int
var prihodOvogMeseca float64
if n, err := h.IzvestajRepo.BrojArtikala(ctx); err != nil {
slog.Error("dashboard: broj artikala", "error", err)
@@ -50,15 +49,7 @@ func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) {
aktivniServisi = n
}
// prihod se dohvata samo ako korisnik ima dozvolu dashboard.prihod
korisnikDash := middleware.KorisnikIzKonteksta(ctx)
if h.DozvoleRepo.ImaDozvolu(ctx, korisnikDash.Uloga, "dashboard.prihod") {
if v, err := h.IzvestajRepo.PrihodTekuciMesec(ctx); err != nil {
slog.Error("dashboard: prihod ovog meseca", "error", err)
} else {
prihodOvogMeseca = v
}
}
if n, err := h.IzvestajRepo.BrojKriticnihZaliha(ctx); err != nil {
slog.Error("dashboard: kriticna zaliha", "error", err)
@@ -133,7 +124,6 @@ func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) {
PodaciStranice: ps,
BrojArtikala: brojArtikala,
AktivniServisi: aktivniServisi,
PrihodOvogMeseca: prihodOvogMeseca,
KriticnaZaliha: kriticnaZaliha,
AktivniPodsetnici: aktivniPodsetnici,
PoslednjiServisi: poslednjiServisi,
+14 -4
View File
@@ -28,6 +28,19 @@ func formatujMesec(yyyymm string) string {
return fmt.Sprintf("%s %d", srpskaImenaMeseci[mes], god)
}
// kljuceviMeseci vraća ključeve ("2006-01") za poslednjih n meseci zaključno sa
// mesecom datuma `sada`, hronološki (najstariji prvi). Sidri na prvi u mesecu —
// inače AddDate prelije dan (npr. 31. mart 1 mesec = „31. feb" → 3. mart) i neki
// mesec bi se preskočio ili duplirao na 2931. u mesecu.
func kljuceviMeseci(sada time.Time, n int) []string {
prvi := time.Date(sada.Year(), sada.Month(), 1, 0, 0, 0, 0, sada.Location())
kljucevi := make([]string, 0, n)
for i := n - 1; i >= 0; i-- {
kljucevi = append(kljucevi, prvi.AddDate(0, -i, 0).Format("2006-01"))
}
return kljucevi
}
// PodaciIzvestaja su podaci za stranicu izveštaja
type PodaciIzvestaja struct {
model.PodaciStranice
@@ -108,15 +121,12 @@ func (h *Handler) Izvestaji(w http.ResponseWriter, r *http.Request) {
}
// gradimo niz za poslednjih 12 meseci (hronološki)
sada := time.Now()
var mesecniPrihodi []MesecniPrihod
var grafikonLabele []string
var grafikonProdaja []float64
var grafikonServis []float64
for i := 11; i >= 0; i-- {
t := sada.AddDate(0, -i, 0)
kljuc := t.Format("2006-01")
for _, kljuc := range kljuceviMeseci(time.Now(), 12) {
prod := prodajaPoMesecu[kljuc]
serv := servisPoMesecu[kljuc]
mesecniPrihodi = append(mesecniPrihodi, MesecniPrihod{
+69
View File
@@ -0,0 +1,69 @@
package handler
import (
"testing"
"time"
)
// TestKljuceviMeseci proverava da se na kraju meseca (2931.) nijedan mesec ne
// preskoči niti duplira — klasičan AddBug sa prelivom dana.
func TestKljuceviMeseci(t *testing.T) {
slucajevi := []struct {
naziv string
sada time.Time
n int
ocekuje []string
}{
{
// 31. mart: bez sidrenja na 1. dan, "31. feb" bi se prelio u mart i
// februar bi nestao. Mora dati uredan niz jan..mart.
naziv: "31. mart — bez preskakanja februara",
sada: time.Date(2026, 3, 31, 12, 0, 0, 0, time.UTC),
n: 3,
ocekuje: []string{"2026-01", "2026-02", "2026-03"},
},
{
// 31. maj unazad: mart/april imaju 31/30 dana
naziv: "31. maj — uredan niz unazad",
sada: time.Date(2026, 5, 31, 0, 0, 0, 0, time.UTC),
n: 4,
ocekuje: []string{"2026-02", "2026-03", "2026-04", "2026-05"},
},
{
// prelaz preko godine
naziv: "30. januar — prelaz preko godine",
sada: time.Date(2026, 1, 30, 0, 0, 0, 0, time.UTC),
n: 3,
ocekuje: []string{"2025-11", "2025-12", "2026-01"},
},
{
// puna godina (12 meseci), bez duplikata
naziv: "12 meseci, bez duplikata",
sada: time.Date(2026, 6, 27, 0, 0, 0, 0, time.UTC),
n: 12,
ocekuje: []string{
"2025-07", "2025-08", "2025-09", "2025-10", "2025-11", "2025-12",
"2026-01", "2026-02", "2026-03", "2026-04", "2026-05", "2026-06",
},
},
}
for _, s := range slucajevi {
t.Run(s.naziv, func(t *testing.T) {
dobijeno := kljuceviMeseci(s.sada, s.n)
if len(dobijeno) != len(s.ocekuje) {
t.Fatalf("dužina = %d, očekivano %d (%v)", len(dobijeno), len(s.ocekuje), dobijeno)
}
vidjeni := map[string]bool{}
for i := range s.ocekuje {
if dobijeno[i] != s.ocekuje[i] {
t.Errorf("[%d] = %q, očekivano %q", i, dobijeno[i], s.ocekuje[i])
}
if vidjeni[dobijeno[i]] {
t.Errorf("mesec %q se duplira", dobijeno[i])
}
vidjeni[dobijeno[i]] = true
}
})
}
}
+4 -15
View File
@@ -2035,16 +2035,12 @@ func (h *Handler) PromeniStatus(w http.ResponseWriter, r *http.Request) {
if noviStatus == model.StatusPreuzeto {
nalog, _ := h.ServisRepo.DohvatiID(r.Context(), id)
if nalog != nil && nalog.CenaKonacna == nil {
// auto-izračunaj: dijagnostika + radovi + delovi
// auto-izračunaj: dijagnostika + radovi (delovi se računaju posebno)
radovi, _ := h.ServisniRadoviRepo.DohvatiZaNalog(r.Context(), id)
delovi, _ := h.ServisniDeloviRepo.DohvatiZaNalog(r.Context(), id)
ukupno := nalog.CenaDijagnostike
for _, rad := range radovi {
ukupno += rad.Ukupno()
}
for _, d := range delovi {
ukupno += d.Ukupno()
}
h.ServisRepo.AzurirajCenuKonacnu(r.Context(), id, ukupno)
nalog.CenaKonacna = &ukupno
}
@@ -2115,25 +2111,18 @@ func (h *Handler) PromeniStatus(w http.ResponseWriter, r *http.Request) {
Izvor: "servis",
IzvorID: &id,
}
// cena_komada rada/dela je NETO; PDV se dodaje naviše po stvarnoj stopi
for _, r := range radovi {
if r.Predlozeno {
continue
}
osnovica := r.Ukupno() / 1.2 // 20% PDV
pdv := r.Ukupno() - osnovica
kir.OsnovicaOpsta += osnovica
kir.PdvOpsta += pdv
kir.Ukupno += r.Ukupno()
kir.DodajNeto(r.Ukupno(), r.PdvStopa)
}
for _, d := range delovi {
if d.Predlozeno {
continue
}
osnovica := d.Ukupno() / 1.2
pdv := d.Ukupno() - osnovica
kir.OsnovicaOpsta += osnovica
kir.PdvOpsta += pdv
kir.Ukupno += d.Ukupno()
kir.DodajNeto(d.Ukupno(), d.PdvStopa)
}
if kir.Ukupno > 0 {
if _, e := h.PdvKirRepo.Kreiraj(r.Context(), &kir); e != nil {