Troškovi: zaseban modul (šifrarnik vrsta troškova)
Opcija 1 — katalog dodatnih troškova (Prevoz, Carina, Ambalaža) koji se vezuju za radni nalog i ulaze u cenu koštanja. Zaseban entitet: tabela troskovi (šifra TRO-001…999, naziv, podrazumevana cena, opis), repo, handleri, forma i lista. Stari tip-filter handler uklonjen iz magacina. (Knjiga rashoda firme je zaseban budući modul — Opcija 2.)
This commit is contained in:
@@ -308,6 +308,11 @@ func main() {
|
||||
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("/troskovi/novi", h.NoviTrosak)
|
||||
r.With(doz("artikal.dodaj")).Post("/troskovi/novi", h.SacuvajTrosak)
|
||||
r.Get("/troskovi/izmeni/{id}", h.IzmeniTrosak)
|
||||
r.With(doz("artikal.izmeni")).Post("/troskovi/izmeni/{id}", h.SacuvajIzmenuTroska)
|
||||
r.With(doz("artikal.obrisi")).Post("/troskovi/obrisi/{id}", h.ObrisiTrosak)
|
||||
r.Get("/magacin/kartica/{id}", h.MagacinskaKartica)
|
||||
r.Get("/magacin/novi", h.NoviArtikal)
|
||||
r.With(doz("artikal.dodaj")).Post("/magacin/novi", h.SacuvajArtikal)
|
||||
|
||||
@@ -126,6 +126,25 @@ type UslugaRepository interface {
|
||||
SledecaSifra(ctx context.Context) (string, error)
|
||||
}
|
||||
|
||||
// TrosakFilter su parametri za listanje troškova
|
||||
type TrosakFilter struct {
|
||||
Pretraga string
|
||||
Arhivirani bool
|
||||
Limit int
|
||||
Offset int
|
||||
}
|
||||
|
||||
// TrosakRepository definiše operacije nad šifrarnikom vrsta troškova
|
||||
type TrosakRepository interface {
|
||||
Lista(ctx context.Context, filter TrosakFilter) ([]model.Trosak, error)
|
||||
DohvatiID(ctx context.Context, id int64) (*model.Trosak, error)
|
||||
Kreiraj(ctx context.Context, t *model.Trosak) (int64, error)
|
||||
Izmeni(ctx context.Context, t *model.Trosak) error
|
||||
Obrisi(ctx context.Context, id int64) error
|
||||
// SledecaSifra vraća predlog sledeće auto-šifre troška (TRO-001 … TRO-999)
|
||||
SledecaSifra(ctx context.Context) (string, error)
|
||||
}
|
||||
|
||||
// NabavkaRepository definiše operacije nad nabavkama
|
||||
type NabavkaRepository interface {
|
||||
Lista(ctx context.Context) ([]model.NabavkaSaDetaljem, error)
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"ntech/internal/db"
|
||||
"ntech/internal/model"
|
||||
)
|
||||
|
||||
// TrosakRepo je SQLite implementacija db.TrosakRepository
|
||||
type TrosakRepo struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// NoviTrosakRepo kreira repozitorijum troškova nad datom bazom
|
||||
func NoviTrosakRepo(baza *sql.DB) *TrosakRepo {
|
||||
return &TrosakRepo{db: baza}
|
||||
}
|
||||
|
||||
// skenirajTrosak čita jedan red u model.Trosak
|
||||
func skenirajTrosak(s interface {
|
||||
Scan(dest ...any) error
|
||||
}) (model.Trosak, error) {
|
||||
var t model.Trosak
|
||||
var sifra sql.NullString
|
||||
var arhiviran int
|
||||
if err := s.Scan(&t.ID, &sifra, &t.Naziv, &t.Cena, &t.Opis, &arhiviran, &t.DatumUnosa); err != nil {
|
||||
return t, err
|
||||
}
|
||||
t.Sifra = sifra.String
|
||||
t.Arhiviran = arhiviran == 1
|
||||
return t, nil
|
||||
}
|
||||
|
||||
const trosakKolone = "id, sifra, naziv, cena, opis, arhiviran, datum_unosa"
|
||||
|
||||
// Lista vraća troškove prema filteru (pretraga po nazivu/šifri, aktivne/arhivirane)
|
||||
func (r *TrosakRepo) Lista(ctx context.Context, filter db.TrosakFilter) ([]model.Trosak, error) {
|
||||
upit := "SELECT " + trosakKolone + " FROM troskovi WHERE 1=1"
|
||||
var args []any
|
||||
|
||||
if filter.Pretraga != "" {
|
||||
upit += " AND (naziv LIKE ? OR sifra LIKE ?)"
|
||||
t := "%" + filter.Pretraga + "%"
|
||||
args = append(args, 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: TrosakRepo.Lista: %w", err)
|
||||
}
|
||||
defer redovi.Close()
|
||||
|
||||
var troskovi []model.Trosak
|
||||
for redovi.Next() {
|
||||
t, err := skenirajTrosak(redovi)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ntech: TrosakRepo.Lista: %w", err)
|
||||
}
|
||||
troskovi = append(troskovi, t)
|
||||
}
|
||||
return troskovi, redovi.Err()
|
||||
}
|
||||
|
||||
// DohvatiID vraća jedan trošak po ID-u
|
||||
func (r *TrosakRepo) DohvatiID(ctx context.Context, id int64) (*model.Trosak, error) {
|
||||
red := r.db.QueryRowContext(ctx, "SELECT "+trosakKolone+" FROM troskovi WHERE id = ?", id)
|
||||
t, err := skenirajTrosak(red)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ntech: TrosakRepo.DohvatiID: %w", err)
|
||||
}
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
// Kreiraj upisuje nov trošak i vraća njegov ID
|
||||
func (r *TrosakRepo) Kreiraj(ctx context.Context, t *model.Trosak) (int64, error) {
|
||||
var sifra any
|
||||
if t.Sifra != "" {
|
||||
sifra = t.Sifra
|
||||
}
|
||||
rez, err := r.db.ExecContext(ctx, `
|
||||
INSERT INTO troskovi (sifra, naziv, cena, opis)
|
||||
VALUES (?, ?, ?, ?)`,
|
||||
sifra, t.Naziv, t.Cena, t.Opis,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("ntech: TrosakRepo.Kreiraj: %w", err)
|
||||
}
|
||||
id, err := rez.LastInsertId()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("ntech: TrosakRepo.Kreiraj: last insert id: %w", err)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// Izmeni ažurira postojeći trošak
|
||||
func (r *TrosakRepo) Izmeni(ctx context.Context, t *model.Trosak) error {
|
||||
var sifra any
|
||||
if t.Sifra != "" {
|
||||
sifra = t.Sifra
|
||||
}
|
||||
_, err := r.db.ExecContext(ctx, `
|
||||
UPDATE troskovi SET sifra = ?, naziv = ?, cena = ?, opis = ?
|
||||
WHERE id = ?`,
|
||||
sifra, t.Naziv, t.Cena, t.Opis, t.ID,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntech: TrosakRepo.Izmeni: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Obrisi trajno briše trošak po ID-u
|
||||
func (r *TrosakRepo) Obrisi(ctx context.Context, id int64) error {
|
||||
if _, err := r.db.ExecContext(ctx, "DELETE FROM troskovi WHERE id = ?", id); err != nil {
|
||||
return fmt.Errorf("ntech: TrosakRepo.Obrisi: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SledecaSifra vraća predlog sledeće auto-šifre troška u formatu TRO-001 … TRO-999
|
||||
func (r *TrosakRepo) SledecaSifra(ctx context.Context) (string, error) {
|
||||
var maks sql.NullInt64
|
||||
err := r.db.QueryRowContext(ctx,
|
||||
"SELECT MAX(CAST(SUBSTR(sifra, 5) AS INTEGER)) FROM troskovi WHERE sifra LIKE 'TRO-%'").Scan(&maks)
|
||||
if err != nil {
|
||||
return "TRO-001", fmt.Errorf("ntech: TrosakRepo.SledecaSifra: %w", err)
|
||||
}
|
||||
return fmt.Sprintf("TRO-%03d", maks.Int64+1), nil
|
||||
}
|
||||
@@ -21,6 +21,7 @@ type Handler struct {
|
||||
PutanjaBaze string
|
||||
Artikli db.ArtikalRepository
|
||||
UslugeRepo db.UslugaRepository
|
||||
TroskoviRepo db.TrosakRepository
|
||||
KategorijeRepo db.KategorijaRepository
|
||||
DobavljaciRepo db.DobavljacRepository
|
||||
NabavkeRepo db.NabavkaRepository
|
||||
@@ -86,6 +87,7 @@ func Novi(baza *sql.DB, totpKljuc []byte) *Handler {
|
||||
totpKljuc: totpKljuc,
|
||||
Artikli: sqlite.NoviArtikalRepo(baza),
|
||||
UslugeRepo: sqlite.NoviUslugaRepo(baza),
|
||||
TroskoviRepo: sqlite.NoviTrosakRepo(baza),
|
||||
KategorijeRepo: sqlite.NovaKategorijaRepo(baza),
|
||||
DobavljaciRepo: sqlite.NoviDobavljacRepo(baza),
|
||||
NabavkeRepo: sqlite.NoviNabavkaRepo(baza),
|
||||
@@ -116,6 +118,7 @@ func (h *Handler) reinicijalizujRepozitorijume(novaDB *sql.DB) {
|
||||
h.DB = novaDB
|
||||
h.Artikli = sqlite.NoviArtikalRepo(novaDB)
|
||||
h.UslugeRepo = sqlite.NoviUslugaRepo(novaDB)
|
||||
h.TroskoviRepo = sqlite.NoviTrosakRepo(novaDB)
|
||||
h.KategorijeRepo = sqlite.NovaKategorijaRepo(novaDB)
|
||||
h.DobavljaciRepo = sqlite.NoviDobavljacRepo(novaDB)
|
||||
h.NabavkeRepo = sqlite.NoviNabavkaRepo(novaDB)
|
||||
|
||||
@@ -38,6 +38,7 @@ var saSidebar = []string{
|
||||
"prodaja", "prodaja_detalji", "prodaja_forma",
|
||||
"servis", "servis_arhiva", "servis_forma", "servis_detalji",
|
||||
"usluge", "usluge_forma",
|
||||
"troskovi", "troskovi_forma",
|
||||
}
|
||||
|
||||
// standalone su šabloni bez base layouta
|
||||
|
||||
@@ -42,11 +42,6 @@ func (h *Handler) Magacin(w http.ResponseWriter, r *http.Request) {
|
||||
h.magacinPrikaz(w, r, model.TipProizvod, "magacin", "Magacin", "/magacin")
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
// magacinPrikaz je zajednička logika liste artikala, parametrizovana tipom; isti
|
||||
// šablon služi za proizvode, usluge i troškove (razlikuju se samo tip i bazna ruta)
|
||||
func (h *Handler) magacinPrikaz(w http.ResponseWriter, r *http.Request, tip, stranica, naslov, osnovniUrl string) {
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"ntech/internal/db"
|
||||
"ntech/internal/db/sqlite"
|
||||
"ntech/internal/model"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
// PodaciTroskovi su podaci za listu troškova
|
||||
type PodaciTroskovi struct {
|
||||
model.PodaciStranice
|
||||
Troskovi []model.Trosak
|
||||
Pretraga string
|
||||
Sacuvano bool
|
||||
Obrisan bool
|
||||
}
|
||||
|
||||
// PodaciFormeTroska su podaci za formu novog/izmenjenog troška
|
||||
type PodaciFormeTroska struct {
|
||||
model.PodaciStranice
|
||||
Trosak model.Trosak
|
||||
Greska string
|
||||
Izmena bool
|
||||
}
|
||||
|
||||
// Troskovi renderuje listu šifrarnika vrsta troškova
|
||||
func (h *Handler) Troskovi(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")
|
||||
troskovi, err := h.TroskoviRepo.Lista(r.Context(), db.TrosakFilter{Pretraga: pretraga})
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri učitavanju troškova", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
ps := h.popuniPodaciStranice(r, podesavanja)
|
||||
ps.Stranica = "troskovi"
|
||||
ps.NaslovStranice = "Troškovi"
|
||||
h.renderujTemplate(w, "troskovi", PodaciTroskovi{
|
||||
PodaciStranice: ps,
|
||||
Troskovi: troskovi,
|
||||
Pretraga: pretraga,
|
||||
Sacuvano: r.URL.Query().Get("sacuvano") == "1",
|
||||
Obrisan: r.URL.Query().Get("obrisan") == "1",
|
||||
})
|
||||
}
|
||||
|
||||
// NoviTrosak prikazuje praznu formu sa predloženom auto-šifrom (TRO-NNN)
|
||||
func (h *Handler) NoviTrosak(w http.ResponseWriter, r *http.Request) {
|
||||
sifra, err := h.TroskoviRepo.SledecaSifra(r.Context())
|
||||
if err != nil {
|
||||
sifra = "TRO-001"
|
||||
}
|
||||
h.renderujFormuTroska(w, r, model.Trosak{Sifra: sifra}, false, "")
|
||||
}
|
||||
|
||||
// IzmeniTrosak prikazuje formu sa postojećim troškom
|
||||
func (h *Handler) IzmeniTrosak(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||
if err != nil {
|
||||
http.Error(w, "Neispravan ID troška", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
trosak, err := h.TroskoviRepo.DohvatiID(r.Context(), id)
|
||||
if err != nil {
|
||||
http.Error(w, "Trošak nije pronađen", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
h.renderujFormuTroska(w, r, *trosak, true, "")
|
||||
}
|
||||
|
||||
// SacuvajTrosak prima POST i kreira nov trošak
|
||||
func (h *Handler) SacuvajTrosak(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := h.zahtevajDozvolu(w, r, "artikal.dodaj"); !ok {
|
||||
return
|
||||
}
|
||||
trosak, greska := parseFormuTroska(r)
|
||||
if greska != "" {
|
||||
h.renderujFormuTroska(w, r, trosak, false, greska)
|
||||
return
|
||||
}
|
||||
if _, err := h.TroskoviRepo.Kreiraj(r.Context(), &trosak); err != nil {
|
||||
h.renderujFormuTroska(w, r, trosak, false, "Greška pri čuvanju troška. Pokušajte ponovo.")
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/troskovi?sacuvano=1", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// SacuvajIzmenuTroska prima POST i ažurira postojeći trošak
|
||||
func (h *Handler) SacuvajIzmenuTroska(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 troška", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
trosak, greska := parseFormuTroska(r)
|
||||
trosak.ID = id
|
||||
if greska != "" {
|
||||
h.renderujFormuTroska(w, r, trosak, true, greska)
|
||||
return
|
||||
}
|
||||
if err := h.TroskoviRepo.Izmeni(r.Context(), &trosak); err != nil {
|
||||
h.renderujFormuTroska(w, r, trosak, true, "Greška pri čuvanju troška. Pokušajte ponovo.")
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/troskovi?sacuvano=1", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// ObrisiTrosak briše trošak po ID-u
|
||||
func (h *Handler) ObrisiTrosak(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 troška", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := h.TroskoviRepo.Obrisi(r.Context(), id); err != nil {
|
||||
http.Error(w, "Greška pri brisanju troška", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/troskovi?obrisan=1", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// parseFormuTroska čita i validira polja forme troška
|
||||
func parseFormuTroska(r *http.Request) (model.Trosak, string) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
return model.Trosak{}, "Greška pri čitanju forme."
|
||||
}
|
||||
naziv := strings.TrimSpace(r.FormValue("naziv"))
|
||||
if naziv == "" {
|
||||
return model.Trosak{}, "Naziv troška je obavezan."
|
||||
}
|
||||
cena, err := strconv.ParseFloat(strings.TrimSpace(r.FormValue("cena")), 64)
|
||||
if err != nil || cena < 0 {
|
||||
cena = 0
|
||||
}
|
||||
return model.Trosak{
|
||||
Sifra: strings.TrimSpace(r.FormValue("sifra")),
|
||||
Naziv: naziv,
|
||||
Cena: cena,
|
||||
Opis: strings.TrimSpace(r.FormValue("opis")),
|
||||
}, ""
|
||||
}
|
||||
|
||||
// renderujFormuTroska prikazuje formu troška (zajedničko za nov i izmenu)
|
||||
func (h *Handler) renderujFormuTroska(w http.ResponseWriter, r *http.Request, trosak model.Trosak, 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
|
||||
}
|
||||
ps := h.popuniPodaciStranice(r, podesavanja)
|
||||
ps.Stranica = "troskovi"
|
||||
if izmena {
|
||||
ps.NaslovStranice = "Izmena troška"
|
||||
} else {
|
||||
ps.NaslovStranice = "Nov trošak"
|
||||
}
|
||||
h.renderujTemplate(w, "troskovi_forma", PodaciFormeTroska{
|
||||
PodaciStranice: ps,
|
||||
Trosak: trosak,
|
||||
Greska: greska,
|
||||
Izmena: izmena,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package model
|
||||
|
||||
// Trosak je stavka šifrarnika vrsta troškova (npr. Prevoz, Carina, Ambalaža).
|
||||
// To je katalog dodatnih troškova koji se vezuju za radni nalog i ulaze u cenu
|
||||
// koštanja. NIJE evidencija rashoda firme (struja, kirija) — to je poseban modul.
|
||||
type Trosak struct {
|
||||
ID int64
|
||||
Sifra string
|
||||
Naziv string
|
||||
Cena float64
|
||||
Opis string
|
||||
Arhiviran bool
|
||||
DatumUnosa string
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
-- Šifrarnik vrsta troškova (Opcija 1): katalog dodatnih troškova koji se vezuju
|
||||
-- za radni nalog (npr. Prevoz, Carina, Ambalaža). Ulaze u cenu koštanja naloga,
|
||||
-- ne na fakturu klijentu. Ovo NIJE knjiga rashoda firme (struja/kirija) — to je
|
||||
-- poseban modul. Trošak ima samo podrazumevanu cenu; iznos se zadaje pri vezivanju.
|
||||
CREATE TABLE IF NOT EXISTS troskovi (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sifra TEXT,
|
||||
naziv TEXT NOT NULL,
|
||||
cena REAL NOT NULL DEFAULT 0,
|
||||
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_troskovi_arhiviran ON troskovi(arhiviran);
|
||||
@@ -1224,6 +1224,7 @@ select {
|
||||
.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; }
|
||||
.troskovi-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; }
|
||||
@@ -1278,6 +1279,7 @@ select {
|
||||
.magacin-tabela { display: none; } .magacin-kartice { display: flex; }
|
||||
.servis-tabela { display: none; } .servis-kartice { display: flex; }
|
||||
.usluge-tabela { display: none; } .usluge-kartice { display: flex; }
|
||||
.troskovi-tabela { display: none; } .troskovi-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; }
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "naslov"}}Troškovi — NTech{{end}}
|
||||
|
||||
{{define "sadrzaj"}}
|
||||
<div class="kolona" style="gap:16px;">
|
||||
|
||||
{{if .Sacuvano}}
|
||||
<div class="poruka-uspeh poruka-animacija">Trošak je uspešno sačuvan.</div>
|
||||
{{end}}
|
||||
{{if .Obrisan}}
|
||||
<div class="poruka-uspeh poruka-animacija">Trošak je uspešno obrisan.</div>
|
||||
{{end}}
|
||||
|
||||
<div style="font-size:13px;color:var(--tekst-sporedni);">
|
||||
Šifrarnik vrsta troškova (npr. Prevoz, Carina, Ambalaža) koji se vezuju za radni nalog kao dodatni trošak.
|
||||
</div>
|
||||
|
||||
<!-- dugme nov -->
|
||||
<div style="display:flex;gap:10px;flex-wrap:wrap;">
|
||||
{{if index .Dozvole "artikal.dodaj"}}
|
||||
<a href="/troskovi/novi" class="btn-primarno">+ Nov trošak</a>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<!-- pretraga -->
|
||||
<form method="GET" action="/troskovi" class="kolona" style="gap:10px;">
|
||||
<input type="text" name="pretraga" value="{{.Pretraga}}"
|
||||
placeholder="Pretraži troškove po nazivu ili šifri..."
|
||||
style="width:100%;"
|
||||
hx-get="/troskovi" hx-trigger="keyup[target.value.length==0||target.value.length>=3] changed delay:150ms, search"
|
||||
hx-target="#troskovi-rezultati" hx-select="#troskovi-rezultati" hx-swap="innerHTML" hx-push-url="true">
|
||||
</form>
|
||||
|
||||
<div id="troskovi-rezultati">
|
||||
|
||||
<!-- desktop tabela -->
|
||||
<div class="kartica troskovi-tabela animiraj" style="padding:0;overflow:hidden;">
|
||||
<div style="overflow-x:auto;">
|
||||
<table class="tabela">
|
||||
<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);">Šifra</th>
|
||||
<th style="padding:12px 16px;text-align:left;font-size:12px;font-weight:500;color:var(--tekst-sporedni);">Naziv</th>
|
||||
<th style="padding:12px 16px;text-align:right;font-size:12px;font-weight:500;color:var(--tekst-sporedni);">Podrazumevana cena</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 .Troskovi}}
|
||||
<tr class="animiraj red-tabele">
|
||||
<td style="padding:12px 16px;font-size:13px;font-family:monospace;white-space:nowrap;color:var(--tekst-sporedni);">{{if .Sifra}}{{.Sifra}}{{else}}—{{end}}</td>
|
||||
<td style="padding:12px 16px;font-size:14px;color:var(--tekst-glavni);font-weight:500;">{{.Naziv}}</td>
|
||||
<td style="padding:12px 16px;text-align:right;font-size:14px;color:var(--tekst-glavni);">{{dinari .Cena}} din</td>
|
||||
<td style="padding:12px 16px;text-align:center;">
|
||||
<div style="display:flex;align-items:center;justify-content:center;gap:8px;">
|
||||
<a href="/troskovi/izmeni/{{.ID}}" class="btn-primarno-malo">Izmeni</a>
|
||||
{{if index $.Dozvole "artikal.obrisi"}}
|
||||
<form method="POST" action="/troskovi/obrisi/{{.ID}}" style="display:inline;">
|
||||
<input type="hidden" name="_csrf" value="{{$.CsrfToken}}">
|
||||
<button type="submit" class="btn-obrisi-malo"
|
||||
data-potvrda="Obrisati trošak '{{.Naziv}}'?">Obriši</button>
|
||||
</form>
|
||||
{{end}}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr>
|
||||
<td colspan="4" style="padding:32px;text-align:center;font-size:14px;color:var(--tekst-sporedni);">
|
||||
{{if $.Pretraga}}Za datu pretragu nema troškova u bazi.{{else}}Nema troškova. <a href="/troskovi/novi" style="color:var(--sb-akcent);">Dodaj prvi trošak.</a>{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- mobilne kartice -->
|
||||
<div class="troskovi-kartice">
|
||||
{{range .Troskovi}}
|
||||
<div class="kartica animiraj">
|
||||
<div class="red-izmedju" style="align-items:flex-start;gap:12px;margin-bottom:8px;">
|
||||
<div>
|
||||
<div style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">{{.Naziv}}</div>
|
||||
{{if .Sifra}}<div style="font-size:12px;color:var(--tekst-sporedni);margin-top:2px;font-family:monospace;">{{.Sifra}}</div>{{end}}
|
||||
</div>
|
||||
<div style="font-size:15px;font-weight:600;color:var(--sb-akcent);white-space:nowrap;">{{dinari .Cena}} din</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;">
|
||||
<a href="/troskovi/izmeni/{{.ID}}" class="btn-primarno-malo">Izmeni</a>
|
||||
{{if index $.Dozvole "artikal.obrisi"}}
|
||||
<form method="POST" action="/troskovi/obrisi/{{.ID}}" style="display:inline;">
|
||||
<input type="hidden" name="_csrf" value="{{$.CsrfToken}}">
|
||||
<button type="submit" class="btn-obrisi-malo" data-potvrda="Obrisati trošak '{{.Naziv}}'?">Obriši</button>
|
||||
</form>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div style="padding:32px;text-align:center;font-size:14px;color:var(--tekst-sporedni);">
|
||||
{{if $.Pretraga}}Za datu pretragu nema troškova u bazi.{{else}}Nema troškova. <a href="/troskovi/novi" style="color:var(--sb-akcent);">Dodaj prvi trošak.</a>{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,62 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "naslov"}}{{if .Izmena}}Izmena troška{{else}}Nov trošak{{end}} — NTech{{end}}
|
||||
|
||||
{{define "sadrzaj"}}
|
||||
<div class="kolona" style="gap:16px;max-width:680px;">
|
||||
|
||||
<a href="/troskovi" class="nazad-link">
|
||||
<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" aria-hidden="true"><polyline points="15 18 9 12 15 6"/></svg>
|
||||
Nazad na troškove
|
||||
</a>
|
||||
|
||||
{{if .Greska}}
|
||||
<div class="poruka-greska">{{.Greska}}</div>
|
||||
{{end}}
|
||||
|
||||
<form method="POST" action="{{if .Izmena}}/troskovi/izmeni/{{.Trosak.ID}}{{else}}/troskovi/novi{{end}}">
|
||||
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
|
||||
<div class="kartica animiraj">
|
||||
<div style="margin-bottom:16px;padding-bottom:12px;border-bottom:0.5px solid var(--ivica);">
|
||||
<span style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">{{if .Izmena}}Izmena troška{{else}}Nov trošak{{end}}</span>
|
||||
</div>
|
||||
|
||||
<div class="kolona" style="gap:14px;">
|
||||
<div class="forma-grid-2">
|
||||
<div>
|
||||
<label class="polje-labela">Naziv troška</label>
|
||||
<input type="text" name="naziv" value="{{.Trosak.Naziv}}" style="width:100%;"
|
||||
placeholder="npr. Prevoz">
|
||||
</div>
|
||||
<div>
|
||||
<label class="polje-labela">Šifra</label>
|
||||
<input type="text" name="sifra" value="{{.Trosak.Sifra}}" style="width:100%;"
|
||||
placeholder="npr. TRO-001">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="forma-grid-2">
|
||||
<div>
|
||||
<label class="polje-labela">Podrazumevana cena (din)</label>
|
||||
<input type="number" name="cena" min="0" step="0.01"
|
||||
value="{{if .Trosak.Cena}}{{.Trosak.Cena}}{{end}}" placeholder="0" style="width:100%;">
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="polje-labela">Opis</label>
|
||||
<textarea name="opis" rows="2" placeholder="Kratak opis troška..."
|
||||
style="width:100%;resize:vertical;">{{.Trosak.Opis}}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;justify-content:flex-end;gap:10px;margin-top:20px;">
|
||||
<a href="/troskovi" class="btn-sekundarno">Odustani</a>
|
||||
<button type="submit" class="btn-primarno">Sačuvaj trošak</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user