From 7800c3842d77e802929aa52720fc1e26500400e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dalibor=20Markovi=C4=87?= Date: Tue, 2 Jun 2026 00:15:30 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20dodata=20podr=C5=A1ka=20za=20servise?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dodana osnovna funkcionalnost za upravljanje servisima: - Novi model (`internal/model/service.go`) - SQLite repository (`internal/db/sqlite/service.go`) - Handler (`internal/handler/service.go`) - Tri nove stranice (lista, detalji, forma) Izmenjeni: - main.go - repository.go - utils.go - handler.go --- cmd/ntech/main.go | 7 + internal/db/repository.go | 10 + internal/db/sqlite/servis.go | 201 +++++++++ internal/db/sqlite/utils.go | 23 +- internal/handler/handler.go | 2 + internal/handler/servis.go | 468 +++++++++++++++++++++ internal/model/servis.go | 47 +++ web/templates/stranice/servis.html | 236 +++++++++++ web/templates/stranice/servis_detalji.html | 202 +++++++++ web/templates/stranice/servis_forma.html | 197 +++++++++ 10 files changed, 1392 insertions(+), 1 deletion(-) create mode 100644 internal/db/sqlite/servis.go create mode 100644 internal/handler/servis.go create mode 100644 internal/model/servis.go create mode 100644 web/templates/stranice/servis.html create mode 100644 web/templates/stranice/servis_detalji.html create mode 100644 web/templates/stranice/servis_forma.html diff --git a/cmd/ntech/main.go b/cmd/ntech/main.go index 5513cf3..acc3c7c 100644 --- a/cmd/ntech/main.go +++ b/cmd/ntech/main.go @@ -83,6 +83,13 @@ func main() { r.Get("/klijenti/izmeni/{id}", h.IzmeniKlijenta) r.Post("/klijenti/izmeni/{id}", h.SacuvajIzmenuKlijenta) r.Post("/klijenti/obrisi/{id}", h.ObrisiKlijenta) + r.Get("/servis", h.Servis) + r.Get("/servis/novi", h.NoviNalog) + r.Post("/servis/novi", h.SacuvajNalog) + r.Get("/servis/izmeni/{id}", h.IzmeniNalog) + r.Post("/servis/izmeni/{id}", h.SacuvajIzmenaNaloga) + r.Post("/servis/obrisi/{id}", h.ObrisiNalog) + r.Get("/servis/{id}", h.DetaljiNaloga) 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 bacf7c7..f2a271e 100644 --- a/internal/db/repository.go +++ b/internal/db/repository.go @@ -54,3 +54,13 @@ type KlijentRepository interface { Izmeni(ctx context.Context, k *model.Klijent) error Obrisi(ctx context.Context, id int64) error } + +// ServisRepository definiše operacije nad servisnim nalozima +type ServisRepository interface { + Lista(ctx context.Context, pretraga, status string) ([]model.ServisniNalogSaKlijentom, error) + DohvatiID(ctx context.Context, id int64) (*model.ServisniNalog, error) + Kreiraj(ctx context.Context, n *model.ServisniNalog) (int64, error) + Izmeni(ctx context.Context, n *model.ServisniNalog) error + Obrisi(ctx context.Context, id int64) error + SledeciBroj(ctx context.Context) (string, error) +} diff --git a/internal/db/sqlite/servis.go b/internal/db/sqlite/servis.go new file mode 100644 index 0000000..6b9ce10 --- /dev/null +++ b/internal/db/sqlite/servis.go @@ -0,0 +1,201 @@ +package sqlite + +import ( + "context" + "database/sql" + "fmt" + "time" + + "ntech/internal/model" +) + +// ServisRepo je SQLite implementacija ServisRepository interfejsa +type ServisRepo struct { + db *sql.DB +} + +// NoviServisRepo kreira novi ServisRepo +func NoviServisRepo(db *sql.DB) *ServisRepo { + return &ServisRepo{db: db} +} + +// SledeciBroj generiše sledeći broj naloga u formatu SN-GGGG-NNNN +func (r *ServisRepo) SledeciBroj(ctx context.Context) (string, error) { + godina := time.Now().Year() + uzorak := fmt.Sprintf("SN-%d-%%", godina) + + var sledeci int + err := r.db.QueryRowContext(ctx, ` + SELECT COALESCE(MAX(CAST(SUBSTR(broj_naloga, 9) AS INTEGER)), 0) + 1 + FROM servisni_nalozi + WHERE broj_naloga LIKE ?`, uzorak).Scan(&sledeci) + if err != nil { + return "", fmt.Errorf("ntech: ServisRepo.SledeciBroj: %w", err) + } + + return fmt.Sprintf("SN-%d-%04d", godina, sledeci), nil +} + +// Lista vraća listu servisnih naloga sa imenom klijenta, opcionim filterima +func (r *ServisRepo) Lista(ctx context.Context, pretraga, status string) ([]model.ServisniNalogSaKlijentom, error) { + upit := ` + SELECT + sn.id, sn.klijent_id, sn.broj_naloga, sn.uredjaj, sn.serijski_broj, + sn.opis_kvara, sn.status, sn.cena_od, sn.cena_do, sn.cena_konacna, + sn.avans, sn.napomena, sn.datum_prijema, sn.datum_zavrsetka, + COALESCE(NULLIF(k.naziv_firme, ''), TRIM(COALESCE(k.ime, '') || ' ' || COALESCE(k.prezime, '')), '') AS klijent_naziv + FROM servisni_nalozi sn + LEFT JOIN klijenti k ON k.id = sn.klijent_id + WHERE 1=1` + + args := []any{} + + if pretraga != "" { + upit += " AND (sn.broj_naloga LIKE ? OR sn.uredjaj LIKE ?)" + p := "%" + pretraga + "%" + args = append(args, p, p) + } + + if status != "" { + upit += " AND sn.status = ?" + args = append(args, status) + } + + upit += " ORDER BY sn.datum_prijema DESC" + + redovi, err := r.db.QueryContext(ctx, upit, args...) + if err != nil { + return nil, fmt.Errorf("ntech: ServisRepo.Lista: %w", err) + } + defer redovi.Close() + + var rezultat []model.ServisniNalogSaKlijentom + for redovi.Next() { + var n model.ServisniNalogSaKlijentom + err := scanNalog(redovi.Scan, &n.ServisniNalog, &n.KlijentNaziv) + if err != nil { + return nil, fmt.Errorf("ntech: ServisRepo.Lista: scan: %w", err) + } + rezultat = append(rezultat, n) + } + + return rezultat, nil +} + +// DohvatiID vraća jedan servisni nalog po ID-u +func (r *ServisRepo) DohvatiID(ctx context.Context, id int64) (*model.ServisniNalog, error) { + red := r.db.QueryRowContext(ctx, ` + SELECT + id, klijent_id, broj_naloga, uredjaj, serijski_broj, + opis_kvara, status, cena_od, cena_do, cena_konacna, + avans, napomena, datum_prijema, datum_zavrsetka + FROM servisni_nalozi WHERE id = ?`, id) + + var n model.ServisniNalog + err := scanNalog(red.Scan, &n, nil) + if err != nil { + return nil, fmt.Errorf("ntech: ServisRepo.DohvatiID: %w", err) + } + + return &n, nil +} + +// Kreiraj upisuje novi servisni nalog u bazu +func (r *ServisRepo) Kreiraj(ctx context.Context, n *model.ServisniNalog) (int64, error) { + rezultat, err := r.db.ExecContext(ctx, ` + INSERT INTO servisni_nalozi + (klijent_id, broj_naloga, uredjaj, serijski_broj, opis_kvara, + status, cena_od, cena_do, cena_konacna, avans, napomena, datum_zavrsetka) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + nullInt64(n.KlijentID), n.BrojNaloga, n.Uredjaj, nullString(n.SerijskiBroj), + n.OpisKvara, n.Status, nullFloat64(n.CenaOd), nullFloat64(n.CenaDo), + nullFloat64(n.CenaKonacna), nullFloat64(n.Avans), nullString(n.Napomena), + nullTime(n.DatumZavrsetka), + ) + if err != nil { + return 0, fmt.Errorf("ntech: ServisRepo.Kreiraj: %w", err) + } + + id, err := rezultat.LastInsertId() + if err != nil { + return 0, fmt.Errorf("ntech: ServisRepo.Kreiraj: last insert id: %w", err) + } + + return id, nil +} + +// Izmeni ažurira postojeći servisni nalog — broj_naloga i datum_prijema se ne menjaju +func (r *ServisRepo) Izmeni(ctx context.Context, n *model.ServisniNalog) error { + _, err := r.db.ExecContext(ctx, ` + UPDATE servisni_nalozi SET + klijent_id = ?, uredjaj = ?, serijski_broj = ?, opis_kvara = ?, + status = ?, cena_od = ?, cena_do = ?, cena_konacna = ?, + avans = ?, napomena = ?, datum_zavrsetka = ? + WHERE id = ?`, + nullInt64(n.KlijentID), n.Uredjaj, nullString(n.SerijskiBroj), n.OpisKvara, + n.Status, nullFloat64(n.CenaOd), nullFloat64(n.CenaDo), nullFloat64(n.CenaKonacna), + nullFloat64(n.Avans), nullString(n.Napomena), nullTime(n.DatumZavrsetka), + n.ID, + ) + if err != nil { + return fmt.Errorf("ntech: ServisRepo.Izmeni: %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) + if err != nil { + return fmt.Errorf("ntech: ServisRepo.Obrisi: %w", err) + } + + return nil +} + +// scanNalog čita redove iz upita u ServisniNalog struct — +// klijentNaziv je opcioni pokazivač, nil kada se čita bez JOIN-a +func scanNalog(scan func(...any) error, n *model.ServisniNalog, klijentNaziv *string) error { + var klijentID sql.NullInt64 + var serijskiBroj, napomena sql.NullString + var cenaOd, cenaDo, cenaKonacna, avans sql.NullFloat64 + var datumZavrsetka sql.NullTime + + args := []any{ + &n.ID, &klijentID, &n.BrojNaloga, &n.Uredjaj, &serijskiBroj, + &n.OpisKvara, &n.Status, &cenaOd, &cenaDo, &cenaKonacna, + &avans, &napomena, &n.DatumPrijema, &datumZavrsetka, + } + + if klijentNaziv != nil { + args = append(args, klijentNaziv) + } + + if err := scan(args...); err != nil { + return err + } + + if klijentID.Valid { + n.KlijentID = &klijentID.Int64 + } + n.SerijskiBroj = serijskiBroj.String + n.Napomena = napomena.String + if cenaOd.Valid { + n.CenaOd = &cenaOd.Float64 + } + if cenaDo.Valid { + n.CenaDo = &cenaDo.Float64 + } + if cenaKonacna.Valid { + n.CenaKonacna = &cenaKonacna.Float64 + } + if avans.Valid { + n.Avans = &avans.Float64 + } + if datumZavrsetka.Valid { + n.DatumZavrsetka = &datumZavrsetka.Time + } + + return nil +} diff --git a/internal/db/sqlite/utils.go b/internal/db/sqlite/utils.go index 1f74869..b61935c 100644 --- a/internal/db/sqlite/utils.go +++ b/internal/db/sqlite/utils.go @@ -1,6 +1,9 @@ package sqlite -import "database/sql" +import ( + "database/sql" + "time" +) // 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 @@ -16,3 +19,21 @@ func nullInt64(v *int64) sql.NullInt64 { } return sql.NullInt64{Int64: *v, Valid: true} } + +// nullFloat64 pretvara *float64 pokazivač u sql.NullFloat64 — +// koristi se za opciona numerička polja kao što su cene +func nullFloat64(v *float64) sql.NullFloat64 { + if v == nil { + return sql.NullFloat64{} + } + return sql.NullFloat64{Float64: *v, Valid: true} +} + +// nullTime pretvara *time.Time pokazivač u sql.NullTime — +// koristi se za opciona datumska polja kao što je datum završetka +func nullTime(v *time.Time) sql.NullTime { + if v == nil { + return sql.NullTime{} + } + return sql.NullTime{Time: *v, Valid: true} +} diff --git a/internal/handler/handler.go b/internal/handler/handler.go index a99beeb..4b91ac8 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -15,6 +15,7 @@ type Handler struct { DobavljaciRepo db.DobavljacRepository NabavkeRepo db.NabavkaRepository KlijentiRepo db.KlijentRepository + ServisRepo db.ServisRepository } // Novi kreira novi Handler sa datom bazom @@ -26,5 +27,6 @@ func Novi(baza *sql.DB) *Handler { DobavljaciRepo: sqlite.NoviDobavljacRepo(baza), NabavkeRepo: sqlite.NoviNabavkaRepo(baza), KlijentiRepo: sqlite.NoviKlijentRepo(baza), + ServisRepo: sqlite.NoviServisRepo(baza), } } diff --git a/internal/handler/servis.go b/internal/handler/servis.go new file mode 100644 index 0000000..496cc88 --- /dev/null +++ b/internal/handler/servis.go @@ -0,0 +1,468 @@ +package handler + +import ( + "html/template" + "log" + "net/http" + "strconv" + "strings" + "time" + + "ntech/internal/db/sqlite" + "ntech/internal/model" + + "github.com/go-chi/chi/v5" +) + +// PodaciServisa su podaci za stranicu sa listom servisnih naloga +type PodaciServisa struct { + model.PodaciStranice + Nalozi []model.ServisniNalogSaKlijentom + Pretraga string + FilterStatus string + SviStatusi []string + Sacuvano bool + Obrisan bool +} + +// PodaciFormeNaloga su podaci za formu novog/izmenjenog servisnog naloga +type PodaciFormeNaloga struct { + model.PodaciStranice + Nalog model.ServisniNalog + Klijenti []model.Klijent + SviStatusi []string + Greska string + Izmena bool +} + +// PodaciDetaljiNaloga su podaci za pregled jednog servisnog naloga +type PodaciDetaljiNaloga struct { + model.PodaciStranice + Nalog model.ServisniNalog + KlijentNaziv string + Sacuvano bool +} + +// Servis renderuje listu servisnih naloga sa opcionom pretragom i filterom statusa +func (h *Handler) Servis(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") + filterStatus := r.URL.Query().Get("status") + + nalozi, err := h.ServisRepo.Lista(r.Context(), pretraga, filterStatus) + if err != nil { + http.Error(w, "Greška pri učitavanju naloga", http.StatusInternalServerError) + return + } + + podaci := PodaciServisa{ + PodaciStranice: model.PodaciStranice{ + Stranica: "servis", + NaslovStranice: "Servis", + Tema: podesavanja["tema"], + NazivFirme: podesavanja["naziv_firme"], + Podnazlov: podesavanja["podnazlov"], + LogoTip: podesavanja["logo_tip"], + LogoPutanja: podesavanja["logo_putanja"], + Korisnik: "Admin", + }, + Nalozi: nalozi, + Pretraga: pretraga, + FilterStatus: filterStatus, + SviStatusi: model.SviStatusi, + 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/servis.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) + } +} + +// NoviNalog generiše broj naloga i prikazuje praznu formu za unos +func (h *Handler) NoviNalog(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 + } + + brojNaloga, err := h.ServisRepo.SledeciBroj(r.Context()) + if err != nil { + http.Error(w, "Greška pri generisanju broja naloga", http.StatusInternalServerError) + return + } + + klijenti, err := h.KlijentiRepo.Lista(r.Context(), "") + if err != nil { + http.Error(w, "Greška pri učitavanju klijenata", http.StatusInternalServerError) + return + } + + renderujFormuNaloga(w, PodaciFormeNaloga{ + PodaciStranice: model.PodaciStranice{ + Stranica: "servis", + NaslovStranice: "Novi nalog", + Tema: podesavanja["tema"], + NazivFirme: podesavanja["naziv_firme"], + Podnazlov: podesavanja["podnazlov"], + LogoTip: podesavanja["logo_tip"], + LogoPutanja: podesavanja["logo_putanja"], + Korisnik: "Admin", + }, + Nalog: model.ServisniNalog{BrojNaloga: brojNaloga, Status: model.StatusPrimljeno}, + Klijenti: klijenti, + SviStatusi: model.SviStatusi, + Izmena: false, + }) +} + +// SacuvajNalog prima POST formu i upisuje novi servisni nalog u bazu +func (h *Handler) SacuvajNalog(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest) + return + } + + nalog, greska := parseFormuNaloga(r) + if greska != "" { + podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB) + klijenti, _ := h.KlijentiRepo.Lista(r.Context(), "") + renderujFormuNaloga(w, PodaciFormeNaloga{ + PodaciStranice: model.PodaciStranice{ + Stranica: "servis", + NaslovStranice: "Novi nalog", + Tema: podesavanja["tema"], + NazivFirme: podesavanja["naziv_firme"], + Podnazlov: podesavanja["podnazlov"], + LogoTip: podesavanja["logo_tip"], + LogoPutanja: podesavanja["logo_putanja"], + Korisnik: "Admin", + }, + Nalog: nalog, + Klijenti: klijenti, + SviStatusi: model.SviStatusi, + Greska: greska, + Izmena: false, + }) + return + } + + id, err := h.ServisRepo.Kreiraj(r.Context(), &nalog) + if err != nil { + log.Printf("greška pri čuvanju naloga: %v", err) + podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB) + klijenti, _ := h.KlijentiRepo.Lista(r.Context(), "") + renderujFormuNaloga(w, PodaciFormeNaloga{ + PodaciStranice: model.PodaciStranice{ + Stranica: "servis", + NaslovStranice: "Novi nalog", + Tema: podesavanja["tema"], + NazivFirme: podesavanja["naziv_firme"], + Podnazlov: podesavanja["podnazlov"], + LogoTip: podesavanja["logo_tip"], + LogoPutanja: podesavanja["logo_putanja"], + Korisnik: "Admin", + }, + Nalog: nalog, + Klijenti: klijenti, + SviStatusi: model.SviStatusi, + Greska: "Došlo je do greške pri čuvanju. Pokušajte ponovo.", + Izmena: false, + }) + return + } + + http.Redirect(w, r, "/servis/"+strconv.FormatInt(id, 10)+"?sacuvano=1", http.StatusSeeOther) +} + +// IzmeniNalog učitava servisni nalog po ID-u i prikazuje popunjenu formu +func (h *Handler) IzmeniNalog(w http.ResponseWriter, r *http.Request) { + id, err := parseID(chi.URLParam(r, "id")) + if err != nil { + http.Error(w, "Neispravan ID naloga", http.StatusBadRequest) + return + } + + nalog, err := h.ServisRepo.DohvatiID(r.Context(), id) + if err != nil { + http.Error(w, "Nalog 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 + } + + klijenti, err := h.KlijentiRepo.Lista(r.Context(), "") + if err != nil { + http.Error(w, "Greška pri učitavanju klijenata", http.StatusInternalServerError) + return + } + + renderujFormuNaloga(w, PodaciFormeNaloga{ + PodaciStranice: model.PodaciStranice{ + Stranica: "servis", + NaslovStranice: "Izmeni nalog", + Tema: podesavanja["tema"], + NazivFirme: podesavanja["naziv_firme"], + Podnazlov: podesavanja["podnazlov"], + LogoTip: podesavanja["logo_tip"], + LogoPutanja: podesavanja["logo_putanja"], + Korisnik: "Admin", + }, + Nalog: *nalog, + Klijenti: klijenti, + SviStatusi: model.SviStatusi, + Izmena: true, + }) +} + +// SacuvajIzmenaNaloga prima POST formu i ažurira postojeći servisni nalog +func (h *Handler) SacuvajIzmenaNaloga(w http.ResponseWriter, r *http.Request) { + 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 + } + + nalog, greska := parseFormuNaloga(r) + if greska != "" { + podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB) + klijenti, _ := h.KlijentiRepo.Lista(r.Context(), "") + nalog.ID = id + renderujFormuNaloga(w, PodaciFormeNaloga{ + PodaciStranice: model.PodaciStranice{ + Stranica: "servis", + NaslovStranice: "Izmeni nalog", + Tema: podesavanja["tema"], + NazivFirme: podesavanja["naziv_firme"], + Podnazlov: podesavanja["podnazlov"], + LogoTip: podesavanja["logo_tip"], + LogoPutanja: podesavanja["logo_putanja"], + Korisnik: "Admin", + }, + Nalog: nalog, + Klijenti: klijenti, + SviStatusi: model.SviStatusi, + Greska: greska, + Izmena: true, + }) + return + } + + nalog.ID = id + if err := h.ServisRepo.Izmeni(r.Context(), &nalog); err != nil { + log.Printf("greška pri čuvanju izmene naloga: %v", err) + podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB) + klijenti, _ := h.KlijentiRepo.Lista(r.Context(), "") + renderujFormuNaloga(w, PodaciFormeNaloga{ + PodaciStranice: model.PodaciStranice{ + Stranica: "servis", + NaslovStranice: "Izmeni nalog", + Tema: podesavanja["tema"], + NazivFirme: podesavanja["naziv_firme"], + Podnazlov: podesavanja["podnazlov"], + LogoTip: podesavanja["logo_tip"], + LogoPutanja: podesavanja["logo_putanja"], + Korisnik: "Admin", + }, + Nalog: nalog, + Klijenti: klijenti, + SviStatusi: model.SviStatusi, + Greska: "Došlo je do greške pri čuvanju. Pokušajte ponovo.", + Izmena: true, + }) + return + } + + http.Redirect(w, r, "/servis/"+strconv.FormatInt(id, 10)+"?sacuvano=1", http.StatusSeeOther) +} + +// ObrisiNalog prima POST zahtev i briše servisni nalog po ID-u +func (h *Handler) ObrisiNalog(w http.ResponseWriter, r *http.Request) { + id, err := parseID(chi.URLParam(r, "id")) + if err != nil { + http.Error(w, "Neispravan ID naloga", http.StatusBadRequest) + return + } + + if err := h.ServisRepo.Obrisi(r.Context(), id); err != nil { + http.Error(w, "Greška pri brisanju naloga", http.StatusInternalServerError) + return + } + + http.Redirect(w, r, "/servis?obrisan=1", http.StatusSeeOther) +} + +// DetaljiNaloga prikazuje sve podatke jednog servisnog naloga +func (h *Handler) DetaljiNaloga(w http.ResponseWriter, r *http.Request) { + id, err := parseID(chi.URLParam(r, "id")) + if err != nil { + http.Error(w, "Neispravan ID naloga", http.StatusBadRequest) + return + } + + nalog, err := h.ServisRepo.DohvatiID(r.Context(), id) + if err != nil { + http.Error(w, "Nalog 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 + } + + klijentNaziv := "" + if nalog.KlijentID != nil { + klijent, err := h.KlijentiRepo.DohvatiID(r.Context(), *nalog.KlijentID) + if err == nil { + if klijent.NazivFirme != "" { + klijentNaziv = klijent.NazivFirme + } else { + klijentNaziv = strings.TrimSpace(klijent.Ime + " " + klijent.Prezime) + } + } + } + + podaci := PodaciDetaljiNaloga{ + PodaciStranice: model.PodaciStranice{ + Stranica: "servis", + NaslovStranice: "Detalji naloga", + Tema: podesavanja["tema"], + NazivFirme: podesavanja["naziv_firme"], + Podnazlov: podesavanja["podnazlov"], + LogoTip: podesavanja["logo_tip"], + LogoPutanja: podesavanja["logo_putanja"], + Korisnik: "Admin", + }, + Nalog: *nalog, + KlijentNaziv: klijentNaziv, + Sacuvano: r.URL.Query().Get("sacuvano") == "1", + } + + tmpl, err := template.ParseFiles( + "web/templates/teme/podrazumevana/base.html", + "web/templates/komponente/sidebar.html", + "web/templates/komponente/topbar.html", + "web/templates/stranice/servis_detalji.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) + } +} + +// parseFormuNaloga čita i validira polja iz HTTP forme +func parseFormuNaloga(r *http.Request) (model.ServisniNalog, string) { + uredjaj := strings.TrimSpace(r.FormValue("uredjaj")) + if uredjaj == "" { + return model.ServisniNalog{}, "Naziv uređaja je obavezan." + } + + opisKvara := strings.TrimSpace(r.FormValue("opis_kvara")) + if opisKvara == "" { + return model.ServisniNalog{}, "Opis kvara je obavezan." + } + + nalog := model.ServisniNalog{ + BrojNaloga: strings.TrimSpace(r.FormValue("broj_naloga")), + Uredjaj: uredjaj, + SerijskiBroj: strings.TrimSpace(r.FormValue("serijski_broj")), + OpisKvara: opisKvara, + Status: r.FormValue("status"), + Napomena: strings.TrimSpace(r.FormValue("napomena")), + } + + if nalog.Status == "" { + nalog.Status = model.StatusPrimljeno + } + + // opcioni klijent + if kidStr := r.FormValue("klijent_id"); kidStr != "" { + if kid, err := strconv.ParseInt(kidStr, 10, 64); err == nil { + nalog.KlijentID = &kid + } + } + + // opcione cene — prazno polje ostaje nil + nalog.CenaOd = parseOpcionuCenu(r.FormValue("cena_od")) + nalog.CenaDo = parseOpcionuCenu(r.FormValue("cena_do")) + nalog.CenaKonacna = parseOpcionuCenu(r.FormValue("cena_konacna")) + nalog.Avans = parseOpcionuCenu(r.FormValue("avans")) + + // opcioni datum završetka + if dz := strings.TrimSpace(r.FormValue("datum_zavrsetka")); dz != "" { + if t, err := time.Parse("2006-01-02", dz); err == nil { + nalog.DatumZavrsetka = &t + } + } + + return nalog, "" +} + +// parseOpcionuCenu pretvara string u *float64 — prazno polje ili neispravna vrednost vraća nil +func parseOpcionuCenu(s string) *float64 { + s = strings.TrimSpace(s) + if s == "" { + return nil + } + v, err := strconv.ParseFloat(s, 64) + if err != nil || v < 0 { + return nil + } + return &v +} + +// renderujFormuNaloga renderuje HTML šablon forme za unos ili izmenu servisnog naloga +func renderujFormuNaloga(w http.ResponseWriter, podaci PodaciFormeNaloga) { + tmpl, err := template.ParseFiles( + "web/templates/teme/podrazumevana/base.html", + "web/templates/komponente/sidebar.html", + "web/templates/komponente/topbar.html", + "web/templates/stranice/servis_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/servis.go b/internal/model/servis.go new file mode 100644 index 0000000..4a044f2 --- /dev/null +++ b/internal/model/servis.go @@ -0,0 +1,47 @@ +package model + +import "time" + +// Statusi servisnog naloga +const ( + StatusPrimljeno = "Primljeno" + StatusDijagnostika = "U dijagnostici" + StatusCekaDelove = "Čeka delove" + StatusUPopravci = "U popravci" + StatusZavrseno = "Završeno" + StatusPreuzeto = "Preuzeto" +) + +// SviStatusi je uređena lista statusa za prikaz u dropdownu +var SviStatusi = []string{ + StatusPrimljeno, + StatusDijagnostika, + StatusCekaDelove, + StatusUPopravci, + StatusZavrseno, + StatusPreuzeto, +} + +// ServisniNalog predstavlja jedan servisni nalog +type ServisniNalog struct { + ID int64 + KlijentID *int64 + BrojNaloga string + Uredjaj string + SerijskiBroj string + OpisKvara string + Status string + CenaOd *float64 + CenaDo *float64 + CenaKonacna *float64 + Avans *float64 + Napomena string + DatumPrijema time.Time + DatumZavrsetka *time.Time +} + +// ServisniNalogSaKlijentom proširuje ServisniNalog sa nazivom klijenta za prikaz u listi +type ServisniNalogSaKlijentom struct { + ServisniNalog + KlijentNaziv string +} diff --git a/web/templates/stranice/servis.html b/web/templates/stranice/servis.html new file mode 100644 index 0000000..7eddc61 --- /dev/null +++ b/web/templates/stranice/servis.html @@ -0,0 +1,236 @@ +{{template "base" .}} + +{{define "naslov"}}Servis — NTech{{end}} + +{{define "dodatni-css"}} + +{{end}} + +{{define "status-badge"}} +{{if eq . "Primljeno"}}{{.}} +{{else if eq . "U dijagnostici"}}{{.}} +{{else if eq . "Čeka delove"}}{{.}} +{{else if eq . "U popravci"}}{{.}} +{{else if eq . "Završeno"}}{{.}} +{{else if eq . "Preuzeto"}}{{.}} +{{else}}{{.}} +{{end}} +{{end}} + +{{define "sadrzaj"}} +
+ + {{if .Sacuvano}} +
Nalog je uspešno sačuvan.
+ {{end}} + {{if .Obrisan}} +
Nalog je uspešno obrisan.
+ {{end}} + + +
+ + + Novi nalog + +
+ + + +
+
+ + +
+
+ + + + + + + + + + + + + {{range .Nalozi}} + + + + + + + + + {{else}} + + + + {{end}} + +
Broj nalogaKlijentUređajStatusDatum prijemaAkcije
+ + {{.BrojNaloga}} + + + {{if .KlijentNaziv}}{{.KlijentNaziv}}{{else}}—{{end}} + {{.Uredjaj}} + {{template "status-badge" .Status}} + + {{.DatumPrijema.Format "02.01.2006."}} + +
+ + Izmeni + +
+ +
+
+
+ Nema servisnih naloga. Dodaj prvi nalog. +
+
+
+ + +
+ {{range .Nalozi}} +
+
+
+ + {{.BrojNaloga}} + +
{{.Uredjaj}}
+
+
+ {{template "status-badge" .Status}} +
+
+
+ {{if .KlijentNaziv}} +
+ Klijent: {{.KlijentNaziv}} +
+ {{end}} +
+ Primljeno: {{.DatumPrijema.Format "02.01.2006."}} +
+
+
+ + Izmeni + +
+ +
+
+
+ {{else}} +
+ Nema servisnih naloga. Dodaj prvi nalog. +
+ {{end}} +
+ +
+{{end}} diff --git a/web/templates/stranice/servis_detalji.html b/web/templates/stranice/servis_detalji.html new file mode 100644 index 0000000..ce5b6a7 --- /dev/null +++ b/web/templates/stranice/servis_detalji.html @@ -0,0 +1,202 @@ +{{template "base" .}} + +{{define "naslov"}}Detalji naloga — NTech{{end}} + +{{define "dodatni-css"}} + +{{end}} + +{{define "status-badge-detalji"}} +{{if eq . "Primljeno"}}{{.}} +{{else if eq . "U dijagnostici"}}{{.}} +{{else if eq . "Čeka delove"}}{{.}} +{{else if eq . "U popravci"}}{{.}} +{{else if eq . "Završeno"}}{{.}} +{{else if eq . "Preuzeto"}}{{.}} +{{else}}{{.}} +{{end}} +{{end}} + +{{define "sadrzaj"}} +
+ + {{if .Sacuvano}} +
Nalog je uspešno sačuvan.
+ {{end}} + + + + + Nazad na servis + + + +
+
+
+ + {{.Nalog.BrojNaloga}} + + {{template "status-badge-detalji" .Nalog.Status}} +
+ + Izmeni nalog + +
+
+
+
Datum prijema
+
{{.Nalog.DatumPrijema.Format "02.01.2006. u 15:04"}}
+
+ {{if .Nalog.DatumZavrsetka}} +
+
Datum završetka
+
{{.Nalog.DatumZavrsetka.Format "02.01.2006."}}
+
+ {{end}} +
+
Klijent
+
+ {{if .KlijentNaziv}}{{.KlijentNaziv}}{{else}}—{{end}} +
+
+
+
+ + +
+
+ Uređaj +
+
+
+
+
Naziv uređaja
+
{{.Nalog.Uredjaj}}
+
+
+
Serijski broj
+
+ {{if .Nalog.SerijskiBroj}}{{.Nalog.SerijskiBroj}}{{else}}—{{end}} +
+
+
+
+
Opis kvara
+
{{.Nalog.OpisKvara}}
+
+ {{if .Nalog.Napomena}} +
+
Napomena
+
{{.Nalog.Napomena}}
+
+ {{end}} +
+
+ + +
+
+ Cene +
+
+
+
Procena od
+
+ {{with .Nalog.CenaOd}}{{printf "%.2f" .}} din{{else}}—{{end}} +
+
+
+
Procena do
+
+ {{with .Nalog.CenaDo}}{{printf "%.2f" .}} din{{else}}—{{end}} +
+
+
+
Konačna cena
+
+ {{with .Nalog.CenaKonacna}}{{printf "%.2f" .}} din{{else}}—{{end}} +
+
+
+
Avans
+
+ {{with .Nalog.Avans}}{{printf "%.2f" .}} din{{else}}—{{end}} +
+
+
+
+ + +
+
+ + Istorija promena statusa i vezani artikli biće dostupni u narednoj verziji. +
+
+ + +
+
+
+
Brisanje naloga
+
+ Brisanje je trajno. Nalog i svi vezani podaci biće trajno uklonjeni. +
+
+
+ +
+
+
+ +
+{{end}} diff --git a/web/templates/stranice/servis_forma.html b/web/templates/stranice/servis_forma.html new file mode 100644 index 0000000..eb5daef --- /dev/null +++ b/web/templates/stranice/servis_forma.html @@ -0,0 +1,197 @@ +{{template "base" .}} + +{{define "naslov"}}{{if .Izmena}}Izmeni nalog{{else}}Novi nalog{{end}} — NTech{{end}} + +{{define "dodatni-css"}} + +{{end}} + +{{define "sadrzaj"}} +
+ + + + + Nazad na servis + + +
+
+ + {{if .Izmena}}Izmeni nalog{{else}}Novi nalog{{end}} + + + {{.Nalog.BrojNaloga}} + +
+ + {{if .Greska}} +
+ {{.Greska}} +
+ {{end}} + +
+ + + + +
+ + +
+
Uređaj
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ + +
+
Klijent
+ +
+ + +
+
Status
+
+
+ + +
+
+ + +
+
+
+ + +
+
Cene (din)
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+ + +
+ + +
+ + Odustani + + +
+ +
+
+
+
+{{end}}