feat: dodata podrška za servise
Dodana osnovna funkcionalnost za upravljanje servisima: - Novi model (`internal/model/service.go`) - SQLite repository (`internal/db/sqlite/service.go`) - Handler (`internal/handler/service.go`) - Tri nove stranice (lista, detalji, forma) Izmenjeni: - main.go - repository.go - utils.go - handler.go
This commit is contained in:
@@ -83,6 +83,13 @@ func main() {
|
||||
r.Get("/klijenti/izmeni/{id}", h.IzmeniKlijenta)
|
||||
r.Post("/klijenti/izmeni/{id}", h.SacuvajIzmenuKlijenta)
|
||||
r.Post("/klijenti/obrisi/{id}", h.ObrisiKlijenta)
|
||||
r.Get("/servis", h.Servis)
|
||||
r.Get("/servis/novi", h.NoviNalog)
|
||||
r.Post("/servis/novi", h.SacuvajNalog)
|
||||
r.Get("/servis/izmeni/{id}", h.IzmeniNalog)
|
||||
r.Post("/servis/izmeni/{id}", h.SacuvajIzmenaNaloga)
|
||||
r.Post("/servis/obrisi/{id}", h.ObrisiNalog)
|
||||
r.Get("/servis/{id}", h.DetaljiNaloga)
|
||||
|
||||
log.Printf("NTech pokrenut na portu %s", port)
|
||||
err = http.ListenAndServe(":"+port, r)
|
||||
|
||||
@@ -54,3 +54,13 @@ type KlijentRepository interface {
|
||||
Izmeni(ctx context.Context, k *model.Klijent) error
|
||||
Obrisi(ctx context.Context, id int64) error
|
||||
}
|
||||
|
||||
// ServisRepository definiše operacije nad servisnim nalozima
|
||||
type ServisRepository interface {
|
||||
Lista(ctx context.Context, pretraga, status string) ([]model.ServisniNalogSaKlijentom, error)
|
||||
DohvatiID(ctx context.Context, id int64) (*model.ServisniNalog, error)
|
||||
Kreiraj(ctx context.Context, n *model.ServisniNalog) (int64, error)
|
||||
Izmeni(ctx context.Context, n *model.ServisniNalog) error
|
||||
Obrisi(ctx context.Context, id int64) error
|
||||
SledeciBroj(ctx context.Context) (string, error)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"ntech/internal/model"
|
||||
)
|
||||
|
||||
// ServisRepo je SQLite implementacija ServisRepository interfejsa
|
||||
type ServisRepo struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// NoviServisRepo kreira novi ServisRepo
|
||||
func NoviServisRepo(db *sql.DB) *ServisRepo {
|
||||
return &ServisRepo{db: db}
|
||||
}
|
||||
|
||||
// SledeciBroj generiše sledeći broj naloga u formatu SN-GGGG-NNNN
|
||||
func (r *ServisRepo) SledeciBroj(ctx context.Context) (string, error) {
|
||||
godina := time.Now().Year()
|
||||
uzorak := fmt.Sprintf("SN-%d-%%", godina)
|
||||
|
||||
var sledeci int
|
||||
err := r.db.QueryRowContext(ctx, `
|
||||
SELECT COALESCE(MAX(CAST(SUBSTR(broj_naloga, 9) AS INTEGER)), 0) + 1
|
||||
FROM servisni_nalozi
|
||||
WHERE broj_naloga LIKE ?`, uzorak).Scan(&sledeci)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("ntech: ServisRepo.SledeciBroj: %w", err)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("SN-%d-%04d", godina, sledeci), nil
|
||||
}
|
||||
|
||||
// Lista vraća listu servisnih naloga sa imenom klijenta, opcionim filterima
|
||||
func (r *ServisRepo) Lista(ctx context.Context, pretraga, status string) ([]model.ServisniNalogSaKlijentom, error) {
|
||||
upit := `
|
||||
SELECT
|
||||
sn.id, sn.klijent_id, sn.broj_naloga, sn.uredjaj, sn.serijski_broj,
|
||||
sn.opis_kvara, sn.status, sn.cena_od, sn.cena_do, sn.cena_konacna,
|
||||
sn.avans, sn.napomena, sn.datum_prijema, sn.datum_zavrsetka,
|
||||
COALESCE(NULLIF(k.naziv_firme, ''), TRIM(COALESCE(k.ime, '') || ' ' || COALESCE(k.prezime, '')), '') AS klijent_naziv
|
||||
FROM servisni_nalozi sn
|
||||
LEFT JOIN klijenti k ON k.id = sn.klijent_id
|
||||
WHERE 1=1`
|
||||
|
||||
args := []any{}
|
||||
|
||||
if pretraga != "" {
|
||||
upit += " AND (sn.broj_naloga LIKE ? OR sn.uredjaj LIKE ?)"
|
||||
p := "%" + pretraga + "%"
|
||||
args = append(args, p, p)
|
||||
}
|
||||
|
||||
if status != "" {
|
||||
upit += " AND sn.status = ?"
|
||||
args = append(args, status)
|
||||
}
|
||||
|
||||
upit += " ORDER BY sn.datum_prijema DESC"
|
||||
|
||||
redovi, err := r.db.QueryContext(ctx, upit, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ntech: ServisRepo.Lista: %w", err)
|
||||
}
|
||||
defer redovi.Close()
|
||||
|
||||
var rezultat []model.ServisniNalogSaKlijentom
|
||||
for redovi.Next() {
|
||||
var n model.ServisniNalogSaKlijentom
|
||||
err := scanNalog(redovi.Scan, &n.ServisniNalog, &n.KlijentNaziv)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ntech: ServisRepo.Lista: scan: %w", err)
|
||||
}
|
||||
rezultat = append(rezultat, n)
|
||||
}
|
||||
|
||||
return rezultat, nil
|
||||
}
|
||||
|
||||
// DohvatiID vraća jedan servisni nalog po ID-u
|
||||
func (r *ServisRepo) DohvatiID(ctx context.Context, id int64) (*model.ServisniNalog, error) {
|
||||
red := r.db.QueryRowContext(ctx, `
|
||||
SELECT
|
||||
id, klijent_id, broj_naloga, uredjaj, serijski_broj,
|
||||
opis_kvara, status, cena_od, cena_do, cena_konacna,
|
||||
avans, napomena, datum_prijema, datum_zavrsetka
|
||||
FROM servisni_nalozi WHERE id = ?`, id)
|
||||
|
||||
var n model.ServisniNalog
|
||||
err := scanNalog(red.Scan, &n, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ntech: ServisRepo.DohvatiID: %w", err)
|
||||
}
|
||||
|
||||
return &n, nil
|
||||
}
|
||||
|
||||
// Kreiraj upisuje novi servisni nalog u bazu
|
||||
func (r *ServisRepo) Kreiraj(ctx context.Context, n *model.ServisniNalog) (int64, error) {
|
||||
rezultat, err := r.db.ExecContext(ctx, `
|
||||
INSERT INTO servisni_nalozi
|
||||
(klijent_id, broj_naloga, uredjaj, serijski_broj, opis_kvara,
|
||||
status, cena_od, cena_do, cena_konacna, avans, napomena, datum_zavrsetka)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
nullInt64(n.KlijentID), n.BrojNaloga, n.Uredjaj, nullString(n.SerijskiBroj),
|
||||
n.OpisKvara, n.Status, nullFloat64(n.CenaOd), nullFloat64(n.CenaDo),
|
||||
nullFloat64(n.CenaKonacna), nullFloat64(n.Avans), nullString(n.Napomena),
|
||||
nullTime(n.DatumZavrsetka),
|
||||
)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("ntech: ServisRepo.Kreiraj: %w", err)
|
||||
}
|
||||
|
||||
id, err := rezultat.LastInsertId()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("ntech: ServisRepo.Kreiraj: last insert id: %w", err)
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// Izmeni ažurira postojeći servisni nalog — broj_naloga i datum_prijema se ne menjaju
|
||||
func (r *ServisRepo) Izmeni(ctx context.Context, n *model.ServisniNalog) error {
|
||||
_, err := r.db.ExecContext(ctx, `
|
||||
UPDATE servisni_nalozi SET
|
||||
klijent_id = ?, uredjaj = ?, serijski_broj = ?, opis_kvara = ?,
|
||||
status = ?, cena_od = ?, cena_do = ?, cena_konacna = ?,
|
||||
avans = ?, napomena = ?, datum_zavrsetka = ?
|
||||
WHERE id = ?`,
|
||||
nullInt64(n.KlijentID), n.Uredjaj, nullString(n.SerijskiBroj), n.OpisKvara,
|
||||
n.Status, nullFloat64(n.CenaOd), nullFloat64(n.CenaDo), nullFloat64(n.CenaKonacna),
|
||||
nullFloat64(n.Avans), nullString(n.Napomena), nullTime(n.DatumZavrsetka),
|
||||
n.ID,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntech: ServisRepo.Izmeni: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Obrisi briše servisni nalog po ID-u
|
||||
func (r *ServisRepo) Obrisi(ctx context.Context, id int64) error {
|
||||
_, err := r.db.ExecContext(ctx, "DELETE FROM servisni_nalozi WHERE id = ?", id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntech: ServisRepo.Obrisi: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// scanNalog čita redove iz upita u ServisniNalog struct —
|
||||
// klijentNaziv je opcioni pokazivač, nil kada se čita bez JOIN-a
|
||||
func scanNalog(scan func(...any) error, n *model.ServisniNalog, klijentNaziv *string) error {
|
||||
var klijentID sql.NullInt64
|
||||
var serijskiBroj, napomena sql.NullString
|
||||
var cenaOd, cenaDo, cenaKonacna, avans sql.NullFloat64
|
||||
var datumZavrsetka sql.NullTime
|
||||
|
||||
args := []any{
|
||||
&n.ID, &klijentID, &n.BrojNaloga, &n.Uredjaj, &serijskiBroj,
|
||||
&n.OpisKvara, &n.Status, &cenaOd, &cenaDo, &cenaKonacna,
|
||||
&avans, &napomena, &n.DatumPrijema, &datumZavrsetka,
|
||||
}
|
||||
|
||||
if klijentNaziv != nil {
|
||||
args = append(args, klijentNaziv)
|
||||
}
|
||||
|
||||
if err := scan(args...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if klijentID.Valid {
|
||||
n.KlijentID = &klijentID.Int64
|
||||
}
|
||||
n.SerijskiBroj = serijskiBroj.String
|
||||
n.Napomena = napomena.String
|
||||
if cenaOd.Valid {
|
||||
n.CenaOd = &cenaOd.Float64
|
||||
}
|
||||
if cenaDo.Valid {
|
||||
n.CenaDo = &cenaDo.Float64
|
||||
}
|
||||
if cenaKonacna.Valid {
|
||||
n.CenaKonacna = &cenaKonacna.Float64
|
||||
}
|
||||
if avans.Valid {
|
||||
n.Avans = &avans.Float64
|
||||
}
|
||||
if datumZavrsetka.Valid {
|
||||
n.DatumZavrsetka = &datumZavrsetka.Time
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
package sqlite
|
||||
|
||||
import "database/sql"
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
)
|
||||
|
||||
// nullString pretvara prazan Go string u sql.NullString sa NULL vrednošću —
|
||||
// koristi se pri unosu i izmeni kada polje u bazi sme biti NULL
|
||||
@@ -16,3 +19,21 @@ func nullInt64(v *int64) sql.NullInt64 {
|
||||
}
|
||||
return sql.NullInt64{Int64: *v, Valid: true}
|
||||
}
|
||||
|
||||
// nullFloat64 pretvara *float64 pokazivač u sql.NullFloat64 —
|
||||
// koristi se za opciona numerička polja kao što su cene
|
||||
func nullFloat64(v *float64) sql.NullFloat64 {
|
||||
if v == nil {
|
||||
return sql.NullFloat64{}
|
||||
}
|
||||
return sql.NullFloat64{Float64: *v, Valid: true}
|
||||
}
|
||||
|
||||
// nullTime pretvara *time.Time pokazivač u sql.NullTime —
|
||||
// koristi se za opciona datumska polja kao što je datum završetka
|
||||
func nullTime(v *time.Time) sql.NullTime {
|
||||
if v == nil {
|
||||
return sql.NullTime{}
|
||||
}
|
||||
return sql.NullTime{Time: *v, Valid: true}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ type Handler struct {
|
||||
DobavljaciRepo db.DobavljacRepository
|
||||
NabavkeRepo db.NabavkaRepository
|
||||
KlijentiRepo db.KlijentRepository
|
||||
ServisRepo db.ServisRepository
|
||||
}
|
||||
|
||||
// Novi kreira novi Handler sa datom bazom
|
||||
@@ -26,5 +27,6 @@ func Novi(baza *sql.DB) *Handler {
|
||||
DobavljaciRepo: sqlite.NoviDobavljacRepo(baza),
|
||||
NabavkeRepo: sqlite.NoviNabavkaRepo(baza),
|
||||
KlijentiRepo: sqlite.NoviKlijentRepo(baza),
|
||||
ServisRepo: sqlite.NoviServisRepo(baza),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,468 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"ntech/internal/db/sqlite"
|
||||
"ntech/internal/model"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
// PodaciServisa su podaci za stranicu sa listom servisnih naloga
|
||||
type PodaciServisa struct {
|
||||
model.PodaciStranice
|
||||
Nalozi []model.ServisniNalogSaKlijentom
|
||||
Pretraga string
|
||||
FilterStatus string
|
||||
SviStatusi []string
|
||||
Sacuvano bool
|
||||
Obrisan bool
|
||||
}
|
||||
|
||||
// PodaciFormeNaloga su podaci za formu novog/izmenjenog servisnog naloga
|
||||
type PodaciFormeNaloga struct {
|
||||
model.PodaciStranice
|
||||
Nalog model.ServisniNalog
|
||||
Klijenti []model.Klijent
|
||||
SviStatusi []string
|
||||
Greska string
|
||||
Izmena bool
|
||||
}
|
||||
|
||||
// PodaciDetaljiNaloga su podaci za pregled jednog servisnog naloga
|
||||
type PodaciDetaljiNaloga struct {
|
||||
model.PodaciStranice
|
||||
Nalog model.ServisniNalog
|
||||
KlijentNaziv string
|
||||
Sacuvano bool
|
||||
}
|
||||
|
||||
// Servis renderuje listu servisnih naloga sa opcionom pretragom i filterom statusa
|
||||
func (h *Handler) Servis(w http.ResponseWriter, r *http.Request) {
|
||||
podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
pretraga := r.URL.Query().Get("pretraga")
|
||||
filterStatus := r.URL.Query().Get("status")
|
||||
|
||||
nalozi, err := h.ServisRepo.Lista(r.Context(), pretraga, filterStatus)
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri učitavanju naloga", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
podaci := PodaciServisa{
|
||||
PodaciStranice: model.PodaciStranice{
|
||||
Stranica: "servis",
|
||||
NaslovStranice: "Servis",
|
||||
Tema: podesavanja["tema"],
|
||||
NazivFirme: podesavanja["naziv_firme"],
|
||||
Podnazlov: podesavanja["podnazlov"],
|
||||
LogoTip: podesavanja["logo_tip"],
|
||||
LogoPutanja: podesavanja["logo_putanja"],
|
||||
Korisnik: "Admin",
|
||||
},
|
||||
Nalozi: nalozi,
|
||||
Pretraga: pretraga,
|
||||
FilterStatus: filterStatus,
|
||||
SviStatusi: model.SviStatusi,
|
||||
Sacuvano: r.URL.Query().Get("sacuvano") == "1",
|
||||
Obrisan: r.URL.Query().Get("obrisan") == "1",
|
||||
}
|
||||
|
||||
tmpl, err := template.ParseFiles(
|
||||
"web/templates/teme/podrazumevana/base.html",
|
||||
"web/templates/komponente/sidebar.html",
|
||||
"web/templates/komponente/topbar.html",
|
||||
"web/templates/stranice/servis.html",
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("greška pri učitavanju šablona: %v", err)
|
||||
http.Error(w, "Greška pri učitavanju stranice", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err := tmpl.ExecuteTemplate(w, "base", podaci); err != nil {
|
||||
log.Printf("greška pri renderovanju: %v", err)
|
||||
http.Error(w, "Greška pri prikazu stranice", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// NoviNalog generiše broj naloga i prikazuje praznu formu za unos
|
||||
func (h *Handler) NoviNalog(w http.ResponseWriter, r *http.Request) {
|
||||
podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
brojNaloga, err := h.ServisRepo.SledeciBroj(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri generisanju broja naloga", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
klijenti, err := h.KlijentiRepo.Lista(r.Context(), "")
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri učitavanju klijenata", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
renderujFormuNaloga(w, PodaciFormeNaloga{
|
||||
PodaciStranice: model.PodaciStranice{
|
||||
Stranica: "servis",
|
||||
NaslovStranice: "Novi nalog",
|
||||
Tema: podesavanja["tema"],
|
||||
NazivFirme: podesavanja["naziv_firme"],
|
||||
Podnazlov: podesavanja["podnazlov"],
|
||||
LogoTip: podesavanja["logo_tip"],
|
||||
LogoPutanja: podesavanja["logo_putanja"],
|
||||
Korisnik: "Admin",
|
||||
},
|
||||
Nalog: model.ServisniNalog{BrojNaloga: brojNaloga, Status: model.StatusPrimljeno},
|
||||
Klijenti: klijenti,
|
||||
SviStatusi: model.SviStatusi,
|
||||
Izmena: false,
|
||||
})
|
||||
}
|
||||
|
||||
// SacuvajNalog prima POST formu i upisuje novi servisni nalog u bazu
|
||||
func (h *Handler) SacuvajNalog(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
nalog, greska := parseFormuNaloga(r)
|
||||
if greska != "" {
|
||||
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||
klijenti, _ := h.KlijentiRepo.Lista(r.Context(), "")
|
||||
renderujFormuNaloga(w, PodaciFormeNaloga{
|
||||
PodaciStranice: model.PodaciStranice{
|
||||
Stranica: "servis",
|
||||
NaslovStranice: "Novi nalog",
|
||||
Tema: podesavanja["tema"],
|
||||
NazivFirme: podesavanja["naziv_firme"],
|
||||
Podnazlov: podesavanja["podnazlov"],
|
||||
LogoTip: podesavanja["logo_tip"],
|
||||
LogoPutanja: podesavanja["logo_putanja"],
|
||||
Korisnik: "Admin",
|
||||
},
|
||||
Nalog: nalog,
|
||||
Klijenti: klijenti,
|
||||
SviStatusi: model.SviStatusi,
|
||||
Greska: greska,
|
||||
Izmena: false,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
id, err := h.ServisRepo.Kreiraj(r.Context(), &nalog)
|
||||
if err != nil {
|
||||
log.Printf("greška pri čuvanju naloga: %v", err)
|
||||
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||
klijenti, _ := h.KlijentiRepo.Lista(r.Context(), "")
|
||||
renderujFormuNaloga(w, PodaciFormeNaloga{
|
||||
PodaciStranice: model.PodaciStranice{
|
||||
Stranica: "servis",
|
||||
NaslovStranice: "Novi nalog",
|
||||
Tema: podesavanja["tema"],
|
||||
NazivFirme: podesavanja["naziv_firme"],
|
||||
Podnazlov: podesavanja["podnazlov"],
|
||||
LogoTip: podesavanja["logo_tip"],
|
||||
LogoPutanja: podesavanja["logo_putanja"],
|
||||
Korisnik: "Admin",
|
||||
},
|
||||
Nalog: nalog,
|
||||
Klijenti: klijenti,
|
||||
SviStatusi: model.SviStatusi,
|
||||
Greska: "Došlo je do greške pri čuvanju. Pokušajte ponovo.",
|
||||
Izmena: false,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(id, 10)+"?sacuvano=1", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// IzmeniNalog učitava servisni nalog po ID-u i prikazuje popunjenu formu
|
||||
func (h *Handler) IzmeniNalog(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := parseID(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
http.Error(w, "Neispravan ID naloga", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
nalog, err := h.ServisRepo.DohvatiID(r.Context(), id)
|
||||
if err != nil {
|
||||
http.Error(w, "Nalog nije pronađen", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
klijenti, err := h.KlijentiRepo.Lista(r.Context(), "")
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri učitavanju klijenata", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
renderujFormuNaloga(w, PodaciFormeNaloga{
|
||||
PodaciStranice: model.PodaciStranice{
|
||||
Stranica: "servis",
|
||||
NaslovStranice: "Izmeni nalog",
|
||||
Tema: podesavanja["tema"],
|
||||
NazivFirme: podesavanja["naziv_firme"],
|
||||
Podnazlov: podesavanja["podnazlov"],
|
||||
LogoTip: podesavanja["logo_tip"],
|
||||
LogoPutanja: podesavanja["logo_putanja"],
|
||||
Korisnik: "Admin",
|
||||
},
|
||||
Nalog: *nalog,
|
||||
Klijenti: klijenti,
|
||||
SviStatusi: model.SviStatusi,
|
||||
Izmena: true,
|
||||
})
|
||||
}
|
||||
|
||||
// SacuvajIzmenaNaloga prima POST formu i ažurira postojeći servisni nalog
|
||||
func (h *Handler) SacuvajIzmenaNaloga(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := parseID(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
http.Error(w, "Neispravan ID naloga", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
nalog, greska := parseFormuNaloga(r)
|
||||
if greska != "" {
|
||||
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||
klijenti, _ := h.KlijentiRepo.Lista(r.Context(), "")
|
||||
nalog.ID = id
|
||||
renderujFormuNaloga(w, PodaciFormeNaloga{
|
||||
PodaciStranice: model.PodaciStranice{
|
||||
Stranica: "servis",
|
||||
NaslovStranice: "Izmeni nalog",
|
||||
Tema: podesavanja["tema"],
|
||||
NazivFirme: podesavanja["naziv_firme"],
|
||||
Podnazlov: podesavanja["podnazlov"],
|
||||
LogoTip: podesavanja["logo_tip"],
|
||||
LogoPutanja: podesavanja["logo_putanja"],
|
||||
Korisnik: "Admin",
|
||||
},
|
||||
Nalog: nalog,
|
||||
Klijenti: klijenti,
|
||||
SviStatusi: model.SviStatusi,
|
||||
Greska: greska,
|
||||
Izmena: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
nalog.ID = id
|
||||
if err := h.ServisRepo.Izmeni(r.Context(), &nalog); err != nil {
|
||||
log.Printf("greška pri čuvanju izmene naloga: %v", err)
|
||||
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||
klijenti, _ := h.KlijentiRepo.Lista(r.Context(), "")
|
||||
renderujFormuNaloga(w, PodaciFormeNaloga{
|
||||
PodaciStranice: model.PodaciStranice{
|
||||
Stranica: "servis",
|
||||
NaslovStranice: "Izmeni nalog",
|
||||
Tema: podesavanja["tema"],
|
||||
NazivFirme: podesavanja["naziv_firme"],
|
||||
Podnazlov: podesavanja["podnazlov"],
|
||||
LogoTip: podesavanja["logo_tip"],
|
||||
LogoPutanja: podesavanja["logo_putanja"],
|
||||
Korisnik: "Admin",
|
||||
},
|
||||
Nalog: nalog,
|
||||
Klijenti: klijenti,
|
||||
SviStatusi: model.SviStatusi,
|
||||
Greska: "Došlo je do greške pri čuvanju. Pokušajte ponovo.",
|
||||
Izmena: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(id, 10)+"?sacuvano=1", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// ObrisiNalog prima POST zahtev i briše servisni nalog po ID-u
|
||||
func (h *Handler) ObrisiNalog(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := parseID(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
http.Error(w, "Neispravan ID naloga", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.ServisRepo.Obrisi(r.Context(), id); err != nil {
|
||||
http.Error(w, "Greška pri brisanju naloga", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/servis?obrisan=1", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// DetaljiNaloga prikazuje sve podatke jednog servisnog naloga
|
||||
func (h *Handler) DetaljiNaloga(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := parseID(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
http.Error(w, "Neispravan ID naloga", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
nalog, err := h.ServisRepo.DohvatiID(r.Context(), id)
|
||||
if err != nil {
|
||||
http.Error(w, "Nalog nije pronađen", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
klijentNaziv := ""
|
||||
if nalog.KlijentID != nil {
|
||||
klijent, err := h.KlijentiRepo.DohvatiID(r.Context(), *nalog.KlijentID)
|
||||
if err == nil {
|
||||
if klijent.NazivFirme != "" {
|
||||
klijentNaziv = klijent.NazivFirme
|
||||
} else {
|
||||
klijentNaziv = strings.TrimSpace(klijent.Ime + " " + klijent.Prezime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
podaci := PodaciDetaljiNaloga{
|
||||
PodaciStranice: model.PodaciStranice{
|
||||
Stranica: "servis",
|
||||
NaslovStranice: "Detalji naloga",
|
||||
Tema: podesavanja["tema"],
|
||||
NazivFirme: podesavanja["naziv_firme"],
|
||||
Podnazlov: podesavanja["podnazlov"],
|
||||
LogoTip: podesavanja["logo_tip"],
|
||||
LogoPutanja: podesavanja["logo_putanja"],
|
||||
Korisnik: "Admin",
|
||||
},
|
||||
Nalog: *nalog,
|
||||
KlijentNaziv: klijentNaziv,
|
||||
Sacuvano: r.URL.Query().Get("sacuvano") == "1",
|
||||
}
|
||||
|
||||
tmpl, err := template.ParseFiles(
|
||||
"web/templates/teme/podrazumevana/base.html",
|
||||
"web/templates/komponente/sidebar.html",
|
||||
"web/templates/komponente/topbar.html",
|
||||
"web/templates/stranice/servis_detalji.html",
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("greška pri učitavanju šablona: %v", err)
|
||||
http.Error(w, "Greška pri učitavanju stranice", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err := tmpl.ExecuteTemplate(w, "base", podaci); err != nil {
|
||||
log.Printf("greška pri renderovanju: %v", err)
|
||||
http.Error(w, "Greška pri prikazu stranice", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// parseFormuNaloga čita i validira polja iz HTTP forme
|
||||
func parseFormuNaloga(r *http.Request) (model.ServisniNalog, string) {
|
||||
uredjaj := strings.TrimSpace(r.FormValue("uredjaj"))
|
||||
if uredjaj == "" {
|
||||
return model.ServisniNalog{}, "Naziv uređaja je obavezan."
|
||||
}
|
||||
|
||||
opisKvara := strings.TrimSpace(r.FormValue("opis_kvara"))
|
||||
if opisKvara == "" {
|
||||
return model.ServisniNalog{}, "Opis kvara je obavezan."
|
||||
}
|
||||
|
||||
nalog := model.ServisniNalog{
|
||||
BrojNaloga: strings.TrimSpace(r.FormValue("broj_naloga")),
|
||||
Uredjaj: uredjaj,
|
||||
SerijskiBroj: strings.TrimSpace(r.FormValue("serijski_broj")),
|
||||
OpisKvara: opisKvara,
|
||||
Status: r.FormValue("status"),
|
||||
Napomena: strings.TrimSpace(r.FormValue("napomena")),
|
||||
}
|
||||
|
||||
if nalog.Status == "" {
|
||||
nalog.Status = model.StatusPrimljeno
|
||||
}
|
||||
|
||||
// opcioni klijent
|
||||
if kidStr := r.FormValue("klijent_id"); kidStr != "" {
|
||||
if kid, err := strconv.ParseInt(kidStr, 10, 64); err == nil {
|
||||
nalog.KlijentID = &kid
|
||||
}
|
||||
}
|
||||
|
||||
// opcione cene — prazno polje ostaje nil
|
||||
nalog.CenaOd = parseOpcionuCenu(r.FormValue("cena_od"))
|
||||
nalog.CenaDo = parseOpcionuCenu(r.FormValue("cena_do"))
|
||||
nalog.CenaKonacna = parseOpcionuCenu(r.FormValue("cena_konacna"))
|
||||
nalog.Avans = parseOpcionuCenu(r.FormValue("avans"))
|
||||
|
||||
// opcioni datum završetka
|
||||
if dz := strings.TrimSpace(r.FormValue("datum_zavrsetka")); dz != "" {
|
||||
if t, err := time.Parse("2006-01-02", dz); err == nil {
|
||||
nalog.DatumZavrsetka = &t
|
||||
}
|
||||
}
|
||||
|
||||
return nalog, ""
|
||||
}
|
||||
|
||||
// parseOpcionuCenu pretvara string u *float64 — prazno polje ili neispravna vrednost vraća nil
|
||||
func parseOpcionuCenu(s string) *float64 {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
v, err := strconv.ParseFloat(s, 64)
|
||||
if err != nil || v < 0 {
|
||||
return nil
|
||||
}
|
||||
return &v
|
||||
}
|
||||
|
||||
// renderujFormuNaloga renderuje HTML šablon forme za unos ili izmenu servisnog naloga
|
||||
func renderujFormuNaloga(w http.ResponseWriter, podaci PodaciFormeNaloga) {
|
||||
tmpl, err := template.ParseFiles(
|
||||
"web/templates/teme/podrazumevana/base.html",
|
||||
"web/templates/komponente/sidebar.html",
|
||||
"web/templates/komponente/topbar.html",
|
||||
"web/templates/stranice/servis_forma.html",
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("greška pri učitavanju šablona: %v", err)
|
||||
http.Error(w, "Greška pri učitavanju stranice", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err := tmpl.ExecuteTemplate(w, "base", podaci); err != nil {
|
||||
log.Printf("greška pri renderovanju: %v", err)
|
||||
http.Error(w, "Greška pri prikazu stranice", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// Statusi servisnog naloga
|
||||
const (
|
||||
StatusPrimljeno = "Primljeno"
|
||||
StatusDijagnostika = "U dijagnostici"
|
||||
StatusCekaDelove = "Čeka delove"
|
||||
StatusUPopravci = "U popravci"
|
||||
StatusZavrseno = "Završeno"
|
||||
StatusPreuzeto = "Preuzeto"
|
||||
)
|
||||
|
||||
// SviStatusi je uređena lista statusa za prikaz u dropdownu
|
||||
var SviStatusi = []string{
|
||||
StatusPrimljeno,
|
||||
StatusDijagnostika,
|
||||
StatusCekaDelove,
|
||||
StatusUPopravci,
|
||||
StatusZavrseno,
|
||||
StatusPreuzeto,
|
||||
}
|
||||
|
||||
// ServisniNalog predstavlja jedan servisni nalog
|
||||
type ServisniNalog struct {
|
||||
ID int64
|
||||
KlijentID *int64
|
||||
BrojNaloga string
|
||||
Uredjaj string
|
||||
SerijskiBroj string
|
||||
OpisKvara string
|
||||
Status string
|
||||
CenaOd *float64
|
||||
CenaDo *float64
|
||||
CenaKonacna *float64
|
||||
Avans *float64
|
||||
Napomena string
|
||||
DatumPrijema time.Time
|
||||
DatumZavrsetka *time.Time
|
||||
}
|
||||
|
||||
// ServisniNalogSaKlijentom proširuje ServisniNalog sa nazivom klijenta za prikaz u listi
|
||||
type ServisniNalogSaKlijentom struct {
|
||||
ServisniNalog
|
||||
KlijentNaziv string
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "naslov"}}Servis — 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; }
|
||||
|
||||
.servis-tabela tbody tr {
|
||||
animation: fadeInUp 0.25s ease forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.servis-tabela tbody tr:nth-child(1) { animation-delay: 0.04s; }
|
||||
.servis-tabela tbody tr:nth-child(2) { animation-delay: 0.08s; }
|
||||
.servis-tabela tbody tr:nth-child(3) { animation-delay: 0.12s; }
|
||||
.servis-tabela tbody tr:nth-child(4) { animation-delay: 0.16s; }
|
||||
.servis-tabela tbody tr:nth-child(5) { animation-delay: 0.20s; }
|
||||
.servis-tabela tbody tr:nth-child(6) { animation-delay: 0.24s; }
|
||||
.servis-tabela tbody tr:nth-child(7) { animation-delay: 0.28s; }
|
||||
.servis-tabela tbody tr:nth-child(8) { animation-delay: 0.32s; }
|
||||
.servis-tabela tbody tr:nth-child(9) { animation-delay: 0.36s; }
|
||||
.servis-tabela tbody tr:nth-child(10) { animation-delay: 0.40s; }
|
||||
|
||||
.servis-kartice {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.servis-kartica {
|
||||
animation: fadeInUp 0.25s ease forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.servis-kartica:nth-child(1) { animation-delay: 0.04s; }
|
||||
.servis-kartica:nth-child(2) { animation-delay: 0.10s; }
|
||||
.servis-kartica:nth-child(3) { animation-delay: 0.16s; }
|
||||
.servis-kartica:nth-child(4) { animation-delay: 0.22s; }
|
||||
.servis-kartica:nth-child(5) { animation-delay: 0.28s; }
|
||||
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 3px 10px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status-primljeno { background: rgba(148,163,184,0.15); color: #94a3b8; }
|
||||
.status-dijagnostika { background: rgba(59,130,246,0.15); color: #3b82f6; }
|
||||
.status-ceka { background: rgba(249,115,22,0.15); color: #f97316; }
|
||||
.status-popravka { background: rgba(234,179,8,0.15); color: #ca8a04; }
|
||||
.status-zavrseno { background: rgba(34,197,94,0.15); color: #16a34a; }
|
||||
.status-preuzeto { background: rgba(21,128,61,0.15); color: #15803d; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.servis-tabela { display: none; }
|
||||
.servis-kartice { display: flex; }
|
||||
}
|
||||
</style>
|
||||
{{end}}
|
||||
|
||||
{{define "status-badge"}}
|
||||
{{if eq . "Primljeno"}}<span class="status-badge status-primljeno">{{.}}</span>
|
||||
{{else if eq . "U dijagnostici"}}<span class="status-badge status-dijagnostika">{{.}}</span>
|
||||
{{else if eq . "Čeka delove"}}<span class="status-badge status-ceka">{{.}}</span>
|
||||
{{else if eq . "U popravci"}}<span class="status-badge status-popravka">{{.}}</span>
|
||||
{{else if eq . "Završeno"}}<span class="status-badge status-zavrseno">{{.}}</span>
|
||||
{{else if eq . "Preuzeto"}}<span class="status-badge status-preuzeto">{{.}}</span>
|
||||
{{else}}<span class="status-badge status-primljeno">{{.}}</span>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{define "sadrzaj"}}
|
||||
<div style="display:flex;flex-direction:column;gap:16px;">
|
||||
|
||||
{{if .Sacuvano}}
|
||||
<div class="poruka-uspeh poruka-animacija">Nalog je uspešno sačuvan.</div>
|
||||
{{end}}
|
||||
{{if .Obrisan}}
|
||||
<div class="poruka-uspeh poruka-animacija">Nalog je uspešno obrisan.</div>
|
||||
{{end}}
|
||||
|
||||
<!-- gornja traka: dugme + pretraga + filter statusa -->
|
||||
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center;">
|
||||
<a href="/servis/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 nalog
|
||||
</a>
|
||||
<form method="GET" action="/servis" style="display:flex;gap:8px;flex:1;flex-wrap:wrap;min-width:200px;">
|
||||
<input type="text" name="pretraga" value="{{.Pretraga}}"
|
||||
placeholder="Pretraži po broju naloga ili uređaju..."
|
||||
style="flex:1;min-width:160px;">
|
||||
<select name="status" style="min-width:160px;">
|
||||
<option value="">— svi statusi —</option>
|
||||
{{range .SviStatusi}}
|
||||
<option value="{{.}}" {{if eq . $.FilterStatus}}selected{{end}}>{{.}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
<button type="submit"
|
||||
style="padding:8px 16px;background:var(--sb-aktivan);color:#fff;border:none;border-radius:8px;font-size:14px;cursor:pointer;white-space:nowrap;transition:opacity 0.2s;"
|
||||
onmouseover="this.style.opacity='0.85'" onmouseout="this.style.opacity='1'">
|
||||
Traži
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- desktop tabela -->
|
||||
<div class="kartica servis-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);">Broj naloga</th>
|
||||
<th style="padding:12px 16px;text-align:left;font-size:12px;font-weight:500;color:var(--tekst-sporedni);">Klijent</th>
|
||||
<th style="padding:12px 16px;text-align:left;font-size:12px;font-weight:500;color:var(--tekst-sporedni);">Uređaj</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:left;font-size:12px;font-weight:500;color:var(--tekst-sporedni);">Datum prijema</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 .Nalozi}}
|
||||
<tr 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;">
|
||||
<a href="/servis/{{.ID}}"
|
||||
style="font-size:14px;font-weight:500;color:var(--sb-akcent);text-decoration:none;font-family:monospace;"
|
||||
onmouseover="this.style.textDecoration='underline'"
|
||||
onmouseout="this.style.textDecoration='none'">
|
||||
{{.BrojNaloga}}
|
||||
</a>
|
||||
</td>
|
||||
<td style="padding:12px 16px;font-size:13px;color:var(--tekst-sporedni);">
|
||||
{{if .KlijentNaziv}}{{.KlijentNaziv}}{{else}}—{{end}}
|
||||
</td>
|
||||
<td style="padding:12px 16px;font-size:14px;color:var(--tekst-glavni);">{{.Uredjaj}}</td>
|
||||
<td style="padding:12px 16px;">
|
||||
{{template "status-badge" .Status}}
|
||||
</td>
|
||||
<td style="padding:12px 16px;font-size:13px;color:var(--tekst-sporedni);">
|
||||
{{.DatumPrijema.Format "02.01.2006."}}
|
||||
</td>
|
||||
<td style="padding:12px 16px;text-align:center;">
|
||||
<div style="display:flex;align-items:center;justify-content:center;gap:8px;">
|
||||
<a href="/servis/izmeni/{{.ID}}"
|
||||
style="padding:4px 10px;background:var(--sb-aktivan);color:#fff;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="/servis/obrisi/{{.ID}}" style="display:inline;">
|
||||
<button type="submit"
|
||||
onclick="return confirm('Da li ste sigurni da želite da obrišete nalog {{.BrojNaloga}}?')"
|
||||
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="6" style="padding:32px;text-align:center;font-size:14px;color:var(--tekst-sporedni);">
|
||||
Nema servisnih naloga. <a href="/servis/novi" style="color:var(--sb-akcent);">Dodaj prvi nalog.</a>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- mobilne kartice -->
|
||||
<div class="servis-kartice">
|
||||
{{range .Nalozi}}
|
||||
<div class="kartica servis-kartica">
|
||||
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:12px;margin-bottom:10px;">
|
||||
<div>
|
||||
<a href="/servis/{{.ID}}"
|
||||
style="font-size:15px;font-weight:500;color:var(--sb-akcent);text-decoration:none;font-family:monospace;">
|
||||
{{.BrojNaloga}}
|
||||
</a>
|
||||
<div style="font-size:13px;color:var(--tekst-glavni);margin-top:2px;">{{.Uredjaj}}</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;flex-shrink:0;align-items:center;">
|
||||
{{template "status-badge" .Status}}
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;flex-direction:column;gap:6px;margin-bottom:10px;">
|
||||
{{if .KlijentNaziv}}
|
||||
<div style="font-size:13px;color:var(--tekst-sporedni);">
|
||||
<span style="color:var(--tekst-glavni);font-weight:500;">Klijent:</span> {{.KlijentNaziv}}
|
||||
</div>
|
||||
{{end}}
|
||||
<div style="font-size:12px;color:var(--tekst-sporedni);">
|
||||
Primljeno: {{.DatumPrijema.Format "02.01.2006."}}
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;">
|
||||
<a href="/servis/izmeni/{{.ID}}"
|
||||
style="padding:6px 14px;background:var(--sb-aktivan);color:#fff;border-radius:6px;font-size:13px;text-decoration:none;">
|
||||
Izmeni
|
||||
</a>
|
||||
<form method="POST" action="/servis/obrisi/{{.ID}}" style="display:inline;">
|
||||
<button type="submit"
|
||||
onclick="return confirm('Da li ste sigurni da želite da obrišete nalog {{.BrojNaloga}}?')"
|
||||
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 servisnih naloga. <a href="/servis/novi" style="color:var(--sb-akcent);">Dodaj prvi nalog.</a>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,202 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "naslov"}}Detalji naloga — NTech{{end}}
|
||||
|
||||
{{define "dodatni-css"}}
|
||||
<style>
|
||||
@keyframes fadeInUp {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from { opacity: 0; transform: translateY(-10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.detalji-kartica {
|
||||
animation: fadeInUp 0.3s ease forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.detalji-kartica:nth-child(1) { animation-delay: 0.04s; }
|
||||
.detalji-kartica:nth-child(2) { animation-delay: 0.12s; }
|
||||
.detalji-kartica:nth-child(3) { animation-delay: 0.20s; }
|
||||
.detalji-kartica:nth-child(4) { animation-delay: 0.28s; }
|
||||
.detalji-kartica:nth-child(5) { animation-delay: 0.36s; }
|
||||
|
||||
.poruka-animacija { animation: slideDown 0.3s ease forwards; }
|
||||
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status-primljeno { background: rgba(148,163,184,0.15); color: #94a3b8; }
|
||||
.status-dijagnostika { background: rgba(59,130,246,0.15); color: #3b82f6; }
|
||||
.status-ceka { background: rgba(249,115,22,0.15); color: #f97316; }
|
||||
.status-popravka { background: rgba(234,179,8,0.15); color: #ca8a04; }
|
||||
.status-zavrseno { background: rgba(34,197,94,0.15); color: #16a34a; }
|
||||
.status-preuzeto { background: rgba(21,128,61,0.15); color: #15803d; }
|
||||
</style>
|
||||
{{end}}
|
||||
|
||||
{{define "status-badge-detalji"}}
|
||||
{{if eq . "Primljeno"}}<span class="status-badge status-primljeno">{{.}}</span>
|
||||
{{else if eq . "U dijagnostici"}}<span class="status-badge status-dijagnostika">{{.}}</span>
|
||||
{{else if eq . "Čeka delove"}}<span class="status-badge status-ceka">{{.}}</span>
|
||||
{{else if eq . "U popravci"}}<span class="status-badge status-popravka">{{.}}</span>
|
||||
{{else if eq . "Završeno"}}<span class="status-badge status-zavrseno">{{.}}</span>
|
||||
{{else if eq . "Preuzeto"}}<span class="status-badge status-preuzeto">{{.}}</span>
|
||||
{{else}}<span class="status-badge status-primljeno">{{.}}</span>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{define "sadrzaj"}}
|
||||
<div style="display:flex;flex-direction:column;gap:16px;">
|
||||
|
||||
{{if .Sacuvano}}
|
||||
<div class="poruka-uspeh poruka-animacija">Nalog je uspešno sačuvan.</div>
|
||||
{{end}}
|
||||
|
||||
<!-- nazad dugme -->
|
||||
<a href="/servis"
|
||||
style="display:inline-flex;align-items:center;gap:6px;font-size:13px;color:var(--tekst-sporedni);text-decoration:none;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 servis
|
||||
</a>
|
||||
|
||||
<!-- zaglavlje naloga -->
|
||||
<div class="kartica detalji-kartica">
|
||||
<div style="display:flex;justify-content:space-between;align-items:flex-start;flex-wrap:wrap;gap:12px;margin-bottom:16px;padding-bottom:14px;border-bottom:0.5px solid var(--ivica);">
|
||||
<div style="display:flex;flex-direction:column;gap:8px;">
|
||||
<span style="font-size:20px;font-weight:600;color:var(--tekst-glavni);font-family:monospace;">
|
||||
{{.Nalog.BrojNaloga}}
|
||||
</span>
|
||||
{{template "status-badge-detalji" .Nalog.Status}}
|
||||
</div>
|
||||
<a href="/servis/izmeni/{{.Nalog.ID}}"
|
||||
style="padding:8px 18px;background:var(--sb-aktivan);color:#fff;border-radius:8px;font-size:14px;font-weight:500;text-decoration:none;transition:opacity 0.2s;white-space:nowrap;"
|
||||
onmouseover="this.style.opacity='0.85'" onmouseout="this.style.opacity='1'">
|
||||
Izmeni nalog
|
||||
</a>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fit, minmax(140px, 1fr));gap:16px;">
|
||||
<div>
|
||||
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:4px;">Datum prijema</div>
|
||||
<div style="font-size:14px;color:var(--tekst-glavni);">{{.Nalog.DatumPrijema.Format "02.01.2006. u 15:04"}}</div>
|
||||
</div>
|
||||
{{if .Nalog.DatumZavrsetka}}
|
||||
<div>
|
||||
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:4px;">Datum završetka</div>
|
||||
<div style="font-size:14px;color:var(--tekst-glavni);">{{.Nalog.DatumZavrsetka.Format "02.01.2006."}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<div>
|
||||
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:4px;">Klijent</div>
|
||||
<div style="font-size:14px;font-weight:500;color:var(--tekst-glavni);">
|
||||
{{if .KlijentNaziv}}{{.KlijentNaziv}}{{else}}—{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- podaci o uređaju -->
|
||||
<div class="kartica detalji-kartica">
|
||||
<div style="margin-bottom:16px;padding-bottom:14px;border-bottom:0.5px solid var(--ivica);">
|
||||
<span style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">Uređaj</span>
|
||||
</div>
|
||||
<div style="display:flex;flex-direction:column;gap:14px;">
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fit, minmax(160px, 1fr));gap:16px;">
|
||||
<div>
|
||||
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:4px;">Naziv uređaja</div>
|
||||
<div style="font-size:14px;font-weight:500;color:var(--tekst-glavni);">{{.Nalog.Uredjaj}}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:4px;">Serijski broj</div>
|
||||
<div style="font-size:14px;color:var(--tekst-glavni);">
|
||||
{{if .Nalog.SerijskiBroj}}{{.Nalog.SerijskiBroj}}{{else}}—{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:6px;">Opis kvara</div>
|
||||
<div style="font-size:14px;color:var(--tekst-glavni);line-height:1.6;white-space:pre-wrap;">{{.Nalog.OpisKvara}}</div>
|
||||
</div>
|
||||
{{if .Nalog.Napomena}}
|
||||
<div>
|
||||
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:6px;">Napomena</div>
|
||||
<div style="font-size:13px;color:var(--tekst-sporedni);line-height:1.6;white-space:pre-wrap;">{{.Nalog.Napomena}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- cene -->
|
||||
<div class="kartica detalji-kartica">
|
||||
<div style="margin-bottom:16px;padding-bottom:14px;border-bottom:0.5px solid var(--ivica);">
|
||||
<span style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">Cene</span>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fit, minmax(130px, 1fr));gap:16px;">
|
||||
<div>
|
||||
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:4px;">Procena od</div>
|
||||
<div style="font-size:16px;font-weight:500;color:var(--tekst-glavni);">
|
||||
{{with .Nalog.CenaOd}}{{printf "%.2f" .}} din{{else}}—{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:4px;">Procena do</div>
|
||||
<div style="font-size:16px;font-weight:500;color:var(--tekst-glavni);">
|
||||
{{with .Nalog.CenaDo}}{{printf "%.2f" .}} din{{else}}—{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:4px;">Konačna cena</div>
|
||||
<div style="font-size:20px;font-weight:600;color:var(--sb-akcent);">
|
||||
{{with .Nalog.CenaKonacna}}{{printf "%.2f" .}} din{{else}}—{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:4px;">Avans</div>
|
||||
<div style="font-size:16px;font-weight:500;color:var(--tekst-glavni);">
|
||||
{{with .Nalog.Avans}}{{printf "%.2f" .}} din{{else}}—{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- placeholder za buduće funkcije -->
|
||||
<div class="kartica detalji-kartica" style="border-style:dashed;">
|
||||
<div style="display:flex;align-items:center;gap:10px;color:var(--tekst-sporedni);">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
|
||||
<span style="font-size:13px;">Istorija promena statusa i vezani artikli biće dostupni u narednoj verziji.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- zona za brisanje -->
|
||||
<div class="kartica detalji-kartica" style="border-color:#dc262633;">
|
||||
<div style="display:flex;align-items:flex-start;gap:12px;flex-wrap:wrap;">
|
||||
<div style="flex:1;min-width:200px;">
|
||||
<div style="font-size:14px;font-weight:500;color:#dc2626;margin-bottom:4px;">Brisanje naloga</div>
|
||||
<div style="font-size:13px;color:var(--tekst-sporedni);">
|
||||
Brisanje je trajno. Nalog i svi vezani podaci biće trajno uklonjeni.
|
||||
</div>
|
||||
</div>
|
||||
<form method="POST" action="/servis/obrisi/{{.Nalog.ID}}">
|
||||
<button type="submit"
|
||||
onclick="return confirm('Da li ste sigurni da želite da obrišete nalog {{.Nalog.BrojNaloga}}?')"
|
||||
style="padding:9px 20px;background:#dc2626;color:#fff;border:none;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;white-space:nowrap;transition:opacity 0.2s;"
|
||||
onmouseover="this.style.opacity='0.85'" onmouseout="this.style.opacity='1'">
|
||||
Obriši nalog
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,197 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "naslov"}}{{if .Izmena}}Izmeni nalog{{else}}Novi nalog{{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; }
|
||||
|
||||
.sekcija-naslov {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--tekst-sporedni);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.forma-grid-2 { grid-template-columns: 1fr !important; }
|
||||
.forma-grid-4 { grid-template-columns: 1fr 1fr !important; }
|
||||
}
|
||||
</style>
|
||||
{{end}}
|
||||
|
||||
{{define "sadrzaj"}}
|
||||
<div style="width:100%;">
|
||||
|
||||
<!-- nazad dugme -->
|
||||
<a href="/servis"
|
||||
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 servis
|
||||
</a>
|
||||
|
||||
<div class="kartica forma-kartica">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:20px;padding-bottom:14px;border-bottom:0.5px solid var(--ivica);flex-wrap:wrap;gap:10px;">
|
||||
<span style="font-size:16px;font-weight:500;color:var(--tekst-glavni);">
|
||||
{{if .Izmena}}Izmeni nalog{{else}}Novi nalog{{end}}
|
||||
</span>
|
||||
<span style="font-size:14px;font-weight:500;color:var(--tekst-sporedni);font-family:monospace;">
|
||||
{{.Nalog.BrojNaloga}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{{if .Greska}}
|
||||
<div class="greska-animacija"
|
||||
style="background:var(--kartica);border:0.5px solid #dc2626;border-radius:8px;padding:10px 14px;margin-bottom:16px;font-size:13px;color:#dc2626;">
|
||||
{{.Greska}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<form method="POST" action="{{if .Izmena}}/servis/izmeni/{{.Nalog.ID}}{{else}}/servis/novi{{end}}">
|
||||
|
||||
<!-- broj naloga ide kao hidden — server ga je generisao, vraćamo ga pri POST-u -->
|
||||
<input type="hidden" name="broj_naloga" value="{{.Nalog.BrojNaloga}}">
|
||||
|
||||
<div style="display:flex;flex-direction:column;gap:20px;">
|
||||
|
||||
<!-- uređaj -->
|
||||
<div>
|
||||
<div class="sekcija-naslov">Uređaj</div>
|
||||
<div style="display:flex;flex-direction:column;gap:12px;">
|
||||
<div class="forma-grid-2" style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
|
||||
<div>
|
||||
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:6px;">
|
||||
Uređaj <span style="color:#dc2626;">*</span>
|
||||
</label>
|
||||
<input type="text" name="uredjaj" value="{{.Nalog.Uredjaj}}"
|
||||
placeholder="npr. Laptop HP EliteBook 840"
|
||||
style="width:100%;">
|
||||
</div>
|
||||
<div>
|
||||
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:6px;">Serijski broj</label>
|
||||
<input type="text" name="serijski_broj" value="{{.Nalog.SerijskiBroj}}"
|
||||
placeholder="npr. 5CD1234XYZ"
|
||||
style="width:100%;">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:6px;">
|
||||
Opis kvara <span style="color:#dc2626;">*</span>
|
||||
</label>
|
||||
<textarea name="opis_kvara" rows="3"
|
||||
placeholder="Opišite problem koji je klijent prijavio..."
|
||||
style="width:100%;resize:vertical;">{{.Nalog.OpisKvara}}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- klijent -->
|
||||
<div>
|
||||
<div class="sekcija-naslov">Klijent</div>
|
||||
<select name="klijent_id" style="width:100%;">
|
||||
<option value="">— bez klijenta —</option>
|
||||
{{range .Klijenti}}
|
||||
<option value="{{.ID}}"
|
||||
{{if $.Nalog.KlijentID}}{{if eq .ID $.Nalog.KlijentID}}selected{{end}}{{end}}>
|
||||
{{if .NazivFirme}}{{.NazivFirme}}{{else}}{{.Ime}} {{.Prezime}}{{end}}
|
||||
</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- status i datum završetka -->
|
||||
<div>
|
||||
<div class="sekcija-naslov">Status</div>
|
||||
<div class="forma-grid-2" style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
|
||||
<div>
|
||||
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:6px;">Status naloga</label>
|
||||
<select name="status" style="width:100%;">
|
||||
{{range .SviStatusi}}
|
||||
<option value="{{.}}" {{if eq . $.Nalog.Status}}selected{{end}}>{{.}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:6px;">Datum završetka</label>
|
||||
<input type="date" name="datum_zavrsetka"
|
||||
value="{{if .Nalog.DatumZavrsetka}}{{.Nalog.DatumZavrsetka.Format "2006-01-02"}}{{end}}"
|
||||
style="width:100%;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- cene -->
|
||||
<div>
|
||||
<div class="sekcija-naslov">Cene (din)</div>
|
||||
<div class="forma-grid-4" style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:12px;">
|
||||
<div>
|
||||
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:6px;">Procena od</label>
|
||||
<input type="number" name="cena_od" min="0" step="0.01"
|
||||
value="{{if .Nalog.CenaOd}}{{.Nalog.CenaOd}}{{end}}"
|
||||
placeholder="0" style="width:100%;">
|
||||
</div>
|
||||
<div>
|
||||
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:6px;">Procena do</label>
|
||||
<input type="number" name="cena_do" min="0" step="0.01"
|
||||
value="{{if .Nalog.CenaDo}}{{.Nalog.CenaDo}}{{end}}"
|
||||
placeholder="0" style="width:100%;">
|
||||
</div>
|
||||
<div>
|
||||
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:6px;">Konačna cena</label>
|
||||
<input type="number" name="cena_konacna" min="0" step="0.01"
|
||||
value="{{if .Nalog.CenaKonacna}}{{.Nalog.CenaKonacna}}{{end}}"
|
||||
placeholder="0" style="width:100%;">
|
||||
</div>
|
||||
<div>
|
||||
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:6px;">Avans</label>
|
||||
<input type="number" name="avans" min="0" step="0.01"
|
||||
value="{{if .Nalog.Avans}}{{.Nalog.Avans}}{{end}}"
|
||||
placeholder="0" style="width:100%;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- napomena -->
|
||||
<div>
|
||||
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:6px;">Napomena</label>
|
||||
<textarea name="napomena" rows="2"
|
||||
placeholder="Interna napomena o nalogu..."
|
||||
style="width:100%;resize:vertical;">{{.Nalog.Napomena}}</textarea>
|
||||
</div>
|
||||
|
||||
<!-- dugmad -->
|
||||
<div style="display:flex;justify-content:flex-end;gap:10px;margin-top:6px;">
|
||||
<a href="/servis"
|
||||
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}}Sačuvaj nalog{{end}}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user