feat(nabavka): UI zavisnih troškova — forma i prikaz u detaljima (Faza C, celina 2)

- forma nabavke: sekcija „Zavisni troškovi" (slobodne stavke naziv+iznos,
  dodavanje/uklanjanje) + izbor metoda raspodele (po vrednosti / po količini)
- JS: kalkNabavna (kalkulativna nabavna po stavci, prati server) i preracunajSve —
  promena troška/metoda/količine/cene preračunava prodajne svih stavki
- detalji nabavke: prikaz zavisnih troškova, metoda raspodele i ukupnog iznosa
- handler DetaljiNabavke dohvata troškove (DohvatiTroskove)
This commit is contained in:
2026-06-14 16:23:11 +02:00
parent 30db396ee6
commit 803e1f6341
4 changed files with 126 additions and 6 deletions
+14
View File
@@ -39,6 +39,8 @@ type PodaciDetaljiNabavke struct {
model.PodaciStranice model.PodaciStranice
Nabavka model.Nabavka Nabavka model.Nabavka
Stavke []model.StavkaSaArtiklom Stavke []model.StavkaSaArtiklom
Troskovi []model.NabavkaTrosak
UkupanTrosak float64
DobavljacNaziv string DobavljacNaziv string
} }
@@ -259,6 +261,12 @@ func (h *Handler) DetaljiNabavke(w http.ResponseWriter, r *http.Request) {
return return
} }
troskovi, err := h.NabavkeRepo.DohvatiTroskove(r.Context(), id)
if err != nil {
http.Error(w, "Greška pri učitavanju troškova", http.StatusInternalServerError)
return
}
podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB) podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
if err != nil { if err != nil {
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError) http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
@@ -277,10 +285,16 @@ func (h *Handler) DetaljiNabavke(w http.ResponseWriter, r *http.Request) {
ps := h.popuniPodaciStranice(r, podesavanja) ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "nabavke" ps.Stranica = "nabavke"
ps.NaslovStranice = "Detalji nabavke" ps.NaslovStranice = "Detalji nabavke"
var ukupanTrosak float64
for _, t := range troskovi {
ukupanTrosak += t.Iznos
}
podaci := PodaciDetaljiNabavke{ podaci := PodaciDetaljiNabavke{
PodaciStranice: ps, PodaciStranice: ps,
Nabavka: *nabavka, Nabavka: *nabavka,
Stavke: stavke, Stavke: stavke,
Troskovi: troskovi,
UkupanTrosak: ukupanTrosak,
DobavljacNaziv: dobavljacNaziv, DobavljacNaziv: dobavljacNaziv,
} }
+40 -3
View File
@@ -189,6 +189,8 @@ document.addEventListener('alpine:init', () => {
stavke: [{artikal_id: '', kolicina: 1, cena: 0, marza: 0, prodajna: 0}], stavke: [{artikal_id: '', kolicina: 1, cena: 0, marza: 0, prodajna: 0}],
artikliOpcije: [], artikliOpcije: [],
marzaDefault: 0, marzaDefault: 0,
troskovi: [], // zavisni troškovi {naziv, iznos}
metodRaspodele: 'vrednost', // 'vrednost' ili 'kolicina'
isMobile: false, isMobile: false,
modal: false, modal: false,
modalUcitavanje: false, modalUcitavanje: false,
@@ -222,6 +224,7 @@ document.addEventListener('alpine:init', () => {
}, },
dodajStavku() { dodajStavku() {
this.stavke.push({artikal_id: '', kolicina: 1, cena: 0, marza: this.marzaDefault, prodajna: 0}) this.stavke.push({artikal_id: '', kolicina: 1, cena: 0, marza: this.marzaDefault, prodajna: 0})
this.preracunajSve()
}, },
// PDV stopa izabranog artikla (iz JSON liste) — za obračun prodajne cene // PDV stopa izabranog artikla (iz JSON liste) — za obračun prodajne cene
pdvStopa(artikalId) { pdvStopa(artikalId) {
@@ -238,15 +241,49 @@ document.addEventListener('alpine:init', () => {
} }
this.izracunajProdajnu(s) this.izracunajProdajnu(s)
}, },
// prodajna (sa PDV) = nabavna × (1 + marža/100) × (1 + pdvStopa/100), zaokruženo na 2 decimale // ukupan zavisni trošak nabavke
izracunajProdajnu(s) { ukupanTrosak() {
return this.troskovi.reduce((z, t) => z + (parseFloat(t.iznos) || 0), 0)
},
// osnovica raspodele po izabranom metodu (zbir po svim stavkama)
osnovicaRaspodele() {
return this.stavke.reduce((z, s) => {
const kol = parseFloat(s.kolicina) || 0
return z + (this.metodRaspodele === 'kolicina' ? kol : kol * (parseFloat(s.cena) || 0))
}, 0)
},
// kalkulativna nabavna cena po komadu (fakturna + raspodeljeni trošak) — isto kao server
kalkNabavna(s) {
const cena = parseFloat(s.cena) || 0 const cena = parseFloat(s.cena) || 0
const kol = parseFloat(s.kolicina) || 0
const trosak = this.ukupanTrosak()
const osn = this.osnovicaRaspodele()
if (trosak <= 0 || osn <= 0 || kol <= 0) return cena
const baza = this.metodRaspodele === 'kolicina' ? kol : kol * cena
const trosakPoKomadu = (trosak * (baza / osn)) / kol
return Math.round((cena + trosakPoKomadu) * 100) / 100
},
// prodajna (sa PDV) = kalkulativna nabavna × (1 + marža/100) × (1 + pdvStopa/100)
izracunajProdajnu(s) {
const nabavna = this.kalkNabavna(s)
const marza = parseFloat(s.marza) || 0 const marza = parseFloat(s.marza) || 0
const pdv = this.pdvStopa(s.artikal_id) const pdv = this.pdvStopa(s.artikal_id)
s.prodajna = Math.round(cena * (1 + marza / 100) * (1 + pdv / 100) * 100) / 100 s.prodajna = Math.round(nabavna * (1 + marza / 100) * (1 + pdv / 100) * 100) / 100
},
// raspodela zavisi od svih stavki — promena troška/metoda/količine/cene preračunava sve
preracunajSve() {
this.stavke.forEach(s => this.izracunajProdajnu(s))
},
dodajTrosak() {
this.troskovi.push({naziv: '', iznos: 0})
},
ukloniTrosak(i) {
this.troskovi.splice(i, 1)
this.preracunajSve()
}, },
ukloniStavku(i) { ukloniStavku(i) {
if (this.stavke.length > 1) this.stavke.splice(i, 1) if (this.stavke.length > 1) this.stavke.splice(i, 1)
this.preracunajSve()
}, },
ukupnoStavke(s) { ukupnoStavke(s) {
return (parseFloat(s.kolicina) * parseFloat(s.cena) || 0).toFixed(2) return (parseFloat(s.kolicina) * parseFloat(s.cena) || 0).toFixed(2)
@@ -122,6 +122,30 @@
</div> </div>
</div> </div>
<!-- zavisni troškovi -->
{{if .Troskovi}}
<div class="kartica detalji-kartica animiraj" style="padding:0;overflow:hidden;">
<div style="padding:14px 16px;border-bottom:0.5px solid var(--ivica);display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px;">
<span style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">Zavisni troškovi</span>
<span class="pomocni-tekst">
Raspodela: {{if eq .Nabavka.MetodRaspodele "kolicina"}}po količini{{else}}po vrednosti stavke{{end}}
</span>
</div>
<div style="padding:8px 16px;">
{{range .Troskovi}}
<div style="display:flex;justify-content:space-between;align-items:center;padding:8px 0;border-bottom:0.5px solid var(--ivica);">
<span style="font-size:14px;color:var(--tekst-glavni);">{{.Naziv}}</span>
<span style="font-size:14px;color:var(--tekst-sporedni);">{{printf "%.2f" .Iznos}} din</span>
</div>
{{end}}
<div style="display:flex;justify-content:space-between;align-items:center;padding:10px 0;">
<span style="font-size:13px;font-weight:500;color:var(--tekst-sporedni);">Ukupno troškovi:</span>
<span style="font-size:15px;font-weight:600;color:var(--tekst-glavni);">{{printf "%.2f" .UkupanTrosak}} din</span>
</div>
</div>
</div>
{{end}}
<!-- zona za brisanje --> <!-- zona za brisanje -->
<div class="kartica detalji-kartica animiraj" style="border-color:#dc262633;"> <div class="kartica detalji-kartica animiraj" style="border-color:#dc262633;">
<div style="display:flex;align-items:flex-start;gap:12px;flex-wrap:wrap;"> <div style="display:flex;align-items:flex-start;gap:12px;flex-wrap:wrap;">
+48 -3
View File
@@ -107,11 +107,12 @@
</td> </td>
<td style="padding:8px 10px;"> <td style="padding:8px 10px;">
<input type="number" :name="'kolicina[]'" x-model="stavka.kolicina" <input type="number" :name="'kolicina[]'" x-model="stavka.kolicina"
@input="preracunajSve()"
min="1" :disabled="isMobile" style="width:100%;text-align:center;"> min="1" :disabled="isMobile" style="width:100%;text-align:center;">
</td> </td>
<td style="padding:8px 10px;"> <td style="padding:8px 10px;">
<input type="number" :name="'cena_po_komadu[]'" x-model="stavka.cena" <input type="number" :name="'cena_po_komadu[]'" x-model="stavka.cena"
@input="izracunajProdajnu(stavka)" @input="preracunajSve()"
min="0" step="0.01" :disabled="isMobile" style="width:100%;text-align:right;"> min="0" step="0.01" :disabled="isMobile" style="width:100%;text-align:right;">
</td> </td>
<td style="padding:8px 10px;"> <td style="padding:8px 10px;">
@@ -176,11 +177,11 @@
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;"> <div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
<div> <div>
<label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">Količina</label> <label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">Količina</label>
<input type="number" :name="'kolicina[]'" x-model="stavka.kolicina" min="1" :disabled="!isMobile" style="width:100%;"> <input type="number" :name="'kolicina[]'" x-model="stavka.kolicina" @input="preracunajSve()" min="1" :disabled="!isMobile" style="width:100%;">
</div> </div>
<div> <div>
<label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">Cena/kom (din)</label> <label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">Cena/kom (din)</label>
<input type="number" :name="'cena_po_komadu[]'" x-model="stavka.cena" @input="izracunajProdajnu(stavka)" min="0" step="0.01" :disabled="!isMobile" style="width:100%;"> <input type="number" :name="'cena_po_komadu[]'" x-model="stavka.cena" @input="preracunajSve()" min="0" step="0.01" :disabled="!isMobile" style="width:100%;">
</div> </div>
</div> </div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;"> <div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
@@ -205,6 +206,50 @@
</div> </div>
</div> </div>
<!-- zavisni troškovi -->
<div class="kartica forma-kartica animiraj" style="margin-bottom:16px;">
<div style="margin-bottom:16px;padding-bottom:14px;border-bottom:0.5px solid var(--ivica);display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px;">
<span style="font-size:16px;font-weight:500;color:var(--tekst-glavni);">Zavisni troškovi</span>
<button type="button" @click="dodajTrosak()" class="btn-primarno" style="font-size:13px;padding:6px 14px;">
+ Dodaj trošak
</button>
</div>
<template x-if="troskovi.length > 0">
<div class="kolona" style="gap:10px;">
<!-- metod raspodele -->
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;">
<label class="polje-labela" style="margin:0;">Raspodela na stavke:</label>
<select name="metod_raspodele" x-model="metodRaspodele" @change="preracunajSve()" style="max-width:240px;">
<option value="vrednost">po vrednosti stavke</option>
<option value="kolicina">po količini</option>
</select>
</div>
<!-- redovi troškova -->
<template x-for="(t, i) in troskovi" :key="i">
<div style="display:flex;gap:8px;align-items:center;">
<input type="text" :name="'trosak_naziv[]'" x-model="t.naziv"
placeholder="npr. Prevoz, Carina, Špedicija" style="flex:1;">
<input type="number" :name="'trosak_iznos[]'" x-model="t.iznos"
@input="preracunajSve()" min="0" step="0.01"
placeholder="iznos (din)" style="width:140px;text-align:right;">
<button type="button" @click="ukloniTrosak(i)"
style="background:none;border:none;cursor:pointer;color:#dc2626;font-size:18px;line-height:1;padding:2px 6px;border-radius:4px;"
title="Ukloni trošak">×</button>
</div>
</template>
<div style="text-align:right;font-size:13px;color:var(--tekst-sporedni);">
Ukupno troškovi: <strong x-text="ukupanTrosak().toFixed(2) + ' din'"></strong>
</div>
</div>
</template>
<template x-if="troskovi.length === 0">
<div style="font-size:13px;color:var(--tekst-sporedni);">
Nema zavisnih troškova. Dodaj trošak (prevoz, carina…) — raspodeliće se na stavke i ući u kalkulativnu nabavnu cenu.
</div>
</template>
</div>
<!-- dugmad forme --> <!-- dugmad forme -->
<div style="display:flex;justify-content:flex-end;gap:10px;"> <div style="display:flex;justify-content:flex-end;gap:10px;">
<a href="/nabavke" class="btn-sekundarno">Odustani</a> <a href="/nabavke" class="btn-sekundarno">Odustani</a>