Servis: vizuelni indikator prekoracenja — backorder i crvena boja
Dozvoljen backorder ugradnje delova (lager ide u minus), handler prebacuje nalog u Ceka delove, a JS boji polja crveno kad je kolicina > stanje. CSS klasa .prekoraceno umesto inline stilova.
This commit is contained in:
@@ -5,7 +5,6 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"ntech/internal/db"
|
|
||||||
"ntech/internal/model"
|
"ntech/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -66,10 +65,9 @@ func (r *ServisniDeloviRepo) Dodaj(ctx context.Context, nalogID, artikalID int64
|
|||||||
return 0, fmt.Errorf("ntech: ServisniDeloviRepo.Dodaj: dohvati artikal: %w", err)
|
return 0, fmt.Errorf("ntech: ServisniDeloviRepo.Dodaj: dohvati artikal: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if stanjePre < kolicina {
|
// dozvoljavamo backorder: deo se može ugraditi i kad nema dovoljno na stanju
|
||||||
return 0, &db.ErrNedovoljnoKolicine{ArtikalNaziv: naziv}
|
// (lager tada ide u minus kao signal da treba nabavka); handler nalog prebacuje
|
||||||
}
|
// u „Čeka delove". Zato ovde NE odbijamo prekoračenje.
|
||||||
|
|
||||||
stanjePosle := stanjePre - kolicina
|
stanjePosle := stanjePre - kolicina
|
||||||
_, err = tx.ExecContext(ctx,
|
_, err = tx.ExecContext(ctx,
|
||||||
"UPDATE artikli SET kolicina = ? WHERE id = ?", stanjePosle, artikalID,
|
"UPDATE artikli SET kolicina = ? WHERE id = ?", stanjePosle, artikalID,
|
||||||
@@ -156,4 +154,3 @@ func (r *ServisniDeloviRepo) Obrisi(ctx context.Context, id int64, korisnikID *i
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+34
-11
@@ -525,25 +525,31 @@ func (h *Handler) DodajDeloNalogu(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// ako tražena količina prelazi magacinsko stanje, deo se NE ugrađuje (da lager
|
// prepoznaj prekoračenje magacinskog stanja PRE dodavanja (posle dodavanja
|
||||||
// ne ode u minus), a nalog se automatski prebacuje u „Čeka delove" radi nabavke
|
// lager se smanji); deo se svejedno ugrađuje (dozvoljen backorder/minus lager)
|
||||||
if art, e := h.Artikli.DohvatiID(r.Context(), artikalID); e == nil && art != nil && kolicina > art.Kolicina {
|
prekoraceno := false
|
||||||
if e := h.ServisRepo.AzurirajStatus(r.Context(), nalogID, model.StatusCekaDelove); e != nil {
|
imeArtikla := ""
|
||||||
slog.Error("greška pri prebacivanju naloga u Čeka delove", "error", e)
|
if art, e := h.Artikli.DohvatiID(r.Context(), artikalID); e == nil && art != nil {
|
||||||
}
|
prekoraceno = kolicina > art.Kolicina
|
||||||
middleware.SetFlash(w, r, h.DB, "greska",
|
imeArtikla = art.Naziv
|
||||||
"Nema dovoljno „"+art.Naziv+"\" na stanju (ima "+strconv.Itoa(art.Kolicina)+"). Nalog je prebačen u „Čeka delove\" — naručite deo.")
|
|
||||||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(nalogID, 10), http.StatusSeeOther)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := h.ServisniDeloviRepo.Dodaj(r.Context(), nalogID, artikalID, kolicina, cena, &k.ID); err != nil {
|
if _, err := h.ServisniDeloviRepo.Dodaj(r.Context(), nalogID, artikalID, kolicina, cena, &k.ID); err != nil {
|
||||||
slog.Error("greška pri dodavanju dela", "error", err)
|
slog.Error("greška pri dodavanju dela", "error", err)
|
||||||
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri dodavanju dela. Proverite stanje na magacinu.")
|
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri dodavanju dela.")
|
||||||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(nalogID, 10), http.StatusSeeOther)
|
http.Redirect(w, r, "/servis/"+strconv.FormatInt(nalogID, 10), http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// prekoračenje → nalog automatski u „Čeka delove" (signal da treba nabavka)
|
||||||
|
if prekoraceno {
|
||||||
|
if e := h.ServisRepo.AzurirajStatus(r.Context(), nalogID, model.StatusCekaDelove); e != nil {
|
||||||
|
slog.Error("greška pri prebacivanju naloga u Čeka delove", "error", e)
|
||||||
|
}
|
||||||
|
middleware.SetFlash(w, r, h.DB, "greska",
|
||||||
|
"Deo „"+imeArtikla+"\" je dodat iznad stanja na magacinu — nalog je prebačen u „Čeka delove\". Naručite deo.")
|
||||||
|
}
|
||||||
|
|
||||||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(nalogID, 10)+"?sacuvano=1", http.StatusSeeOther)
|
http.Redirect(w, r, "/servis/"+strconv.FormatInt(nalogID, 10)+"?sacuvano=1", http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1180,6 +1186,23 @@ func (h *Handler) PromeniStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, "Nepoznat status", http.StatusBadRequest)
|
http.Error(w, "Nepoznat status", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// zabrana izlaska iz „Čeka delove" dok god neki ugrađeni deo nema pokriće na
|
||||||
|
// zalihama (lager u minusu / backorder) — deo prvo treba nabaviti
|
||||||
|
if nalog, e := h.ServisRepo.DohvatiID(r.Context(), id); e == nil &&
|
||||||
|
nalog.Status == model.StatusCekaDelove && noviStatus != model.StatusCekaDelove {
|
||||||
|
var bezPokrica int
|
||||||
|
_ = h.DB.QueryRowContext(r.Context(),
|
||||||
|
`SELECT COUNT(*) FROM servisni_delovi sd JOIN artikli a ON a.id = sd.artikal_id
|
||||||
|
WHERE sd.nalog_id = ? AND a.kolicina < 0`, id).Scan(&bezPokrica)
|
||||||
|
if bezPokrica > 0 {
|
||||||
|
middleware.SetFlash(w, r, h.DB, "greska",
|
||||||
|
"Nalog ne može da napusti „Čeka delove\" dok ima ugrađenih delova kojih nema na stanju (lager u minusu). Dopunite zalihe nabavkom.")
|
||||||
|
http.Redirect(w, r, "/servis/"+strconv.FormatInt(id, 10), http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := h.ServisRepo.AzurirajStatus(r.Context(), id, noviStatus); err != nil {
|
if err := h.ServisRepo.AzurirajStatus(r.Context(), id, noviStatus); err != nil {
|
||||||
slog.Error("greška pri promeni statusa naloga", "id", id, "error", err)
|
slog.Error("greška pri promeni statusa naloga", "id", id, "error", err)
|
||||||
http.Error(w, "Greška pri promeni statusa", http.StatusInternalServerError)
|
http.Error(w, "Greška pri promeni statusa", http.StatusInternalServerError)
|
||||||
|
|||||||
@@ -1111,6 +1111,11 @@ select {
|
|||||||
.bez-anim .tabela tbody tr,
|
.bez-anim .tabela tbody tr,
|
||||||
.bez-anim.tabela tbody tr { animation: none !important; }
|
.bez-anim.tabela tbody tr { animation: none !important; }
|
||||||
|
|
||||||
|
/* vizuelni indikator prekoračenja magacinskog stanja (crvena boja) —
|
||||||
|
koristi se u JS (classList.toggle) na #deo-umagacinu i #deo-kol */
|
||||||
|
.prekoraceno { border-color: #dc2626 !important; }
|
||||||
|
#deo-umagacinu.prekoraceno { color: #dc2626; border-color: #dc2626; }
|
||||||
|
|
||||||
/* korisnikova preferencija animacije: body[data-animacija] nadjačava podrazumevano.
|
/* korisnikova preferencija animacije: body[data-animacija] nadjačava podrazumevano.
|
||||||
Kad korisnik odabere stil, animiraju se i redovi tabela i mobilne kartice. */
|
Kad korisnik odabere stil, animiraju se i redovi tabela i mobilne kartice. */
|
||||||
[data-animacija="bez"] .animiraj,
|
[data-animacija="bez"] .animiraj,
|
||||||
|
|||||||
@@ -260,11 +260,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">U magacinu</label>
|
<label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">U magacinu</label>
|
||||||
<div id="deo-umagacinu" style="font-size:14px;font-weight:500;color:var(--tekst-glavni);text-align:center;padding:8px 0;border:0.5px solid transparent;">—</div>
|
<div id="deo-umagacinu" style="width:100%;box-sizing:border-box;padding:8px 12px;border:0.5px solid var(--ivica);border-radius:8px;font-size:14px;font-weight:500;text-align:center;background:var(--pozadina);color:var(--tekst-glavni);">—</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">Kol.</label>
|
<label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">Kol.</label>
|
||||||
<input type="number" name="kolicina" id="deo-kol" min="1" value="1" style="width:100%;" required>
|
<input type="number" name="kolicina" id="deo-kol" min="1" value="1" style="width:100%;box-sizing:border-box;" required>
|
||||||
</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>
|
||||||
@@ -363,8 +363,19 @@
|
|||||||
var artikalSel = document.getElementById('deo-artikal');
|
var artikalSel = document.getElementById('deo-artikal');
|
||||||
var cena = document.getElementById('deo-cena');
|
var cena = document.getElementById('deo-cena');
|
||||||
var umagacinu = document.getElementById('deo-umagacinu');
|
var umagacinu = document.getElementById('deo-umagacinu');
|
||||||
|
var kol = document.getElementById('deo-kol');
|
||||||
|
var stanjeIzabrano = 0;
|
||||||
if (!artikalSel) return;
|
if (!artikalSel) return;
|
||||||
|
|
||||||
|
// boji „U magacinu" i polje količine crveno kada tražimo više nego što ima na
|
||||||
|
// stanju (dozvoljeno — backorder; nalog će preći u „Čeka delove" pri čuvanju);
|
||||||
|
// koristi CSS klasu .prekoraceno umesto inline stilova (main.css)
|
||||||
|
function osveziPrekoracenje() {
|
||||||
|
var preko = (parseInt(kol && kol.value, 10) || 0) > stanjeIzabrano && artikalSel.value;
|
||||||
|
if (umagacinu) umagacinu.classList.toggle('prekoraceno', preko);
|
||||||
|
if (kol) kol.classList.toggle('prekoraceno', preko);
|
||||||
|
}
|
||||||
|
|
||||||
if (kategSel) {
|
if (kategSel) {
|
||||||
var kategs = [];
|
var kategs = [];
|
||||||
ARTIKLI.forEach(function (a) {
|
ARTIKLI.forEach(function (a) {
|
||||||
@@ -394,8 +405,11 @@
|
|||||||
var c = parseFloat(opt.getAttribute('data-cena'));
|
var c = parseFloat(opt.getAttribute('data-cena'));
|
||||||
if (cena) cena.value = (!isNaN(c) && c > 0) ? c.toFixed(2) : '0';
|
if (cena) cena.value = (!isNaN(c) && c > 0) ? c.toFixed(2) : '0';
|
||||||
var st = parseInt(opt.getAttribute('data-stanje'), 10);
|
var st = parseInt(opt.getAttribute('data-stanje'), 10);
|
||||||
if (umagacinu) umagacinu.textContent = isNaN(st) ? '—' : st;
|
stanjeIzabrano = isNaN(st) ? 0 : st;
|
||||||
|
if (umagacinu) umagacinu.textContent = artikalSel.value ? stanjeIzabrano : '—';
|
||||||
|
osveziPrekoracenje();
|
||||||
});
|
});
|
||||||
|
if (kol) kol.addEventListener('input', osveziPrekoracenje);
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
Reference in New Issue
Block a user