diff --git a/cmd/ntech/main.go b/cmd/ntech/main.go index cf33c5b..6d46b42 100644 --- a/cmd/ntech/main.go +++ b/cmd/ntech/main.go @@ -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}/predracun", h.StampaPredracuna) 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/{deo_id}/obrisi", h.ObrisiDeloNaloga) r.With(doz("servis.izmeni")).Post("/servis/{id}/radovi", h.DodajRadNalogu) diff --git a/internal/db/repository.go b/internal/db/repository.go index e36b983..e8a1e2f 100644 --- a/internal/db/repository.go +++ b/internal/db/repository.go @@ -191,6 +191,7 @@ type ServisRepository interface { Kreiraj(ctx context.Context, n *model.ServisniNalog) (int64, error) Izmeni(ctx context.Context, n *model.ServisniNalog) 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 SledeciBroj(ctx context.Context) (string, error) } @@ -210,9 +211,18 @@ type ProdajaRepository interface { type ServisniDeloviRepository interface { DohvatiZaNalog(ctx context.Context, nalogID int64) ([]model.ServisniDeoSaArtiklom, 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 } +// 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 type ServisniRadoviRepository interface { DohvatiZaNalog(ctx context.Context, nalogID int64) ([]model.ServisniRad, error) diff --git a/internal/db/sqlite/servis.go b/internal/db/sqlite/servis.go index 2955314..cc74e6c 100644 --- a/internal/db/sqlite/servis.go +++ b/internal/db/sqlite/servis.go @@ -207,6 +207,28 @@ func (r *ServisRepo) AzurirajStatus(ctx context.Context, id int64, status string 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 func (r *ServisRepo) Obrisi(ctx context.Context, id int64) error { _, err := r.db.ExecContext(ctx, "DELETE FROM servisni_nalozi WHERE id = ?", id) diff --git a/internal/db/sqlite/servisni_delovi.go b/internal/db/sqlite/servisni_delovi.go index 95a74a1..645c021 100644 --- a/internal/db/sqlite/servisni_delovi.go +++ b/internal/db/sqlite/servisni_delovi.go @@ -180,3 +180,15 @@ func (r *ServisniDeloviRepo) Obrisi(ctx context.Context, id int64, korisnikID *i 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 +} diff --git a/internal/db/sqlite/servisni_potrazivani_delovi.go b/internal/db/sqlite/servisni_potrazivani_delovi.go new file mode 100644 index 0000000..da9eda8 --- /dev/null +++ b/internal/db/sqlite/servisni_potrazivani_delovi.go @@ -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 +} diff --git a/internal/handler/handler.go b/internal/handler/handler.go index 4747194..78acbdc 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -17,38 +17,39 @@ import ( // Handler drži zavisnosti koje su potrebne svim handlerima type Handler struct { - DB *sql.DB - PutanjaBaze string - Artikli db.ArtikalRepository - UslugeRepo db.UslugaRepository - TroskoviRepo db.TrosakRepository - KategorijeRepo db.KategorijaRepository - DobavljaciRepo db.DobavljacRepository - NabavkeRepo db.NabavkaRepository - KlijentiRepo db.KlijentRepository - ServisRepo db.ServisRepository - ServisniDeloviRepo db.ServisniDeloviRepository - ServisniRadoviRepo db.ServisniRadoviRepository - MagacinskePromeneRepo db.MagacinskePromeneRepository - ProdajaRepo db.ProdajaRepository - KorisniciRepo db.KorisniciRepository - SesijeRepo db.SesijeRepository - PodsetnikRepo db.PodsetnikRepository - IzvestajRepo db.IzvestajRepository - RezervniKodoviRepo db.RezervniKodoviRepository - PokusajiRepo db.PokusajiPrijaveRepository - LoginIstorijsaRepo db.LoginIstorijsaRepository - DozvoleRepo db.DozvoleRepository - PdvStopeRepo db.PdvStopaRepository - PdvKirRepo db.PdvKirRepository - PdvKprRepo db.PdvKprRepository - NivelacijaRepo db.NivelacijaRepository - Verzija string - JelDemo bool - AssetV string // verzija statičkih fajlova za cache-busting (postavlja se pri pokretanju) - Templates map[string]*template.Template - TemplatesFS fs.FS - totpKljuc []byte // ključ za šifrovanje TOTP tajni (prosleđuje se KorisniciRepo) + DB *sql.DB + PutanjaBaze string + Artikli db.ArtikalRepository + UslugeRepo db.UslugaRepository + TroskoviRepo db.TrosakRepository + KategorijeRepo db.KategorijaRepository + DobavljaciRepo db.DobavljacRepository + NabavkeRepo db.NabavkaRepository + KlijentiRepo db.KlijentRepository + ServisRepo db.ServisRepository + ServisniDeloviRepo db.ServisniDeloviRepository + ServisniRadoviRepo db.ServisniRadoviRepository + ServisniPotrazivaniDeloviRepo db.ServisniPotrazivaniDeloviRepository + MagacinskePromeneRepo db.MagacinskePromeneRepository + ProdajaRepo db.ProdajaRepository + KorisniciRepo db.KorisniciRepository + SesijeRepo db.SesijeRepository + PodsetnikRepo db.PodsetnikRepository + IzvestajRepo db.IzvestajRepository + RezervniKodoviRepo db.RezervniKodoviRepository + PokusajiRepo db.PokusajiPrijaveRepository + LoginIstorijsaRepo db.LoginIstorijsaRepository + DozvoleRepo db.DozvoleRepository + PdvStopeRepo db.PdvStopaRepository + PdvKirRepo db.PdvKirRepository + PdvKprRepo db.PdvKprRepository + NivelacijaRepo db.NivelacijaRepository + Verzija string + JelDemo bool + AssetV string // verzija statičkih fajlova za cache-busting (postavlja se pri pokretanju) + Templates map[string]*template.Template + 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). // 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. func Novi(baza *sql.DB, totpKljuc []byte) *Handler { return &Handler{ - DB: baza, - totpKljuc: totpKljuc, - Artikli: sqlite.NoviArtikalRepo(baza), - UslugeRepo: sqlite.NoviUslugaRepo(baza), - TroskoviRepo: sqlite.NoviTrosakRepo(baza), - KategorijeRepo: sqlite.NovaKategorijaRepo(baza), - DobavljaciRepo: sqlite.NoviDobavljacRepo(baza), - NabavkeRepo: sqlite.NoviNabavkaRepo(baza), - KlijentiRepo: sqlite.NoviKlijentRepo(baza), - ServisRepo: sqlite.NoviServisRepo(baza), - ServisniDeloviRepo: sqlite.NoviServisniDeloviRepo(baza), - ServisniRadoviRepo: sqlite.NoviServisniRadoviRepo(baza), - MagacinskePromeneRepo: sqlite.NoviMagacinskePromeneRepo(baza), - ProdajaRepo: sqlite.NoviProdajaRepo(baza), - KorisniciRepo: sqlite.NoviKorisniciRepo(baza, totpKljuc), - SesijeRepo: sqlite.NoviSesijeRepo(baza), - PodsetnikRepo: sqlite.NoviPodsetnikRepo(baza), - IzvestajRepo: sqlite.NoviIzvestajRepo(baza), - RezervniKodoviRepo: sqlite.NoviRezervniKodoviRepo(baza), - PokusajiRepo: sqlite.NoviPokusajiPrijaveRepo(baza), - LoginIstorijsaRepo: sqlite.NoviLoginIstorijsaRepo(baza), - DozvoleRepo: sqlite.NoviDozvoleRepo(baza, middleware.ImaDozvolu, middleware.SveAkcije()), - PdvStopeRepo: sqlite.NoviPdvStopaRepo(baza), - PdvKirRepo: sqlite.NoviPdvKirRepo(baza), - PdvKprRepo: sqlite.NoviPdvKprRepo(baza), - NivelacijaRepo: sqlite.NoviNivelacijaRepo(baza), + DB: baza, + totpKljuc: totpKljuc, + Artikli: sqlite.NoviArtikalRepo(baza), + UslugeRepo: sqlite.NoviUslugaRepo(baza), + TroskoviRepo: sqlite.NoviTrosakRepo(baza), + KategorijeRepo: sqlite.NovaKategorijaRepo(baza), + DobavljaciRepo: sqlite.NoviDobavljacRepo(baza), + NabavkeRepo: sqlite.NoviNabavkaRepo(baza), + KlijentiRepo: sqlite.NoviKlijentRepo(baza), + ServisRepo: sqlite.NoviServisRepo(baza), + ServisniDeloviRepo: sqlite.NoviServisniDeloviRepo(baza), + ServisniRadoviRepo: sqlite.NoviServisniRadoviRepo(baza), + ServisniPotrazivaniDeloviRepo: sqlite.NoviServisniPotrazivaniDeloviRepo(baza), + MagacinskePromeneRepo: sqlite.NoviMagacinskePromeneRepo(baza), + ProdajaRepo: sqlite.NoviProdajaRepo(baza), + KorisniciRepo: sqlite.NoviKorisniciRepo(baza, totpKljuc), + SesijeRepo: sqlite.NoviSesijeRepo(baza), + PodsetnikRepo: sqlite.NoviPodsetnikRepo(baza), + IzvestajRepo: sqlite.NoviIzvestajRepo(baza), + RezervniKodoviRepo: sqlite.NoviRezervniKodoviRepo(baza), + PokusajiRepo: sqlite.NoviPokusajiPrijaveRepo(baza), + LoginIstorijsaRepo: sqlite.NoviLoginIstorijsaRepo(baza), + DozvoleRepo: sqlite.NoviDozvoleRepo(baza, middleware.ImaDozvolu, middleware.SveAkcije()), + PdvStopeRepo: sqlite.NoviPdvStopaRepo(baza), + PdvKirRepo: sqlite.NoviPdvKirRepo(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.ServisniDeloviRepo = sqlite.NoviServisniDeloviRepo(novaDB) h.ServisniRadoviRepo = sqlite.NoviServisniRadoviRepo(novaDB) + h.ServisniPotrazivaniDeloviRepo = sqlite.NoviServisniPotrazivaniDeloviRepo(novaDB) h.MagacinskePromeneRepo = sqlite.NoviMagacinskePromeneRepo(novaDB) h.ProdajaRepo = sqlite.NoviProdajaRepo(novaDB) h.KorisniciRepo = sqlite.NoviKorisniciRepo(novaDB, h.totpKljuc) diff --git a/internal/handler/servis.go b/internal/handler/servis.go index 3c6e1e9..ee8ab29 100644 --- a/internal/handler/servis.go +++ b/internal/handler/servis.go @@ -43,19 +43,20 @@ type PodaciFormeNaloga struct { // PodaciDetaljiNaloga su podaci za pregled jednog servisnog naloga type PodaciDetaljiNaloga struct { model.PodaciStranice - Nalog model.ServisniNalog - KlijentNaziv string - TehnicarNaziv string - ServisniDelovi []model.ServisniDeoSaArtiklom - ServisniRadovi []model.ServisniRad - Artikli []model.ArtikalSaKategorijom - Usluge []model.Usluga - Sacuvano bool - UkupnoDelovi float64 - UkupnoRadovi float64 - UkupnoSve float64 - PreostaloSve float64 - SviStatusi []string + Nalog model.ServisniNalog + KlijentNaziv string + TehnicarNaziv string + ServisniDelovi []model.ServisniDeoSaArtiklom + ServisniRadovi []model.ServisniRad + Artikli []model.ArtikalSaKategorijom + Usluge []model.Usluga + Sacuvano bool + UkupnoDelovi float64 + UkupnoRadovi float64 + UkupnoSve float64 + PreostaloSve float64 + ZakljucanStatus bool // onemogući promenu statusa dok ima potraživanih delova + SviStatusi []string } // 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 { 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 } + zakljucanStatus := false + for _, d := range delovi { + if d.Potrazivano > 0 { + zakljucanStatus = true + break + } + } + ps := h.popuniPodaciStranice(r, podesavanja) ps.Stranica = "servis" ps.NaslovStranice = "Detalji naloga" podaci := PodaciDetaljiNaloga{ - PodaciStranice: ps, - Nalog: *nalog, - KlijentNaziv: klijentNaziv, - TehnicarNaziv: tehnicarNaziv, - ServisniDelovi: delovi, - ServisniRadovi: radovi, - Artikli: artikli, - Usluge: usluge, - Sacuvano: r.URL.Query().Get("sacuvano") == "1", - UkupnoDelovi: ukupnoDelovi, - UkupnoRadovi: ukupnoRadovi, - UkupnoSve: ukupnoSve, - PreostaloSve: preostaloSve, - SviStatusi: model.SviStatusi, + PodaciStranice: ps, + Nalog: *nalog, + KlijentNaziv: klijentNaziv, + TehnicarNaziv: tehnicarNaziv, + ServisniDelovi: delovi, + ServisniRadovi: radovi, + Artikli: artikli, + Usluge: usluge, + Sacuvano: r.URL.Query().Get("sacuvano") == "1", + UkupnoDelovi: ukupnoDelovi, + UkupnoRadovi: ukupnoRadovi, + UkupnoSve: ukupnoSve, + PreostaloSve: preostaloSve, + ZakljucanStatus: zakljucanStatus, + SviStatusi: model.SviStatusi, } h.renderujTemplate(w, "servis_detalji", podaci) @@ -525,34 +535,88 @@ func (h *Handler) DodajDeloNalogu(w http.ResponseWriter, r *http.Request) { return } - // prepoznaj prekoračenje magacinskog stanja PRE dodavanja (posle dodavanja - // lager se smanji); deo se svejedno ugrađuje (dozvoljen backorder/minus lager) - prekoraceno := false + // proveri stanje na magacinu: ako tražimo više nego što ima, ugrađujemo samo + // ono što je na stanju, a razliku beležimo u potraživane delove (ne skida se + // sa lagera dok ne stigne); lager NIKAD ne ide u minus + ugradjeno := kolicina + nedostaje := 0 imeArtikla := "" if art, e := h.Artikli.DohvatiID(r.Context(), artikalID); e == nil && art != nil { - prekoraceno = kolicina > art.Kolicina 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 { - slog.Error("greška pri dodavanju dela", "error", err) - middleware.SetFlash(w, r, h.DB, "greska", "Greška pri dodavanju dela.") - http.Redirect(w, r, "/servis/"+strconv.FormatInt(nalogID, 10), http.StatusSeeOther) - return + // ugrađujemo samo ono što fizički imamo (ako ima > 0) + if ugradjeno > 0 { + if _, err := h.ServisniDeloviRepo.Dodaj(r.Context(), nalogID, artikalID, ugradjeno, 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.") + 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 { + // višak → potraživani delovi (ne skidamo sa lagera) + 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 { 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.") + if ugradjeno > 0 { + 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) } +// 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 func (h *Handler) ObrisiDeloNaloga(w http.ResponseWriter, r *http.Request) { k, ok := h.zahtevajDozvolu(w, r, "servis.izmeni") @@ -572,6 +636,13 @@ func (h *Handler) ObrisiDeloNaloga(w http.ResponseWriter, r *http.Request) { 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 { slog.Error("greška pri brisanju dela", "error", err) 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 } - delovi, err := h.ServisniDeloviRepo.DohvatiZaNalog(r.Context(), id) + delovi, err := h.deloviSaPotrazivanima(r.Context(), id) if err != nil { http.Error(w, "Greška pri učitavanju delova", http.StatusInternalServerError) return @@ -945,7 +1016,7 @@ func (h *Handler) StampaOtpremnice(w http.ResponseWriter, r *http.Request) { return } - delovi, err := h.ServisniDeloviRepo.DohvatiZaNalog(r.Context(), id) + delovi, err := h.deloviSaPotrazivanima(r.Context(), id) if err != nil { http.Error(w, "Greška pri učitavanju delova", http.StatusInternalServerError) return @@ -1066,7 +1137,7 @@ func (h *Handler) StampaPredracuna(w http.ResponseWriter, r *http.Request) { return } - delovi, err := h.ServisniDeloviRepo.DohvatiZaNalog(r.Context(), id) + delovi, err := h.deloviSaPotrazivanima(r.Context(), id) if err != nil { http.Error(w, "Greška pri učitavanju delova", http.StatusInternalServerError) return @@ -1187,17 +1258,14 @@ func (h *Handler) PromeniStatus(w http.ResponseWriter, r *http.Request) { 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 + // zabrana izlaska iz „Čeka delove" dok postoje potraživani delovi — + // prvo nabaviti delove, pa obrisati iz potraživanih (ili kroz nabavku) 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 { + potrazivani, err := h.ServisniPotrazivaniDeloviRepo.DohvatiZaNalog(r.Context(), id) + if err == nil && len(potrazivani) > 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.") + "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) 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) } +// 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 type PodaciJavnogStatusa struct { Nalog model.ServisniNalog diff --git a/internal/model/servis.go b/internal/model/servis.go index 5fee20d..1e5e712 100644 --- a/internal/model/servis.go +++ b/internal/model/servis.go @@ -68,6 +68,19 @@ func (d ServisniDeo) Ukupno() float64 { type ServisniDeoSaArtiklom struct { ServisniDeo 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. diff --git a/migrations/067_servisni_potrazivani_delovi.sql b/migrations/067_servisni_potrazivani_delovi.sql new file mode 100644 index 0000000..deb1dcb --- /dev/null +++ b/migrations/067_servisni_potrazivani_delovi.sql @@ -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 +); diff --git a/web/templates/stranice/servis_detalji.html b/web/templates/stranice/servis_detalji.html index 819136e..044946d 100644 --- a/web/templates/stranice/servis_detalji.html +++ b/web/templates/stranice/servis_detalji.html @@ -47,12 +47,16 @@ {{template "status-badge-detalji" .Nalog.Status}}
@@ -86,12 +90,30 @@