Servis: potrazivani delovi, garancija, zakljucavanje statusa

Nova tabela servisni_potrazivani_delovi — visak preko stanja ide u potrazivane, lager NE ide u minus. Merge istog artikla na nalogu. Vizuelni indikator prekoracenja u tabeli (crveno). Status zakljucan na Ceka delove dok ima potrazivanih. Garancija: kalendar sa default vrednoscu. data-refresh podrzava vise selektora. Brisanje dela brise i potrazivane. Inline animation:none za AJAX refresh.
This commit is contained in:
2026-06-22 03:17:29 +02:00
parent c1ca619444
commit d4dcecb3ea
11 changed files with 475 additions and 155 deletions
+1
View File
@@ -359,6 +359,7 @@ func main() {
r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "servis.pregled")).Get("/servis/{id}/otpremnica", h.StampaOtpremnice) r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "servis.pregled")).Get("/servis/{id}/otpremnica", h.StampaOtpremnice)
r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "servis.pregled")).Get("/servis/{id}/predracun", h.StampaPredracuna) r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "servis.pregled")).Get("/servis/{id}/predracun", h.StampaPredracuna)
r.With(doz("servis.izmeni")).Post("/servis/{id}/status", h.PromeniStatus) r.With(doz("servis.izmeni")).Post("/servis/{id}/status", h.PromeniStatus)
r.With(doz("servis.izmeni")).Post("/servis/{id}/garancija", h.AzurirajGaranciju)
r.With(doz("servis.izmeni")).Post("/servis/{id}/delovi", h.DodajDeloNalogu) r.With(doz("servis.izmeni")).Post("/servis/{id}/delovi", h.DodajDeloNalogu)
r.With(doz("servis.izmeni")).Post("/servis/{id}/delovi/{deo_id}/obrisi", h.ObrisiDeloNaloga) r.With(doz("servis.izmeni")).Post("/servis/{id}/delovi/{deo_id}/obrisi", h.ObrisiDeloNaloga)
r.With(doz("servis.izmeni")).Post("/servis/{id}/radovi", h.DodajRadNalogu) r.With(doz("servis.izmeni")).Post("/servis/{id}/radovi", h.DodajRadNalogu)
+10
View File
@@ -191,6 +191,7 @@ type ServisRepository interface {
Kreiraj(ctx context.Context, n *model.ServisniNalog) (int64, error) Kreiraj(ctx context.Context, n *model.ServisniNalog) (int64, error)
Izmeni(ctx context.Context, n *model.ServisniNalog) error Izmeni(ctx context.Context, n *model.ServisniNalog) error
AzurirajStatus(ctx context.Context, id int64, status string) error AzurirajStatus(ctx context.Context, id int64, status string) error
AzurirajGaranciju(ctx context.Context, id int64, garancijaDo *time.Time) error
Obrisi(ctx context.Context, id int64) error Obrisi(ctx context.Context, id int64) error
SledeciBroj(ctx context.Context) (string, error) SledeciBroj(ctx context.Context) (string, error)
} }
@@ -210,9 +211,18 @@ type ProdajaRepository interface {
type ServisniDeloviRepository interface { type ServisniDeloviRepository interface {
DohvatiZaNalog(ctx context.Context, nalogID int64) ([]model.ServisniDeoSaArtiklom, error) DohvatiZaNalog(ctx context.Context, nalogID int64) ([]model.ServisniDeoSaArtiklom, error)
Dodaj(ctx context.Context, nalogID, artikalID int64, kolicina int, cenaKomada float64, korisnikID *int64) (int64, error) Dodaj(ctx context.Context, nalogID, artikalID int64, kolicina int, cenaKomada float64, korisnikID *int64) (int64, error)
DohvatiArtikalID(ctx context.Context, deoID int64) (int64, error)
Obrisi(ctx context.Context, id int64, korisnikID *int64) error Obrisi(ctx context.Context, id int64, korisnikID *int64) error
} }
// ServisniPotrazivaniDeloviRepository definiše operacije nad delovima koji nedostaju
type ServisniPotrazivaniDeloviRepository interface {
DohvatiZaNalog(ctx context.Context, nalogID int64) ([]model.ServisniPotrazivaniDeo, error)
DodajIliUvecaj(ctx context.Context, nalogID, artikalID int64, kolicina int) (int64, error)
Obrisi(ctx context.Context, id int64) error
ObrisiZaArtikal(ctx context.Context, nalogID, artikalID int64) error
}
// ServisniRadoviRepository definiše operacije nad radovima (uslugama) na nalogu // ServisniRadoviRepository definiše operacije nad radovima (uslugama) na nalogu
type ServisniRadoviRepository interface { type ServisniRadoviRepository interface {
DohvatiZaNalog(ctx context.Context, nalogID int64) ([]model.ServisniRad, error) DohvatiZaNalog(ctx context.Context, nalogID int64) ([]model.ServisniRad, error)
+22
View File
@@ -207,6 +207,28 @@ func (r *ServisRepo) AzurirajStatus(ctx context.Context, id int64, status string
return nil return nil
} }
// AzurirajGaranciju postavlja ili briše datum garancije na servisnom nalogu.
// garancijaDo == nil → bez garancije.
func (r *ServisRepo) AzurirajGaranciju(ctx context.Context, id int64, garancijaDo *time.Time) error {
if garancijaDo != nil {
_, err := r.db.ExecContext(ctx,
"UPDATE servisni_nalozi SET garancija_do = ? WHERE id = ?",
garancijaDo.Format("2006-01-02"), id,
)
if err != nil {
return fmt.Errorf("ntech: ServisRepo.AzurirajGaranciju: %w", err)
}
} else {
_, err := r.db.ExecContext(ctx,
"UPDATE servisni_nalozi SET garancija_do = NULL WHERE id = ?", id,
)
if err != nil {
return fmt.Errorf("ntech: ServisRepo.AzurirajGaranciju: %w", err)
}
}
return nil
}
// Obrisi briše servisni nalog po ID-u // Obrisi briše servisni nalog po ID-u
func (r *ServisRepo) Obrisi(ctx context.Context, id int64) error { func (r *ServisRepo) Obrisi(ctx context.Context, id int64) error {
_, err := r.db.ExecContext(ctx, "DELETE FROM servisni_nalozi WHERE id = ?", id) _, err := r.db.ExecContext(ctx, "DELETE FROM servisni_nalozi WHERE id = ?", id)
+12
View File
@@ -180,3 +180,15 @@ func (r *ServisniDeloviRepo) Obrisi(ctx context.Context, id int64, korisnikID *i
return nil return nil
} }
// DohvatiArtikalID vraća artikal_id za dati servisni deo (pre brisanja)
func (r *ServisniDeloviRepo) DohvatiArtikalID(ctx context.Context, deoID int64) (int64, error) {
var artikalID int64
err := r.db.QueryRowContext(ctx,
"SELECT artikal_id FROM servisni_delovi WHERE id = ?", deoID,
).Scan(&artikalID)
if err != nil {
return 0, fmt.Errorf("ntech: ServisniDeloviRepo.DohvatiArtikalID: %w", err)
}
return artikalID, nil
}
@@ -0,0 +1,107 @@
package sqlite
import (
"context"
"database/sql"
"errors"
"fmt"
"ntech/internal/model"
)
// ServisniPotrazivaniDeloviRepo je SQLite implementacija ServisniPotrazivaniDeloviRepository
type ServisniPotrazivaniDeloviRepo struct {
db *sql.DB
}
// NoviServisniPotrazivaniDeloviRepo kreira novi repo
func NoviServisniPotrazivaniDeloviRepo(db *sql.DB) *ServisniPotrazivaniDeloviRepo {
return &ServisniPotrazivaniDeloviRepo{db: db}
}
// DohvatiZaNalog vraća sve potraživane delove za dati servisni nalog
func (r *ServisniPotrazivaniDeloviRepo) DohvatiZaNalog(ctx context.Context, nalogID int64) ([]model.ServisniPotrazivaniDeo, error) {
redovi, err := r.db.QueryContext(ctx, `
SELECT spd.id, spd.nalog_id, spd.artikal_id, spd.kolicina, spd.datum,
a.naziv
FROM servisni_potrazivani_delovi spd
JOIN artikli a ON a.id = spd.artikal_id
WHERE spd.nalog_id = ?
ORDER BY spd.datum`, nalogID)
if err != nil {
return nil, fmt.Errorf("ntech: ServisniPotrazivaniDeloviRepo.DohvatiZaNalog: %w", err)
}
defer redovi.Close()
var rezultat []model.ServisniPotrazivaniDeo
for redovi.Next() {
var d model.ServisniPotrazivaniDeo
err := redovi.Scan(&d.ID, &d.NalogID, &d.ArtikalID, &d.Kolicina, &d.Datum, &d.ArtikalNaziv)
if err != nil {
return nil, fmt.Errorf("ntech: ServisniPotrazivaniDeloviRepo.DohvatiZaNalog: scan: %w", err)
}
rezultat = append(rezultat, d)
}
return rezultat, nil
}
// DodajIliUvecaj dodaje novi potraživani deo ili uvećava količinu ako već postoji.
// Vraća ID reda (novog ili postojećeg).
func (r *ServisniPotrazivaniDeloviRepo) DodajIliUvecaj(ctx context.Context, nalogID, artikalID int64, kolicina int) (int64, error) {
var postojeciID int64
var postojeciKol int
err := r.db.QueryRowContext(ctx,
"SELECT id, kolicina FROM servisni_potrazivani_delovi WHERE nalog_id = ? AND artikal_id = ?",
nalogID, artikalID,
).Scan(&postojeciID, &postojeciKol)
if err == nil {
novaKol := postojeciKol + kolicina
_, err = r.db.ExecContext(ctx,
"UPDATE servisni_potrazivani_delovi SET kolicina = ? WHERE id = ?",
novaKol, postojeciID,
)
if err != nil {
return 0, fmt.Errorf("ntech: ServisniPotrazivaniDeloviRepo.DodajIliUvecaj: update: %w", err)
}
return postojeciID, nil
}
if errors.Is(err, sql.ErrNoRows) {
rez, err := r.db.ExecContext(ctx,
"INSERT INTO servisni_potrazivani_delovi (nalog_id, artikal_id, kolicina) VALUES (?, ?, ?)",
nalogID, artikalID, kolicina,
)
if err != nil {
return 0, fmt.Errorf("ntech: ServisniPotrazivaniDeloviRepo.DodajIliUvecaj: insert: %w", err)
}
id, err := rez.LastInsertId()
if err != nil {
return 0, fmt.Errorf("ntech: ServisniPotrazivaniDeloviRepo.DodajIliUvecaj: last insert id: %w", err)
}
return id, nil
}
return 0, fmt.Errorf("ntech: ServisniPotrazivaniDeloviRepo.DodajIliUvecaj: proveri: %w", err)
}
// Obrisi uklanja potraživani deo po ID-u
func (r *ServisniPotrazivaniDeloviRepo) Obrisi(ctx context.Context, id int64) error {
_, err := r.db.ExecContext(ctx, "DELETE FROM servisni_potrazivani_delovi WHERE id = ?", id)
if err != nil {
return fmt.Errorf("ntech: ServisniPotrazivaniDeloviRepo.Obrisi: %w", err)
}
return nil
}
// ObrisiZaArtikal briše sve potraživane delove za dati artikal na datom nalogu
func (r *ServisniPotrazivaniDeloviRepo) ObrisiZaArtikal(ctx context.Context, nalogID, artikalID int64) error {
_, err := r.db.ExecContext(ctx,
"DELETE FROM servisni_potrazivani_delovi WHERE nalog_id = ? AND artikal_id = ?",
nalogID, artikalID,
)
if err != nil {
return fmt.Errorf("ntech: ServisniPotrazivaniDeloviRepo.ObrisiZaArtikal: %w", err)
}
return nil
}
+61 -58
View File
@@ -17,38 +17,39 @@ import (
// Handler drži zavisnosti koje su potrebne svim handlerima // Handler drži zavisnosti koje su potrebne svim handlerima
type Handler struct { type Handler struct {
DB *sql.DB DB *sql.DB
PutanjaBaze string PutanjaBaze string
Artikli db.ArtikalRepository Artikli db.ArtikalRepository
UslugeRepo db.UslugaRepository UslugeRepo db.UslugaRepository
TroskoviRepo db.TrosakRepository TroskoviRepo db.TrosakRepository
KategorijeRepo db.KategorijaRepository KategorijeRepo db.KategorijaRepository
DobavljaciRepo db.DobavljacRepository DobavljaciRepo db.DobavljacRepository
NabavkeRepo db.NabavkaRepository NabavkeRepo db.NabavkaRepository
KlijentiRepo db.KlijentRepository KlijentiRepo db.KlijentRepository
ServisRepo db.ServisRepository ServisRepo db.ServisRepository
ServisniDeloviRepo db.ServisniDeloviRepository ServisniDeloviRepo db.ServisniDeloviRepository
ServisniRadoviRepo db.ServisniRadoviRepository ServisniRadoviRepo db.ServisniRadoviRepository
MagacinskePromeneRepo db.MagacinskePromeneRepository ServisniPotrazivaniDeloviRepo db.ServisniPotrazivaniDeloviRepository
ProdajaRepo db.ProdajaRepository MagacinskePromeneRepo db.MagacinskePromeneRepository
KorisniciRepo db.KorisniciRepository ProdajaRepo db.ProdajaRepository
SesijeRepo db.SesijeRepository KorisniciRepo db.KorisniciRepository
PodsetnikRepo db.PodsetnikRepository SesijeRepo db.SesijeRepository
IzvestajRepo db.IzvestajRepository PodsetnikRepo db.PodsetnikRepository
RezervniKodoviRepo db.RezervniKodoviRepository IzvestajRepo db.IzvestajRepository
PokusajiRepo db.PokusajiPrijaveRepository RezervniKodoviRepo db.RezervniKodoviRepository
LoginIstorijsaRepo db.LoginIstorijsaRepository PokusajiRepo db.PokusajiPrijaveRepository
DozvoleRepo db.DozvoleRepository LoginIstorijsaRepo db.LoginIstorijsaRepository
PdvStopeRepo db.PdvStopaRepository DozvoleRepo db.DozvoleRepository
PdvKirRepo db.PdvKirRepository PdvStopeRepo db.PdvStopaRepository
PdvKprRepo db.PdvKprRepository PdvKirRepo db.PdvKirRepository
NivelacijaRepo db.NivelacijaRepository PdvKprRepo db.PdvKprRepository
Verzija string NivelacijaRepo db.NivelacijaRepository
JelDemo bool Verzija string
AssetV string // verzija statičkih fajlova za cache-busting (postavlja se pri pokretanju) JelDemo bool
Templates map[string]*template.Template AssetV string // verzija statičkih fajlova za cache-busting (postavlja se pri pokretanju)
TemplatesFS fs.FS Templates map[string]*template.Template
totpKljuc []byte // ključ za šifrovanje TOTP tajni (prosleđuje se KorisniciRepo) TemplatesFS fs.FS
totpKljuc []byte // ključ za šifrovanje TOTP tajni (prosleđuje se KorisniciRepo)
// mu štiti DB i sve repozitorijume od zamene u letu (obnova backupa). // mu štiti DB i sve repozitorijume od zamene u letu (obnova backupa).
// Zahtevi drže deljeno (read) zaključavanje preko ZakljucajCitanje, a obnova // Zahtevi drže deljeno (read) zaključavanje preko ZakljucajCitanje, a obnova
@@ -84,32 +85,33 @@ func (h *Handler) SaBazom(fn func(*sql.DB)) {
// totpKljuc je 32-bajtni ključ za šifrovanje TOTP tajni u mirovanju. // totpKljuc je 32-bajtni ključ za šifrovanje TOTP tajni u mirovanju.
func Novi(baza *sql.DB, totpKljuc []byte) *Handler { func Novi(baza *sql.DB, totpKljuc []byte) *Handler {
return &Handler{ return &Handler{
DB: baza, DB: baza,
totpKljuc: totpKljuc, totpKljuc: totpKljuc,
Artikli: sqlite.NoviArtikalRepo(baza), Artikli: sqlite.NoviArtikalRepo(baza),
UslugeRepo: sqlite.NoviUslugaRepo(baza), UslugeRepo: sqlite.NoviUslugaRepo(baza),
TroskoviRepo: sqlite.NoviTrosakRepo(baza), TroskoviRepo: sqlite.NoviTrosakRepo(baza),
KategorijeRepo: sqlite.NovaKategorijaRepo(baza), KategorijeRepo: sqlite.NovaKategorijaRepo(baza),
DobavljaciRepo: sqlite.NoviDobavljacRepo(baza), DobavljaciRepo: sqlite.NoviDobavljacRepo(baza),
NabavkeRepo: sqlite.NoviNabavkaRepo(baza), NabavkeRepo: sqlite.NoviNabavkaRepo(baza),
KlijentiRepo: sqlite.NoviKlijentRepo(baza), KlijentiRepo: sqlite.NoviKlijentRepo(baza),
ServisRepo: sqlite.NoviServisRepo(baza), ServisRepo: sqlite.NoviServisRepo(baza),
ServisniDeloviRepo: sqlite.NoviServisniDeloviRepo(baza), ServisniDeloviRepo: sqlite.NoviServisniDeloviRepo(baza),
ServisniRadoviRepo: sqlite.NoviServisniRadoviRepo(baza), ServisniRadoviRepo: sqlite.NoviServisniRadoviRepo(baza),
MagacinskePromeneRepo: sqlite.NoviMagacinskePromeneRepo(baza), ServisniPotrazivaniDeloviRepo: sqlite.NoviServisniPotrazivaniDeloviRepo(baza),
ProdajaRepo: sqlite.NoviProdajaRepo(baza), MagacinskePromeneRepo: sqlite.NoviMagacinskePromeneRepo(baza),
KorisniciRepo: sqlite.NoviKorisniciRepo(baza, totpKljuc), ProdajaRepo: sqlite.NoviProdajaRepo(baza),
SesijeRepo: sqlite.NoviSesijeRepo(baza), KorisniciRepo: sqlite.NoviKorisniciRepo(baza, totpKljuc),
PodsetnikRepo: sqlite.NoviPodsetnikRepo(baza), SesijeRepo: sqlite.NoviSesijeRepo(baza),
IzvestajRepo: sqlite.NoviIzvestajRepo(baza), PodsetnikRepo: sqlite.NoviPodsetnikRepo(baza),
RezervniKodoviRepo: sqlite.NoviRezervniKodoviRepo(baza), IzvestajRepo: sqlite.NoviIzvestajRepo(baza),
PokusajiRepo: sqlite.NoviPokusajiPrijaveRepo(baza), RezervniKodoviRepo: sqlite.NoviRezervniKodoviRepo(baza),
LoginIstorijsaRepo: sqlite.NoviLoginIstorijsaRepo(baza), PokusajiRepo: sqlite.NoviPokusajiPrijaveRepo(baza),
DozvoleRepo: sqlite.NoviDozvoleRepo(baza, middleware.ImaDozvolu, middleware.SveAkcije()), LoginIstorijsaRepo: sqlite.NoviLoginIstorijsaRepo(baza),
PdvStopeRepo: sqlite.NoviPdvStopaRepo(baza), DozvoleRepo: sqlite.NoviDozvoleRepo(baza, middleware.ImaDozvolu, middleware.SveAkcije()),
PdvKirRepo: sqlite.NoviPdvKirRepo(baza), PdvStopeRepo: sqlite.NoviPdvStopaRepo(baza),
PdvKprRepo: sqlite.NoviPdvKprRepo(baza), PdvKirRepo: sqlite.NoviPdvKirRepo(baza),
NivelacijaRepo: sqlite.NoviNivelacijaRepo(baza), PdvKprRepo: sqlite.NoviPdvKprRepo(baza),
NivelacijaRepo: sqlite.NoviNivelacijaRepo(baza),
} }
} }
@@ -128,6 +130,7 @@ func (h *Handler) reinicijalizujRepozitorijume(novaDB *sql.DB) {
h.ServisRepo = sqlite.NoviServisRepo(novaDB) h.ServisRepo = sqlite.NoviServisRepo(novaDB)
h.ServisniDeloviRepo = sqlite.NoviServisniDeloviRepo(novaDB) h.ServisniDeloviRepo = sqlite.NoviServisniDeloviRepo(novaDB)
h.ServisniRadoviRepo = sqlite.NoviServisniRadoviRepo(novaDB) h.ServisniRadoviRepo = sqlite.NoviServisniRadoviRepo(novaDB)
h.ServisniPotrazivaniDeloviRepo = sqlite.NoviServisniPotrazivaniDeloviRepo(novaDB)
h.MagacinskePromeneRepo = sqlite.NoviMagacinskePromeneRepo(novaDB) h.MagacinskePromeneRepo = sqlite.NoviMagacinskePromeneRepo(novaDB)
h.ProdajaRepo = sqlite.NoviProdajaRepo(novaDB) h.ProdajaRepo = sqlite.NoviProdajaRepo(novaDB)
h.KorisniciRepo = sqlite.NoviKorisniciRepo(novaDB, h.totpKljuc) h.KorisniciRepo = sqlite.NoviKorisniciRepo(novaDB, h.totpKljuc)
+166 -52
View File
@@ -43,19 +43,20 @@ type PodaciFormeNaloga struct {
// PodaciDetaljiNaloga su podaci za pregled jednog servisnog naloga // PodaciDetaljiNaloga su podaci za pregled jednog servisnog naloga
type PodaciDetaljiNaloga struct { type PodaciDetaljiNaloga struct {
model.PodaciStranice model.PodaciStranice
Nalog model.ServisniNalog Nalog model.ServisniNalog
KlijentNaziv string KlijentNaziv string
TehnicarNaziv string TehnicarNaziv string
ServisniDelovi []model.ServisniDeoSaArtiklom ServisniDelovi []model.ServisniDeoSaArtiklom
ServisniRadovi []model.ServisniRad ServisniRadovi []model.ServisniRad
Artikli []model.ArtikalSaKategorijom Artikli []model.ArtikalSaKategorijom
Usluge []model.Usluga Usluge []model.Usluga
Sacuvano bool Sacuvano bool
UkupnoDelovi float64 UkupnoDelovi float64
UkupnoRadovi float64 UkupnoRadovi float64
UkupnoSve float64 UkupnoSve float64
PreostaloSve float64 PreostaloSve float64
SviStatusi []string ZakljucanStatus bool // onemogući promenu statusa dok ima potraživanih delova
SviStatusi []string
} }
// Servis renderuje listu servisnih naloga sa opcionom pretragom i filterom statusa // Servis renderuje listu servisnih naloga sa opcionom pretragom i filterom statusa
@@ -424,7 +425,7 @@ func (h *Handler) DetaljiNaloga(w http.ResponseWriter, r *http.Request) {
} }
} }
delovi, err := h.ServisniDeloviRepo.DohvatiZaNalog(r.Context(), id) delovi, err := h.deloviSaPotrazivanima(r.Context(), id)
if err != nil { if err != nil {
slog.Error("greška pri učitavanju delova", "error", err) slog.Error("greška pri učitavanju delova", "error", err)
} }
@@ -463,24 +464,33 @@ func (h *Handler) DetaljiNaloga(w http.ResponseWriter, r *http.Request) {
preostaloSve = 0 preostaloSve = 0
} }
zakljucanStatus := false
for _, d := range delovi {
if d.Potrazivano > 0 {
zakljucanStatus = true
break
}
}
ps := h.popuniPodaciStranice(r, podesavanja) ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "servis" ps.Stranica = "servis"
ps.NaslovStranice = "Detalji naloga" ps.NaslovStranice = "Detalji naloga"
podaci := PodaciDetaljiNaloga{ podaci := PodaciDetaljiNaloga{
PodaciStranice: ps, PodaciStranice: ps,
Nalog: *nalog, Nalog: *nalog,
KlijentNaziv: klijentNaziv, KlijentNaziv: klijentNaziv,
TehnicarNaziv: tehnicarNaziv, TehnicarNaziv: tehnicarNaziv,
ServisniDelovi: delovi, ServisniDelovi: delovi,
ServisniRadovi: radovi, ServisniRadovi: radovi,
Artikli: artikli, Artikli: artikli,
Usluge: usluge, Usluge: usluge,
Sacuvano: r.URL.Query().Get("sacuvano") == "1", Sacuvano: r.URL.Query().Get("sacuvano") == "1",
UkupnoDelovi: ukupnoDelovi, UkupnoDelovi: ukupnoDelovi,
UkupnoRadovi: ukupnoRadovi, UkupnoRadovi: ukupnoRadovi,
UkupnoSve: ukupnoSve, UkupnoSve: ukupnoSve,
PreostaloSve: preostaloSve, PreostaloSve: preostaloSve,
SviStatusi: model.SviStatusi, ZakljucanStatus: zakljucanStatus,
SviStatusi: model.SviStatusi,
} }
h.renderujTemplate(w, "servis_detalji", podaci) h.renderujTemplate(w, "servis_detalji", podaci)
@@ -525,34 +535,88 @@ func (h *Handler) DodajDeloNalogu(w http.ResponseWriter, r *http.Request) {
return return
} }
// prepoznaj prekoračenje magacinskog stanja PRE dodavanja (posle dodavanja // proveri stanje na magacinu: ako tražimo više nego što ima, ugrađujemo samo
// lager se smanji); deo se svejedno ugrađuje (dozvoljen backorder/minus lager) // ono što je na stanju, a razliku beležimo u potraživane delove (ne skida se
prekoraceno := false // sa lagera dok ne stigne); lager NIKAD ne ide u minus
ugradjeno := kolicina
nedostaje := 0
imeArtikla := "" imeArtikla := ""
if art, e := h.Artikli.DohvatiID(r.Context(), artikalID); e == nil && art != nil { if art, e := h.Artikli.DohvatiID(r.Context(), artikalID); e == nil && art != nil {
prekoraceno = kolicina > art.Kolicina
imeArtikla = art.Naziv imeArtikla = art.Naziv
if kolicina > art.Kolicina {
ugradjeno = art.Kolicina
nedostaje = kolicina - art.Kolicina
}
} }
if _, err := h.ServisniDeloviRepo.Dodaj(r.Context(), nalogID, artikalID, kolicina, cena, &k.ID); err != nil { // ugrađujemo samo ono što fizički imamo (ako ima > 0)
slog.Error("greška pri dodavanju dela", "error", err) if ugradjeno > 0 {
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri dodavanju dela.") if _, err := h.ServisniDeloviRepo.Dodaj(r.Context(), nalogID, artikalID, ugradjeno, cena, &k.ID); err != nil {
http.Redirect(w, r, "/servis/"+strconv.FormatInt(nalogID, 10), http.StatusSeeOther) slog.Error("greška pri dodavanju dela", "error", err)
return 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) // višak → potraživani delovi (ne skidamo sa lagera)
if prekoraceno { if nedostaje > 0 {
if _, err := h.ServisniPotrazivaniDeloviRepo.DodajIliUvecaj(r.Context(), nalogID, artikalID, nedostaje); err != nil {
slog.Error("greška pri beleženju potraživanog dela", "error", err)
}
// automatski prebaci nalog u „Čeka delove"
if e := h.ServisRepo.AzurirajStatus(r.Context(), nalogID, model.StatusCekaDelove); e != nil { if e := h.ServisRepo.AzurirajStatus(r.Context(), nalogID, model.StatusCekaDelove); e != nil {
slog.Error("greška pri prebacivanju naloga u Čeka delove", "error", e) slog.Error("greška pri prebacivanju naloga u Čeka delove", "error", e)
} }
middleware.SetFlash(w, r, h.DB, "greska", if ugradjeno > 0 {
"Deo „"+imeArtikla+"\" je dodat iznad stanja na magacinu — nalog je prebačen u „Čeka delove\". Naručite deo.") middleware.SetFlash(w, r, h.DB, "greska",
"Ugrađeno "+strconv.Itoa(ugradjeno)+" kom. artikla „"+imeArtikla+
"\", nedostaje još "+strconv.Itoa(nedostaje)+" — nalog je prebačen u „Čeka delove\".")
} else {
middleware.SetFlash(w, r, h.DB, "greska",
"Nema „"+imeArtikla+"\" na stanju — potrebno "+strconv.Itoa(nedostaje)+
" kom. Nalog je prebačen u „Čeka delove\".")
}
} }
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)
} }
// deloviSaPotrazivanima vraća ugrađene delove spojene sa potraživanima (ista tabela).
// Potraživani delovi bez ugrađenog (stanje bilo 0) se dodaju sa Kolicina=0.
func (h *Handler) deloviSaPotrazivanima(ctx context.Context, nalogID int64) ([]model.ServisniDeoSaArtiklom, error) {
delovi, err := h.ServisniDeloviRepo.DohvatiZaNalog(ctx, nalogID)
if err != nil {
return nil, err
}
potrazivani, err := h.ServisniPotrazivaniDeloviRepo.DohvatiZaNalog(ctx, nalogID)
if err != nil {
return delovi, nil // ne prekidamo ako potraživani nisu dostupni
}
potrMap := make(map[int64]int)
for _, p := range potrazivani {
potrMap[p.ArtikalID] += p.Kolicina
}
for i := range delovi {
if kol, ok := potrMap[delovi[i].ArtikalID]; ok {
delovi[i].Potrazivano = kol
delete(potrMap, delovi[i].ArtikalID)
}
}
for artikalID, kol := range potrMap {
naziv := ""
if art, e := h.Artikli.DohvatiID(ctx, artikalID); e == nil && art != nil {
naziv = art.Naziv
}
delovi = append(delovi, model.ServisniDeoSaArtiklom{
ArtikalNaziv: naziv,
ServisniDeo: model.ServisniDeo{ArtikalID: artikalID},
Potrazivano: kol,
})
}
return delovi, nil
}
// ObrisiDeloNaloga prima POST zahtev i uklanja deo iz servisnog naloga // ObrisiDeloNaloga prima POST zahtev i uklanja deo iz servisnog naloga
func (h *Handler) ObrisiDeloNaloga(w http.ResponseWriter, r *http.Request) { func (h *Handler) ObrisiDeloNaloga(w http.ResponseWriter, r *http.Request) {
k, ok := h.zahtevajDozvolu(w, r, "servis.izmeni") k, ok := h.zahtevajDozvolu(w, r, "servis.izmeni")
@@ -572,6 +636,13 @@ func (h *Handler) ObrisiDeloNaloga(w http.ResponseWriter, r *http.Request) {
return return
} }
// obriši i potraživane delove za isti artikal (inače ostaju blokirani u tabeli)
if artikalID, err := h.ServisniDeloviRepo.DohvatiArtikalID(r.Context(), deoID); err == nil {
if err := h.ServisniPotrazivaniDeloviRepo.ObrisiZaArtikal(r.Context(), nalogID, artikalID); err != nil {
slog.Error("greška pri brisanju potraživanih delova", "error", err)
}
}
if err := h.ServisniDeloviRepo.Obrisi(r.Context(), deoID, &k.ID); err != nil { if err := h.ServisniDeloviRepo.Obrisi(r.Context(), deoID, &k.ID); err != nil {
slog.Error("greška pri brisanju dela", "error", err) slog.Error("greška pri brisanju dela", "error", err)
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri uklanjanju dela.") middleware.SetFlash(w, r, h.DB, "greska", "Greška pri uklanjanju dela.")
@@ -854,7 +925,7 @@ func (h *Handler) StampaServisa(w http.ResponseWriter, r *http.Request) {
return return
} }
delovi, err := h.ServisniDeloviRepo.DohvatiZaNalog(r.Context(), id) delovi, err := h.deloviSaPotrazivanima(r.Context(), id)
if err != nil { if err != nil {
http.Error(w, "Greška pri učitavanju delova", http.StatusInternalServerError) http.Error(w, "Greška pri učitavanju delova", http.StatusInternalServerError)
return return
@@ -945,7 +1016,7 @@ func (h *Handler) StampaOtpremnice(w http.ResponseWriter, r *http.Request) {
return return
} }
delovi, err := h.ServisniDeloviRepo.DohvatiZaNalog(r.Context(), id) delovi, err := h.deloviSaPotrazivanima(r.Context(), id)
if err != nil { if err != nil {
http.Error(w, "Greška pri učitavanju delova", http.StatusInternalServerError) http.Error(w, "Greška pri učitavanju delova", http.StatusInternalServerError)
return return
@@ -1066,7 +1137,7 @@ func (h *Handler) StampaPredracuna(w http.ResponseWriter, r *http.Request) {
return return
} }
delovi, err := h.ServisniDeloviRepo.DohvatiZaNalog(r.Context(), id) delovi, err := h.deloviSaPotrazivanima(r.Context(), id)
if err != nil { if err != nil {
http.Error(w, "Greška pri učitavanju delova", http.StatusInternalServerError) http.Error(w, "Greška pri učitavanju delova", http.StatusInternalServerError)
return return
@@ -1187,17 +1258,14 @@ func (h *Handler) PromeniStatus(w http.ResponseWriter, r *http.Request) {
return return
} }
// zabrana izlaska iz „Čeka delove" dok god neki ugrađeni deo nema pokriće na // zabrana izlaska iz „Čeka delove" dok postoje potraživani delovi —
// zalihama (lager u minusu / backorder) — deo prvo treba nabaviti // prvo nabaviti delove, pa obrisati iz potraživanih (ili kroz nabavku)
if nalog, e := h.ServisRepo.DohvatiID(r.Context(), id); e == nil && if nalog, e := h.ServisRepo.DohvatiID(r.Context(), id); e == nil &&
nalog.Status == model.StatusCekaDelove && noviStatus != model.StatusCekaDelove { nalog.Status == model.StatusCekaDelove && noviStatus != model.StatusCekaDelove {
var bezPokrica int potrazivani, err := h.ServisniPotrazivaniDeloviRepo.DohvatiZaNalog(r.Context(), id)
_ = h.DB.QueryRowContext(r.Context(), if err == nil && len(potrazivani) > 0 {
`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", 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.") "Nalog ne može da napusti „Čeka delove\" dok ima delova koji nedostaju. Obrišite ih iz tabele ili dopunite zalihe.")
http.Redirect(w, r, "/servis/"+strconv.FormatInt(id, 10), http.StatusSeeOther) http.Redirect(w, r, "/servis/"+strconv.FormatInt(id, 10), http.StatusSeeOther)
return return
} }
@@ -1211,6 +1279,52 @@ func (h *Handler) PromeniStatus(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/servis/"+strconv.FormatInt(id, 10)+"?sacuvano=1", http.StatusSeeOther) http.Redirect(w, r, "/servis/"+strconv.FormatInt(id, 10)+"?sacuvano=1", http.StatusSeeOther)
} }
// AzurirajGaranciju ažurira datum garancije na servisnom nalogu
func (h *Handler) AzurirajGaranciju(w http.ResponseWriter, r *http.Request) {
if _, ok := h.zahtevajDozvolu(w, r, "servis.izmeni"); !ok {
return
}
id, err := parseID(chi.URLParam(r, "id"))
if err != nil {
http.Error(w, "Neispravan ID naloga", http.StatusBadRequest)
return
}
if err := r.ParseForm(); err != nil {
http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest)
return
}
var garancijaDo *time.Time
if r.FormValue("bez_garancije") != "1" {
if gd := strings.TrimSpace(r.FormValue("garancija_do")); gd != "" {
t, err := time.Parse("2006-01-02", gd)
if err != nil {
http.Error(w, "Neispravan datum garancije", http.StatusBadRequest)
return
}
garancijaDo = &t
} else {
// odštiklirano „bez garancije" bez izabranog datuma → vrati default
nalog, err := h.ServisRepo.DohvatiID(r.Context(), id)
if err != nil {
slog.Error("greška pri dohvatanju naloga za garanciju", "id", id, "error", err)
} else {
podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
if err != nil {
slog.Error("greška pri učitavanju podešavanja za garanciju", "error", err)
} else {
garancijaDo = defaultGarancija(nalog.DatumPrijema, podesavanja)
}
}
}
}
if err := h.ServisRepo.AzurirajGaranciju(r.Context(), id, garancijaDo); err != nil {
slog.Error("greška pri ažuriranju garancije", "id", id, "error", err)
http.Error(w, "Greška pri ažuriranju garancije", http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/servis/"+strconv.FormatInt(id, 10), http.StatusSeeOther)
}
// PodaciJavnogStatusa su podaci za javnu status stranicu servisnog naloga // PodaciJavnogStatusa su podaci za javnu status stranicu servisnog naloga
type PodaciJavnogStatusa struct { type PodaciJavnogStatusa struct {
Nalog model.ServisniNalog Nalog model.ServisniNalog
+13
View File
@@ -68,6 +68,19 @@ func (d ServisniDeo) Ukupno() float64 {
type ServisniDeoSaArtiklom struct { type ServisniDeoSaArtiklom struct {
ServisniDeo ServisniDeo
ArtikalNaziv string ArtikalNaziv string
Potrazivano int // komada koji nedostaju (iz servisni_potrazivani_delovi), 0 ako nema
}
// ServisniPotrazivaniDeo beleži artikle koji nedostaju za servisni nalog —
// količina koja se traži a nije na stanju; ne skida se sa lagera dok ne stigne
type ServisniPotrazivaniDeo struct {
ID int64
NalogID int64
ArtikalID int64
Kolicina int
Datum time.Time
// za prikaz:
ArtikalNaziv string
} }
// ServisniRad predstavlja jednu stavku rada (uslugu) na servisnom nalogu. // ServisniRad predstavlja jednu stavku rada (uslugu) na servisnom nalogu.
@@ -0,0 +1,7 @@
CREATE TABLE IF NOT EXISTS servisni_potrazivani_delovi (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nalog_id INTEGER NOT NULL REFERENCES servisni_nalozi(id),
artikal_id INTEGER NOT NULL REFERENCES artikli(id),
kolicina INTEGER NOT NULL DEFAULT 0,
datum TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
+53 -15
View File
@@ -47,12 +47,16 @@
{{template "status-badge-detalji" .Nalog.Status}} {{template "status-badge-detalji" .Nalog.Status}}
<form method="post" action="/servis/{{.Nalog.ID}}/status" style="display:flex;align-items:center;gap:6px;" data-refresh="#nalog-zaglavlje"> <form method="post" action="/servis/{{.Nalog.ID}}/status" style="display:flex;align-items:center;gap:6px;" data-refresh="#nalog-zaglavlje">
<input type="hidden" name="_csrf" value="{{.CsrfToken}}"> <input type="hidden" name="_csrf" value="{{.CsrfToken}}">
<select name="status" style="font-size:13px;padding:4px 8px;border-radius:6px;border:0.5px solid var(--ivica);background:var(--kartica-pozadina);color:var(--tekst-glavni);cursor:pointer;"> <select name="status" style="font-size:13px;padding:4px 8px;border-radius:6px;border:0.5px solid var(--ivica);background:var(--kartica-pozadina);color:var(--tekst-glavni);cursor:pointer;"{{if .ZakljucanStatus}} disabled title="Status zaključan dok ima delova koji nedostaju"{{end}}>
{{range .SviStatusi}} {{range .SviStatusi}}
<option value="{{.}}"{{if eq . $.Nalog.Status}} selected{{end}}>{{.}}</option> <option value="{{.}}"{{if eq . $.Nalog.Status}} selected{{end}}>{{.}}</option>
{{end}} {{end}}
</select> </select>
{{if .ZakljucanStatus}}
<span style="font-size:12px;color:#dc2626;">🔒 Status zaključan — obrišite delove koji nedostaju</span>
{{else}}
<button type="submit" class="btn-sekundarno" style="font-size:13px;padding:4px 12px;">Promeni</button> <button type="submit" class="btn-sekundarno" style="font-size:13px;padding:4px 12px;">Promeni</button>
{{end}}
</form> </form>
</div> </div>
</div> </div>
@@ -86,12 +90,30 @@
<div style="font-size:14px;color:var(--tekst-glavni);">{{.Nalog.DatumZavrsetka.Format "02.01.2006."}}</div> <div style="font-size:14px;color:var(--tekst-glavni);">{{.Nalog.DatumZavrsetka.Format "02.01.2006."}}</div>
</div> </div>
{{end}} {{end}}
{{if .Nalog.GarancijaDo}}
<div> <div>
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:4px;">Garancija do</div> <div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:4px;">Garancija do</div>
<div style="font-size:14px;color:#16a34a;font-weight:500;">{{.Nalog.GarancijaDo.Format "02.01.2006."}}</div> {{if index .Dozvole "servis.izmeni"}}
<form method="post" action="/servis/{{.Nalog.ID}}/garancija" data-refresh="#nalog-zaglavlje" style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
<span style="font-size:14px;{{if .Nalog.GarancijaDo}}color:#16a34a;font-weight:500;{{else}}color:var(--tekst-sporedni);{{end}}">
{{if .Nalog.GarancijaDo}}{{.Nalog.GarancijaDo.Format "02.01.2006."}}{{else}}—{{end}}
</span>
<input type="date" name="garancija_do" id="garancija-datum" value="{{if .Nalog.GarancijaDo}}{{.Nalog.GarancijaDo.Format "2006-01-02"}}{{end}}"
onchange="var cb=this.form.querySelector('[name=bez_garancije]');if(cb)cb.checked=false;this.form.requestSubmit()"
style="position:absolute;opacity:0;width:0;height:0;pointer-events:none;" tabindex="-1">
<button type="button" onclick="document.getElementById('garancija-datum').showPicker()"
style="background:none;border:0.5px solid var(--ivica);border-radius:6px;cursor:pointer;padding:4px 6px;font-size:14px;line-height:1;" title="Izmeni datum garancije">📅</button>
<label style="font-size:12px;color:var(--tekst-sporedni);cursor:pointer;display:flex;align-items:center;gap:3px;">
<input type="checkbox" name="bez_garancije" value="1" onchange="this.form.requestSubmit()" style="cursor:pointer;"
{{if not .Nalog.GarancijaDo}}checked{{end}}> bez garancije
</label>
</form>
{{else}}
<div style="font-size:14px;{{if .Nalog.GarancijaDo}}color:#16a34a;font-weight:500;{{else}}color:var(--tekst-sporedni);{{end}}">
{{if .Nalog.GarancijaDo}}{{.Nalog.GarancijaDo.Format "02.01.2006."}}{{else}}—{{end}}
</div>
{{end}}
</div> </div>
{{end}}
<div> <div>
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:4px;">Klijent</div> <div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:4px;">Klijent</div>
<div style="font-size:14px;font-weight:500;color:var(--tekst-glavni);"> <div style="font-size:14px;font-weight:500;color:var(--tekst-glavni);">
@@ -245,7 +267,7 @@
<!-- forma za dodavanje dela --> <!-- forma za dodavanje dela -->
{{if index .Dozvole "servis.izmeni"}} {{if index .Dozvole "servis.izmeni"}}
<form method="POST" action="/servis/{{.Nalog.ID}}/delovi" style="margin-bottom:16px;" data-refresh="#delovi-spisak"> <form method="POST" action="/servis/{{.Nalog.ID}}/delovi" style="margin-bottom:16px;" data-refresh="#delovi-spisak, #nalog-zaglavlje">
<input type="hidden" name="_csrf" value="{{.CsrfToken}}"> <input type="hidden" name="_csrf" value="{{.CsrfToken}}">
<div style="margin-bottom:10px;"> <div style="margin-bottom:10px;">
<label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">Kategorija</label> <label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">Kategorija</label>
@@ -292,14 +314,20 @@
</thead> </thead>
<tbody> <tbody>
{{range .ServisniDelovi}} {{range .ServisniDelovi}}
<tr style="border-bottom:0.5px solid var(--ivica);"> <tr data-artikal-id="{{.ArtikalID}}" data-kolicina="{{.Kolicina}}" data-potrazivano="{{.Potrazivano}}" style="border-bottom:0.5px solid var(--ivica);{{if .Potrazivano}}background:rgba(220,38,38,0.06);{{end}}">
<td style="padding:9px 10px;font-size:14px;color:var(--tekst-glavni);">{{.ArtikalNaziv}}</td> <td style="padding:9px 10px;font-size:14px;color:var(--tekst-glavni);">{{.ArtikalNaziv}}</td>
<td style="padding:9px 10px;text-align:center;font-size:14px;color:var(--tekst-glavni);">{{.Kolicina}}</td> <td style="padding:9px 10px;text-align:center;font-size:14px;{{if .Potrazivano}}color:#dc2626;font-weight:600;{{else}}color:var(--tekst-glavni);{{end}}">
<td style="padding:9px 10px;text-align:right;font-size:14px;color:var(--tekst-glavni);">{{dinari .CenaKomada}} din</td> {{.Kolicina}}{{if .Potrazivano}}<span style="font-weight:400;"> + {{.Potrazivano}} na čekanju</span>{{end}}
<td style="padding:9px 10px;text-align:right;font-size:14px;font-weight:500;color:var(--tekst-glavni);">{{dinari .Ukupno}} din</td> </td>
<td style="padding:9px 10px;text-align:right;font-size:14px;color:var(--tekst-glavni);">
{{if .Kolicina}}{{dinari .CenaKomada}} din{{else}}—{{end}}
</td>
<td style="padding:9px 10px;text-align:right;font-size:14px;font-weight:500;color:var(--tekst-glavni);">
{{if .Kolicina}}{{dinari .Ukupno}} din{{else}}—{{end}}
</td>
<td style="padding:9px 10px;text-align:center;"> <td style="padding:9px 10px;text-align:center;">
{{if index $.Dozvole "servis.izmeni"}} {{if and .ID (index $.Dozvole "servis.izmeni")}}
<form method="POST" action="/servis/{{$.Nalog.ID}}/delovi/{{.ID}}/obrisi" style="display:inline;" data-refresh="#delovi-spisak"> <form method="POST" action="/servis/{{$.Nalog.ID}}/delovi/{{.ID}}/obrisi" style="display:inline;" data-refresh="#delovi-spisak, #nalog-zaglavlje">
<input type="hidden" name="_csrf" value="{{$.CsrfToken}}"> <input type="hidden" name="_csrf" value="{{$.CsrfToken}}">
<button type="submit" <button type="submit"
data-potvrda="Ukloniti deo '{{.ArtikalNaziv}}'?" data-potvrda="Ukloniti deo '{{.ArtikalNaziv}}'?"
@@ -367,11 +395,21 @@
var stanjeIzabrano = 0; 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 // boji „U magacinu" i polje količine crveno kada ukupno (postojeće u tabeli
// stanju (dozvoljeno — backorder; nalog će preći u „Čeka delove" pri čuvanju); // + nova količina) prelazi stanje; čita direktno iz DOM-a tabele jer se ona
// koristi CSS klasu .prekoraceno umesto inline stilova (main.css) // osvežava AJAX-om, pa JSON varijabla zastareva
function osveziPrekoracenje() { function osveziPrekoracenje() {
var preko = (parseInt(kol && kol.value, 10) || 0) > stanjeIzabrano && artikalSel.value; var novaKol = parseInt(kol && kol.value, 10) || 0;
var postojeciKol = 0;
if (artikalSel.value) {
var aid = artikalSel.value;
var redovi = document.querySelectorAll('#delovi-spisak tr[data-artikal-id="' + aid + '"]');
redovi.forEach(function (tr) {
postojeciKol += (parseInt(tr.getAttribute('data-kolicina'), 10) || 0) +
(parseInt(tr.getAttribute('data-potrazivano'), 10) || 0);
});
}
var preko = (postojeciKol + novaKol) > stanjeIzabrano && artikalSel.value;
if (umagacinu) umagacinu.classList.toggle('prekoraceno', preko); if (umagacinu) umagacinu.classList.toggle('prekoraceno', preko);
if (kol) kol.classList.toggle('prekoraceno', preko); if (kol) kol.classList.toggle('prekoraceno', preko);
} }
+23 -30
View File
@@ -391,36 +391,29 @@
.then(function (rr) { return rr.text(); }) .then(function (rr) { return rr.text(); })
.then(function (html) { .then(function (html) {
var doc = new DOMParser().parseFromString(html, 'text/html'); var doc = new DOMParser().parseFromString(html, 'text/html');
var novi = doc.querySelector(ciljSel); // podržava više selektora razdvojenih zarezom (npr. "#a, #b")
var stari = document.querySelector(ciljSel); var sc = document.scrollingElement || document.documentElement;
if (novi && stari) { var syDoc = sc ? sc.scrollTop : 0;
// bez-anim spречava treptaj re-animacije kartica pri zameni var gsStari = document.getElementById('sadrzaj');
novi.classList.add('bez-anim'); var syGs = gsStari ? gsStari.scrollTop : 0;
var sc = document.scrollingElement || document.documentElement; ciljSel.split(',').forEach(function (sel) {
var syDoc = sc ? sc.scrollTop : 0; sel = sel.trim();
var gsStari = document.getElementById('sadrzaj'); var novi = doc.querySelector(sel);
var syGs = gsStari ? gsStari.scrollTop : 0; var stari = document.querySelector(sel);
stari.replaceWith(novi); if (novi && stari) {
void novi.offsetHeight; novi.style.animation = 'none';
novi.classList.remove('bez-anim'); stari.replaceWith(novi);
// replaceWith ne pokreće <script> iz fragmenta re-izvrši ih novi.querySelectorAll('script').forEach(function (old) {
// ručno (npr. da se combobox/pretrage ponovo povežu) var s = document.createElement('script');
novi.querySelectorAll('script').forEach(function (old) { if (old.src) s.src = old.src; else s.textContent = old.textContent;
var s = document.createElement('script'); old.parentNode.replaceChild(s, old);
if (old.src) s.src = old.src; else s.textContent = old.textContent; });
old.parentNode.replaceChild(s, old); }
}); });
if (sc) sc.scrollTop = syDoc; if (sc) sc.scrollTop = syDoc;
var gsNovi = document.getElementById('sadrzaj'); var gsNovi = document.getElementById('sadrzaj');
if (gsNovi) gsNovi.scrollTop = syGs; if (gsNovi) gsNovi.scrollTop = syGs;
// re-veži event handlere (AJAX submit, potvrda brisanja...) ntechInicijalizuj();
// na sveže umetnute elemente — inače bi nove forme išle
// punim POST-om (skok na vrh), a × bez potvrde
ntechInicijalizuj();
}
// forma koja nije deo osveženog dela ostaje u DOM-u —
// isprazni je za sledeći unos (za one koje osvežavaju sebe
// ovo je bezuspešno jer su zamenjene, što je u redu)
if (document.body.contains(f)) f.reset(); if (document.body.contains(f)) f.reset();
if (btn) btn.disabled = false; if (btn) btn.disabled = false;
}) })