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:
@@ -39,6 +39,8 @@ type PodaciDetaljiNabavke struct {
|
||||
model.PodaciStranice
|
||||
Nabavka model.Nabavka
|
||||
Stavke []model.StavkaSaArtiklom
|
||||
Troskovi []model.NabavkaTrosak
|
||||
UkupanTrosak float64
|
||||
DobavljacNaziv string
|
||||
}
|
||||
|
||||
@@ -259,6 +261,12 @@ func (h *Handler) DetaljiNabavke(w http.ResponseWriter, r *http.Request) {
|
||||
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)
|
||||
if err != nil {
|
||||
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.Stranica = "nabavke"
|
||||
ps.NaslovStranice = "Detalji nabavke"
|
||||
var ukupanTrosak float64
|
||||
for _, t := range troskovi {
|
||||
ukupanTrosak += t.Iznos
|
||||
}
|
||||
podaci := PodaciDetaljiNabavke{
|
||||
PodaciStranice: ps,
|
||||
Nabavka: *nabavka,
|
||||
Stavke: stavke,
|
||||
Troskovi: troskovi,
|
||||
UkupanTrosak: ukupanTrosak,
|
||||
DobavljacNaziv: dobavljacNaziv,
|
||||
}
|
||||
|
||||
|
||||
+40
-3
@@ -189,6 +189,8 @@ document.addEventListener('alpine:init', () => {
|
||||
stavke: [{artikal_id: '', kolicina: 1, cena: 0, marza: 0, prodajna: 0}],
|
||||
artikliOpcije: [],
|
||||
marzaDefault: 0,
|
||||
troskovi: [], // zavisni troškovi {naziv, iznos}
|
||||
metodRaspodele: 'vrednost', // 'vrednost' ili 'kolicina'
|
||||
isMobile: false,
|
||||
modal: false,
|
||||
modalUcitavanje: false,
|
||||
@@ -222,6 +224,7 @@ document.addEventListener('alpine:init', () => {
|
||||
},
|
||||
dodajStavku() {
|
||||
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
|
||||
pdvStopa(artikalId) {
|
||||
@@ -238,15 +241,49 @@ document.addEventListener('alpine:init', () => {
|
||||
}
|
||||
this.izracunajProdajnu(s)
|
||||
},
|
||||
// prodajna (sa PDV) = nabavna × (1 + marža/100) × (1 + pdvStopa/100), zaokruženo na 2 decimale
|
||||
izracunajProdajnu(s) {
|
||||
// ukupan zavisni trošak nabavke
|
||||
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 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 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) {
|
||||
if (this.stavke.length > 1) this.stavke.splice(i, 1)
|
||||
this.preracunajSve()
|
||||
},
|
||||
ukupnoStavke(s) {
|
||||
return (parseFloat(s.kolicina) * parseFloat(s.cena) || 0).toFixed(2)
|
||||
|
||||
@@ -122,6 +122,30 @@
|
||||
</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 -->
|
||||
<div class="kartica detalji-kartica animiraj" style="border-color:#dc262633;">
|
||||
<div style="display:flex;align-items:flex-start;gap:12px;flex-wrap:wrap;">
|
||||
|
||||
@@ -107,11 +107,12 @@
|
||||
</td>
|
||||
<td style="padding:8px 10px;">
|
||||
<input type="number" :name="'kolicina[]'" x-model="stavka.kolicina"
|
||||
@input="preracunajSve()"
|
||||
min="1" :disabled="isMobile" style="width:100%;text-align:center;">
|
||||
</td>
|
||||
<td style="padding:8px 10px;">
|
||||
<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;">
|
||||
</td>
|
||||
<td style="padding:8px 10px;">
|
||||
@@ -176,11 +177,11 @@
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
|
||||
<div>
|
||||
<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>
|
||||
<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 style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
|
||||
@@ -205,6 +206,50 @@
|
||||
</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 -->
|
||||
<div style="display:flex;justify-content:flex-end;gap:10px;">
|
||||
<a href="/nabavke" class="btn-sekundarno">Odustani</a>
|
||||
|
||||
Reference in New Issue
Block a user