diff --git a/.gitignore b/.gitignore index 05ec401..3c23cef 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ # Binarne datoteke -ntech -ntech.exe +/ntech +/ntech.exe # Baza podataka *.db @@ -25,3 +25,5 @@ ntech.exe # IDE podešavanja .idea/ *.swp + + diff --git a/cmd/ntech/main.go b/cmd/ntech/main.go index f8c8a15..3a27691 100644 --- a/cmd/ntech/main.go +++ b/cmd/ntech/main.go @@ -66,6 +66,17 @@ func main() { r.Get("/magacin/kategorije", h.Kategorije) r.Post("/magacin/kategorije/dodaj", h.DodajKategoriju) r.Get("/magacin/kategorije/obrisi/{id}", h.ObrisiKategoriju) + r.Get("/nabavke", h.Nabavke) + r.Get("/nabavke/nova", h.NovaNabavka) + r.Post("/nabavke/nova", h.SacuvajNabavku) + r.Get("/nabavke/{id}", h.DetaljiNabavke) + r.Post("/nabavke/obrisi/{id}", h.ObrisiNabavku) + r.Get("/dobavljaci", h.Dobavljaci) + r.Get("/dobavljaci/novi", h.NoviDobavljac) + r.Post("/dobavljaci/novi", h.SacuvajDobavljaca) + r.Get("/dobavljaci/izmeni/{id}", h.IzmeniDobavljaca) + r.Post("/dobavljaci/izmeni/{id}", h.SacuvajIzmeneDobavljaca) + r.Post("/dobavljaci/obrisi/{id}", h.ObrisiDobavljaca) log.Printf("NTech pokrenut na portu %s", port) err = http.ListenAndServe(":"+port, r) diff --git a/internal/db/repository.go b/internal/db/repository.go index 826790a..3f31dba 100644 --- a/internal/db/repository.go +++ b/internal/db/repository.go @@ -23,7 +23,25 @@ type KategorijaRepository interface { // ArtikalFilter definiše parametre za filtriranje liste artikala type ArtikalFilter struct { - Pretraga string - KategorijaID *int64 - SamoKriticni bool + Pretraga string + KategorijaID *int64 + SamoKriticni bool +} + +// NabavkaRepository definiše operacije nad nabavkama +type NabavkaRepository interface { + Lista(ctx context.Context) ([]model.NabavkaSaDetaljem, error) + DohvatiID(ctx context.Context, id int64) (*model.Nabavka, error) + DohvatiStavke(ctx context.Context, nabavkaID int64) ([]model.StavkaSaArtiklom, error) + Kreiraj(ctx context.Context, n *model.Nabavka, stavke []model.StavkaNabavke) (int64, error) + Obrisi(ctx context.Context, id int64) error +} + +// DobavljacRepository definiše operacije nad dobavljačima +type DobavljacRepository interface { + Lista(ctx context.Context, pretraga string) ([]model.Dobavljac, error) + DohvatiID(ctx context.Context, id int64) (*model.Dobavljac, error) + Kreiraj(ctx context.Context, d *model.Dobavljac) (int64, error) + Izmeni(ctx context.Context, d *model.Dobavljac) error + Obrisi(ctx context.Context, id int64) error } diff --git a/internal/db/sqlite/dobavljac.go b/internal/db/sqlite/dobavljac.go new file mode 100644 index 0000000..b3f96d4 --- /dev/null +++ b/internal/db/sqlite/dobavljac.go @@ -0,0 +1,130 @@ +package sqlite + +import ( + "context" + "database/sql" + "fmt" + + "ntech/internal/model" +) + +// DobavljacRepo je SQLite implementacija DobavljacRepository interfejsa +type DobavljacRepo struct { + db *sql.DB +} + +// NoviDobavljacRepo kreira novi DobavljacRepo +func NoviDobavljacRepo(db *sql.DB) *DobavljacRepo { + return &DobavljacRepo{db: db} +} + +// Lista vraća listu dobavljača sa opcionom pretragom po nazivu +func (r *DobavljacRepo) Lista(ctx context.Context, pretraga string) ([]model.Dobavljac, error) { + upit := ` + SELECT id, naziv, kontakt_osoba, telefon, email, napomena, datum_unosa + FROM dobavljaci + WHERE 1=1` + + args := []any{} + + if pretraga != "" { + upit += " AND naziv LIKE ?" + args = append(args, "%"+pretraga+"%") + } + + upit += " ORDER BY naziv ASC" + + redovi, err := r.db.QueryContext(ctx, upit, args...) + if err != nil { + return nil, fmt.Errorf("ntech: DobavljacRepo.Lista: %w", err) + } + defer redovi.Close() + + var rezultat []model.Dobavljac + for redovi.Next() { + var d model.Dobavljac + var kontaktOsoba, telefon, email, napomena sql.NullString + err := redovi.Scan( + &d.ID, &d.Naziv, &kontaktOsoba, &telefon, &email, &napomena, &d.DatumUnosa, + ) + if err != nil { + return nil, fmt.Errorf("ntech: DobavljacRepo.Lista: scan: %w", err) + } + d.KontaktOsoba = kontaktOsoba.String + d.Telefon = telefon.String + d.Email = email.String + d.Napomena = napomena.String + rezultat = append(rezultat, d) + } + + return rezultat, nil +} + +// DohvatiID vraća jednog dobavljača po ID-u +func (r *DobavljacRepo) DohvatiID(ctx context.Context, id int64) (*model.Dobavljac, error) { + var d model.Dobavljac + var kontaktOsoba, telefon, email, napomena sql.NullString + + err := r.db.QueryRowContext(ctx, ` + SELECT id, naziv, kontakt_osoba, telefon, email, napomena, datum_unosa + FROM dobavljaci WHERE id = ?`, id).Scan( + &d.ID, &d.Naziv, &kontaktOsoba, &telefon, &email, &napomena, &d.DatumUnosa, + ) + if err != nil { + return nil, fmt.Errorf("ntech: DobavljacRepo.DohvatiID: %w", err) + } + + d.KontaktOsoba = kontaktOsoba.String + d.Telefon = telefon.String + d.Email = email.String + d.Napomena = napomena.String + + return &d, nil +} + +// Kreiraj dodaje novog dobavljača u bazu +func (r *DobavljacRepo) Kreiraj(ctx context.Context, d *model.Dobavljac) (int64, error) { + rezultat, err := r.db.ExecContext(ctx, ` + INSERT INTO dobavljaci (naziv, kontakt_osoba, telefon, email, napomena) + VALUES (?, ?, ?, ?, ?)`, + d.Naziv, nullString(d.KontaktOsoba), nullString(d.Telefon), + nullString(d.Email), nullString(d.Napomena), + ) + if err != nil { + return 0, fmt.Errorf("ntech: DobavljacRepo.Kreiraj: %w", err) + } + + id, err := rezultat.LastInsertId() + if err != nil { + return 0, fmt.Errorf("ntech: DobavljacRepo.Kreiraj: last insert id: %w", err) + } + + return id, nil +} + +// Izmeni ažurira postojećeg dobavljača +func (r *DobavljacRepo) Izmeni(ctx context.Context, d *model.Dobavljac) error { + _, err := r.db.ExecContext(ctx, ` + UPDATE dobavljaci SET + naziv = ?, kontakt_osoba = ?, telefon = ?, email = ?, napomena = ? + WHERE id = ?`, + d.Naziv, nullString(d.KontaktOsoba), nullString(d.Telefon), + nullString(d.Email), nullString(d.Napomena), d.ID, + ) + if err != nil { + return fmt.Errorf("ntech: DobavljacRepo.Izmeni: %w", err) + } + + return nil +} + +// Obrisi briše dobavljača po ID-u +func (r *DobavljacRepo) Obrisi(ctx context.Context, id int64) error { + _, err := r.db.ExecContext(ctx, "DELETE FROM dobavljaci WHERE id = ?", id) + if err != nil { + return fmt.Errorf("ntech: DobavljacRepo.Obrisi: %w", err) + } + + return nil +} + diff --git a/internal/db/sqlite/nabavka.go b/internal/db/sqlite/nabavka.go new file mode 100644 index 0000000..2e3b735 --- /dev/null +++ b/internal/db/sqlite/nabavka.go @@ -0,0 +1,181 @@ +package sqlite + +import ( + "context" + "database/sql" + "fmt" + + "ntech/internal/model" +) + +// NabavkaRepo je SQLite implementacija NabavkaRepository interfejsa +type NabavkaRepo struct { + db *sql.DB +} + +// NoviNabavkaRepo kreira novi NabavkaRepo +func NoviNabavkaRepo(db *sql.DB) *NabavkaRepo { + return &NabavkaRepo{db: db} +} + +// Lista vraća sve nabavke sa nazivom dobavljača, sortirano od najnovije +func (r *NabavkaRepo) Lista(ctx context.Context) ([]model.NabavkaSaDetaljem, error) { + redovi, err := r.db.QueryContext(ctx, ` + SELECT + n.id, n.dobavljac_id, n.napomena, n.ukupno, n.datum, + COALESCE(d.naziv, '') AS dobavljac_naziv + FROM nabavke n + LEFT JOIN dobavljaci d ON n.dobavljac_id = d.id + ORDER BY n.datum DESC`) + if err != nil { + return nil, fmt.Errorf("ntech: NabavkaRepo.Lista: %w", err) + } + defer redovi.Close() + + var rezultat []model.NabavkaSaDetaljem + for redovi.Next() { + var n model.NabavkaSaDetaljem + var dobavljacID sql.NullInt64 + var napomena sql.NullString + + err := redovi.Scan( + &n.ID, &dobavljacID, &napomena, &n.Ukupno, &n.Datum, + &n.DobavljacNaziv, + ) + if err != nil { + return nil, fmt.Errorf("ntech: NabavkaRepo.Lista: scan: %w", err) + } + + if dobavljacID.Valid { + n.DobavljacID = &dobavljacID.Int64 + } + n.Napomena = napomena.String + + rezultat = append(rezultat, n) + } + + return rezultat, nil +} + +// DohvatiID vraća zaglavlje jedne nabavke po ID-u +func (r *NabavkaRepo) DohvatiID(ctx context.Context, id int64) (*model.Nabavka, error) { + var n model.Nabavka + var dobavljacID sql.NullInt64 + var napomena sql.NullString + + err := r.db.QueryRowContext(ctx, ` + SELECT id, dobavljac_id, napomena, ukupno, datum + FROM nabavke WHERE id = ?`, id).Scan( + &n.ID, &dobavljacID, &napomena, &n.Ukupno, &n.Datum, + ) + if err != nil { + return nil, fmt.Errorf("ntech: NabavkaRepo.DohvatiID: %w", err) + } + + if dobavljacID.Valid { + n.DobavljacID = &dobavljacID.Int64 + } + n.Napomena = napomena.String + + return &n, nil +} + +// DohvatiStavke vraća sve stavke jedne nabavke sa nazivima artikala +func (r *NabavkaRepo) DohvatiStavke(ctx context.Context, nabavkaID int64) ([]model.StavkaSaArtiklom, error) { + redovi, err := r.db.QueryContext(ctx, ` + SELECT + s.id, s.nabavka_id, s.artikal_id, s.kolicina, + s.cena_po_komadu, s.ukupno, + a.naziv AS artikal_naziv + FROM stavke_nabavke s + JOIN artikli a ON s.artikal_id = a.id + WHERE s.nabavka_id = ? + ORDER BY s.id ASC`, nabavkaID) + if err != nil { + return nil, fmt.Errorf("ntech: NabavkaRepo.DohvatiStavke: %w", err) + } + defer redovi.Close() + + var rezultat []model.StavkaSaArtiklom + for redovi.Next() { + var s model.StavkaSaArtiklom + err := redovi.Scan( + &s.ID, &s.NabavkaID, &s.ArtikalID, &s.Kolicina, + &s.CenaPoKomadu, &s.Ukupno, + &s.ArtikalNaziv, + ) + if err != nil { + return nil, fmt.Errorf("ntech: NabavkaRepo.DohvatiStavke: scan: %w", err) + } + rezultat = append(rezultat, s) + } + + return rezultat, nil +} + +// Kreiraj upisuje novu nabavku sa svim stavkama u jednoj transakciji i ažurira stanje magacina +func (r *NabavkaRepo) Kreiraj(ctx context.Context, n *model.Nabavka, stavke []model.StavkaNabavke) (int64, error) { + tx, err := r.db.BeginTx(ctx, nil) + if err != nil { + return 0, fmt.Errorf("ntech: NabavkaRepo.Kreiraj: begin: %w", err) + } + defer tx.Rollback() + + // računamo ukupan iznos nabavke kao zbir svih stavki + var ukupno float64 + for i := range stavke { + stavke[i].Ukupno = float64(stavke[i].Kolicina) * stavke[i].CenaPoKomadu + ukupno += stavke[i].Ukupno + } + + // upisujemo zaglavlje nabavke + rezultat, err := tx.ExecContext(ctx, ` + INSERT INTO nabavke (dobavljac_id, napomena, ukupno) + VALUES (?, ?, ?)`, + nullInt64(n.DobavljacID), nullString(n.Napomena), ukupno, + ) + if err != nil { + return 0, fmt.Errorf("ntech: NabavkaRepo.Kreiraj: insert nabavka: %w", err) + } + + nabavkaID, err := rezultat.LastInsertId() + if err != nil { + return 0, fmt.Errorf("ntech: NabavkaRepo.Kreiraj: last insert id: %w", err) + } + + // upisujemo svaku stavku i ažuriramo stanje artikla u magacinu + for _, s := range stavke { + _, err := tx.ExecContext(ctx, ` + INSERT INTO stavke_nabavke (nabavka_id, artikal_id, kolicina, cena_po_komadu, ukupno) + VALUES (?, ?, ?, ?, ?)`, + nabavkaID, s.ArtikalID, s.Kolicina, s.CenaPoKomadu, s.Ukupno, + ) + if err != nil { + return 0, fmt.Errorf("ntech: NabavkaRepo.Kreiraj: insert stavka: %w", err) + } + + _, err = tx.ExecContext(ctx, + "UPDATE artikli SET kolicina = kolicina + ? WHERE id = ?", + s.Kolicina, s.ArtikalID, + ) + if err != nil { + return 0, fmt.Errorf("ntech: NabavkaRepo.Kreiraj: update kolicina: %w", err) + } + } + + if err := tx.Commit(); err != nil { + return 0, fmt.Errorf("ntech: NabavkaRepo.Kreiraj: commit: %w", err) + } + + return nabavkaID, nil +} + +// Obrisi briše nabavku po ID-u — stavke se brišu automatski (ON DELETE CASCADE) +// Napomena: brisanje ne vraća količine artikala u magacin +func (r *NabavkaRepo) Obrisi(ctx context.Context, id int64) error { + _, err := r.db.ExecContext(ctx, "DELETE FROM nabavke WHERE id = ?", id) + if err != nil { + return fmt.Errorf("ntech: NabavkaRepo.Obrisi: %w", err) + } + return nil +} diff --git a/internal/db/sqlite/utils.go b/internal/db/sqlite/utils.go new file mode 100644 index 0000000..1f74869 --- /dev/null +++ b/internal/db/sqlite/utils.go @@ -0,0 +1,18 @@ +package sqlite + +import "database/sql" + +// nullString pretvara prazan Go string u sql.NullString sa NULL vrednošću — +// koristi se pri unosu i izmeni kada polje u bazi sme biti NULL +func nullString(s string) sql.NullString { + return sql.NullString{String: s, Valid: s != ""} +} + +// nullInt64 pretvara *int64 pokazivač u sql.NullInt64 — +// koristi se za opciona FK polja koja smeju biti NULL u bazi +func nullInt64(v *int64) sql.NullInt64 { + if v == nil { + return sql.NullInt64{} + } + return sql.NullInt64{Int64: *v, Valid: true} +} diff --git a/internal/handler/dobavljac.go b/internal/handler/dobavljac.go new file mode 100644 index 0000000..84b590f --- /dev/null +++ b/internal/handler/dobavljac.go @@ -0,0 +1,277 @@ +package handler + +import ( + "html/template" + "log" + "net/http" + "strings" + + "ntech/internal/db/sqlite" + "ntech/internal/model" + + "github.com/go-chi/chi/v5" +) + +// PodaciDobavljaca su podaci za stranicu sa listom dobavljača +type PodaciDobavljaca struct { + model.PodaciStranice + Dobavljaci []model.Dobavljac + Pretraga string + Sacuvano bool + Obrisan bool +} + +// PodaciFormeDobavljaca su podaci za formu novog/izmenjenog dobavljača +type PodaciFormeDobavljaca struct { + model.PodaciStranice + Dobavljac model.Dobavljac + Greska string + Izmena bool +} + +// Dobavljaci renderuje listu svih dobavljača +func (h *Handler) Dobavljaci(w http.ResponseWriter, r *http.Request) { + podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB) + if err != nil { + http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError) + return + } + + pretraga := r.URL.Query().Get("pretraga") + + dobavljaci, err := h.DobavljaciRepo.Lista(r.Context(), pretraga) + if err != nil { + http.Error(w, "Greška pri učitavanju dobavljača", http.StatusInternalServerError) + return + } + + podaci := PodaciDobavljaca{ + PodaciStranice: model.PodaciStranice{ + Stranica: "dobavljaci", + NaslovStranice: "Dobavljači", + Tema: podesavanja["tema"], + NazivFirme: podesavanja["naziv_firme"], + Podnazlov: podesavanja["podnazlov"], + LogoTip: podesavanja["logo_tip"], + LogoPutanja: podesavanja["logo_putanja"], + Korisnik: "Admin", + }, + Dobavljaci: dobavljaci, + Pretraga: pretraga, + Sacuvano: r.URL.Query().Get("sacuvano") == "1", + Obrisan: r.URL.Query().Get("obrisan") == "1", + } + + tmpl, err := template.ParseFiles( + "web/templates/teme/podrazumevana/base.html", + "web/templates/komponente/sidebar.html", + "web/templates/komponente/topbar.html", + "web/templates/stranice/dobavljaci.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 + } + + 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) + } +} + +// NoviDobavljac prikazuje praznu formu za unos novog dobavljača +func (h *Handler) NoviDobavljac(w http.ResponseWriter, r *http.Request) { + podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB) + if err != nil { + http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError) + return + } + + renderujFormuDobavljaca(w, PodaciFormeDobavljaca{ + PodaciStranice: model.PodaciStranice{ + Stranica: "dobavljaci", + NaslovStranice: "Novi dobavljač", + Tema: podesavanja["tema"], + NazivFirme: podesavanja["naziv_firme"], + Podnazlov: podesavanja["podnazlov"], + LogoTip: podesavanja["logo_tip"], + LogoPutanja: podesavanja["logo_putanja"], + Korisnik: "Admin", + }, + Izmena: false, + }) +} + +// SacuvajDobavljaca prima POST formu i upisuje novog dobavljača u bazu +func (h *Handler) SacuvajDobavljaca(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest) + return + } + + dobavljac, greska := parseFormuDobavljaca(r) + if greska != "" { + podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB) + renderujFormuDobavljaca(w, PodaciFormeDobavljaca{ + PodaciStranice: model.PodaciStranice{ + Stranica: "dobavljaci", + NaslovStranice: "Novi dobavljač", + Tema: podesavanja["tema"], + NazivFirme: podesavanja["naziv_firme"], + Podnazlov: podesavanja["podnazlov"], + LogoTip: podesavanja["logo_tip"], + LogoPutanja: podesavanja["logo_putanja"], + Korisnik: "Admin", + }, + Dobavljac: dobavljac, + Greska: greska, + Izmena: false, + }) + return + } + + if _, err := h.DobavljaciRepo.Kreiraj(r.Context(), &dobavljac); err != nil { + http.Error(w, "Greška pri čuvanju dobavljača", http.StatusInternalServerError) + return + } + + http.Redirect(w, r, "/dobavljaci?sacuvano=1", http.StatusSeeOther) +} + +// IzmeniDobavljaca učitava dobavljača po ID-u i prikazuje popunjenu formu za izmenu +func (h *Handler) IzmeniDobavljaca(w http.ResponseWriter, r *http.Request) { + id, err := parseID(chi.URLParam(r, "id")) + if err != nil { + http.Error(w, "Neispravan ID dobavljača", http.StatusBadRequest) + return + } + + dobavljac, err := h.DobavljaciRepo.DohvatiID(r.Context(), id) + if err != nil { + http.Error(w, "Dobavljač nije pronađen", http.StatusNotFound) + return + } + + podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB) + if err != nil { + http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError) + return + } + + renderujFormuDobavljaca(w, PodaciFormeDobavljaca{ + PodaciStranice: model.PodaciStranice{ + Stranica: "dobavljaci", + NaslovStranice: "Izmeni dobavljača", + Tema: podesavanja["tema"], + NazivFirme: podesavanja["naziv_firme"], + Podnazlov: podesavanja["podnazlov"], + LogoTip: podesavanja["logo_tip"], + LogoPutanja: podesavanja["logo_putanja"], + Korisnik: "Admin", + }, + Dobavljac: *dobavljac, + Izmena: true, + }) +} + +// SacuvajIzmeneDobavljaca prima POST formu i ažurira postojećeg dobavljača u bazi +func (h *Handler) SacuvajIzmeneDobavljaca(w http.ResponseWriter, r *http.Request) { + id, err := parseID(chi.URLParam(r, "id")) + if err != nil { + http.Error(w, "Neispravan ID dobavljača", http.StatusBadRequest) + return + } + + if err := r.ParseForm(); err != nil { + http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest) + return + } + + dobavljac, greska := parseFormuDobavljaca(r) + if greska != "" { + podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB) + dobavljac.ID = id + renderujFormuDobavljaca(w, PodaciFormeDobavljaca{ + PodaciStranice: model.PodaciStranice{ + Stranica: "dobavljaci", + NaslovStranice: "Izmeni dobavljača", + Tema: podesavanja["tema"], + NazivFirme: podesavanja["naziv_firme"], + Podnazlov: podesavanja["podnazlov"], + LogoTip: podesavanja["logo_tip"], + LogoPutanja: podesavanja["logo_putanja"], + Korisnik: "Admin", + }, + Dobavljac: dobavljac, + Greska: greska, + Izmena: true, + }) + return + } + + dobavljac.ID = id + if err := h.DobavljaciRepo.Izmeni(r.Context(), &dobavljac); err != nil { + http.Error(w, "Greška pri čuvanju izmene", http.StatusInternalServerError) + return + } + + http.Redirect(w, r, "/dobavljaci?sacuvano=1", http.StatusSeeOther) +} + +// ObrisiDobavljaca prima POST zahtev i briše dobavljača po ID-u +func (h *Handler) ObrisiDobavljaca(w http.ResponseWriter, r *http.Request) { + id, err := parseID(chi.URLParam(r, "id")) + if err != nil { + http.Error(w, "Neispravan ID dobavljača", http.StatusBadRequest) + return + } + + if err := h.DobavljaciRepo.Obrisi(r.Context(), id); err != nil { + http.Error(w, "Greška pri brisanju dobavljača", http.StatusInternalServerError) + return + } + + http.Redirect(w, r, "/dobavljaci?obrisan=1", http.StatusSeeOther) +} + +// parseFormuDobavljaca čita polja iz HTTP forme, validira ih i vraća model i eventualnu grešku +func parseFormuDobavljaca(r *http.Request) (model.Dobavljac, string) { + naziv := strings.TrimSpace(r.FormValue("naziv")) + if naziv == "" { + return model.Dobavljac{}, "Naziv dobavljača je obavezan." + } + + email := strings.TrimSpace(r.FormValue("email")) + if email != "" && !strings.Contains(email, "@") { + return model.Dobavljac{}, "Adresa e-pošte nije ispravna." + } + + return model.Dobavljac{ + Naziv: naziv, + KontaktOsoba: strings.TrimSpace(r.FormValue("kontakt_osoba")), + Telefon: strings.TrimSpace(r.FormValue("telefon")), + Email: email, + Napomena: strings.TrimSpace(r.FormValue("napomena")), + }, "" +} + +// renderujFormuDobavljaca renderuje HTML šablon forme za unos ili izmenu dobavljača +func renderujFormuDobavljaca(w http.ResponseWriter, podaci PodaciFormeDobavljaca) { + tmpl, err := template.ParseFiles( + "web/templates/teme/podrazumevana/base.html", + "web/templates/komponente/sidebar.html", + "web/templates/komponente/topbar.html", + "web/templates/stranice/dobavljac_forma.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 + } + + 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) + } +} diff --git a/internal/handler/handler.go b/internal/handler/handler.go index 41e78e0..52892ce 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -12,6 +12,8 @@ type Handler struct { DB *sql.DB Artikli db.ArtikalRepository KategorijeRepo db.KategorijaRepository + DobavljaciRepo db.DobavljacRepository + NabavkeRepo db.NabavkaRepository } // Novi kreira novi Handler sa datom bazom @@ -20,5 +22,7 @@ func Novi(baza *sql.DB) *Handler { DB: baza, Artikli: sqlite.NoviArtikalRepo(baza), KategorijeRepo: sqlite.NovaKategorijaRepo(baza), + DobavljaciRepo: sqlite.NoviDobavljacRepo(baza), + NabavkeRepo: sqlite.NoviNabavkaRepo(baza), } } diff --git a/internal/handler/magacin_forma.go b/internal/handler/magacin_forma.go index 0208682..5987473 100644 --- a/internal/handler/magacin_forma.go +++ b/internal/handler/magacin_forma.go @@ -1,6 +1,7 @@ package handler import ( + "fmt" "html/template" "log" "net/http" @@ -89,11 +90,19 @@ func (h *Handler) SacuvajArtikal(w http.ResponseWriter, r *http.Request) { return } - if _, err := h.Artikli.Kreiraj(r.Context(), &artikal); err != nil { + id, err := h.Artikli.Kreiraj(r.Context(), &artikal) + if err != nil { http.Error(w, "Greška pri čuvanju artikla", http.StatusInternalServerError) return } + // fetch zahtev (iz modala) dobija JSON sa ID-em i nazivom novog artikla + if r.Header.Get("X-Requested-With") == "fetch" { + w.Header().Set("Content-Type", "application/json") + fmt.Fprintf(w, `{"id":%d,"naziv":%q}`, id, artikal.Naziv) + return + } + http.Redirect(w, r, "/magacin?sacuvano=1", http.StatusSeeOther) } diff --git a/internal/handler/nabavka.go b/internal/handler/nabavka.go new file mode 100644 index 0000000..65b1002 --- /dev/null +++ b/internal/handler/nabavka.go @@ -0,0 +1,348 @@ +package handler + +import ( + "encoding/json" + "html/template" + "log" + "net/http" + "strconv" + "strings" + + "ntech/internal/db" + "ntech/internal/db/sqlite" + "ntech/internal/model" + + "github.com/go-chi/chi/v5" +) + +// PodaciNabavki su podaci za stranicu sa listom nabavki +type PodaciNabavki struct { + model.PodaciStranice + Nabavke []model.NabavkaSaDetaljem + Sacuvano bool + Obrisan bool +} + +// PodaciFormeNabavke su podaci za formu unosa nove nabavke +type PodaciFormeNabavke struct { + model.PodaciStranice + Artikli []model.ArtikalSaKategorijom + ArtikliJSON template.JS // JSON niz artikala za Alpine.js — bezbedan za umetanje u + +
+ + + + + Nazad na nabavke + + +
+ + {{if .Greska}} +
+ {{.Greska}} +
+ {{end}} + + +
+
+ Nova nabavka +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ Stavke +
+ + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + +
ArtikalKoličinaCena/kom (din)Ukupno
Ukupno: + +
+
+ + + +
+ + +
+ + Odustani + + +
+ +
+ + +
+ + +
+ +
+{{end}} diff --git a/web/templates/stranice/nabavke.html b/web/templates/stranice/nabavke.html new file mode 100644 index 0000000..f1401b2 --- /dev/null +++ b/web/templates/stranice/nabavke.html @@ -0,0 +1,182 @@ +{{template "base" .}} + +{{define "naslov"}}Nabavke — NTech{{end}} + +{{define "dodatni-css"}} + +{{end}} + +{{define "sadrzaj"}} +
+ + {{if .Sacuvano}} +
Nabavka je uspešno sačuvana.
+ {{end}} + {{if .Obrisan}} +
Nabavka je uspešno obrisana.
+ {{end}} + + +
+ + + Nova nabavka + +
+ + +
+
+ + + + + + + + + + + + {{range .Nabavke}} + + + + + + + + {{else}} + + + + {{end}} + +
DatumDobavljačNapomenaUkupnoAkcije
+ {{.Datum.Format "02.01.2006."}} + + {{if .DobavljacNaziv}}{{.DobavljacNaziv}}{{else}}—{{end}} + + {{if .Napomena}}{{.Napomena}}{{else}}—{{end}} + + {{printf "%.2f" .Ukupno}} din + +
+ + Detalji + +
+ +
+
+
+ Nema nabavki. Dodaj prvu nabavku. +
+
+
+ + +
+ {{range .Nabavke}} +
+
+
+
+ {{if .DobavljacNaziv}}{{.DobavljacNaziv}}{{else}}Bez dobavljača{{end}} +
+
+ {{.Datum.Format "02.01.2006."}} +
+
+
+ {{printf "%.2f" .Ukupno}} din +
+
+ {{if .Napomena}} +
{{.Napomena}}
+ {{end}} +
+ + Detalji + +
+ +
+
+
+ {{else}} +
+ Nema nabavki. Dodaj prvu nabavku. +
+ {{end}} +
+ +
+{{end}}