Compare commits
17 Commits
057c17dcdd
...
880456a5ba
| Author | SHA1 | Date | |
|---|---|---|---|
|
880456a5ba
|
|||
|
32d7813be6
|
|||
|
10f62abf84
|
|||
|
07b851f0cf
|
|||
|
cb192a15e1
|
|||
|
41e6282404
|
|||
|
b65fb02146
|
|||
|
0f4056bd03
|
|||
|
5f017fd7ed
|
|||
|
86cbace213
|
|||
|
a3c68632be
|
|||
|
4cf061e89a
|
|||
|
8048834f87
|
|||
|
695bb3e617
|
|||
|
bdb0f4b1ae
|
|||
|
9e4d658d05
|
|||
|
a20d2baae2
|
@@ -300,6 +300,7 @@ func main() {
|
||||
r.With(modul("pdv"), doz("pdv.obrisi")).Post("/pdv/kpr/obrisi/{id}", h.ObrisiPdvKpr)
|
||||
r.With(modul("pdv")).Get("/pdv/obracun", h.PdvObracunStranica)
|
||||
r.Get("/magacin", h.Magacin)
|
||||
r.Get("/magacin/kartica/{id}", h.MagacinskaKartica)
|
||||
r.Get("/magacin/novi", h.NoviArtikal)
|
||||
r.With(doz("artikal.dodaj")).Post("/magacin/novi", h.SacuvajArtikal)
|
||||
r.Get("/magacin/izmeni/{id}", h.IzmeniArtikal)
|
||||
@@ -336,9 +337,16 @@ func main() {
|
||||
r.With(doz("servis.izmeni")).Post("/servis/izmeni/{id}", h.SacuvajIzmenaNaloga)
|
||||
r.With(doz("servis.obrisi")).Post("/servis/obrisi/{id}", h.ObrisiNalog)
|
||||
r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "servis.pregled")).Get("/servis/{id}", h.DetaljiNaloga)
|
||||
r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "servis.pregled")).Get("/servis/{id}/stampa", h.StampaServisa)
|
||||
r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "servis.pregled")).Get("/servis/{id}/otpremnica", h.StampaOtpremnice)
|
||||
r.With(doz("servis.izmeni")).Post("/servis/{id}/status", h.PromeniStatus)
|
||||
r.With(doz("servis.izmeni")).Post("/servis/{id}/delovi", h.DodajDeloNalogu)
|
||||
r.With(doz("servis.izmeni")).Post("/servis/{id}/delovi/{deo_id}/obrisi", h.ObrisiDeloNaloga)
|
||||
r.Get("/izvestaji", h.Izvestaji)
|
||||
r.Get("/izvestaji/prometni-list", h.PrometniListMagacina)
|
||||
r.Get("/izvestaji/stanje-zaliha", h.StanjeZalihaIzvestaj)
|
||||
r.Get("/izvestaji/popis", h.Popis)
|
||||
r.With(doz("artikal.izmeni")).Post("/izvestaji/popis", h.SacuvajPopis)
|
||||
r.With(ntechmw.RequireDozvola(h.DozvoleRepo.ImaDozvolu, "prodaja.pregled")).Get("/prodaja", h.Prodaja)
|
||||
r.Get("/prodaja/nova", h.NovaProdaja)
|
||||
r.With(doz("prodaja.dodaj")).Post("/prodaja/nova", h.SacuvajProdaju)
|
||||
|
||||
@@ -17,6 +17,7 @@ require (
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
|
||||
github.com/stretchr/testify v1.11.1 // indirect
|
||||
golang.org/x/sys v0.45.0 // indirect
|
||||
golang.org/x/tools v0.44.0 // indirect
|
||||
|
||||
@@ -26,6 +26,8 @@ github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
|
||||
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
|
||||
@@ -17,6 +17,10 @@ type ArtikalRepository interface {
|
||||
AzurirajCene(ctx context.Context, id int64, nabavna, prodajna float64) error
|
||||
PremestiKategoriju(ctx context.Context, id int64, kategorijaID *int64) error
|
||||
Obrisi(ctx context.Context, id int64) error
|
||||
// SledecaSifra vraća predlog sledeće auto-šifre (npr. ART-00042)
|
||||
SledecaSifra(ctx context.Context) (string, error)
|
||||
// KorigujKolicinu postavlja novu količinu artikla i upisuje korekciju u magacinske_promene
|
||||
KorigujKolicinu(ctx context.Context, artikalID int64, novaKolicina int, korisnikID *int64, napomena string) error
|
||||
}
|
||||
|
||||
// KategorijaRepository definiše operacije nad kategorijama
|
||||
@@ -110,6 +114,7 @@ type ServisRepository interface {
|
||||
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
|
||||
AzurirajStatus(ctx context.Context, id int64, status string) error
|
||||
Obrisi(ctx context.Context, id int64) error
|
||||
SledeciBroj(ctx context.Context) (string, error)
|
||||
}
|
||||
@@ -220,6 +225,9 @@ type IzvestajRepository interface {
|
||||
StariOtvoreniNalozi(ctx context.Context) ([]model.StariNalogRed, error)
|
||||
TopArtikli(ctx context.Context, limit int) ([]model.TopArtikalRed, error)
|
||||
TopKlijenti(ctx context.Context, limit int) ([]model.TopKlijentRed, error)
|
||||
// magacinski izveštaji
|
||||
PrometniList(ctx context.Context, od, do time.Time) ([]model.PrometniRed, error)
|
||||
StanjeZaliha(ctx context.Context) ([]model.StanjeZalihaRed, error)
|
||||
}
|
||||
|
||||
// PodsetnikRepository definiše operacije nad podsetnicima
|
||||
|
||||
@@ -23,7 +23,7 @@ func NoviArtikalRepo(db *sql.DB) *ArtikalRepo {
|
||||
func (r *ArtikalRepo) Lista(ctx context.Context, filter db.ArtikalFilter) ([]model.ArtikalSaKategorijom, error) {
|
||||
upit := `
|
||||
SELECT
|
||||
a.id, a.kategorija_id, a.naziv, a.opis,
|
||||
a.id, a.kategorija_id, a.sifra, a.barkod, a.naziv, a.opis,
|
||||
a.kolicina, a.kolicina_min, a.lokacija,
|
||||
a.nabavna_cena, a.prodajna_cena, a.pdv_stopa, a.marza, a.napomena, a.datum_unosa,
|
||||
COALESCE(k.naziv, '') as kategorija_naziv, k.marza as kategorija_marza
|
||||
@@ -34,9 +34,9 @@ func (r *ArtikalRepo) Lista(ctx context.Context, filter db.ArtikalFilter) ([]mod
|
||||
args := []any{}
|
||||
|
||||
if filter.Pretraga != "" {
|
||||
upit += " AND (a.naziv LIKE ? OR a.lokacija LIKE ? OR k.naziv LIKE ?)"
|
||||
upit += " AND (a.naziv LIKE ? OR a.sifra LIKE ? OR a.barkod LIKE ? OR a.lokacija LIKE ? OR k.naziv LIKE ?)"
|
||||
t := "%" + filter.Pretraga + "%"
|
||||
args = append(args, t, t, t)
|
||||
args = append(args, t, t, t, t, t)
|
||||
}
|
||||
|
||||
if filter.KategorijaID != nil {
|
||||
@@ -60,10 +60,11 @@ func (r *ArtikalRepo) Lista(ctx context.Context, filter db.ArtikalFilter) ([]mod
|
||||
for redovi.Next() {
|
||||
var a model.ArtikalSaKategorijom
|
||||
var kategorijaID sql.NullInt64
|
||||
var sifra, barkod sql.NullString
|
||||
var marza, katMarza sql.NullFloat64
|
||||
|
||||
err := redovi.Scan(
|
||||
&a.ID, &kategorijaID, &a.Naziv, &a.Opis,
|
||||
&a.ID, &kategorijaID, &sifra, &barkod, &a.Naziv, &a.Opis,
|
||||
&a.Kolicina, &a.KolicinMin, &a.Lokacija,
|
||||
&a.NabavnaCena, &a.ProdajnaCena, &a.PdvStopa, &marza, &a.Napomena, &a.DatumUnosa,
|
||||
&a.KategorijaNaziv, &katMarza,
|
||||
@@ -75,6 +76,12 @@ func (r *ArtikalRepo) Lista(ctx context.Context, filter db.ArtikalFilter) ([]mod
|
||||
if kategorijaID.Valid {
|
||||
a.KategorijaID = &kategorijaID.Int64
|
||||
}
|
||||
if sifra.Valid {
|
||||
a.Sifra = sifra.String
|
||||
}
|
||||
if barkod.Valid {
|
||||
a.Barkod = barkod.String
|
||||
}
|
||||
if marza.Valid {
|
||||
a.Marza = &marza.Float64
|
||||
}
|
||||
@@ -94,13 +101,14 @@ func (r *ArtikalRepo) Lista(ctx context.Context, filter db.ArtikalFilter) ([]mod
|
||||
func (r *ArtikalRepo) DohvatiID(ctx context.Context, id int64) (*model.Artikal, error) {
|
||||
var a model.Artikal
|
||||
var kategorijaID sql.NullInt64
|
||||
var sifra, barkod sql.NullString
|
||||
var marza sql.NullFloat64
|
||||
|
||||
err := r.db.QueryRowContext(ctx, `
|
||||
SELECT id, kategorija_id, naziv, opis, kolicina, kolicina_min,
|
||||
SELECT id, kategorija_id, sifra, barkod, naziv, opis, kolicina, kolicina_min,
|
||||
lokacija, nabavna_cena, prodajna_cena, pdv_stopa, marza, napomena, datum_unosa
|
||||
FROM artikli WHERE id = ?`, id).Scan(
|
||||
&a.ID, &kategorijaID, &a.Naziv, &a.Opis,
|
||||
&a.ID, &kategorijaID, &sifra, &barkod, &a.Naziv, &a.Opis,
|
||||
&a.Kolicina, &a.KolicinMin, &a.Lokacija,
|
||||
&a.NabavnaCena, &a.ProdajnaCena, &a.PdvStopa, &marza, &a.Napomena, &a.DatumUnosa,
|
||||
)
|
||||
@@ -111,6 +119,12 @@ func (r *ArtikalRepo) DohvatiID(ctx context.Context, id int64) (*model.Artikal,
|
||||
if kategorijaID.Valid {
|
||||
a.KategorijaID = &kategorijaID.Int64
|
||||
}
|
||||
if sifra.Valid {
|
||||
a.Sifra = sifra.String
|
||||
}
|
||||
if barkod.Valid {
|
||||
a.Barkod = barkod.String
|
||||
}
|
||||
if marza.Valid {
|
||||
a.Marza = &marza.Float64
|
||||
}
|
||||
@@ -120,12 +134,20 @@ func (r *ArtikalRepo) DohvatiID(ctx context.Context, id int64) (*model.Artikal,
|
||||
|
||||
// Kreiraj dodaje novi artikal u bazu
|
||||
func (r *ArtikalRepo) Kreiraj(ctx context.Context, a *model.Artikal) (int64, error) {
|
||||
var sifra, barkod any
|
||||
if a.Sifra != "" {
|
||||
sifra = a.Sifra
|
||||
}
|
||||
if a.Barkod != "" {
|
||||
barkod = a.Barkod
|
||||
}
|
||||
|
||||
rezultat, err := r.db.ExecContext(ctx, `
|
||||
INSERT INTO artikli
|
||||
(kategorija_id, naziv, opis, kolicina, kolicina_min, lokacija,
|
||||
(kategorija_id, sifra, barkod, naziv, opis, kolicina, kolicina_min, lokacija,
|
||||
nabavna_cena, prodajna_cena, pdv_stopa, marza, napomena)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
a.KategorijaID, a.Naziv, a.Opis, a.Kolicina, a.KolicinMin,
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
a.KategorijaID, sifra, barkod, a.Naziv, a.Opis, a.Kolicina, a.KolicinMin,
|
||||
a.Lokacija, a.NabavnaCena, a.ProdajnaCena, a.PdvStopa, a.Marza, a.Napomena,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -142,13 +164,21 @@ func (r *ArtikalRepo) Kreiraj(ctx context.Context, a *model.Artikal) (int64, err
|
||||
|
||||
// Izmeni ažurira postojeći artikal
|
||||
func (r *ArtikalRepo) Izmeni(ctx context.Context, a *model.Artikal) error {
|
||||
var sifra, barkod any
|
||||
if a.Sifra != "" {
|
||||
sifra = a.Sifra
|
||||
}
|
||||
if a.Barkod != "" {
|
||||
barkod = a.Barkod
|
||||
}
|
||||
|
||||
_, err := r.db.ExecContext(ctx, `
|
||||
UPDATE artikli SET
|
||||
kategorija_id = ?, naziv = ?, opis = ?, kolicina = ?,
|
||||
kategorija_id = ?, sifra = ?, barkod = ?, naziv = ?, opis = ?, kolicina = ?,
|
||||
kolicina_min = ?, lokacija = ?,
|
||||
nabavna_cena = ?, prodajna_cena = ?, pdv_stopa = ?, marza = ?, napomena = ?
|
||||
WHERE id = ?`,
|
||||
a.KategorijaID, a.Naziv, a.Opis, a.Kolicina,
|
||||
a.KategorijaID, sifra, barkod, a.Naziv, a.Opis, a.Kolicina,
|
||||
a.KolicinMin, a.Lokacija,
|
||||
a.NabavnaCena, a.ProdajnaCena, a.PdvStopa, a.Marza, a.Napomena, a.ID,
|
||||
)
|
||||
@@ -159,6 +189,16 @@ func (r *ArtikalRepo) Izmeni(ctx context.Context, a *model.Artikal) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SledecaSifra vraća predlog sledeće auto-šifre u formatu ART-XXXXX
|
||||
func (r *ArtikalRepo) SledecaSifra(ctx context.Context) (string, error) {
|
||||
var n int64
|
||||
err := r.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM artikli").Scan(&n)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("ntech: ArtikalRepo.SledecaSifra: %w", err)
|
||||
}
|
||||
return fmt.Sprintf("ART-%05d", n+1), nil
|
||||
}
|
||||
|
||||
// AzurirajCene menja samo nabavnu i prodajnu cenu artikla (kalkulacija pri prijemu robe).
|
||||
func (r *ArtikalRepo) AzurirajCene(ctx context.Context, id int64, nabavna, prodajna float64) error {
|
||||
_, err := r.db.ExecContext(ctx,
|
||||
@@ -189,3 +229,32 @@ func (r *ArtikalRepo) Obrisi(ctx context.Context, id int64) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// KorigujKolicinu postavlja novu količinu i upisuje korekciju u magacinske_promene
|
||||
func (r *ArtikalRepo) KorigujKolicinu(ctx context.Context, artikalID int64, novaKolicina int, korisnikID *int64, napomena string) error {
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntech: ArtikalRepo.KorigujKolicinu: begin: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
var staraCena float64
|
||||
var staraKolicina int
|
||||
err = tx.QueryRowContext(ctx, "SELECT kolicina, nabavna_cena FROM artikli WHERE id = ?", artikalID).
|
||||
Scan(&staraKolicina, &staraCena)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntech: ArtikalRepo.KorigujKolicinu: dohvati: %w", err)
|
||||
}
|
||||
|
||||
if _, err = tx.ExecContext(ctx, "UPDATE artikli SET kolicina = ? WHERE id = ?", novaKolicina, artikalID); err != nil {
|
||||
return fmt.Errorf("ntech: ArtikalRepo.KorigujKolicinu: update: %w", err)
|
||||
}
|
||||
|
||||
promena := novaKolicina - staraKolicina
|
||||
if err = zabeleziMagacinPromenu(ctx, tx, artikalID, model.PromenaKorekcija, promena,
|
||||
staraKolicina, novaKolicina, 0, korisnikID, napomena); err != nil {
|
||||
return fmt.Errorf("ntech: ArtikalRepo.KorigujKolicinu: %w", err)
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"ntech/internal/model"
|
||||
)
|
||||
@@ -147,6 +148,7 @@ func (r *sqliteIzvestajRepo) MesecniPrihodServis(ctx context.Context) ([]model.M
|
||||
SELECT substr(datum_zavrsetka, 1, 7), SUM(cena_konacna)
|
||||
FROM servisni_nalozi
|
||||
WHERE datum_zavrsetka IS NOT NULL
|
||||
AND status = 'Preuzeto'
|
||||
AND substr(datum_zavrsetka, 1, 10) >= date('now', '-11 months', 'start of month')
|
||||
GROUP BY substr(datum_zavrsetka, 1, 7)`, "MesecniPrihodServis")
|
||||
}
|
||||
@@ -232,3 +234,59 @@ func (r *sqliteIzvestajRepo) TopKlijenti(ctx context.Context, limit int) ([]mode
|
||||
}
|
||||
return lista, rows.Err()
|
||||
}
|
||||
|
||||
// PrometniList vraća sve magacinske promene u zadatom periodu
|
||||
func (r *sqliteIzvestajRepo) PrometniList(ctx context.Context, od, do time.Time) ([]model.PrometniRed, error) {
|
||||
rows, err := r.db.QueryContext(ctx, `
|
||||
SELECT mp.datum, a.naziv, COALESCE(a.sifra, ''), mp.tip_promene,
|
||||
mp.promena_kolicine, mp.stanje_pre, mp.stanje_posle,
|
||||
COALESCE(mp.napomena, '')
|
||||
FROM magacinske_promene mp
|
||||
JOIN artikli a ON a.id = mp.artikal_id
|
||||
WHERE DATE(mp.datum) >= DATE(?) AND DATE(mp.datum) <= DATE(?)
|
||||
ORDER BY mp.datum ASC`,
|
||||
od.Format("2006-01-02"), do.Format("2006-01-02"),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ntech: IzvestajRepo.PrometniList: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var lista []model.PrometniRed
|
||||
for rows.Next() {
|
||||
var p model.PrometniRed
|
||||
if err := rows.Scan(&p.Datum, &p.ArtikalNaziv, &p.ArtikalSifra, &p.TipPromene,
|
||||
&p.PromenaKolicine, &p.StanjePre, &p.StanjePosle, &p.Napomena); err != nil {
|
||||
return nil, fmt.Errorf("ntech: IzvestajRepo.PrometniList: scan: %w", err)
|
||||
}
|
||||
lista = append(lista, p)
|
||||
}
|
||||
return lista, rows.Err()
|
||||
}
|
||||
|
||||
// StanjeZaliha vraća trenutno stanje svih artikala sa vrednostima
|
||||
func (r *sqliteIzvestajRepo) StanjeZaliha(ctx context.Context) ([]model.StanjeZalihaRed, error) {
|
||||
rows, err := r.db.QueryContext(ctx, `
|
||||
SELECT a.naziv, COALESCE(a.sifra, ''), COALESCE(k.naziv, ''),
|
||||
a.kolicina, a.kolicina_min, a.nabavna_cena, a.prodajna_cena,
|
||||
a.kolicina * a.nabavna_cena AS vrednost
|
||||
FROM artikli a
|
||||
LEFT JOIN kategorije k ON k.id = a.kategorija_id
|
||||
ORDER BY k.naziv ASC, a.naziv ASC`,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ntech: IzvestajRepo.StanjeZaliha: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var lista []model.StanjeZalihaRed
|
||||
for rows.Next() {
|
||||
var s model.StanjeZalihaRed
|
||||
if err := rows.Scan(&s.Naziv, &s.Sifra, &s.Kategorija,
|
||||
&s.Kolicina, &s.KolicinMin, &s.NabavnaCena, &s.ProdajnaCena, &s.VrednostZalihe); err != nil {
|
||||
return nil, fmt.Errorf("ntech: IzvestajRepo.StanjeZaliha: scan: %w", err)
|
||||
}
|
||||
lista = append(lista, s)
|
||||
}
|
||||
return lista, rows.Err()
|
||||
}
|
||||
|
||||
@@ -152,6 +152,24 @@ func (r *ServisRepo) Izmeni(ctx context.Context, n *model.ServisniNalog) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// AzurirajStatus menja samo status naloga; ako nalog prelazi u završno stanje
|
||||
// i datum_zavrsetka još nije postavljen, automatski ga postavlja na danas.
|
||||
func (r *ServisRepo) AzurirajStatus(ctx context.Context, id int64, status string) error {
|
||||
var upit string
|
||||
if status == model.StatusZavrseno || status == model.StatusPreuzeto {
|
||||
upit = `UPDATE servisni_nalozi SET status = ?,
|
||||
datum_zavrsetka = COALESCE(datum_zavrsetka, date('now', 'localtime'))
|
||||
WHERE id = ?`
|
||||
} else {
|
||||
upit = `UPDATE servisni_nalozi SET status = ? WHERE id = ?`
|
||||
}
|
||||
_, err := r.db.ExecContext(ctx, upit, status, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntech: ServisRepo.AzurirajStatus: %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)
|
||||
|
||||
@@ -6,9 +6,12 @@ import (
|
||||
"html/template"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
appdbPkg "ntech/internal/db"
|
||||
"ntech/internal/db/sqlite"
|
||||
"ntech/internal/middleware"
|
||||
"ntech/internal/model"
|
||||
)
|
||||
|
||||
@@ -202,3 +205,187 @@ func (h *Handler) Izvestaji(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
h.renderujTemplate(w, "izvestaji", podaci)
|
||||
}
|
||||
|
||||
// PodaciPrometногLista su podaci za prometni list magacina
|
||||
type PodaciPrometногLista struct {
|
||||
model.PodaciStranice
|
||||
Promene []model.PrometniRed
|
||||
Od string
|
||||
Do string
|
||||
Ukupno int
|
||||
}
|
||||
|
||||
// PrometniListMagacina renderuje prometni list magacina za odabrani period
|
||||
func (h *Handler) PrometniListMagacina(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := h.zahtevajDozvolu(w, r, "izvestaj.pregled"); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
danas := time.Now()
|
||||
odStr := r.URL.Query().Get("od")
|
||||
doStr := r.URL.Query().Get("do")
|
||||
|
||||
if odStr == "" {
|
||||
odStr = danas.Format("2006-01-02")[:7] + "-01"
|
||||
}
|
||||
if doStr == "" {
|
||||
doStr = danas.Format("2006-01-02")
|
||||
}
|
||||
|
||||
od, err := time.Parse("2006-01-02", odStr)
|
||||
if err != nil {
|
||||
od = time.Now()
|
||||
}
|
||||
do, err := time.Parse("2006-01-02", doStr)
|
||||
if err != nil {
|
||||
do = time.Now()
|
||||
}
|
||||
|
||||
promene, err := h.IzvestajRepo.PrometniList(r.Context(), od, do)
|
||||
if err != nil {
|
||||
slog.Error("prometni list: greška", "error", err)
|
||||
promene = nil
|
||||
}
|
||||
|
||||
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||
ps := h.popuniPodaciStranice(r, podesavanja)
|
||||
ps.Stranica = "izvestaji"
|
||||
ps.NaslovStranice = "Prometni list"
|
||||
|
||||
h.renderujTemplate(w, "prometni_list", PodaciPrometногLista{
|
||||
PodaciStranice: ps,
|
||||
Promene: promene,
|
||||
Od: odStr,
|
||||
Do: doStr,
|
||||
Ukupno: len(promene),
|
||||
})
|
||||
}
|
||||
|
||||
// PodaciStanjaZaliha su podaci za izveštaj o stanju zaliha
|
||||
type PodaciStanjaZaliha struct {
|
||||
model.PodaciStranice
|
||||
Zalihe []model.StanjeZalihaRed
|
||||
UkupnaVrednost float64
|
||||
BrojArtikala int
|
||||
}
|
||||
|
||||
// StanjeZalihaIzvestaj renderuje izveštaj o trenutnom stanju zaliha
|
||||
func (h *Handler) StanjeZalihaIzvestaj(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := h.zahtevajDozvolu(w, r, "izvestaj.pregled"); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
zalihe, err := h.IzvestajRepo.StanjeZaliha(r.Context())
|
||||
if err != nil {
|
||||
slog.Error("stanje zaliha: greška", "error", err)
|
||||
zalihe = nil
|
||||
}
|
||||
|
||||
var ukupnaVrednost float64
|
||||
for _, z := range zalihe {
|
||||
ukupnaVrednost += z.VrednostZalihe
|
||||
}
|
||||
|
||||
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||
ps := h.popuniPodaciStranice(r, podesavanja)
|
||||
ps.Stranica = "izvestaji"
|
||||
ps.NaslovStranice = "Stanje zaliha"
|
||||
|
||||
h.renderujTemplate(w, "stanje_zaliha", PodaciStanjaZaliha{
|
||||
PodaciStranice: ps,
|
||||
Zalihe: zalihe,
|
||||
UkupnaVrednost: ukupnaVrednost,
|
||||
BrojArtikala: len(zalihe),
|
||||
})
|
||||
}
|
||||
|
||||
// PodaciPopisa su podaci za stranicu popisa
|
||||
type PodaciPopisa struct {
|
||||
model.PodaciStranice
|
||||
Artikli []model.ArtikalSaKategorijom
|
||||
Sacuvano bool
|
||||
Greska string
|
||||
}
|
||||
|
||||
// Popis prikazuje formu za unos stvarnog stanja (inventuru)
|
||||
func (h *Handler) Popis(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := h.zahtevajDozvolu(w, r, "artikal.izmeni"); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
artikli, err := h.Artikli.Lista(r.Context(), appdbPkg.ArtikalFilter{})
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri učitavanju artikala", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||
ps := h.popuniPodaciStranice(r, podesavanja)
|
||||
ps.Stranica = "izvestaji"
|
||||
ps.NaslovStranice = "Popis"
|
||||
|
||||
h.renderujTemplate(w, "popis", PodaciPopisa{
|
||||
PodaciStranice: ps,
|
||||
Artikli: artikli,
|
||||
Sacuvano: r.URL.Query().Get("sacuvano") == "1",
|
||||
})
|
||||
}
|
||||
|
||||
// SacuvajPopis prima POST formu sa prebrojenim količinama i upisuje korekcije
|
||||
func (h *Handler) SacuvajPopis(w http.ResponseWriter, r *http.Request) {
|
||||
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "artikal.izmeni") {
|
||||
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
artikli, err := h.Artikli.Lista(r.Context(), appdbPkg.ArtikalFilter{})
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri učitavanju artikala", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
napomena := r.FormValue("napomena")
|
||||
if napomena == "" {
|
||||
napomena = "Godišnji popis"
|
||||
}
|
||||
|
||||
var greskaBroj int
|
||||
for _, a := range artikli {
|
||||
kljuc := fmt.Sprintf("kolicina_%d", a.ID)
|
||||
vr := r.FormValue(kljuc)
|
||||
if vr == "" {
|
||||
continue
|
||||
}
|
||||
nova, err := strconv.Atoi(vr)
|
||||
if err != nil || nova < 0 {
|
||||
continue
|
||||
}
|
||||
if nova == a.Kolicina {
|
||||
continue
|
||||
}
|
||||
if err := h.Artikli.KorigujKolicinu(r.Context(), a.ID, nova, &k.ID, napomena); err != nil {
|
||||
slog.Error("popis: korekcija artikla", "id", a.ID, "error", err)
|
||||
greskaBroj++
|
||||
}
|
||||
}
|
||||
|
||||
if greskaBroj > 0 {
|
||||
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||
ps := h.popuniPodaciStranice(r, podesavanja)
|
||||
ps.Stranica = "izvestaji"
|
||||
ps.NaslovStranice = "Popis"
|
||||
h.renderujTemplate(w, "popis", PodaciPopisa{
|
||||
PodaciStranice: ps,
|
||||
Artikli: artikli,
|
||||
Greska: fmt.Sprintf("Došlo je do greške pri upisivanju %d artikala.", greskaBroj),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/izvestaji/popis?sacuvano=1", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
@@ -19,10 +19,10 @@ var saSidebar = []string{
|
||||
"admin_korisnici", "admin_profil", "admin_login_istorija", "admin_dozvole",
|
||||
"dashboard",
|
||||
"dobavljaci", "dobavljac_forma",
|
||||
"izvestaji",
|
||||
"izvestaji", "prometni_list", "stanje_zaliha", "popis",
|
||||
"kategorije",
|
||||
"klijenti", "klijent_forma",
|
||||
"magacin", "magacin_forma",
|
||||
"magacin", "magacin_forma", "magacin_kartica",
|
||||
"nabavke", "nabavka_forma", "nabavka_detalji",
|
||||
"podesavanja", "podesavanja_opste", "podesavanja_izgled", "podesavanja_sistem", "podesavanja_servis",
|
||||
"pdv_stope",
|
||||
@@ -38,7 +38,7 @@ var saSidebar = []string{
|
||||
|
||||
// standalone su šabloni bez base layouta
|
||||
var standaloneIme = []string{
|
||||
"prijava", "setup", "totp_provera", "prodaja_stampa",
|
||||
"prijava", "setup", "totp_provera", "prodaja_stampa", "servis_stampa", "servis_otpremnica",
|
||||
}
|
||||
|
||||
// sablonskeFunkcije su pomoćne funkcije dostupne u svim šablonima.
|
||||
|
||||
@@ -123,3 +123,47 @@ func (h *Handler) ObrisiArtikal(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
http.Redirect(w, r, "/magacin?obrisan=1", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// PodaciMagacinskeKartice su podaci za karticu jednog artikla
|
||||
type PodaciMagacinskeKartice struct {
|
||||
model.PodaciStranice
|
||||
Artikal model.Artikal
|
||||
Promene []model.MagacinskaPromenaSaDetaljem
|
||||
}
|
||||
|
||||
// MagacinskaKartica prikazuje sve promene stanja za jedan artikal
|
||||
func (h *Handler) MagacinskaKartica(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||
if err != nil {
|
||||
http.Error(w, "Neispravan ID artikla", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
artikal, err := h.Artikli.DohvatiID(r.Context(), id)
|
||||
if err != nil {
|
||||
http.Error(w, "Artikal nije pronađen", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
promene, err := h.MagacinskePromeneRepo.Lista(r.Context(), &id, 0)
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri učitavanju promena", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
ps := h.popuniPodaciStranice(r, podesavanja)
|
||||
ps.Stranica = "magacin"
|
||||
ps.NaslovStranice = "Kartica: " + artikal.Naziv
|
||||
|
||||
h.renderujTemplate(w, "magacin_kartica", PodaciMagacinskeKartice{
|
||||
PodaciStranice: ps,
|
||||
Artikal: *artikal,
|
||||
Promene: promene,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -37,12 +37,19 @@ func (h *Handler) NoviArtikal(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
predlogSifre, err := h.Artikli.SledecaSifra(r.Context())
|
||||
if err != nil {
|
||||
slog.Error("greška pri generisanju predloga šifre", "err", err)
|
||||
predlogSifre = "ART-00001"
|
||||
}
|
||||
|
||||
ps := h.popuniPodaciStranice(r, podesavanja)
|
||||
ps.Stranica = "magacin"
|
||||
ps.NaslovStranice = "Novi artikal"
|
||||
h.renderujFormuArtikla(w, PodaciFormeArtikla{
|
||||
PodaciStranice: ps,
|
||||
Kategorije: kategorije,
|
||||
Artikal: model.Artikal{Sifra: predlogSifre},
|
||||
Izmena: false,
|
||||
})
|
||||
}
|
||||
@@ -87,6 +94,16 @@ func (h *Handler) SacuvajArtikal(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// ako korisnik nije uneo šifru, auto-generišemo po ID-u
|
||||
if artikal.Sifra == "" {
|
||||
autoSifra := fmt.Sprintf("ART-%05d", id)
|
||||
artikal.ID = id
|
||||
artikal.Sifra = autoSifra
|
||||
if err := h.Artikli.Izmeni(r.Context(), &artikal); err != nil {
|
||||
slog.Error("greška pri upisu auto-šifre", "id", id, "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
// fetch zahtev (iz modala) dobija JSON sa ID-em i nazivom novog artikla
|
||||
if r.Header.Get("X-Requested-With") == "fetch" {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -221,6 +238,8 @@ func parseFormuArtikla(r *http.Request) (model.Artikal, string) {
|
||||
|
||||
var artikal model.Artikal
|
||||
artikal.Naziv = naziv
|
||||
artikal.Sifra = r.FormValue("sifra")
|
||||
artikal.Barkod = r.FormValue("barkod")
|
||||
artikal.Opis = r.FormValue("opis")
|
||||
artikal.Lokacija = r.FormValue("lokacija")
|
||||
artikal.Napomena = r.FormValue("napomena")
|
||||
@@ -241,6 +260,14 @@ func parseFormuArtikla(r *http.Request) (model.Artikal, string) {
|
||||
artikal.KolicinMin = v
|
||||
}
|
||||
|
||||
if c := r.FormValue("nabavna_cena"); c != "" {
|
||||
v, err := strconv.ParseFloat(c, 64)
|
||||
if err != nil || v < 0 {
|
||||
return artikal, "Nabavna cena mora biti pozitivan broj."
|
||||
}
|
||||
artikal.NabavnaCena = v
|
||||
}
|
||||
|
||||
if c := r.FormValue("prodajna_cena"); c != "" {
|
||||
v, err := strconv.ParseFloat(c, 64)
|
||||
if err != nil || v < 0 {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@@ -13,6 +14,7 @@ import (
|
||||
"ntech/internal/model"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
qrcode "github.com/skip2/go-qrcode"
|
||||
)
|
||||
|
||||
// PodaciServisa su podaci za stranicu sa listom servisnih naloga
|
||||
@@ -46,6 +48,10 @@ type PodaciDetaljiNaloga struct {
|
||||
ServisniDelovi []model.ServisniDeoSaArtiklom
|
||||
Artikli []model.ArtikalSaKategorijom
|
||||
Sacuvano bool
|
||||
UkupnoDelovi float64
|
||||
UkupnoSve float64
|
||||
PreostaloSve float64
|
||||
SviStatusi []string
|
||||
}
|
||||
|
||||
// Servis renderuje listu servisnih naloga sa opcionom pretragom i filterom statusa
|
||||
@@ -355,6 +361,23 @@ func (h *Handler) DetaljiNaloga(w http.ResponseWriter, r *http.Request) {
|
||||
slog.Error("greška pri učitavanju artikala", "error", err)
|
||||
}
|
||||
|
||||
var ukupnoDelovi float64
|
||||
for _, d := range delovi {
|
||||
ukupnoDelovi += d.Ukupno()
|
||||
}
|
||||
var ukupnoSve, preostaloSve float64
|
||||
if nalog.CenaKonacna != nil {
|
||||
ukupnoSve = *nalog.CenaKonacna + ukupnoDelovi
|
||||
avans := 0.0
|
||||
if nalog.Avans != nil {
|
||||
avans = *nalog.Avans
|
||||
}
|
||||
preostaloSve = ukupnoSve - avans
|
||||
if preostaloSve < 0 {
|
||||
preostaloSve = 0
|
||||
}
|
||||
}
|
||||
|
||||
ps := h.popuniPodaciStranice(r, podesavanja)
|
||||
ps.Stranica = "servis"
|
||||
ps.NaslovStranice = "Detalji naloga"
|
||||
@@ -366,6 +389,10 @@ func (h *Handler) DetaljiNaloga(w http.ResponseWriter, r *http.Request) {
|
||||
ServisniDelovi: delovi,
|
||||
Artikli: artikli,
|
||||
Sacuvano: r.URL.Query().Get("sacuvano") == "1",
|
||||
UkupnoDelovi: ukupnoDelovi,
|
||||
UkupnoSve: ukupnoSve,
|
||||
PreostaloSve: preostaloSve,
|
||||
SviStatusi: model.SviStatusi,
|
||||
}
|
||||
|
||||
h.renderujTemplate(w, "servis_detalji", podaci)
|
||||
@@ -553,3 +580,236 @@ func parseOpcionuCenu(s string) *float64 {
|
||||
func (h *Handler) renderujFormuNaloga(w http.ResponseWriter, podaci PodaciFormeNaloga) {
|
||||
h.renderujTemplate(w, "servis_forma", podaci)
|
||||
}
|
||||
|
||||
// PodaciStampeServisa su podaci za print-friendly prikaz servisnog naloga
|
||||
type PodaciStampeServisa struct {
|
||||
Nalog model.ServisniNalog
|
||||
ServisniDelovi []model.ServisniDeoSaArtiklom
|
||||
UkupnoDelovi float64
|
||||
KlijentNaziv string
|
||||
TehnicarNaziv string
|
||||
NazivFirme string
|
||||
Podnazlov string
|
||||
Adresa string
|
||||
Telefon string
|
||||
PIB string
|
||||
QRKod string // base64 PNG QR koda sa URL-om naloga
|
||||
}
|
||||
|
||||
// StampaServisa renderuje print-friendly stranicu za servisni nalog
|
||||
func (h *Handler) StampaServisa(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
|
||||
}
|
||||
|
||||
delovi, err := h.ServisniDeloviRepo.DohvatiZaNalog(r.Context(), id)
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri učitavanju delova", http.StatusInternalServerError)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tehnicarNaziv := ""
|
||||
if nalog.TehnicarID != nil {
|
||||
tehnicar, err := h.KorisniciRepo.DohvatiPoID(r.Context(), *nalog.TehnicarID)
|
||||
if err == nil {
|
||||
tehnicarNaziv = tehnicar.KorisnickoIme
|
||||
}
|
||||
}
|
||||
|
||||
var ukupnoDelovi float64
|
||||
for _, d := range delovi {
|
||||
ukupnoDelovi += d.Ukupno()
|
||||
}
|
||||
|
||||
// QR kod sadrži URL naloga — isti host kao što korisnik koristi
|
||||
scheme := "http"
|
||||
if r.TLS != nil {
|
||||
scheme = "https"
|
||||
}
|
||||
nalogURL := scheme + "://" + r.Host + "/servis/" + strconv.FormatInt(id, 10)
|
||||
var qrKod string
|
||||
if png, err := qrcode.Encode(nalogURL, qrcode.Medium, 160); err == nil {
|
||||
qrKod = base64.StdEncoding.EncodeToString(png)
|
||||
}
|
||||
|
||||
h.renderujStandalone(w, "servis_stampa", PodaciStampeServisa{
|
||||
Nalog: *nalog,
|
||||
ServisniDelovi: delovi,
|
||||
UkupnoDelovi: ukupnoDelovi,
|
||||
KlijentNaziv: klijentNaziv,
|
||||
TehnicarNaziv: tehnicarNaziv,
|
||||
NazivFirme: podesavanja["naziv_firme"],
|
||||
Podnazlov: podesavanja["podnazlov"],
|
||||
Adresa: podesavanja["adresa"],
|
||||
Telefon: podesavanja["telefon"],
|
||||
PIB: podesavanja["pib"],
|
||||
QRKod: qrKod,
|
||||
})
|
||||
}
|
||||
|
||||
// PodaciOtpremnice su podaci za otpremnicu pri preuzimanju uređaja
|
||||
type PodaciOtpremnice struct {
|
||||
Nalog model.ServisniNalog
|
||||
ServisniDelovi []model.ServisniDeoSaArtiklom
|
||||
UkupnoDelovi float64
|
||||
PreostaloSve float64
|
||||
ImaAvans bool
|
||||
QRKod string
|
||||
Klijent *model.Klijent
|
||||
KlijentNaziv string
|
||||
TehnicarNaziv string
|
||||
NazivFirme string
|
||||
Podnazlov string
|
||||
Adresa string
|
||||
Telefon string
|
||||
PIB string
|
||||
}
|
||||
|
||||
// StampaOtpremnice renderuje otpremnicu pri preuzimanju uređaja od strane klijenta
|
||||
func (h *Handler) StampaOtpremnice(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
|
||||
}
|
||||
|
||||
delovi, err := h.ServisniDeloviRepo.DohvatiZaNalog(r.Context(), id)
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri učitavanju delova", http.StatusInternalServerError)
|
||||
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
|
||||
}
|
||||
|
||||
var klijent *model.Klijent
|
||||
klijentNaziv := ""
|
||||
if nalog.KlijentID != nil {
|
||||
k, err := h.KlijentiRepo.DohvatiID(r.Context(), *nalog.KlijentID)
|
||||
if err == nil {
|
||||
klijent = k
|
||||
if k.NazivFirme != "" {
|
||||
klijentNaziv = k.NazivFirme
|
||||
} else {
|
||||
klijentNaziv = strings.TrimSpace(k.Ime + " " + k.Prezime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tehnicarNaziv := ""
|
||||
if nalog.TehnicarID != nil {
|
||||
tehnicar, err := h.KorisniciRepo.DohvatiPoID(r.Context(), *nalog.TehnicarID)
|
||||
if err == nil {
|
||||
tehnicarNaziv = tehnicar.KorisnickoIme
|
||||
}
|
||||
}
|
||||
|
||||
var ukupnoDelovi float64
|
||||
for _, d := range delovi {
|
||||
ukupnoDelovi += d.Ukupno()
|
||||
}
|
||||
var preostaloSve float64
|
||||
var imaAvans bool
|
||||
if nalog.CenaKonacna != nil {
|
||||
ukupnoSve := *nalog.CenaKonacna + ukupnoDelovi
|
||||
avans := 0.0
|
||||
if nalog.Avans != nil && *nalog.Avans > 0 {
|
||||
avans = *nalog.Avans
|
||||
imaAvans = true
|
||||
}
|
||||
preostaloSve = ukupnoSve - avans
|
||||
if preostaloSve < 0 {
|
||||
preostaloSve = 0
|
||||
}
|
||||
}
|
||||
|
||||
nalogURL := "http"
|
||||
if r.TLS != nil {
|
||||
nalogURL += "s"
|
||||
}
|
||||
nalogURL += "://" + r.Host + "/servis/" + strconv.FormatInt(id, 10)
|
||||
var qrKodOtpr string
|
||||
if png, err := qrcode.Encode(nalogURL, qrcode.Medium, 160); err == nil {
|
||||
qrKodOtpr = base64.StdEncoding.EncodeToString(png)
|
||||
}
|
||||
|
||||
h.renderujStandalone(w, "servis_otpremnica", PodaciOtpremnice{
|
||||
Nalog: *nalog,
|
||||
ServisniDelovi: delovi,
|
||||
UkupnoDelovi: ukupnoDelovi,
|
||||
PreostaloSve: preostaloSve,
|
||||
ImaAvans: imaAvans,
|
||||
QRKod: qrKodOtpr,
|
||||
Klijent: klijent,
|
||||
KlijentNaziv: klijentNaziv,
|
||||
TehnicarNaziv: tehnicarNaziv,
|
||||
NazivFirme: podesavanja["naziv_firme"],
|
||||
Podnazlov: podesavanja["podnazlov"],
|
||||
Adresa: podesavanja["adresa"],
|
||||
Telefon: podesavanja["telefon"],
|
||||
PIB: podesavanja["pib"],
|
||||
})
|
||||
}
|
||||
|
||||
// PromeniStatus obrađuje POST /servis/{id}/status i menja samo status naloga
|
||||
func (h *Handler) PromeniStatus(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
|
||||
}
|
||||
noviStatus := strings.TrimSpace(r.FormValue("status"))
|
||||
dozvoljenStatusi := map[string]bool{}
|
||||
for _, s := range model.SviStatusi {
|
||||
dozvoljenStatusi[s] = true
|
||||
}
|
||||
if !dozvoljenStatusi[noviStatus] {
|
||||
http.Error(w, "Nepoznat status", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := h.ServisRepo.AzurirajStatus(r.Context(), id, noviStatus); err != nil {
|
||||
slog.Error("greška pri promeni statusa naloga", "id", id, "error", err)
|
||||
http.Error(w, "Greška pri promeni statusa", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(id, 10)+"?sacuvano=1", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import "time"
|
||||
type Artikal struct {
|
||||
ID int64
|
||||
KategorijaID *int64
|
||||
Sifra string
|
||||
Barkod string
|
||||
Naziv string
|
||||
Opis string
|
||||
Kolicina int
|
||||
|
||||
@@ -58,3 +58,27 @@ type TopKlijentRed struct {
|
||||
UkupnoVrednost float64
|
||||
BrojNaloga int
|
||||
}
|
||||
|
||||
// PrometniRed je jedan red prometnog lista magacina
|
||||
type PrometniRed struct {
|
||||
Datum time.Time
|
||||
ArtikalNaziv string
|
||||
ArtikalSifra string
|
||||
TipPromene string
|
||||
PromenaKolicine int
|
||||
StanjePre int
|
||||
StanjePosle int
|
||||
Napomena string
|
||||
}
|
||||
|
||||
// StanjeZalihaRed je jedan red izveštaja o stanju zaliha
|
||||
type StanjeZalihaRed struct {
|
||||
Naziv string
|
||||
Sifra string
|
||||
Kategorija string
|
||||
Kolicina int
|
||||
KolicinMin int
|
||||
NabavnaCena float64
|
||||
ProdajnaCena float64
|
||||
VrednostZalihe float64 // kolicina × nabavna_cena
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
ALTER TABLE artikli ADD COLUMN sifra TEXT;
|
||||
ALTER TABLE artikli ADD COLUMN barkod TEXT;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_artikli_sifra ON artikli(sifra) WHERE sifra IS NOT NULL;
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_artikli_barkod ON artikli(barkod) WHERE barkod IS NOT NULL;
|
||||
+21
-2
@@ -752,6 +752,9 @@ select {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* kad JS radi, .poruka-uspeh se konvertuje u toast — sakrivamo odmah da ne treptne */
|
||||
.js-on .poruka-uspeh { display: none !important; }
|
||||
|
||||
/* poruka o uspehu — konzistentna za sve teme */
|
||||
.poruka-uspeh {
|
||||
background: var(--poruka-uspeh-bg);
|
||||
@@ -790,8 +793,8 @@ select {
|
||||
animation: toastIn 0.3s ease forwards;
|
||||
max-width: 340px;
|
||||
}
|
||||
.toast-greska { background: #fef2f2; color: #dc2626; border: 0.5px solid #fca5a5; }
|
||||
.toast-uspeh { background: #f0fdf4; color: #16a34a; border: 0.5px solid #86efac; }
|
||||
.toast-greska { background: rgba(207, 87, 87, 0.12); color: var(--greska); border: 0.5px solid var(--greska); }
|
||||
.toast-uspeh { background: var(--poruka-uspeh-bg); color: var(--poruka-uspeh-boja); border: 0.5px solid var(--poruka-uspeh-boja); }
|
||||
.toast.nestaje { animation: toastOut 0.3s ease forwards; }
|
||||
@keyframes toastIn {
|
||||
from { opacity: 0; transform: translateY(12px); }
|
||||
@@ -1121,6 +1124,22 @@ select {
|
||||
.dash-stat.animiraj:nth-child(4) { animation-delay: 0.23s; }
|
||||
.dash-stat.animiraj:nth-child(5) { animation-delay: 0.28s; }
|
||||
|
||||
/* kartica koja je ujedno i link — zamena za inline text-decoration/display/cursor */
|
||||
.kartica-link { text-decoration: none; display: block; cursor: pointer; }
|
||||
|
||||
/* ikona wrapper na stat karticama dashboarda */
|
||||
.dash-ikona {
|
||||
width: 36px; height: 36px; border-radius: 8px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
margin-bottom: 10px;
|
||||
transition: filter 0.25s;
|
||||
}
|
||||
.dash-ikona-plava { background: #eff2ff; }
|
||||
.dash-ikona-zelena { background: #f0fdf4; }
|
||||
.dash-ikona-narandzasta { background: #fff7ed; }
|
||||
.dash-ikona-crvena { background: #fef2f2; }
|
||||
.dash-ikona-nebo { background: #f0f9ff; }
|
||||
|
||||
/* Bedž statusa servisnog naloga — JEDNO mesto za izgled i boje statusa (lista i detalji).
|
||||
Mora biti u main.css: HTMX navigacija odbacuje <head>, pa page <style> ne bi važio. */
|
||||
.status-badge { display: inline-block; padding: 3px 10px; border-radius: 20px; font-size: 12px; font-weight: 500; white-space: nowrap; }
|
||||
|
||||
@@ -201,6 +201,7 @@ document.addEventListener('alpine:init', () => {
|
||||
modalOpis: '',
|
||||
modalKolicina: '',
|
||||
modalKolicinaMin: '',
|
||||
modalNabavnaCena: '',
|
||||
modalCena: '',
|
||||
modalLokacija: '',
|
||||
modalNapomena: '',
|
||||
@@ -313,6 +314,7 @@ document.addEventListener('alpine:init', () => {
|
||||
this.modalOpis = ''
|
||||
this.modalKolicina = ''
|
||||
this.modalKolicinaMin = ''
|
||||
this.modalNabavnaCena = ''
|
||||
this.modalCena = ''
|
||||
this.modalLokacija = ''
|
||||
this.modalNapomena = ''
|
||||
@@ -334,6 +336,7 @@ document.addEventListener('alpine:init', () => {
|
||||
params.append('opis', this.modalOpis.trim())
|
||||
if (this.modalKolicina) params.append('kolicina', this.modalKolicina)
|
||||
if (this.modalKolicinaMin) params.append('kolicina_min', this.modalKolicinaMin)
|
||||
if (this.modalNabavnaCena) params.append('nabavna_cena', this.modalNabavnaCena)
|
||||
if (this.modalCena) params.append('prodajna_cena', this.modalCena)
|
||||
params.append('lokacija', this.modalLokacija.trim())
|
||||
params.append('napomena', this.modalNapomena.trim())
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
{{ end }}
|
||||
<div class="grid grid-cols-2 md:grid-cols-5 gap-3 mb-6">
|
||||
<!-- stat kartice -->
|
||||
<a href="/magacin" class="kartica dash-stat animiraj" style="text-decoration:none;display:block;cursor:pointer;">
|
||||
<div style="width:36px;height:36px;border-radius:8px;background:#eff2ff;display:flex;align-items:center;justify-content:center;margin-bottom:10px;">
|
||||
<a href="/magacin" class="kartica dash-stat animiraj kartica-link">
|
||||
<div class="dash-ikona dash-ikona-plava">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#4f7ef8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" />
|
||||
</svg>
|
||||
@@ -31,8 +31,8 @@
|
||||
<div style="font-size:14px;color:var(--tekst-glavni);margin-top:4px;font-weight:500;">Artikala na stanju</div>
|
||||
</a>
|
||||
|
||||
<a href="/servis" class="kartica dash-stat animiraj" style="text-decoration:none;display:block;cursor:pointer;">
|
||||
<div style="width:36px;height:36px;border-radius:8px;background:#f0fdf4;display:flex;align-items:center;justify-content:center;margin-bottom:10px;">
|
||||
<a href="/servis" class="kartica dash-stat animiraj kartica-link">
|
||||
<div class="dash-ikona dash-ikona-zelena">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#16a34a" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z" />
|
||||
</svg>
|
||||
@@ -42,8 +42,8 @@
|
||||
</a>
|
||||
|
||||
{{ if index .Dozvole "dashboard.prihod" }}
|
||||
<a href="/izvestaji" class="kartica dash-stat animiraj" style="text-decoration:none;display:block;cursor:pointer;">
|
||||
<div style="width:36px;height:36px;border-radius:8px;background:#fff7ed;display:flex;align-items:center;justify-content:center;margin-bottom:10px;">
|
||||
<a href="/izvestaji" class="kartica dash-stat animiraj kartica-link">
|
||||
<div class="dash-ikona dash-ikona-narandzasta">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#ea580c" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="9" cy="21" r="1" />
|
||||
<circle cx="20" cy="21" r="1" />
|
||||
@@ -55,8 +55,8 @@
|
||||
</a>
|
||||
{{ end }}
|
||||
|
||||
<a href="/magacin?kriticni=1" class="kartica dash-stat animiraj" style="text-decoration:none;display:block;cursor:pointer;">
|
||||
<div style="width:36px;height:36px;border-radius:8px;background:#fef2f2;display:flex;align-items:center;justify-content:center;margin-bottom:10px;">
|
||||
<a href="/magacin?kriticni=1" class="kartica dash-stat animiraj kartica-link">
|
||||
<div class="dash-ikona dash-ikona-crvena">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#dc2626" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
|
||||
<line x1="12" y1="9" x2="12" y2="13" />
|
||||
@@ -67,8 +67,8 @@
|
||||
<div style="font-size:14px;color:var(--tekst-glavni);margin-top:4px;font-weight:500;">Kritično niska zaliha</div>
|
||||
</a>
|
||||
|
||||
<a href="/podsetnici" class="kartica dash-stat animiraj" style="text-decoration:none;display:block;cursor:pointer;">
|
||||
<div style="width:36px;height:36px;border-radius:8px;background:#f0f9ff;display:flex;align-items:center;justify-content:center;margin-bottom:10px;">
|
||||
<a href="/podsetnici" class="kartica dash-stat animiraj kartica-link">
|
||||
<div class="dash-ikona dash-ikona-nebo">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#0284c7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" />
|
||||
<path d="M13.73 21a2 2 0 0 1-3.46 0" />
|
||||
|
||||
@@ -22,6 +22,13 @@
|
||||
{{define "sadrzaj"}}
|
||||
<div class="kolona" style="gap:20px;">
|
||||
|
||||
<!-- brzi linkovi ka magacinskim izveštajima -->
|
||||
<div style="display:flex;gap:10px;flex-wrap:wrap;">
|
||||
<a href="/izvestaji/prometni-list" class="btn-sekundarno">Prometni list magacina</a>
|
||||
<a href="/izvestaji/stanje-zaliha" class="btn-sekundarno">Stanje zaliha</a>
|
||||
<a href="/izvestaji/popis" class="btn-sekundarno">Popis (inventura)</a>
|
||||
</div>
|
||||
|
||||
<!-- 1. mesečni prihod -->
|
||||
<div class="kartica izv-sekcija animiraj">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px;margin-bottom:4px;">
|
||||
|
||||
@@ -79,6 +79,9 @@
|
||||
</td>
|
||||
<td style="padding:12px 16px;text-align:center;">
|
||||
<div style="display:flex;align-items:center;justify-content:center;gap:8px;">
|
||||
<a href="/magacin/kartica/{{.ID}}" class="btn-sekundarno-malo">
|
||||
Kartica
|
||||
</a>
|
||||
{{if index $.Dozvole "artikal.izmeni"}}
|
||||
<a href="/magacin/izmeni/{{.ID}}" class="btn-primarno-malo">
|
||||
Izmeni
|
||||
@@ -121,6 +124,7 @@
|
||||
{{end}}
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;flex-shrink:0;flex-wrap:wrap;justify-content:flex-end;">
|
||||
<a href="/magacin/kartica/{{.ID}}" class="btn-sekundarno-malo">Kartica</a>
|
||||
{{if index $.Dozvole "artikal.izmeni"}}
|
||||
<a href="/magacin/izmeni/{{.ID}}" class="btn-primarno-malo">Izmeni</a>
|
||||
{{template "promeniCenuMeni" (dict "ID" .ID "Cena" .ProdajnaCena)}}
|
||||
|
||||
@@ -25,6 +25,24 @@
|
||||
<form method="POST" action="{{if .Izmena}}/magacin/izmeni/{{.Artikal.ID}}{{else}}/magacin/novi{{end}}">
|
||||
<div class="kolona" style="gap:14px;">
|
||||
|
||||
<!-- šifra i barkod -->
|
||||
<div class="forma-grid-2" style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
|
||||
<div>
|
||||
<label class="polje-labela">Šifra artikla</label>
|
||||
<input type="text" name="sifra" value="{{.Artikal.Sifra}}"
|
||||
placeholder="npr. ART-00001"
|
||||
style="width:100%;font-family:monospace;">
|
||||
<div style="font-size:11px;color:var(--tekst-slabi);margin-top:4px;">Ako ostaviš prazno, šifra se automatski dodeljuje.</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="polje-labela">Barkod (EAN)</label>
|
||||
<input type="text" name="barkod" value="{{.Artikal.Barkod}}"
|
||||
placeholder="npr. 3830057592015"
|
||||
style="width:100%;font-family:monospace;">
|
||||
<div style="font-size:11px;color:var(--tekst-slabi);margin-top:4px;">Barkod sa pakovanja (opciono).</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- naziv -->
|
||||
<div>
|
||||
<label class="polje-labela">
|
||||
@@ -66,9 +84,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="polje-labela">Prodajna cena (din)</label>
|
||||
<input type="number" name="prodajna_cena" value="{{.Artikal.ProdajnaCena}}" min="0" step="0.01" style="width:100%;">
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
|
||||
<div>
|
||||
<label class="polje-labela">Nabavna cena (din)</label>
|
||||
<input type="number" name="nabavna_cena" value="{{.Artikal.NabavnaCena}}" min="0" step="0.01" style="width:100%;">
|
||||
</div>
|
||||
<div>
|
||||
<label class="polje-labela">Prodajna cena (din)</label>
|
||||
<input type="number" name="prodajna_cena" value="{{.Artikal.ProdajnaCena}}" min="0" step="0.01" style="width:100%;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- marža za kalkulaciju; prazno = nasleđuje maržu kategorije ili globalnu -->
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "naslov"}}Kartica: {{.Artikal.Naziv}} — NTech{{end}}
|
||||
|
||||
{{define "sadrzaj"}}
|
||||
<div class="kolona" style="gap:16px;">
|
||||
|
||||
<a href="/magacin" 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 magacin
|
||||
</a>
|
||||
|
||||
<!-- zaglavlje artikla -->
|
||||
<div class="kartica animiraj">
|
||||
<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap;">
|
||||
<div>
|
||||
<div style="font-size:18px;font-weight:600;color:var(--tekst-glavni);">{{.Artikal.Naziv}}</div>
|
||||
<div style="margin-top:6px;display:flex;gap:16px;flex-wrap:wrap;">
|
||||
{{if .Artikal.Sifra}}
|
||||
<span style="font-size:12px;color:var(--tekst-slabi);">Šifra: <span style="font-family:monospace;color:var(--tekst-glavni);">{{.Artikal.Sifra}}</span></span>
|
||||
{{end}}
|
||||
{{if .Artikal.Barkod}}
|
||||
<span style="font-size:12px;color:var(--tekst-slabi);">Barkod: <span style="font-family:monospace;color:var(--tekst-glavni);">{{.Artikal.Barkod}}</span></span>
|
||||
{{end}}
|
||||
{{if .Artikal.Lokacija}}
|
||||
<span style="font-size:12px;color:var(--tekst-slabi);">Lokacija: <span style="color:var(--tekst-glavni);">{{.Artikal.Lokacija}}</span></span>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div style="text-align:right;">
|
||||
<div style="font-size:24px;font-weight:700;color:{{if le .Artikal.Kolicina .Artikal.KolicinMin}}#dc2626{{else}}var(--tekst-glavni){{end}};">
|
||||
{{.Artikal.Kolicina}} kom
|
||||
</div>
|
||||
<div style="font-size:12px;color:var(--tekst-slabi);">trenutno stanje</div>
|
||||
{{if gt .Artikal.KolicinMin 0}}
|
||||
<div style="font-size:11px;color:var(--tekst-slabi);margin-top:2px;">min. {{.Artikal.KolicinMin}} kom</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- tabela promena -->
|
||||
<div class="kartica animiraj">
|
||||
<div style="font-size:14px;font-weight:500;color:var(--tekst-glavni);margin-bottom:14px;padding-bottom:10px;border-bottom:0.5px solid var(--ivica);">
|
||||
Prometna kartica
|
||||
<span style="font-size:12px;font-weight:400;color:var(--tekst-slabi);margin-left:8px;">{{len .Promene}} zapisa</span>
|
||||
</div>
|
||||
|
||||
{{if not .Promene}}
|
||||
<div style="text-align:center;padding:32px 0;color:var(--tekst-slabi);font-size:14px;">
|
||||
Nema evidentiranih promena za ovaj artikal.
|
||||
</div>
|
||||
{{else}}
|
||||
<div style="overflow-x:auto;">
|
||||
<table class="tabela">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Datum</th>
|
||||
<th>Vrsta promene</th>
|
||||
<th style="text-align:right;">Promena</th>
|
||||
<th style="text-align:right;">Pre</th>
|
||||
<th style="text-align:right;">Posle</th>
|
||||
<th>Napomena</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Promene}}
|
||||
<tr>
|
||||
<td style="white-space:nowrap;color:var(--tekst-slabi);font-size:12px;">
|
||||
{{.Datum.Format "02.01.2006. 15:04"}}
|
||||
</td>
|
||||
<td>
|
||||
{{if eq .TipPromene "ulaz_nabavka"}}
|
||||
<span class="bedz" style="background:rgba(34,197,94,0.12);color:#22c55e;">Ulaz — nabavka</span>
|
||||
{{else if eq .TipPromene "izlaz_prodaja"}}
|
||||
<span class="bedz" style="background:rgba(59,130,246,0.12);color:#3b82f6;">Izlaz — prodaja</span>
|
||||
{{else if eq .TipPromene "izlaz_servis"}}
|
||||
<span class="bedz" style="background:rgba(249,115,22,0.12);color:#f97316;">Izlaz — servis</span>
|
||||
{{else if eq .TipPromene "povracaj"}}
|
||||
<span class="bedz" style="background:rgba(168,85,247,0.12);color:#a855f7;">Povraćaj</span>
|
||||
{{else if eq .TipPromene "korekcija"}}
|
||||
<span class="bedz" style="background:rgba(156,163,175,0.12);color:#9ca3af;">Korekcija</span>
|
||||
{{else}}
|
||||
<span class="bedz">{{.TipPromene}}</span>
|
||||
{{end}}
|
||||
</td>
|
||||
<td style="text-align:right;font-weight:600;font-family:monospace;{{if gt .PromenaKolicine 0}}color:#22c55e;{{else}}color:#dc2626;{{end}}">
|
||||
{{if gt .PromenaKolicine 0}}+{{end}}{{.PromenaKolicine}}
|
||||
</td>
|
||||
<td style="text-align:right;font-family:monospace;color:var(--tekst-slabi);">{{.StanjePre}}</td>
|
||||
<td style="text-align:right;font-family:monospace;font-weight:500;">{{.StanjePosle}}</td>
|
||||
<td style="font-size:12px;color:var(--tekst-slabi);">{{.Napomena}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -317,11 +317,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="polje-labela">Prodajna cena (din)</label>
|
||||
<input type="number" x-model="modalCena" min="0" step="0.01"
|
||||
placeholder="0"
|
||||
style="width:100%;">
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
|
||||
<div>
|
||||
<label class="polje-labela">Nabavna cena (din)</label>
|
||||
<input type="number" x-model="modalNabavnaCena" min="0" step="0.01"
|
||||
placeholder="0" style="width:100%;">
|
||||
</div>
|
||||
<div>
|
||||
<label class="polje-labela">Prodajna cena (din)</label>
|
||||
<input type="number" x-model="modalCena" min="0" step="0.01"
|
||||
placeholder="0" style="width:100%;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "naslov"}}Popis — NTech{{end}}
|
||||
|
||||
{{define "dodatni-css"}}
|
||||
<style>
|
||||
.popis-input {
|
||||
width: 80px;
|
||||
text-align: right;
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
padding: 5px 8px;
|
||||
}
|
||||
.popis-input.izmenjeno {
|
||||
border-color: #f97316;
|
||||
background: rgba(249,115,22,0.06);
|
||||
}
|
||||
.popis-razlika {
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
min-width: 40px;
|
||||
text-align: right;
|
||||
}
|
||||
.red-kriticno td { background: rgba(220,38,38,0.04); }
|
||||
</style>
|
||||
{{end}}
|
||||
|
||||
{{define "sadrzaj"}}
|
||||
<div class="kolona" style="gap:16px;">
|
||||
|
||||
{{if .Sacuvano}}
|
||||
<div class="poruka-uspeh poruka-animacija">Popis je uspešno sačuvan. Korekcije su upisane u magacinsku evidenciju.</div>
|
||||
{{end}}
|
||||
{{if .Greska}}
|
||||
<div class="poruka-greska greska-animacija">{{.Greska}}</div>
|
||||
{{end}}
|
||||
|
||||
<!-- uputstvo -->
|
||||
<div class="kartica animiraj" style="padding:14px 18px;">
|
||||
<div style="display:flex;align-items:flex-start;gap:12px;flex-wrap:wrap;">
|
||||
<div style="flex:1;min-width:260px;">
|
||||
<div style="font-size:14px;font-weight:500;color:var(--tekst-glavni);margin-bottom:4px;">Godišnji popis magacina</div>
|
||||
<div style="font-size:13px;color:var(--tekst-slabi);line-height:1.6;">
|
||||
Unesite stvarno prebrojano stanje za svaki artikal. Polja sa razlikom biće istaknuta narandžasto.
|
||||
Samo artikli sa izmenjenom količinom biće korigovani — ostali ostaju nepromenjeni.
|
||||
</div>
|
||||
</div>
|
||||
<a href="/izvestaji" class="btn-sekundarno" style="white-space:nowrap;">← Izveštaji</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- forma -->
|
||||
<form method="POST" action="/izvestaji/popis">
|
||||
<input type="hidden" name="csrf_token" value="{{.CsrfToken}}">
|
||||
|
||||
<div class="kartica animiraj" style="padding:0;overflow:hidden;">
|
||||
<div style="padding:12px 20px;border-bottom:0.5px solid var(--ivica);display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px;">
|
||||
<span style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">Artikli</span>
|
||||
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;">
|
||||
<div>
|
||||
<label class="polje-labela" style="display:inline;margin-right:6px;">Napomena:</label>
|
||||
<input type="text" name="napomena" value="Godišnji popis" style="width:200px;font-size:13px;padding:5px 8px;">
|
||||
</div>
|
||||
<button type="submit" class="btn-primarno">Sačuvaj popis</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{if not .Artikli}}
|
||||
<div style="padding:40px;text-align:center;color:var(--tekst-slabi);font-size:14px;">
|
||||
Nema artikala u magacinu.
|
||||
</div>
|
||||
{{else}}
|
||||
<div style="overflow-x:auto;">
|
||||
<table class="tabela">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Artikal</th>
|
||||
<th>Šifra</th>
|
||||
<th>Kategorija</th>
|
||||
<th style="text-align:right;">Knjižno stanje</th>
|
||||
<th style="text-align:right;">Stvarno stanje</th>
|
||||
<th style="text-align:right;">Razlika</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Artikli}}
|
||||
<tr class="{{if le .Kolicina .KolicinMin}}red-kriticno{{end}}" id="red-{{.ID}}">
|
||||
<td style="font-weight:500;">{{.Naziv}}</td>
|
||||
<td style="font-family:monospace;font-size:12px;color:var(--tekst-slabi);">{{if .Sifra}}{{.Sifra}}{{else}}—{{end}}</td>
|
||||
<td style="font-size:12px;color:var(--tekst-slabi);">{{if .KategorijaNaziv}}{{.KategorijaNaziv}}{{else}}—{{end}}</td>
|
||||
<td style="text-align:right;font-family:monospace;font-weight:600;" id="knjizno-{{.ID}}">{{.Kolicina}}</td>
|
||||
<td style="text-align:right;">
|
||||
<input type="number"
|
||||
class="popis-input"
|
||||
name="kolicina_{{.ID}}"
|
||||
id="input-{{.ID}}"
|
||||
value="{{.Kolicina}}"
|
||||
min="0"
|
||||
data-knjizno="{{.Kolicina}}"
|
||||
data-id="{{.ID}}"
|
||||
oninput="azurirajRazliku(this)">
|
||||
</td>
|
||||
<td style="text-align:right;">
|
||||
<span class="popis-razlika" id="razlika-{{.ID}}">—</span>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- dugme na dnu -->
|
||||
<div style="padding:14px 20px;border-top:0.5px solid var(--ivica);display:flex;justify-content:flex-end;">
|
||||
<button type="submit" class="btn-primarno">Sačuvaj popis</button>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function azurirajRazliku(input) {
|
||||
var id = input.dataset.id;
|
||||
var knjizno = parseInt(input.dataset.knjizno, 10);
|
||||
var stvarno = parseInt(input.value, 10);
|
||||
var span = document.getElementById('razlika-' + id);
|
||||
|
||||
if (isNaN(stvarno)) {
|
||||
span.textContent = '—';
|
||||
span.style.color = '';
|
||||
input.classList.remove('izmenjeno');
|
||||
return;
|
||||
}
|
||||
|
||||
var razlika = stvarno - knjizno;
|
||||
if (razlika === 0) {
|
||||
span.textContent = '—';
|
||||
span.style.color = '';
|
||||
input.classList.remove('izmenjeno');
|
||||
} else if (razlika > 0) {
|
||||
span.textContent = '+' + razlika;
|
||||
span.style.color = '#22c55e';
|
||||
input.classList.add('izmenjeno');
|
||||
} else {
|
||||
span.textContent = razlika;
|
||||
span.style.color = '#dc2626';
|
||||
input.classList.add('izmenjeno');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{{end}}
|
||||
@@ -4,6 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Prijava — NTech</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f1117; min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 16px; position: relative; }
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "naslov"}}Prometni list — NTech{{end}}
|
||||
|
||||
{{define "sadrzaj"}}
|
||||
<div class="kolona" style="gap:16px;">
|
||||
|
||||
<!-- filter perioda -->
|
||||
<div class="kartica animiraj">
|
||||
<form method="GET" action="/izvestaji/prometni-list" style="display:flex;gap:12px;align-items:flex-end;flex-wrap:wrap;">
|
||||
<div>
|
||||
<label class="polje-labela">Od datuma</label>
|
||||
<input type="date" name="od" value="{{.Od}}" style="width:160px;">
|
||||
</div>
|
||||
<div>
|
||||
<label class="polje-labela">Do datuma</label>
|
||||
<input type="date" name="do" value="{{.Do}}" style="width:160px;">
|
||||
</div>
|
||||
<button type="submit" class="btn-primarno">Prikaži</button>
|
||||
<a href="/izvestaji" class="btn-sekundarno">← Izveštaji</a>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- tabela promena -->
|
||||
<div class="kartica animiraj" style="padding:0;overflow:hidden;">
|
||||
<div style="padding:16px 20px;border-bottom:0.5px solid var(--ivica);display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px;">
|
||||
<span style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">Prometni list magacina</span>
|
||||
<span style="font-size:12px;color:var(--tekst-slabi);">{{.Od}} — {{.Do}} · {{.Ukupno}} promena</span>
|
||||
</div>
|
||||
|
||||
{{if not .Promene}}
|
||||
<div style="padding:40px;text-align:center;color:var(--tekst-slabi);font-size:14px;">
|
||||
Nema promena u odabranom periodu.
|
||||
</div>
|
||||
{{else}}
|
||||
<div style="overflow-x:auto;">
|
||||
<table class="tabela">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Datum</th>
|
||||
<th>Artikal</th>
|
||||
<th>Šifra</th>
|
||||
<th>Vrsta</th>
|
||||
<th style="text-align:right;">Promena</th>
|
||||
<th style="text-align:right;">Pre</th>
|
||||
<th style="text-align:right;">Posle</th>
|
||||
<th>Napomena</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Promene}}
|
||||
<tr>
|
||||
<td style="white-space:nowrap;font-size:12px;color:var(--tekst-slabi);">
|
||||
{{.Datum.Format "02.01.2006. 15:04"}}
|
||||
</td>
|
||||
<td style="font-weight:500;">{{.ArtikalNaziv}}</td>
|
||||
<td style="font-family:monospace;font-size:12px;color:var(--tekst-slabi);">{{if .ArtikalSifra}}{{.ArtikalSifra}}{{else}}—{{end}}</td>
|
||||
<td>
|
||||
{{if eq .TipPromene "ulaz_nabavka"}}
|
||||
<span class="bedz" style="background:rgba(34,197,94,0.12);color:#22c55e;">Ulaz</span>
|
||||
{{else if eq .TipPromene "izlaz_prodaja"}}
|
||||
<span class="bedz" style="background:rgba(59,130,246,0.12);color:#3b82f6;">Prodaja</span>
|
||||
{{else if eq .TipPromene "izlaz_servis"}}
|
||||
<span class="bedz" style="background:rgba(249,115,22,0.12);color:#f97316;">Servis</span>
|
||||
{{else if eq .TipPromene "povracaj"}}
|
||||
<span class="bedz" style="background:rgba(168,85,247,0.12);color:#a855f7;">Povraćaj</span>
|
||||
{{else if eq .TipPromene "korekcija"}}
|
||||
<span class="bedz" style="background:rgba(156,163,175,0.12);color:#9ca3af;">Korekcija</span>
|
||||
{{else}}
|
||||
<span class="bedz">{{.TipPromene}}</span>
|
||||
{{end}}
|
||||
</td>
|
||||
<td style="text-align:right;font-family:monospace;font-weight:600;{{if gt .PromenaKolicine 0}}color:#22c55e;{{else}}color:#dc2626;{{end}}">
|
||||
{{if gt .PromenaKolicine 0}}+{{end}}{{.PromenaKolicine}}
|
||||
</td>
|
||||
<td style="text-align:right;font-family:monospace;color:var(--tekst-slabi);">{{.StanjePre}}</td>
|
||||
<td style="text-align:right;font-family:monospace;font-weight:500;">{{.StanjePosle}}</td>
|
||||
<td style="font-size:12px;color:var(--tekst-slabi);">{{.Napomena}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -43,11 +43,30 @@
|
||||
<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 style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">
|
||||
{{template "status-badge-detalji" .Nalog.Status}}
|
||||
<form method="post" action="/servis/{{.Nalog.ID}}/status" style="display:flex;align-items:center;gap:6px;">
|
||||
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
|
||||
<select name="status" style="font-size:13px;padding:4px 8px;border-radius:6px;border:0.5px solid var(--ivica);background:var(--kartica-pozadina);color:var(--tekst-glavni);cursor:pointer;">
|
||||
{{range .SviStatusi}}
|
||||
<option value="{{.}}"{{if eq . $.Nalog.Status}} selected{{end}}>{{.}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
<button type="submit" class="btn-sekundarno" style="font-size:13px;padding:4px 12px;">Promeni</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap;">
|
||||
<a href="/servis/{{.Nalog.ID}}/stampa" target="_blank" class="btn-sekundarno">
|
||||
Radni nalog
|
||||
</a>
|
||||
<a href="/servis/{{.Nalog.ID}}/otpremnica" target="_blank" class="btn-sekundarno">
|
||||
Otpremnica
|
||||
</a>
|
||||
<a href="/servis/izmeni/{{.Nalog.ID}}" class="btn-primarno">
|
||||
Izmeni nalog
|
||||
</a>
|
||||
</div>
|
||||
<a href="/servis/izmeni/{{.Nalog.ID}}" class="btn-primarno">
|
||||
Izmeni nalog
|
||||
</a>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fit, minmax(140px, 1fr));gap:16px;">
|
||||
<div>
|
||||
@@ -153,22 +172,38 @@
|
||||
</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);">
|
||||
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:4px;">Cena rada</div>
|
||||
<div style="font-size:16px;font-weight:500;color:var(--tekst-glavni);">
|
||||
{{if .Nalog.CenaKonacna}}{{.Nalog.CenaKonacnaStr}} din{{else}}—{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{if gt .UkupnoDelovi 0.0}}
|
||||
<div>
|
||||
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:4px;">Ugrađeni delovi</div>
|
||||
<div style="font-size:16px;font-weight:500;color:var(--tekst-glavni);">
|
||||
{{printf "%.2f" .UkupnoDelovi}} din
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{if .Nalog.CenaKonacna}}
|
||||
<div>
|
||||
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:4px;">Ukupno</div>
|
||||
<div style="font-size:20px;font-weight:600;color:var(--sb-akcent);">
|
||||
{{printf "%.2f" .UkupnoSve}} din
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<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);">
|
||||
{{if .Nalog.Avans}}{{.Nalog.AvansStr}} din{{else}}—{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{if .Nalog.PreostaloZaNaplatu}}
|
||||
{{if .Nalog.CenaKonacna}}
|
||||
<div>
|
||||
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:4px;">Preostalo za naplatu</div>
|
||||
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:4px;">Za naplatu</div>
|
||||
<div style="font-size:20px;font-weight:600;color:#16a34a;">
|
||||
{{.Nalog.PreostaloZaNaplatuStr}} din
|
||||
{{printf "%.2f" .PreostaloSve}} din
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -191,7 +226,7 @@
|
||||
<select name="artikal_id" style="width:100%;" required>
|
||||
<option value="">— odaberi —</option>
|
||||
{{range .Artikli}}
|
||||
<option value="{{.ID}}">{{.Naziv}} ({{.Kolicina}} kom)</option>
|
||||
<option value="{{.ID}}" data-cena="{{.ProdajnaCena}}">{{.Naziv}} ({{.Kolicina}} kom)</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
@@ -271,4 +306,21 @@
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
var sel = document.querySelector('select[name="artikal_id"]');
|
||||
var cenaInput = document.querySelector('input[name="cena_komada"]');
|
||||
if (!sel || !cenaInput) return;
|
||||
sel.addEventListener('change', function() {
|
||||
var opt = sel.options[sel.selectedIndex];
|
||||
var cena = parseFloat(opt.dataset.cena);
|
||||
if (!isNaN(cena) && cena > 0) {
|
||||
cenaInput.value = cena.toFixed(2);
|
||||
} else {
|
||||
cenaInput.value = '0';
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
@@ -179,7 +179,7 @@
|
||||
placeholder="0" style="width:100%;">
|
||||
</div>
|
||||
<div>
|
||||
<label class="polje-labela">Konačna cena</label>
|
||||
<label class="polje-labela">Cena rada</label>
|
||||
<input type="number" name="cena_konacna" min="0" step="0.01"
|
||||
value="{{if .Nalog.CenaKonacna}}{{.Nalog.CenaKonacna}}{{end}}"
|
||||
placeholder="0" style="width:100%;">
|
||||
@@ -218,6 +218,7 @@ function toggleGarancija(chk) {
|
||||
var input = document.getElementById('garancija_do_input');
|
||||
var hidden = document.getElementById('bez_garancije_val');
|
||||
if (chk.checked) {
|
||||
if (input.value) input.dataset.sacuvano = input.value;
|
||||
input.value = '';
|
||||
input.disabled = true;
|
||||
input.style.opacity = '0.4';
|
||||
@@ -225,6 +226,7 @@ function toggleGarancija(chk) {
|
||||
} else {
|
||||
input.disabled = false;
|
||||
input.style.opacity = '1';
|
||||
if (input.dataset.sacuvano) input.value = input.dataset.sacuvano;
|
||||
hidden.value = '0';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="sr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Otpremnica — {{.Nalog.BrojNaloga}}</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif; font-size: 13px; color: #111; background: #fff; }
|
||||
|
||||
.strana { max-width: 780px; margin: 0 auto; padding: 32px 40px; }
|
||||
|
||||
/* zaglavlje */
|
||||
.zaglavlje { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 24px; padding-bottom: 16px; border-bottom: 2px solid #111; gap: 20px; }
|
||||
.firma-naziv { font-size: 20px; font-weight: 700; }
|
||||
.firma-info { font-size: 11px; color: #555; margin-top: 4px; line-height: 1.7; }
|
||||
.dok-naslov { text-align: right; }
|
||||
.dok-tip { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 1px; color: #555; margin-bottom: 4px; }
|
||||
.dok-broj { font-size: 22px; font-weight: 700; font-family: monospace; }
|
||||
.dok-datum { font-size: 12px; color: #555; margin-top: 6px; }
|
||||
|
||||
/* strane */
|
||||
.strane-blok { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 20px; }
|
||||
.strana-kartica { padding: 12px 14px; border: 0.5px solid #ddd; border-radius: 6px; }
|
||||
.strana-tip { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.6px; color: #888; margin-bottom: 6px; }
|
||||
.strana-naziv { font-size: 14px; font-weight: 600; }
|
||||
.strana-info { font-size: 12px; color: #555; margin-top: 3px; line-height: 1.6; }
|
||||
|
||||
/* odeljci */
|
||||
.odeljak { margin-bottom: 18px; }
|
||||
.odeljak-naslov { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.8px; color: #555; margin-bottom: 8px; padding-bottom: 4px; border-bottom: 0.5px solid #ccc; }
|
||||
|
||||
/* podaci */
|
||||
.podaci-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
|
||||
.podaci-grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
||||
.polje-labela { font-size: 10px; color: #777; text-transform: uppercase; letter-spacing: 0.4px; margin-bottom: 3px; }
|
||||
.polje-vrednost { font-size: 13px; font-weight: 500; }
|
||||
.polje-vrednost-mono { font-family: monospace; font-size: 13px; font-weight: 500; }
|
||||
.polje-tekst { font-size: 13px; line-height: 1.6; white-space: pre-wrap; }
|
||||
.prazno { color: #aaa; font-style: italic; }
|
||||
|
||||
/* tabela delova */
|
||||
.tabela { width: 100%; border-collapse: collapse; margin-top: 6px; }
|
||||
.tabela th { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.4px; color: #555; padding: 6px 8px; border-bottom: 1.5px solid #111; text-align: left; }
|
||||
.tabela th.desno { text-align: right; }
|
||||
.tabela td { padding: 7px 8px; border-bottom: 0.5px solid #ddd; font-size: 13px; vertical-align: middle; }
|
||||
.tabela td.desno { text-align: right; }
|
||||
.tabela .ukupno-red td { font-weight: 600; border-top: 1.5px solid #111; border-bottom: none; background: #fafafa; }
|
||||
|
||||
/* iznos za naplatu */
|
||||
.naplata-blok { display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; border: 1.5px solid #111; border-radius: 6px; margin-top: 12px; }
|
||||
.naplata-labela { font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.4px; }
|
||||
.naplata-iznos { font-size: 22px; font-weight: 700; }
|
||||
|
||||
/* garancija */
|
||||
.garancija-blok { padding: 10px 14px; border: 0.5px solid #86efac; background: #f0fdf4; border-radius: 6px; display: flex; justify-content: space-between; align-items: center; }
|
||||
.garancija-tekst { font-size: 12px; color: #15803d; font-weight: 500; }
|
||||
.garancija-datum { font-size: 15px; font-weight: 700; color: #15803d; }
|
||||
|
||||
/* potpisi */
|
||||
.potpisi { display: grid; grid-template-columns: 1fr 1fr; gap: 40px; margin-top: 36px; padding-top: 16px; border-top: 0.5px solid #ccc; }
|
||||
.potpis-linija { border-top: 1px solid #888; margin-top: 44px; padding-top: 6px; font-size: 11px; color: #555; text-align: center; }
|
||||
|
||||
/* napomena na dnu */
|
||||
.napomena-dno { margin-top: 20px; padding: 10px 14px; background: #f9fafb; border-left: 3px solid #ddd; font-size: 12px; color: #555; line-height: 1.6; }
|
||||
|
||||
@media print {
|
||||
body { font-size: 12px; }
|
||||
.strana { padding: 16px 20px; max-width: 100%; }
|
||||
.dugme-stampa { display: none !important; }
|
||||
@page { size: A4; margin: 12mm 14mm; }
|
||||
}
|
||||
.dugme-stampa { position: fixed; bottom: 24px; right: 24px; background: #111; color: #fff; border: none; padding: 12px 22px; border-radius: 8px; font-size: 14px; font-weight: 500; cursor: pointer; box-shadow: 0 4px 12px rgba(0,0,0,0.2); }
|
||||
.dugme-stampa:hover { background: #333; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="strana">
|
||||
|
||||
<!-- zaglavlje -->
|
||||
<div class="zaglavlje">
|
||||
<div>
|
||||
<div class="firma-naziv">{{if .NazivFirme}}{{.NazivFirme}}{{else}}— naziv firme —{{end}}</div>
|
||||
<div class="firma-info">
|
||||
{{if .Podnazlov}}{{.Podnazlov}}<br>{{end}}
|
||||
{{if .Adresa}}{{.Adresa}}<br>{{end}}
|
||||
{{if .Telefon}}Tel: {{.Telefon}}<br>{{end}}
|
||||
{{if .PIB}}PIB: {{.PIB}}{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;align-items:flex-start;gap:14px;">
|
||||
<div class="dok-naslov">
|
||||
<div class="dok-tip">Otpremnica</div>
|
||||
<div class="dok-broj">{{.Nalog.BrojNaloga}}</div>
|
||||
<div class="dok-datum">Datum: {{.Nalog.DatumPrijema.Format "02.01.2006."}}</div>
|
||||
{{if .Nalog.DatumZavrsetka}}
|
||||
<div class="dok-datum">Završeno: {{.Nalog.DatumZavrsetka.Format "02.01.2006."}}</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{if .QRKod}}
|
||||
<img src="data:image/png;base64,{{.QRKod}}" width="76" height="76"
|
||||
alt="QR {{.Nalog.BrojNaloga}}"
|
||||
style="image-rendering:pixelated;border:1px solid #ddd;border-radius:4px;flex-shrink:0;">
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- isporučilac i primalac -->
|
||||
<div class="strane-blok">
|
||||
<div class="strana-kartica">
|
||||
<div class="strana-tip">Isporučilac</div>
|
||||
<div class="strana-naziv">{{if .NazivFirme}}{{.NazivFirme}}{{else}}—{{end}}</div>
|
||||
<div class="strana-info">
|
||||
{{if .Adresa}}{{.Adresa}}<br>{{end}}
|
||||
{{if .Telefon}}{{.Telefon}}{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="strana-kartica">
|
||||
<div class="strana-tip">Primalac (klijent)</div>
|
||||
{{if .KlijentNaziv}}
|
||||
<div class="strana-naziv">{{.KlijentNaziv}}</div>
|
||||
<div class="strana-info">
|
||||
{{if .Klijent}}
|
||||
{{if .Klijent.Telefon}}Tel: {{.Klijent.Telefon}}<br>{{end}}
|
||||
{{if .Klijent.Email}}{{.Klijent.Email}}<br>{{end}}
|
||||
{{if .Klijent.Mesto}}{{.Klijent.Mesto}}{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="strana-naziv prazno">— klijent nije naveden —</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- uređaj koji se vraća -->
|
||||
<div class="odeljak">
|
||||
<div class="odeljak-naslov">Uređaj koji se preuzima</div>
|
||||
<div class="podaci-grid" style="margin-bottom:10px;">
|
||||
<div>
|
||||
<div class="polje-labela">Naziv uređaja</div>
|
||||
<div class="polje-vrednost">{{.Nalog.Uredjaj}}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="polje-labela">Serijski broj</div>
|
||||
<div class="polje-vrednost-mono">{{if .Nalog.SerijskiBroj}}{{.Nalog.SerijskiBroj}}{{else}}<span class="prazno">—</span>{{end}}</div>
|
||||
</div>
|
||||
{{if .TehnicarNaziv}}
|
||||
<div>
|
||||
<div class="polje-labela">Tehničar</div>
|
||||
<div class="polje-vrednost">{{.TehnicarNaziv}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{if .Nalog.Pribor}}
|
||||
<div style="margin-top:8px;">
|
||||
<div class="polje-labela">Pribor i oprema</div>
|
||||
<div class="polje-tekst">{{.Nalog.Pribor}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<!-- opis izvršenih radova -->
|
||||
<div class="odeljak">
|
||||
<div class="odeljak-naslov">Opis izvršenih radova</div>
|
||||
<div class="polje-tekst">{{if .Nalog.OpisKvara}}{{.Nalog.OpisKvara}}{{else}}<span class="prazno">—</span>{{end}}</div>
|
||||
</div>
|
||||
|
||||
<!-- ugrađeni delovi -->
|
||||
{{if .ServisniDelovi}}
|
||||
<div class="odeljak">
|
||||
<div class="odeljak-naslov">Ugrađeni delovi i materijal</div>
|
||||
<table class="tabela">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Artikal</th>
|
||||
<th class="desno" style="width:70px;">Kol.</th>
|
||||
<th class="desno" style="width:120px;">Cena/kom</th>
|
||||
<th class="desno" style="width:120px;">Ukupno</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .ServisniDelovi}}
|
||||
<tr>
|
||||
<td>{{.ArtikalNaziv}}</td>
|
||||
<td class="desno">{{.Kolicina}}</td>
|
||||
<td class="desno">{{printf "%.2f" .CenaKomada}} din</td>
|
||||
<td class="desno">{{printf "%.2f" .Ukupno}} din</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
<tr class="ukupno-red">
|
||||
<td colspan="3">Ukupno delovi</td>
|
||||
<td class="desno">{{printf "%.2f" .UkupnoDelovi}} din</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<!-- iznos za naplatu -->
|
||||
{{if .Nalog.CenaKonacna}}
|
||||
<div class="odeljak">
|
||||
<div class="odeljak-naslov">Obračun</div>
|
||||
<table class="tabela" style="margin-bottom:10px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Cena rada</td>
|
||||
<td class="desno" style="width:160px;">{{.Nalog.CenaKonacnaStr}} din</td>
|
||||
</tr>
|
||||
{{if gt .UkupnoDelovi 0.0}}
|
||||
<tr>
|
||||
<td>Ugrađeni delovi i materijal</td>
|
||||
<td class="desno">{{printf "%.2f" .UkupnoDelovi}} din</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
{{if .ImaAvans}}
|
||||
<tr>
|
||||
<td>Avans (plaćeno)</td>
|
||||
<td class="desno">− {{.Nalog.AvansStr}} din</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="naplata-blok">
|
||||
<div class="naplata-labela">Za naplatu pri preuzimanju:</div>
|
||||
<div class="naplata-iznos">{{printf "%.2f" .PreostaloSve}} din</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<!-- garancija -->
|
||||
{{if .Nalog.GarancijaDo}}
|
||||
<div class="odeljak">
|
||||
<div class="garancija-blok">
|
||||
<div class="garancija-tekst">Garancija na izvršeni servis važi do:</div>
|
||||
<div class="garancija-datum">{{.Nalog.GarancijaDo.Format "02.01.2006."}}</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .Nalog.Napomena}}
|
||||
<div class="napomena-dno"><strong>Napomena:</strong> {{.Nalog.Napomena}}</div>
|
||||
{{end}}
|
||||
|
||||
<!-- potpisi -->
|
||||
<div class="potpisi">
|
||||
<div>
|
||||
<div class="potpis-linija">Predao (tehničar / firma)</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="potpis-linija">Preuzeo (klijent)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<button class="dugme-stampa" onclick="window.print()">Štampaj</button>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,255 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="sr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Servisni nalog {{.Nalog.BrojNaloga}}</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif; font-size: 13px; color: #111; background: #fff; }
|
||||
|
||||
.strana { max-width: 780px; margin: 0 auto; padding: 32px 40px; }
|
||||
|
||||
/* zaglavlje */
|
||||
.zaglavlje { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 28px; padding-bottom: 16px; border-bottom: 2px solid #111; }
|
||||
.firma-naziv { font-size: 20px; font-weight: 700; }
|
||||
.firma-info { font-size: 11px; color: #555; margin-top: 4px; line-height: 1.6; }
|
||||
.nalog-broj { text-align: right; }
|
||||
.nalog-broj-vrednost { font-size: 22px; font-weight: 700; font-family: monospace; }
|
||||
.nalog-naslov { font-size: 11px; color: #555; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; }
|
||||
|
||||
/* odeljci */
|
||||
.odeljak { margin-bottom: 20px; }
|
||||
.odeljak-naslov { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.8px; color: #555; margin-bottom: 8px; padding-bottom: 4px; border-bottom: 0.5px solid #ccc; }
|
||||
|
||||
/* grid sa podacima */
|
||||
.podaci-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; }
|
||||
.podaci-grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
|
||||
.polje-labela { font-size: 10px; color: #777; text-transform: uppercase; letter-spacing: 0.4px; margin-bottom: 3px; }
|
||||
.polje-vrednost { font-size: 13px; font-weight: 500; }
|
||||
.polje-vrednost-mono { font-family: monospace; font-size: 13px; }
|
||||
.polje-tekst { font-size: 13px; line-height: 1.6; white-space: pre-wrap; }
|
||||
|
||||
/* status bedž */
|
||||
.status { display: inline-block; padding: 2px 10px; border-radius: 20px; font-size: 11px; font-weight: 600; border: 1.5px solid #111; }
|
||||
|
||||
/* tabela delova */
|
||||
.tabela { width: 100%; border-collapse: collapse; margin-top: 8px; }
|
||||
.tabela th { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.4px; color: #555; padding: 6px 8px; border-bottom: 1.5px solid #111; text-align: left; }
|
||||
.tabela th.desno { text-align: right; }
|
||||
.tabela td { padding: 7px 8px; border-bottom: 0.5px solid #ddd; font-size: 13px; vertical-align: middle; }
|
||||
.tabela td.desno { text-align: right; }
|
||||
.tabela .ukupno-red td { font-weight: 600; border-top: 1.5px solid #111; border-bottom: none; }
|
||||
|
||||
/* cene */
|
||||
.cene-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
|
||||
.cena-blok { padding: 10px; border: 0.5px solid #ddd; border-radius: 6px; }
|
||||
.cena-labela { font-size: 10px; color: #777; text-transform: uppercase; letter-spacing: 0.4px; margin-bottom: 4px; }
|
||||
.cena-vrednost { font-size: 16px; font-weight: 700; }
|
||||
.cena-konacna { border-color: #111; }
|
||||
|
||||
/* potpisi */
|
||||
.potpisi { display: grid; grid-template-columns: 1fr 1fr; gap: 40px; margin-top: 32px; padding-top: 16px; border-top: 0.5px solid #ccc; }
|
||||
.potpis-linija { border-top: 1px solid #888; margin-top: 40px; padding-top: 6px; font-size: 11px; color: #555; }
|
||||
|
||||
/* štampa */
|
||||
@media print {
|
||||
body { font-size: 12px; }
|
||||
.strana { padding: 16px 20px; max-width: 100%; }
|
||||
.dugme-stampa { display: none !important; }
|
||||
@page { size: A4; margin: 12mm 14mm; }
|
||||
}
|
||||
|
||||
.dugme-stampa { position: fixed; bottom: 24px; right: 24px; background: #111; color: #fff; border: none; padding: 12px 22px; border-radius: 8px; font-size: 14px; font-weight: 500; cursor: pointer; box-shadow: 0 4px 12px rgba(0,0,0,0.2); }
|
||||
.dugme-stampa:hover { background: #333; }
|
||||
|
||||
.prazno { color: #aaa; font-style: italic; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="strana">
|
||||
|
||||
<!-- zaglavlje firme i broj naloga -->
|
||||
<div class="zaglavlje">
|
||||
<div>
|
||||
<div class="firma-naziv">{{if .NazivFirme}}{{.NazivFirme}}{{else}}— naziv firme —{{end}}</div>
|
||||
<div class="firma-info">
|
||||
{{if .Podnazlov}}{{.Podnazlov}}<br>{{end}}
|
||||
{{if .Adresa}}{{.Adresa}}<br>{{end}}
|
||||
{{if .Telefon}}Tel: {{.Telefon}}<br>{{end}}
|
||||
{{if .PIB}}PIB: {{.PIB}}{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="nalog-broj">
|
||||
<div style="display:flex;align-items:flex-start;gap:14px;justify-content:flex-end;">
|
||||
<div>
|
||||
<div class="nalog-naslov">Servisni nalog</div>
|
||||
<div class="nalog-broj-vrednost">{{.Nalog.BrojNaloga}}</div>
|
||||
<div style="margin-top:6px;"><span class="status">{{.Nalog.Status}}</span></div>
|
||||
</div>
|
||||
{{if .QRKod}}
|
||||
<img src="data:image/png;base64,{{.QRKod}}" width="80" height="80"
|
||||
alt="QR {{.Nalog.BrojNaloga}}"
|
||||
style="image-rendering:pixelated;border:1px solid #ddd;border-radius:4px;">
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- datumi i klijent -->
|
||||
<div class="odeljak">
|
||||
<div class="odeljak-naslov">Osnovni podaci</div>
|
||||
<div class="podaci-grid">
|
||||
<div>
|
||||
<div class="polje-labela">Datum prijema</div>
|
||||
<div class="polje-vrednost">{{.Nalog.DatumPrijema.Format "02.01.2006. u 15:04"}}</div>
|
||||
</div>
|
||||
{{if .Nalog.DatumZavrsetka}}
|
||||
<div>
|
||||
<div class="polje-labela">Datum završetka</div>
|
||||
<div class="polje-vrednost">{{.Nalog.DatumZavrsetka.Format "02.01.2006."}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{if .Nalog.GarancijaDo}}
|
||||
<div>
|
||||
<div class="polje-labela">Garancija do</div>
|
||||
<div class="polje-vrednost">{{.Nalog.GarancijaDo.Format "02.01.2006."}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<div>
|
||||
<div class="polje-labela">Klijent</div>
|
||||
<div class="polje-vrednost">{{if .KlijentNaziv}}{{.KlijentNaziv}}{{else}}<span class="prazno">—</span>{{end}}</div>
|
||||
</div>
|
||||
{{if .TehnicarNaziv}}
|
||||
<div>
|
||||
<div class="polje-labela">Tehničar</div>
|
||||
<div class="polje-vrednost">{{.TehnicarNaziv}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- uređaj -->
|
||||
<div class="odeljak">
|
||||
<div class="odeljak-naslov">Uređaj</div>
|
||||
<div class="podaci-grid" style="margin-bottom:12px;">
|
||||
<div>
|
||||
<div class="polje-labela">Naziv uređaja</div>
|
||||
<div class="polje-vrednost">{{.Nalog.Uredjaj}}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="polje-labela">Serijski broj</div>
|
||||
<div class="polje-vrednost-mono">{{if .Nalog.SerijskiBroj}}{{.Nalog.SerijskiBroj}}{{else}}<span class="prazno">—</span>{{end}}</div>
|
||||
</div>
|
||||
{{if .Nalog.PinUredjaja}}
|
||||
<div>
|
||||
<div class="polje-labela">PIN / lozinka</div>
|
||||
<div class="polje-vrednost-mono">{{.Nalog.PinUredjaja}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<div style="margin-bottom:10px;">
|
||||
<div class="polje-labela">Opis kvara</div>
|
||||
<div class="polje-tekst">{{if .Nalog.OpisKvara}}{{.Nalog.OpisKvara}}{{else}}<span class="prazno">—</span>{{end}}</div>
|
||||
</div>
|
||||
{{if or .Nalog.Ostecenja .Nalog.Pribor}}
|
||||
<div class="podaci-grid-2">
|
||||
{{if .Nalog.Ostecenja}}
|
||||
<div>
|
||||
<div class="polje-labela">Oštećenja pri prijemu</div>
|
||||
<div class="polje-tekst">{{.Nalog.Ostecenja}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{if .Nalog.Pribor}}
|
||||
<div>
|
||||
<div class="polje-labela">Pribor i oprema</div>
|
||||
<div class="polje-tekst">{{.Nalog.Pribor}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<!-- ugrađeni delovi -->
|
||||
{{if .ServisniDelovi}}
|
||||
<div class="odeljak">
|
||||
<div class="odeljak-naslov">Ugrađeni delovi</div>
|
||||
<table class="tabela">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Artikal</th>
|
||||
<th class="desno" style="width:80px;">Kol.</th>
|
||||
<th class="desno" style="width:130px;">Cena/kom</th>
|
||||
<th class="desno" style="width:130px;">Ukupno</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .ServisniDelovi}}
|
||||
<tr>
|
||||
<td>{{.ArtikalNaziv}}</td>
|
||||
<td class="desno">{{.Kolicina}}</td>
|
||||
<td class="desno">{{printf "%.2f" .CenaKomada}} din</td>
|
||||
<td class="desno">{{printf "%.2f" .Ukupno}} din</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
<tr class="ukupno-red">
|
||||
<td colspan="3">Ukupno delovi</td>
|
||||
<td class="desno">{{printf "%.2f" .UkupnoDelovi}} din</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<!-- cene -->
|
||||
<div class="odeljak">
|
||||
<div class="odeljak-naslov">Cene usluge</div>
|
||||
<div class="cene-grid">
|
||||
<div class="cena-blok">
|
||||
<div class="cena-labela">Procena od</div>
|
||||
<div class="cena-vrednost">{{if .Nalog.CenaOd}}{{.Nalog.CenaOdStr}} din{{else}}—{{end}}</div>
|
||||
</div>
|
||||
<div class="cena-blok">
|
||||
<div class="cena-labela">Procena do</div>
|
||||
<div class="cena-vrednost">{{if .Nalog.CenaDo}}{{.Nalog.CenaDoStr}} din{{else}}—{{end}}</div>
|
||||
</div>
|
||||
<div class="cena-blok">
|
||||
<div class="cena-labela">Avans</div>
|
||||
<div class="cena-vrednost">{{if .Nalog.Avans}}{{.Nalog.AvansStr}} din{{else}}—{{end}}</div>
|
||||
</div>
|
||||
<div class="cena-blok cena-konacna">
|
||||
<div class="cena-labela">Cena rada</div>
|
||||
<div class="cena-vrednost">{{if .Nalog.CenaKonacna}}{{.Nalog.CenaKonacnaStr}} din{{else}}—{{end}}</div>
|
||||
</div>
|
||||
</div>
|
||||
{{if .Nalog.PreostaloZaNaplatu}}
|
||||
<div style="margin-top:10px;padding:10px 14px;background:#f0fdf4;border:1px solid #86efac;border-radius:6px;display:flex;justify-content:space-between;align-items:center;">
|
||||
<span style="font-size:12px;color:#15803d;font-weight:500;">Preostalo za naplatu:</span>
|
||||
<span style="font-size:18px;font-weight:700;color:#15803d;">{{.Nalog.PreostaloZaNaplatuStr}} din</span>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{if .Nalog.Napomena}}
|
||||
<div class="odeljak">
|
||||
<div class="odeljak-naslov">Napomena</div>
|
||||
<div class="polje-tekst" style="color:#555;">{{.Nalog.Napomena}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<!-- potpisi -->
|
||||
<div class="potpisi">
|
||||
<div>
|
||||
<div class="potpis-linija">Predao klijent</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="potpis-linija">Primio tehničar</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<button class="dugme-stampa" onclick="window.print()">Štampaj</button>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -4,6 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Podešavanje — NTech</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "naslov"}}Stanje zaliha — NTech{{end}}
|
||||
|
||||
{{define "sadrzaj"}}
|
||||
<div class="kolona" style="gap:16px;">
|
||||
|
||||
<!-- zaglavlje -->
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px;">
|
||||
<div style="display:flex;align-items:center;gap:12px;">
|
||||
<a href="/izvestaji" class="nazad-link" style="margin-bottom:0;">
|
||||
<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>
|
||||
Izveštaji
|
||||
</a>
|
||||
</div>
|
||||
<div style="display:flex;gap:16px;flex-wrap:wrap;">
|
||||
<div style="text-align:right;">
|
||||
<div style="font-size:12px;color:var(--tekst-slabi);">Broj artikala</div>
|
||||
<div style="font-size:18px;font-weight:600;color:var(--tekst-glavni);">{{.BrojArtikala}}</div>
|
||||
</div>
|
||||
<div style="text-align:right;">
|
||||
<div style="font-size:12px;color:var(--tekst-slabi);">Ukupna vrednost zalihe</div>
|
||||
<div style="font-size:18px;font-weight:600;color:var(--tekst-glavni);">{{printf "%.2f" .UkupnaVrednost}} din</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- tabela -->
|
||||
<div class="kartica animiraj" style="padding:0;overflow:hidden;">
|
||||
<div style="padding:14px 20px;border-bottom:0.5px solid var(--ivica);">
|
||||
<span style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">Stanje zaliha</span>
|
||||
</div>
|
||||
|
||||
{{if not .Zalihe}}
|
||||
<div style="padding:40px;text-align:center;color:var(--tekst-slabi);font-size:14px;">
|
||||
Nema artikala u magacinu.
|
||||
</div>
|
||||
{{else}}
|
||||
<div style="overflow-x:auto;">
|
||||
<table class="tabela">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Artikal</th>
|
||||
<th>Šifra</th>
|
||||
<th>Kategorija</th>
|
||||
<th style="text-align:right;">Stanje</th>
|
||||
<th style="text-align:right;">Min.</th>
|
||||
<th style="text-align:right;">Nab. cena</th>
|
||||
<th style="text-align:right;">Prod. cena</th>
|
||||
<th style="text-align:right;">Vrednost</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Zalihe}}
|
||||
<tr {{if le .Kolicina .KolicinMin}}style="background:rgba(220,38,38,0.05);"{{end}}>
|
||||
<td style="font-weight:500;{{if le .Kolicina .KolicinMin}}color:#dc2626;{{end}}">{{.Naziv}}</td>
|
||||
<td style="font-family:monospace;font-size:12px;color:var(--tekst-slabi);">{{if .Sifra}}{{.Sifra}}{{else}}—{{end}}</td>
|
||||
<td style="font-size:12px;color:var(--tekst-slabi);">{{if .Kategorija}}{{.Kategorija}}{{else}}—{{end}}</td>
|
||||
<td style="text-align:right;font-weight:600;{{if le .Kolicina .KolicinMin}}color:#dc2626;{{end}}">{{.Kolicina}}</td>
|
||||
<td style="text-align:right;font-size:12px;color:var(--tekst-slabi);">{{.KolicinMin}}</td>
|
||||
<td style="text-align:right;font-family:monospace;font-size:12px;">{{printf "%.2f" .NabavnaCena}}</td>
|
||||
<td style="text-align:right;font-family:monospace;font-size:12px;">{{printf "%.2f" .ProdajnaCena}}</td>
|
||||
<td style="text-align:right;font-family:monospace;font-weight:500;">{{printf "%.2f" .VrednostZalihe}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr style="border-top:1.5px solid var(--ivica);font-weight:600;">
|
||||
<td colspan="7" style="padding:10px 12px;font-size:13px;">Ukupna vrednost zalihe</td>
|
||||
<td style="text-align:right;padding:10px 12px;font-family:monospace;">{{printf "%.2f" .UkupnaVrednost}} din</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -4,6 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Dvostepena verifikacija — NTech</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f1117; min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 16px; }
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
if (window.innerWidth > 768 && localStorage.getItem('sidebar-skupljen') === 'true') {
|
||||
document.documentElement.classList.add('sidebar-init-skupljen');
|
||||
}
|
||||
// sakriva poruke-uspeha odmah da ne trepnu pre toast konverzije
|
||||
document.documentElement.classList.add('js-on');
|
||||
</script>
|
||||
|
||||
<!-- tema — učitava se prva -->
|
||||
@@ -57,6 +59,7 @@
|
||||
.topbar-naslov { color: rgba(255,255,255,0.95) !important; text-shadow: 0 1px 2px rgba(0,0,0,0.9), 0 0 6px rgba(0,0,0,0.55); }
|
||||
.kartica, .premesti-modal { background: rgba(0,0,0,{{if .AppPozadinaGlassOpacity}}{{.AppPozadinaGlassOpacity}}%{{else}}0.08{{end}}) !important; backdrop-filter: blur({{.AppPozadinaBlur}}px); -webkit-backdrop-filter: blur({{.AppPozadinaBlur}}px); border: 1px solid rgba(255,255,255,0.12) !important; }
|
||||
body:not([data-hover]) .kartica:hover { background: rgba(0,0,0,0.38) !important; }
|
||||
body:not([data-hover]) .kartica:hover .dash-ikona { filter: brightness(0.55); }
|
||||
.kartica p, .kartica span, .kartica h1, .kartica h2, .kartica h3, .kartica h4, .kartica label, .kartica td, .kartica th, .kartica li, .kartica a { color: rgba(255,255,255,0.95) !important; text-shadow: 0 1px 2px rgba(0,0,0,0.9), 0 0 6px rgba(0,0,0,0.55); }
|
||||
/* naslovi kartica i labele su goli <div> (boju im NE diramo, da namerno obojeni — crveni/akcenat — ostanu) — samo senka da se vide */
|
||||
.kartica div { text-shadow: 0 1px 2px rgba(0,0,0,0.9), 0 0 6px rgba(0,0,0,0.55); }
|
||||
@@ -225,9 +228,85 @@
|
||||
|
||||
{{block "dodatni-js" .}}{{end}}
|
||||
|
||||
<!-- modal za potvrdu akcije -->
|
||||
<div id="potvrda-modal" style="display:none;position:fixed;inset:0;z-index:9999;align-items:center;justify-content:center;">
|
||||
<div id="potvrda-pozadina" style="position:absolute;inset:0;background:rgba(0,0,0,0.55);backdrop-filter:blur(3px);"></div>
|
||||
<div style="position:relative;background:var(--kartica-pozadina);border:0.5px solid var(--ivica);border-radius:12px;padding:28px 28px 22px;max-width:380px;width:90%;box-shadow:0 20px 60px rgba(0,0,0,0.35);">
|
||||
<div style="font-size:15px;font-weight:500;color:var(--tekst-glavni);margin-bottom:20px;line-height:1.5;" id="potvrda-poruka"></div>
|
||||
<div style="display:flex;gap:10px;justify-content:flex-end;">
|
||||
<button id="potvrda-odustani" class="btn-sekundarno">Odustani</button>
|
||||
<button id="potvrda-potvrdi" class="btn-opasno">Potvrdi</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CSRF i potvrda: inicijalizacija na učitavanju i posle htmx swap-a -->
|
||||
<script>
|
||||
(function() {
|
||||
var modal = document.getElementById('potvrda-modal');
|
||||
var poruka = document.getElementById('potvrda-poruka');
|
||||
var btnPotvrdi = document.getElementById('potvrda-potvrdi');
|
||||
var btnOdustani = document.getElementById('potvrda-odustani');
|
||||
var pozadina = document.getElementById('potvrda-pozadina');
|
||||
var _resolveFn = null;
|
||||
|
||||
function prikaziModal(tekst) {
|
||||
return new Promise(function(resolve) {
|
||||
poruka.textContent = tekst;
|
||||
modal.style.display = 'flex';
|
||||
_resolveFn = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
function zatvoriModal(rezultat) {
|
||||
modal.style.display = 'none';
|
||||
if (_resolveFn) { _resolveFn(rezultat); _resolveFn = null; }
|
||||
}
|
||||
|
||||
btnPotvrdi.addEventListener('click', function() { zatvoriModal(true); });
|
||||
btnOdustani.addEventListener('click', function() { zatvoriModal(false); });
|
||||
pozadina.addEventListener('click', function() { zatvoriModal(false); });
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape' && modal.style.display === 'flex') zatvoriModal(false);
|
||||
});
|
||||
|
||||
window._ntechPotvrdi = prikaziModal;
|
||||
})();
|
||||
|
||||
// toast obaveštenja — prikazuje poruku u uglu ekrana, nestaje posle 4s
|
||||
window.ntechToast = function(tekst, tip) {
|
||||
var t = document.createElement('div');
|
||||
t.className = 'toast ' + (tip === 'greska' ? 'toast-greska' : 'toast-uspeh');
|
||||
// ikonica
|
||||
var svg = tip === 'greska'
|
||||
? '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24"><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>'
|
||||
: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg>';
|
||||
t.innerHTML = svg + '<span>' + tekst + '</span>';
|
||||
// prilagođavamo poziciju za mobilne uređaje
|
||||
t.style.bottom = '24px';
|
||||
t.style.right = '16px';
|
||||
document.body.appendChild(t);
|
||||
var ukloni = function() {
|
||||
t.classList.add('nestaje');
|
||||
setTimeout(function() { if (t.parentNode) t.parentNode.removeChild(t); }, 320);
|
||||
};
|
||||
t.addEventListener('click', ukloni);
|
||||
setTimeout(ukloni, 4000);
|
||||
};
|
||||
|
||||
// konvertuje sve .poruka-uspeh u toast i sklanja original
|
||||
function ntechKonvertujPoruke() {
|
||||
document.querySelectorAll('.poruka-uspeh').forEach(function(el) {
|
||||
if (el.dataset.toastPrikazan) return;
|
||||
el.dataset.toastPrikazan = '1';
|
||||
var tekst = el.textContent.trim();
|
||||
el.style.display = 'none';
|
||||
if (tekst) window.ntechToast(tekst, 'uspeh');
|
||||
});
|
||||
}
|
||||
|
||||
function ntechInicijalizuj() {
|
||||
ntechKonvertujPoruke();
|
||||
var m = document.querySelector('meta[name="csrf-token"]');
|
||||
if (m && m.content) {
|
||||
document.querySelectorAll('form[method="POST"],form[method="post"]').forEach(function(f) {
|
||||
@@ -241,11 +320,18 @@
|
||||
if (el._potvrda) return;
|
||||
el._potvrda = true;
|
||||
el.addEventListener('click', function(e) {
|
||||
if (!confirm(el.getAttribute('data-potvrda'))) e.preventDefault();
|
||||
e.preventDefault();
|
||||
window._ntechPotvrdi(el.getAttribute('data-potvrda')).then(function(ok) {
|
||||
if (!ok) return;
|
||||
// dugme unutar forme — submit forme
|
||||
var forma = el.closest('form');
|
||||
if (forma) { forma.submit(); return; }
|
||||
// link
|
||||
if (el.href) { window.location.href = el.href; }
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
// htmx:afterSettle listener se dodaje samo jednom — ne sme da se gomila po swap-ovima
|
||||
if (!window._ntechCsrfDodato) {
|
||||
window._ntechCsrfDodato = true;
|
||||
document.addEventListener('htmx:afterSettle', ntechInicijalizuj);
|
||||
|
||||
Reference in New Issue
Block a user