Ispravke — bezbednost, CSS teme, handleri, sidebar, servis forma

This commit is contained in:
2026-06-03 23:13:15 +02:00
parent f5b6b0c7ee
commit 4b7ed36473
19 changed files with 352 additions and 519 deletions
+41 -29
View File
@@ -1,6 +1,6 @@
# NTech
![Go Version](https://img.shields.io/badge/go-1.26-blue)
![Go Version](https://img.shields.io/badge/go-1.24-blue)
![License](https://img.shields.io/badge/license-MIT-green)
Poslovna aplikacija za upravljanje servisom računara, magacinom delova i prodajom. Napravljena u Go-u, radi u brauzeru, ne zahteva internet vezu ni eksterne servise.
@@ -23,22 +23,27 @@ Cilj je jednostavan: sve što servis treba da prati nalazi se na jednom mestu, b
- Inicijalno podešavanje pri prvom pokretanju (setup wizard)
- Sistem migracija baze podataka
- Osnovna struktura baze: artikli, kategorije, klijenti, dobavljači, servisni nalozi, prodajni nalozi, podsetnici
### U razvoju
- Korisničko sučelje — sidebar navigacija, sistem tema, dashboard
- Magacin — praćenje stanja delova, lokacija, količina
- Servisni nalozi — prijem, dijagnostika, statusna traka, cene
- Prodajni nalozi — stavke, obračun, evidencija kupaca
- Klijenti i dobavljači — baza kontakata, istorija
- Korisnički interfejs — sidebar navigacija, sistem tema (tamna/svetla), dashboard sa statistikama
- Prijava korisnika — sesije na serveru, zaključavanje naloga
- Dvofaktorska autentifikacija (TOTP) — aktivacija sa QR kodom, rezervni kodovi
- Bruteforce zaštita — IP zaključavanje nakon 5 neuspelih pokušaja u 15 minuta
- CSRF zaštita — double-submit cookie pattern, automatska injekcija tokena u sve forme
- Bezbednosni HTTP headeri (CSP, X-Frame-Options, Referrer-Policy, nosniff...)
- Evidencija pokušaja prijave — istorija po korisniku, IP, razlog, datum
- Korisnici i uloge — admin panel, upravljanje korisnicima
- Magacin — artikli, kategorije, filtriranje, kritični nivoi zaliha
- Servisni nalozi — prijem, statusna traka, troškovi, priznanica
- Prodajni nalozi — stavke, obračun, priznanica sa podacima firme i klijenta
- Nabavke — evidencija nabavki od dobavljača
- Klijenti i dobavljači — baza kontakata
- Podsetnici — evidencija sa rokom
- Izveštaji — pregled prihoda, stanje magacina
- Podešavanja — naziv, adresa, PIB, logo firme; promena teme
### Planirano
- Prijava korisnika — sesije, zaključavanje naloga
- Dvofaktorska autentifikacija (TOTP)
- Izveštaji — prodaja, stanje magacina, prihodi
- Podrška za PostgreSQL (za višekorisničko okruženje)
- WebAuthn / Passkey prijava (šema baze je pripremljena)
- Obaveštenja (e-pošta / WhatsApp) — odloženo za kasniju fazu
- Skeniranje barkodova putem kamere — odloženo za kasniju fazu
@@ -47,12 +52,10 @@ Cilj je jednostavan: sve što servis treba da prati nalazi se na jednom mestu, b
## Tehnologije
| Tehnologija | Uloga |
| ------------------------------------------------------------------------------------ | -------------------------------- |
| ------------------------------------------------------------------------------------ | ------------------------------- |
| [Go](https://go.dev) | backend jezik |
| [chi](https://github.com/go-chi/chi) | HTTP ruter |
| [html/template](https://pkg.go.dev/html/template) | serverski šabloni |
| [HTMX](https://htmx.org) | interaktivnost bez build procesa |
| [TailwindCSS](https://tailwindcss.com) | stilizovanje |
| [Alpine.js](https://alpinejs.dev) | UI logika na strani klijenta |
| [SQLite](https://sqlite.org) + [modernc.org/sqlite](https://gitlab.com/cznic/sqlite) | glavna baza (čisti Go, bez CGO) |
| [PostgreSQL](https://www.postgresql.org) + [pgx/v5](https://github.com/jackc/pgx) | opciona baza za produkciju |
@@ -73,22 +76,29 @@ Cilj je jednostavan: sve što servis treba da prati nalazi se na jednom mestu, b
git clone <url-repozitorijuma>
cd GoNtech
# 2. Kopiranje i popunjavanje .env fajla
cp .env.example .env
# Otvori .env i postavi vrednosti (videti tabelu ispod)
# 2. Kopiranje konfiguracionog fajla
cp ntech.env.example ntech.env
# Otvori ntech.env i postavi vrednosti (videti tabelu ispod)
# 3. Pokretanje u razvojnom okruženju
# 3. Učitavanje promenljivih i pokretanje u razvojnom okruženju
export $(grep -v '^#' ntech.env | xargs)
go run ./cmd/ntech
```
Program se otvara na `http://localhost:8080`.
Program se otvara na `http://localhost:8080` (ili na portu definisanom u `ntech.env`).
Pri prvom pokretanju automatski se pokreće setup wizard.
### Produkcioni build
```bash
CGO_ENABLED=0 go build -o ntech ./cmd/ntech
# Pomoću build.sh skripte (prima opcioni argument verzije)
./build.sh 1.0.0
# Ili ručno
CGO_ENABLED=0 GOARCH=amd64 GOOS=linux go build \
-ldflags "-X main.Verzija=1.0.0 -s -w" \
-o ntech ./cmd/ntech
./ntech
```
@@ -98,7 +108,7 @@ Rezultat je jedan statički binarni fajl bez zavisnosti.
## Promenljive okruženja
Kopirati `.env.example` u `.env` i popuniti vrednosti. Fajl `.env` se **ne commituje** u Git.
Kopirati `ntech.env.example` u `ntech.env` i popuniti vrednosti. Fajl `ntech.env` se **ne commituje** u Git.
| Promenljiva | Podrazumevano | Opis |
| -------------- | ------------- | -------------------------------------------- |
@@ -118,19 +128,21 @@ ntech/
├── cmd/
│ └── ntech/ # ulazna tačka programa
├── internal/
│ ├── auth/ # prijava, sesije, 2FA
│ ├── auth/ # prijava, sesije, fail2ban log
│ ├── config/ # podešavanja, setup wizard
│ ├── db/ # sloj baze podataka
│ │ ── sqlite/ # SQLite implementacija
│ │ └── postgres/ # PostgreSQL implementacija
│ │ ── sqlite/ # SQLite implementacija
│ ├── handler/ # HTTP handleri
│ ├── middleware/ # autentifikacija, logovanje, ograničavanje zahteva
│ ├── middleware/ # CSRF, bezbednost headeri, autentifikacija
│ └── model/ # zajednički tipovi podataka
├── web/
│ ├── static/ # CSS, JavaScript, slike
│ ├── static/ # CSS, JavaScript, slike, logotipi
│ └── templates/ # HTML šabloni
├── migrations/ # SQL migracije (001_opis.sql, 002_opis.sql, ...)
├── .env.example # primer konfiguracije
├── logs/ # auth.log i ostali logovi
├── backups/ # rezervne kopije baze
├── build.sh # skripta za produkcioni build
├── ntech.env # lokalna konfiguracija (ne commituje se)
├── go.mod
└── go.sum
```
+3 -3
View File
@@ -192,7 +192,7 @@ func (h *Handler) AdminProfil(w http.ResponseWriter, r *http.Request) {
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "admin"
ps.Stranica = "profil"
ps.NaslovStranice = "Moj profil"
podaci := podaciAdminProfil{
@@ -261,7 +261,7 @@ func (h *Handler) AdminTotpPokreni(w http.ResponseWriter, r *http.Request) {
}
ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "admin"
ps.Stranica = "profil"
ps.NaslovStranice = "Podesi 2FA"
podaci := podaciAdminProfil{
@@ -294,7 +294,7 @@ func (h *Handler) AdminTotpAktivacija(w http.ResponseWriter, r *http.Request) {
// ponovni prikaz sa greškom — regenerišemo isti tajnu
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "admin"
ps.Stranica = "profil"
ps.NaslovStranice = "Podesi 2FA"
// regenerišemo QR za već generisanu tajnu (korisnik je video ovaj QR)
+1 -1
View File
@@ -30,7 +30,7 @@ func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) {
if err := h.DB.QueryRowContext(ctx, `
SELECT COUNT(*) FROM servisni_nalozi
WHERE status NOT IN ('Završeno', 'Preuzeto')`,
WHERE status != 'Završeno'`,
).Scan(&aktivniServisi); err != nil {
log.Printf("dashboard: aktivni servisi: %v", err)
}
+20 -50
View File
@@ -43,17 +43,11 @@ func (h *Handler) Dobavljaci(w http.ResponseWriter, r *http.Request) {
return
}
ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "dobavljaci"
ps.NaslovStranice = "Dobavljači"
podaci := PodaciDobavljaca{
PodaciStranice: model.PodaciStranice{
Stranica: "dobavljaci",
NaslovStranice: "Dobavljači",
Tema: podesavanja["tema"],
NazivFirme: podesavanja["naziv_firme"],
Podnazlov: podesavanja["podnazlov"],
LogoTip: podesavanja["logo_tip"],
LogoPutanja: podesavanja["logo_putanja"],
Korisnik: "Admin",
},
PodaciStranice: ps,
Dobavljaci: dobavljaci,
Pretraga: pretraga,
Sacuvano: r.URL.Query().Get("sacuvano") == "1",
@@ -71,17 +65,11 @@ func (h *Handler) NoviDobavljac(w http.ResponseWriter, r *http.Request) {
return
}
ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "dobavljaci"
ps.NaslovStranice = "Novi dobavljač"
h.renderujFormuDobavljaca(w, PodaciFormeDobavljaca{
PodaciStranice: model.PodaciStranice{
Stranica: "dobavljaci",
NaslovStranice: "Novi dobavljač",
Tema: podesavanja["tema"],
NazivFirme: podesavanja["naziv_firme"],
Podnazlov: podesavanja["podnazlov"],
LogoTip: podesavanja["logo_tip"],
LogoPutanja: podesavanja["logo_putanja"],
Korisnik: "Admin",
},
PodaciStranice: ps,
Izmena: false,
})
}
@@ -96,17 +84,11 @@ func (h *Handler) SacuvajDobavljaca(w http.ResponseWriter, r *http.Request) {
dobavljac, greska := parseFormuDobavljaca(r)
if greska != "" {
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "dobavljaci"
ps.NaslovStranice = "Novi dobavljač"
h.renderujFormuDobavljaca(w, PodaciFormeDobavljaca{
PodaciStranice: model.PodaciStranice{
Stranica: "dobavljaci",
NaslovStranice: "Novi dobavljač",
Tema: podesavanja["tema"],
NazivFirme: podesavanja["naziv_firme"],
Podnazlov: podesavanja["podnazlov"],
LogoTip: podesavanja["logo_tip"],
LogoPutanja: podesavanja["logo_putanja"],
Korisnik: "Admin",
},
PodaciStranice: ps,
Dobavljac: dobavljac,
Greska: greska,
Izmena: false,
@@ -142,17 +124,11 @@ func (h *Handler) IzmeniDobavljaca(w http.ResponseWriter, r *http.Request) {
return
}
ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "dobavljaci"
ps.NaslovStranice = "Izmeni dobavljača"
h.renderujFormuDobavljaca(w, PodaciFormeDobavljaca{
PodaciStranice: model.PodaciStranice{
Stranica: "dobavljaci",
NaslovStranice: "Izmeni dobavljača",
Tema: podesavanja["tema"],
NazivFirme: podesavanja["naziv_firme"],
Podnazlov: podesavanja["podnazlov"],
LogoTip: podesavanja["logo_tip"],
LogoPutanja: podesavanja["logo_putanja"],
Korisnik: "Admin",
},
PodaciStranice: ps,
Dobavljac: *dobavljac,
Izmena: true,
})
@@ -175,17 +151,11 @@ func (h *Handler) SacuvajIzmeneDobavljaca(w http.ResponseWriter, r *http.Request
if greska != "" {
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
dobavljac.ID = id
ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "dobavljaci"
ps.NaslovStranice = "Izmeni dobavljača"
h.renderujFormuDobavljaca(w, PodaciFormeDobavljaca{
PodaciStranice: model.PodaciStranice{
Stranica: "dobavljaci",
NaslovStranice: "Izmeni dobavljača",
Tema: podesavanja["tema"],
NazivFirme: podesavanja["naziv_firme"],
Podnazlov: podesavanja["podnazlov"],
LogoTip: podesavanja["logo_tip"],
LogoPutanja: podesavanja["logo_putanja"],
Korisnik: "Admin",
},
PodaciStranice: ps,
Dobavljac: dobavljac,
Greska: greska,
Izmena: true,
+4 -10
View File
@@ -32,17 +32,11 @@ func (h *Handler) Kategorije(w http.ResponseWriter, r *http.Request) {
return
}
ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "magacin"
ps.NaslovStranice = "Kategorije"
podaci := PodaciKategorija{
PodaciStranice: model.PodaciStranice{
Stranica: "magacin",
NaslovStranice: "Kategorije",
Tema: podesavanja["tema"],
NazivFirme: podesavanja["naziv_firme"],
Podnazlov: podesavanja["podnazlov"],
LogoTip: podesavanja["logo_tip"],
LogoPutanja: podesavanja["logo_putanja"],
Korisnik: "Admin",
},
PodaciStranice: ps,
Kategorije: kategorije,
Sacuvano: r.URL.Query().Get("sacuvano") == "1",
Obrisana: r.URL.Query().Get("obrisana") == "1",
+28 -70
View File
@@ -44,17 +44,11 @@ func (h *Handler) Klijenti(w http.ResponseWriter, r *http.Request) {
return
}
ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "klijenti"
ps.NaslovStranice = "Klijenti"
podaci := PodaciKlijenata{
PodaciStranice: model.PodaciStranice{
Stranica: "klijenti",
NaslovStranice: "Klijenti",
Tema: podesavanja["tema"],
NazivFirme: podesavanja["naziv_firme"],
Podnazlov: podesavanja["podnazlov"],
LogoTip: podesavanja["logo_tip"],
LogoPutanja: podesavanja["logo_putanja"],
Korisnik: "Admin",
},
PodaciStranice: ps,
Klijenti: klijenti,
Pretraga: pretraga,
Sacuvano: r.URL.Query().Get("sacuvano") == "1",
@@ -72,17 +66,11 @@ func (h *Handler) NoviKlijent(w http.ResponseWriter, r *http.Request) {
return
}
ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "klijenti"
ps.NaslovStranice = "Novi klijent"
h.renderujFormuKlijenta(w, PodaciFormeKlijenta{
PodaciStranice: model.PodaciStranice{
Stranica: "klijenti",
NaslovStranice: "Novi klijent",
Tema: podesavanja["tema"],
NazivFirme: podesavanja["naziv_firme"],
Podnazlov: podesavanja["podnazlov"],
LogoTip: podesavanja["logo_tip"],
LogoPutanja: podesavanja["logo_putanja"],
Korisnik: "Admin",
},
PodaciStranice: ps,
Izmena: false,
})
}
@@ -97,17 +85,11 @@ func (h *Handler) SacuvajKlijenta(w http.ResponseWriter, r *http.Request) {
klijent, greska := parseFormuKlijenta(r)
if greska != "" {
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "klijenti"
ps.NaslovStranice = "Novi klijent"
h.renderujFormuKlijenta(w, PodaciFormeKlijenta{
PodaciStranice: model.PodaciStranice{
Stranica: "klijenti",
NaslovStranice: "Novi klijent",
Tema: podesavanja["tema"],
NazivFirme: podesavanja["naziv_firme"],
Podnazlov: podesavanja["podnazlov"],
LogoTip: podesavanja["logo_tip"],
LogoPutanja: podesavanja["logo_putanja"],
Korisnik: "Admin",
},
PodaciStranice: ps,
Klijent: klijent,
Greska: greska,
Izmena: false,
@@ -118,17 +100,11 @@ 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)
ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "klijenti"
ps.NaslovStranice = "Novi klijent"
h.renderujFormuKlijenta(w, PodaciFormeKlijenta{
PodaciStranice: model.PodaciStranice{
Stranica: "klijenti",
NaslovStranice: "Novi klijent",
Tema: podesavanja["tema"],
NazivFirme: podesavanja["naziv_firme"],
Podnazlov: podesavanja["podnazlov"],
LogoTip: podesavanja["logo_tip"],
LogoPutanja: podesavanja["logo_putanja"],
Korisnik: "Admin",
},
PodaciStranice: ps,
Klijent: klijent,
Greska: "Došlo je do greške pri čuvanju. Pokušajte ponovo.",
Izmena: false,
@@ -159,17 +135,11 @@ func (h *Handler) IzmeniKlijenta(w http.ResponseWriter, r *http.Request) {
return
}
ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "klijenti"
ps.NaslovStranice = "Izmeni klijenta"
h.renderujFormuKlijenta(w, PodaciFormeKlijenta{
PodaciStranice: model.PodaciStranice{
Stranica: "klijenti",
NaslovStranice: "Izmeni klijenta",
Tema: podesavanja["tema"],
NazivFirme: podesavanja["naziv_firme"],
Podnazlov: podesavanja["podnazlov"],
LogoTip: podesavanja["logo_tip"],
LogoPutanja: podesavanja["logo_putanja"],
Korisnik: "Admin",
},
PodaciStranice: ps,
Klijent: *klijent,
Izmena: true,
})
@@ -192,17 +162,11 @@ func (h *Handler) SacuvajIzmenuKlijenta(w http.ResponseWriter, r *http.Request)
if greska != "" {
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
klijent.ID = id
ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "klijenti"
ps.NaslovStranice = "Izmeni klijenta"
h.renderujFormuKlijenta(w, PodaciFormeKlijenta{
PodaciStranice: model.PodaciStranice{
Stranica: "klijenti",
NaslovStranice: "Izmeni klijenta",
Tema: podesavanja["tema"],
NazivFirme: podesavanja["naziv_firme"],
Podnazlov: podesavanja["podnazlov"],
LogoTip: podesavanja["logo_tip"],
LogoPutanja: podesavanja["logo_putanja"],
Korisnik: "Admin",
},
PodaciStranice: ps,
Klijent: klijent,
Greska: greska,
Izmena: true,
@@ -214,17 +178,11 @@ 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)
ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "klijenti"
ps.NaslovStranice = "Izmeni klijenta"
h.renderujFormuKlijenta(w, PodaciFormeKlijenta{
PodaciStranice: model.PodaciStranice{
Stranica: "klijenti",
NaslovStranice: "Izmeni klijenta",
Tema: podesavanja["tema"],
NazivFirme: podesavanja["naziv_firme"],
Podnazlov: podesavanja["podnazlov"],
LogoTip: podesavanja["logo_tip"],
LogoPutanja: podesavanja["logo_putanja"],
Korisnik: "Admin",
},
PodaciStranice: ps,
Klijent: klijent,
Greska: "Došlo je do greške pri čuvanju. Pokušajte ponovo.",
Izmena: true,
+4 -10
View File
@@ -56,17 +56,11 @@ func (h *Handler) Magacin(w http.ResponseWriter, r *http.Request) {
return
}
ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "magacin"
ps.NaslovStranice = "Magacin"
podaci := PodaciMagacina{
PodaciStranice: model.PodaciStranice{
Stranica: "magacin",
NaslovStranice: "Magacin",
Tema: podesavanja["tema"],
NazivFirme: podesavanja["naziv_firme"],
Podnazlov: podesavanja["podnazlov"],
LogoTip: podesavanja["logo_tip"],
LogoPutanja: podesavanja["logo_putanja"],
Korisnik: "Admin",
},
PodaciStranice: ps,
Artikli: artikli,
Kategorije: kategorije,
Filter: filter,
+20 -48
View File
@@ -35,22 +35,14 @@ func (h *Handler) NoviArtikal(w http.ResponseWriter, r *http.Request) {
return
}
podaci := PodaciFormeArtikla{
PodaciStranice: model.PodaciStranice{
Stranica: "magacin",
NaslovStranice: "Novi artikal",
Tema: podesavanja["tema"],
NazivFirme: podesavanja["naziv_firme"],
Podnazlov: podesavanja["podnazlov"],
LogoTip: podesavanja["logo_tip"],
LogoPutanja: podesavanja["logo_putanja"],
Korisnik: "Admin",
},
ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "magacin"
ps.NaslovStranice = "Novi artikal"
h.renderujFormuArtikla(w, PodaciFormeArtikla{
PodaciStranice: ps,
Kategorije: kategorije,
Izmena: false,
}
h.renderujFormuArtikla(w, podaci)
})
}
// SacuvajArtikal prima POST formu i čuva novi artikal
@@ -68,17 +60,11 @@ func (h *Handler) SacuvajArtikal(w http.ResponseWriter, r *http.Request) {
if artikal.KategorijaID != nil {
katIDStr = strconv.FormatInt(*artikal.KategorijaID, 10)
}
ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "magacin"
ps.NaslovStranice = "Novi artikal"
h.renderujFormuArtikla(w, PodaciFormeArtikla{
PodaciStranice: model.PodaciStranice{
Stranica: "magacin",
NaslovStranice: "Novi artikal",
Tema: podesavanja["tema"],
NazivFirme: podesavanja["naziv_firme"],
Podnazlov: podesavanja["podnazlov"],
LogoTip: podesavanja["logo_tip"],
LogoPutanja: podesavanja["logo_putanja"],
Korisnik: "Admin",
},
PodaciStranice: ps,
Artikal: artikal,
Kategorije: kategorije,
KategorijaIDStr: katIDStr,
@@ -136,24 +122,16 @@ func (h *Handler) IzmeniArtikal(w http.ResponseWriter, r *http.Request) {
katIDStr = strconv.FormatInt(*artikal.KategorijaID, 10)
}
podaci := PodaciFormeArtikla{
PodaciStranice: model.PodaciStranice{
Stranica: "magacin",
NaslovStranice: "Izmeni artikal",
Tema: podesavanja["tema"],
NazivFirme: podesavanja["naziv_firme"],
Podnazlov: podesavanja["podnazlov"],
LogoTip: podesavanja["logo_tip"],
LogoPutanja: podesavanja["logo_putanja"],
Korisnik: "Admin",
},
ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "magacin"
ps.NaslovStranice = "Izmeni artikal"
h.renderujFormuArtikla(w, PodaciFormeArtikla{
PodaciStranice: ps,
Artikal: *artikal,
Kategorije: kategorije,
KategorijaIDStr: katIDStr,
Izmena: true,
}
h.renderujFormuArtikla(w, podaci)
})
}
// SacuvajIzmenuArtikla prima POST formu i čuva izmenu artikla
@@ -179,17 +157,11 @@ func (h *Handler) SacuvajIzmenuArtikla(w http.ResponseWriter, r *http.Request) {
if artikal.KategorijaID != nil {
katIDStr = strconv.FormatInt(*artikal.KategorijaID, 10)
}
ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "magacin"
ps.NaslovStranice = "Izmeni artikal"
h.renderujFormuArtikla(w, PodaciFormeArtikla{
PodaciStranice: model.PodaciStranice{
Stranica: "magacin",
NaslovStranice: "Izmeni artikal",
Tema: podesavanja["tema"],
NazivFirme: podesavanja["naziv_firme"],
Podnazlov: podesavanja["podnazlov"],
LogoTip: podesavanja["logo_tip"],
LogoPutanja: podesavanja["logo_putanja"],
Korisnik: "Admin",
},
PodaciStranice: ps,
Artikal: artikal,
Kategorije: kategorije,
KategorijaIDStr: katIDStr,
+16 -40
View File
@@ -68,17 +68,11 @@ func (h *Handler) Nabavke(w http.ResponseWriter, r *http.Request) {
return
}
ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "nabavke"
ps.NaslovStranice = "Nabavke"
podaci := PodaciNabavki{
PodaciStranice: model.PodaciStranice{
Stranica: "nabavke",
NaslovStranice: "Nabavke",
Tema: podesavanja["tema"],
NazivFirme: podesavanja["naziv_firme"],
Podnazlov: podesavanja["podnazlov"],
LogoTip: podesavanja["logo_tip"],
LogoPutanja: podesavanja["logo_putanja"],
Korisnik: "Admin",
},
PodaciStranice: ps,
Nabavke: nabavke,
Sacuvano: r.URL.Query().Get("sacuvano") == "1",
Obrisan: r.URL.Query().Get("obrisan") == "1",
@@ -113,17 +107,11 @@ func (h *Handler) NovaNabavka(w http.ResponseWriter, r *http.Request) {
return
}
ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "nabavke"
ps.NaslovStranice = "Nova nabavka"
h.renderujFormuNabavke(w, PodaciFormeNabavke{
PodaciStranice: model.PodaciStranice{
Stranica: "nabavke",
NaslovStranice: "Nova nabavka",
Tema: podesavanja["tema"],
NazivFirme: podesavanja["naziv_firme"],
Podnazlov: podesavanja["podnazlov"],
LogoTip: podesavanja["logo_tip"],
LogoPutanja: podesavanja["logo_putanja"],
Korisnik: "Admin",
},
PodaciStranice: ps,
Artikli: artikli,
ArtikliJSON: artikalUJSON(artikli),
Dobavljaci: dobavljaci,
@@ -144,17 +132,11 @@ 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())
ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "nabavke"
ps.NaslovStranice = "Nova nabavka"
h.renderujFormuNabavke(w, PodaciFormeNabavke{
PodaciStranice: model.PodaciStranice{
Stranica: "nabavke",
NaslovStranice: "Nova nabavka",
Tema: podesavanja["tema"],
NazivFirme: podesavanja["naziv_firme"],
Podnazlov: podesavanja["podnazlov"],
LogoTip: podesavanja["logo_tip"],
LogoPutanja: podesavanja["logo_putanja"],
Korisnik: "Admin",
},
PodaciStranice: ps,
Artikli: artikli,
ArtikliJSON: artikalUJSON(artikli),
Dobavljaci: dobavljaci,
@@ -208,17 +190,11 @@ func (h *Handler) DetaljiNabavke(w http.ResponseWriter, r *http.Request) {
}
}
ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "nabavke"
ps.NaslovStranice = "Detalji nabavke"
podaci := PodaciDetaljiNabavke{
PodaciStranice: model.PodaciStranice{
Stranica: "nabavke",
NaslovStranice: "Detalji nabavke",
Tema: podesavanja["tema"],
NazivFirme: podesavanja["naziv_firme"],
Podnazlov: podesavanja["podnazlov"],
LogoTip: podesavanja["logo_tip"],
LogoPutanja: podesavanja["logo_putanja"],
Korisnik: "Admin",
},
PodaciStranice: ps,
Nabavka: *nabavka,
Stavke: stavke,
DobavljacNaziv: dobavljacNaziv,
+4 -10
View File
@@ -40,17 +40,11 @@ func (h *Handler) Podesavanja(w http.ResponseWriter, r *http.Request) {
return
}
ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "podesavanja"
ps.NaslovStranice = "Podešavanja"
podaci := PodaciPodesavanja{
PodaciStranice: model.PodaciStranice{
Stranica: "podesavanja",
NaslovStranice: "Podešavanja",
Tema: podesavanja["tema"],
NazivFirme: podesavanja["naziv_firme"],
Podnazlov: podesavanja["podnazlov"],
LogoTip: podesavanja["logo_tip"],
LogoPutanja: podesavanja["logo_putanja"],
Korisnik: "Admin",
},
PodaciStranice: ps,
NazivFirme: podesavanja["naziv_firme"],
Podnazlov: podesavanja["podnazlov"],
Adresa: podesavanja["adresa"],
+16 -40
View File
@@ -88,17 +88,11 @@ func (h *Handler) Prodaja(w http.ResponseWriter, r *http.Request) {
return
}
ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "prodaja"
ps.NaslovStranice = "Prodaja"
podaci := PodaciProdaje{
PodaciStranice: model.PodaciStranice{
Stranica: "prodaja",
NaslovStranice: "Prodaja",
Tema: podesavanja["tema"],
NazivFirme: podesavanja["naziv_firme"],
Podnazlov: podesavanja["podnazlov"],
LogoTip: podesavanja["logo_tip"],
LogoPutanja: podesavanja["logo_putanja"],
Korisnik: "Admin",
},
PodaciStranice: ps,
Nalozi: nalozi,
Sacuvano: r.URL.Query().Get("sacuvano") == "1",
Obrisan: r.URL.Query().Get("obrisan") == "1",
@@ -128,17 +122,11 @@ func (h *Handler) NovaProdaja(w http.ResponseWriter, r *http.Request) {
return
}
ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "prodaja"
ps.NaslovStranice = "Nova prodaja"
h.renderujFormuProdaje(w, PodaciFormeProdaje{
PodaciStranice: model.PodaciStranice{
Stranica: "prodaja",
NaslovStranice: "Nova prodaja",
Tema: podesavanja["tema"],
NazivFirme: podesavanja["naziv_firme"],
Podnazlov: podesavanja["podnazlov"],
LogoTip: podesavanja["logo_tip"],
LogoPutanja: podesavanja["logo_putanja"],
Korisnik: "Admin",
},
PodaciStranice: ps,
Artikli: artikli,
ArtikliJSON: artikalUJSONSaCenom(artikli),
Klijenti: klijenti,
@@ -158,17 +146,11 @@ 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(), "")
ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "prodaja"
ps.NaslovStranice = "Nova prodaja"
h.renderujFormuProdaje(w, PodaciFormeProdaje{
PodaciStranice: model.PodaciStranice{
Stranica: "prodaja",
NaslovStranice: "Nova prodaja",
Tema: podesavanja["tema"],
NazivFirme: podesavanja["naziv_firme"],
Podnazlov: podesavanja["podnazlov"],
LogoTip: podesavanja["logo_tip"],
LogoPutanja: podesavanja["logo_putanja"],
Korisnik: "Admin",
},
PodaciStranice: ps,
Artikli: artikli,
ArtikliJSON: artikalUJSONSaCenom(artikli),
Klijenti: klijenti,
@@ -250,17 +232,11 @@ func (h *Handler) DetaljiProdaje(w http.ResponseWriter, r *http.Request) {
}
}
ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "prodaja"
ps.NaslovStranice = "Detalji prodaje"
podaci := PodaciDetaljiProdaje{
PodaciStranice: model.PodaciStranice{
Stranica: "prodaja",
NaslovStranice: "Detalji prodaje",
Tema: podesavanja["tema"],
NazivFirme: podesavanja["naziv_firme"],
Podnazlov: podesavanja["podnazlov"],
LogoTip: podesavanja["logo_tip"],
LogoPutanja: podesavanja["logo_putanja"],
Korisnik: "Admin",
},
PodaciStranice: ps,
Nalog: *nalog,
Stavke: stavke,
KlijentNaziv: klijentNaziv,
+32 -80
View File
@@ -59,17 +59,11 @@ func (h *Handler) Servis(w http.ResponseWriter, r *http.Request) {
return
}
ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "servis"
ps.NaslovStranice = "Servis"
podaci := PodaciServisa{
PodaciStranice: model.PodaciStranice{
Stranica: "servis",
NaslovStranice: "Servis",
Tema: podesavanja["tema"],
NazivFirme: podesavanja["naziv_firme"],
Podnazlov: podesavanja["podnazlov"],
LogoTip: podesavanja["logo_tip"],
LogoPutanja: podesavanja["logo_putanja"],
Korisnik: "Admin",
},
PodaciStranice: ps,
Nalozi: nalozi,
Pretraga: pretraga,
FilterStatus: filterStatus,
@@ -101,17 +95,11 @@ func (h *Handler) NoviNalog(w http.ResponseWriter, r *http.Request) {
return
}
ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "servis"
ps.NaslovStranice = "Novi nalog"
h.renderujFormuNaloga(w, PodaciFormeNaloga{
PodaciStranice: model.PodaciStranice{
Stranica: "servis",
NaslovStranice: "Novi nalog",
Tema: podesavanja["tema"],
NazivFirme: podesavanja["naziv_firme"],
Podnazlov: podesavanja["podnazlov"],
LogoTip: podesavanja["logo_tip"],
LogoPutanja: podesavanja["logo_putanja"],
Korisnik: "Admin",
},
PodaciStranice: ps,
Nalog: model.ServisniNalog{BrojNaloga: brojNaloga, Status: model.StatusPrimljeno},
Klijenti: klijenti,
SviStatusi: model.SviStatusi,
@@ -130,17 +118,11 @@ 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(), "")
ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "servis"
ps.NaslovStranice = "Novi nalog"
h.renderujFormuNaloga(w, PodaciFormeNaloga{
PodaciStranice: model.PodaciStranice{
Stranica: "servis",
NaslovStranice: "Novi nalog",
Tema: podesavanja["tema"],
NazivFirme: podesavanja["naziv_firme"],
Podnazlov: podesavanja["podnazlov"],
LogoTip: podesavanja["logo_tip"],
LogoPutanja: podesavanja["logo_putanja"],
Korisnik: "Admin",
},
PodaciStranice: ps,
Nalog: nalog,
Klijenti: klijenti,
SviStatusi: model.SviStatusi,
@@ -155,17 +137,11 @@ 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(), "")
ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "servis"
ps.NaslovStranice = "Novi nalog"
h.renderujFormuNaloga(w, PodaciFormeNaloga{
PodaciStranice: model.PodaciStranice{
Stranica: "servis",
NaslovStranice: "Novi nalog",
Tema: podesavanja["tema"],
NazivFirme: podesavanja["naziv_firme"],
Podnazlov: podesavanja["podnazlov"],
LogoTip: podesavanja["logo_tip"],
LogoPutanja: podesavanja["logo_putanja"],
Korisnik: "Admin",
},
PodaciStranice: ps,
Nalog: nalog,
Klijenti: klijenti,
SviStatusi: model.SviStatusi,
@@ -204,17 +180,11 @@ func (h *Handler) IzmeniNalog(w http.ResponseWriter, r *http.Request) {
return
}
ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "servis"
ps.NaslovStranice = "Izmeni nalog"
h.renderujFormuNaloga(w, PodaciFormeNaloga{
PodaciStranice: model.PodaciStranice{
Stranica: "servis",
NaslovStranice: "Izmeni nalog",
Tema: podesavanja["tema"],
NazivFirme: podesavanja["naziv_firme"],
Podnazlov: podesavanja["podnazlov"],
LogoTip: podesavanja["logo_tip"],
LogoPutanja: podesavanja["logo_putanja"],
Korisnik: "Admin",
},
PodaciStranice: ps,
Nalog: *nalog,
Klijenti: klijenti,
SviStatusi: model.SviStatusi,
@@ -240,17 +210,11 @@ 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
ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "servis"
ps.NaslovStranice = "Izmeni nalog"
h.renderujFormuNaloga(w, PodaciFormeNaloga{
PodaciStranice: model.PodaciStranice{
Stranica: "servis",
NaslovStranice: "Izmeni nalog",
Tema: podesavanja["tema"],
NazivFirme: podesavanja["naziv_firme"],
Podnazlov: podesavanja["podnazlov"],
LogoTip: podesavanja["logo_tip"],
LogoPutanja: podesavanja["logo_putanja"],
Korisnik: "Admin",
},
PodaciStranice: ps,
Nalog: nalog,
Klijenti: klijenti,
SviStatusi: model.SviStatusi,
@@ -265,17 +229,11 @@ 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(), "")
ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "servis"
ps.NaslovStranice = "Izmeni nalog"
h.renderujFormuNaloga(w, PodaciFormeNaloga{
PodaciStranice: model.PodaciStranice{
Stranica: "servis",
NaslovStranice: "Izmeni nalog",
Tema: podesavanja["tema"],
NazivFirme: podesavanja["naziv_firme"],
Podnazlov: podesavanja["podnazlov"],
LogoTip: podesavanja["logo_tip"],
LogoPutanja: podesavanja["logo_putanja"],
Korisnik: "Admin",
},
PodaciStranice: ps,
Nalog: nalog,
Klijenti: klijenti,
SviStatusi: model.SviStatusi,
@@ -336,17 +294,11 @@ func (h *Handler) DetaljiNaloga(w http.ResponseWriter, r *http.Request) {
}
}
ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "servis"
ps.NaslovStranice = "Detalji naloga"
podaci := PodaciDetaljiNaloga{
PodaciStranice: model.PodaciStranice{
Stranica: "servis",
NaslovStranice: "Detalji naloga",
Tema: podesavanja["tema"],
NazivFirme: podesavanja["naziv_firme"],
Podnazlov: podesavanja["podnazlov"],
LogoTip: podesavanja["logo_tip"],
LogoPutanja: podesavanja["logo_putanja"],
Korisnik: "Admin",
},
PodaciStranice: ps,
Nalog: *nalog,
KlijentNaziv: klijentNaziv,
Sacuvano: r.URL.Query().Get("sacuvano") == "1",
+1 -1
View File
@@ -14,7 +14,7 @@ func BezbednostHeaders() func(http.Handler) http.Handler {
h.Set("Content-Security-Policy",
"default-src 'self'; "+
"style-src 'self' 'unsafe-inline' https://cdn.tailwindcss.com; "+
"script-src 'self' 'unsafe-inline' https://cdn.tailwindcss.com https://cdn.jsdelivr.net; "+
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.tailwindcss.com https://cdn.jsdelivr.net; "+
"img-src 'self' data: blob:; "+
"font-src 'self'; "+
"connect-src 'self'")
+8
View File
@@ -49,6 +49,14 @@ type ServisniNalogSaKlijentom struct {
KlijentNaziv string
}
// KlijentIDVrednost vraća vrednost KlijentID pointera, ili 0 ako je nil
func (n ServisniNalog) KlijentIDVrednost() int64 {
if n.KlijentID == nil {
return 0
}
return *n.KlijentID
}
// CenaOdStr vraća formatiranu procenu od, ili prazan string ako nije uneta
func (n ServisniNalog) CenaOdStr() string {
if n.CenaOd == nil {
+26 -1
View File
@@ -264,13 +264,31 @@ input[type="text"],
input[type="email"],
input[type="password"],
input[type="number"],
textarea,
input[type="date"],
select {
height: 38px;
padding: 8px 12px;
box-sizing: border-box;
line-height: 1.5;
background: var(--kartica) !important;
color: var(--tekst-glavni) !important;
border: 0.5px solid var(--ivica) !important;
border-radius: 8px;
font-size: 14px;
outline: none;
transition: border-color 0.2s;
}
textarea {
height: auto;
min-height: 80px;
padding: 8px 12px;
box-sizing: border-box;
line-height: 1.5;
background: var(--kartica) !important;
color: var(--tekst-glavni) !important;
border: 0.5px solid var(--ivica) !important;
border-radius: 8px;
font-size: 14px;
outline: none;
transition: border-color 0.2s;
@@ -280,9 +298,16 @@ input[type="text"]:focus,
input[type="email"]:focus,
input[type="password"]:focus,
input[type="number"]:focus,
input[type="date"]:focus,
textarea:focus,
select:focus {
border-color: var(--sb-akcent) !important;
outline: none;
}
select {
width: 100%;
min-width: 0;
}
/* poruka o uspehu — konzistentna za sve teme */
+1
View File
@@ -1,4 +1,5 @@
:root {
color-scheme: light;
--pozadina: #f6f8fa;
--kartica: #ffffff;
--kartica-2: #f0f2f5;
+1
View File
@@ -1,4 +1,5 @@
:root {
color-scheme: dark;
--pozadina: #1f2228;
--kartica: #22262d;
--kartica-2: #2c313a;
+1 -1
View File
@@ -92,7 +92,7 @@
</a>
{{end}}
<a href="/admin/profil" class="nav-stavka">
<a href="/admin/profil" class="nav-stavka {{if eq .Stranica "profil"}}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="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
<span>Moj profil</span>
<span class="nav-tooltip">Moj profil</span>
+1 -1
View File
@@ -109,7 +109,7 @@
<option value="">— bez klijenta —</option>
{{range .Klijenti}}
<option value="{{.ID}}"
{{if $.Nalog.KlijentID}}{{if eq .ID $.Nalog.KlijentID}}selected{{end}}{{end}}>
{{if eq .ID $.Nalog.KlijentIDVrednost}}selected{{end}}>
{{if .NazivFirme}}{{.NazivFirme}}{{else}}{{.Ime}} {{.Prezime}}{{end}}
</option>
{{end}}