diff --git a/internal/db/sqlite/servisni_delovi.go b/internal/db/sqlite/servisni_delovi.go index 280531b..5a2c3a6 100644 --- a/internal/db/sqlite/servisni_delovi.go +++ b/internal/db/sqlite/servisni_delovi.go @@ -5,7 +5,6 @@ import ( "database/sql" "fmt" - "ntech/internal/db" "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) } - if stanjePre < kolicina { - return 0, &db.ErrNedovoljnoKolicine{ArtikalNaziv: naziv} - } - + // dozvoljavamo backorder: deo se može ugraditi i kad nema dovoljno na stanju + // (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 _, err = tx.ExecContext(ctx, "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 } - diff --git a/internal/handler/servis.go b/internal/handler/servis.go index b3a63fc..3c6e1e9 100644 --- a/internal/handler/servis.go +++ b/internal/handler/servis.go @@ -525,25 +525,31 @@ func (h *Handler) DodajDeloNalogu(w http.ResponseWriter, r *http.Request) { return } - // ako tražena količina prelazi magacinsko stanje, deo se NE ugrađuje (da lager - // ne ode u minus), a nalog se automatski prebacuje u „Čeka delove" radi nabavke - if art, e := h.Artikli.DohvatiID(r.Context(), artikalID); e == nil && art != nil && kolicina > art.Kolicina { - 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", - "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 + // prepoznaj prekoračenje magacinskog stanja PRE dodavanja (posle dodavanja + // lager se smanji); deo se svejedno ugrađuje (dozvoljen backorder/minus lager) + prekoraceno := false + imeArtikla := "" + if art, e := h.Artikli.DohvatiID(r.Context(), artikalID); e == nil && art != nil { + prekoraceno = kolicina > art.Kolicina + imeArtikla = art.Naziv } if _, err := h.ServisniDeloviRepo.Dodaj(r.Context(), nalogID, artikalID, kolicina, cena, &k.ID); err != nil { 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) 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) } @@ -1180,6 +1186,23 @@ func (h *Handler) PromeniStatus(w http.ResponseWriter, r *http.Request) { http.Error(w, "Nepoznat status", http.StatusBadRequest) 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 { slog.Error("greška pri promeni statusa naloga", "id", id, "error", err) http.Error(w, "Greška pri promeni statusa", http.StatusInternalServerError) diff --git a/web/static/css/main.css b/web/static/css/main.css index a61d45e..9088a7f 100644 --- a/web/static/css/main.css +++ b/web/static/css/main.css @@ -1111,6 +1111,11 @@ select { .bez-anim .tabela tbody tr, .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. Kad korisnik odabere stil, animiraju se i redovi tabela i mobilne kartice. */ [data-animacija="bez"] .animiraj, diff --git a/web/templates/stranice/servis_detalji.html b/web/templates/stranice/servis_detalji.html index e58029a..52b0b78 100644 --- a/web/templates/stranice/servis_detalji.html +++ b/web/templates/stranice/servis_detalji.html @@ -260,11 +260,11 @@
-
+
- +
@@ -363,8 +363,19 @@ var artikalSel = document.getElementById('deo-artikal'); var cena = document.getElementById('deo-cena'); var umagacinu = document.getElementById('deo-umagacinu'); + var kol = document.getElementById('deo-kol'); + var stanjeIzabrano = 0; 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) { var kategs = []; ARTIKLI.forEach(function (a) { @@ -394,8 +405,11 @@ var c = parseFloat(opt.getAttribute('data-cena')); if (cena) cena.value = (!isNaN(c) && c > 0) ? c.toFixed(2) : '0'; 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); })(); {{end}}