diff --git a/cmd/ntech/main.go b/cmd/ntech/main.go index b12082b..f8c8a15 100644 --- a/cmd/ntech/main.go +++ b/cmd/ntech/main.go @@ -57,6 +57,15 @@ func main() { r.Get("/podesavanja", h.Podesavanja) r.Post("/podesavanja/sacuvaj", h.SacuvajPodesavanja) r.Get("/tema/{tema}", h.PromeniTemu) + r.Get("/magacin", h.Magacin) + r.Get("/magacin/novi", h.NoviArtikal) + r.Post("/magacin/novi", h.SacuvajArtikal) + r.Get("/magacin/izmeni/{id}", h.IzmeniArtikal) + r.Post("/magacin/izmeni/{id}", h.SacuvajIzmenuArtikla) + r.Get("/magacin/obrisi/{id}", h.ObrisiArtikal) + r.Get("/magacin/kategorije", h.Kategorije) + r.Post("/magacin/kategorije/dodaj", h.DodajKategoriju) + r.Get("/magacin/kategorije/obrisi/{id}", h.ObrisiKategoriju) 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 new file mode 100644 index 0000000..826790a --- /dev/null +++ b/internal/db/repository.go @@ -0,0 +1,29 @@ +package db + +import ( + "context" + + "ntech/internal/model" +) + +// ArtikalRepository definiše operacije nad artiklima +type ArtikalRepository interface { + Lista(ctx context.Context, filter ArtikalFilter) ([]model.ArtikalSaKategorijom, error) + DohvatiID(ctx context.Context, id int64) (*model.Artikal, error) + Kreiraj(ctx context.Context, a *model.Artikal) (int64, error) + Izmeni(ctx context.Context, a *model.Artikal) error + Obrisi(ctx context.Context, id int64) error +} + +// KategorijaRepository definiše operacije nad kategorijama +type KategorijaRepository interface { + Lista(ctx context.Context) ([]model.Kategorija, error) + Kreiraj(ctx context.Context, k *model.Kategorija) (int64, error) +} + +// ArtikalFilter definiše parametre za filtriranje liste artikala +type ArtikalFilter struct { + Pretraga string + KategorijaID *int64 + SamoKriticni bool +} diff --git a/internal/db/sqlite/artikal.go b/internal/db/sqlite/artikal.go new file mode 100644 index 0000000..16fedc6 --- /dev/null +++ b/internal/db/sqlite/artikal.go @@ -0,0 +1,158 @@ +package sqlite + +import ( + "context" + "database/sql" + "fmt" + + "ntech/internal/db" + "ntech/internal/model" +) + +// ArtikalRepo je SQLite implementacija ArtikalRepository interfejsa +type ArtikalRepo struct { + db *sql.DB +} + +// NoviArtikalRepo kreira novi ArtikalRepo +func NoviArtikalRepo(db *sql.DB) *ArtikalRepo { + return &ArtikalRepo{db: db} +} + +// Lista vraća listu artikala sa opcionalnim filterima +func (r *ArtikalRepo) Lista(ctx context.Context, filter db.ArtikalFilter) ([]model.ArtikalSaKategorijom, error) { + upit := ` + SELECT + a.id, a.kategorija_id, a.naziv, a.opis, + a.kolicina, a.kolicina_min, a.lokacija, + a.nabavna_cena, a.prodajna_cena, a.napomena, a.datum_unosa, + COALESCE(k.naziv, '') as kategorija_naziv + FROM artikli a + LEFT JOIN kategorije k ON a.kategorija_id = k.id + WHERE 1=1` + + args := []any{} + + if filter.Pretraga != "" { + upit += " AND a.naziv LIKE ?" + args = append(args, "%"+filter.Pretraga+"%") + } + + if filter.KategorijaID != nil { + upit += " AND a.kategorija_id = ?" + args = append(args, *filter.KategorijaID) + } + + if filter.SamoKriticni { + upit += " AND a.kolicina <= a.kolicina_min" + } + + upit += " ORDER BY a.naziv ASC" + + redovi, err := r.db.QueryContext(ctx, upit, args...) + if err != nil { + return nil, fmt.Errorf("ntech: ArtikalRepo.Lista: %w", err) + } + defer redovi.Close() + + var rezultat []model.ArtikalSaKategorijom + for redovi.Next() { + var a model.ArtikalSaKategorijom + var kategorijaID sql.NullInt64 + + err := redovi.Scan( + &a.ID, &kategorijaID, &a.Naziv, &a.Opis, + &a.Kolicina, &a.KolicinMin, &a.Lokacija, + &a.NabavnaCena, &a.ProdajnaCena, &a.Napomena, &a.DatumUnosa, + &a.KategorijaNaziv, + ) + if err != nil { + return nil, fmt.Errorf("ntech: ArtikalRepo.Lista: scan: %w", err) + } + + if kategorijaID.Valid { + a.KategorijaID = &kategorijaID.Int64 + } + + a.KriticnaZaliha = a.Kolicina <= a.KolicinMin + + rezultat = append(rezultat, a) + } + + return rezultat, nil +} + +// DohvatiID vraća jedan artikal po ID-u +func (r *ArtikalRepo) DohvatiID(ctx context.Context, id int64) (*model.Artikal, error) { + var a model.Artikal + var kategorijaID sql.NullInt64 + + err := r.db.QueryRowContext(ctx, ` + SELECT id, kategorija_id, naziv, opis, kolicina, kolicina_min, + lokacija, nabavna_cena, prodajna_cena, napomena, datum_unosa + FROM artikli WHERE id = ?`, id).Scan( + &a.ID, &kategorijaID, &a.Naziv, &a.Opis, + &a.Kolicina, &a.KolicinMin, &a.Lokacija, + &a.NabavnaCena, &a.ProdajnaCena, &a.Napomena, &a.DatumUnosa, + ) + if err != nil { + return nil, fmt.Errorf("ntech: ArtikalRepo.DohvatiID: %w", err) + } + + if kategorijaID.Valid { + a.KategorijaID = &kategorijaID.Int64 + } + + return &a, nil +} + +// Kreiraj dodaje novi artikal u bazu +func (r *ArtikalRepo) Kreiraj(ctx context.Context, a *model.Artikal) (int64, error) { + rezultat, err := r.db.ExecContext(ctx, ` + INSERT INTO artikli + (kategorija_id, naziv, opis, kolicina, kolicina_min, lokacija, + nabavna_cena, prodajna_cena, napomena) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + a.KategorijaID, a.Naziv, a.Opis, a.Kolicina, a.KolicinMin, + a.Lokacija, a.NabavnaCena, a.ProdajnaCena, a.Napomena, + ) + if err != nil { + return 0, fmt.Errorf("ntech: ArtikalRepo.Kreiraj: %w", err) + } + + id, err := rezultat.LastInsertId() + if err != nil { + return 0, fmt.Errorf("ntech: ArtikalRepo.Kreiraj: last insert id: %w", err) + } + + return id, nil +} + +// Izmeni ažurira postojeći artikal +func (r *ArtikalRepo) Izmeni(ctx context.Context, a *model.Artikal) error { + _, err := r.db.ExecContext(ctx, ` + UPDATE artikli SET + kategorija_id = ?, naziv = ?, opis = ?, kolicina = ?, + kolicina_min = ?, lokacija = ?, nabavna_cena = ?, + prodajna_cena = ?, napomena = ? + WHERE id = ?`, + a.KategorijaID, a.Naziv, a.Opis, a.Kolicina, + a.KolicinMin, a.Lokacija, a.NabavnaCena, + a.ProdajnaCena, a.Napomena, a.ID, + ) + if err != nil { + return fmt.Errorf("ntech: ArtikalRepo.Izmeni: %w", err) + } + + return nil +} + +// Obrisi briše artikal po ID-u +func (r *ArtikalRepo) Obrisi(ctx context.Context, id int64) error { + _, err := r.db.ExecContext(ctx, "DELETE FROM artikli WHERE id = ?", id) + if err != nil { + return fmt.Errorf("ntech: ArtikalRepo.Obrisi: %w", err) + } + + return nil +} diff --git a/internal/db/sqlite/kategorija.go b/internal/db/sqlite/kategorija.go new file mode 100644 index 0000000..3a83ce6 --- /dev/null +++ b/internal/db/sqlite/kategorija.go @@ -0,0 +1,61 @@ +package sqlite + +import ( + "context" + "database/sql" + "fmt" + + "ntech/internal/model" +) + +// KategorijaRepo je SQLite implementacija KategorijaRepository interfejsa +type KategorijaRepo struct { + db *sql.DB +} + +// NovaKategorijaRepo kreira novi KategorijaRepo +func NovaKategorijaRepo(db *sql.DB) *KategorijaRepo { + return &KategorijaRepo{db: db} +} + +// Lista vraća sve kategorije +func (r *KategorijaRepo) Lista(ctx context.Context) ([]model.Kategorija, error) { + redovi, err := r.db.QueryContext(ctx, "SELECT id, naziv, opis FROM kategorije ORDER BY naziv ASC") + if err != nil { + return nil, fmt.Errorf("ntech: KategorijaRepo.Lista: %w", err) + } + defer redovi.Close() + + var rezultat []model.Kategorija + for redovi.Next() { + var k model.Kategorija + var opis sql.NullString + if err := redovi.Scan(&k.ID, &k.Naziv, &opis); err != nil { + return nil, fmt.Errorf("ntech: KategorijaRepo.Lista: scan: %w", err) + } + if opis.Valid { + k.Opis = opis.String + } + rezultat = append(rezultat, k) + } + + return rezultat, nil +} + +// Kreiraj dodaje novu kategoriju +func (r *KategorijaRepo) Kreiraj(ctx context.Context, k *model.Kategorija) (int64, error) { + rezultat, err := r.db.ExecContext(ctx, + "INSERT INTO kategorije (naziv, opis) VALUES (?, ?)", + k.Naziv, k.Opis, + ) + if err != nil { + return 0, fmt.Errorf("ntech: KategorijaRepo.Kreiraj: %w", err) + } + + id, err := rezultat.LastInsertId() + if err != nil { + return 0, fmt.Errorf("ntech: KategorijaRepo.Kreiraj: last insert id: %w", err) + } + + return id, nil +} diff --git a/internal/handler/handler.go b/internal/handler/handler.go index d022c19..41e78e0 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -1,13 +1,24 @@ package handler -import "database/sql" +import ( + "database/sql" + + "ntech/internal/db" + "ntech/internal/db/sqlite" +) // Handler drži zavisnosti koje su potrebne svim handlerima type Handler struct { - DB *sql.DB + DB *sql.DB + Artikli db.ArtikalRepository + KategorijeRepo db.KategorijaRepository } // Novi kreira novi Handler sa datom bazom -func Novi(db *sql.DB) *Handler { - return &Handler{DB: db} +func Novi(baza *sql.DB) *Handler { + return &Handler{ + DB: baza, + Artikli: sqlite.NoviArtikalRepo(baza), + KategorijeRepo: sqlite.NovaKategorijaRepo(baza), + } } diff --git a/internal/handler/kategorija.go b/internal/handler/kategorija.go new file mode 100644 index 0000000..3a26e83 --- /dev/null +++ b/internal/handler/kategorija.go @@ -0,0 +1,112 @@ +package handler + +import ( + "html/template" + "log" + "net/http" + "strconv" + + "ntech/internal/db/sqlite" + "ntech/internal/model" + + "github.com/go-chi/chi/v5" +) + +// PodaciKategorija su podaci za stranicu kategorija +type PodaciKategorija struct { + model.PodaciStranice + Kategorije []model.Kategorija + Sacuvano bool + Obrisana bool +} + +// Kategorije renderuje listu kategorija +func (h *Handler) Kategorije(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 + } + + kategorije, err := h.KategorijeRepo.Lista(r.Context()) + if err != nil { + http.Error(w, "Greška pri učitavanju kategorija", http.StatusInternalServerError) + return + } + + podaci := PodaciKategorija{ + PodaciStranice: model.PodaciStranice{ + Stranica: "magacin", + NaslovStranice: "Kategorije", + Tema: podesavanja["tema"], + NazivFirme: podesavanja["naziv_firme"], + Podnazlov: podesavanja["podnazlov"], + LogoTip: podesavanja["logo_tip"], + LogoPutanja: podesavanja["logo_putanja"], + Korisnik: "Admin", + }, + Kategorije: kategorije, + Sacuvano: r.URL.Query().Get("sacuvano") == "1", + Obrisana: r.URL.Query().Get("obrisana") == "1", + } + + tmpl, err := template.ParseFiles( + "web/templates/teme/podrazumevana/base.html", + "web/templates/komponente/sidebar.html", + "web/templates/komponente/topbar.html", + "web/templates/stranice/kategorije.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) + } +} + +// DodajKategoriju prima POST i čuva novu kategoriju +func (h *Handler) DodajKategoriju(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest) + return + } + + naziv := r.FormValue("naziv") + if naziv == "" { + http.Redirect(w, r, "/magacin/kategorije", http.StatusSeeOther) + return + } + + k := &model.Kategorija{ + Naziv: naziv, + Opis: r.FormValue("opis"), + } + + if _, err := h.KategorijeRepo.Kreiraj(r.Context(), k); err != nil { + http.Error(w, "Greška pri čuvanju kategorije", http.StatusInternalServerError) + return + } + + http.Redirect(w, r, "/magacin/kategorije?sacuvano=1", http.StatusSeeOther) +} + +// ObrisiKategoriju briše kategoriju po ID-u +func (h *Handler) ObrisiKategoriju(w http.ResponseWriter, r *http.Request) { + idStr := chi.URLParam(r, "id") + id, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + http.Error(w, "Neispravan ID kategorije", http.StatusBadRequest) + return + } + + if _, err := h.DB.ExecContext(r.Context(), "DELETE FROM kategorije WHERE id = ?", id); err != nil { + http.Error(w, "Greška pri brisanju kategorije", http.StatusInternalServerError) + return + } + + http.Redirect(w, r, "/magacin/kategorije?obrisana=1", http.StatusSeeOther) +} diff --git a/internal/handler/magacin.go b/internal/handler/magacin.go new file mode 100644 index 0000000..af26b62 --- /dev/null +++ b/internal/handler/magacin.go @@ -0,0 +1,114 @@ +package handler + +import ( + "html/template" + "log" + "net/http" + "strconv" + + "ntech/internal/db" + "ntech/internal/db/sqlite" + "ntech/internal/model" + + "github.com/go-chi/chi/v5" +) + +// PodaciMagacina su podaci za stranicu magacina +type PodaciMagacina struct { + model.PodaciStranice + Artikli []model.ArtikalSaKategorijom + Kategorije []model.Kategorija + Filter db.ArtikalFilter + KategorijaIDStr string + Sacuvano bool + Obrisan bool +} + +// Magacin renderuje listu artikala +func (h *Handler) Magacin(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 + } + + filter := db.ArtikalFilter{ + Pretraga: r.URL.Query().Get("pretraga"), + SamoKriticni: r.URL.Query().Get("kriticni") == "1", + } + + katIDStr := "" + if katID := r.URL.Query().Get("kategorija"); katID != "" { + id, err := strconv.ParseInt(katID, 10, 64) + if err == nil { + filter.KategorijaID = &id + katIDStr = katID + } + } + + artikli, err := h.Artikli.Lista(r.Context(), filter) + if err != nil { + http.Error(w, "Greška pri učitavanju artikala", http.StatusInternalServerError) + return + } + + kategorije, err := h.KategorijeRepo.Lista(r.Context()) + if err != nil { + http.Error(w, "Greška pri učitavanju kategorija", http.StatusInternalServerError) + return + } + + podaci := PodaciMagacina{ + PodaciStranice: model.PodaciStranice{ + Stranica: "magacin", + NaslovStranice: "Magacin", + Tema: podesavanja["tema"], + NazivFirme: podesavanja["naziv_firme"], + Podnazlov: podesavanja["podnazlov"], + LogoTip: podesavanja["logo_tip"], + LogoPutanja: podesavanja["logo_putanja"], + Korisnik: "Admin", + }, + Artikli: artikli, + Kategorije: kategorije, + Filter: filter, + KategorijaIDStr: katIDStr, + 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/magacin.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) + return + } +} + +// ObrisiArtikal briše artikal po ID-u +func (h *Handler) ObrisiArtikal(w http.ResponseWriter, r *http.Request) { + idStr := chi.URLParam(r, "id") + id, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + http.Error(w, "Neispravan ID artikla", http.StatusBadRequest) + return + } + + if err := h.Artikli.Obrisi(r.Context(), id); err != nil { + http.Error(w, "Greška pri brisanju artikla", http.StatusInternalServerError) + return + } + + http.Redirect(w, r, "/magacin?obrisan=1", http.StatusSeeOther) +} diff --git a/internal/handler/magacin_forma.go b/internal/handler/magacin_forma.go new file mode 100644 index 0000000..06928ee --- /dev/null +++ b/internal/handler/magacin_forma.go @@ -0,0 +1,277 @@ +package handler + +import ( + "html/template" + "log" + "net/http" + "strconv" + + "ntech/internal/db/sqlite" + "ntech/internal/model" + + "github.com/go-chi/chi/v5" +) + +// PodaciFormeArtikla su podaci za formu novog/izmenjenog artikla +type PodaciFormeArtikla struct { + model.PodaciStranice + Artikal model.Artikal + Kategorije []model.Kategorija + KategorijaIDStr string + Greska string + Izmena bool +} + +// NoviArtikal prikazuje formu za unos novog artikla +func (h *Handler) NoviArtikal(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 + } + + kategorije, err := h.KategorijeRepo.Lista(r.Context()) + if err != nil { + http.Error(w, "Greška pri učitavanju kategorija", http.StatusInternalServerError) + return + } + + podaci := PodaciFormeArtikla{ + PodaciStranice: model.PodaciStranice{ + Stranica: "magacin", + NaslovStranice: "Novi artikal", + Tema: podesavanja["tema"], + NazivFirme: podesavanja["naziv_firme"], + Podnazlov: podesavanja["podnazlov"], + LogoTip: podesavanja["logo_tip"], + LogoPutanja: podesavanja["logo_putanja"], + Korisnik: "Admin", + }, + Kategorije: kategorije, + Izmena: false, + } + + renderujFormuArtikla(w, podaci) +} + +// SacuvajArtikal prima POST formu i čuva novi artikal +func (h *Handler) SacuvajArtikal(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest) + return + } + + artikal, greska := parseFormuArtikla(r) + if greska != "" { + podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB) + kategorije, _ := h.KategorijeRepo.Lista(r.Context()) + katIDStr := "" + if artikal.KategorijaID != nil { + katIDStr = strconv.FormatInt(*artikal.KategorijaID, 10) + } + renderujFormuArtikla(w, PodaciFormeArtikla{ + PodaciStranice: model.PodaciStranice{ + Stranica: "magacin", + NaslovStranice: "Novi artikal", + Tema: podesavanja["tema"], + NazivFirme: podesavanja["naziv_firme"], + Podnazlov: podesavanja["podnazlov"], + LogoTip: podesavanja["logo_tip"], + LogoPutanja: podesavanja["logo_putanja"], + Korisnik: "Admin", + }, + Artikal: artikal, + Kategorije: kategorije, + KategorijaIDStr: katIDStr, + Greska: greska, + Izmena: false, + }) + return + } + + if _, err := h.Artikli.Kreiraj(r.Context(), &artikal); err != nil { + http.Error(w, "Greška pri čuvanju artikla", http.StatusInternalServerError) + return + } + + http.Redirect(w, r, "/magacin?sacuvano=1", http.StatusSeeOther) +} + +// IzmeniArtikal prikazuje formu za izmenu postojećeg artikla +func (h *Handler) IzmeniArtikal(w http.ResponseWriter, r *http.Request) { + idStr := chi.URLParam(r, "id") + id, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + http.Error(w, "Neispravan ID artikla", http.StatusBadRequest) + return + } + + artikal, err := h.Artikli.DohvatiID(r.Context(), id) + if err != nil { + http.Error(w, "Artikal 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 + } + + kategorije, err := h.KategorijeRepo.Lista(r.Context()) + if err != nil { + http.Error(w, "Greška pri učitavanju kategorija", http.StatusInternalServerError) + return + } + + katIDStr := "" + if artikal.KategorijaID != nil { + katIDStr = strconv.FormatInt(*artikal.KategorijaID, 10) + } + + podaci := PodaciFormeArtikla{ + PodaciStranice: model.PodaciStranice{ + Stranica: "magacin", + NaslovStranice: "Izmeni artikal", + Tema: podesavanja["tema"], + NazivFirme: podesavanja["naziv_firme"], + Podnazlov: podesavanja["podnazlov"], + LogoTip: podesavanja["logo_tip"], + LogoPutanja: podesavanja["logo_putanja"], + Korisnik: "Admin", + }, + Artikal: *artikal, + Kategorije: kategorije, + KategorijaIDStr: katIDStr, + Izmena: true, + } + + renderujFormuArtikla(w, podaci) +} + +// SacuvajIzmenuArtikla prima POST formu i čuva izmenu artikla +func (h *Handler) SacuvajIzmenuArtikla(w http.ResponseWriter, r *http.Request) { + idStr := chi.URLParam(r, "id") + id, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + http.Error(w, "Neispravan ID artikla", http.StatusBadRequest) + return + } + + if err := r.ParseForm(); err != nil { + http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest) + return + } + + artikal, greska := parseFormuArtikla(r) + if greska != "" { + podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB) + kategorije, _ := h.KategorijeRepo.Lista(r.Context()) + artikal.ID = id + katIDStr := "" + if artikal.KategorijaID != nil { + katIDStr = strconv.FormatInt(*artikal.KategorijaID, 10) + } + renderujFormuArtikla(w, PodaciFormeArtikla{ + PodaciStranice: model.PodaciStranice{ + Stranica: "magacin", + NaslovStranice: "Izmeni artikal", + Tema: podesavanja["tema"], + NazivFirme: podesavanja["naziv_firme"], + Podnazlov: podesavanja["podnazlov"], + LogoTip: podesavanja["logo_tip"], + LogoPutanja: podesavanja["logo_putanja"], + Korisnik: "Admin", + }, + Artikal: artikal, + Kategorije: kategorije, + KategorijaIDStr: katIDStr, + Greska: greska, + Izmena: true, + }) + return + } + + artikal.ID = id + if err := h.Artikli.Izmeni(r.Context(), &artikal); err != nil { + http.Error(w, "Greška pri čuvanju izmene", http.StatusInternalServerError) + return + } + + http.Redirect(w, r, "/magacin?sacuvano=1", http.StatusSeeOther) +} + +// parseFormuArtikla čita polja iz forme i vraća artikal i eventualnu grešku +func parseFormuArtikla(r *http.Request) (model.Artikal, string) { + naziv := r.FormValue("naziv") + if naziv == "" { + return model.Artikal{}, "Naziv artikla je obavezan." + } + + var artikal model.Artikal + artikal.Naziv = naziv + artikal.Opis = r.FormValue("opis") + artikal.Lokacija = r.FormValue("lokacija") + artikal.Napomena = r.FormValue("napomena") + + if k := r.FormValue("kolicina"); k != "" { + v, err := strconv.Atoi(k) + if err != nil || v < 0 { + return artikal, "Količina mora biti pozitivan broj." + } + artikal.Kolicina = v + } + + if k := r.FormValue("kolicina_min"); k != "" { + v, err := strconv.Atoi(k) + if err != nil || v < 0 { + return artikal, "Minimalna količina mora biti pozitivan broj." + } + artikal.KolicinMin = v + } + + if c := r.FormValue("nabavna_cena"); c != "" { + v, err := strconv.ParseFloat(c, 64) + if err != nil || v < 0 { + return artikal, "Nabavna cena mora biti pozitivan broj." + } + artikal.NabavnaCena = v + } + + if c := r.FormValue("prodajna_cena"); c != "" { + v, err := strconv.ParseFloat(c, 64) + if err != nil || v < 0 { + return artikal, "Prodajna cena mora biti pozitivan broj." + } + artikal.ProdajnaCena = v + } + + if katID := r.FormValue("kategorija_id"); katID != "" { + id, err := strconv.ParseInt(katID, 10, 64) + if err == nil { + artikal.KategorijaID = &id + } + } + + return artikal, "" +} + +// renderujFormuArtikla renderuje HTML formu za artikal +func renderujFormuArtikla(w http.ResponseWriter, podaci PodaciFormeArtikla) { + tmpl, err := template.ParseFiles( + "web/templates/teme/podrazumevana/base.html", + "web/templates/komponente/sidebar.html", + "web/templates/komponente/topbar.html", + "web/templates/stranice/magacin_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/model/artikal.go b/internal/model/artikal.go new file mode 100644 index 0000000..a4f67f5 --- /dev/null +++ b/internal/model/artikal.go @@ -0,0 +1,32 @@ +package model + +import "time" + +// Artikal predstavlja jedan artikal u magacinu +type Artikal struct { + ID int64 + KategorijaID *int64 + Naziv string + Opis string + Kolicina int + KolicinMin int + Lokacija string + NabavnaCena float64 + ProdajnaCena float64 + Napomena string + DatumUnosa time.Time +} + +// Kategorija predstavlja kategoriju artikala +type Kategorija struct { + ID int64 + Naziv string + Opis string +} + +// ArtikalSaKategorijom je artikal sa nazivom kategorije — za prikaz u tabeli +type ArtikalSaKategorijom struct { + Artikal + KategorijaNaziv string + KriticnaZaliha bool // true ako je kolicina <= kolicina_min +} diff --git a/web/static/css/main.css b/web/static/css/main.css index 9702d7d..eff23e4 100644 --- a/web/static/css/main.css +++ b/web/static/css/main.css @@ -393,3 +393,54 @@ select:focus { .tema-krug-aktivan { border-color: var(--tekst-glavni) !important; } + +/* forme — responsive */ +@media (max-width: 768px) { + .forma-grid-2 { + grid-template-columns: 1fr !important; + } +} + +/* gornja traka magacina — responsive */ +.magacin-traka { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; +} + +.magacin-traka form { + display: flex; + align-items: center; + gap: 8px; + flex: 1; + min-width: 200px; + flex-wrap: wrap; +} + +@media (max-width: 768px) { + .magacin-traka { + flex-direction: column; + align-items: stretch; + } + + .magacin-traka form { + flex-direction: column; + align-items: stretch; + } + + .magacin-traka a { + text-align: center; + } +} + +/* dodatna korekcija magacin trake */ +@media (max-width: 768px) { + .magacin-traka form input, + .magacin-traka form select, + .magacin-traka form button, + .magacin-traka form label { + width: 100%; + justify-content: flex-start; + } +} diff --git a/web/templates/stranice/kategorije.html b/web/templates/stranice/kategorije.html new file mode 100644 index 0000000..bcdaa4a --- /dev/null +++ b/web/templates/stranice/kategorije.html @@ -0,0 +1,77 @@ +{{template "base" .}} + +{{define "naslov"}}Kategorije — NTech{{end}} + +{{define "sadrzaj"}} +