From 974d76360a5af602bdc034d0d4a1d36ed109749c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dalibor=20Markovi=C4=87?= Date: Wed, 3 Jun 2026 21:18:12 +0200 Subject: [PATCH] =?UTF-8?q?Optimizacije=20=E2=80=94=20SQLite=20WAL,=20temp?= =?UTF-8?q?late=20ke=C5=A1,=20gzip=20kompresija,=20build=20skript?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 1 + .gitignore | 3 +- build.sh | 15 ++ cmd/ntech/main.go | 29 ++- internal/config/firstrun.go | 6 +- internal/db/repository.go | 16 ++ internal/db/sqlite/migracije.go | 15 +- internal/db/sqlite/podsetnici.go | 152 +++++++++++ internal/handler/admin.go | 24 +- internal/handler/dashboard.go | 41 ++- internal/handler/dobavljac.go | 46 +--- internal/handler/handler.go | 13 +- internal/handler/izvestaji.go | 16 +- internal/handler/kategorija.go | 19 +- internal/handler/kes.go | 119 +++++++++ internal/handler/klijent.go | 49 +--- internal/handler/magacin.go | 20 +- internal/handler/magacin_forma.go | 29 +-- internal/handler/nabavka.go | 58 +---- internal/handler/podesavanja.go | 102 ++++++-- internal/handler/podsetnici.go | 267 ++++++++++++++++++++ internal/handler/prijava.go | 17 +- internal/handler/prodaja.go | 69 +---- internal/handler/servis.go | 66 +---- internal/model/podsetnik.go | 22 ++ internal/model/stranica.go | 15 +- web/static/css/main.css | 47 ++-- web/static/css/teme/ljubicasta.css | 15 -- web/static/css/teme/svetla.css | 32 ++- web/static/css/teme/tamna.css | 32 ++- web/static/css/teme/zelena.css | 15 -- web/static/uploads/logo.svg | 29 +++ web/templates/komponente/sidebar.html | 22 +- web/templates/komponente/topbar.html | 6 +- web/templates/stranice/admin_korisnici.html | 42 ++- web/templates/stranice/admin_profil.html | 11 +- web/templates/stranice/dashboard.html | 51 +++- web/templates/stranice/izvestaji.html | 30 ++- web/templates/stranice/kategorije.html | 42 ++- web/templates/stranice/magacin.html | 26 +- web/templates/stranice/podesavanja.html | 68 +++-- web/templates/stranice/podsetnici.html | 217 ++++++++++++++++ web/templates/stranice/podsetnik_forma.html | 99 ++++++++ web/templates/stranice/prijava.html | 12 +- web/templates/stranice/setup.html | 12 +- web/templates/stranice/totp_provera.html | 12 +- 46 files changed, 1470 insertions(+), 579 deletions(-) create mode 100644 .env create mode 100755 build.sh create mode 100644 internal/db/sqlite/podsetnici.go create mode 100644 internal/handler/kes.go create mode 100644 internal/handler/podsetnici.go create mode 100644 internal/model/podsetnik.go delete mode 100644 web/static/css/teme/ljubicasta.css delete mode 100644 web/static/css/teme/zelena.css create mode 100644 web/static/uploads/logo.svg create mode 100644 web/templates/stranice/podsetnici.html create mode 100644 web/templates/stranice/podsetnik_forma.html diff --git a/.env b/.env new file mode 100644 index 0000000..516a787 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +NTECH_PORT=3000 diff --git a/.gitignore b/.gitignore index 3c23cef..d27bd86 100644 --- a/.gitignore +++ b/.gitignore @@ -8,8 +8,7 @@ *.db-wal # Promenljive okruženja -.env -.env.* +ntech.env # Privremeni fajlovi *.tmp diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..4372a9c --- /dev/null +++ b/build.sh @@ -0,0 +1,15 @@ +Napravi build.sh skriptu u korenu projekta sa sledećim sadržajem: + +#!/bin/bash +VERSION=${1:-"dev"} +echo "Buildovanje NTech v$VERSION..." +CGO_ENABLED=0 GOARCH=amd64 GOOS=linux go build -ldflags "-X main.Verzija=$VERSION -s -w" -o ntech ./cmd/ntech +if [ $? -eq 0 ]; then + echo "Build završen: ntech v$VERSION" + ls -lh ntech +else + echo "Build neuspešan" + exit 1 +fi + + diff --git a/cmd/ntech/main.go b/cmd/ntech/main.go index 4cc06eb..7d2d22f 100644 --- a/cmd/ntech/main.go +++ b/cmd/ntech/main.go @@ -16,11 +16,15 @@ import ( ntechmw "ntech/internal/middleware" "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" "github.com/joho/godotenv" ) +// Verzija se postavlja pri produkcijskom buildu: go build -ldflags "-X main.Verzija=1.2.0" +var Verzija = "dev" + func main() { - godotenv.Load() + godotenv.Load("ntech.env") if config.JelPrvoPokretanje() { config.PokreniSetup() @@ -60,9 +64,22 @@ func main() { } }() + os.MkdirAll("web/static/uploads", 0755) + h := handler.Novi(db) + h.Verzija = Verzija + + if os.Getenv("NTECH_ENV") == "production" { + kes, err := handler.KreirajKes() + if err != nil { + log.Fatalf("Greška pri kreiranju keša šablona: %v", err) + } + h.Templates = kes + log.Printf("Keš šablona kreiran: %d šablona", len(kes)) + } r := chi.NewRouter() + r.Use(middleware.Compress(5)) // statični fajlovi r.Handle("/static/*", http.StripPrefix("/static/", http.FileServer(http.Dir("web/static")))) @@ -86,6 +103,7 @@ func main() { r.Get("/dashboard", h.Dashboard) r.Get("/podesavanja", h.Podesavanja) r.Post("/podesavanja/sacuvaj", h.SacuvajPodesavanja) + r.Post("/podesavanja/logo", h.OtpremiLogo) r.Get("/podesavanja/backup", h.BackupBaze) r.Get("/tema/{tema}", h.PromeniTemu) r.Get("/magacin", h.Magacin) @@ -129,6 +147,15 @@ func main() { r.Get("/prodaja/{id}/stampa", h.StampaProdaje) r.Get("/prodaja/{id}", h.DetaljiProdaje) + // podsetnici + r.Get("/podsetnici", h.Podsetnici) + r.Get("/podsetnici/novi", h.NoviPodsetnik) + r.Post("/podsetnici/novi", h.SacuvajPodsetnik) + r.Get("/podsetnici/izmeni/{id}", h.IzmeniPodsetnik) + r.Post("/podsetnici/izmeni/{id}", h.SacuvajIzmenePodsetnika) + r.Post("/podsetnici/zavrseno/{id}", h.OznaciPodsetnik) + r.Post("/podsetnici/obrisi/{id}", h.ObrisiPodsetnik) + // admin rute r.Get("/admin/korisnici", h.AdminKorisnici) r.Post("/admin/korisnici/novi", h.AdminSacuvajKorisnika) diff --git a/internal/config/firstrun.go b/internal/config/firstrun.go index 025d424..3fc40c9 100644 --- a/internal/config/firstrun.go +++ b/internal/config/firstrun.go @@ -33,7 +33,7 @@ func NadjiSlobodanPort() int { // proverava da li je ovo prvo pokretanje programa func JelPrvoPokretanje() bool { - _, err := os.Stat(".env") + _, err := os.Stat("ntech.env") return os.IsNotExist(err) } @@ -55,8 +55,8 @@ func StatusPortova() []PortStatus { return rezultat } -// SacuvajEnv upisuje izabrani port u .env fajl +// SacuvajEnv upisuje izabrani port u ntech.env fajl func SacuvajEnv(port int) error { sadrzaj := fmt.Sprintf("NTECH_PORT=%d\n", port) - return os.WriteFile(".env", []byte(sadrzaj), 0600) + return os.WriteFile("ntech.env", []byte(sadrzaj), 0600) } diff --git a/internal/db/repository.go b/internal/db/repository.go index 8074350..6fcfc5f 100644 --- a/internal/db/repository.go +++ b/internal/db/repository.go @@ -97,3 +97,19 @@ type SesijeRepository interface { Obrisi(ctx context.Context, token string) error ObrisiIstekle(ctx context.Context) error } + +// PodsetnikFilter definiše parametre za filtriranje liste podsetnika +type PodsetnikFilter struct { + SamoAktivni bool // true = samo nezavršeni; false = svi +} + +// PodsetnikRepository definiše operacije nad podsetnicima +type PodsetnikRepository interface { + Lista(ctx context.Context, filter PodsetnikFilter) ([]model.Podsetnik, error) + DohvatiID(ctx context.Context, id int64) (*model.Podsetnik, error) + Kreiraj(ctx context.Context, p *model.Podsetnik) (int64, error) + Izmeni(ctx context.Context, p *model.Podsetnik) error + OznaciZavrsenim(ctx context.Context, id int64, zavrseno bool) error + Obrisi(ctx context.Context, id int64) error + BrojAktivnih(ctx context.Context) (int, error) +} diff --git a/internal/db/sqlite/migracije.go b/internal/db/sqlite/migracije.go index 69f757e..4532f02 100644 --- a/internal/db/sqlite/migracije.go +++ b/internal/db/sqlite/migracije.go @@ -10,16 +10,23 @@ import ( _ "modernc.org/sqlite" ) -// OtvoriDB otvara konekciju ka SQLite bazi i uključuje strane ključeve +// OtvoriDB otvara konekciju ka SQLite bazi i primenjuje performance PRAGMA podešavanja func OtvoriDB(putanja string) (*sql.DB, error) { db, err := sql.Open("sqlite", putanja) if err != nil { return nil, fmt.Errorf("ntech: OtvoriDB: %w", err) } - // uključujemo podršku za strane ključeve — SQLite je ne uključuje automatski - if _, err := db.Exec("PRAGMA foreign_keys = ON"); err != nil { - return nil, fmt.Errorf("ntech: OtvoriDB: foreign_keys: %w", err) + pragme := []string{ + "PRAGMA journal_mode=WAL", + "PRAGMA synchronous=NORMAL", + "PRAGMA cache_size=10000", + "PRAGMA foreign_keys=ON", + } + for _, p := range pragme { + if _, err := db.Exec(p); err != nil { + return nil, fmt.Errorf("ntech: OtvoriDB: %s: %w", p, err) + } } return db, nil diff --git a/internal/db/sqlite/podsetnici.go b/internal/db/sqlite/podsetnici.go new file mode 100644 index 0000000..a66e5fb --- /dev/null +++ b/internal/db/sqlite/podsetnici.go @@ -0,0 +1,152 @@ +package sqlite + +import ( + "context" + "database/sql" + "fmt" + + "ntech/internal/db" + "ntech/internal/model" +) + +// PodsetnikRepo je SQLite implementacija PodsetnikRepository interfejsa +type PodsetnikRepo struct { + db *sql.DB +} + +// NoviPodsetnikRepo kreira novi PodsetnikRepo +func NoviPodsetnikRepo(db *sql.DB) *PodsetnikRepo { + return &PodsetnikRepo{db: db} +} + +// Lista vraća listu podsetnika sa opcionim filterom +func (r *PodsetnikRepo) Lista(ctx context.Context, filter db.PodsetnikFilter) ([]model.Podsetnik, error) { + upit := ` + SELECT id, naslov, napomena, datum_podsecanja, zavrseno, tip, datum_unosa + FROM podsetnici + WHERE 1=1` + + if filter.SamoAktivni { + upit += " AND zavrseno = 0" + } + + upit += " ORDER BY datum_podsecanja ASC" + + redovi, err := r.db.QueryContext(ctx, upit) + if err != nil { + return nil, fmt.Errorf("ntech: PodsetnikRepo.Lista: %w", err) + } + defer redovi.Close() + + var rezultat []model.Podsetnik + for redovi.Next() { + var p model.Podsetnik + var napomena sql.NullString + var zavrseno int + err := redovi.Scan( + &p.ID, &p.Naslov, &napomena, &p.DatumPodsecanja, &zavrseno, &p.Tip, &p.DatumUnosa, + ) + if err != nil { + return nil, fmt.Errorf("ntech: PodsetnikRepo.Lista: scan: %w", err) + } + p.Napomena = napomena.String + p.Zavrseno = zavrseno == 1 + rezultat = append(rezultat, p) + } + + return rezultat, nil +} + +// DohvatiID vraća jedan podsetnik po ID-u +func (r *PodsetnikRepo) DohvatiID(ctx context.Context, id int64) (*model.Podsetnik, error) { + var p model.Podsetnik + var napomena sql.NullString + var zavrseno int + + err := r.db.QueryRowContext(ctx, ` + SELECT id, naslov, napomena, datum_podsecanja, zavrseno, tip, datum_unosa + FROM podsetnici WHERE id = ?`, id).Scan( + &p.ID, &p.Naslov, &napomena, &p.DatumPodsecanja, &zavrseno, &p.Tip, &p.DatumUnosa, + ) + if err != nil { + return nil, fmt.Errorf("ntech: PodsetnikRepo.DohvatiID: %w", err) + } + + p.Napomena = napomena.String + p.Zavrseno = zavrseno == 1 + + return &p, nil +} + +// Kreiraj dodaje novi podsetnik u bazu +func (r *PodsetnikRepo) Kreiraj(ctx context.Context, p *model.Podsetnik) (int64, error) { + rezultat, err := r.db.ExecContext(ctx, ` + INSERT INTO podsetnici (naslov, napomena, datum_podsecanja, tip) + VALUES (?, ?, ?, ?)`, + p.Naslov, nullString(p.Napomena), p.DatumPodsecanja, p.Tip, + ) + if err != nil { + return 0, fmt.Errorf("ntech: PodsetnikRepo.Kreiraj: %w", err) + } + + id, err := rezultat.LastInsertId() + if err != nil { + return 0, fmt.Errorf("ntech: PodsetnikRepo.Kreiraj: last insert id: %w", err) + } + + return id, nil +} + +// Izmeni ažurira postojeći podsetnik +func (r *PodsetnikRepo) Izmeni(ctx context.Context, p *model.Podsetnik) error { + _, err := r.db.ExecContext(ctx, ` + UPDATE podsetnici SET + naslov = ?, napomena = ?, datum_podsecanja = ?, tip = ? + WHERE id = ?`, + p.Naslov, nullString(p.Napomena), p.DatumPodsecanja, p.Tip, p.ID, + ) + if err != nil { + return fmt.Errorf("ntech: PodsetnikRepo.Izmeni: %w", err) + } + + return nil +} + +// OznaciZavrsenim postavlja ili uklanja oznaku završenosti +func (r *PodsetnikRepo) OznaciZavrsenim(ctx context.Context, id int64, zavrseno bool) error { + val := 0 + if zavrseno { + val = 1 + } + _, err := r.db.ExecContext(ctx, + "UPDATE podsetnici SET zavrseno = ? WHERE id = ?", val, id, + ) + if err != nil { + return fmt.Errorf("ntech: PodsetnikRepo.OznaciZavrsenim: %w", err) + } + + return nil +} + +// Obrisi briše podsetnik po ID-u +func (r *PodsetnikRepo) Obrisi(ctx context.Context, id int64) error { + _, err := r.db.ExecContext(ctx, "DELETE FROM podsetnici WHERE id = ?", id) + if err != nil { + return fmt.Errorf("ntech: PodsetnikRepo.Obrisi: %w", err) + } + + return nil +} + +// BrojAktivnih vraća broj nezavršenih podsetnika +func (r *PodsetnikRepo) BrojAktivnih(ctx context.Context) (int, error) { + var broj int + err := r.db.QueryRowContext(ctx, + "SELECT COUNT(*) FROM podsetnici WHERE zavrseno = 0", + ).Scan(&broj) + if err != nil { + return 0, fmt.Errorf("ntech: PodsetnikRepo.BrojAktivnih: %w", err) + } + + return broj, nil +} diff --git a/internal/handler/admin.go b/internal/handler/admin.go index 33db7ff..9af4b14 100644 --- a/internal/handler/admin.go +++ b/internal/handler/admin.go @@ -56,7 +56,7 @@ func (h *Handler) AdminKorisnici(w http.ResponseWriter, r *http.Request) { Sacuvano: r.URL.Query().Get("sacuvano") == "1", } - renderujAdminTemplate(w, "web/templates/stranice/admin_korisnici.html", podaci) + h.renderujTemplate(w, "admin_korisnici", podaci) } // AdminSacuvajKorisnika kreira novog korisnika @@ -196,7 +196,7 @@ func (h *Handler) AdminProfil(w http.ResponseWriter, r *http.Request) { TotpAktivan: svezi.TotpTajna != "", } - renderujAdminTemplate(w, "web/templates/stranice/admin_profil.html", podaci) + h.renderujTemplate(w, "admin_profil", podaci) } // AdminPromeniLozinku menja lozinku prijavljenog korisnika @@ -265,7 +265,7 @@ func (h *Handler) AdminTotpPokreni(w http.ResponseWriter, r *http.Request) { TotpQR: template.URL("data:image/png;base64," + totp.QRBase64), } - renderujAdminTemplate(w, "web/templates/stranice/admin_profil.html", podaci) + h.renderujTemplate(w, "admin_profil", podaci) } // AdminTotpAktivacija verifikuje TOTP kod i čuva tajnu @@ -305,7 +305,7 @@ func (h *Handler) AdminTotpAktivacija(w http.ResponseWriter, r *http.Request) { TotpQR: template.URL("data:image/png;base64," + qr), Greska: "totp", } - renderujAdminTemplate(w, "web/templates/stranice/admin_profil.html", podaci) + h.renderujTemplate(w, "admin_profil", podaci) return } @@ -333,22 +333,6 @@ func (h *Handler) AdminTotpDeaktivacija(w http.ResponseWriter, r *http.Request) http.Redirect(w, r, "/admin/profil?sacuvano=totp_off", http.StatusSeeOther) } -func renderujAdminTemplate(w http.ResponseWriter, stranica string, podaci any) { - tmpl, err := template.ParseFiles( - "web/templates/teme/podrazumevana/base.html", - "web/templates/komponente/sidebar.html", - "web/templates/komponente/topbar.html", - stranica, - ) - if err != nil { - http.Error(w, "Greška pri učitavanju stranice", http.StatusInternalServerError) - return - } - if err := tmpl.ExecuteTemplate(w, "base", podaci); err != nil { - http.Error(w, "Greška pri prikazu stranice", http.StatusInternalServerError) - } -} - // parseBoolForm čita boolean vrednost iz forme func parseBoolForm(s string) bool { b, _ := strconv.ParseBool(s) diff --git a/internal/handler/dashboard.go b/internal/handler/dashboard.go index 7418f1d..f09dbf8 100644 --- a/internal/handler/dashboard.go +++ b/internal/handler/dashboard.go @@ -1,7 +1,6 @@ package handler import ( - "html/template" "log" "net/http" "time" @@ -20,7 +19,7 @@ func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) { return } - var brojArtikala, aktivniServisi, kriticnaZaliha int + var brojArtikala, aktivniServisi, kriticnaZaliha, aktivniPodsetnici int var prihodOvogMeseca float64 if err := h.DB.QueryRowContext(ctx, @@ -49,6 +48,12 @@ func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) { log.Printf("dashboard: kriticna zaliha: %v", err) } + if n, err := h.PodsetniciFRepo.BrojAktivnih(ctx); err != nil { + log.Printf("dashboard: aktivni podsetnici: %v", err) + } else { + aktivniPodsetnici = n + } + // poslednjih 5 servisnih naloga sa datumom prijema servisRedovi, err := h.DB.QueryContext(ctx, ` SELECT uredjaj, status, datum_prijema FROM servisni_nalozi @@ -132,31 +137,17 @@ func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) { LogoPutanja: podesavanja["logo_putanja"], Korisnik: "Admin", }, - BrojArtikala: brojArtikala, - AktivniServisi: aktivniServisi, - PrihodOvogMeseca: prihodOvogMeseca, - KriticnaZaliha: kriticnaZaliha, - PoslednjiServisi: poslednjiServisi, - KriticneZalihe: kriticneZalihe, - PoslednjeProdaje: poslednjeProdaje, + BrojArtikala: brojArtikala, + AktivniServisi: aktivniServisi, + PrihodOvogMeseca: prihodOvogMeseca, + KriticnaZaliha: kriticnaZaliha, + AktivniPodsetnici: aktivniPodsetnici, + PoslednjiServisi: poslednjiServisi, + KriticneZalihe: kriticneZalihe, + PoslednjeProdaje: poslednjeProdaje, } - tmpl, err := template.ParseFiles( - "web/templates/teme/podrazumevana/base.html", - "web/templates/komponente/sidebar.html", - "web/templates/komponente/topbar.html", - "web/templates/stranice/dashboard.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) - } + h.renderujTemplate(w, "dashboard", podaci) } // bojaTackeServisa vraća hex boju tačke prema statusu naloga diff --git a/internal/handler/dobavljac.go b/internal/handler/dobavljac.go index 84b590f..6528576 100644 --- a/internal/handler/dobavljac.go +++ b/internal/handler/dobavljac.go @@ -1,8 +1,6 @@ package handler import ( - "html/template" - "log" "net/http" "strings" @@ -62,22 +60,7 @@ func (h *Handler) Dobavljaci(w http.ResponseWriter, r *http.Request) { 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) - } + h.renderujTemplate(w, "dobavljaci", podaci) } // NoviDobavljac prikazuje praznu formu za unos novog dobavljača @@ -88,7 +71,7 @@ func (h *Handler) NoviDobavljac(w http.ResponseWriter, r *http.Request) { return } - renderujFormuDobavljaca(w, PodaciFormeDobavljaca{ + h.renderujFormuDobavljaca(w, PodaciFormeDobavljaca{ PodaciStranice: model.PodaciStranice{ Stranica: "dobavljaci", NaslovStranice: "Novi dobavljač", @@ -113,7 +96,7 @@ func (h *Handler) SacuvajDobavljaca(w http.ResponseWriter, r *http.Request) { dobavljac, greska := parseFormuDobavljaca(r) if greska != "" { podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB) - renderujFormuDobavljaca(w, PodaciFormeDobavljaca{ + h.renderujFormuDobavljaca(w, PodaciFormeDobavljaca{ PodaciStranice: model.PodaciStranice{ Stranica: "dobavljaci", NaslovStranice: "Novi dobavljač", @@ -159,7 +142,7 @@ func (h *Handler) IzmeniDobavljaca(w http.ResponseWriter, r *http.Request) { return } - renderujFormuDobavljaca(w, PodaciFormeDobavljaca{ + h.renderujFormuDobavljaca(w, PodaciFormeDobavljaca{ PodaciStranice: model.PodaciStranice{ Stranica: "dobavljaci", NaslovStranice: "Izmeni dobavljača", @@ -192,7 +175,7 @@ func (h *Handler) SacuvajIzmeneDobavljaca(w http.ResponseWriter, r *http.Request if greska != "" { podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB) dobavljac.ID = id - renderujFormuDobavljaca(w, PodaciFormeDobavljaca{ + h.renderujFormuDobavljaca(w, PodaciFormeDobavljaca{ PodaciStranice: model.PodaciStranice{ Stranica: "dobavljaci", NaslovStranice: "Izmeni dobavljača", @@ -257,21 +240,6 @@ func parseFormuDobavljaca(r *http.Request) (model.Dobavljac, string) { } // 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) - } +func (h *Handler) renderujFormuDobavljaca(w http.ResponseWriter, podaci PodaciFormeDobavljaca) { + h.renderujTemplate(w, "dobavljac_forma", podaci) } diff --git a/internal/handler/handler.go b/internal/handler/handler.go index ffa4d04..fc28c60 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -2,6 +2,7 @@ package handler import ( "database/sql" + "html/template" "ntech/internal/db" "ntech/internal/db/sqlite" @@ -20,8 +21,11 @@ type Handler struct { KlijentiRepo db.KlijentRepository ServisRepo db.ServisRepository ProdajaRepo db.ProdajaRepository - KorisniciRepo db.KorisniciRepository - SesijeRepo db.SesijeRepository + KorisniciRepo db.KorisniciRepository + SesijeRepo db.SesijeRepository + PodsetniciFRepo db.PodsetnikRepository + Verzija string + Templates map[string]*template.Template } // Novi kreira novi Handler sa datom bazom @@ -35,8 +39,9 @@ func Novi(baza *sql.DB) *Handler { KlijentiRepo: sqlite.NoviKlijentRepo(baza), ServisRepo: sqlite.NoviServisRepo(baza), ProdajaRepo: sqlite.NoviProdajaRepo(baza), - KorisniciRepo: sqlite.NoviKorisniciRepo(baza), - SesijeRepo: sqlite.NoviSesijeRepo(baza), + KorisniciRepo: sqlite.NoviKorisniciRepo(baza), + SesijeRepo: sqlite.NoviSesijeRepo(baza), + PodsetniciFRepo: sqlite.NoviPodsetnikRepo(baza), } } diff --git a/internal/handler/izvestaji.go b/internal/handler/izvestaji.go index e254760..5a5cc99 100644 --- a/internal/handler/izvestaji.go +++ b/internal/handler/izvestaji.go @@ -257,19 +257,5 @@ func (h *Handler) Izvestaji(w http.ResponseWriter, r *http.Request) { TopKlijenti: topKlijenti, } - tmpl, err := template.ParseFiles( - "web/templates/teme/podrazumevana/base.html", - "web/templates/komponente/sidebar.html", - "web/templates/komponente/topbar.html", - "web/templates/stranice/izvestaji.html", - ) - if err != nil { - http.Error(w, "Greška pri učitavanju stranice", http.StatusInternalServerError) - return - } - - if err := tmpl.ExecuteTemplate(w, "base", podaci); err != nil { - http.Error(w, "Greška pri prikazu stranice", http.StatusInternalServerError) - return - } + h.renderujTemplate(w, "izvestaji", podaci) } diff --git a/internal/handler/kategorija.go b/internal/handler/kategorija.go index 3a26e83..6d9a0c2 100644 --- a/internal/handler/kategorija.go +++ b/internal/handler/kategorija.go @@ -1,8 +1,6 @@ package handler import ( - "html/template" - "log" "net/http" "strconv" @@ -50,22 +48,7 @@ func (h *Handler) Kategorije(w http.ResponseWriter, r *http.Request) { 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) - } + h.renderujTemplate(w, "kategorije", podaci) } // DodajKategoriju prima POST i čuva novu kategoriju diff --git a/internal/handler/kes.go b/internal/handler/kes.go new file mode 100644 index 0000000..bedd579 --- /dev/null +++ b/internal/handler/kes.go @@ -0,0 +1,119 @@ +package handler + +import ( + "fmt" + "html/template" + "log" + "net/http" +) + +var bazniSabloni = []string{ + "web/templates/teme/podrazumevana/base.html", + "web/templates/komponente/sidebar.html", + "web/templates/komponente/topbar.html", +} + +// saSidebar su šabloni koji koriste base layout (sidebar + topbar) +var saSidebar = []string{ + "admin_korisnici", "admin_profil", + "dashboard", + "dobavljaci", "dobavljac_forma", + "izvestaji", + "kategorije", + "klijenti", "klijent_forma", + "magacin", "magacin_forma", + "nabavke", "nabavka_forma", "nabavka_detalji", + "podesavanja", + "podsetnici", "podsetnik_forma", + "prodaja", "prodaja_detalji", "prodaja_forma", + "servis", "servis_forma", "servis_detalji", +} + +// standalone su šabloni bez base layouta +var standaloneIme = []string{ + "prijava", "setup", "totp_provera", "prodaja_stampa", +} + +// KreirajKes parsuje sve šablone i vraća ih keširane u mapi +func KreirajKes() (map[string]*template.Template, error) { + kes := make(map[string]*template.Template) + + for _, ime := range saSidebar { + fajlovi := make([]string, len(bazniSabloni), len(bazniSabloni)+1) + copy(fajlovi, bazniSabloni) + fajlovi = append(fajlovi, "web/templates/stranice/"+ime+".html") + t, err := template.ParseFiles(fajlovi...) + if err != nil { + return nil, fmt.Errorf("kes: %s: %w", ime, err) + } + kes[ime] = t + } + + for _, ime := range standaloneIme { + t, err := template.ParseFiles("web/templates/stranice/" + ime + ".html") + if err != nil { + return nil, fmt.Errorf("kes: %s: %w", ime, err) + } + kes[ime] = t + } + + return kes, nil +} + +// renderujTemplate renderuje šablon sa base layoutom +// U produkciji koristi keš; u razvoju parsuje svaki put (hot reload) +func (h *Handler) renderujTemplate(w http.ResponseWriter, ime string, podaci any) { + var tmpl *template.Template + + if h.Templates != nil { + t, ok := h.Templates[ime] + if !ok { + log.Printf("kes: šablon '%s' nije pronađen", ime) + http.Error(w, "Greška pri učitavanju stranice", http.StatusInternalServerError) + return + } + tmpl = t + } else { + fajlovi := make([]string, len(bazniSabloni), len(bazniSabloni)+1) + copy(fajlovi, bazniSabloni) + fajlovi = append(fajlovi, "web/templates/stranice/"+ime+".html") + var err error + if tmpl, err = template.ParseFiles(fajlovi...); err != nil { + log.Printf("greška pri parsiranju šablona %s: %v", ime, 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 šablona %s: %v", ime, err) + http.Error(w, "Greška pri prikazu stranice", http.StatusInternalServerError) + } +} + +// renderujStandalone renderuje šablon bez base layouta (prijava, setup, itd.) +func (h *Handler) renderujStandalone(w http.ResponseWriter, ime string, podaci any) { + var tmpl *template.Template + + if h.Templates != nil { + t, ok := h.Templates[ime] + if !ok { + log.Printf("kes: standalone šablon '%s' nije pronađen", ime) + http.Error(w, "Greška pri učitavanju stranice", http.StatusInternalServerError) + return + } + tmpl = t + } else { + var err error + if tmpl, err = template.ParseFiles("web/templates/stranice/" + ime + ".html"); err != nil { + log.Printf("greška pri parsiranju šablona %s: %v", ime, err) + http.Error(w, "Greška pri učitavanju stranice", http.StatusInternalServerError) + return + } + } + + if err := tmpl.Execute(w, podaci); err != nil { + log.Printf("greška pri renderovanju šablona %s: %v", ime, err) + http.Error(w, "Greška pri prikazu stranice", http.StatusInternalServerError) + } +} diff --git a/internal/handler/klijent.go b/internal/handler/klijent.go index 962a054..c25aec5 100644 --- a/internal/handler/klijent.go +++ b/internal/handler/klijent.go @@ -1,7 +1,6 @@ package handler import ( - "html/template" "log" "net/http" "strings" @@ -62,22 +61,7 @@ func (h *Handler) Klijenti(w http.ResponseWriter, r *http.Request) { 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) - } + h.renderujTemplate(w, "klijenti", podaci) } // NoviKlijent prikazuje praznu formu za unos novog klijenta @@ -88,7 +72,7 @@ func (h *Handler) NoviKlijent(w http.ResponseWriter, r *http.Request) { return } - renderujFormuKlijenta(w, PodaciFormeKlijenta{ + h.renderujFormuKlijenta(w, PodaciFormeKlijenta{ PodaciStranice: model.PodaciStranice{ Stranica: "klijenti", NaslovStranice: "Novi klijent", @@ -113,7 +97,7 @@ func (h *Handler) SacuvajKlijenta(w http.ResponseWriter, r *http.Request) { klijent, greska := parseFormuKlijenta(r) if greska != "" { podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB) - renderujFormuKlijenta(w, PodaciFormeKlijenta{ + h.renderujFormuKlijenta(w, PodaciFormeKlijenta{ PodaciStranice: model.PodaciStranice{ Stranica: "klijenti", NaslovStranice: "Novi klijent", @@ -134,7 +118,7 @@ func (h *Handler) SacuvajKlijenta(w http.ResponseWriter, r *http.Request) { 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{ + h.renderujFormuKlijenta(w, PodaciFormeKlijenta{ PodaciStranice: model.PodaciStranice{ Stranica: "klijenti", NaslovStranice: "Novi klijent", @@ -175,7 +159,7 @@ func (h *Handler) IzmeniKlijenta(w http.ResponseWriter, r *http.Request) { return } - renderujFormuKlijenta(w, PodaciFormeKlijenta{ + h.renderujFormuKlijenta(w, PodaciFormeKlijenta{ PodaciStranice: model.PodaciStranice{ Stranica: "klijenti", NaslovStranice: "Izmeni klijenta", @@ -208,7 +192,7 @@ func (h *Handler) SacuvajIzmenuKlijenta(w http.ResponseWriter, r *http.Request) if greska != "" { podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB) klijent.ID = id - renderujFormuKlijenta(w, PodaciFormeKlijenta{ + h.renderujFormuKlijenta(w, PodaciFormeKlijenta{ PodaciStranice: model.PodaciStranice{ Stranica: "klijenti", NaslovStranice: "Izmeni klijenta", @@ -230,7 +214,7 @@ func (h *Handler) SacuvajIzmenuKlijenta(w http.ResponseWriter, r *http.Request) 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{ + h.renderujFormuKlijenta(w, PodaciFormeKlijenta{ PodaciStranice: model.PodaciStranice{ Stranica: "klijenti", NaslovStranice: "Izmeni klijenta", @@ -293,21 +277,6 @@ func parseFormuKlijenta(r *http.Request) (model.Klijent, string) { } // 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) - } +func (h *Handler) renderujFormuKlijenta(w http.ResponseWriter, podaci PodaciFormeKlijenta) { + h.renderujTemplate(w, "klijent_forma", podaci) } diff --git a/internal/handler/magacin.go b/internal/handler/magacin.go index af26b62..1afc4c4 100644 --- a/internal/handler/magacin.go +++ b/internal/handler/magacin.go @@ -1,8 +1,6 @@ package handler import ( - "html/template" - "log" "net/http" "strconv" @@ -77,23 +75,7 @@ func (h *Handler) Magacin(w http.ResponseWriter, r *http.Request) { 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 - } + h.renderujTemplate(w, "magacin", podaci) } // ObrisiArtikal briše artikal po ID-u diff --git a/internal/handler/magacin_forma.go b/internal/handler/magacin_forma.go index 5987473..f72c2da 100644 --- a/internal/handler/magacin_forma.go +++ b/internal/handler/magacin_forma.go @@ -2,8 +2,6 @@ package handler import ( "fmt" - "html/template" - "log" "net/http" "strconv" @@ -52,7 +50,7 @@ func (h *Handler) NoviArtikal(w http.ResponseWriter, r *http.Request) { Izmena: false, } - renderujFormuArtikla(w, podaci) + h.renderujFormuArtikla(w, podaci) } // SacuvajArtikal prima POST formu i čuva novi artikal @@ -70,7 +68,7 @@ func (h *Handler) SacuvajArtikal(w http.ResponseWriter, r *http.Request) { if artikal.KategorijaID != nil { katIDStr = strconv.FormatInt(*artikal.KategorijaID, 10) } - renderujFormuArtikla(w, PodaciFormeArtikla{ + h.renderujFormuArtikla(w, PodaciFormeArtikla{ PodaciStranice: model.PodaciStranice{ Stranica: "magacin", NaslovStranice: "Novi artikal", @@ -155,7 +153,7 @@ func (h *Handler) IzmeniArtikal(w http.ResponseWriter, r *http.Request) { Izmena: true, } - renderujFormuArtikla(w, podaci) + h.renderujFormuArtikla(w, podaci) } // SacuvajIzmenuArtikla prima POST formu i čuva izmenu artikla @@ -181,7 +179,7 @@ func (h *Handler) SacuvajIzmenuArtikla(w http.ResponseWriter, r *http.Request) { if artikal.KategorijaID != nil { katIDStr = strconv.FormatInt(*artikal.KategorijaID, 10) } - renderujFormuArtikla(w, PodaciFormeArtikla{ + h.renderujFormuArtikla(w, PodaciFormeArtikla{ PodaciStranice: model.PodaciStranice{ Stranica: "magacin", NaslovStranice: "Izmeni artikal", @@ -258,21 +256,6 @@ func parseFormuArtikla(r *http.Request) (model.Artikal, string) { } // 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) - } +func (h *Handler) renderujFormuArtikla(w http.ResponseWriter, podaci PodaciFormeArtikla) { + h.renderujTemplate(w, "magacin_forma", podaci) } diff --git a/internal/handler/nabavka.go b/internal/handler/nabavka.go index 65b1002..4f7d575 100644 --- a/internal/handler/nabavka.go +++ b/internal/handler/nabavka.go @@ -3,7 +3,6 @@ package handler import ( "encoding/json" "html/template" - "log" "net/http" "strconv" "strings" @@ -85,22 +84,7 @@ func (h *Handler) Nabavke(w http.ResponseWriter, r *http.Request) { 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/nabavke.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) - } + h.renderujTemplate(w, "nabavke", podaci) } // NovaNabavka prikazuje formu za unos nove nabavke @@ -129,7 +113,7 @@ func (h *Handler) NovaNabavka(w http.ResponseWriter, r *http.Request) { return } - renderujFormuNabavke(w, PodaciFormeNabavke{ + h.renderujFormuNabavke(w, PodaciFormeNabavke{ PodaciStranice: model.PodaciStranice{ Stranica: "nabavke", NaslovStranice: "Nova nabavka", @@ -160,7 +144,7 @@ func (h *Handler) SacuvajNabavku(w http.ResponseWriter, r *http.Request) { artikli, _ := h.Artikli.Lista(r.Context(), db.ArtikalFilter{}) dobavljaci, _ := h.DobavljaciRepo.Lista(r.Context(), "") kategorije, _ := h.KategorijeRepo.Lista(r.Context()) - renderujFormuNabavke(w, PodaciFormeNabavke{ + h.renderujFormuNabavke(w, PodaciFormeNabavke{ PodaciStranice: model.PodaciStranice{ Stranica: "nabavke", NaslovStranice: "Nova nabavka", @@ -240,22 +224,7 @@ func (h *Handler) DetaljiNabavke(w http.ResponseWriter, r *http.Request) { DobavljacNaziv: dobavljacNaziv, } - tmpl, err := template.ParseFiles( - "web/templates/teme/podrazumevana/base.html", - "web/templates/komponente/sidebar.html", - "web/templates/komponente/topbar.html", - "web/templates/stranice/nabavka_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) - } + h.renderujTemplate(w, "nabavka_detalji", podaci) } // ObrisiNabavku prima POST zahtev i briše nabavku po ID-u @@ -328,21 +297,6 @@ func parseFormuNabavke(r *http.Request) (model.Nabavka, []model.StavkaNabavke, s } // renderujFormuNabavke renderuje HTML šablon forme za unos nove nabavke -func renderujFormuNabavke(w http.ResponseWriter, podaci PodaciFormeNabavke) { - tmpl, err := template.ParseFiles( - "web/templates/teme/podrazumevana/base.html", - "web/templates/komponente/sidebar.html", - "web/templates/komponente/topbar.html", - "web/templates/stranice/nabavka_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) - } +func (h *Handler) renderujFormuNabavke(w http.ResponseWriter, podaci PodaciFormeNabavke) { + h.renderujTemplate(w, "nabavka_forma", podaci) } diff --git a/internal/handler/podesavanja.go b/internal/handler/podesavanja.go index c24d6d1..c65bc4a 100644 --- a/internal/handler/podesavanja.go +++ b/internal/handler/podesavanja.go @@ -2,9 +2,12 @@ package handler import ( "fmt" - "html/template" + "io" + "log" "net/http" "os" + "path/filepath" + "strings" "time" "ntech/internal/db/sqlite" @@ -25,6 +28,8 @@ type PodaciPodesavanja struct { LogoPutanja string Tema string Sacuvano bool + Verzija string + LogoGreska string } // Podesavanja renderuje stranicu podešavanja @@ -55,23 +60,11 @@ func (h *Handler) Podesavanja(w http.ResponseWriter, r *http.Request) { LogoPutanja: podesavanja["logo_putanja"], Tema: podesavanja["tema"], Sacuvano: r.URL.Query().Get("sacuvano") == "1", + Verzija: h.Verzija, + LogoGreska: r.URL.Query().Get("logo_greska"), } - tmpl, err := template.ParseFiles( - "web/templates/teme/podrazumevana/base.html", - "web/templates/komponente/sidebar.html", - "web/templates/komponente/topbar.html", - "web/templates/stranice/podesavanja.html", - ) - if err != nil { - http.Error(w, "Greška pri učitavanju stranice", http.StatusInternalServerError) - return - } - - if err := tmpl.ExecuteTemplate(w, "base", podaci); err != nil { - http.Error(w, "Greška pri prikazu stranice", http.StatusInternalServerError) - return - } + h.renderujTemplate(w, "podesavanja", podaci) } // SacuvajPodesavanja prima POST i čuva podešavanja u bazu @@ -120,13 +113,88 @@ func (h *Handler) BackupBaze(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, privremeni) } +// OtpremiLogo prima multipart upload slike loga i čuva je u web/static/uploads/ +func (h *Handler) OtpremiLogo(w http.ResponseWriter, r *http.Request) { + if err := r.ParseMultipartForm(2 << 20); err != nil { + http.Redirect(w, r, "/podesavanja?logo_greska=Fajl+je+prevelik+%28maksimum+2+MB%29", http.StatusSeeOther) + return + } + + fajl, zaglavlje, err := r.FormFile("logo") + if err != nil { + http.Redirect(w, r, "/podesavanja?logo_greska=Nije+odabran+fajl", http.StatusSeeOther) + return + } + defer fajl.Close() + + // proveravamo ekstenziju + ext := strings.ToLower(filepath.Ext(zaglavlje.Filename)) + dozvoljenoExt := map[string]string{ + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".svg": "image/svg+xml", + } + ocekivaniMime, ok := dozvoljenoExt[ext] + if !ok { + http.Redirect(w, r, "/podesavanja?logo_greska=Dozvoljeni+formati+su+PNG%2C+JPG+i+SVG", http.StatusSeeOther) + return + } + + // za binarne formate proveravamo i stvarni tip fajla (SVG je tekstualni, preskačemo) + if ext != ".svg" { + buf := make([]byte, 512) + n, _ := fajl.Read(buf) + stvarniMime := http.DetectContentType(buf[:n]) + if !strings.HasPrefix(stvarniMime, ocekivaniMime) { + http.Redirect(w, r, "/podesavanja?logo_greska=Sadržaj+fajla+ne+odgovara+odabranoj+ekstenziji", http.StatusSeeOther) + return + } + // vraćamo kursor na početak + if _, err := fajl.Seek(0, io.SeekStart); err != nil { + http.Redirect(w, r, "/podesavanja?logo_greska=Greška+pri+obradi+fajla", http.StatusSeeOther) + return + } + } + + // brišemo stare logo fajlove + stari, _ := filepath.Glob("web/static/uploads/logo.*") + for _, s := range stari { + os.Remove(s) + } + + odrediste := "web/static/uploads/logo" + ext + dst, err := os.Create(odrediste) + if err != nil { + log.Printf("upload loga: ne mogu kreirati fajl: %v", err) + http.Redirect(w, r, "/podesavanja?logo_greska=Greška+pri+čuvanju+fajla", http.StatusSeeOther) + return + } + defer dst.Close() + + if _, err := io.Copy(dst, fajl); err != nil { + log.Printf("upload loga: greška pri kopiranju: %v", err) + http.Redirect(w, r, "/podesavanja?logo_greska=Greška+pri+čuvanju+fajla", http.StatusSeeOther) + return + } + + putanja := "/static/uploads/logo" + ext + if err := sqlite.SacuvajPodesavanje(r.Context(), h.DB, "logo_putanja", putanja); err != nil { + log.Printf("upload loga: greška pri čuvanju putanje: %v", err) + http.Redirect(w, r, "/podesavanja?logo_greska=Greška+pri+čuvanju+podešavanja", http.StatusSeeOther) + return + } + + http.Redirect(w, r, "/podesavanja?sacuvano=1", http.StatusSeeOther) +} + // PromeniTemu menja aktivnu temu i vraća na prethodnu stranicu func (h *Handler) PromeniTemu(w http.ResponseWriter, r *http.Request) { tema := chi.URLParam(r, "tema") // proveravamo da li je validna tema validne := map[string]bool{ - "tamna": true, "svetla": true, "zelena": true, "ljubicasta": true, + "tamna": true, "svetla": true, } if !validne[tema] { http.Redirect(w, r, "/dashboard", http.StatusSeeOther) diff --git a/internal/handler/podsetnici.go b/internal/handler/podsetnici.go new file mode 100644 index 0000000..ee7a67a --- /dev/null +++ b/internal/handler/podsetnici.go @@ -0,0 +1,267 @@ +package handler + +import ( + "log" + "net/http" + "strings" + "time" + + "ntech/internal/db" + "ntech/internal/db/sqlite" + "ntech/internal/model" + + "github.com/go-chi/chi/v5" +) + +// podaciPodsetnici su podaci za stranicu sa listom podsetnika +type podaciPodsetnici struct { + model.PodaciStranice + Podsetnici []model.Podsetnik + SamoAktivni bool + Sacuvano bool + Obrisan bool +} + +// podaciPodsetnikForma su podaci za formu novog/izmenjenog podsetnika +type podaciPodsetnikForma struct { + model.PodaciStranice + Podsetnik model.Podsetnik + Greska string + Izmena bool +} + +// Podsetnici renderuje listu podsetnika +func (h *Handler) Podsetnici(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 + } + + samoAktivni := r.URL.Query().Get("samo_aktivni") == "1" + + lista, err := h.PodsetniciFRepo.Lista(r.Context(), db.PodsetnikFilter{SamoAktivni: samoAktivni}) + if err != nil { + http.Error(w, "Greška pri učitavanju podsetnika", http.StatusInternalServerError) + return + } + + ps := h.popuniPodaciStranice(r, podesavanja) + ps.Stranica = "podsetnici" + ps.NaslovStranice = "Podsetnici" + + podaci := podaciPodsetnici{ + PodaciStranice: ps, + Podsetnici: lista, + SamoAktivni: samoAktivni, + Sacuvano: r.URL.Query().Get("sacuvano") == "1", + Obrisan: r.URL.Query().Get("obrisan") == "1", + } + + h.renderujTemplate(w, "podsetnici", podaci) +} + +// NoviPodsetnik prikazuje praznu formu za unos novog podsetnika +func (h *Handler) NoviPodsetnik(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 + } + + ps := h.popuniPodaciStranice(r, podesavanja) + ps.Stranica = "podsetnici" + ps.NaslovStranice = "Novi podsetnik" + + h.renderujFormuPodsetnika(w, podaciPodsetnikForma{ + PodaciStranice: ps, + Izmena: false, + }) +} + +// SacuvajPodsetnik prima POST formu i upisuje novi podsetnik u bazu +func (h *Handler) SacuvajPodsetnik(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest) + return + } + + podsetnik, greska := parseFormuPodsetnika(r) + if greska != "" { + podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB) + ps := h.popuniPodaciStranice(r, podesavanja) + ps.Stranica = "podsetnici" + ps.NaslovStranice = "Novi podsetnik" + h.renderujFormuPodsetnika(w, podaciPodsetnikForma{ + PodaciStranice: ps, + Podsetnik: podsetnik, + Greska: greska, + Izmena: false, + }) + return + } + + if _, err := h.PodsetniciFRepo.Kreiraj(r.Context(), &podsetnik); err != nil { + log.Printf("greška pri čuvanju podsetnika: %v", err) + podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB) + ps := h.popuniPodaciStranice(r, podesavanja) + ps.Stranica = "podsetnici" + ps.NaslovStranice = "Novi podsetnik" + h.renderujFormuPodsetnika(w, podaciPodsetnikForma{ + PodaciStranice: ps, + Podsetnik: podsetnik, + Greska: "Došlo je do greške pri čuvanju. Pokušajte ponovo.", + Izmena: false, + }) + return + } + + http.Redirect(w, r, "/podsetnici?sacuvano=1", http.StatusSeeOther) +} + +// IzmeniPodsetnik učitava podsetnik po ID-u i prikazuje popunjenu formu za izmenu +func (h *Handler) IzmeniPodsetnik(w http.ResponseWriter, r *http.Request) { + id, err := parseID(chi.URLParam(r, "id")) + if err != nil { + http.Error(w, "Neispravan ID podsetnika", http.StatusBadRequest) + return + } + + podsetnik, err := h.PodsetniciFRepo.DohvatiID(r.Context(), id) + if err != nil { + http.Error(w, "Podsetnik 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 + } + + ps := h.popuniPodaciStranice(r, podesavanja) + ps.Stranica = "podsetnici" + ps.NaslovStranice = "Izmeni podsetnik" + + h.renderujFormuPodsetnika(w, podaciPodsetnikForma{ + PodaciStranice: ps, + Podsetnik: *podsetnik, + Izmena: true, + }) +} + +// SacuvajIzmenePodsetnika prima POST formu i ažurira postojeći podsetnik u bazi +func (h *Handler) SacuvajIzmenePodsetnika(w http.ResponseWriter, r *http.Request) { + id, err := parseID(chi.URLParam(r, "id")) + if err != nil { + http.Error(w, "Neispravan ID podsetnika", http.StatusBadRequest) + return + } + + if err := r.ParseForm(); err != nil { + http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest) + return + } + + podsetnik, greska := parseFormuPodsetnika(r) + podsetnik.ID = id + if greska != "" { + podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB) + ps := h.popuniPodaciStranice(r, podesavanja) + ps.Stranica = "podsetnici" + ps.NaslovStranice = "Izmeni podsetnik" + h.renderujFormuPodsetnika(w, podaciPodsetnikForma{ + PodaciStranice: ps, + Podsetnik: podsetnik, + Greska: greska, + Izmena: true, + }) + return + } + + if err := h.PodsetniciFRepo.Izmeni(r.Context(), &podsetnik); err != nil { + log.Printf("greška pri čuvanju izmene podsetnika: %v", err) + podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB) + ps := h.popuniPodaciStranice(r, podesavanja) + ps.Stranica = "podsetnici" + ps.NaslovStranice = "Izmeni podsetnik" + h.renderujFormuPodsetnika(w, podaciPodsetnikForma{ + PodaciStranice: ps, + Podsetnik: podsetnik, + Greska: "Došlo je do greške pri čuvanju. Pokušajte ponovo.", + Izmena: true, + }) + return + } + + http.Redirect(w, r, "/podsetnici?sacuvano=1", http.StatusSeeOther) +} + +// OznaciPodsetnik prima POST zahtev i menja status završenosti podsetnika +func (h *Handler) OznaciPodsetnik(w http.ResponseWriter, r *http.Request) { + id, err := parseID(chi.URLParam(r, "id")) + if err != nil { + http.Error(w, "Neispravan ID podsetnika", http.StatusBadRequest) + return + } + + // učitamo trenutni status pa ga preokrenemo + podsetnik, err := h.PodsetniciFRepo.DohvatiID(r.Context(), id) + if err != nil { + http.Error(w, "Podsetnik nije pronađen", http.StatusNotFound) + return + } + + if err := h.PodsetniciFRepo.OznaciZavrsenim(r.Context(), id, !podsetnik.Zavrseno); err != nil { + http.Error(w, "Greška pri ažuriranju statusa", http.StatusInternalServerError) + return + } + + http.Redirect(w, r, "/podsetnici", http.StatusSeeOther) +} + +// ObrisiPodsetnik prima POST zahtev i briše podsetnik po ID-u +func (h *Handler) ObrisiPodsetnik(w http.ResponseWriter, r *http.Request) { + id, err := parseID(chi.URLParam(r, "id")) + if err != nil { + http.Error(w, "Neispravan ID podsetnika", http.StatusBadRequest) + return + } + + if err := h.PodsetniciFRepo.Obrisi(r.Context(), id); err != nil { + http.Error(w, "Greška pri brisanju podsetnika", http.StatusInternalServerError) + return + } + + http.Redirect(w, r, "/podsetnici?obrisan=1", http.StatusSeeOther) +} + +// parseFormuPodsetnika čita polja iz HTTP forme, validira ih i vraća model i eventualnu grešku +func parseFormuPodsetnika(r *http.Request) (model.Podsetnik, string) { + naslov := strings.TrimSpace(r.FormValue("naslov")) + if naslov == "" { + return model.Podsetnik{}, "Naslov je obavezan." + } + + datumStr := strings.TrimSpace(r.FormValue("datum_podsecanja")) + if datumStr == "" { + return model.Podsetnik{Naslov: naslov}, "Datum podsećanja je obavezan." + } + + datum, err := time.Parse("2006-01-02", datumStr) + if err != nil { + return model.Podsetnik{Naslov: naslov}, "Datum podsećanja nije u ispravnom formatu." + } + + return model.Podsetnik{ + Naslov: naslov, + Napomena: strings.TrimSpace(r.FormValue("napomena")), + DatumPodsecanja: datum, + Tip: model.TipOpsti, + }, "" +} + +// renderujFormuPodsetnika renderuje HTML šablon forme za unos ili izmenu podsetnika +func (h *Handler) renderujFormuPodsetnika(w http.ResponseWriter, podaci podaciPodsetnikForma) { + h.renderujTemplate(w, "podsetnik_forma", podaci) +} diff --git a/internal/handler/prijava.go b/internal/handler/prijava.go index 58bd9ec..68d20a9 100644 --- a/internal/handler/prijava.go +++ b/internal/handler/prijava.go @@ -1,7 +1,6 @@ package handler import ( - "html/template" "net/http" "time" @@ -22,7 +21,7 @@ func (h *Handler) PrikazPrijave(w http.ResponseWriter, r *http.Request) { } greska := r.URL.Query().Get("greska") - renderujStandaloneTemplate(w, "web/templates/stranice/prijava.html", map[string]any{ + h.renderujStandalone(w, "prijava", map[string]any{ "Greska": greska, }) } @@ -84,7 +83,7 @@ func (h *Handler) PrikazTotp(w http.ResponseWriter, r *http.Request) { } greska := r.URL.Query().Get("greska") - renderujStandaloneTemplate(w, "web/templates/stranice/totp_provera.html", map[string]any{ + h.renderujStandalone(w, "totp_provera", map[string]any{ "Greska": greska, }) } @@ -133,7 +132,7 @@ func (h *Handler) PrikazSetupa(w http.ResponseWriter, r *http.Request) { return } greska := r.URL.Query().Get("greska") - renderujStandaloneTemplate(w, "web/templates/stranice/setup.html", map[string]any{ + h.renderujStandalone(w, "setup", map[string]any{ "Greska": greska, }) } @@ -200,13 +199,3 @@ func napraviKolacic(token string, istice time.Time) *http.Cookie { } } -func renderujStandaloneTemplate(w http.ResponseWriter, putanja string, podaci any) { - tmpl, err := template.ParseFiles(putanja) - if err != nil { - http.Error(w, "Greška pri učitavanju stranice", http.StatusInternalServerError) - return - } - if err := tmpl.Execute(w, podaci); err != nil { - http.Error(w, "Greška pri prikazu stranice", http.StatusInternalServerError) - } -} diff --git a/internal/handler/prodaja.go b/internal/handler/prodaja.go index f62d9e9..e8a6813 100644 --- a/internal/handler/prodaja.go +++ b/internal/handler/prodaja.go @@ -105,22 +105,7 @@ func (h *Handler) Prodaja(w http.ResponseWriter, r *http.Request) { Pretraga: pretraga, } - tmpl, err := template.ParseFiles( - "web/templates/teme/podrazumevana/base.html", - "web/templates/komponente/sidebar.html", - "web/templates/komponente/topbar.html", - "web/templates/stranice/prodaja.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) - } + h.renderujTemplate(w, "prodaja", podaci) } // NovaProdaja prikazuje formu za unos novog prodajnog naloga @@ -143,7 +128,7 @@ func (h *Handler) NovaProdaja(w http.ResponseWriter, r *http.Request) { return } - renderujFormuProdaje(w, PodaciFormeProdaje{ + h.renderujFormuProdaje(w, PodaciFormeProdaje{ PodaciStranice: model.PodaciStranice{ Stranica: "prodaja", NaslovStranice: "Nova prodaja", @@ -173,7 +158,7 @@ func (h *Handler) SacuvajProdaju(w http.ResponseWriter, r *http.Request) { podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB) artikli, _ := h.Artikli.Lista(r.Context(), appdb.ArtikalFilter{}) klijenti, _ := h.KlijentiRepo.Lista(r.Context(), "") - renderujFormuProdaje(w, PodaciFormeProdaje{ + h.renderujFormuProdaje(w, PodaciFormeProdaje{ PodaciStranice: model.PodaciStranice{ Stranica: "prodaja", NaslovStranice: "Nova prodaja", @@ -282,22 +267,7 @@ func (h *Handler) DetaljiProdaje(w http.ResponseWriter, r *http.Request) { 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/prodaja_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) - } + h.renderujTemplate(w, "prodaja_detalji", podaci) } // StampaProdaje renderuje print-friendly stranicu za dati prodajni nalog @@ -349,17 +319,7 @@ func (h *Handler) StampaProdaje(w http.ResponseWriter, r *http.Request) { PIB: podesavanja["pib"], } - tmpl, err := template.ParseFiles("web/templates/stranice/prodaja_stampa.html") - if err != nil { - log.Printf("greška pri učitavanju šablona za štampu: %v", err) - http.Error(w, "Greška pri učitavanju stranice", http.StatusInternalServerError) - return - } - - if err := tmpl.ExecuteTemplate(w, "prodaja_stampa.html", podaci); err != nil { - log.Printf("greška pri renderovanju štampe: %v", err) - http.Error(w, "Greška pri prikazu stranice", http.StatusInternalServerError) - } + h.renderujStandalone(w, "prodaja_stampa", podaci) } // ObrisiProdaju prima POST zahtev, vraća stanje na magacin i briše nalog @@ -430,21 +390,6 @@ func parseFormuProdaje(r *http.Request) (model.ProdajniNalog, []model.StavkaProd } // renderujFormuProdaje renderuje HTML šablon forme za unos nove prodaje -func renderujFormuProdaje(w http.ResponseWriter, podaci PodaciFormeProdaje) { - tmpl, err := template.ParseFiles( - "web/templates/teme/podrazumevana/base.html", - "web/templates/komponente/sidebar.html", - "web/templates/komponente/topbar.html", - "web/templates/stranice/prodaja_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) - } +func (h *Handler) renderujFormuProdaje(w http.ResponseWriter, podaci PodaciFormeProdaje) { + h.renderujTemplate(w, "prodaja_forma", podaci) } diff --git a/internal/handler/servis.go b/internal/handler/servis.go index 496cc88..95555f1 100644 --- a/internal/handler/servis.go +++ b/internal/handler/servis.go @@ -1,7 +1,6 @@ package handler import ( - "html/template" "log" "net/http" "strconv" @@ -79,22 +78,7 @@ func (h *Handler) Servis(w http.ResponseWriter, r *http.Request) { 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) - } + h.renderujTemplate(w, "servis", podaci) } // NoviNalog generiše broj naloga i prikazuje praznu formu za unos @@ -117,7 +101,7 @@ func (h *Handler) NoviNalog(w http.ResponseWriter, r *http.Request) { return } - renderujFormuNaloga(w, PodaciFormeNaloga{ + h.renderujFormuNaloga(w, PodaciFormeNaloga{ PodaciStranice: model.PodaciStranice{ Stranica: "servis", NaslovStranice: "Novi nalog", @@ -146,7 +130,7 @@ func (h *Handler) SacuvajNalog(w http.ResponseWriter, r *http.Request) { if greska != "" { podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB) klijenti, _ := h.KlijentiRepo.Lista(r.Context(), "") - renderujFormuNaloga(w, PodaciFormeNaloga{ + h.renderujFormuNaloga(w, PodaciFormeNaloga{ PodaciStranice: model.PodaciStranice{ Stranica: "servis", NaslovStranice: "Novi nalog", @@ -171,7 +155,7 @@ func (h *Handler) SacuvajNalog(w http.ResponseWriter, r *http.Request) { 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{ + h.renderujFormuNaloga(w, PodaciFormeNaloga{ PodaciStranice: model.PodaciStranice{ Stranica: "servis", NaslovStranice: "Novi nalog", @@ -220,7 +204,7 @@ func (h *Handler) IzmeniNalog(w http.ResponseWriter, r *http.Request) { return } - renderujFormuNaloga(w, PodaciFormeNaloga{ + h.renderujFormuNaloga(w, PodaciFormeNaloga{ PodaciStranice: model.PodaciStranice{ Stranica: "servis", NaslovStranice: "Izmeni nalog", @@ -256,7 +240,7 @@ func (h *Handler) SacuvajIzmenaNaloga(w http.ResponseWriter, r *http.Request) { podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB) klijenti, _ := h.KlijentiRepo.Lista(r.Context(), "") nalog.ID = id - renderujFormuNaloga(w, PodaciFormeNaloga{ + h.renderujFormuNaloga(w, PodaciFormeNaloga{ PodaciStranice: model.PodaciStranice{ Stranica: "servis", NaslovStranice: "Izmeni nalog", @@ -281,7 +265,7 @@ func (h *Handler) SacuvajIzmenaNaloga(w http.ResponseWriter, r *http.Request) { 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{ + h.renderujFormuNaloga(w, PodaciFormeNaloga{ PodaciStranice: model.PodaciStranice{ Stranica: "servis", NaslovStranice: "Izmeni nalog", @@ -368,22 +352,7 @@ func (h *Handler) DetaljiNaloga(w http.ResponseWriter, r *http.Request) { 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) - } + h.renderujTemplate(w, "servis_detalji", podaci) } // parseFormuNaloga čita i validira polja iz HTTP forme @@ -448,21 +417,6 @@ func parseOpcionuCenu(s string) *float64 { } // 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) - } +func (h *Handler) renderujFormuNaloga(w http.ResponseWriter, podaci PodaciFormeNaloga) { + h.renderujTemplate(w, "servis_forma", podaci) } diff --git a/internal/model/podsetnik.go b/internal/model/podsetnik.go new file mode 100644 index 0000000..f983874 --- /dev/null +++ b/internal/model/podsetnik.go @@ -0,0 +1,22 @@ +package model + +import "time" + +// TipOpsti je jedini trenutno podržani tip podsetnika +const TipOpsti = "opsti" + +// Podsetnik predstavlja jedan podsetnik +type Podsetnik struct { + ID int64 + Naslov string + Napomena string + DatumPodsecanja time.Time + Zavrseno bool + Tip string + DatumUnosa time.Time +} + +// JePrekoracen vraća true ako datum podsećanja je prošao a podsetnik nije završen +func (p Podsetnik) JePrekoracen() bool { + return !p.Zavrseno && p.DatumPodsecanja.Before(time.Now()) +} diff --git a/internal/model/stranica.go b/internal/model/stranica.go index e0456bb..531efbe 100644 --- a/internal/model/stranica.go +++ b/internal/model/stranica.go @@ -40,11 +40,12 @@ type PodaciStranice struct { // PodaciDashboarda su podaci specifični za dashboard stranicu type PodaciDashboarda struct { PodaciStranice - BrojArtikala int - AktivniServisi int - PrihodOvogMeseca float64 - KriticnaZaliha int - PoslednjiServisi []StavkaServisa - KriticneZalihe []StavkaZalihe - PoslednjeProdaje []StavkaProdajePregled + BrojArtikala int + AktivniServisi int + PrihodOvogMeseca float64 + KriticnaZaliha int + AktivniPodsetnici int + PoslednjiServisi []StavkaServisa + KriticneZalihe []StavkaZalihe + PoslednjeProdaje []StavkaProdajePregled } diff --git a/web/static/css/main.css b/web/static/css/main.css index eff23e4..42cc86d 100644 --- a/web/static/css/main.css +++ b/web/static/css/main.css @@ -21,7 +21,7 @@ body { /* sidebar */ .sidebar { width: 220px; - background: var(--sb-pozadina); + background: var(--sidebar-pozadina); display: flex; flex-direction: column; transition: width 0.28s cubic-bezier(.4,0,.2,1); @@ -40,7 +40,7 @@ body { height: 72px; padding: 0 12px; gap: 10px; - border-bottom: 0.5px solid rgba(255,255,255,0.07); + border-bottom: 0.5px solid var(--ivica); flex-shrink: 0; } @@ -49,7 +49,7 @@ body { background: none; border: none; cursor: pointer; - color: var(--sb-tekst-aktivan); + color: var(--tekst-jak); padding: 6px; border-radius: 6px; display: flex; @@ -60,7 +60,7 @@ body { } .hamburger:hover { - background: var(--sb-hover); + background: var(--pozadina-hover); } /* logo zona */ @@ -81,14 +81,14 @@ body { } .logo-naziv { - color: var(--sb-tekst-aktivan); + color: var(--tekst-jak); font-weight: 500; font-size: 15px; white-space: nowrap; } .logo-podnazlov { - color: var(--sb-tekst); + color: var(--tekst-sporedni); font-size: 11px; white-space: nowrap; } @@ -110,7 +110,7 @@ body { font-size: 10px; text-transform: uppercase; letter-spacing: 0.08em; - color: var(--sb-tekst); + color: var(--tekst-sporedni); padding: 12px 16px 4px; white-space: nowrap; opacity: 1; @@ -127,7 +127,7 @@ body { gap: 12px; padding: 9px 16px; cursor: pointer; - color: var(--sb-tekst); + color: var(--tekst-sporedni); white-space: nowrap; position: relative; text-decoration: none; @@ -135,13 +135,13 @@ body { } .nav-stavka:hover { - background: var(--sb-hover); - color: var(--sb-tekst-aktivan); + background: var(--pozadina-hover); + color: var(--tekst-jak); } .nav-stavka.aktivan { background: var(--sb-aktivan); - color: var(--sb-tekst-aktivan); + color: var(--tekst-jak); } .nav-stavka.aktivan::before { @@ -174,7 +174,7 @@ body { .nav-separator { height: 0.5px; - background: rgba(255,255,255,0.07); + background: var(--ivica); margin: 8px 12px; } @@ -207,7 +207,7 @@ body { /* dno sidebara */ .sidebar-dno { padding: 8px 0; - border-top: 0.5px solid rgba(255,255,255,0.07); + border-top: 0.5px solid var(--ivica); } /* glavni sadržaj */ @@ -221,7 +221,7 @@ body { /* topbar */ .topbar { height: 56px; - background: var(--topbar); + background: var(--kartica); border-bottom: 0.5px solid var(--ivica); display: flex; align-items: center; @@ -250,12 +250,13 @@ body { border: 0.5px solid var(--ivica); border-radius: 12px; padding: 16px; + box-shadow: var(--senka); transition: transform 0.25s cubic-bezier(.4,0,.2,1), box-shadow 0.25s; } .kartica:hover { transform: translateY(-4px); - box-shadow: 0 8px 24px rgba(0,0,0,0.08); + box-shadow: var(--senka); } /* input polja — konzistentna za sve teme */ @@ -286,13 +287,23 @@ select:focus { /* poruka o uspehu — konzistentna za sve teme */ .poruka-uspeh { - background: var(--kartica); - border: 0.5px solid var(--sb-akcent); + background: var(--poruka-uspeh-bg); + border: 0.5px solid var(--poruka-uspeh-boja); border-radius: 10px; padding: 12px 16px; margin-bottom: 20px; font-size: 14px; - color: var(--sb-akcent); + color: var(--poruka-uspeh-boja); +} + +.poruka-greska { + background: rgba(207, 87, 87, 0.12); + border: 0.5px solid var(--greska); + border-radius: 10px; + padding: 12px 16px; + margin-bottom: 20px; + font-size: 14px; + color: var(--greska); } /* overlay za mobilni — tamni sloj iza sidebara */ diff --git a/web/static/css/teme/ljubicasta.css b/web/static/css/teme/ljubicasta.css deleted file mode 100644 index 523d9fb..0000000 --- a/web/static/css/teme/ljubicasta.css +++ /dev/null @@ -1,15 +0,0 @@ -:root { - --sb-pozadina: #4a3580; - --sb-hover: #5a4295; - --sb-aktivan: #6d52b0; - --sb-tekst: #c4b5e8; - --sb-tekst-aktivan: #ffffff; - --sb-akcent: #a855f7; - - --pozadina: #f5f0ff; - --kartica: #ffffff; - --tekst-glavni: #1a1d2e; - --tekst-sporedni: #6b7280; - --ivica: #e5e7eb; - --topbar: #ffffff; -} diff --git a/web/static/css/teme/svetla.css b/web/static/css/teme/svetla.css index d422345..a0d0b22 100644 --- a/web/static/css/teme/svetla.css +++ b/web/static/css/teme/svetla.css @@ -1,15 +1,21 @@ :root { - --sb-pozadina: #f8fafc; - --sb-hover: #f1f5f9; - --sb-aktivan: #e8f0fe; - --sb-tekst: #64748b; - --sb-tekst-aktivan: #1a1d2e; - --sb-akcent: #4f7ef8; - - --pozadina: #f0f2f8; - --kartica: #ffffff; - --tekst-glavni: #1a1d2e; - --tekst-sporedni: #6b7280; - --ivica: #e5e7eb; - --topbar: #ffffff; + --pozadina: #f6f8fa; + --kartica: #ffffff; + --kartica-2: #f0f2f5; + --pozadina-hover: #ebeef1; + --tekst-glavni: #24292f; + --tekst-sporedni: #57606a; + --tekst-jak: #1f2328; + --ivica: #d0d7de; + --sb-akcent: #1f6feb; + --sb-akcent-hover: #388bfd; + --sb-aktivan: #ebeef1; + --uspeh: #1a7f37; + --upozorenje: #9a6700; + --greska: #cf222e; + --info: #0969da; + --senka: 0 4px 12px rgba(0, 0, 0, 0.08); + --sidebar-pozadina: #ffffff; + --poruka-uspeh-bg: rgba(26, 127, 55, 0.1); + --poruka-uspeh-boja: #1a7f37; } diff --git a/web/static/css/teme/tamna.css b/web/static/css/teme/tamna.css index a526c16..bf5e4ec 100644 --- a/web/static/css/teme/tamna.css +++ b/web/static/css/teme/tamna.css @@ -1,15 +1,21 @@ :root { - --sb-pozadina: #1a1d2e; - --sb-hover: #2a2d40; - --sb-aktivan: #3d4f7c; - --sb-tekst: #a0a8c0; - --sb-tekst-aktivan: #ffffff; - --sb-akcent: #4f7ef8; - - --pozadina: #0f1117; - --kartica: #1a1d2e; - --tekst-glavni: #e2e8f0; - --tekst-sporedni: #8892a4; - --ivica: #2a2d40; - --topbar: #13151f; + --pozadina: #1f2228; + --kartica: #22262d; + --kartica-2: #2c313a; + --pozadina-hover: #2a2f37; + --tekst-glavni: #dbdee4; + --tekst-sporedni: #a0a8b5; + --tekst-jak: #f0f3f6; + --ivica: #353a44; + --sb-akcent: #4684d6; + --sb-akcent-hover: #5a96e6; + --sb-aktivan: #2a2f37; + --uspeh: #5db876; + --upozorenje: #e1b04a; + --greska: #cf5757; + --info: #5cb1d6; + --senka: 0 4px 12px rgba(0, 0, 0, 0.4); + --sidebar-pozadina: #1a1c20; + --poruka-uspeh-bg: rgba(93, 184, 118, 0.15); + --poruka-uspeh-boja: #5db876; } diff --git a/web/static/css/teme/zelena.css b/web/static/css/teme/zelena.css deleted file mode 100644 index 62ace57..0000000 --- a/web/static/css/teme/zelena.css +++ /dev/null @@ -1,15 +0,0 @@ -:root { - --sb-pozadina: #1a4d2e; - --sb-hover: #245c38; - --sb-aktivan: #2d7a47; - --sb-tekst: #a8d5b5; - --sb-tekst-aktivan: #ffffff; - --sb-akcent: #22c55e; - - --pozadina: #f0faf4; - --kartica: #ffffff; - --tekst-glavni: #1a1d2e; - --tekst-sporedni: #6b7280; - --ivica: #e5e7eb; - --topbar: #ffffff; -} diff --git a/web/static/uploads/logo.svg b/web/static/uploads/logo.svg new file mode 100644 index 0000000..5b79679 --- /dev/null +++ b/web/static/uploads/logo.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + VM + + diff --git a/web/templates/komponente/sidebar.html b/web/templates/komponente/sidebar.html index b564f38..260b029 100644 --- a/web/templates/komponente/sidebar.html +++ b/web/templates/komponente/sidebar.html @@ -10,18 +10,12 @@
{{if eq .LogoTip "slika"}} - Logo -
-
{{.NazivFirme}}
-
{{.Podnazlov}}
-
- {{else if eq .LogoTip "tekst"}} -
-
{{.NazivFirme}}
-
{{.Podnazlov}}
-
+ Logo + {{else if eq .LogoTip "bez_naziva"}} {{else}} -
{{.NazivFirme}}
{{.Podnazlov}}
@@ -69,6 +63,12 @@ Klijenti + + + Podsetnici + Podsetnici + + Dobavljači diff --git a/web/templates/komponente/topbar.html b/web/templates/komponente/topbar.html index da1fa25..f8a658f 100644 --- a/web/templates/komponente/topbar.html +++ b/web/templates/komponente/topbar.html @@ -13,10 +13,8 @@ {{.NaslovStranice}}
- - - - + +
diff --git a/web/templates/stranice/admin_korisnici.html b/web/templates/stranice/admin_korisnici.html index c5b0cd8..7964942 100644 --- a/web/templates/stranice/admin_korisnici.html +++ b/web/templates/stranice/admin_korisnici.html @@ -2,11 +2,47 @@ {{define "naslov"}}Korisnici — NTech{{end}} +{{define "dodatni-css"}} + +{{end}} + {{define "sadrzaj"}}
{{if .Sacuvano}} -
Promene su uspešno sačuvane.
+
Promene su uspešno sačuvane.
{{end}} {{if eq .Greska "1"}} @@ -21,7 +57,7 @@ Korisnici sistema
- +
@@ -87,7 +123,7 @@ -
+
Novi korisnik
diff --git a/web/templates/stranice/admin_profil.html b/web/templates/stranice/admin_profil.html index 8f49604..8f8cba0 100644 --- a/web/templates/stranice/admin_profil.html +++ b/web/templates/stranice/admin_profil.html @@ -31,18 +31,15 @@
- +
- +
- +
Korisničko ime
@@ -215,7 +235,7 @@ -
+
Najvažniji klijenti — top 10
{{if .TopKlijenti}}
diff --git a/web/templates/stranice/kategorije.html b/web/templates/stranice/kategorije.html index bcdaa4a..9676c3d 100644 --- a/web/templates/stranice/kategorije.html +++ b/web/templates/stranice/kategorije.html @@ -2,14 +2,48 @@ {{define "naslov"}}Kategorije — NTech{{end}} +{{define "dodatni-css"}} + +{{end}} + {{define "sadrzaj"}}
{{if .Sacuvano}} -
Kategorija je uspešno sačuvana.
+
Kategorija je uspešno sačuvana.
{{end}} {{if .Obrisana}} -
Kategorija je uspešno obrisana.
+
Kategorija je uspešno obrisana.
{{end}} @@ -19,7 +53,7 @@ -
+
Nova kategorija
@@ -53,7 +87,7 @@ Postojeće kategorije
{{range .Kategorije}} -
+
{{.Naziv}}
{{if .Opis}} diff --git a/web/templates/stranice/magacin.html b/web/templates/stranice/magacin.html index e7bd7aa..3122daa 100644 --- a/web/templates/stranice/magacin.html +++ b/web/templates/stranice/magacin.html @@ -2,6 +2,30 @@ {{define "naslov"}}Magacin — NTech{{end}} +{{define "dodatni-css"}} + +{{end}} + {{define "sadrzaj"}}
@@ -50,7 +74,7 @@
-
+
diff --git a/web/templates/stranice/podesavanja.html b/web/templates/stranice/podesavanja.html index fb3cd86..83dbfaa 100644 --- a/web/templates/stranice/podesavanja.html +++ b/web/templates/stranice/podesavanja.html @@ -11,6 +11,37 @@ {{end}} + + +
+
+ + Logo firme +
+ {{if .LogoGreska}} +
+ {{.LogoGreska}} +
+ {{end}} + {{if .LogoPutanja}} +
+ Trenutni logo +
+ {{end}} +
+ + +
+
PNG, JPG ili SVG — maksimum 2 MB
+
+ + @@ -59,21 +90,22 @@
-
-
+
@@ -89,24 +121,14 @@
- -
@@ -118,7 +140,7 @@ Sistem
-
Verzija programa: 1.0.0
+
Verzija programa: {{.Verzija}}
diff --git a/web/templates/stranice/podsetnici.html b/web/templates/stranice/podsetnici.html new file mode 100644 index 0000000..32bc153 --- /dev/null +++ b/web/templates/stranice/podsetnici.html @@ -0,0 +1,217 @@ +{{template "base" .}} + +{{define "naslov"}}Podsetnici — NTech{{end}} + +{{define "dodatni-css"}} + +{{end}} + +{{define "sadrzaj"}} +
Naziv
+ + + + + + + + + + {{range .Podsetnici}} + + + + + + + {{else}} + + + + {{end}} + +
NaslovDatum podsećanjaStatusAkcije
+
{{.Naslov}}
+ {{if .Napomena}} +
{{.Napomena}}
+ {{end}} +
+ {{.DatumPodsecanja.Format "02.01.2006."}} + + {{if .Zavrseno}} + Završeno + {{else if .JePrekoracen}} + Prekoračeno + {{else}} + Aktivno + {{end}} + +
+ + Izmeni + +
+ +
+
+ +
+
+
+ Nema podsetnika. Dodaj prvi podsetnik. +
+
+
+ + +
+ {{range .Podsetnici}} +
+
+
+
{{.Naslov}}
+ {{if .Napomena}} +
{{.Napomena}}
+ {{end}} +
+
+ {{if .Zavrseno}} + Završeno + {{else if .JePrekoracen}} + Prekoračeno + {{else}} + Aktivno + {{end}} +
+
+
+ {{.DatumPodsecanja.Format "02.01.2006."}} +
+
+ + Izmeni + +
+ +
+
+ +
+
+
+ {{else}} +
+ Nema podsetnika. Dodaj prvi podsetnik. +
+ {{end}} +
+ +
+{{end}} diff --git a/web/templates/stranice/podsetnik_forma.html b/web/templates/stranice/podsetnik_forma.html new file mode 100644 index 0000000..e4e4d65 --- /dev/null +++ b/web/templates/stranice/podsetnik_forma.html @@ -0,0 +1,99 @@ +{{template "base" .}} + +{{define "naslov"}}{{if .Izmena}}Izmeni podsetnik{{else}}Novi podsetnik{{end}} — NTech{{end}} + +{{define "dodatni-css"}} + +{{end}} + +{{define "sadrzaj"}} +
+ + + + Nazad na podsetnik + + +
+
+ + {{if .Izmena}}Izmeni podsetnik{{else}}Novi podsetnik{{end}} + +
+ + {{if .Greska}} +
+ {{.Greska}} +
+ {{end}} + +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + Odustani + + +
+ +
+
+
+
+{{end}} diff --git a/web/templates/stranice/prijava.html b/web/templates/stranice/prijava.html index 06d2088..d0d912c 100644 --- a/web/templates/stranice/prijava.html +++ b/web/templates/stranice/prijava.html @@ -16,8 +16,8 @@ padding: 16px; } .kartica { - background: #1a1d2e; - border: 0.5px solid #2a2d3e; + background: #1a1d27; + border: 0.5px solid #2d3148; border-radius: 16px; padding: 40px; width: 100%; @@ -53,20 +53,20 @@ } input { width: 100%; - padding: 10px 14px; + padding: 8px 12px; background: #0f1117; - border: 0.5px solid #2a2d3e; + border: 0.5px solid #2d3148; border-radius: 8px; font-size: 14px; color: #fff; outline: none; transition: border-color 0.2s; } - input:focus { border-color: #4f7ef8; } + input:focus { border-color: #e53e3e; } .dugme { width: 100%; padding: 11px; - background: #4f7ef8; + background: #e53e3e; color: #fff; border: none; border-radius: 8px; diff --git a/web/templates/stranice/setup.html b/web/templates/stranice/setup.html index 3f2f9d4..5d24d78 100644 --- a/web/templates/stranice/setup.html +++ b/web/templates/stranice/setup.html @@ -16,8 +16,8 @@ padding: 16px; } .kartica { - background: #1a1d2e; - border: 0.5px solid #2a2d3e; + background: #1a1d27; + border: 0.5px solid #2d3148; border-radius: 16px; padding: 40px; width: 100%; @@ -32,20 +32,20 @@ label { display: block; font-size: 13px; color: #9ca3af; margin-bottom: 6px; } input { width: 100%; - padding: 10px 14px; + padding: 8px 12px; background: #0f1117; - border: 0.5px solid #2a2d3e; + border: 0.5px solid #2d3148; border-radius: 8px; font-size: 14px; color: #fff; outline: none; transition: border-color 0.2s; } - input:focus { border-color: #4f7ef8; } + input:focus { border-color: #e53e3e; } .dugme { width: 100%; padding: 11px; - background: #4f7ef8; + background: #e53e3e; color: #fff; border: none; border-radius: 8px; diff --git a/web/templates/stranice/totp_provera.html b/web/templates/stranice/totp_provera.html index 35f0945..ba8b9a7 100644 --- a/web/templates/stranice/totp_provera.html +++ b/web/templates/stranice/totp_provera.html @@ -16,8 +16,8 @@ padding: 16px; } .kartica { - background: #1a1d2e; - border: 0.5px solid #2a2d3e; + background: #1a1d27; + border: 0.5px solid #2d3148; border-radius: 16px; padding: 40px; width: 100%; @@ -31,9 +31,9 @@ label { display: block; font-size: 13px; color: #9ca3af; margin-bottom: 6px; } input { width: 100%; - padding: 10px 14px; + padding: 8px 12px; background: #0f1117; - border: 0.5px solid #2a2d3e; + border: 0.5px solid #2d3148; border-radius: 8px; font-size: 20px; color: #fff; @@ -42,11 +42,11 @@ letter-spacing: 6px; transition: border-color 0.2s; } - input:focus { border-color: #4f7ef8; } + input:focus { border-color: #e53e3e; } .dugme { width: 100%; padding: 11px; - background: #4f7ef8; + background: #e53e3e; color: #fff; border: none; border-radius: 8px;