PDV modul: uslovno prikazivanje + auto cena_konacna + stanje zaliha sa PDV-om

- PDV pravilo (Moduli.pdv) primenjeno na: magacin, usluge, prodaja_detalji, usluge_forma
- magacin: toggle cena bez/sa PDV u zaglavlju kolone, jedna kolona
- stanje_zaliha: Cena sa PDV i Vred. sa PDV kolone, tamne kartice za zaglavlje
- PromeniStatus: auto-izračunavanje cena_konacna (dijagnostika+radovi+delovi) pri Preuzeto
- migrations/088: popravlja postojeće Preuzeto naloge bez cene
- dashboard: prihod cifra sakrivena, hover 1s → prikaz
- izvestaji: aktivni servisi bez Preuzeto, COALESCE u SQL, 0→—
- izvestaji.html: vraćeno — umesto 0 din
This commit is contained in:
2026-06-27 00:26:46 +02:00
parent 84bb8aa158
commit 2c8b206ec2
13 changed files with 106 additions and 37 deletions
+1
View File
@@ -200,6 +200,7 @@ type ServisRepository interface {
AzurirajNalazDijagnostike(ctx context.Context, id int64, tekst string) error
AzurirajUradjeno(ctx context.Context, id int64, tekst string) error
AzurirajCenaDijagnostike(ctx context.Context, id int64, cena float64) error
AzurirajCenuKonacnu(ctx context.Context, id int64, cena float64) error
OdbijPopravku(ctx context.Context, id int64, cena float64) error
AzurirajKomentarKlijenta(ctx context.Context, id int64, tekst string) error
SacuvajOdlukuKlijenta(ctx context.Context, id int64, odluka string, odgovor string) error
+7 -4
View File
@@ -27,7 +27,7 @@ func (r *sqliteIzvestajRepo) BrojArtikala(ctx context.Context) (int, error) {
func (r *sqliteIzvestajRepo) BrojAktivnihServisa(ctx context.Context) (int, error) {
var n int
err := r.db.QueryRowContext(ctx,
`SELECT COUNT(*) FROM servisni_nalozi WHERE status != 'Završeno'`).Scan(&n)
`SELECT COUNT(*) FROM servisni_nalozi WHERE status NOT IN ('Završeno', 'Preuzeto')`).Scan(&n)
if err != nil {
return 0, fmt.Errorf("ntech: izvestaj.BrojAktivnihServisa: %w", err)
}
@@ -145,7 +145,7 @@ func (r *sqliteIzvestajRepo) MesecniPrihodProdaja(ctx context.Context) ([]model.
func (r *sqliteIzvestajRepo) MesecniPrihodServis(ctx context.Context) ([]model.MesecniIznos, error) {
return r.mesecniPrihod(ctx, `
SELECT substr(datum_zavrsetka, 1, 7), SUM(cena_konacna)
SELECT substr(datum_zavrsetka, 1, 7), SUM(COALESCE(cena_konacna, 0))
FROM servisni_nalozi
WHERE datum_zavrsetka IS NOT NULL
AND status = 'Preuzeto'
@@ -269,7 +269,9 @@ func (r *sqliteIzvestajRepo) StanjeZaliha(ctx context.Context) ([]model.StanjeZa
rows, err := r.db.QueryContext(ctx, `
SELECT a.naziv, COALESCE(a.sifra, ''), COALESCE(k.naziv, ''),
a.kolicina, a.kolicina_min, a.nabavna_cena, a.prodajna_cena,
a.kolicina * a.nabavna_cena AS vrednost
a.cena_sa_pdv,
a.kolicina * a.nabavna_cena AS vrednost,
a.kolicina * a.cena_sa_pdv AS vrednost_sa_pdv
FROM artikli a
LEFT JOIN kategorije k ON k.id = a.kategorija_id
ORDER BY k.naziv ASC, a.naziv ASC`,
@@ -283,7 +285,8 @@ func (r *sqliteIzvestajRepo) StanjeZaliha(ctx context.Context) ([]model.StanjeZa
for rows.Next() {
var s model.StanjeZalihaRed
if err := rows.Scan(&s.Naziv, &s.Sifra, &s.Kategorija,
&s.Kolicina, &s.KolicinMin, &s.NabavnaCena, &s.ProdajnaCena, &s.VrednostZalihe); err != nil {
&s.Kolicina, &s.KolicinMin, &s.NabavnaCena, &s.ProdajnaCena,
&s.CenaSaPdv, &s.VrednostZalihe, &s.VrednostSaPdv); err != nil {
return nil, fmt.Errorf("ntech: IzvestajRepo.StanjeZaliha: scan: %w", err)
}
lista = append(lista, s)
+10
View File
@@ -339,6 +339,16 @@ func (r *ServisRepo) AzurirajCenaDijagnostike(ctx context.Context, id int64, cen
return nil
}
func (r *ServisRepo) AzurirajCenuKonacnu(ctx context.Context, id int64, cena float64) error {
_, err := r.db.ExecContext(ctx,
"UPDATE servisni_nalozi SET cena_konacna = ? WHERE id = ?", cena, id,
)
if err != nil {
return fmt.Errorf("ntech: ServisRepo.AzurirajCenuKonacnu: %w", err)
}
return nil
}
// AzurirajKomentarKlijenta čuva poruku klijenta uz prihvatanje/odbijanje predloga
func (r *ServisRepo) AzurirajKomentarKlijenta(ctx context.Context, id int64, tekst string) error {
_, err := r.db.ExecContext(ctx,
+4
View File
@@ -266,6 +266,7 @@ type PodaciStanjaZaliha struct {
model.PodaciStranice
Zalihe []model.StanjeZalihaRed
UkupnaVrednost float64
UkupnaVrednostSaPdv float64
BrojArtikala int
}
@@ -282,8 +283,10 @@ func (h *Handler) StanjeZalihaIzvestaj(w http.ResponseWriter, r *http.Request) {
}
var ukupnaVrednost float64
var ukupnaVrednostSaPdv float64
for _, z := range zalihe {
ukupnaVrednost += z.VrednostZalihe
ukupnaVrednostSaPdv += z.VrednostSaPdv
}
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
@@ -295,6 +298,7 @@ func (h *Handler) StanjeZalihaIzvestaj(w http.ResponseWriter, r *http.Request) {
PodaciStranice: ps,
Zalihe: zalihe,
UkupnaVrednost: ukupnaVrednost,
UkupnaVrednostSaPdv: ukupnaVrednostSaPdv,
BrojArtikala: len(zalihe),
})
}
+16 -1
View File
@@ -1969,8 +1969,23 @@ func (h *Handler) PromeniStatus(w http.ResponseWriter, r *http.Request) {
return
}
// pri prelasku u Preuzeto — sačuvaj naplatu i fiskalizuj
// pri prelasku u Preuzeto — auto-izračunaj cenu_konacnu ako nije uneta, pa sačuvaj naplatu i fiskalizuj
if noviStatus == model.StatusPreuzeto {
nalog, _ := h.ServisRepo.DohvatiID(r.Context(), id)
if nalog != nil && nalog.CenaKonacna == nil {
// auto-izračunaj: dijagnostika + radovi + delovi
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
}
nacin := strings.TrimSpace(r.FormValue("nacin_placanja"))
if nacin == "" {
nacin = "Gotovina"
+2
View File
@@ -81,5 +81,7 @@ type StanjeZalihaRed struct {
KolicinMin int
NabavnaCena float64
ProdajnaCena float64
CenaSaPdv float64
VrednostZalihe float64 // kolicina × nabavna_cena
VrednostSaPdv float64 // kolicina × cena_sa_pdv
}
@@ -0,0 +1,8 @@
-- Popuni cena_konacna za postojeće „Preuzeto" naloge:
-- dijagnostika + radovi + delovi
UPDATE servisni_nalozi
SET cena_konacna = COALESCE(cena_dijagnostike, 0)
+ COALESCE((SELECT SUM(cena_komada * kolicina) FROM servis_radovi WHERE nalog_id = servisni_nalozi.id), 0)
+ COALESCE((SELECT SUM(cena_komada * kolicina) FROM servisni_delovi WHERE nalog_id = servisni_nalozi.id), 0)
WHERE status = 'Preuzeto'
AND cena_konacna IS NULL;
+10 -1
View File
@@ -12,6 +12,15 @@
.dash-kartica:nth-child(1) { animation-delay: 0.20s; }
.dash-kartica:nth-child(2) { animation-delay: 0.28s; }
.dash-kartica:nth-child(3) { animation-delay: 0.36s; }
/* prihod cifra — sakrivena, prikazuje se posle 1s hovera */
.prihod-cifra {
opacity: 0;
transition: opacity 0.3s ease;
}
.dash-stat:hover .prihod-cifra {
opacity: 1;
transition-delay: 1s;
}
</style>
{{ end }}
@@ -50,7 +59,7 @@
<path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6" />
</svg>
</div>
<div style="font-size:22px;font-weight:500;color:var(--tekst-glavni);">{{ dinariCeli .PrihodOvogMeseca }} din</div>
<div style="font-size:22px;font-weight:500;color:var(--tekst-glavni);"><span class="prihod-cifra">{{ dinariCeli .PrihodOvogMeseca }} din</span></div>
<div style="font-size:14px;color:var(--tekst-glavni);margin-top:4px;font-weight:500;">Prihod ovog meseca</div>
</a>
{{ end }}
+21 -11
View File
@@ -79,8 +79,9 @@
<th style="padding:12px 16px;text-align:left;font-size:12px;font-weight:500;color:var(--tekst-jak);">Naziv</th>
<th style="padding:12px 16px;text-align:left;font-size:12px;font-weight:500;color:var(--tekst-jak);">Kategorija</th>
<th style="padding:12px 16px;text-align:center;font-size:12px;font-weight:500;color:var(--tekst-jak);">Količina</th>
<th style="padding:12px 16px;text-align:right;font-size:12px;font-weight:500;color:var(--tekst-jak);">Prodajna</th>
{{if index .Moduli "pdv"}}<th style="padding:12px 16px;text-align:right;font-size:12px;font-weight:500;color:var(--tekst-jak);">Sa PDV-om</th>{{end}}
<th style="padding:12px 16px;text-align:right;font-size:12px;font-weight:500;color:var(--tekst-jak);cursor:pointer;white-space:nowrap;" id="cena-zaglavlje" onclick="toggleCenaPdv()">
Cena <span id="cena-toggle" style="font-size:10px;color:var(--sb-akcent);">▾ bez PDV</span>
</th>
<th style="padding:12px 16px;text-align:left;font-size:12px;font-weight:500;color:var(--tekst-jak);">Lokacija</th>
<th style="padding:12px 16px;text-align:center;font-size:12px;font-weight:500;color:var(--tekst-jak);">Akcije</th>
</tr>
@@ -102,8 +103,9 @@
<span style="font-size:12px;color:var(--tekst-sporedni);">{{if eq .Tip "usluga"}}usluga{{else}}trošak{{end}}</span>
{{end}}
</td>
<td style="padding:12px 16px;text-align:right;font-size:14px;color:var(--tekst-glavni);">{{dinari .ProdajnaCena}} din</td>
{{if index $.Moduli "pdv"}}<td style="padding:12px 16px;text-align:right;font-size:14px;color:var(--tekst-glavni);">{{dinari .CenaSaPdv}} din</td>{{end}}
<td style="padding:12px 16px;text-align:right;font-size:14px;color:var(--tekst-glavni);">
<span class="cena-bez-pdv">{{dinari .ProdajnaCena}}</span><span class="cena-sa-pdv" style="display:none">{{dinari .CenaSaPdv}}</span> din
</td>
<td style="padding:12px 16px;font-size:13px;color:var(--tekst-sporedni);">
{{if .Lokacija}}{{.Lokacija}}{{else}}—{{end}}
</td>
@@ -136,7 +138,7 @@
</tr>
{{else}}
<tr>
<td colspan="{{if index .Moduli "pdv"}}8{{else}}7{{end}}" style="padding:32px;text-align:center;font-size:14px;color:var(--tekst-sporedni);">
<td colspan="7" style="padding:32px;text-align:center;font-size:14px;color:var(--tekst-sporedni);">
{{if or $.Filter.Pretraga $.KategorijaIDStr $.Filter.SamoKriticni}}Za datu pretragu nema stavki u bazi.{{else}}Nema stavki. <a hx-boost="true" hx-target="#glavni-sadrzaj" hx-select="#glavni-sadrzaj" hx-swap="outerHTML" href="/magacin/novi?tip={{$.Tip}}" style="color:var(--sb-akcent);">Dodaj prvu.</a>{{end}}
</td>
</tr>
@@ -194,13 +196,9 @@
{{end}}
</div>
<div class="pomocni-tekst">
<span style="color:var(--tekst-glavni);font-weight:500;">Cena:</span> {{dinari .ProdajnaCena}} din
<span style="color:var(--tekst-glavni);font-weight:500;">Cena:</span>
<span class="cena-bez-pdv">{{dinari .ProdajnaCena}}</span><span class="cena-sa-pdv" style="display:none">{{dinari .CenaSaPdv}}</span> din
</div>
{{if index $.Moduli "pdv"}}
<div class="pomocni-tekst">
<span style="color:var(--tekst-glavni);font-weight:500;">Sa PDV:</span> {{dinari .CenaSaPdv}} din
</div>
{{end}}
{{if .Lokacija}}
<div class="pomocni-tekst">
<span style="color:var(--tekst-glavni);font-weight:500;">Lokacija:</span> {{.Lokacija}}
@@ -260,6 +258,18 @@ document.body.addEventListener('htmx:afterSwap', function (e) {
void rez.offsetHeight;
rez.classList.remove('bez-anim');
});
// toggle cena: bez PDV ↔ sa PDV
var cenaPdvRezim = false; // false = bez PDV
function toggleCenaPdv() {
cenaPdvRezim = !cenaPdvRezim;
var bez = document.querySelectorAll('.cena-bez-pdv');
var sa = document.querySelectorAll('.cena-sa-pdv');
var tgl = document.getElementById('cena-toggle');
for (var i = 0; i < bez.length; i++) { bez[i].style.display = cenaPdvRezim ? 'none' : ''; }
for (var j = 0; j < sa.length; j++) { sa[j].style.display = cenaPdvRezim ? '' : 'none'; }
if (tgl) tgl.textContent = cenaPdvRezim ? '▾ sa PDV' : '▾ bez PDV';
}
</script>
{{end}}
+4 -4
View File
@@ -192,9 +192,9 @@
">
Cena/kom
</th>
<th style="padding:10px 20px;text-align:center;font-size:12px;font-weight:500;color:var(--tekst-sporedni);width:80px;">
{{if index .Moduli "pdv"}}<th style="padding:10px 20px;text-align:center;font-size:12px;font-weight:500;color:var(--tekst-sporedni);width:80px;">
PDV %
</th>
</th>{{end}}
<th style="padding:10px 20px;text-align:right;font-size:12px;font-weight:500;color:var(--tekst-sporedni);width:120px;">
Ukupno
</th>
@@ -206,9 +206,9 @@
<td style="padding:10px 20px;font-size:14px;color:var(--tekst-glavni);">{{.ArtikalNaziv}}</td>
<td style="padding:10px 20px;text-align:center;font-size:14px;color:var(--tekst-glavni);">{{.Kolicina}}</td>
<td style="padding:10px 20px;text-align:right;font-size:14px;color:var(--tekst-sporedni);">{{dinari .CenaPoKomadu}} din</td>
<td style="padding:10px 20px;text-align:center;font-size:13px;color:var(--tekst-sporedni);">
{{if index $.Moduli "pdv"}}<td style="padding:10px 20px;text-align:center;font-size:13px;color:var(--tekst-sporedni);">
{{if .PdvStopa}}{{printf "%.0f" .PdvStopa}}%{{else}}—{{end}}
</td>
</td>{{end}}
<td style="padding:10px 20px;text-align:right;font-size:14px;font-weight:500;color:var(--tekst-glavni);">{{dinari .Ukupno}} din</td>
</tr>
{{end}}
+12 -7
View File
@@ -14,13 +14,13 @@
</a>
</div>
<div style="display:flex;gap:16px;flex-wrap:wrap;">
<div style="text-align:right;">
<div style="font-size:12px;color:var(--tekst-slabi);">Broj artikala</div>
<div style="font-size:18px;font-weight:600;color:var(--tekst-glavni);">{{.BrojArtikala}}</div>
<div style="text-align:right;background:rgba(0,0,0,0.35);padding:8px 14px;border-radius:8px;backdrop-filter:blur(4px);">
<div style="font-size:12px;color:rgba(255,255,255,0.8);">Broj artikala</div>
<div style="font-size:18px;font-weight:600;color:#fff;">{{.BrojArtikala}}</div>
</div>
<div style="text-align:right;">
<div style="font-size:12px;color:var(--tekst-slabi);">Ukupna vrednost zalihe</div>
<div style="font-size:18px;font-weight:600;color:var(--tekst-glavni);">{{dinari .UkupnaVrednost}} din</div>
<div style="text-align:right;background:rgba(0,0,0,0.35);padding:8px 14px;border-radius:8px;backdrop-filter:blur(4px);">
<div style="font-size:12px;color:rgba(255,255,255,0.8);">Ukupna vrednost zalihe</div>
<div style="font-size:18px;font-weight:600;color:#fff;">{{dinari .UkupnaVrednost}} din</div>
</div>
</div>
</div>
@@ -47,7 +47,9 @@
<th style="text-align:right;">Min.</th>
<th style="text-align:right;">Nab. cena</th>
<th style="text-align:right;">Prod. cena</th>
{{if index .Moduli "pdv"}}<th style="text-align:right;">Cena sa PDV</th>{{end}}
<th style="text-align:right;">Vrednost</th>
{{if index .Moduli "pdv"}}<th style="text-align:right;">Vred. sa PDV</th>{{end}}
</tr>
</thead>
<tbody>
@@ -60,14 +62,17 @@
<td style="text-align:right;font-size:12px;color:var(--tekst-slabi);">{{.KolicinMin}}</td>
<td style="text-align:right;font-family:monospace;font-size:12px;">{{dinari .NabavnaCena}}</td>
<td style="text-align:right;font-family:monospace;font-size:12px;">{{dinari .ProdajnaCena}}</td>
{{if index $.Moduli "pdv"}}<td style="text-align:right;font-family:monospace;font-size:12px;">{{dinari .CenaSaPdv}}</td>{{end}}
<td style="text-align:right;font-family:monospace;font-weight:500;">{{dinari .VrednostZalihe}}</td>
{{if index $.Moduli "pdv"}}<td style="text-align:right;font-family:monospace;font-weight:500;">{{dinari .VrednostSaPdv}}</td>{{end}}
</tr>
{{end}}
</tbody>
<tfoot>
<tr style="border-top:1.5px solid var(--ivica);font-weight:600;">
<td colspan="7" style="padding:10px 12px;font-size:13px;">Ukupna vrednost zalihe</td>
<td colspan="{{if index .Moduli "pdv"}}8{{else}}7{{end}}" style="padding:10px 12px;font-size:13px;">Ukupna vrednost zalihe</td>
<td style="text-align:right;padding:10px 12px;font-family:monospace;">{{dinari .UkupnaVrednost}} din</td>
{{if index .Moduli "pdv"}}<td style="text-align:right;padding:10px 12px;font-family:monospace;">{{dinari .UkupnaVrednostSaPdv}} din</td>{{end}}
</tr>
</tfoot>
</table>
+2 -2
View File
@@ -40,7 +40,7 @@
<th style="padding:12px 16px;text-align:left;font-size:12px;font-weight:500;color:var(--tekst-sporedni);">Naziv</th>
<th style="padding:12px 16px;text-align:left;font-size:12px;font-weight:500;color:var(--tekst-sporedni);">JM</th>
<th style="padding:12px 16px;text-align:right;font-size:12px;font-weight:500;color:var(--tekst-sporedni);">Cena</th>
<th style="padding:12px 16px;text-align:center;font-size:12px;font-weight:500;color:var(--tekst-sporedni);">PDV</th>
{{if index .Moduli "pdv"}}<th style="padding:12px 16px;text-align:center;font-size:12px;font-weight:500;color:var(--tekst-sporedni);">PDV</th>{{end}}
<th style="padding:12px 16px;text-align:center;font-size:12px;font-weight:500;color:var(--tekst-sporedni);">Akcije</th>
</tr>
</thead>
@@ -51,7 +51,7 @@
<td style="padding:12px 16px;font-size:14px;color:var(--tekst-glavni);font-weight:500;">{{.Naziv}}</td>
<td style="padding:12px 16px;font-size:13px;color:var(--tekst-sporedni);">{{.JedinicaMere}}</td>
<td style="padding:12px 16px;text-align:right;font-size:14px;color:var(--tekst-glavni);">{{dinari .Cena}} din</td>
<td style="padding:12px 16px;text-align:center;font-size:13px;color:var(--tekst-sporedni);">{{.PdvStopa}}%</td>
{{if index $.Moduli "pdv"}}<td style="padding:12px 16px;text-align:center;font-size:13px;color:var(--tekst-sporedni);">{{.PdvStopa}}%</td>{{end}}
<td style="padding:12px 16px;text-align:center;">
<div style="display:flex;align-items:center;justify-content:center;gap:8px;">
<a hx-boost="true" hx-target="#glavni-sadrzaj" hx-select="#glavni-sadrzaj" hx-swap="outerHTML" href="/usluge/izmeni/{{.ID}}" class="btn-primarno-malo">Izmeni</a>
+2
View File
@@ -50,6 +50,7 @@
<input type="number" name="cena" min="0" step="0.01"
value="{{if .Usluga.Cena}}{{.Usluga.Cena}}{{end}}" placeholder="0" style="width:100%;">
</div>
{{if index .Moduli "pdv"}}
<div>
<label class="polje-labela">PDV stopa</label>
{{$pdv := .Usluga.PdvStopa}}
@@ -59,6 +60,7 @@
<option value="0" {{if eq $pdv 0.0}}selected{{end}}>0%</option>
</select>
</div>
{{end}}
</div>
<div>