From 7c365272f4d36b0f8141cdf5f8c41bbb9d4bedc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dalibor=20Markovi=C4=87?= Date: Sun, 21 Jun 2026 23:04:56 +0200 Subject: [PATCH] =?UTF-8?q?Usluge:=20zaseban=20modul=20(cenovnik=20usluga)?= =?UTF-8?q?=20Usluga=20je=20zaseban=20entitet,=20ne=20artikal:=20svoja=20t?= =?UTF-8?q?abela=20usluge=20(naziv,=20=C5=A1ifra,=20jedinica=20mere,=20cen?= =?UTF-8?q?a,=20PDV,=20opis),=20repo,=20handleri=20i=20forma.=20Auto-?= =?UTF-8?q?=C5=A1ifra=20USL-001=E2=80=A6999,=20jedinica=20mere=20(usluga/s?= =?UTF-8?q?at/kom).=20Bez=20nabavne/prodajne=20cene,=20dobavlja=C4=8Da,=20?= =?UTF-8?q?lagera=20i=20mar=C5=BEe.=20/usluge=20lista=20+=20CRUD;=20stari?= =?UTF-8?q?=20tip-filter=20handler=20uklonjen=20iz=20magacina.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/ntech/main.go | 5 + internal/db/repository.go | 22 +++ internal/db/sqlite/usluga.go | 181 +++++++++++++++++++++ internal/handler/handler.go | 5 +- internal/handler/kes.go | 1 + internal/handler/magacin.go | 5 - internal/handler/usluge.go | 199 +++++++++++++++++++++++ internal/model/usluga.go | 17 ++ migrations/063_usluge.sql | 15 ++ migrations/064_usluga_jedinica_mere.sql | 2 + web/static/css/main.css | 2 + web/templates/stranice/usluge.html | 110 +++++++++++++ web/templates/stranice/usluge_forma.html | 79 +++++++++ 13 files changed, 637 insertions(+), 6 deletions(-) create mode 100644 internal/db/sqlite/usluga.go create mode 100644 internal/handler/usluge.go create mode 100644 internal/model/usluga.go create mode 100644 migrations/063_usluge.sql create mode 100644 migrations/064_usluga_jedinica_mere.sql create mode 100644 web/templates/stranice/usluge.html create mode 100644 web/templates/stranice/usluge_forma.html diff --git a/cmd/ntech/main.go b/cmd/ntech/main.go index 79341b7..f9d2e08 100644 --- a/cmd/ntech/main.go +++ b/cmd/ntech/main.go @@ -302,6 +302,11 @@ func main() { r.With(modul("pdv")).Get("/pdv/obracun", h.PdvObracunStranica) r.Get("/magacin", h.Magacin) r.Get("/usluge", h.Usluge) + r.Get("/usluge/nova", h.NovaUsluga) + r.With(doz("artikal.dodaj")).Post("/usluge/nova", h.SacuvajUslugu) + r.Get("/usluge/izmeni/{id}", h.IzmeniUslugu) + r.With(doz("artikal.izmeni")).Post("/usluge/izmeni/{id}", h.SacuvajIzmenuUsluge) + r.With(doz("artikal.obrisi")).Post("/usluge/obrisi/{id}", h.ObrisiUslugu) r.Get("/troskovi", h.Troskovi) r.Get("/magacin/kartica/{id}", h.MagacinskaKartica) r.Get("/magacin/novi", h.NoviArtikal) diff --git a/internal/db/repository.go b/internal/db/repository.go index d7abb60..9ffe0f0 100644 --- a/internal/db/repository.go +++ b/internal/db/repository.go @@ -104,6 +104,28 @@ type ArtikalFilter struct { Offset int } +// UslugaFilter su parametri za listanje usluga +type UslugaFilter struct { + Pretraga string + Arhivirani bool + Limit int + Offset int +} + +// UslugaRepository definiše operacije nad uslugama (zaseban entitet od artikala) +type UslugaRepository interface { + Lista(ctx context.Context, filter UslugaFilter) ([]model.Usluga, error) + PrebrojiPoFilteru(ctx context.Context, filter UslugaFilter) (int, error) + DohvatiID(ctx context.Context, id int64) (*model.Usluga, error) + Kreiraj(ctx context.Context, u *model.Usluga) (int64, error) + Izmeni(ctx context.Context, u *model.Usluga) error + Obrisi(ctx context.Context, id int64) error + // Kategorije vraća postojeće (distinct) kategorije usluga za predlog pri unosu + Kategorije(ctx context.Context) ([]string, error) + // SledecaSifra vraća predlog sledeće auto-šifre usluge (USL-001 … USL-999) + SledecaSifra(ctx context.Context) (string, error) +} + // NabavkaRepository definiše operacije nad nabavkama type NabavkaRepository interface { Lista(ctx context.Context) ([]model.NabavkaSaDetaljem, error) diff --git a/internal/db/sqlite/usluga.go b/internal/db/sqlite/usluga.go new file mode 100644 index 0000000..8db77c6 --- /dev/null +++ b/internal/db/sqlite/usluga.go @@ -0,0 +1,181 @@ +package sqlite + +import ( + "context" + "database/sql" + "fmt" + + "ntech/internal/db" + "ntech/internal/model" +) + +// UslugaRepo je SQLite implementacija db.UslugaRepository +type UslugaRepo struct { + db *sql.DB +} + +// NoviUslugaRepo kreira repozitorijum usluga nad datom bazom +func NoviUslugaRepo(baza *sql.DB) *UslugaRepo { + return &UslugaRepo{db: baza} +} + +// skenirajUslugu čita jedan red u model.Usluga +func skenirajUslugu(s interface { + Scan(dest ...any) error +}) (model.Usluga, error) { + var u model.Usluga + var sifra sql.NullString + var arhiviran int + if err := s.Scan(&u.ID, &sifra, &u.Naziv, &u.Kategorija, &u.JedinicaMere, &u.Cena, &u.PdvStopa, &u.Opis, &arhiviran, &u.DatumUnosa); err != nil { + return u, err + } + u.Sifra = sifra.String + u.Arhiviran = arhiviran == 1 + return u, nil +} + +const uslugaKolone = "id, sifra, naziv, kategorija, jedinica_mere, cena, pdv_stopa, opis, arhiviran, datum_unosa" + +// Lista vraća usluge prema filteru (pretraga po nazivu/šifri/kategoriji, aktivne/arhivirane) +func (r *UslugaRepo) Lista(ctx context.Context, filter db.UslugaFilter) ([]model.Usluga, error) { + upit := "SELECT " + uslugaKolone + " FROM usluge WHERE 1=1" + var args []any + + if filter.Pretraga != "" { + upit += " AND (naziv LIKE ? OR sifra LIKE ? OR kategorija LIKE ?)" + t := "%" + filter.Pretraga + "%" + args = append(args, t, t, t) + } + if filter.Arhivirani { + upit += " AND arhiviran = 1" + } else { + upit += " AND arhiviran = 0" + } + upit += " ORDER BY naziv" + if filter.Limit > 0 { + upit += " LIMIT ? OFFSET ?" + args = append(args, filter.Limit, filter.Offset) + } + + redovi, err := r.db.QueryContext(ctx, upit, args...) + if err != nil { + return nil, fmt.Errorf("ntech: UslugaRepo.Lista: %w", err) + } + defer redovi.Close() + + var usluge []model.Usluga + for redovi.Next() { + u, err := skenirajUslugu(redovi) + if err != nil { + return nil, fmt.Errorf("ntech: UslugaRepo.Lista: %w", err) + } + usluge = append(usluge, u) + } + return usluge, redovi.Err() +} + +// PrebrojiPoFilteru vraća broj usluga koje zadovoljavaju filter (za paginaciju) +func (r *UslugaRepo) PrebrojiPoFilteru(ctx context.Context, filter db.UslugaFilter) (int, error) { + upit := "SELECT COUNT(*) FROM usluge WHERE 1=1" + var args []any + if filter.Pretraga != "" { + upit += " AND (naziv LIKE ? OR sifra LIKE ? OR kategorija LIKE ?)" + t := "%" + filter.Pretraga + "%" + args = append(args, t, t, t) + } + if filter.Arhivirani { + upit += " AND arhiviran = 1" + } else { + upit += " AND arhiviran = 0" + } + var n int + if err := r.db.QueryRowContext(ctx, upit, args...).Scan(&n); err != nil { + return 0, fmt.Errorf("ntech: UslugaRepo.PrebrojiPoFilteru: %w", err) + } + return n, nil +} + +// DohvatiID vraća jednu uslugu po ID-u +func (r *UslugaRepo) DohvatiID(ctx context.Context, id int64) (*model.Usluga, error) { + red := r.db.QueryRowContext(ctx, "SELECT "+uslugaKolone+" FROM usluge WHERE id = ?", id) + u, err := skenirajUslugu(red) + if err != nil { + return nil, fmt.Errorf("ntech: UslugaRepo.DohvatiID: %w", err) + } + return &u, nil +} + +// Kreiraj upisuje novu uslugu i vraća njen ID +func (r *UslugaRepo) Kreiraj(ctx context.Context, u *model.Usluga) (int64, error) { + var sifra any + if u.Sifra != "" { + sifra = u.Sifra + } + rez, err := r.db.ExecContext(ctx, ` + INSERT INTO usluge (sifra, naziv, kategorija, jedinica_mere, cena, pdv_stopa, opis) + VALUES (?, ?, ?, ?, ?, ?, ?)`, + sifra, u.Naziv, u.Kategorija, u.JedinicaMere, u.Cena, u.PdvStopa, u.Opis, + ) + if err != nil { + return 0, fmt.Errorf("ntech: UslugaRepo.Kreiraj: %w", err) + } + id, err := rez.LastInsertId() + if err != nil { + return 0, fmt.Errorf("ntech: UslugaRepo.Kreiraj: last insert id: %w", err) + } + return id, nil +} + +// Izmeni ažurira postojeću uslugu +func (r *UslugaRepo) Izmeni(ctx context.Context, u *model.Usluga) error { + var sifra any + if u.Sifra != "" { + sifra = u.Sifra + } + _, err := r.db.ExecContext(ctx, ` + UPDATE usluge SET sifra = ?, naziv = ?, kategorija = ?, jedinica_mere = ?, cena = ?, pdv_stopa = ?, opis = ? + WHERE id = ?`, + sifra, u.Naziv, u.Kategorija, u.JedinicaMere, u.Cena, u.PdvStopa, u.Opis, u.ID, + ) + if err != nil { + return fmt.Errorf("ntech: UslugaRepo.Izmeni: %w", err) + } + return nil +} + +// Obrisi trajno briše uslugu po ID-u +func (r *UslugaRepo) Obrisi(ctx context.Context, id int64) error { + if _, err := r.db.ExecContext(ctx, "DELETE FROM usluge WHERE id = ?", id); err != nil { + return fmt.Errorf("ntech: UslugaRepo.Obrisi: %w", err) + } + return nil +} + +// Kategorije vraća distinct neprazne kategorije usluga (za predlog pri unosu) +func (r *UslugaRepo) Kategorije(ctx context.Context) ([]string, error) { + redovi, err := r.db.QueryContext(ctx, "SELECT DISTINCT kategorija FROM usluge WHERE kategorija != '' ORDER BY kategorija") + if err != nil { + return nil, fmt.Errorf("ntech: UslugaRepo.Kategorije: %w", err) + } + defer redovi.Close() + var kat []string + for redovi.Next() { + var k string + if err := redovi.Scan(&k); err != nil { + return nil, fmt.Errorf("ntech: UslugaRepo.Kategorije: %w", err) + } + kat = append(kat, k) + } + return kat, redovi.Err() +} + +// SledecaSifra vraća predlog sledeće auto-šifre usluge u formatu USL-001 … USL-999 +func (r *UslugaRepo) SledecaSifra(ctx context.Context) (string, error) { + var maks sql.NullInt64 + err := r.db.QueryRowContext(ctx, + "SELECT MAX(CAST(SUBSTR(sifra, 5) AS INTEGER)) FROM usluge WHERE sifra LIKE 'USL-%'").Scan(&maks) + if err != nil { + return "USL-001", fmt.Errorf("ntech: UslugaRepo.SledecaSifra: %w", err) + } + return fmt.Sprintf("USL-%03d", maks.Int64+1), nil +} diff --git a/internal/handler/handler.go b/internal/handler/handler.go index 6dcf5fa..75f50c4 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -20,6 +20,7 @@ type Handler struct { DB *sql.DB PutanjaBaze string Artikli db.ArtikalRepository + UslugeRepo db.UslugaRepository KategorijeRepo db.KategorijaRepository DobavljaciRepo db.DobavljacRepository NabavkeRepo db.NabavkaRepository @@ -41,7 +42,7 @@ type Handler struct { PdvKprRepo db.PdvKprRepository NivelacijaRepo db.NivelacijaRepository Verzija string - JelDemo bool + JelDemo bool AssetV string // verzija statičkih fajlova za cache-busting (postavlja se pri pokretanju) Templates map[string]*template.Template TemplatesFS fs.FS @@ -84,6 +85,7 @@ func Novi(baza *sql.DB, totpKljuc []byte) *Handler { DB: baza, totpKljuc: totpKljuc, Artikli: sqlite.NoviArtikalRepo(baza), + UslugeRepo: sqlite.NoviUslugaRepo(baza), KategorijeRepo: sqlite.NovaKategorijaRepo(baza), DobavljaciRepo: sqlite.NoviDobavljacRepo(baza), NabavkeRepo: sqlite.NoviNabavkaRepo(baza), @@ -113,6 +115,7 @@ func Novi(baza *sql.DB, totpKljuc []byte) *Handler { func (h *Handler) reinicijalizujRepozitorijume(novaDB *sql.DB) { h.DB = novaDB h.Artikli = sqlite.NoviArtikalRepo(novaDB) + h.UslugeRepo = sqlite.NoviUslugaRepo(novaDB) h.KategorijeRepo = sqlite.NovaKategorijaRepo(novaDB) h.DobavljaciRepo = sqlite.NoviDobavljacRepo(novaDB) h.NabavkeRepo = sqlite.NoviNabavkaRepo(novaDB) diff --git a/internal/handler/kes.go b/internal/handler/kes.go index 3279e0d..420cf16 100644 --- a/internal/handler/kes.go +++ b/internal/handler/kes.go @@ -37,6 +37,7 @@ var saSidebar = []string{ "profil_tema", "prodaja", "prodaja_detalji", "prodaja_forma", "servis", "servis_arhiva", "servis_forma", "servis_detalji", + "usluge", "usluge_forma", } // standalone su šabloni bez base layouta diff --git a/internal/handler/magacin.go b/internal/handler/magacin.go index 1eb56e3..f766582 100644 --- a/internal/handler/magacin.go +++ b/internal/handler/magacin.go @@ -42,11 +42,6 @@ func (h *Handler) Magacin(w http.ResponseWriter, r *http.Request) { h.magacinPrikaz(w, r, model.TipProizvod, "magacin", "Magacin", "/magacin") } -// Usluge renderuje listu usluga (tip 'usluga') -func (h *Handler) Usluge(w http.ResponseWriter, r *http.Request) { - h.magacinPrikaz(w, r, model.TipUsluga, "usluge", "Usluge", "/usluge") -} - // Troskovi renderuje listu troškova (tip 'trosak') func (h *Handler) Troskovi(w http.ResponseWriter, r *http.Request) { h.magacinPrikaz(w, r, model.TipTrosak, "troskovi", "Troškovi", "/troskovi") diff --git a/internal/handler/usluge.go b/internal/handler/usluge.go new file mode 100644 index 0000000..3498fb6 --- /dev/null +++ b/internal/handler/usluge.go @@ -0,0 +1,199 @@ +package handler + +import ( + "net/http" + "strconv" + "strings" + + "ntech/internal/db" + "ntech/internal/db/sqlite" + "ntech/internal/model" + + "github.com/go-chi/chi/v5" +) + +// PodaciUsluga su podaci za listu usluga +type PodaciUsluga struct { + model.PodaciStranice + Usluge []model.Usluga + Pretraga string + Sacuvano bool + Obrisan bool +} + +// PodaciFormeUsluge su podaci za formu nove/izmenjene usluge +type PodaciFormeUsluge struct { + model.PodaciStranice + Usluga model.Usluga + Kategorije []string // postojeće kategorije za predlog (datalist) + Greska string + Izmena bool +} + +// Usluge renderuje listu usluga (cenovnik usluga) +func (h *Handler) Usluge(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") + usluge, err := h.UslugeRepo.Lista(r.Context(), db.UslugaFilter{Pretraga: pretraga}) + if err != nil { + http.Error(w, "Greška pri učitavanju usluga", http.StatusInternalServerError) + return + } + + ps := h.popuniPodaciStranice(r, podesavanja) + ps.Stranica = "usluge" + ps.NaslovStranice = "Usluge" + h.renderujTemplate(w, "usluge", PodaciUsluga{ + PodaciStranice: ps, + Usluge: usluge, + Pretraga: pretraga, + Sacuvano: r.URL.Query().Get("sacuvano") == "1", + Obrisan: r.URL.Query().Get("obrisan") == "1", + }) +} + +// NovaUsluga prikazuje praznu formu sa predloženom auto-šifrom (USL-NNN) +func (h *Handler) NovaUsluga(w http.ResponseWriter, r *http.Request) { + sifra, err := h.UslugeRepo.SledecaSifra(r.Context()) + if err != nil { + sifra = "USL-001" + } + h.renderujFormuUsluge(w, r, model.Usluga{Sifra: sifra, PdvStopa: 20, JedinicaMere: "usluga"}, false, "") +} + +// IzmeniUslugu prikazuje formu sa postojećom uslugom +func (h *Handler) IzmeniUslugu(w http.ResponseWriter, r *http.Request) { + id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) + if err != nil { + http.Error(w, "Neispravan ID usluge", http.StatusBadRequest) + return + } + usluga, err := h.UslugeRepo.DohvatiID(r.Context(), id) + if err != nil { + http.Error(w, "Usluga nije pronađena", http.StatusNotFound) + return + } + h.renderujFormuUsluge(w, r, *usluga, true, "") +} + +// SacuvajUslugu prima POST i kreira novu uslugu +func (h *Handler) SacuvajUslugu(w http.ResponseWriter, r *http.Request) { + if _, ok := h.zahtevajDozvolu(w, r, "artikal.dodaj"); !ok { + return + } + usluga, greska := parseFormuUsluge(r) + if greska != "" { + h.renderujFormuUsluge(w, r, usluga, false, greska) + return + } + if _, err := h.UslugeRepo.Kreiraj(r.Context(), &usluga); err != nil { + h.renderujFormuUsluge(w, r, usluga, false, "Greška pri čuvanju usluge. Pokušajte ponovo.") + return + } + http.Redirect(w, r, "/usluge?sacuvano=1", http.StatusSeeOther) +} + +// SacuvajIzmenuUsluge prima POST i ažurira postojeću uslugu +func (h *Handler) SacuvajIzmenuUsluge(w http.ResponseWriter, r *http.Request) { + if _, ok := h.zahtevajDozvolu(w, r, "artikal.izmeni"); !ok { + return + } + id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) + if err != nil { + http.Error(w, "Neispravan ID usluge", http.StatusBadRequest) + return + } + usluga, greska := parseFormuUsluge(r) + usluga.ID = id + if greska != "" { + h.renderujFormuUsluge(w, r, usluga, true, greska) + return + } + if err := h.UslugeRepo.Izmeni(r.Context(), &usluga); err != nil { + h.renderujFormuUsluge(w, r, usluga, true, "Greška pri čuvanju usluge. Pokušajte ponovo.") + return + } + http.Redirect(w, r, "/usluge?sacuvano=1", http.StatusSeeOther) +} + +// ObrisiUslugu briše uslugu po ID-u +func (h *Handler) ObrisiUslugu(w http.ResponseWriter, r *http.Request) { + if _, ok := h.zahtevajDozvolu(w, r, "artikal.obrisi"); !ok { + return + } + id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) + if err != nil { + http.Error(w, "Neispravan ID usluge", http.StatusBadRequest) + return + } + if err := h.UslugeRepo.Obrisi(r.Context(), id); err != nil { + http.Error(w, "Greška pri brisanju usluge", http.StatusInternalServerError) + return + } + http.Redirect(w, r, "/usluge?obrisan=1", http.StatusSeeOther) +} + +// parseFormuUsluge čita i validira polja forme usluge. Vraća model i poruku o grešci. +func parseFormuUsluge(r *http.Request) (model.Usluga, string) { + if err := r.ParseForm(); err != nil { + return model.Usluga{}, "Greška pri čitanju forme." + } + naziv := strings.TrimSpace(r.FormValue("naziv")) + if naziv == "" { + return model.Usluga{Kategorija: strings.TrimSpace(r.FormValue("kategorija"))}, "Naziv usluge je obavezan." + } + + cena, err := strconv.ParseFloat(strings.TrimSpace(r.FormValue("cena")), 64) + if err != nil || cena < 0 { + cena = 0 + } + pdv, err := strconv.ParseFloat(strings.TrimSpace(r.FormValue("pdv_stopa")), 64) + if err != nil || pdv < 0 { + pdv = 20 + } + + jm := strings.TrimSpace(r.FormValue("jedinica_mere")) + if jm == "" { + jm = "usluga" + } + + return model.Usluga{ + Sifra: strings.TrimSpace(r.FormValue("sifra")), + Naziv: naziv, + Kategorija: strings.TrimSpace(r.FormValue("kategorija")), + JedinicaMere: jm, + Cena: cena, + PdvStopa: pdv, + Opis: strings.TrimSpace(r.FormValue("opis")), + }, "" +} + +// renderujFormuUsluge prikazuje formu usluge (zajedničko za novu i izmenu) +func (h *Handler) renderujFormuUsluge(w http.ResponseWriter, r *http.Request, usluga model.Usluga, izmena bool, greska string) { + podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB) + if err != nil { + http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError) + return + } + kategorije, _ := h.UslugeRepo.Kategorije(r.Context()) + + ps := h.popuniPodaciStranice(r, podesavanja) + ps.Stranica = "usluge" + if izmena { + ps.NaslovStranice = "Izmena usluge" + } else { + ps.NaslovStranice = "Nova usluga" + } + h.renderujTemplate(w, "usluge_forma", PodaciFormeUsluge{ + PodaciStranice: ps, + Usluga: usluga, + Kategorije: kategorije, + Greska: greska, + Izmena: izmena, + }) +} diff --git a/internal/model/usluga.go b/internal/model/usluga.go new file mode 100644 index 0000000..555b59b --- /dev/null +++ b/internal/model/usluga.go @@ -0,0 +1,17 @@ +package model + +// Usluga je stavka cenovnika usluga (npr. čišćenje laptopa, zamena ventilatora). +// Za razliku od artikla, usluga ne prati lager i nema nabavnu cenu ni dobavljače — +// ima samo cenu usluge i PDV stopu. Kategorija je tekstualna oznaka. +type Usluga struct { + ID int64 + Sifra string + Naziv string + Kategorija string + JedinicaMere string + Cena float64 + PdvStopa float64 + Opis string + Arhiviran bool + DatumUnosa string +} diff --git a/migrations/063_usluge.sql b/migrations/063_usluge.sql new file mode 100644 index 0000000..cf07b29 --- /dev/null +++ b/migrations/063_usluge.sql @@ -0,0 +1,15 @@ +-- Usluge: zaseban entitet (NIJE artikal) — ne prate lager, nemaju nabavnu cenu +-- ni dobavljače. Imaju cenu usluge + PDV stopu. Kategorija je tekstualna oznaka +-- (npr. „Servis", „Instalacija"); slobodan unos uz predlog postojećih vrednosti. +CREATE TABLE IF NOT EXISTS usluge ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + sifra TEXT, + naziv TEXT NOT NULL, + kategorija TEXT NOT NULL DEFAULT '', + cena REAL NOT NULL DEFAULT 0, + pdv_stopa REAL NOT NULL DEFAULT 20, + opis TEXT NOT NULL DEFAULT '', + arhiviran INTEGER NOT NULL DEFAULT 0, + datum_unosa TEXT NOT NULL DEFAULT (datetime('now')) +); +CREATE INDEX IF NOT EXISTS idx_usluge_arhiviran ON usluge(arhiviran); diff --git a/migrations/064_usluga_jedinica_mere.sql b/migrations/064_usluga_jedinica_mere.sql new file mode 100644 index 0000000..b7f2bbf --- /dev/null +++ b/migrations/064_usluga_jedinica_mere.sql @@ -0,0 +1,2 @@ +-- Jedinica mere usluge: 'usluga' (fiksna), 'sat' (naplata po satu) ili 'kom'. +ALTER TABLE usluge ADD COLUMN jedinica_mere TEXT NOT NULL DEFAULT 'usluga'; diff --git a/web/static/css/main.css b/web/static/css/main.css index f19ef6f..42fa97e 100644 --- a/web/static/css/main.css +++ b/web/static/css/main.css @@ -1223,6 +1223,7 @@ select { .klijenti-kartice { display: none; flex-direction: column; gap: 12px; } .magacin-kartice { display: none; flex-direction: column; gap: 12px; } .servis-kartice { display: none; flex-direction: column; gap: 12px; } +.usluge-kartice { display: none; flex-direction: column; gap: 12px; } .prodaja-kartice { display: none; flex-direction: column; gap: 12px; } .pod-kartice { display: none; flex-direction: column; gap: 12px; } .stavke-kartice { display: none; flex-direction: column; gap: 10px; } @@ -1276,6 +1277,7 @@ select { .klijenti-tabela { display: none; } .klijenti-kartice { display: flex; } .magacin-tabela { display: none; } .magacin-kartice { display: flex; } .servis-tabela { display: none; } .servis-kartice { display: flex; } + .usluge-tabela { display: none; } .usluge-kartice { display: flex; } .prodaja-tabela { display: none; } .prodaja-kartice { display: flex; } .pod-tabela { display: none; } .pod-kartice { display: flex; } .stavke-tabela-wrapper { display: none; } .stavke-kartice { display: flex; } diff --git a/web/templates/stranice/usluge.html b/web/templates/stranice/usluge.html new file mode 100644 index 0000000..4503b03 --- /dev/null +++ b/web/templates/stranice/usluge.html @@ -0,0 +1,110 @@ +{{template "base" .}} + +{{define "naslov"}}Usluge — NTech{{end}} + +{{define "sadrzaj"}} +
+ + {{if .Sacuvano}} +
Usluga je uspešno sačuvana.
+ {{end}} + {{if .Obrisan}} +
Usluga je uspešno obrisana.
+ {{end}} + + +
+ {{if index .Dozvole "artikal.dodaj"}} + + Nova usluga + {{end}} +
+ + +
+ +
+ +
+ + +
+
+ + + + + + + + + + + + + {{range .Usluge}} + + + + + + + + + {{else}} + + + + {{end}} + +
ŠifraNazivJMCenaPDVAkcije
{{if .Sifra}}{{.Sifra}}{{else}}—{{end}}{{.Naziv}}{{.JedinicaMere}}{{dinari .Cena}} din{{.PdvStopa}}% +
+ Izmeni + {{if index $.Dozvole "artikal.obrisi"}} +
+ + +
+ {{end}} +
+
+ {{if $.Pretraga}}Za datu pretragu nema usluga u bazi.{{else}}Nema usluga. Dodaj prvu uslugu.{{end}} +
+
+
+ + +
+ {{range .Usluge}} +
+
+
+
{{.Naziv}}
+
{{.JedinicaMere}}
+
+
{{dinari .Cena}} din
+
+
+ Izmeni + {{if index $.Dozvole "artikal.obrisi"}} +
+ + +
+ {{end}} +
+
+ {{else}} +
+ {{if $.Pretraga}}Za datu pretragu nema usluga u bazi.{{else}}Nema usluga. Dodaj prvu uslugu.{{end}} +
+ {{end}} +
+ +
+
+{{end}} diff --git a/web/templates/stranice/usluge_forma.html b/web/templates/stranice/usluge_forma.html new file mode 100644 index 0000000..8002fb6 --- /dev/null +++ b/web/templates/stranice/usluge_forma.html @@ -0,0 +1,79 @@ +{{template "base" .}} + +{{define "naslov"}}{{if .Izmena}}Izmena usluge{{else}}Nova usluga{{end}} — NTech{{end}} + +{{define "sadrzaj"}} +
+ + + + Nazad na usluge + + + {{if .Greska}} +
{{.Greska}}
+ {{end}} + +
+ +
+
+ {{if .Izmena}}Izmena usluge{{else}}Nova usluga{{end}} +
+ +
+
+
+ + +
+
+ + +
+
+ +
+
+ + {{$jm := .Usluga.JedinicaMere}} + +
+
+ + +
+
+ + {{$pdv := .Usluga.PdvStopa}} + +
+
+ +
+ + +
+
+ +
+ Odustani + +
+
+
+ +
+{{end}}