Prodaja — ispravka duplikacije stavki, provera stanja po artiklu u realnom vremenu
This commit is contained in:
+102
-10
@@ -9,15 +9,89 @@ import (
|
||||
"ntech/internal/model"
|
||||
)
|
||||
|
||||
// Dashboard renderuje početnu stranicu
|
||||
// Dashboard renderuje početnu stranicu sa pravim podacima iz baze
|
||||
func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) {
|
||||
// čitamo sva podešavanja iz baze
|
||||
podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||
ctx := r.Context()
|
||||
|
||||
podesavanja, err := sqlite.DohvatiSvaPodesavanja(ctx, h.DB)
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var brojArtikala, aktivniServisi, prodajaOvogMeseca, kriticnaZaliha int
|
||||
|
||||
if err := h.DB.QueryRowContext(ctx,
|
||||
"SELECT COUNT(*) FROM artikli",
|
||||
).Scan(&brojArtikala); err != nil {
|
||||
log.Printf("dashboard: broj artikala: %v", err)
|
||||
}
|
||||
|
||||
if err := h.DB.QueryRowContext(ctx, `
|
||||
SELECT COUNT(*) FROM servisni_nalozi
|
||||
WHERE status NOT IN ('Završeno', 'Preuzeto')`,
|
||||
).Scan(&aktivniServisi); err != nil {
|
||||
log.Printf("dashboard: aktivni servisi: %v", err)
|
||||
}
|
||||
|
||||
if err := h.DB.QueryRowContext(ctx, `
|
||||
SELECT COUNT(*) FROM prodajni_nalozi
|
||||
WHERE strftime('%Y-%m', datum) = strftime('%Y-%m', 'now', 'localtime')`,
|
||||
).Scan(&prodajaOvogMeseca); err != nil {
|
||||
log.Printf("dashboard: prodaja ovog meseca: %v", err)
|
||||
}
|
||||
|
||||
if err := h.DB.QueryRowContext(ctx,
|
||||
"SELECT COUNT(*) FROM artikli WHERE kolicina <= kolicina_min",
|
||||
).Scan(&kriticnaZaliha); err != nil {
|
||||
log.Printf("dashboard: kriticna zaliha: %v", err)
|
||||
}
|
||||
|
||||
// poslednjih 5 servisnih naloga
|
||||
servisRedovi, err := h.DB.QueryContext(ctx, `
|
||||
SELECT uredjaj, status FROM servisni_nalozi
|
||||
ORDER BY datum_prijema DESC LIMIT 5`)
|
||||
if err != nil {
|
||||
log.Printf("dashboard: poslednji servisi: %v", err)
|
||||
}
|
||||
|
||||
var poslednjiServisi []model.StavkaServisa
|
||||
if servisRedovi != nil {
|
||||
defer servisRedovi.Close()
|
||||
for servisRedovi.Next() {
|
||||
var s model.StavkaServisa
|
||||
if err := servisRedovi.Scan(&s.Uredjaj, &s.Status); err == nil {
|
||||
s.BojaTacke = bojaTackeServisa(s.Status)
|
||||
poslednjiServisi = append(poslednjiServisi, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// artikli sa kritičnom zalihom, sortirani po količini rastuće
|
||||
zaliheRedovi, err := h.DB.QueryContext(ctx, `
|
||||
SELECT naziv, kolicina FROM artikli
|
||||
WHERE kolicina <= kolicina_min
|
||||
ORDER BY kolicina ASC LIMIT 5`)
|
||||
if err != nil {
|
||||
log.Printf("dashboard: kriticne zalihe: %v", err)
|
||||
}
|
||||
|
||||
var kriticneZalihe []model.StavkaZalihe
|
||||
if zaliheRedovi != nil {
|
||||
defer zaliheRedovi.Close()
|
||||
for zaliheRedovi.Next() {
|
||||
var z model.StavkaZalihe
|
||||
if err := zaliheRedovi.Scan(&z.Naziv, &z.Kolicina); err == nil {
|
||||
if z.Kolicina == 0 {
|
||||
z.BojaTacke = "#dc2626"
|
||||
} else {
|
||||
z.BojaTacke = "#f97316"
|
||||
}
|
||||
kriticneZalihe = append(kriticneZalihe, z)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
podaci := model.PodaciDashboarda{
|
||||
PodaciStranice: model.PodaciStranice{
|
||||
Stranica: "dashboard",
|
||||
@@ -29,12 +103,12 @@ func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) {
|
||||
LogoPutanja: podesavanja["logo_putanja"],
|
||||
Korisnik: "Admin",
|
||||
},
|
||||
BrojArtikala: 0,
|
||||
AktivniServisi: 0,
|
||||
ProdajaOvogMeseca: 0,
|
||||
KriticnaZaliha: 0,
|
||||
PoslednjiServisi: []model.StavkaServisa{},
|
||||
KriticneZalihe: []model.StavkaZalihe{},
|
||||
BrojArtikala: brojArtikala,
|
||||
AktivniServisi: aktivniServisi,
|
||||
ProdajaOvogMeseca: prodajaOvogMeseca,
|
||||
KriticnaZaliha: kriticnaZaliha,
|
||||
PoslednjiServisi: poslednjiServisi,
|
||||
KriticneZalihe: kriticneZalihe,
|
||||
}
|
||||
|
||||
tmpl, err := template.ParseFiles(
|
||||
@@ -44,6 +118,7 @@ func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) {
|
||||
"web/templates/stranice/dashboard.html",
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("greška pri učitavanju šablona: %v", err)
|
||||
http.Error(w, "Greška pri učitavanju stranice", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
@@ -51,6 +126,23 @@ func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) {
|
||||
if err := tmpl.ExecuteTemplate(w, "base", podaci); err != nil {
|
||||
log.Printf("greška pri renderovanju: %v", err)
|
||||
http.Error(w, "Greška pri prikazu stranice", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// bojaTackeServisa vraća hex boju tačke prema statusu naloga
|
||||
func bojaTackeServisa(status string) string {
|
||||
switch status {
|
||||
case "U dijagnostici":
|
||||
return "#3b82f6"
|
||||
case "Čeka delove":
|
||||
return "#f97316"
|
||||
case "U popravci":
|
||||
return "#ca8a04"
|
||||
case "Završeno":
|
||||
return "#16a34a"
|
||||
case "Preuzeto":
|
||||
return "#15803d"
|
||||
default:
|
||||
return "#94a3b8"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,16 +44,17 @@ type PodaciDetaljiProdaje struct {
|
||||
Sacuvano bool
|
||||
}
|
||||
|
||||
// artikalUJSONSaCenom pretvara listu artikala u template.JS vrednost sa prodajnom cenom
|
||||
// artikalUJSONSaCenom pretvara listu artikala u template.JS vrednost sa prodajnom cenom i stanjem
|
||||
func artikalUJSONSaCenom(artikli []model.ArtikalSaKategorijom) template.JS {
|
||||
type stavka struct {
|
||||
ID int64 `json:"id"`
|
||||
Naziv string `json:"naziv"`
|
||||
Cena float64 `json:"cena"`
|
||||
ID int64 `json:"id"`
|
||||
Naziv string `json:"naziv"`
|
||||
Cena float64 `json:"cena"`
|
||||
Kolicina int `json:"kolicina"`
|
||||
}
|
||||
lista := make([]stavka, 0, len(artikli))
|
||||
for _, a := range artikli {
|
||||
lista = append(lista, stavka{ID: a.ID, Naziv: a.Naziv, Cena: a.ProdajnaCena})
|
||||
lista = append(lista, stavka{ID: a.ID, Naziv: a.Naziv, Cena: a.ProdajnaCena, Kolicina: a.Kolicina})
|
||||
}
|
||||
b, _ := json.Marshal(lista)
|
||||
return template.JS(b)
|
||||
|
||||
@@ -49,9 +49,14 @@
|
||||
<div style="width:100%;"
|
||||
x-data="{
|
||||
stavke: [{artikal_id: '', kolicina: 1, cena: 0}],
|
||||
|
||||
artikliOpcije: _ntechArtikli,
|
||||
isMobile: window.matchMedia('(max-width: 768px)').matches,
|
||||
|
||||
init() {
|
||||
window.matchMedia('(max-width: 768px)').addEventListener('change', e => {
|
||||
this.isMobile = e.matches;
|
||||
});
|
||||
},
|
||||
dodajStavku() {
|
||||
this.stavke.push({artikal_id: '', kolicina: 1, cena: 0});
|
||||
},
|
||||
@@ -199,7 +204,8 @@
|
||||
<template x-for="(stavka, i) in stavke" :key="i">
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:8px 10px;">
|
||||
<select :name="'artikal_id[]'" x-model="stavka.artikal_id" style="width:100%;">
|
||||
<select :name="'artikal_id[]'" x-model="stavka.artikal_id"
|
||||
:disabled="isMobile" style="width:100%;">
|
||||
<option value="">— odaberi artikal —</option>
|
||||
<template x-for="a in artikliOpcije" :key="a.id">
|
||||
<option :value="a.id" x-text="a.naziv"></option>
|
||||
@@ -208,11 +214,11 @@
|
||||
</td>
|
||||
<td style="padding:8px 10px;">
|
||||
<input type="number" :name="'kolicina[]'" x-model="stavka.kolicina"
|
||||
min="1" style="width:100%;text-align:center;">
|
||||
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"
|
||||
min="0" step="0.01" style="width:100%;text-align:right;">
|
||||
min="0" step="0.01" :disabled="isMobile" style="width:100%;text-align:right;">
|
||||
</td>
|
||||
<td style="padding:8px 10px;text-align:right;font-size:14px;font-weight:500;color:var(--tekst-glavni);">
|
||||
<span x-text="ukupnoStavke(stavka) + ' din'"></span>
|
||||
@@ -256,7 +262,8 @@
|
||||
<div style="display:flex;flex-direction:column;gap:10px;">
|
||||
<div>
|
||||
<label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">Artikal</label>
|
||||
<select :name="'artikal_id[]'" x-model="stavka.artikal_id" style="width:100%;">
|
||||
<select :name="'artikal_id[]'" x-model="stavka.artikal_id"
|
||||
:disabled="!isMobile" style="width:100%;">
|
||||
<option value="">— odaberi artikal —</option>
|
||||
<template x-for="a in artikliOpcije" :key="a.id">
|
||||
<option :value="a.id" x-text="a.naziv"></option>
|
||||
@@ -266,11 +273,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" style="width:100%;">
|
||||
<input type="number" :name="'kolicina[]'" x-model="stavka.kolicina" 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" min="0" step="0.01" style="width:100%;">
|
||||
<input type="number" :name="'cena_po_komadu[]'" x-model="stavka.cena" min="0" step="0.01" :disabled="!isMobile" style="width:100%;">
|
||||
</div>
|
||||
</div>
|
||||
<div style="text-align:right;font-size:14px;font-weight:500;color:var(--tekst-glavni);">
|
||||
|
||||
@@ -39,7 +39,13 @@
|
||||
x-data="{
|
||||
stavke: [{artikal_id: '', kolicina: 1, cena: 0}],
|
||||
artikliOpcije: _ntechArtikli,
|
||||
isMobile: window.matchMedia('(max-width: 768px)').matches,
|
||||
|
||||
init() {
|
||||
window.matchMedia('(max-width: 768px)').addEventListener('change', e => {
|
||||
this.isMobile = e.matches;
|
||||
});
|
||||
},
|
||||
dodajStavku() {
|
||||
this.stavke.push({artikal_id: '', kolicina: 1, cena: 0});
|
||||
},
|
||||
@@ -50,6 +56,23 @@
|
||||
const a = this.artikliOpcije.find(x => x.id == stavka.artikal_id);
|
||||
if (a) stavka.cena = a.cena;
|
||||
},
|
||||
dostupnaKolicina(i) {
|
||||
const stavka = this.stavke[i];
|
||||
if (!stavka.artikal_id) return null;
|
||||
const a = this.artikliOpcije.find(x => x.id == stavka.artikal_id);
|
||||
if (!a) return null;
|
||||
const ostale = this.stavke.reduce((sum, s, j) =>
|
||||
sum + (j !== i && s.artikal_id == stavka.artikal_id ? (parseInt(s.kolicina) || 0) : 0), 0);
|
||||
return a.kolicina - ostale;
|
||||
},
|
||||
prekoracenje(i) {
|
||||
const d = this.dostupnaKolicina(i);
|
||||
if (d === null) return false;
|
||||
return (parseInt(this.stavke[i].kolicina) || 0) > d;
|
||||
},
|
||||
imaPrekoracenja() {
|
||||
return this.stavke.some((_, i) => this.prekoracenje(i));
|
||||
},
|
||||
ukupnoStavke(s) {
|
||||
return (parseFloat(s.kolicina) * parseFloat(s.cena) || 0).toFixed(2);
|
||||
},
|
||||
@@ -130,6 +153,7 @@
|
||||
<td style="padding:8px 10px;">
|
||||
<select :name="'artikal_id[]'" x-model="stavka.artikal_id"
|
||||
@change="popuniCenu(stavka)"
|
||||
:disabled="isMobile"
|
||||
style="width:100%;">
|
||||
<option value="">— odaberi artikal —</option>
|
||||
<template x-for="a in artikliOpcije" :key="a.id">
|
||||
@@ -139,11 +163,21 @@
|
||||
</td>
|
||||
<td style="padding:8px 10px;">
|
||||
<input type="number" :name="'kolicina[]'" x-model="stavka.kolicina"
|
||||
min="1" style="width:100%;text-align:center;">
|
||||
min="1" :disabled="isMobile"
|
||||
:style="prekoracenje(i) ? 'border-color:#dc2626;' : ''"
|
||||
style="width:100%;text-align:center;">
|
||||
<div x-show="stavka.artikal_id"
|
||||
:style="prekoracenje(i) ? 'color:#dc2626' : 'color:var(--tekst-sporedni)'"
|
||||
x-text="'Na stanju: ' + dostupnaKolicina(i)"
|
||||
style="font-size:11px;margin-top:3px;text-align:center;white-space:nowrap;"></div>
|
||||
<div x-show="prekoracenje(i)"
|
||||
style="font-size:11px;color:#dc2626;margin-top:1px;text-align:center;white-space:nowrap;">
|
||||
Prekoračena količina
|
||||
</div>
|
||||
</td>
|
||||
<td style="padding:8px 10px;">
|
||||
<input type="number" :name="'cena_po_komadu[]'" x-model="stavka.cena"
|
||||
min="0" step="0.01" style="width:100%;text-align:right;">
|
||||
min="0" step="0.01" :disabled="isMobile" style="width:100%;text-align:right;">
|
||||
</td>
|
||||
<td style="padding:8px 10px;text-align:right;font-size:14px;font-weight:500;color:var(--tekst-glavni);">
|
||||
<span x-text="ukupnoStavke(stavka) + ' din'"></span>
|
||||
@@ -189,6 +223,7 @@
|
||||
<label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">Artikal</label>
|
||||
<select :name="'artikal_id[]'" x-model="stavka.artikal_id"
|
||||
@change="popuniCenu(stavka)"
|
||||
:disabled="!isMobile"
|
||||
style="width:100%;">
|
||||
<option value="">— odaberi artikal —</option>
|
||||
<template x-for="a in artikliOpcije" :key="a.id">
|
||||
@@ -199,11 +234,18 @@
|
||||
<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" style="width:100%;">
|
||||
<input type="number" :name="'kolicina[]'" x-model="stavka.kolicina" min="1" :disabled="!isMobile"
|
||||
:style="prekoracenje(i) ? 'border-color:#dc2626;' : ''"
|
||||
style="width:100%;">
|
||||
<div x-show="stavka.artikal_id"
|
||||
:style="prekoracenje(i) ? 'color:#dc2626' : 'color:var(--tekst-sporedni)'"
|
||||
x-text="'Na stanju: ' + dostupnaKolicina(i)"
|
||||
style="font-size:11px;margin-top:3px;"></div>
|
||||
<div x-show="prekoracenje(i)" style="font-size:11px;color:#dc2626;margin-top:1px;">Prekoračena količina</div>
|
||||
</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" min="0" step="0.01" style="width:100%;">
|
||||
<input type="number" :name="'cena_po_komadu[]'" x-model="stavka.cena" min="0" step="0.01" :disabled="!isMobile" style="width:100%;">
|
||||
</div>
|
||||
</div>
|
||||
<div style="text-align:right;font-size:14px;font-weight:500;color:var(--tekst-glavni);">
|
||||
@@ -226,8 +268,11 @@
|
||||
Odustani
|
||||
</a>
|
||||
<button type="submit"
|
||||
:disabled="imaPrekoracenja()"
|
||||
:style="imaPrekoracenja() ? 'opacity:0.4;cursor:not-allowed;' : ''"
|
||||
style="padding:9px 20px;background:var(--sb-akcent);color:#fff;border:none;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;transition:opacity 0.2s;"
|
||||
onmouseover="this.style.opacity='0.85'" onmouseout="this.style.opacity='1'">
|
||||
@mouseover="if(!imaPrekoracenja()) $el.style.opacity='0.85'"
|
||||
@mouseout="$el.style.opacity=imaPrekoracenja()?'0.4':'1'">
|
||||
Sačuvaj prodaju
|
||||
</button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user