diff --git a/cmd/ntech/main.go b/cmd/ntech/main.go index 3a27691..5513cf3 100644 --- a/cmd/ntech/main.go +++ b/cmd/ntech/main.go @@ -77,6 +77,12 @@ func main() { r.Get("/dobavljaci/izmeni/{id}", h.IzmeniDobavljaca) r.Post("/dobavljaci/izmeni/{id}", h.SacuvajIzmeneDobavljaca) r.Post("/dobavljaci/obrisi/{id}", h.ObrisiDobavljaca) + r.Get("/klijenti", h.Klijenti) + r.Get("/klijenti/novi", h.NoviKlijent) + r.Post("/klijenti/novi", h.SacuvajKlijenta) + r.Get("/klijenti/izmeni/{id}", h.IzmeniKlijenta) + r.Post("/klijenti/izmeni/{id}", h.SacuvajIzmenuKlijenta) + r.Post("/klijenti/obrisi/{id}", h.ObrisiKlijenta) 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 3f31dba..bacf7c7 100644 --- a/internal/db/repository.go +++ b/internal/db/repository.go @@ -45,3 +45,12 @@ type DobavljacRepository interface { Izmeni(ctx context.Context, d *model.Dobavljac) error Obrisi(ctx context.Context, id int64) error } + +// KlijentRepository definiše operacije nad klijentima +type KlijentRepository interface { + Lista(ctx context.Context, pretraga string) ([]model.Klijent, error) + DohvatiID(ctx context.Context, id int64) (*model.Klijent, error) + Kreiraj(ctx context.Context, k *model.Klijent) (int64, error) + Izmeni(ctx context.Context, k *model.Klijent) error + Obrisi(ctx context.Context, id int64) error +} diff --git a/internal/db/sqlite/klijent.go b/internal/db/sqlite/klijent.go new file mode 100644 index 0000000..06ee46d --- /dev/null +++ b/internal/db/sqlite/klijent.go @@ -0,0 +1,137 @@ +package sqlite + +import ( + "context" + "database/sql" + "fmt" + + "ntech/internal/model" +) + +// KlijentRepo je SQLite implementacija KlijentRepository interfejsa +type KlijentRepo struct { + db *sql.DB +} + +// NoviKlijentRepo kreira novi KlijentRepo +func NoviKlijentRepo(db *sql.DB) *KlijentRepo { + return &KlijentRepo{db: db} +} + +// Lista vraća listu klijenata sa opcionom pretragom po imenu, prezimenu ili nazivu firme +func (r *KlijentRepo) Lista(ctx context.Context, pretraga string) ([]model.Klijent, error) { + upit := ` + SELECT id, ime, prezime, naziv_firme, pib, telefon, email, napomena, datum_unosa + FROM klijenti + WHERE 1=1` + + args := []any{} + + if pretraga != "" { + upit += " AND (ime LIKE ? OR prezime LIKE ? OR naziv_firme LIKE ?)" + p := "%" + pretraga + "%" + args = append(args, p, p, p) + } + + upit += " ORDER BY datum_unosa DESC" + + redovi, err := r.db.QueryContext(ctx, upit, args...) + if err != nil { + return nil, fmt.Errorf("ntech: KlijentRepo.Lista: %w", err) + } + defer redovi.Close() + + var rezultat []model.Klijent + for redovi.Next() { + var k model.Klijent + var ime, prezime, nazivFirme, pib, telefon, email, napomena sql.NullString + err := redovi.Scan( + &k.ID, &ime, &prezime, &nazivFirme, &pib, &telefon, &email, &napomena, &k.DatumUnosa, + ) + if err != nil { + return nil, fmt.Errorf("ntech: KlijentRepo.Lista: scan: %w", err) + } + k.Ime = ime.String + k.Prezime = prezime.String + k.NazivFirme = nazivFirme.String + k.PIB = pib.String + k.Telefon = telefon.String + k.Email = email.String + k.Napomena = napomena.String + rezultat = append(rezultat, k) + } + + return rezultat, nil +} + +// DohvatiID vraća jednog klijenta po ID-u +func (r *KlijentRepo) DohvatiID(ctx context.Context, id int64) (*model.Klijent, error) { + var k model.Klijent + var ime, prezime, nazivFirme, pib, telefon, email, napomena sql.NullString + + err := r.db.QueryRowContext(ctx, ` + SELECT id, ime, prezime, naziv_firme, pib, telefon, email, napomena, datum_unosa + FROM klijenti WHERE id = ?`, id).Scan( + &k.ID, &ime, &prezime, &nazivFirme, &pib, &telefon, &email, &napomena, &k.DatumUnosa, + ) + if err != nil { + return nil, fmt.Errorf("ntech: KlijentRepo.DohvatiID: %w", err) + } + + k.Ime = ime.String + k.Prezime = prezime.String + k.NazivFirme = nazivFirme.String + k.PIB = pib.String + k.Telefon = telefon.String + k.Email = email.String + k.Napomena = napomena.String + + return &k, nil +} + +// Kreiraj dodaje novog klijenta u bazu +func (r *KlijentRepo) Kreiraj(ctx context.Context, k *model.Klijent) (int64, error) { + rezultat, err := r.db.ExecContext(ctx, ` + INSERT INTO klijenti (ime, prezime, naziv_firme, pib, telefon, email, napomena) + VALUES (?, ?, ?, ?, ?, ?, ?)`, + nullString(k.Ime), nullString(k.Prezime), nullString(k.NazivFirme), + nullString(k.PIB), nullString(k.Telefon), nullString(k.Email), nullString(k.Napomena), + ) + if err != nil { + return 0, fmt.Errorf("ntech: KlijentRepo.Kreiraj: %w", err) + } + + id, err := rezultat.LastInsertId() + if err != nil { + return 0, fmt.Errorf("ntech: KlijentRepo.Kreiraj: last insert id: %w", err) + } + + return id, nil +} + +// Izmeni ažurira postojećeg klijenta +func (r *KlijentRepo) Izmeni(ctx context.Context, k *model.Klijent) error { + _, err := r.db.ExecContext(ctx, ` + UPDATE klijenti SET + ime = ?, prezime = ?, naziv_firme = ?, pib = ?, telefon = ?, email = ?, napomena = ? + WHERE id = ?`, + nullString(k.Ime), nullString(k.Prezime), nullString(k.NazivFirme), + nullString(k.PIB), nullString(k.Telefon), nullString(k.Email), nullString(k.Napomena), + k.ID, + ) + if err != nil { + return fmt.Errorf("ntech: KlijentRepo.Izmeni: %w", err) + } + + return nil +} + +// Obrisi briše klijenta po ID-u +func (r *KlijentRepo) Obrisi(ctx context.Context, id int64) error { + _, err := r.db.ExecContext(ctx, "DELETE FROM klijenti WHERE id = ?", id) + if err != nil { + return fmt.Errorf("ntech: KlijentRepo.Obrisi: %w", err) + } + + return nil +} diff --git a/internal/handler/handler.go b/internal/handler/handler.go index 52892ce..a99beeb 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -14,6 +14,7 @@ type Handler struct { KategorijeRepo db.KategorijaRepository DobavljaciRepo db.DobavljacRepository NabavkeRepo db.NabavkaRepository + KlijentiRepo db.KlijentRepository } // Novi kreira novi Handler sa datom bazom @@ -24,5 +25,6 @@ func Novi(baza *sql.DB) *Handler { KategorijeRepo: sqlite.NovaKategorijaRepo(baza), DobavljaciRepo: sqlite.NoviDobavljacRepo(baza), NabavkeRepo: sqlite.NoviNabavkaRepo(baza), + KlijentiRepo: sqlite.NoviKlijentRepo(baza), } } diff --git a/internal/handler/klijent.go b/internal/handler/klijent.go new file mode 100644 index 0000000..962a054 --- /dev/null +++ b/internal/handler/klijent.go @@ -0,0 +1,313 @@ +package handler + +import ( + "html/template" + "log" + "net/http" + "strings" + + "ntech/internal/db/sqlite" + "ntech/internal/model" + + "github.com/go-chi/chi/v5" +) + +// PodaciKlijenata su podaci za stranicu sa listom klijenata +type PodaciKlijenata struct { + model.PodaciStranice + Klijenti []model.Klijent + Pretraga string + Sacuvano bool + Obrisan bool +} + +// PodaciFormeKlijenta su podaci za formu novog/izmenjenog klijenta +type PodaciFormeKlijenta struct { + model.PodaciStranice + Klijent model.Klijent + Greska string + Izmena bool +} + +// Klijenti renderuje listu svih klijenata sa opcionom pretragom +func (h *Handler) Klijenti(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") + + klijenti, err := h.KlijentiRepo.Lista(r.Context(), pretraga) + if err != nil { + http.Error(w, "Greška pri učitavanju klijenata", http.StatusInternalServerError) + return + } + + podaci := PodaciKlijenata{ + PodaciStranice: model.PodaciStranice{ + Stranica: "klijenti", + NaslovStranice: "Klijenti", + Tema: podesavanja["tema"], + NazivFirme: podesavanja["naziv_firme"], + Podnazlov: podesavanja["podnazlov"], + LogoTip: podesavanja["logo_tip"], + LogoPutanja: podesavanja["logo_putanja"], + Korisnik: "Admin", + }, + Klijenti: klijenti, + 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/klijenti.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) + } +} + +// NoviKlijent prikazuje praznu formu za unos novog klijenta +func (h *Handler) NoviKlijent(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 + } + + renderujFormuKlijenta(w, PodaciFormeKlijenta{ + PodaciStranice: model.PodaciStranice{ + Stranica: "klijenti", + NaslovStranice: "Novi klijent", + Tema: podesavanja["tema"], + NazivFirme: podesavanja["naziv_firme"], + Podnazlov: podesavanja["podnazlov"], + LogoTip: podesavanja["logo_tip"], + LogoPutanja: podesavanja["logo_putanja"], + Korisnik: "Admin", + }, + Izmena: false, + }) +} + +// SacuvajKlijenta prima POST formu i upisuje novog klijenta u bazu +func (h *Handler) SacuvajKlijenta(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest) + return + } + + klijent, greska := parseFormuKlijenta(r) + if greska != "" { + podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB) + renderujFormuKlijenta(w, PodaciFormeKlijenta{ + PodaciStranice: model.PodaciStranice{ + Stranica: "klijenti", + NaslovStranice: "Novi klijent", + Tema: podesavanja["tema"], + NazivFirme: podesavanja["naziv_firme"], + Podnazlov: podesavanja["podnazlov"], + LogoTip: podesavanja["logo_tip"], + LogoPutanja: podesavanja["logo_putanja"], + Korisnik: "Admin", + }, + Klijent: klijent, + Greska: greska, + Izmena: false, + }) + return + } + + if _, err := h.KlijentiRepo.Kreiraj(r.Context(), &klijent); err != nil { + log.Printf("greška pri čuvanju klijenta: %v", err) + podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB) + renderujFormuKlijenta(w, PodaciFormeKlijenta{ + PodaciStranice: model.PodaciStranice{ + Stranica: "klijenti", + NaslovStranice: "Novi klijent", + Tema: podesavanja["tema"], + NazivFirme: podesavanja["naziv_firme"], + Podnazlov: podesavanja["podnazlov"], + LogoTip: podesavanja["logo_tip"], + LogoPutanja: podesavanja["logo_putanja"], + Korisnik: "Admin", + }, + Klijent: klijent, + Greska: "Došlo je do greške pri čuvanju. Pokušajte ponovo.", + Izmena: false, + }) + return + } + + http.Redirect(w, r, "/klijenti?sacuvano=1", http.StatusSeeOther) +} + +// IzmeniKlijenta učitava klijenta po ID-u i prikazuje popunjenu formu za izmenu +func (h *Handler) IzmeniKlijenta(w http.ResponseWriter, r *http.Request) { + id, err := parseID(chi.URLParam(r, "id")) + if err != nil { + http.Error(w, "Neispravan ID klijenta", http.StatusBadRequest) + return + } + + klijent, err := h.KlijentiRepo.DohvatiID(r.Context(), id) + if err != nil { + http.Error(w, "Klijent 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 + } + + renderujFormuKlijenta(w, PodaciFormeKlijenta{ + PodaciStranice: model.PodaciStranice{ + Stranica: "klijenti", + NaslovStranice: "Izmeni klijenta", + Tema: podesavanja["tema"], + NazivFirme: podesavanja["naziv_firme"], + Podnazlov: podesavanja["podnazlov"], + LogoTip: podesavanja["logo_tip"], + LogoPutanja: podesavanja["logo_putanja"], + Korisnik: "Admin", + }, + Klijent: *klijent, + Izmena: true, + }) +} + +// SacuvajIzmenuKlijenta prima POST formu i ažurira postojećeg klijenta u bazi +func (h *Handler) SacuvajIzmenuKlijenta(w http.ResponseWriter, r *http.Request) { + id, err := parseID(chi.URLParam(r, "id")) + if err != nil { + http.Error(w, "Neispravan ID klijenta", http.StatusBadRequest) + return + } + + if err := r.ParseForm(); err != nil { + http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest) + return + } + + klijent, greska := parseFormuKlijenta(r) + if greska != "" { + podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB) + klijent.ID = id + renderujFormuKlijenta(w, PodaciFormeKlijenta{ + PodaciStranice: model.PodaciStranice{ + Stranica: "klijenti", + NaslovStranice: "Izmeni klijenta", + Tema: podesavanja["tema"], + NazivFirme: podesavanja["naziv_firme"], + Podnazlov: podesavanja["podnazlov"], + LogoTip: podesavanja["logo_tip"], + LogoPutanja: podesavanja["logo_putanja"], + Korisnik: "Admin", + }, + Klijent: klijent, + Greska: greska, + Izmena: true, + }) + return + } + + klijent.ID = id + if err := h.KlijentiRepo.Izmeni(r.Context(), &klijent); err != nil { + log.Printf("greška pri čuvanju izmene klijenta: %v", err) + podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB) + renderujFormuKlijenta(w, PodaciFormeKlijenta{ + PodaciStranice: model.PodaciStranice{ + Stranica: "klijenti", + NaslovStranice: "Izmeni klijenta", + Tema: podesavanja["tema"], + NazivFirme: podesavanja["naziv_firme"], + Podnazlov: podesavanja["podnazlov"], + LogoTip: podesavanja["logo_tip"], + LogoPutanja: podesavanja["logo_putanja"], + Korisnik: "Admin", + }, + Klijent: klijent, + Greska: "Došlo je do greške pri čuvanju. Pokušajte ponovo.", + Izmena: true, + }) + return + } + + http.Redirect(w, r, "/klijenti?sacuvano=1", http.StatusSeeOther) +} + +// ObrisiKlijenta prima POST zahtev i briše klijenta po ID-u +func (h *Handler) ObrisiKlijenta(w http.ResponseWriter, r *http.Request) { + id, err := parseID(chi.URLParam(r, "id")) + if err != nil { + http.Error(w, "Neispravan ID klijenta", http.StatusBadRequest) + return + } + + if err := h.KlijentiRepo.Obrisi(r.Context(), id); err != nil { + http.Error(w, "Greška pri brisanju klijenta", http.StatusInternalServerError) + return + } + + http.Redirect(w, r, "/klijenti?obrisan=1", http.StatusSeeOther) +} + +// parseFormuKlijenta čita polja iz HTTP forme, validira ih i vraća model i eventualnu grešku +func parseFormuKlijenta(r *http.Request) (model.Klijent, string) { + ime := strings.TrimSpace(r.FormValue("ime")) + nazivFirme := strings.TrimSpace(r.FormValue("naziv_firme")) + + if ime == "" && nazivFirme == "" { + return model.Klijent{}, "Mora biti uneto ime i prezime ili naziv firme." + } + + email := strings.TrimSpace(r.FormValue("email")) + if email != "" && !strings.Contains(email, "@") { + return model.Klijent{}, "Adresa e-pošte nije ispravna." + } + + return model.Klijent{ + Ime: ime, + Prezime: strings.TrimSpace(r.FormValue("prezime")), + NazivFirme: nazivFirme, + PIB: strings.TrimSpace(r.FormValue("pib")), + Telefon: strings.TrimSpace(r.FormValue("telefon")), + Email: email, + Napomena: strings.TrimSpace(r.FormValue("napomena")), + }, "" +} + +// renderujFormuKlijenta renderuje HTML šablon forme za unos ili izmenu klijenta +func renderujFormuKlijenta(w http.ResponseWriter, podaci PodaciFormeKlijenta) { + tmpl, err := template.ParseFiles( + "web/templates/teme/podrazumevana/base.html", + "web/templates/komponente/sidebar.html", + "web/templates/komponente/topbar.html", + "web/templates/stranice/klijent_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/klijent.go b/internal/model/klijent.go new file mode 100644 index 0000000..c431052 --- /dev/null +++ b/internal/model/klijent.go @@ -0,0 +1,16 @@ +package model + +import "time" + +// Klijent predstavlja jednog klijenta — fizičko lice ili firmu +type Klijent struct { + ID int64 + Ime string + Prezime string + NazivFirme string + PIB string + Telefon string + Email string + Napomena string + DatumUnosa time.Time +} diff --git a/migrations/014_klijenti_ime_nullable.sql b/migrations/014_klijenti_ime_nullable.sql new file mode 100644 index 0000000..29efe4b --- /dev/null +++ b/migrations/014_klijenti_ime_nullable.sql @@ -0,0 +1,18 @@ +-- uklanja NOT NULL sa ime i prezime jer klijent može biti samo firma +CREATE TABLE IF NOT EXISTS klijenti_novi ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + ime TEXT, + prezime TEXT, + naziv_firme TEXT, + pib TEXT, + telefon TEXT, + email TEXT, + napomena TEXT, + datum_unosa DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +INSERT INTO klijenti_novi SELECT id, ime, prezime, naziv_firme, pib, telefon, email, napomena, datum_unosa FROM klijenti; + +DROP TABLE klijenti; + +ALTER TABLE klijenti_novi RENAME TO klijenti; diff --git a/web/templates/stranice/klijent_forma.html b/web/templates/stranice/klijent_forma.html new file mode 100644 index 0000000..8eebd86 --- /dev/null +++ b/web/templates/stranice/klijent_forma.html @@ -0,0 +1,156 @@ +{{template "base" .}} + +{{define "naslov"}}{{if .Izmena}}Izmeni klijenta{{else}}Novi klijent{{end}} — NTech{{end}} + +{{define "dodatni-css"}} + +{{end}} + +{{define "sadrzaj"}} +