Optimizacije — SQLite WAL, template keš, gzip kompresija, build skript
This commit is contained in:
+1
-2
@@ -8,8 +8,7 @@
|
||||
*.db-wal
|
||||
|
||||
# Promenljive okruženja
|
||||
.env
|
||||
.env.*
|
||||
ntech.env
|
||||
|
||||
# Privremeni fajlovi
|
||||
*.tmp
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+28
-1
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
+10
-56
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+29
-18
@@ -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 */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<svg width="300" height="300" viewBox="0 0 300 300" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<!-- Gradijent pozadine -->
|
||||
<linearGradient id="bgGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#0f2027"></stop>
|
||||
<stop offset="50%" stop-color="#203a43"></stop>
|
||||
<stop offset="100%" stop-color="#2c5364"></stop>
|
||||
</linearGradient>
|
||||
|
||||
<!-- Senka -->
|
||||
<filter id="textShadow" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feDropShadow dx="0" dy="6" stdDeviation="6" flood-color="#000" flood-opacity="0.5"></feDropShadow>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- Pozadina -->
|
||||
<rect width="300" height="300" rx="30" ry="30" fill="url(#bgGrad)"></rect>
|
||||
|
||||
<!-- Prsten -->
|
||||
<ellipse cx="150" cy="160" rx="130" ry="36" fill="none" stroke="white" stroke-width="3" opacity="0.15" transform="rotate(-15 150 160)"></ellipse>
|
||||
|
||||
<!-- Unutrašnji prsten -->
|
||||
<ellipse cx="150" cy="160" rx="110" ry="30" fill="none" stroke="white" stroke-width="1.5" opacity="0.1" transform="rotate(-15 150 160)"></ellipse>
|
||||
|
||||
<!-- VM tekst -->
|
||||
<text x="50%" y="162" text-anchor="middle" dominant-baseline="middle" font-family="'Orbitron', sans-serif" font-size="100" fill="#ffffff" filter="url(#textShadow)">
|
||||
VM
|
||||
</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -10,18 +10,12 @@
|
||||
</button>
|
||||
<div class="logo-zona">
|
||||
{{if eq .LogoTip "slika"}}
|
||||
<img src="/static/{{.LogoPutanja}}" alt="Logo" style="width:36px;height:36px;object-fit:contain;border-radius:6px;flex-shrink:0;">
|
||||
<div>
|
||||
<div class="logo-naziv">{{.NazivFirme}}</div>
|
||||
<div class="logo-podnazlov">{{.Podnazlov}}</div>
|
||||
</div>
|
||||
{{else if eq .LogoTip "tekst"}}
|
||||
<div>
|
||||
<div class="logo-naziv">{{.NazivFirme}}</div>
|
||||
<div class="logo-podnazlov">{{.Podnazlov}}</div>
|
||||
</div>
|
||||
<img src="{{.LogoPutanja}}" alt="Logo"
|
||||
style="max-height:48px;width:auto;border-radius:8px;padding:4px;background:var(--kartica);box-shadow:var(--senka);transition:transform 0.2s;flex-shrink:0;"
|
||||
onmouseover="this.style.transform='scale(1.02)'"
|
||||
onmouseout="this.style.transform='scale(1)'">
|
||||
{{else if eq .LogoTip "bez_naziva"}}
|
||||
{{else}}
|
||||
<div style="width:36px;height:36px;border-radius:8px;background:rgba(255,255,255,0.1);border:1.5px dashed rgba(255,255,255,0.25);flex-shrink:0;"></div>
|
||||
<div>
|
||||
<div class="logo-naziv">{{.NazivFirme}}</div>
|
||||
<div class="logo-podnazlov">{{.Podnazlov}}</div>
|
||||
@@ -69,6 +63,12 @@
|
||||
<span class="nav-tooltip">Klijenti</span>
|
||||
</a>
|
||||
|
||||
<a href="/podsetnici" class="nav-stavka {{if eq .Stranica "podsetnici"}}aktivan{{end}}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>
|
||||
<span>Podsetnici</span>
|
||||
<span class="nav-tooltip">Podsetnici</span>
|
||||
</a>
|
||||
|
||||
<a href="/dobavljaci" class="nav-stavka {{if eq .Stranica "dobavljaci"}}aktivan{{end}}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="1" y="3" width="15" height="13" rx="1"/><path d="M16 8h4l3 3v5h-7V8z"/><circle cx="5.5" cy="18.5" r="2.5"/><circle cx="18.5" cy="18.5" r="2.5"/></svg>
|
||||
<span>Dobavljači</span>
|
||||
|
||||
@@ -13,10 +13,8 @@
|
||||
<span class="topbar-naslov">{{.NaslovStranice}}</span>
|
||||
|
||||
<div class="topbar-teme" style="display:flex;align-items:center;gap:8px;">
|
||||
<a href="/tema/tamna" class="tema-krug {{if eq .Tema "tamna"}}tema-krug-aktivan{{end}}" style="background:#4f7ef8;" title="Tamna"></a>
|
||||
<a href="/tema/svetla" class="tema-krug {{if eq .Tema "svetla"}}tema-krug-aktivan{{end}}" style="background:#e2e8f0;" title="Svetla"></a>
|
||||
<a href="/tema/zelena" class="tema-krug {{if eq .Tema "zelena"}}tema-krug-aktivan{{end}}" style="background:#22c55e;" title="Zelena"></a>
|
||||
<a href="/tema/ljubicasta" class="tema-krug {{if eq .Tema "ljubicasta"}}tema-krug-aktivan{{end}}" style="background:#a855f7;" title="Ljubičasta"></a>
|
||||
<a href="/tema/tamna" class="tema-krug {{if eq .Tema "tamna"}}tema-krug-aktivan{{end}}" style="background:#1a1d27;" title="Tamna"></a>
|
||||
<a href="/tema/svetla" class="tema-krug {{if eq .Tema "svetla"}}tema-krug-aktivan{{end}}" style="background:#f0f2f5;border:1px solid #e2e6ed;" title="Svetla"></a>
|
||||
</div>
|
||||
|
||||
<div style="width:32px;height:32px;border-radius:50%;background:var(--sb-aktivan);display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:500;color:#fff;flex-shrink:0;">
|
||||
|
||||
@@ -2,11 +2,47 @@
|
||||
|
||||
{{define "naslov"}}Korisnici — NTech{{end}}
|
||||
|
||||
{{define "dodatni-css"}}
|
||||
<style>
|
||||
@keyframes slideDown {
|
||||
from { opacity: 0; transform: translateY(-10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
@keyframes fadeInUp {
|
||||
from { opacity: 0; transform: translateY(8px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.poruka-animacija { animation: slideDown 0.3s ease forwards; }
|
||||
|
||||
.korisnici-tabela tbody tr {
|
||||
animation: fadeInUp 0.25s ease forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
.korisnici-tabela tbody tr:nth-child(1) { animation-delay: 0.04s; }
|
||||
.korisnici-tabela tbody tr:nth-child(2) { animation-delay: 0.08s; }
|
||||
.korisnici-tabela tbody tr:nth-child(3) { animation-delay: 0.12s; }
|
||||
.korisnici-tabela tbody tr:nth-child(4) { animation-delay: 0.16s; }
|
||||
.korisnici-tabela tbody tr:nth-child(5) { animation-delay: 0.20s; }
|
||||
.korisnici-tabela tbody tr:nth-child(6) { animation-delay: 0.24s; }
|
||||
.korisnici-tabela tbody tr:nth-child(7) { animation-delay: 0.28s; }
|
||||
.korisnici-tabela tbody tr:nth-child(8) { animation-delay: 0.32s; }
|
||||
.korisnici-tabela tbody tr:nth-child(9) { animation-delay: 0.36s; }
|
||||
.korisnici-tabela tbody tr:nth-child(10) { animation-delay: 0.40s; }
|
||||
|
||||
.nova-forma-kartica {
|
||||
animation: fadeInUp 0.25s ease forwards;
|
||||
opacity: 0;
|
||||
animation-delay: 0.30s;
|
||||
}
|
||||
</style>
|
||||
{{end}}
|
||||
|
||||
{{define "sadrzaj"}}
|
||||
<div style="display:flex;flex-direction:column;gap:16px;">
|
||||
|
||||
{{if .Sacuvano}}
|
||||
<div class="poruka-uspeh">Promene su uspešno sačuvane.</div>
|
||||
<div class="poruka-uspeh poruka-animacija">Promene su uspešno sačuvane.</div>
|
||||
{{end}}
|
||||
|
||||
{{if eq .Greska "1"}}
|
||||
@@ -21,7 +57,7 @@
|
||||
<span style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">Korisnici sistema</span>
|
||||
</div>
|
||||
<div style="overflow-x:auto;">
|
||||
<table style="width:100%;border-collapse:collapse;">
|
||||
<table class="korisnici-tabela" style="width:100%;border-collapse:collapse;">
|
||||
<thead>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<th style="padding:10px 20px;text-align:left;font-size:12px;font-weight:500;color:var(--tekst-sporedni);">Korisničko ime</th>
|
||||
@@ -87,7 +123,7 @@
|
||||
</div>
|
||||
|
||||
<!-- forma za novog korisnika -->
|
||||
<div class="kartica">
|
||||
<div class="kartica nova-forma-kartica">
|
||||
<div style="font-size:15px;font-weight:500;color:var(--tekst-glavni);margin-bottom:16px;padding-bottom:12px;border-bottom:0.5px solid var(--ivica);">
|
||||
Novi korisnik
|
||||
</div>
|
||||
|
||||
@@ -31,18 +31,15 @@
|
||||
<div style="display:flex;flex-direction:column;gap:12px;">
|
||||
<div>
|
||||
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:6px;">Trenutna lozinka</label>
|
||||
<input type="password" name="stara_lozinka" required
|
||||
style="width:100%;padding:8px 12px;border:0.5px solid var(--ivica);border-radius:8px;font-size:14px;background:var(--pozadina);color:var(--tekst-glavni);outline:none;">
|
||||
<input type="password" name="stara_lozinka" required style="width:100%;">
|
||||
</div>
|
||||
<div>
|
||||
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:6px;">Nova lozinka</label>
|
||||
<input type="password" name="nova_lozinka" required minlength="8"
|
||||
style="width:100%;padding:8px 12px;border:0.5px solid var(--ivica);border-radius:8px;font-size:14px;background:var(--pozadina);color:var(--tekst-glavni);outline:none;">
|
||||
<input type="password" name="nova_lozinka" required minlength="8" style="width:100%;">
|
||||
</div>
|
||||
<div>
|
||||
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:6px;">Potvrda nove lozinke</label>
|
||||
<input type="password" name="nova_lozinka_potvrda" required
|
||||
style="width:100%;padding:8px 12px;border:0.5px solid var(--ivica);border-radius:8px;font-size:14px;background:var(--pozadina);color:var(--tekst-glavni);outline:none;">
|
||||
<input type="password" name="nova_lozinka_potvrda" required style="width:100%;">
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit"
|
||||
@@ -85,7 +82,7 @@
|
||||
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:6px;">Verifikacioni kod</label>
|
||||
<input type="text" name="kod" inputmode="numeric" pattern="[0-9]{6}"
|
||||
maxlength="6" required autofocus
|
||||
style="width:160px;padding:8px 12px;border:0.5px solid var(--ivica);border-radius:8px;font-size:18px;background:var(--pozadina);color:var(--tekst-glavni);outline:none;text-align:center;letter-spacing:4px;">
|
||||
style="width:160px;font-size:18px;text-align:center;letter-spacing:4px;">
|
||||
</div>
|
||||
<button type="submit"
|
||||
style="padding:9px 20px;background:var(--sb-akcent);color:#fff;border:none;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;">
|
||||
|
||||
@@ -2,10 +2,37 @@
|
||||
|
||||
{{define "naslov"}}Dashboard — NTech{{end}}
|
||||
|
||||
{{define "dodatni-css"}}
|
||||
<style>
|
||||
@keyframes fadeInUp {
|
||||
from { opacity: 0; transform: translateY(8px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.dash-stat {
|
||||
animation: fadeInUp 0.25s ease forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
.dash-stat:nth-child(1) { animation-delay: 0.04s; }
|
||||
.dash-stat:nth-child(2) { animation-delay: 0.10s; }
|
||||
.dash-stat:nth-child(3) { animation-delay: 0.16s; }
|
||||
.dash-stat:nth-child(4) { animation-delay: 0.22s; }
|
||||
.dash-stat:nth-child(5) { animation-delay: 0.28s; }
|
||||
|
||||
.dash-kartica {
|
||||
animation: fadeInUp 0.25s ease forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
.dash-kartica:nth-child(1) { animation-delay: 0.20s; }
|
||||
.dash-kartica:nth-child(2) { animation-delay: 0.28s; }
|
||||
.dash-kartica:nth-child(3) { animation-delay: 0.36s; }
|
||||
</style>
|
||||
{{end}}
|
||||
|
||||
{{define "sadrzaj"}}
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-3 mb-6">
|
||||
<div class="grid grid-cols-2 md:grid-cols-5 gap-3 mb-6">
|
||||
<!-- stat kartice -->
|
||||
<div class="kartica">
|
||||
<div class="kartica dash-stat">
|
||||
<div style="width:36px;height:36px;border-radius:8px;background:#eff2ff;display:flex;align-items:center;justify-content:center;margin-bottom:10px;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#4f7ef8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/></svg>
|
||||
</div>
|
||||
@@ -13,7 +40,7 @@
|
||||
<div style="font-size:12px;color:var(--tekst-sporedni);margin-top:4px;">Artikala na stanju</div>
|
||||
</div>
|
||||
|
||||
<div class="kartica">
|
||||
<div class="kartica dash-stat">
|
||||
<div style="width:36px;height:36px;border-radius:8px;background:#f0fdf4;display:flex;align-items:center;justify-content:center;margin-bottom:10px;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#16a34a" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>
|
||||
</div>
|
||||
@@ -21,7 +48,7 @@
|
||||
<div style="font-size:12px;color:var(--tekst-sporedni);margin-top:4px;">Aktivnih servisa</div>
|
||||
</div>
|
||||
|
||||
<div class="kartica">
|
||||
<div class="kartica dash-stat">
|
||||
<div style="width:36px;height:36px;border-radius:8px;background:#fff7ed;display:flex;align-items:center;justify-content:center;margin-bottom:10px;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#ea580c" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="21" r="1"/><circle cx="20" cy="21" r="1"/><path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"/></svg>
|
||||
</div>
|
||||
@@ -29,18 +56,26 @@
|
||||
<div style="font-size:12px;color:var(--tekst-sporedni);margin-top:4px;">Prihod ovog meseca</div>
|
||||
</div>
|
||||
|
||||
<div class="kartica">
|
||||
<div class="kartica dash-stat">
|
||||
<div style="width:36px;height:36px;border-radius:8px;background:#fef2f2;display:flex;align-items:center;justify-content:center;margin-bottom:10px;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#dc2626" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
|
||||
</div>
|
||||
<div style="font-size:22px;font-weight:500;color:var(--tekst-glavni);">{{.KriticnaZaliha}}</div>
|
||||
<div style="font-size:12px;color:var(--tekst-sporedni);margin-top:4px;">Kritično niska zaliha</div>
|
||||
</div>
|
||||
|
||||
<a href="/podsetnici" class="kartica dash-stat" style="text-decoration:none;display:block;">
|
||||
<div style="width:36px;height:36px;border-radius:8px;background:#f0f9ff;display:flex;align-items:center;justify-content:center;margin-bottom:10px;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#0284c7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>
|
||||
</div>
|
||||
<div style="font-size:22px;font-weight:500;color:var(--tekst-glavni);">{{.AktivniPodsetnici}}</div>
|
||||
<div style="font-size:12px;color:var(--tekst-sporedni);margin-top:4px;">Aktivnih podsetnika</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<!-- poslednji servisi -->
|
||||
<div class="kartica">
|
||||
<div class="kartica dash-kartica">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:14px;">
|
||||
<span style="font-size:14px;font-weight:500;color:var(--tekst-glavni);">Poslednji servisi</span>
|
||||
</div>
|
||||
@@ -56,7 +91,7 @@
|
||||
</div>
|
||||
|
||||
<!-- kritične zalihe -->
|
||||
<div class="kartica">
|
||||
<div class="kartica dash-kartica">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:14px;">
|
||||
<span style="font-size:14px;font-weight:500;color:var(--tekst-glavni);">Kritične zalihe</span>
|
||||
<span style="font-size:11px;padding:2px 8px;border-radius:20px;background:#fffbeb;color:#b45309;font-weight:500;">Upozorenje</span>
|
||||
@@ -73,7 +108,7 @@
|
||||
</div>
|
||||
|
||||
<!-- poslednje prodaje -->
|
||||
<div class="kartica">
|
||||
<div class="kartica dash-kartica">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:14px;">
|
||||
<span style="font-size:14px;font-weight:500;color:var(--tekst-glavni);">Poslednje prodaje</span>
|
||||
</div>
|
||||
|
||||
@@ -82,6 +82,26 @@
|
||||
|
||||
.badge-dana.upozorenje { background: #fffbeb; color: #b45309; }
|
||||
.badge-dana.kritican { background: #fef2f2; color: #dc2626; }
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from { opacity: 0; transform: translateY(8px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.izv-sekcija {
|
||||
animation: fadeInUp 0.25s ease forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
.izv-sekcija:nth-child(1) { animation-delay: 0.04s; }
|
||||
.izv-sekcija:nth-child(2) { animation-delay: 0.12s; }
|
||||
.izv-sekcija:nth-child(3) { animation-delay: 0.20s; }
|
||||
|
||||
.izv-grid-kartica {
|
||||
animation: fadeInUp 0.25s ease forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
.izv-grid-kartica:nth-child(1) { animation-delay: 0.24s; }
|
||||
.izv-grid-kartica:nth-child(2) { animation-delay: 0.32s; }
|
||||
</style>
|
||||
{{end}}
|
||||
|
||||
@@ -89,7 +109,7 @@
|
||||
<div style="display:flex;flex-direction:column;gap:20px;">
|
||||
|
||||
<!-- 1. mesečni prihod -->
|
||||
<div class="kartica">
|
||||
<div class="kartica izv-sekcija">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px;margin-bottom:4px;">
|
||||
<div class="izv-naslov" style="margin-bottom:0;">Mesečni prihod — poslednjih 12 meseci</div>
|
||||
<div class="toggle-red" style="margin-bottom:0;">
|
||||
@@ -136,7 +156,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 2. stari otvoreni nalozi -->
|
||||
<div class="kartica">
|
||||
<div class="kartica izv-sekcija">
|
||||
<div class="izv-naslov">Stari otvoreni nalozi — bez završetka duže od 14 dana</div>
|
||||
{{if .StariNalozi}}
|
||||
<div style="overflow-x:auto;">
|
||||
@@ -178,10 +198,10 @@
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 izv-sekcija">
|
||||
|
||||
<!-- 3. top 10 artikala -->
|
||||
<div class="kartica">
|
||||
<div class="kartica izv-grid-kartica">
|
||||
<div class="izv-naslov">Najprodavaniji artikli — top 10</div>
|
||||
{{if .TopArtikli}}
|
||||
<table style="width:100%;border-collapse:collapse;">
|
||||
@@ -215,7 +235,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 4. top 10 klijenata -->
|
||||
<div class="kartica">
|
||||
<div class="kartica izv-grid-kartica">
|
||||
<div class="izv-naslov">Najvažniji klijenti — top 10</div>
|
||||
{{if .TopKlijenti}}
|
||||
<table style="width:100%;border-collapse:collapse;">
|
||||
|
||||
@@ -2,14 +2,48 @@
|
||||
|
||||
{{define "naslov"}}Kategorije — NTech{{end}}
|
||||
|
||||
{{define "dodatni-css"}}
|
||||
<style>
|
||||
@keyframes slideDown {
|
||||
from { opacity: 0; transform: translateY(-10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
@keyframes fadeInUp {
|
||||
from { opacity: 0; transform: translateY(8px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.poruka-animacija { animation: slideDown 0.3s ease forwards; }
|
||||
|
||||
.kat-forma-kartica {
|
||||
animation: fadeInUp 0.25s ease forwards;
|
||||
opacity: 0;
|
||||
animation-delay: 0.04s;
|
||||
}
|
||||
|
||||
.kat-red {
|
||||
animation: fadeInUp 0.25s ease forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
.kat-red:nth-child(1) { animation-delay: 0.12s; }
|
||||
.kat-red:nth-child(2) { animation-delay: 0.18s; }
|
||||
.kat-red:nth-child(3) { animation-delay: 0.24s; }
|
||||
.kat-red:nth-child(4) { animation-delay: 0.30s; }
|
||||
.kat-red:nth-child(5) { animation-delay: 0.36s; }
|
||||
.kat-red:nth-child(6) { animation-delay: 0.42s; }
|
||||
.kat-red:nth-child(7) { animation-delay: 0.48s; }
|
||||
.kat-red:nth-child(8) { animation-delay: 0.54s; }
|
||||
</style>
|
||||
{{end}}
|
||||
|
||||
{{define "sadrzaj"}}
|
||||
<div style="display:flex;flex-direction:column;gap:16px;">
|
||||
|
||||
{{if .Sacuvano}}
|
||||
<div class="poruka-uspeh">Kategorija je uspešno sačuvana.</div>
|
||||
<div class="poruka-uspeh poruka-animacija">Kategorija je uspešno sačuvana.</div>
|
||||
{{end}}
|
||||
{{if .Obrisana}}
|
||||
<div class="poruka-uspeh">Kategorija je uspešno obrisana.</div>
|
||||
<div class="poruka-uspeh poruka-animacija">Kategorija je uspešno obrisana.</div>
|
||||
{{end}}
|
||||
|
||||
<!-- nazad dugme -->
|
||||
@@ -19,7 +53,7 @@
|
||||
</a>
|
||||
|
||||
<!-- forma za novu kategoriju -->
|
||||
<div class="kartica">
|
||||
<div class="kartica kat-forma-kartica">
|
||||
<div style="margin-bottom:16px;padding-bottom:12px;border-bottom:0.5px solid var(--ivica);">
|
||||
<span style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">Nova kategorija</span>
|
||||
</div>
|
||||
@@ -53,7 +87,7 @@
|
||||
<span style="font-size:14px;font-weight:500;color:var(--tekst-glavni);">Postojeće kategorije</span>
|
||||
</div>
|
||||
{{range .Kategorije}}
|
||||
<div style="display:flex;align-items:center;gap:12px;padding:12px 16px;border-bottom:0.5px solid var(--ivica);">
|
||||
<div class="kat-red" style="display:flex;align-items:center;gap:12px;padding:12px 16px;border-bottom:0.5px solid var(--ivica);">
|
||||
<div style="flex:1;">
|
||||
<div style="font-size:14px;color:var(--tekst-glavni);">{{.Naziv}}</div>
|
||||
{{if .Opis}}
|
||||
|
||||
@@ -2,6 +2,30 @@
|
||||
|
||||
{{define "naslov"}}Magacin — NTech{{end}}
|
||||
|
||||
{{define "dodatni-css"}}
|
||||
<style>
|
||||
@keyframes fadeInUp {
|
||||
from { opacity: 0; transform: translateY(8px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.magacin-tabela tbody tr {
|
||||
animation: fadeInUp 0.25s ease forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
.magacin-tabela tbody tr:nth-child(1) { animation-delay: 0.04s; }
|
||||
.magacin-tabela tbody tr:nth-child(2) { animation-delay: 0.08s; }
|
||||
.magacin-tabela tbody tr:nth-child(3) { animation-delay: 0.12s; }
|
||||
.magacin-tabela tbody tr:nth-child(4) { animation-delay: 0.16s; }
|
||||
.magacin-tabela tbody tr:nth-child(5) { animation-delay: 0.20s; }
|
||||
.magacin-tabela tbody tr:nth-child(6) { animation-delay: 0.24s; }
|
||||
.magacin-tabela tbody tr:nth-child(7) { animation-delay: 0.28s; }
|
||||
.magacin-tabela tbody tr:nth-child(8) { animation-delay: 0.32s; }
|
||||
.magacin-tabela tbody tr:nth-child(9) { animation-delay: 0.36s; }
|
||||
.magacin-tabela tbody tr:nth-child(10) { animation-delay: 0.40s; }
|
||||
</style>
|
||||
{{end}}
|
||||
|
||||
{{define "sadrzaj"}}
|
||||
<div style="display:flex;flex-direction:column;gap:16px;">
|
||||
|
||||
@@ -50,7 +74,7 @@
|
||||
<!-- tabela artikala -->
|
||||
<div class="kartica" style="padding:0;overflow:hidden;">
|
||||
<div style="overflow-x:auto;">
|
||||
<table style="width:100%;border-collapse:collapse;">
|
||||
<table class="magacin-tabela" style="width:100%;border-collapse:collapse;">
|
||||
<thead>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<th style="padding:12px 16px;text-align:left;font-size:12px;font-weight:500;color:var(--tekst-sporedni);">Naziv</th>
|
||||
|
||||
@@ -11,6 +11,37 @@
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<!-- upload loga — posebna forma jer je multipart, mora biti van glavne forme -->
|
||||
<form method="POST" action="/podesavanja/logo" enctype="multipart/form-data">
|
||||
<div class="kartica" style="margin-bottom:16px;">
|
||||
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px;padding-bottom:12px;border-bottom:0.5px solid var(--ivica);">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--sb-akcent)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
|
||||
<span style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">Logo firme</span>
|
||||
</div>
|
||||
{{if .LogoGreska}}
|
||||
<div style="background:rgba(207,87,87,0.1);border:0.5px solid var(--greska);border-radius:8px;padding:8px 12px;margin-bottom:10px;font-size:13px;color:var(--greska);">
|
||||
{{.LogoGreska}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{if .LogoPutanja}}
|
||||
<div style="margin-bottom:12px;">
|
||||
<img src="{{.LogoPutanja}}" alt="Trenutni logo"
|
||||
style="max-height:60px;max-width:200px;object-fit:contain;border:0.5px solid var(--ivica);border-radius:8px;padding:6px;background:var(--kartica);">
|
||||
</div>
|
||||
{{end}}
|
||||
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;">
|
||||
<input type="file" name="logo" accept=".png,.jpg,.jpeg,.svg"
|
||||
style="flex:1;min-width:200px;font-size:13px;color:var(--tekst-sporedni);">
|
||||
<button type="submit"
|
||||
style="padding:8px 16px;background:var(--sb-aktivan);color:var(--tekst-jak);border:0.5px solid var(--ivica);border-radius:8px;font-size:13px;cursor:pointer;white-space:nowrap;transition:opacity 0.2s;"
|
||||
onmouseover="this.style.opacity='0.8'" onmouseout="this.style.opacity='1'">
|
||||
Otpremi logo
|
||||
</button>
|
||||
</div>
|
||||
<div style="font-size:12px;color:var(--tekst-sporedni);margin-top:6px;">PNG, JPG ili SVG — maksimum 2 MB</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form method="POST" action="/podesavanja/sacuvaj">
|
||||
|
||||
<!-- sekcija: firma -->
|
||||
@@ -59,21 +90,22 @@
|
||||
|
||||
<div>
|
||||
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:8px;">Logo zona</label>
|
||||
<div style="display:flex;gap:10px;">
|
||||
<label style="display:flex;align-items:center;gap:8px;padding:10px 14px;border:0.5px solid var(--ivica);border-radius:8px;cursor:pointer;flex:1;{{if eq .LogoTip "ikonica"}}border-color:var(--sb-akcent);background:var(--pozadina);{{end}}">
|
||||
<input type="radio" name="logo_tip" value="ikonica" {{if eq .LogoTip "ikonica"}}checked{{end}} style="accent-color:var(--sb-akcent);">
|
||||
<span style="font-size:13px;color:var(--tekst-glavni);">Ikonica</span>
|
||||
<div style="display:flex;gap:10px;flex-wrap:wrap;">
|
||||
<label style="display:flex;align-items:center;gap:8px;padding:10px 14px;border:0.5px solid var(--ivica);border-radius:8px;cursor:pointer;flex:1;{{if or (eq .LogoTip "sa_nazivom") (eq .LogoTip "tekst") (eq .LogoTip "ikonica") (not .LogoTip)}}border-color:var(--sb-akcent);background:var(--pozadina);{{end}}">
|
||||
<input type="radio" name="logo_tip" value="sa_nazivom" {{if or (eq .LogoTip "sa_nazivom") (eq .LogoTip "tekst") (eq .LogoTip "ikonica") (not .LogoTip)}}checked{{end}} style="accent-color:var(--sb-akcent);">
|
||||
<span style="font-size:13px;color:var(--tekst-glavni);">Sa nazivom</span>
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:8px;padding:10px 14px;border:0.5px solid var(--ivica);border-radius:8px;cursor:pointer;flex:1;{{if eq .LogoTip "tekst"}}border-color:var(--sb-akcent);background:var(--pozadina);{{end}}">
|
||||
<input type="radio" name="logo_tip" value="tekst" {{if eq .LogoTip "tekst"}}checked{{end}} style="accent-color:var(--sb-akcent);">
|
||||
<span style="font-size:13px;color:var(--tekst-glavni);">Bez ikonice</span>
|
||||
<label style="display:flex;align-items:center;gap:8px;padding:10px 14px;border:0.5px solid var(--ivica);border-radius:8px;cursor:pointer;flex:1;{{if eq .LogoTip "bez_naziva"}}border-color:var(--sb-akcent);background:var(--pozadina);{{end}}">
|
||||
<input type="radio" name="logo_tip" value="bez_naziva" {{if eq .LogoTip "bez_naziva"}}checked{{end}} style="accent-color:var(--sb-akcent);">
|
||||
<span style="font-size:13px;color:var(--tekst-glavni);">Bez naziva</span>
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:8px;padding:10px 14px;border:0.5px solid var(--ivica);border-radius:8px;cursor:pointer;flex:1;{{if eq .LogoTip "slika"}}border-color:var(--sb-akcent);background:var(--pozadina);{{end}}">
|
||||
<input type="radio" name="logo_tip" value="slika" {{if eq .LogoTip "slika"}}checked{{end}} style="accent-color:var(--sb-akcent);">
|
||||
<span style="font-size:13px;color:var(--tekst-glavni);">Slika</span>
|
||||
<label style="display:flex;align-items:center;gap:8px;padding:10px 14px;border:0.5px solid var(--ivica);border-radius:8px;flex:1;{{if eq .LogoTip "slika"}}border-color:var(--sb-akcent);background:var(--pozadina);{{end}}{{if not .LogoPutanja}}opacity:0.45;cursor:not-allowed;{{else}}cursor:pointer;{{end}}">
|
||||
<input type="radio" name="logo_tip" value="slika" {{if eq .LogoTip "slika"}}checked{{end}} {{if not .LogoPutanja}}disabled{{end}} style="accent-color:var(--sb-akcent);">
|
||||
<span style="font-size:13px;color:var(--tekst-glavni);">Sa logom</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -89,24 +121,14 @@
|
||||
<div class="teme-grid" style="display:flex;gap:10px;">
|
||||
<label style="display:flex;align-items:center;gap:8px;padding:10px 14px;border:0.5px solid var(--ivica);border-radius:8px;cursor:pointer;flex:1;{{if eq .Tema "tamna"}}border-color:var(--sb-akcent);{{end}}">
|
||||
<input type="radio" name="tema" value="tamna" {{if eq .Tema "tamna"}}checked{{end}} style="accent-color:var(--sb-akcent);">
|
||||
<div style="width:16px;height:16px;border-radius:50%;background:#1a1d2e;flex-shrink:0;"></div>
|
||||
<div style="width:16px;height:16px;border-radius:50%;background:#1a1d27;flex-shrink:0;"></div>
|
||||
<span style="font-size:13px;color:var(--tekst-glavni);">Tamna</span>
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:8px;padding:10px 14px;border:0.5px solid var(--ivica);border-radius:8px;cursor:pointer;flex:1;{{if eq .Tema "svetla"}}border-color:var(--sb-akcent);{{end}}">
|
||||
<input type="radio" name="tema" value="svetla" {{if eq .Tema "svetla"}}checked{{end}} style="accent-color:var(--sb-akcent);">
|
||||
<div style="width:16px;height:16px;border-radius:50%;background:#f8fafc;border:0.5px solid #cbd5e1;flex-shrink:0;"></div>
|
||||
<div style="width:16px;height:16px;border-radius:50%;background:#f0f2f5;border:0.5px solid #e2e6ed;flex-shrink:0;"></div>
|
||||
<span style="font-size:13px;color:var(--tekst-glavni);">Svetla</span>
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:8px;padding:10px 14px;border:0.5px solid var(--ivica);border-radius:8px;cursor:pointer;flex:1;{{if eq .Tema "zelena"}}border-color:var(--sb-akcent);{{end}}">
|
||||
<input type="radio" name="tema" value="zelena" {{if eq .Tema "zelena"}}checked{{end}} style="accent-color:var(--sb-akcent);">
|
||||
<div style="width:16px;height:16px;border-radius:50%;background:#3a975c;flex-shrink:0;"></div>
|
||||
<span style="font-size:13px;color:var(--tekst-glavni);">Zelena</span>
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:8px;padding:10px 14px;border:0.5px solid var(--ivica);border-radius:8px;cursor:pointer;flex:1;{{if eq .Tema "ljubicasta"}}border-color:var(--sb-akcent);{{end}}">
|
||||
<input type="radio" name="tema" value="ljubicasta" {{if eq .Tema "ljubicasta"}}checked{{end}} style="accent-color:var(--sb-akcent);">
|
||||
<div style="width:16px;height:16px;border-radius:50%;background:#462f80;flex-shrink:0;"></div>
|
||||
<span style="font-size:13px;color:var(--tekst-glavni);">Ljubičasta</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -118,7 +140,7 @@
|
||||
<span style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">Sistem</span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px;">
|
||||
<div style="font-size:13px;color:var(--tekst-sporedni);">Verzija programa: <span style="color:var(--tekst-glavni);font-weight:500;">1.0.0</span></div>
|
||||
<div style="font-size:13px;color:var(--tekst-sporedni);">Verzija programa: <span style="color:var(--tekst-glavni);font-weight:500;">{{.Verzija}}</span></div>
|
||||
<a href="/podesavanja/backup"
|
||||
style="display:inline-flex;align-items:center;gap:6px;padding:8px 16px;background:var(--kartica);border:0.5px solid var(--ivica);border-radius:8px;font-size:13px;color:var(--tekst-sporedni);text-decoration:none;transition:background 0.2s;"
|
||||
onmouseover="this.style.background='var(--pozadina)'" onmouseout="this.style.background='var(--kartica)'">
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "naslov"}}Podsetnici — NTech{{end}}
|
||||
|
||||
{{define "dodatni-css"}}
|
||||
<style>
|
||||
@keyframes slideDown {
|
||||
from { opacity: 0; transform: translateY(-10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from { opacity: 0; transform: translateY(8px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.poruka-animacija {
|
||||
animation: slideDown 0.3s ease forwards;
|
||||
}
|
||||
|
||||
.pod-tabela tbody tr {
|
||||
animation: fadeInUp 0.25s ease forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.pod-tabela tbody tr:nth-child(1) { animation-delay: 0.04s; }
|
||||
.pod-tabela tbody tr:nth-child(2) { animation-delay: 0.08s; }
|
||||
.pod-tabela tbody tr:nth-child(3) { animation-delay: 0.12s; }
|
||||
.pod-tabela tbody tr:nth-child(4) { animation-delay: 0.16s; }
|
||||
.pod-tabela tbody tr:nth-child(5) { animation-delay: 0.20s; }
|
||||
.pod-tabela tbody tr:nth-child(6) { animation-delay: 0.24s; }
|
||||
.pod-tabela tbody tr:nth-child(7) { animation-delay: 0.28s; }
|
||||
.pod-tabela tbody tr:nth-child(8) { animation-delay: 0.32s; }
|
||||
.pod-tabela tbody tr:nth-child(9) { animation-delay: 0.36s; }
|
||||
.pod-tabela tbody tr:nth-child(10) { animation-delay: 0.40s; }
|
||||
|
||||
.pod-kartice {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.pod-kartica {
|
||||
animation: fadeInUp 0.25s ease forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.pod-kartica:nth-child(1) { animation-delay: 0.04s; }
|
||||
.pod-kartica:nth-child(2) { animation-delay: 0.10s; }
|
||||
.pod-kartica:nth-child(3) { animation-delay: 0.16s; }
|
||||
.pod-kartica:nth-child(4) { animation-delay: 0.22s; }
|
||||
.pod-kartica:nth-child(5) { animation-delay: 0.28s; }
|
||||
|
||||
.red-prekoracen td {
|
||||
background: rgba(207, 87, 87, 0.06);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.pod-tabela { display: none; }
|
||||
.pod-kartice { display: flex; }
|
||||
}
|
||||
</style>
|
||||
{{end}}
|
||||
|
||||
{{define "sadrzaj"}}
|
||||
<div style="display:flex;flex-direction:column;gap:16px;">
|
||||
|
||||
{{if .Sacuvano}}
|
||||
<div class="poruka-uspeh poruka-animacija">Podsetnik je uspešno sačuvan.</div>
|
||||
{{end}}
|
||||
{{if .Obrisan}}
|
||||
<div class="poruka-uspeh poruka-animacija">Podsetnik je uspešno obrisan.</div>
|
||||
{{end}}
|
||||
|
||||
<!-- gornja traka -->
|
||||
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center;">
|
||||
<a href="/podsetnici/novi"
|
||||
style="padding:8px 16px;background:var(--sb-akcent);color:#fff;border-radius:8px;font-size:14px;font-weight:500;text-decoration:none;white-space:nowrap;transition:opacity 0.2s;"
|
||||
onmouseover="this.style.opacity='0.85'" onmouseout="this.style.opacity='1'">
|
||||
+ Novi podsetnik
|
||||
</a>
|
||||
<form method="GET" action="/podsetnici" style="display:flex;gap:8px;align-items:center;flex:1;min-width:200px;flex-wrap:wrap;">
|
||||
<label style="display:flex;align-items:center;gap:6px;font-size:14px;color:var(--tekst-sporedni);cursor:pointer;white-space:nowrap;">
|
||||
<input type="checkbox" name="samo_aktivni" value="1" {{if .SamoAktivni}}checked{{end}}
|
||||
onchange="this.form.submit()"
|
||||
style="width:auto;padding:0;border:none !important;background:none !important;">
|
||||
Samo aktivni
|
||||
</label>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- desktop tabela -->
|
||||
<div class="kartica pod-tabela" style="padding:0;overflow:hidden;">
|
||||
<div style="overflow-x:auto;">
|
||||
<table style="width:100%;border-collapse:collapse;">
|
||||
<thead>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<th style="padding:12px 16px;text-align:left;font-size:12px;font-weight:500;color:var(--tekst-sporedni);">Naslov</th>
|
||||
<th style="padding:12px 16px;text-align:left;font-size:12px;font-weight:500;color:var(--tekst-sporedni);">Datum podsećanja</th>
|
||||
<th style="padding:12px 16px;text-align:left;font-size:12px;font-weight:500;color:var(--tekst-sporedni);">Status</th>
|
||||
<th style="padding:12px 16px;text-align:center;font-size:12px;font-weight:500;color:var(--tekst-sporedni);">Akcije</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Podsetnici}}
|
||||
<tr class="{{if .JePrekoracen}}red-prekoracen{{end}}"
|
||||
style="border-bottom:0.5px solid var(--ivica);transition:background 0.15s;"
|
||||
onmouseover="this.style.background='var(--pozadina)'"
|
||||
onmouseout="this.style.background=''">
|
||||
<td style="padding:12px 16px;">
|
||||
<div style="font-size:14px;font-weight:500;color:var(--tekst-glavni);">{{.Naslov}}</div>
|
||||
{{if .Napomena}}
|
||||
<div style="font-size:12px;color:var(--tekst-sporedni);margin-top:2px;">{{.Napomena}}</div>
|
||||
{{end}}
|
||||
</td>
|
||||
<td style="padding:12px 16px;font-size:13px;color:{{if .JePrekoracen}}var(--greska){{else}}var(--tekst-sporedni){{end}};">
|
||||
{{.DatumPodsecanja.Format "02.01.2006."}}
|
||||
</td>
|
||||
<td style="padding:12px 16px;">
|
||||
{{if .Zavrseno}}
|
||||
<span style="display:inline-block;padding:3px 10px;background:var(--pozadina-hover);color:var(--tekst-sporedni);border-radius:20px;font-size:12px;">Završeno</span>
|
||||
{{else if .JePrekoracen}}
|
||||
<span style="display:inline-block;padding:3px 10px;background:rgba(207,87,87,0.12);color:var(--greska);border-radius:20px;font-size:12px;">Prekoračeno</span>
|
||||
{{else}}
|
||||
<span style="display:inline-block;padding:3px 10px;background:rgba(93,184,118,0.12);color:var(--uspeh);border-radius:20px;font-size:12px;">Aktivno</span>
|
||||
{{end}}
|
||||
</td>
|
||||
<td style="padding:12px 16px;text-align:center;">
|
||||
<div style="display:flex;align-items:center;justify-content:center;gap:8px;">
|
||||
<a href="/podsetnici/izmeni/{{.ID}}"
|
||||
style="padding:4px 10px;background:var(--sb-aktivan);color:var(--tekst-jak);border-radius:6px;font-size:12px;text-decoration:none;transition:opacity 0.2s;"
|
||||
onmouseover="this.style.opacity='0.8'" onmouseout="this.style.opacity='1'">
|
||||
Izmeni
|
||||
</a>
|
||||
<form method="POST" action="/podsetnici/zavrseno/{{.ID}}" style="display:inline;">
|
||||
<button type="submit"
|
||||
style="padding:4px 10px;background:var(--sb-aktivan);color:var(--tekst-jak);border:none;border-radius:6px;font-size:12px;cursor:pointer;transition:opacity 0.2s;"
|
||||
onmouseover="this.style.opacity='0.8'" onmouseout="this.style.opacity='1'">
|
||||
{{if .Zavrseno}}Aktiviraj{{else}}Završi{{end}}
|
||||
</button>
|
||||
</form>
|
||||
<form method="POST" action="/podsetnici/obrisi/{{.ID}}" style="display:inline;">
|
||||
<button type="submit"
|
||||
onclick="return confirm('Da li ste sigurni da želite da obrišete podsetnik?')"
|
||||
style="padding:4px 10px;background:#dc2626;color:#fff;border:none;border-radius:6px;font-size:12px;cursor:pointer;transition:opacity 0.2s;"
|
||||
onmouseover="this.style.opacity='0.8'" onmouseout="this.style.opacity='1'">
|
||||
Obriši
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr>
|
||||
<td colspan="4" style="padding:32px;text-align:center;font-size:14px;color:var(--tekst-sporedni);">
|
||||
Nema podsetnika. <a href="/podsetnici/novi" style="color:var(--sb-akcent);">Dodaj prvi podsetnik.</a>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- mobilne kartice -->
|
||||
<div class="pod-kartice">
|
||||
{{range .Podsetnici}}
|
||||
<div class="kartica pod-kartica">
|
||||
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:12px;margin-bottom:10px;">
|
||||
<div>
|
||||
<div style="font-size:15px;font-weight:500;color:{{if .JePrekoracen}}var(--greska){{else}}var(--tekst-glavni){{end}};">{{.Naslov}}</div>
|
||||
{{if .Napomena}}
|
||||
<div style="font-size:12px;color:var(--tekst-sporedni);margin-top:2px;">{{.Napomena}}</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<div>
|
||||
{{if .Zavrseno}}
|
||||
<span style="display:inline-block;padding:3px 10px;background:var(--pozadina-hover);color:var(--tekst-sporedni);border-radius:20px;font-size:12px;">Završeno</span>
|
||||
{{else if .JePrekoracen}}
|
||||
<span style="display:inline-block;padding:3px 10px;background:rgba(207,87,87,0.12);color:var(--greska);border-radius:20px;font-size:12px;">Prekoračeno</span>
|
||||
{{else}}
|
||||
<span style="display:inline-block;padding:3px 10px;background:rgba(93,184,118,0.12);color:var(--uspeh);border-radius:20px;font-size:12px;">Aktivno</span>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div style="font-size:13px;color:var(--tekst-sporedni);margin-bottom:12px;">
|
||||
{{.DatumPodsecanja.Format "02.01.2006."}}
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap;">
|
||||
<a href="/podsetnici/izmeni/{{.ID}}"
|
||||
style="padding:6px 14px;background:var(--sb-aktivan);color:var(--tekst-jak);border-radius:6px;font-size:13px;text-decoration:none;">
|
||||
Izmeni
|
||||
</a>
|
||||
<form method="POST" action="/podsetnici/zavrseno/{{.ID}}" style="display:inline;">
|
||||
<button type="submit"
|
||||
style="padding:6px 14px;background:var(--sb-aktivan);color:var(--tekst-jak);border:none;border-radius:6px;font-size:13px;cursor:pointer;">
|
||||
{{if .Zavrseno}}Aktiviraj{{else}}Završi{{end}}
|
||||
</button>
|
||||
</form>
|
||||
<form method="POST" action="/podsetnici/obrisi/{{.ID}}" style="display:inline;">
|
||||
<button type="submit"
|
||||
onclick="return confirm('Da li ste sigurni da želite da obrišete podsetnik?')"
|
||||
style="padding:6px 14px;background:#dc2626;color:#fff;border:none;border-radius:6px;font-size:13px;cursor:pointer;">
|
||||
Obriši
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div style="padding:32px;text-align:center;font-size:14px;color:var(--tekst-sporedni);">
|
||||
Nema podsetnika. <a href="/podsetnici/novi" style="color:var(--sb-akcent);">Dodaj prvi podsetnik.</a>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,99 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "naslov"}}{{if .Izmena}}Izmeni podsetnik{{else}}Novi podsetnik{{end}} — NTech{{end}}
|
||||
|
||||
{{define "dodatni-css"}}
|
||||
<style>
|
||||
@keyframes fadeInUp {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
20% { transform: translateX(-6px); }
|
||||
40% { transform: translateX(6px); }
|
||||
60% { transform: translateX(-4px); }
|
||||
80% { transform: translateX(4px); }
|
||||
}
|
||||
|
||||
.forma-kartica {
|
||||
animation: fadeInUp 0.3s ease forwards;
|
||||
}
|
||||
|
||||
.greska-animacija {
|
||||
animation: shake 0.4s ease;
|
||||
}
|
||||
</style>
|
||||
{{end}}
|
||||
|
||||
{{define "sadrzaj"}}
|
||||
<div style="width:100%;">
|
||||
|
||||
<a href="/podsetnici"
|
||||
style="display:inline-flex;align-items:center;gap:6px;font-size:13px;color:var(--tekst-sporedni);text-decoration:none;margin-bottom:20px;transition:color 0.2s;"
|
||||
onmouseover="this.style.color='var(--tekst-glavni)'" onmouseout="this.style.color='var(--tekst-sporedni)'">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>
|
||||
Nazad na podsetnik
|
||||
</a>
|
||||
|
||||
<div class="kartica forma-kartica" style="max-width:600px;">
|
||||
<div style="margin-bottom:20px;padding-bottom:14px;border-bottom:0.5px solid var(--ivica);">
|
||||
<span style="font-size:16px;font-weight:500;color:var(--tekst-glavni);">
|
||||
{{if .Izmena}}Izmeni podsetnik{{else}}Novi podsetnik{{end}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{{if .Greska}}
|
||||
<div class="greska-animacija"
|
||||
style="background:var(--kartica);border:0.5px solid var(--greska);border-radius:8px;padding:10px 14px;margin-bottom:16px;font-size:13px;color:var(--greska);">
|
||||
{{.Greska}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<form method="POST" action="{{if .Izmena}}/podsetnici/izmeni/{{.Podsetnik.ID}}{{else}}/podsetnici/novi{{end}}">
|
||||
<div style="display:flex;flex-direction:column;gap:16px;">
|
||||
|
||||
<div>
|
||||
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:6px;">
|
||||
Naslov <span style="color:var(--greska);">*</span>
|
||||
</label>
|
||||
<input type="text" name="naslov" value="{{.Podsetnik.Naslov}}"
|
||||
placeholder="npr. Obnoviti ugovor sa dobavljačem"
|
||||
style="width:100%;" autofocus>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:6px;">
|
||||
Datum podsećanja <span style="color:var(--greska);">*</span>
|
||||
</label>
|
||||
<input type="date" name="datum_podsecanja"
|
||||
value="{{if not .Podsetnik.DatumPodsecanja.IsZero}}{{.Podsetnik.DatumPodsecanja.Format "2006-01-02"}}{{end}}"
|
||||
style="width:100%;">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:6px;">Napomena</label>
|
||||
<textarea name="napomena" rows="4"
|
||||
placeholder="Detalji ili beleška vezana za ovaj podsetnik..."
|
||||
style="width:100%;resize:vertical;">{{.Podsetnik.Napomena}}</textarea>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;justify-content:flex-end;gap:10px;margin-top:4px;">
|
||||
<a href="/podsetnici"
|
||||
style="padding:9px 20px;border:0.5px solid var(--ivica);border-radius:8px;font-size:14px;color:var(--tekst-sporedni);text-decoration:none;transition:background 0.2s;"
|
||||
onmouseover="this.style.background='var(--pozadina)'" onmouseout="this.style.background=''">
|
||||
Odustani
|
||||
</a>
|
||||
<button type="submit"
|
||||
style="padding:9px 20px;background:var(--sb-akcent);color:#fff;border:none;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;transition:opacity 0.2s;"
|
||||
onmouseover="this.style.opacity='0.85'" onmouseout="this.style.opacity='1'">
|
||||
{{if .Izmena}}Sačuvaj izmene{{else}}Dodaj podsetnik{{end}}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user