Popravka sidebara: kolaps, podmeni i HTMX navigacija
This commit is contained in:
@@ -191,6 +191,8 @@ func main() {
|
||||
r.Post("/servis/izmeni/{id}", h.SacuvajIzmenaNaloga)
|
||||
r.Post("/servis/obrisi/{id}", h.ObrisiNalog)
|
||||
r.Get("/servis/{id}", h.DetaljiNaloga)
|
||||
r.Post("/servis/{id}/delovi", h.DodajDeloNalogu)
|
||||
r.Post("/servis/{id}/delovi/{deo_id}/obrisi", h.ObrisiDeloNaloga)
|
||||
r.Get("/izvestaji", h.Izvestaji)
|
||||
r.Get("/prodaja", h.Prodaja)
|
||||
r.Get("/prodaja/nova", h.NovaProdaja)
|
||||
|
||||
@@ -71,11 +71,24 @@ type ProdajaRepository interface {
|
||||
Lista(ctx context.Context, pretraga string) ([]model.ProdajniNalogSaDetaljem, error)
|
||||
DohvatiID(ctx context.Context, id int64) (*model.ProdajniNalog, error)
|
||||
DohvatiStavke(ctx context.Context, nalogID int64) ([]model.StavkaProdajeSaArtiklom, error)
|
||||
Kreiraj(ctx context.Context, n *model.ProdajniNalog, stavke []model.StavkaProdaje) (int64, error)
|
||||
Kreiraj(ctx context.Context, n *model.ProdajniNalog, stavke []model.StavkaProdaje, korisnikID *int64) (int64, error)
|
||||
Storno(ctx context.Context, id int64, razlog string, korisnikID *int64) error
|
||||
Obrisi(ctx context.Context, id int64) error
|
||||
SledeciBroj(ctx context.Context) (string, error)
|
||||
}
|
||||
|
||||
// ServisniDeloviRepository definiše operacije nad ugrađenim delovima u servisu
|
||||
type ServisniDeloviRepository interface {
|
||||
DohvatiZaNalog(ctx context.Context, nalogID int64) ([]model.ServisniDeoSaArtiklom, error)
|
||||
Dodaj(ctx context.Context, nalogID, artikalID int64, kolicina int, cenaKomada float64, korisnikID *int64) (int64, error)
|
||||
Obrisi(ctx context.Context, id int64, korisnikID *int64) error
|
||||
}
|
||||
|
||||
// MagacinskePromeneRepository definiše operacije nad revizijskim tragom magacina
|
||||
type MagacinskePromeneRepository interface {
|
||||
Lista(ctx context.Context, artikalID *int64, limit int) ([]model.MagacinskaPromenaSaDetaljem, error)
|
||||
}
|
||||
|
||||
// KorisniciRepository definiše operacije nad korisnicima
|
||||
type KorisniciRepository interface {
|
||||
Kreiraj(ctx context.Context, korisnickoIme, lozinkaHash, uloga string) (*model.Korisnik, error)
|
||||
|
||||
@@ -25,7 +25,7 @@ func (r *ArtikalRepo) Lista(ctx context.Context, filter db.ArtikalFilter) ([]mod
|
||||
SELECT
|
||||
a.id, a.kategorija_id, a.naziv, a.opis,
|
||||
a.kolicina, a.kolicina_min, a.lokacija,
|
||||
a.prodajna_cena, a.napomena, a.datum_unosa,
|
||||
a.nabavna_cena, a.prodajna_cena, a.pdv_stopa, a.napomena, a.datum_unosa,
|
||||
COALESCE(k.naziv, '') as kategorija_naziv
|
||||
FROM artikli a
|
||||
LEFT JOIN kategorije k ON a.kategorija_id = k.id
|
||||
@@ -63,7 +63,7 @@ func (r *ArtikalRepo) Lista(ctx context.Context, filter db.ArtikalFilter) ([]mod
|
||||
err := redovi.Scan(
|
||||
&a.ID, &kategorijaID, &a.Naziv, &a.Opis,
|
||||
&a.Kolicina, &a.KolicinMin, &a.Lokacija,
|
||||
&a.ProdajnaCena, &a.Napomena, &a.DatumUnosa,
|
||||
&a.NabavnaCena, &a.ProdajnaCena, &a.PdvStopa, &a.Napomena, &a.DatumUnosa,
|
||||
&a.KategorijaNaziv,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -89,11 +89,11 @@ func (r *ArtikalRepo) DohvatiID(ctx context.Context, id int64) (*model.Artikal,
|
||||
|
||||
err := r.db.QueryRowContext(ctx, `
|
||||
SELECT id, kategorija_id, naziv, opis, kolicina, kolicina_min,
|
||||
lokacija, prodajna_cena, napomena, datum_unosa
|
||||
lokacija, nabavna_cena, prodajna_cena, pdv_stopa, napomena, datum_unosa
|
||||
FROM artikli WHERE id = ?`, id).Scan(
|
||||
&a.ID, &kategorijaID, &a.Naziv, &a.Opis,
|
||||
&a.Kolicina, &a.KolicinMin, &a.Lokacija,
|
||||
&a.ProdajnaCena, &a.Napomena, &a.DatumUnosa,
|
||||
&a.NabavnaCena, &a.ProdajnaCena, &a.PdvStopa, &a.Napomena, &a.DatumUnosa,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ntech: ArtikalRepo.DohvatiID: %w", err)
|
||||
@@ -111,10 +111,10 @@ func (r *ArtikalRepo) Kreiraj(ctx context.Context, a *model.Artikal) (int64, err
|
||||
rezultat, err := r.db.ExecContext(ctx, `
|
||||
INSERT INTO artikli
|
||||
(kategorija_id, naziv, opis, kolicina, kolicina_min, lokacija,
|
||||
prodajna_cena, napomena)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
nabavna_cena, prodajna_cena, pdv_stopa, napomena)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
a.KategorijaID, a.Naziv, a.Opis, a.Kolicina, a.KolicinMin,
|
||||
a.Lokacija, a.ProdajnaCena, a.Napomena,
|
||||
a.Lokacija, a.NabavnaCena, a.ProdajnaCena, a.PdvStopa, a.Napomena,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("ntech: ArtikalRepo.Kreiraj: %w", err)
|
||||
@@ -134,11 +134,11 @@ func (r *ArtikalRepo) Izmeni(ctx context.Context, a *model.Artikal) error {
|
||||
UPDATE artikli SET
|
||||
kategorija_id = ?, naziv = ?, opis = ?, kolicina = ?,
|
||||
kolicina_min = ?, lokacija = ?,
|
||||
prodajna_cena = ?, napomena = ?
|
||||
nabavna_cena = ?, prodajna_cena = ?, pdv_stopa = ?, napomena = ?
|
||||
WHERE id = ?`,
|
||||
a.KategorijaID, a.Naziv, a.Opis, a.Kolicina,
|
||||
a.KolicinMin, a.Lokacija,
|
||||
a.ProdajnaCena, a.Napomena, a.ID,
|
||||
a.NabavnaCena, a.ProdajnaCena, a.PdvStopa, a.Napomena, a.ID,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntech: ArtikalRepo.Izmeni: %w", err)
|
||||
|
||||
@@ -21,7 +21,7 @@ func NoviKlijentRepo(db *sql.DB) *KlijentRepo {
|
||||
// Lista vraća listu klijenata sa opcionom pretragom po imenu, prezimenu ili nazivu firme
|
||||
func (r *KlijentRepo) Lista(ctx context.Context, pretraga string) ([]model.Klijent, error) {
|
||||
upit := `
|
||||
SELECT id, ime, prezime, naziv_firme, pib, telefon, email, napomena, datum_unosa
|
||||
SELECT id, tip, ime, prezime, jmbg, naziv_firme, pib, telefon, email, napomena, datum_unosa
|
||||
FROM klijenti
|
||||
WHERE 1=1`
|
||||
|
||||
@@ -44,15 +44,16 @@ func (r *KlijentRepo) Lista(ctx context.Context, pretraga string) ([]model.Klije
|
||||
var rezultat []model.Klijent
|
||||
for redovi.Next() {
|
||||
var k model.Klijent
|
||||
var ime, prezime, nazivFirme, pib, telefon, email, napomena sql.NullString
|
||||
var ime, prezime, jmbg, nazivFirme, pib, telefon, email, napomena sql.NullString
|
||||
err := redovi.Scan(
|
||||
&k.ID, &ime, &prezime, &nazivFirme, &pib, &telefon, &email, &napomena, &k.DatumUnosa,
|
||||
&k.ID, &k.Tip, &ime, &prezime, &jmbg, &nazivFirme, &pib, &telefon, &email, &napomena, &k.DatumUnosa,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ntech: KlijentRepo.Lista: scan: %w", err)
|
||||
}
|
||||
k.Ime = ime.String
|
||||
k.Prezime = prezime.String
|
||||
k.JMBG = jmbg.String
|
||||
k.NazivFirme = nazivFirme.String
|
||||
k.PIB = pib.String
|
||||
k.Telefon = telefon.String
|
||||
@@ -67,12 +68,12 @@ func (r *KlijentRepo) Lista(ctx context.Context, pretraga string) ([]model.Klije
|
||||
// DohvatiID vraća jednog klijenta po ID-u
|
||||
func (r *KlijentRepo) DohvatiID(ctx context.Context, id int64) (*model.Klijent, error) {
|
||||
var k model.Klijent
|
||||
var ime, prezime, nazivFirme, pib, telefon, email, napomena sql.NullString
|
||||
var ime, prezime, jmbg, nazivFirme, pib, telefon, email, napomena sql.NullString
|
||||
|
||||
err := r.db.QueryRowContext(ctx, `
|
||||
SELECT id, ime, prezime, naziv_firme, pib, telefon, email, napomena, datum_unosa
|
||||
SELECT id, tip, ime, prezime, jmbg, naziv_firme, pib, telefon, email, napomena, datum_unosa
|
||||
FROM klijenti WHERE id = ?`, id).Scan(
|
||||
&k.ID, &ime, &prezime, &nazivFirme, &pib, &telefon, &email, &napomena, &k.DatumUnosa,
|
||||
&k.ID, &k.Tip, &ime, &prezime, &jmbg, &nazivFirme, &pib, &telefon, &email, &napomena, &k.DatumUnosa,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ntech: KlijentRepo.DohvatiID: %w", err)
|
||||
@@ -80,6 +81,7 @@ func (r *KlijentRepo) DohvatiID(ctx context.Context, id int64) (*model.Klijent,
|
||||
|
||||
k.Ime = ime.String
|
||||
k.Prezime = prezime.String
|
||||
k.JMBG = jmbg.String
|
||||
k.NazivFirme = nazivFirme.String
|
||||
k.PIB = pib.String
|
||||
k.Telefon = telefon.String
|
||||
@@ -91,11 +93,15 @@ func (r *KlijentRepo) DohvatiID(ctx context.Context, id int64) (*model.Klijent,
|
||||
|
||||
// Kreiraj dodaje novog klijenta u bazu
|
||||
func (r *KlijentRepo) Kreiraj(ctx context.Context, k *model.Klijent) (int64, error) {
|
||||
if k.Tip == "" {
|
||||
k.Tip = "fizicko"
|
||||
}
|
||||
rezultat, err := r.db.ExecContext(ctx, `
|
||||
INSERT INTO klijenti (ime, prezime, naziv_firme, pib, telefon, email, napomena)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
nullString(k.Ime), nullString(k.Prezime), nullString(k.NazivFirme),
|
||||
nullString(k.PIB), nullString(k.Telefon), nullString(k.Email), nullString(k.Napomena),
|
||||
INSERT INTO klijenti (tip, ime, prezime, jmbg, naziv_firme, pib, telefon, email, napomena)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
k.Tip, nullString(k.Ime), nullString(k.Prezime), nullString(k.JMBG),
|
||||
nullString(k.NazivFirme), nullString(k.PIB), nullString(k.Telefon),
|
||||
nullString(k.Email), nullString(k.Napomena),
|
||||
)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("ntech: KlijentRepo.Kreiraj: %w", err)
|
||||
@@ -111,13 +117,17 @@ func (r *KlijentRepo) Kreiraj(ctx context.Context, k *model.Klijent) (int64, err
|
||||
|
||||
// Izmeni ažurira postojećeg klijenta
|
||||
func (r *KlijentRepo) Izmeni(ctx context.Context, k *model.Klijent) error {
|
||||
if k.Tip == "" {
|
||||
k.Tip = "fizicko"
|
||||
}
|
||||
_, err := r.db.ExecContext(ctx, `
|
||||
UPDATE klijenti SET
|
||||
ime = ?, prezime = ?, naziv_firme = ?, pib = ?, telefon = ?, email = ?, napomena = ?
|
||||
tip = ?, ime = ?, prezime = ?, jmbg = ?, naziv_firme = ?,
|
||||
pib = ?, telefon = ?, email = ?, napomena = ?
|
||||
WHERE id = ?`,
|
||||
nullString(k.Ime), nullString(k.Prezime), nullString(k.NazivFirme),
|
||||
nullString(k.PIB), nullString(k.Telefon), nullString(k.Email), nullString(k.Napomena),
|
||||
k.ID,
|
||||
k.Tip, nullString(k.Ime), nullString(k.Prezime), nullString(k.JMBG),
|
||||
nullString(k.NazivFirme), nullString(k.PIB), nullString(k.Telefon),
|
||||
nullString(k.Email), nullString(k.Napomena), k.ID,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntech: KlijentRepo.Izmeni: %w", err)
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"ntech/internal/model"
|
||||
)
|
||||
|
||||
// MagacinskePromeneRepo je SQLite implementacija MagacinskePromeneRepository interfejsa
|
||||
type MagacinskePromeneRepo struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// NoviMagacinskePromeneRepo kreira novi MagacinskePromeneRepo
|
||||
func NoviMagacinskePromeneRepo(db *sql.DB) *MagacinskePromeneRepo {
|
||||
return &MagacinskePromeneRepo{db: db}
|
||||
}
|
||||
|
||||
// Lista vraća listu magacinskih promena, opcionalno filtrirano po artiklu
|
||||
func (r *MagacinskePromeneRepo) Lista(ctx context.Context, artikalID *int64, limit int) ([]model.MagacinskaPromenaSaDetaljem, error) {
|
||||
if limit <= 0 {
|
||||
limit = 100
|
||||
}
|
||||
|
||||
upit := `
|
||||
SELECT mp.id, mp.artikal_id, a.naziv, mp.tip_promene, mp.referentni_id,
|
||||
mp.promena_kolicine, mp.stanje_pre, mp.stanje_posle,
|
||||
mp.korisnik_id, mp.napomena, mp.datum
|
||||
FROM magacinske_promene mp
|
||||
JOIN artikli a ON a.id = mp.artikal_id
|
||||
WHERE 1=1`
|
||||
|
||||
args := []any{}
|
||||
|
||||
if artikalID != nil {
|
||||
upit += " AND mp.artikal_id = ?"
|
||||
args = append(args, *artikalID)
|
||||
}
|
||||
|
||||
upit += " ORDER BY mp.datum DESC LIMIT ?"
|
||||
args = append(args, limit)
|
||||
|
||||
redovi, err := r.db.QueryContext(ctx, upit, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ntech: MagacinskePromeneRepo.Lista: %w", err)
|
||||
}
|
||||
defer redovi.Close()
|
||||
|
||||
var rezultat []model.MagacinskaPromenaSaDetaljem
|
||||
for redovi.Next() {
|
||||
var p model.MagacinskaPromenaSaDetaljem
|
||||
var korisnikID sql.NullInt64
|
||||
var napomena sql.NullString
|
||||
|
||||
err := redovi.Scan(
|
||||
&p.ID, &p.ArtikalID, &p.ArtikalNaziv, &p.TipPromene, &p.ReferentniID,
|
||||
&p.PromenaKolicine, &p.StanjePre, &p.StanjePosle,
|
||||
&korisnikID, &napomena, &p.Datum,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ntech: MagacinskePromeneRepo.Lista: scan: %w", err)
|
||||
}
|
||||
|
||||
if korisnikID.Valid {
|
||||
v := korisnikID.Int64
|
||||
p.KorisnikID = &v
|
||||
}
|
||||
p.Napomena = napomena.String
|
||||
|
||||
rezultat = append(rezultat, p)
|
||||
}
|
||||
|
||||
return rezultat, nil
|
||||
}
|
||||
|
||||
// zabeleziMagacinPromenu je interni helper koji upisuje jednu promenu stanja artikla.
|
||||
// Poziva se iz Kreiraj/Storno unutar postojeće transakcije.
|
||||
func zabeleziMagacinPromenu(
|
||||
ctx context.Context,
|
||||
tx *sql.Tx,
|
||||
artikalID int64,
|
||||
tipPromene string,
|
||||
promenaKolicine, stanjePre, stanjePosle int,
|
||||
referentniID int64,
|
||||
korisnikID *int64,
|
||||
napomena string,
|
||||
) error {
|
||||
_, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO magacinske_promene
|
||||
(artikal_id, tip_promene, promena_kolicine, stanje_pre, stanje_posle,
|
||||
referentni_id, korisnik_id, napomena)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
artikalID, tipPromene, promenaKolicine, stanjePre, stanjePosle,
|
||||
referentniID, nullInt64(korisnikID), nullString(napomena),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntech: zabeleziMagacinPromenu: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -4,8 +4,10 @@ import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
@@ -82,8 +84,14 @@ func PokreniMigracije(db *sql.DB, fsys fs.FS) error {
|
||||
|
||||
// izvršavamo SQL
|
||||
if _, err := db.Exec(string(sadrzaj)); err != nil {
|
||||
// "duplicate column name" znači da kolona već postoji — željeno stanje je ispunjeno,
|
||||
// pa nastavljamo i beležimo migraciju kao izvršenu
|
||||
if strings.Contains(err.Error(), "duplicate column name") {
|
||||
log.Printf("Upozorenje: migration %s: kolona već postoji, preskačemo", naziv)
|
||||
} else {
|
||||
return fmt.Errorf("ntech: PokreniMigracije: izvršavanje %s: %w", naziv, err)
|
||||
}
|
||||
}
|
||||
|
||||
// upisujemo u tabelu migracija da je izvršena
|
||||
if _, err := db.Exec("INSERT INTO migracije (naziv) VALUES (?)", naziv); err != nil {
|
||||
|
||||
+158
-44
@@ -41,7 +41,8 @@ func (r *ProdajaRepo) SledeciBroj(ctx context.Context) (string, error) {
|
||||
func (r *ProdajaRepo) Lista(ctx context.Context, pretraga string) ([]model.ProdajniNalogSaDetaljem, error) {
|
||||
upit := `
|
||||
SELECT
|
||||
pn.id, pn.klijent_id, pn.broj_naloga, pn.napomena, pn.ukupno, pn.datum,
|
||||
pn.id, pn.klijent_id, pn.broj_naloga, pn.napomena, pn.ukupno,
|
||||
pn.nacin_placanja, pn.stornirano, pn.datum,
|
||||
COALESCE(NULLIF(k.naziv_firme, ''), TRIM(COALESCE(k.ime, '') || ' ' || COALESCE(k.prezime, '')), '') AS klijent_naziv
|
||||
FROM prodajni_nalozi pn
|
||||
LEFT JOIN klijenti k ON k.id = pn.klijent_id
|
||||
@@ -69,7 +70,8 @@ func (r *ProdajaRepo) Lista(ctx context.Context, pretraga string) ([]model.Proda
|
||||
var napomena sql.NullString
|
||||
|
||||
err := redovi.Scan(
|
||||
&n.ID, &klijentID, &n.BrojNaloga, &napomena, &n.Ukupno, &n.Datum,
|
||||
&n.ID, &klijentID, &n.BrojNaloga, &napomena, &n.Ukupno,
|
||||
&n.NacinPlacanja, &n.Stornirano, &n.Datum,
|
||||
&n.KlijentNaziv,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -91,14 +93,18 @@ func (r *ProdajaRepo) Lista(ctx context.Context, pretraga string) ([]model.Proda
|
||||
// DohvatiID vraća jedan prodajni nalog po ID-u
|
||||
func (r *ProdajaRepo) DohvatiID(ctx context.Context, id int64) (*model.ProdajniNalog, error) {
|
||||
red := r.db.QueryRowContext(ctx, `
|
||||
SELECT id, klijent_id, broj_naloga, napomena, ukupno, datum
|
||||
SELECT id, klijent_id, broj_naloga, napomena, ukupno,
|
||||
nacin_placanja, stornirano, razlog_storniranja, datum
|
||||
FROM prodajni_nalozi WHERE id = ?`, id)
|
||||
|
||||
var n model.ProdajniNalog
|
||||
var klijentID sql.NullInt64
|
||||
var napomena sql.NullString
|
||||
var napomena, razlogStorniranja sql.NullString
|
||||
|
||||
err := red.Scan(&n.ID, &klijentID, &n.BrojNaloga, &napomena, &n.Ukupno, &n.Datum)
|
||||
err := red.Scan(
|
||||
&n.ID, &klijentID, &n.BrojNaloga, &napomena, &n.Ukupno,
|
||||
&n.NacinPlacanja, &n.Stornirano, &razlogStorniranja, &n.Datum,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ntech: ProdajaRepo.DohvatiID: %w", err)
|
||||
}
|
||||
@@ -108,15 +114,16 @@ func (r *ProdajaRepo) DohvatiID(ctx context.Context, id int64) (*model.ProdajniN
|
||||
n.KlijentID = &v
|
||||
}
|
||||
n.Napomena = napomena.String
|
||||
n.RazlogStorniranja = razlogStorniranja.String
|
||||
|
||||
return &n, nil
|
||||
}
|
||||
|
||||
// DohvatiStavke vraća stavke prodaje sa nazivima artikala za dati nalog
|
||||
// DohvatiStavke vraća stavke prodaje sa nazivima artikala i PDV podacima za dati nalog
|
||||
func (r *ProdajaRepo) DohvatiStavke(ctx context.Context, nalogID int64) ([]model.StavkaProdajeSaArtiklom, error) {
|
||||
redovi, err := r.db.QueryContext(ctx, `
|
||||
SELECT sp.id, sp.nalog_id, sp.artikal_id, sp.kolicina, sp.cena_po_komadu, sp.ukupno,
|
||||
a.naziv
|
||||
sp.pdv_stopa, sp.pdv_iznos, sp.cena_bez_pdv, a.naziv
|
||||
FROM stavke_prodaje sp
|
||||
JOIN artikli a ON a.id = sp.artikal_id
|
||||
WHERE sp.nalog_id = ?
|
||||
@@ -131,7 +138,9 @@ func (r *ProdajaRepo) DohvatiStavke(ctx context.Context, nalogID int64) ([]model
|
||||
var s model.StavkaProdajeSaArtiklom
|
||||
err := redovi.Scan(
|
||||
&s.ID, &s.NalogID, &s.ArtikalID, &s.Kolicina,
|
||||
&s.CenaPoKomadu, &s.Ukupno, &s.ArtikalNaziv,
|
||||
&s.CenaPoKomadu, &s.Ukupno,
|
||||
&s.PdvStopa, &s.PdvIznos, &s.CenaBezPdv,
|
||||
&s.ArtikalNaziv,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ntech: ProdajaRepo.DohvatiStavke: scan: %w", err)
|
||||
@@ -143,44 +152,24 @@ func (r *ProdajaRepo) DohvatiStavke(ctx context.Context, nalogID int64) ([]model
|
||||
}
|
||||
|
||||
// Kreiraj upisuje novi prodajni nalog u bazu u okviru jedne transakcije.
|
||||
// Za svaku stavku proverava stanje u magacinu i smanjuje ga.
|
||||
// Za svaku stavku proverava stanje u magacinu, smanjuje ga i beleži promenu.
|
||||
// Ako bilo koji artikal nema dovoljno stanja, vraća ErrNedovoljnoKolicine.
|
||||
func (r *ProdajaRepo) Kreiraj(ctx context.Context, n *model.ProdajniNalog, stavke []model.StavkaProdaje) (int64, error) {
|
||||
func (r *ProdajaRepo) Kreiraj(ctx context.Context, n *model.ProdajniNalog, stavke []model.StavkaProdaje, korisnikID *int64) (int64, error) {
|
||||
if n.NacinPlacanja == "" {
|
||||
n.NacinPlacanja = "gotovina"
|
||||
}
|
||||
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("ntech: ProdajaRepo.Kreiraj: begin tx: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
// provera i smanjenje stanja za svaku stavku
|
||||
for _, s := range stavke {
|
||||
var naziv string
|
||||
var kolicinaNaStanju int
|
||||
err := tx.QueryRowContext(ctx,
|
||||
"SELECT naziv, kolicina FROM artikli WHERE id = ?", s.ArtikalID,
|
||||
).Scan(&naziv, &kolicinaNaStanju)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("ntech: ProdajaRepo.Kreiraj: dohvati artikal: %w", err)
|
||||
}
|
||||
|
||||
if kolicinaNaStanju < s.Kolicina {
|
||||
return 0, &db.ErrNedovoljnoKolicine{ArtikalNaziv: naziv}
|
||||
}
|
||||
|
||||
_, err = tx.ExecContext(ctx,
|
||||
"UPDATE artikli SET kolicina = kolicina - ? WHERE id = ?",
|
||||
s.Kolicina, s.ArtikalID,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("ntech: ProdajaRepo.Kreiraj: update stanje: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// insert zaglavlja naloga
|
||||
// insert zaglavlja naloga pre stavki da bismo imali nalogID za magacin
|
||||
rezultat, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO prodajni_nalozi (klijent_id, broj_naloga, napomena, ukupno, datum)
|
||||
VALUES (?, ?, ?, ?, ?)`,
|
||||
nullInt64(n.KlijentID), n.BrojNaloga, nullString(n.Napomena), n.Ukupno, n.Datum,
|
||||
INSERT INTO prodajni_nalozi (klijent_id, broj_naloga, napomena, ukupno, nacin_placanja, datum)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
nullInt64(n.KlijentID), n.BrojNaloga, nullString(n.Napomena), n.Ukupno, n.NacinPlacanja, n.Datum,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("ntech: ProdajaRepo.Kreiraj: insert nalog: %w", err)
|
||||
@@ -191,17 +180,54 @@ func (r *ProdajaRepo) Kreiraj(ctx context.Context, n *model.ProdajniNalog, stavk
|
||||
return 0, fmt.Errorf("ntech: ProdajaRepo.Kreiraj: last insert id: %w", err)
|
||||
}
|
||||
|
||||
// insert stavki
|
||||
// provera stanja, smanjenje i insert stavki
|
||||
for _, s := range stavke {
|
||||
var naziv string
|
||||
var stanjePre int
|
||||
err := tx.QueryRowContext(ctx,
|
||||
"SELECT naziv, kolicina FROM artikli WHERE id = ?", s.ArtikalID,
|
||||
).Scan(&naziv, &stanjePre)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("ntech: ProdajaRepo.Kreiraj: dohvati artikal: %w", err)
|
||||
}
|
||||
|
||||
if stanjePre < s.Kolicina {
|
||||
return 0, &db.ErrNedovoljnoKolicine{ArtikalNaziv: naziv}
|
||||
}
|
||||
|
||||
stanjePosle := stanjePre - s.Kolicina
|
||||
_, err = tx.ExecContext(ctx,
|
||||
"UPDATE artikli SET kolicina = ? WHERE id = ?", stanjePosle, s.ArtikalID,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("ntech: ProdajaRepo.Kreiraj: update stanje: %w", err)
|
||||
}
|
||||
|
||||
// PDV računamo iz cene ako nije eksplicitno postavljeno
|
||||
cenaBezPdv := s.CenaBezPdv
|
||||
pdvIznos := s.PdvIznos
|
||||
if cenaBezPdv == 0 && s.PdvStopa > 0 {
|
||||
cenaBezPdv = s.CenaPoKomadu / (1 + s.PdvStopa/100)
|
||||
pdvIznos = s.CenaPoKomadu - cenaBezPdv
|
||||
}
|
||||
|
||||
ukupnoStavke := float64(s.Kolicina) * s.CenaPoKomadu
|
||||
_, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO stavke_prodaje (nalog_id, artikal_id, kolicina, cena_po_komadu, ukupno)
|
||||
VALUES (?, ?, ?, ?, ?)`,
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
INSERT INTO stavke_prodaje
|
||||
(nalog_id, artikal_id, kolicina, cena_po_komadu, ukupno, pdv_stopa, pdv_iznos, cena_bez_pdv)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
nalogID, s.ArtikalID, s.Kolicina, s.CenaPoKomadu, ukupnoStavke,
|
||||
s.PdvStopa, pdvIznos, cenaBezPdv,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("ntech: ProdajaRepo.Kreiraj: insert stavka: %w", err)
|
||||
}
|
||||
|
||||
err = zabeleziMagacinPromenu(ctx, tx, s.ArtikalID, model.PromenaIzlazProdaja,
|
||||
-s.Kolicina, stanjePre, stanjePosle, nalogID, korisnikID, "")
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("ntech: ProdajaRepo.Kreiraj: magacin: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
@@ -211,6 +237,83 @@ func (r *ProdajaRepo) Kreiraj(ctx context.Context, n *model.ProdajniNalog, stavk
|
||||
return nalogID, nil
|
||||
}
|
||||
|
||||
// Storno označava prodajni nalog kao storniran i vraća sve artikle na stanje
|
||||
func (r *ProdajaRepo) Storno(ctx context.Context, id int64, razlog string, korisnikID *int64) error {
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntech: ProdajaRepo.Storno: begin tx: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
// proverava da li je već storniran
|
||||
var stornirano bool
|
||||
err = tx.QueryRowContext(ctx, "SELECT stornirano FROM prodajni_nalozi WHERE id = ?", id).Scan(&stornirano)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntech: ProdajaRepo.Storno: provera: %w", err)
|
||||
}
|
||||
if stornirano {
|
||||
return fmt.Errorf("ntech: ProdajaRepo.Storno: nalog je već storniran")
|
||||
}
|
||||
|
||||
// vraćanje stanja u magacin
|
||||
redovi, err := tx.QueryContext(ctx,
|
||||
"SELECT artikal_id, kolicina FROM stavke_prodaje WHERE nalog_id = ?", id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntech: ProdajaRepo.Storno: dohvati stavke: %w", err)
|
||||
}
|
||||
|
||||
type stavkaPovrat struct {
|
||||
artikalID int64
|
||||
kolicina int
|
||||
}
|
||||
var stavke []stavkaPovrat
|
||||
for redovi.Next() {
|
||||
var p stavkaPovrat
|
||||
if err := redovi.Scan(&p.artikalID, &p.kolicina); err != nil {
|
||||
redovi.Close()
|
||||
return fmt.Errorf("ntech: ProdajaRepo.Storno: scan stavke: %w", err)
|
||||
}
|
||||
stavke = append(stavke, p)
|
||||
}
|
||||
redovi.Close()
|
||||
|
||||
for _, p := range stavke {
|
||||
var stanjePre int
|
||||
err := tx.QueryRowContext(ctx, "SELECT kolicina FROM artikli WHERE id = ?", p.artikalID).Scan(&stanjePre)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntech: ProdajaRepo.Storno: dohvati stanje: %w", err)
|
||||
}
|
||||
|
||||
stanjePosle := stanjePre + p.kolicina
|
||||
_, err = tx.ExecContext(ctx,
|
||||
"UPDATE artikli SET kolicina = ? WHERE id = ?", stanjePosle, p.artikalID,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntech: ProdajaRepo.Storno: vrati stanje: %w", err)
|
||||
}
|
||||
|
||||
err = zabeleziMagacinPromenu(ctx, tx, p.artikalID, model.PromenaPovracaj,
|
||||
p.kolicina, stanjePre, stanjePosle, id, korisnikID, razlog)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntech: ProdajaRepo.Storno: magacin: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
_, err = tx.ExecContext(ctx,
|
||||
"UPDATE prodajni_nalozi SET stornirano = 1, razlog_storniranja = ? WHERE id = ?",
|
||||
nullString(razlog), id,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntech: ProdajaRepo.Storno: update nalog: %w", err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return fmt.Errorf("ntech: ProdajaRepo.Storno: commit: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Obrisi briše prodajni nalog i vraća količine artikala na stanje (u transakciji)
|
||||
func (r *ProdajaRepo) Obrisi(ctx context.Context, id int64) error {
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
@@ -219,14 +322,24 @@ func (r *ProdajaRepo) Obrisi(ctx context.Context, id int64) error {
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
// vraćanje stanja u magacin
|
||||
// vraćanje stanja u magacin samo ako nalog nije storniran (storno je već vratio)
|
||||
var stornirano bool
|
||||
err = tx.QueryRowContext(ctx, "SELECT stornirano FROM prodajni_nalozi WHERE id = ?", id).Scan(&stornirano)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntech: ProdajaRepo.Obrisi: provera: %w", err)
|
||||
}
|
||||
|
||||
if !stornirano {
|
||||
redovi, err := tx.QueryContext(ctx,
|
||||
"SELECT artikal_id, kolicina FROM stavke_prodaje WHERE nalog_id = ?", id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntech: ProdajaRepo.Obrisi: dohvati stavke: %w", err)
|
||||
}
|
||||
|
||||
type povrat struct{ artikalID int64; kolicina int }
|
||||
type povrat struct {
|
||||
artikalID int64
|
||||
kolicina int
|
||||
}
|
||||
var stavke []povrat
|
||||
for redovi.Next() {
|
||||
var p povrat
|
||||
@@ -247,6 +360,7 @@ func (r *ProdajaRepo) Obrisi(ctx context.Context, id int64) error {
|
||||
return fmt.Errorf("ntech: ProdajaRepo.Obrisi: vrati stanje: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CASCADE briše i stavke_prodaje
|
||||
_, err = tx.ExecContext(ctx, "DELETE FROM prodajni_nalozi WHERE id = ?", id)
|
||||
|
||||
@@ -40,9 +40,9 @@ func (r *ServisRepo) SledeciBroj(ctx context.Context) (string, error) {
|
||||
func (r *ServisRepo) Lista(ctx context.Context, pretraga, status string) ([]model.ServisniNalogSaKlijentom, error) {
|
||||
upit := `
|
||||
SELECT
|
||||
sn.id, sn.klijent_id, sn.broj_naloga, sn.uredjaj, sn.serijski_broj,
|
||||
sn.id, sn.klijent_id, sn.tehnicar_id, sn.broj_naloga, sn.uredjaj, sn.serijski_broj,
|
||||
sn.opis_kvara, sn.status, sn.cena_od, sn.cena_do, sn.cena_konacna,
|
||||
sn.avans, sn.napomena, sn.datum_prijema, sn.datum_zavrsetka,
|
||||
sn.avans, sn.napomena, sn.garancija_do, sn.datum_prijema, sn.datum_zavrsetka,
|
||||
COALESCE(NULLIF(k.naziv_firme, ''), TRIM(COALESCE(k.ime, '') || ' ' || COALESCE(k.prezime, '')), '') AS klijent_naziv
|
||||
FROM servisni_nalozi sn
|
||||
LEFT JOIN klijenti k ON k.id = sn.klijent_id
|
||||
@@ -86,9 +86,9 @@ func (r *ServisRepo) Lista(ctx context.Context, pretraga, status string) ([]mode
|
||||
func (r *ServisRepo) DohvatiID(ctx context.Context, id int64) (*model.ServisniNalog, error) {
|
||||
red := r.db.QueryRowContext(ctx, `
|
||||
SELECT
|
||||
id, klijent_id, broj_naloga, uredjaj, serijski_broj,
|
||||
id, klijent_id, tehnicar_id, broj_naloga, uredjaj, serijski_broj,
|
||||
opis_kvara, status, cena_od, cena_do, cena_konacna,
|
||||
avans, napomena, datum_prijema, datum_zavrsetka
|
||||
avans, napomena, garancija_do, datum_prijema, datum_zavrsetka
|
||||
FROM servisni_nalozi WHERE id = ?`, id)
|
||||
|
||||
var n model.ServisniNalog
|
||||
@@ -104,13 +104,14 @@ func (r *ServisRepo) DohvatiID(ctx context.Context, id int64) (*model.ServisniNa
|
||||
func (r *ServisRepo) Kreiraj(ctx context.Context, n *model.ServisniNalog) (int64, error) {
|
||||
rezultat, err := r.db.ExecContext(ctx, `
|
||||
INSERT INTO servisni_nalozi
|
||||
(klijent_id, broj_naloga, uredjaj, serijski_broj, opis_kvara,
|
||||
status, cena_od, cena_do, cena_konacna, avans, napomena, datum_zavrsetka)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
nullInt64(n.KlijentID), n.BrojNaloga, n.Uredjaj, nullString(n.SerijskiBroj),
|
||||
n.OpisKvara, n.Status, nullFloat64(n.CenaOd), nullFloat64(n.CenaDo),
|
||||
nullFloat64(n.CenaKonacna), nullFloat64(n.Avans), nullString(n.Napomena),
|
||||
nullTime(n.DatumZavrsetka),
|
||||
(klijent_id, tehnicar_id, broj_naloga, uredjaj, serijski_broj, opis_kvara,
|
||||
status, cena_od, cena_do, cena_konacna, avans, napomena, garancija_do, datum_zavrsetka)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
nullInt64(n.KlijentID), nullInt64(n.TehnicarID), n.BrojNaloga, n.Uredjaj,
|
||||
nullString(n.SerijskiBroj), n.OpisKvara, n.Status,
|
||||
nullFloat64(n.CenaOd), nullFloat64(n.CenaDo), nullFloat64(n.CenaKonacna),
|
||||
nullFloat64(n.Avans), nullString(n.Napomena),
|
||||
nullTime(n.GarancijaDo), nullTime(n.DatumZavrsetka),
|
||||
)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("ntech: ServisRepo.Kreiraj: %w", err)
|
||||
@@ -128,13 +129,13 @@ func (r *ServisRepo) Kreiraj(ctx context.Context, n *model.ServisniNalog) (int64
|
||||
func (r *ServisRepo) Izmeni(ctx context.Context, n *model.ServisniNalog) error {
|
||||
_, err := r.db.ExecContext(ctx, `
|
||||
UPDATE servisni_nalozi SET
|
||||
klijent_id = ?, uredjaj = ?, serijski_broj = ?, opis_kvara = ?,
|
||||
klijent_id = ?, tehnicar_id = ?, uredjaj = ?, serijski_broj = ?, opis_kvara = ?,
|
||||
status = ?, cena_od = ?, cena_do = ?, cena_konacna = ?,
|
||||
avans = ?, napomena = ?, datum_zavrsetka = ?
|
||||
avans = ?, napomena = ?, garancija_do = ?, datum_zavrsetka = ?
|
||||
WHERE id = ?`,
|
||||
nullInt64(n.KlijentID), n.Uredjaj, nullString(n.SerijskiBroj), n.OpisKvara,
|
||||
nullInt64(n.KlijentID), nullInt64(n.TehnicarID), n.Uredjaj, nullString(n.SerijskiBroj), n.OpisKvara,
|
||||
n.Status, nullFloat64(n.CenaOd), nullFloat64(n.CenaDo), nullFloat64(n.CenaKonacna),
|
||||
nullFloat64(n.Avans), nullString(n.Napomena), nullTime(n.DatumZavrsetka),
|
||||
nullFloat64(n.Avans), nullString(n.Napomena), nullTime(n.GarancijaDo), nullTime(n.DatumZavrsetka),
|
||||
n.ID,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -157,15 +158,15 @@ func (r *ServisRepo) Obrisi(ctx context.Context, id int64) error {
|
||||
// scanNalog čita redove iz upita u ServisniNalog struct —
|
||||
// klijentNaziv je opcioni pokazivač, nil kada se čita bez JOIN-a
|
||||
func scanNalog(scan func(...any) error, n *model.ServisniNalog, klijentNaziv *string) error {
|
||||
var klijentID sql.NullInt64
|
||||
var klijentID, tehnicarID sql.NullInt64
|
||||
var serijskiBroj, napomena sql.NullString
|
||||
var cenaOd, cenaDo, cenaKonacna, avans sql.NullFloat64
|
||||
var datumZavrsetka sql.NullTime
|
||||
var garancijaDo, datumZavrsetka sql.NullTime
|
||||
|
||||
args := []any{
|
||||
&n.ID, &klijentID, &n.BrojNaloga, &n.Uredjaj, &serijskiBroj,
|
||||
&n.ID, &klijentID, &tehnicarID, &n.BrojNaloga, &n.Uredjaj, &serijskiBroj,
|
||||
&n.OpisKvara, &n.Status, &cenaOd, &cenaDo, &cenaKonacna,
|
||||
&avans, &napomena, &n.DatumPrijema, &datumZavrsetka,
|
||||
&avans, &napomena, &garancijaDo, &n.DatumPrijema, &datumZavrsetka,
|
||||
}
|
||||
|
||||
if klijentNaziv != nil {
|
||||
@@ -180,6 +181,10 @@ func scanNalog(scan func(...any) error, n *model.ServisniNalog, klijentNaziv *st
|
||||
v := klijentID.Int64
|
||||
n.KlijentID = &v
|
||||
}
|
||||
if tehnicarID.Valid {
|
||||
v := tehnicarID.Int64
|
||||
n.TehnicarID = &v
|
||||
}
|
||||
n.SerijskiBroj = serijskiBroj.String
|
||||
n.Napomena = napomena.String
|
||||
if cenaOd.Valid {
|
||||
@@ -198,6 +203,10 @@ func scanNalog(scan func(...any) error, n *model.ServisniNalog, klijentNaziv *st
|
||||
v := avans.Float64
|
||||
n.Avans = &v
|
||||
}
|
||||
if garancijaDo.Valid {
|
||||
v := garancijaDo.Time
|
||||
n.GarancijaDo = &v
|
||||
}
|
||||
if datumZavrsetka.Valid {
|
||||
v := datumZavrsetka.Time
|
||||
n.DatumZavrsetka = &v
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"ntech/internal/db"
|
||||
"ntech/internal/model"
|
||||
)
|
||||
|
||||
// ServisniDeloviRepo je SQLite implementacija ServisniDeloviRepository interfejsa
|
||||
type ServisniDeloviRepo struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// NoviServisniDeloviRepo kreira novi ServisniDeloviRepo
|
||||
func NoviServisniDeloviRepo(db *sql.DB) *ServisniDeloviRepo {
|
||||
return &ServisniDeloviRepo{db: db}
|
||||
}
|
||||
|
||||
// DohvatiZaNalog vraća sve ugrađene delove za dati servisni nalog
|
||||
func (r *ServisniDeloviRepo) DohvatiZaNalog(ctx context.Context, nalogID int64) ([]model.ServisniDeoSaArtiklom, error) {
|
||||
redovi, err := r.db.QueryContext(ctx, `
|
||||
SELECT sd.id, sd.nalog_id, sd.artikal_id, sd.kolicina, sd.cena_komada, sd.datum,
|
||||
a.naziv
|
||||
FROM servisni_delovi sd
|
||||
JOIN artikli a ON a.id = sd.artikal_id
|
||||
WHERE sd.nalog_id = ?
|
||||
ORDER BY sd.datum`, nalogID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ntech: ServisniDeloviRepo.DohvatiZaNalog: %w", err)
|
||||
}
|
||||
defer redovi.Close()
|
||||
|
||||
var rezultat []model.ServisniDeoSaArtiklom
|
||||
for redovi.Next() {
|
||||
var d model.ServisniDeoSaArtiklom
|
||||
err := redovi.Scan(
|
||||
&d.ID, &d.NalogID, &d.ArtikalID, &d.Kolicina, &d.CenaKomada, &d.Datum,
|
||||
&d.ArtikalNaziv,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ntech: ServisniDeloviRepo.DohvatiZaNalog: scan: %w", err)
|
||||
}
|
||||
rezultat = append(rezultat, d)
|
||||
}
|
||||
|
||||
return rezultat, nil
|
||||
}
|
||||
|
||||
// Dodaj dodaje jedan artikal u servisni nalog, smanjuje stanje u magacinu i beleži promenu
|
||||
func (r *ServisniDeloviRepo) Dodaj(ctx context.Context, nalogID, artikalID int64, kolicina int, cenaKomada float64, korisnikID *int64) (int64, error) {
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("ntech: ServisniDeloviRepo.Dodaj: begin tx: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
var naziv string
|
||||
var stanjePre int
|
||||
err = tx.QueryRowContext(ctx,
|
||||
"SELECT naziv, kolicina FROM artikli WHERE id = ?", artikalID,
|
||||
).Scan(&naziv, &stanjePre)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("ntech: ServisniDeloviRepo.Dodaj: dohvati artikal: %w", err)
|
||||
}
|
||||
|
||||
if stanjePre < kolicina {
|
||||
return 0, &db.ErrNedovoljnoKolicine{ArtikalNaziv: naziv}
|
||||
}
|
||||
|
||||
stanjePosle := stanjePre - kolicina
|
||||
_, err = tx.ExecContext(ctx,
|
||||
"UPDATE artikli SET kolicina = ? WHERE id = ?", stanjePosle, artikalID,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("ntech: ServisniDeloviRepo.Dodaj: update stanje: %w", err)
|
||||
}
|
||||
|
||||
rezultat, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO servisni_delovi (nalog_id, artikal_id, kolicina, cena_komada)
|
||||
VALUES (?, ?, ?, ?)`,
|
||||
nalogID, artikalID, kolicina, cenaKomada,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("ntech: ServisniDeloviRepo.Dodaj: insert: %w", err)
|
||||
}
|
||||
|
||||
deoID, err := rezultat.LastInsertId()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("ntech: ServisniDeloviRepo.Dodaj: last insert id: %w", err)
|
||||
}
|
||||
|
||||
err = zabeleziMagacinPromenu(ctx, tx, artikalID, model.PromenaIzlazServis,
|
||||
-kolicina, stanjePre, stanjePosle, nalogID, korisnikID, "")
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("ntech: ServisniDeloviRepo.Dodaj: magacin: %w", err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return 0, fmt.Errorf("ntech: ServisniDeloviRepo.Dodaj: commit: %w", err)
|
||||
}
|
||||
|
||||
return deoID, nil
|
||||
}
|
||||
|
||||
// Obrisi uklanja servisni deo i vraća količinu na stanje u magacinu
|
||||
func (r *ServisniDeloviRepo) Obrisi(ctx context.Context, id int64, korisnikID *int64) error {
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntech: ServisniDeloviRepo.Obrisi: begin tx: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
var artikalID int64
|
||||
var nalogID int64
|
||||
var kolicina int
|
||||
err = tx.QueryRowContext(ctx,
|
||||
"SELECT artikal_id, nalog_id, kolicina FROM servisni_delovi WHERE id = ?", id,
|
||||
).Scan(&artikalID, &nalogID, &kolicina)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntech: ServisniDeloviRepo.Obrisi: dohvati deo: %w", err)
|
||||
}
|
||||
|
||||
var stanjePre int
|
||||
err = tx.QueryRowContext(ctx,
|
||||
"SELECT kolicina FROM artikli WHERE id = ?", artikalID,
|
||||
).Scan(&stanjePre)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntech: ServisniDeloviRepo.Obrisi: dohvati stanje: %w", err)
|
||||
}
|
||||
|
||||
stanjePosle := stanjePre + kolicina
|
||||
_, err = tx.ExecContext(ctx,
|
||||
"UPDATE artikli SET kolicina = ? WHERE id = ?", stanjePosle, artikalID,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntech: ServisniDeloviRepo.Obrisi: vrati stanje: %w", err)
|
||||
}
|
||||
|
||||
_, err = tx.ExecContext(ctx, "DELETE FROM servisni_delovi WHERE id = ?", id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntech: ServisniDeloviRepo.Obrisi: delete: %w", err)
|
||||
}
|
||||
|
||||
err = zabeleziMagacinPromenu(ctx, tx, artikalID, model.PromenaPovracaj,
|
||||
kolicina, stanjePre, stanjePosle, nalogID, korisnikID, "uklonjen servisni deo")
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntech: ServisniDeloviRepo.Obrisi: magacin: %w", err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return fmt.Errorf("ntech: ServisniDeloviRepo.Obrisi: commit: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ type Handler struct {
|
||||
NabavkeRepo db.NabavkaRepository
|
||||
KlijentiRepo db.KlijentRepository
|
||||
ServisRepo db.ServisRepository
|
||||
ServisniDeloviRepo db.ServisniDeloviRepository
|
||||
MagacinskePromeneRepo db.MagacinskePromeneRepository
|
||||
ProdajaRepo db.ProdajaRepository
|
||||
KorisniciRepo db.KorisniciRepository
|
||||
SesijeRepo db.SesijeRepository
|
||||
@@ -44,6 +46,8 @@ func Novi(baza *sql.DB) *Handler {
|
||||
NabavkeRepo: sqlite.NoviNabavkaRepo(baza),
|
||||
KlijentiRepo: sqlite.NoviKlijentRepo(baza),
|
||||
ServisRepo: sqlite.NoviServisRepo(baza),
|
||||
ServisniDeloviRepo: sqlite.NoviServisniDeloviRepo(baza),
|
||||
MagacinskePromeneRepo: sqlite.NoviMagacinskePromeneRepo(baza),
|
||||
ProdajaRepo: sqlite.NoviProdajaRepo(baza),
|
||||
KorisniciRepo: sqlite.NoviKorisniciRepo(baza),
|
||||
SesijeRepo: sqlite.NoviSesijeRepo(baza),
|
||||
@@ -63,6 +67,8 @@ func (h *Handler) reinicijalizujRepozitorijume(novaDB *sql.DB) {
|
||||
h.NabavkeRepo = sqlite.NoviNabavkaRepo(novaDB)
|
||||
h.KlijentiRepo = sqlite.NoviKlijentRepo(novaDB)
|
||||
h.ServisRepo = sqlite.NoviServisRepo(novaDB)
|
||||
h.ServisniDeloviRepo = sqlite.NoviServisniDeloviRepo(novaDB)
|
||||
h.MagacinskePromeneRepo = sqlite.NoviMagacinskePromeneRepo(novaDB)
|
||||
h.ProdajaRepo = sqlite.NoviProdajaRepo(novaDB)
|
||||
h.KorisniciRepo = sqlite.NoviKorisniciRepo(novaDB)
|
||||
h.SesijeRepo = sqlite.NoviSesijeRepo(novaDB)
|
||||
|
||||
@@ -227,21 +227,31 @@ func (h *Handler) ObrisiKlijenta(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// parseFormuKlijenta čita polja iz HTTP forme, validira ih i vraća model i eventualnu grešku
|
||||
func parseFormuKlijenta(r *http.Request) (model.Klijent, string) {
|
||||
tip := r.FormValue("tip")
|
||||
if tip != "fizicko" && tip != "pravno" {
|
||||
tip = "fizicko"
|
||||
}
|
||||
|
||||
ime := strings.TrimSpace(r.FormValue("ime"))
|
||||
nazivFirme := strings.TrimSpace(r.FormValue("naziv_firme"))
|
||||
|
||||
if ime == "" && nazivFirme == "" {
|
||||
return model.Klijent{}, "Mora biti uneto ime i prezime ili naziv firme."
|
||||
if tip == "fizicko" && ime == "" {
|
||||
return model.Klijent{Tip: tip}, "Za fizičko lice obavezno je uneti ime."
|
||||
}
|
||||
if tip == "pravno" && nazivFirme == "" {
|
||||
return model.Klijent{Tip: tip}, "Za pravno lice obavezan je naziv firme."
|
||||
}
|
||||
|
||||
email := strings.TrimSpace(r.FormValue("email"))
|
||||
if email != "" && !strings.Contains(email, "@") {
|
||||
return model.Klijent{}, "Adresa e-pošte nije ispravna."
|
||||
return model.Klijent{Tip: tip}, "Adresa e-pošte nije ispravna."
|
||||
}
|
||||
|
||||
return model.Klijent{
|
||||
Tip: tip,
|
||||
Ime: ime,
|
||||
Prezime: strings.TrimSpace(r.FormValue("prezime")),
|
||||
JMBG: strings.TrimSpace(r.FormValue("jmbg")),
|
||||
NazivFirme: nazivFirme,
|
||||
PIB: strings.TrimSpace(r.FormValue("pib")),
|
||||
Telefon: strings.TrimSpace(r.FormValue("telefon")),
|
||||
|
||||
@@ -606,6 +606,7 @@ func (h *Handler) PodesavanjaOpste(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
podaci.Stranica = "podesavanja-opste"
|
||||
h.renderujTemplate(w, "podesavanja_opste", podaci)
|
||||
}
|
||||
|
||||
@@ -621,6 +622,7 @@ func (h *Handler) PodesavanjaIzgled(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
podaci.Stranica = "podesavanja-izgled"
|
||||
h.renderujTemplate(w, "podesavanja_izgled", podaci)
|
||||
}
|
||||
|
||||
@@ -636,6 +638,7 @@ func (h *Handler) PodesavanjaSistem(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
podaci.Stranica = "podesavanja-sistem"
|
||||
h.renderujTemplate(w, "podesavanja_sistem", podaci)
|
||||
}
|
||||
|
||||
|
||||
@@ -57,17 +57,18 @@ type PodaciStampeProdaje struct {
|
||||
PIB string
|
||||
}
|
||||
|
||||
// artikalUJSONSaCenom pretvara listu artikala u template.JS vrednost sa prodajnom cenom i stanjem
|
||||
// artikalUJSONSaCenom pretvara listu artikala u template.JS vrednost sa prodajnom cenom, PDV stopom i stanjem
|
||||
func artikalUJSONSaCenom(artikli []model.ArtikalSaKategorijom) template.JS {
|
||||
type stavka struct {
|
||||
ID int64 `json:"id"`
|
||||
Naziv string `json:"naziv"`
|
||||
Cena float64 `json:"cena"`
|
||||
PdvStopa float64 `json:"pdv_stopa"`
|
||||
Kolicina int `json:"kolicina"`
|
||||
}
|
||||
lista := make([]stavka, 0, len(artikli))
|
||||
for _, a := range artikli {
|
||||
lista = append(lista, stavka{ID: a.ID, Naziv: a.Naziv, Cena: a.ProdajnaCena, Kolicina: a.Kolicina})
|
||||
lista = append(lista, stavka{ID: a.ID, Naziv: a.Naziv, Cena: a.ProdajnaCena, PdvStopa: a.PdvStopa, Kolicina: a.Kolicina})
|
||||
}
|
||||
b, _ := json.Marshal(lista)
|
||||
return template.JS(b)
|
||||
@@ -185,7 +186,7 @@ func (h *Handler) SacuvajProdaju(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
nalog.Ukupno = ukupno
|
||||
|
||||
id, err := h.ProdajaRepo.Kreiraj(r.Context(), &nalog, stavke)
|
||||
id, err := h.ProdajaRepo.Kreiraj(r.Context(), &nalog, stavke, &k.ID)
|
||||
if err != nil {
|
||||
var errStanje *appdb.ErrNedovoljnoKolicine
|
||||
if errors.As(err, &errStanje) {
|
||||
@@ -336,10 +337,15 @@ func parseFormuProdaje(r *http.Request) (model.ProdajniNalog, []model.StavkaProd
|
||||
}
|
||||
}
|
||||
nalog.Napomena = strings.TrimSpace(r.FormValue("napomena"))
|
||||
nalog.NacinPlacanja = r.FormValue("nacin_placanja")
|
||||
if nalog.NacinPlacanja != "gotovina" && nalog.NacinPlacanja != "kartica" && nalog.NacinPlacanja != "prenos" {
|
||||
nalog.NacinPlacanja = "gotovina"
|
||||
}
|
||||
|
||||
artikalIDovi := r.Form["artikal_id[]"]
|
||||
kolicine := r.Form["kolicina[]"]
|
||||
cene := r.Form["cena_po_komadu[]"]
|
||||
pdvStope := r.Form["pdv_stopa[]"]
|
||||
|
||||
if len(artikalIDovi) == 0 {
|
||||
return nalog, nil, "Prodaja mora imati najmanje jednu stavku."
|
||||
@@ -366,17 +372,23 @@ func parseFormuProdaje(r *http.Request) (model.ProdajniNalog, []model.StavkaProd
|
||||
return nalog, nil, "Cena mora biti pozitivan broj."
|
||||
}
|
||||
|
||||
var pdvStopa float64
|
||||
if i < len(pdvStope) {
|
||||
pdvStopa, _ = strconv.ParseFloat(strings.TrimSpace(pdvStope[i]), 64)
|
||||
}
|
||||
|
||||
stavke = append(stavke, model.StavkaProdaje{
|
||||
ArtikalID: artikalID,
|
||||
Kolicina: kolicina,
|
||||
CenaPoKomadu: cena,
|
||||
PdvStopa: pdvStopa,
|
||||
})
|
||||
}
|
||||
|
||||
return nalog, stavke, ""
|
||||
}
|
||||
|
||||
// StornoProdaje stornira prodajni nalog: vraća artikle na stanje i briše nalog
|
||||
// StornoProdaje stornira prodajni nalog: vraća artikle na stanje i označava nalog kao storniran
|
||||
func (h *Handler) StornoProdaje(w http.ResponseWriter, r *http.Request) {
|
||||
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "prodaja.storno") {
|
||||
@@ -388,12 +400,19 @@ func (h *Handler) StornoProdaje(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "Neispravan ID naloga", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := h.ProdajaRepo.Obrisi(r.Context(), id); err != nil {
|
||||
http.Error(w, "Greška pri storniranju naloga", http.StatusInternalServerError)
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
razlog := strings.TrimSpace(r.FormValue("razlog"))
|
||||
if err := h.ProdajaRepo.Storno(r.Context(), id, razlog, &k.ID); err != nil {
|
||||
log.Printf("greška pri storniranju naloga: %v", err)
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri storniranju. Možda je nalog već storniran.")
|
||||
http.Redirect(w, r, "/prodaja/"+strconv.FormatInt(id, 10), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Prodajni nalog je storniran.")
|
||||
http.Redirect(w, r, "/prodaja", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/prodaja/"+strconv.FormatInt(id, 10), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// renderujFormuProdaje renderuje HTML šablon forme za unos nove prodaje
|
||||
|
||||
+145
-5
@@ -7,6 +7,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
appdbPkg "ntech/internal/db"
|
||||
"ntech/internal/db/sqlite"
|
||||
"ntech/internal/middleware"
|
||||
"ntech/internal/model"
|
||||
@@ -30,6 +31,7 @@ type PodaciFormeNaloga struct {
|
||||
model.PodaciStranice
|
||||
Nalog model.ServisniNalog
|
||||
Klijenti []model.Klijent
|
||||
Tehnicari []model.Korisnik
|
||||
SviStatusi []string
|
||||
Greska string
|
||||
Izmena bool
|
||||
@@ -40,6 +42,9 @@ type PodaciDetaljiNaloga struct {
|
||||
model.PodaciStranice
|
||||
Nalog model.ServisniNalog
|
||||
KlijentNaziv string
|
||||
TehnicarNaziv string
|
||||
ServisniDelovi []model.ServisniDeoSaArtiklom
|
||||
Artikli []model.ArtikalSaKategorijom
|
||||
Sacuvano bool
|
||||
}
|
||||
|
||||
@@ -96,6 +101,12 @@ func (h *Handler) NoviNalog(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
tehnicari, err := h.KorisniciRepo.Lista(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri učitavanju tehničara", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
ps := h.popuniPodaciStranice(r, podesavanja)
|
||||
ps.Stranica = "servis"
|
||||
ps.NaslovStranice = "Novi nalog"
|
||||
@@ -103,6 +114,7 @@ func (h *Handler) NoviNalog(w http.ResponseWriter, r *http.Request) {
|
||||
PodaciStranice: ps,
|
||||
Nalog: model.ServisniNalog{BrojNaloga: brojNaloga, Status: model.StatusPrimljeno},
|
||||
Klijenti: klijenti,
|
||||
Tehnicari: tehnicari,
|
||||
SviStatusi: model.SviStatusi,
|
||||
Izmena: false,
|
||||
})
|
||||
@@ -124,6 +136,7 @@ func (h *Handler) SacuvajNalog(w http.ResponseWriter, r *http.Request) {
|
||||
if greska != "" {
|
||||
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||
klijenti, _ := h.KlijentiRepo.Lista(r.Context(), "")
|
||||
tehnicari, _ := h.KorisniciRepo.Lista(r.Context())
|
||||
ps := h.popuniPodaciStranice(r, podesavanja)
|
||||
ps.Stranica = "servis"
|
||||
ps.NaslovStranice = "Novi nalog"
|
||||
@@ -131,6 +144,7 @@ func (h *Handler) SacuvajNalog(w http.ResponseWriter, r *http.Request) {
|
||||
PodaciStranice: ps,
|
||||
Nalog: nalog,
|
||||
Klijenti: klijenti,
|
||||
Tehnicari: tehnicari,
|
||||
SviStatusi: model.SviStatusi,
|
||||
Greska: greska,
|
||||
Izmena: false,
|
||||
@@ -143,6 +157,7 @@ func (h *Handler) SacuvajNalog(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("greška pri čuvanju naloga: %v", err)
|
||||
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||
klijenti, _ := h.KlijentiRepo.Lista(r.Context(), "")
|
||||
tehnicari, _ := h.KorisniciRepo.Lista(r.Context())
|
||||
ps := h.popuniPodaciStranice(r, podesavanja)
|
||||
ps.Stranica = "servis"
|
||||
ps.NaslovStranice = "Novi nalog"
|
||||
@@ -150,6 +165,7 @@ func (h *Handler) SacuvajNalog(w http.ResponseWriter, r *http.Request) {
|
||||
PodaciStranice: ps,
|
||||
Nalog: nalog,
|
||||
Klijenti: klijenti,
|
||||
Tehnicari: tehnicari,
|
||||
SviStatusi: model.SviStatusi,
|
||||
Greska: "Došlo je do greške pri čuvanju. Pokušajte ponovo.",
|
||||
Izmena: false,
|
||||
@@ -186,6 +202,12 @@ func (h *Handler) IzmeniNalog(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
tehnicari, err := h.KorisniciRepo.Lista(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri učitavanju tehničara", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
ps := h.popuniPodaciStranice(r, podesavanja)
|
||||
ps.Stranica = "servis"
|
||||
ps.NaslovStranice = "Izmeni nalog"
|
||||
@@ -193,6 +215,7 @@ func (h *Handler) IzmeniNalog(w http.ResponseWriter, r *http.Request) {
|
||||
PodaciStranice: ps,
|
||||
Nalog: *nalog,
|
||||
Klijenti: klijenti,
|
||||
Tehnicari: tehnicari,
|
||||
SviStatusi: model.SviStatusi,
|
||||
Izmena: true,
|
||||
})
|
||||
@@ -220,6 +243,7 @@ func (h *Handler) SacuvajIzmenaNaloga(w http.ResponseWriter, r *http.Request) {
|
||||
if greska != "" {
|
||||
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||
klijenti, _ := h.KlijentiRepo.Lista(r.Context(), "")
|
||||
tehnicari, _ := h.KorisniciRepo.Lista(r.Context())
|
||||
nalog.ID = id
|
||||
ps := h.popuniPodaciStranice(r, podesavanja)
|
||||
ps.Stranica = "servis"
|
||||
@@ -228,6 +252,7 @@ func (h *Handler) SacuvajIzmenaNaloga(w http.ResponseWriter, r *http.Request) {
|
||||
PodaciStranice: ps,
|
||||
Nalog: nalog,
|
||||
Klijenti: klijenti,
|
||||
Tehnicari: tehnicari,
|
||||
SviStatusi: model.SviStatusi,
|
||||
Greska: greska,
|
||||
Izmena: true,
|
||||
@@ -240,6 +265,7 @@ func (h *Handler) SacuvajIzmenaNaloga(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("greška pri čuvanju izmene naloga: %v", err)
|
||||
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||
klijenti, _ := h.KlijentiRepo.Lista(r.Context(), "")
|
||||
tehnicari, _ := h.KorisniciRepo.Lista(r.Context())
|
||||
ps := h.popuniPodaciStranice(r, podesavanja)
|
||||
ps.Stranica = "servis"
|
||||
ps.NaslovStranice = "Izmeni nalog"
|
||||
@@ -247,6 +273,7 @@ func (h *Handler) SacuvajIzmenaNaloga(w http.ResponseWriter, r *http.Request) {
|
||||
PodaciStranice: ps,
|
||||
Nalog: nalog,
|
||||
Klijenti: klijenti,
|
||||
Tehnicari: tehnicari,
|
||||
SviStatusi: model.SviStatusi,
|
||||
Greska: "Došlo je do greške pri čuvanju. Pokušajte ponovo.",
|
||||
Izmena: true,
|
||||
@@ -278,7 +305,7 @@ func (h *Handler) ObrisiNalog(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/servis?obrisan=1", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// DetaljiNaloga prikazuje sve podatke jednog servisnog naloga
|
||||
// DetaljiNaloga prikazuje sve podatke jednog servisnog naloga sa ugrađenim delovima
|
||||
func (h *Handler) DetaljiNaloga(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := parseID(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
@@ -302,12 +329,27 @@ func (h *Handler) DetaljiNaloga(w http.ResponseWriter, r *http.Request) {
|
||||
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)
|
||||
klijentNaziv = klijent.PunoIme()
|
||||
}
|
||||
}
|
||||
|
||||
tehnicarNaziv := ""
|
||||
if nalog.TehnicarID != nil {
|
||||
tehnicar, err := h.KorisniciRepo.DohvatiPoID(r.Context(), *nalog.TehnicarID)
|
||||
if err == nil {
|
||||
tehnicarNaziv = tehnicar.KorisnickoIme
|
||||
}
|
||||
}
|
||||
|
||||
delovi, err := h.ServisniDeloviRepo.DohvatiZaNalog(r.Context(), id)
|
||||
if err != nil {
|
||||
log.Printf("greška pri učitavanju delova: %v", err)
|
||||
}
|
||||
|
||||
appdb := appdbPkg.ArtikalFilter{}
|
||||
artikli, err := h.Artikli.Lista(r.Context(), appdb)
|
||||
if err != nil {
|
||||
log.Printf("greška pri učitavanju artikala: %v", err)
|
||||
}
|
||||
|
||||
ps := h.popuniPodaciStranice(r, podesavanja)
|
||||
@@ -317,12 +359,96 @@ func (h *Handler) DetaljiNaloga(w http.ResponseWriter, r *http.Request) {
|
||||
PodaciStranice: ps,
|
||||
Nalog: *nalog,
|
||||
KlijentNaziv: klijentNaziv,
|
||||
TehnicarNaziv: tehnicarNaziv,
|
||||
ServisniDelovi: delovi,
|
||||
Artikli: artikli,
|
||||
Sacuvano: r.URL.Query().Get("sacuvano") == "1",
|
||||
}
|
||||
|
||||
h.renderujTemplate(w, "servis_detalji", podaci)
|
||||
}
|
||||
|
||||
// DodajDeloNalogu prima POST formu i dodaje artikal kao deo servisnog naloga
|
||||
func (h *Handler) DodajDeloNalogu(w http.ResponseWriter, r *http.Request) {
|
||||
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "servis.izmeni") {
|
||||
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
nalogID, 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
|
||||
}
|
||||
|
||||
artikalID, err := strconv.ParseInt(r.FormValue("artikal_id"), 10, 64)
|
||||
if err != nil || artikalID <= 0 {
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Neispravan artikal.")
|
||||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(nalogID, 10), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
kolicina, err := strconv.Atoi(r.FormValue("kolicina"))
|
||||
if err != nil || kolicina <= 0 {
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Količina mora biti pozitivan broj.")
|
||||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(nalogID, 10), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
cena, err := strconv.ParseFloat(r.FormValue("cena_komada"), 64)
|
||||
if err != nil || cena < 0 {
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Cena mora biti pozitivan broj.")
|
||||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(nalogID, 10), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := h.ServisniDeloviRepo.Dodaj(r.Context(), nalogID, artikalID, kolicina, cena, &k.ID); err != nil {
|
||||
log.Printf("greška pri dodavanju dela: %v", err)
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri dodavanju dela. Proverite stanje na magacinu.")
|
||||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(nalogID, 10), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Deo je dodat.")
|
||||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(nalogID, 10), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// ObrisiDeloNaloga prima POST zahtev i uklanja deo iz servisnog naloga
|
||||
func (h *Handler) ObrisiDeloNaloga(w http.ResponseWriter, r *http.Request) {
|
||||
k := middleware.KorisnikIzKonteksta(r.Context())
|
||||
if !h.DozvoleRepo.ImaDozvolu(r.Context(), k.Uloga, "servis.izmeni") {
|
||||
http.Error(w, "Nemate dozvolu za ovu akciju.", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
nalogID, err := parseID(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
http.Error(w, "Neispravan ID naloga", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
deoID, err := parseID(chi.URLParam(r, "deo_id"))
|
||||
if err != nil {
|
||||
http.Error(w, "Neispravan ID dela", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.ServisniDeloviRepo.Obrisi(r.Context(), deoID, &k.ID); err != nil {
|
||||
log.Printf("greška pri brisanju dela: %v", err)
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri uklanjanju dela.")
|
||||
} else {
|
||||
middleware.SetFlash(w, r, h.DB, "uspeh", "Deo je uklonjen.")
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(nalogID, 10), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// parseFormuNaloga čita i validira polja iz HTTP forme
|
||||
func parseFormuNaloga(r *http.Request) (model.ServisniNalog, string) {
|
||||
uredjaj := strings.TrimSpace(r.FormValue("uredjaj"))
|
||||
@@ -355,6 +481,13 @@ func parseFormuNaloga(r *http.Request) (model.ServisniNalog, string) {
|
||||
}
|
||||
}
|
||||
|
||||
// opcioni tehničar
|
||||
if tidStr := r.FormValue("tehnicar_id"); tidStr != "" {
|
||||
if tid, err := strconv.ParseInt(tidStr, 10, 64); err == nil {
|
||||
nalog.TehnicarID = &tid
|
||||
}
|
||||
}
|
||||
|
||||
// opcione cene — prazno polje ostaje nil
|
||||
nalog.CenaOd = parseOpcionuCenu(r.FormValue("cena_od"))
|
||||
nalog.CenaDo = parseOpcionuCenu(r.FormValue("cena_do"))
|
||||
@@ -368,6 +501,13 @@ func parseFormuNaloga(r *http.Request) (model.ServisniNalog, string) {
|
||||
}
|
||||
}
|
||||
|
||||
// opcioni datum garancije
|
||||
if gd := strings.TrimSpace(r.FormValue("garancija_do")); gd != "" {
|
||||
if t, err := time.Parse("2006-01-02", gd); err == nil {
|
||||
nalog.GarancijaDo = &t
|
||||
}
|
||||
}
|
||||
|
||||
return nalog, ""
|
||||
}
|
||||
|
||||
|
||||
@@ -11,11 +11,23 @@ type Artikal struct {
|
||||
Kolicina int
|
||||
KolicinMin int
|
||||
Lokacija string
|
||||
NabavnaCena float64
|
||||
ProdajnaCena float64
|
||||
PdvStopa float64
|
||||
Napomena string
|
||||
DatumUnosa time.Time
|
||||
}
|
||||
|
||||
// CenaBezPdv izračunava prodajnu cenu bez PDV-a
|
||||
func (a Artikal) CenaBezPdv() float64 {
|
||||
return a.ProdajnaCena / (1 + a.PdvStopa/100)
|
||||
}
|
||||
|
||||
// PdvIznos izračunava iznos PDV-a za jednu jedinicu
|
||||
func (a Artikal) PdvIznos() float64 {
|
||||
return a.ProdajnaCena - a.CenaBezPdv()
|
||||
}
|
||||
|
||||
// Kategorija predstavlja kategoriju artikala
|
||||
type Kategorija struct {
|
||||
ID int64
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Klijent predstavlja jednog klijenta — fizičko lice ili firmu
|
||||
type Klijent struct {
|
||||
ID int64
|
||||
Tip string
|
||||
Ime string
|
||||
Prezime string
|
||||
JMBG string
|
||||
NazivFirme string
|
||||
PIB string
|
||||
Telefon string
|
||||
@@ -14,3 +19,11 @@ type Klijent struct {
|
||||
Napomena string
|
||||
DatumUnosa time.Time
|
||||
}
|
||||
|
||||
// PunoIme vraća ime i prezime za fizičko lice, ili naziv firme za pravno
|
||||
func (k Klijent) PunoIme() string {
|
||||
if k.Tip == "pravno" {
|
||||
return k.NazivFirme
|
||||
}
|
||||
return strings.TrimSpace(k.Ime + " " + k.Prezime)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// Tipovi magacinskih promena
|
||||
const (
|
||||
PromenaUlazNabavka = "ulaz_nabavka"
|
||||
PromenaIzlazProdaja = "izlaz_prodaja"
|
||||
PromenaIzlazServis = "izlaz_servis"
|
||||
PromenaPovracaj = "povracaj"
|
||||
PromenaKorekcija = "korekcija"
|
||||
)
|
||||
|
||||
// MagacinskaPromenaSaDetaljem je promena stanja artikla sa nazivom artikla
|
||||
type MagacinskaPromenaSaDetaljem struct {
|
||||
ID int64
|
||||
ArtikalID int64
|
||||
ArtikalNaziv string
|
||||
TipPromene string
|
||||
ReferentniID int64
|
||||
PromenaKolicine int
|
||||
StanjePre int
|
||||
StanjePosle int
|
||||
KorisnikID *int64
|
||||
Napomena string
|
||||
Datum time.Time
|
||||
}
|
||||
@@ -9,6 +9,9 @@ type ProdajniNalog struct {
|
||||
BrojNaloga string
|
||||
Napomena string
|
||||
Ukupno float64
|
||||
NacinPlacanja string
|
||||
Stornirano bool
|
||||
RazlogStorniranja string
|
||||
Datum time.Time
|
||||
}
|
||||
|
||||
@@ -20,6 +23,9 @@ type StavkaProdaje struct {
|
||||
Kolicina int
|
||||
CenaPoKomadu float64
|
||||
Ukupno float64
|
||||
PdvStopa float64
|
||||
PdvIznos float64
|
||||
CenaBezPdv float64
|
||||
}
|
||||
|
||||
// ProdajniNalogSaDetaljem je nalog sa nazivom klijenta — za prikaz u listi
|
||||
|
||||
@@ -29,6 +29,7 @@ var SviStatusi = []string{
|
||||
type ServisniNalog struct {
|
||||
ID int64
|
||||
KlijentID *int64
|
||||
TehnicarID *int64
|
||||
BrojNaloga string
|
||||
Uredjaj string
|
||||
SerijskiBroj string
|
||||
@@ -39,10 +40,32 @@ type ServisniNalog struct {
|
||||
CenaKonacna *float64
|
||||
Avans *float64
|
||||
Napomena string
|
||||
GarancijaDo *time.Time
|
||||
DatumPrijema time.Time
|
||||
DatumZavrsetka *time.Time
|
||||
}
|
||||
|
||||
// ServisniDeo predstavlja jedan artikal ugrađen u servisni nalog
|
||||
type ServisniDeo struct {
|
||||
ID int64
|
||||
NalogID int64
|
||||
ArtikalID int64
|
||||
Kolicina int
|
||||
CenaKomada float64
|
||||
Datum time.Time
|
||||
}
|
||||
|
||||
// Ukupno vraća ukupnu vrednost dela (kolicina × cena)
|
||||
func (d ServisniDeo) Ukupno() float64 {
|
||||
return float64(d.Kolicina) * d.CenaKomada
|
||||
}
|
||||
|
||||
// ServisniDeoSaArtiklom je servisni deo sa nazivom artikla — za prikaz
|
||||
type ServisniDeoSaArtiklom struct {
|
||||
ServisniDeo
|
||||
ArtikalNaziv string
|
||||
}
|
||||
|
||||
// ServisniNalogSaKlijentom proširuje ServisniNalog sa nazivom klijenta za prikaz u listi
|
||||
type ServisniNalogSaKlijentom struct {
|
||||
ServisniNalog
|
||||
@@ -57,6 +80,14 @@ func (n ServisniNalog) KlijentIDVrednost() int64 {
|
||||
return *n.KlijentID
|
||||
}
|
||||
|
||||
// TehnicarIDVrednost vraća vrednost TehnicarID pointera, ili 0 ako je nil
|
||||
func (n ServisniNalog) TehnicarIDVrednost() int64 {
|
||||
if n.TehnicarID == nil {
|
||||
return 0
|
||||
}
|
||||
return *n.TehnicarID
|
||||
}
|
||||
|
||||
// CenaOdStr vraća formatiranu procenu od, ili prazan string ako nije uneta
|
||||
func (n ServisniNalog) CenaOdStr() string {
|
||||
if n.CenaOd == nil {
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
-- ============================================
|
||||
-- Faza 1: POS proširenje — PDV, magacinska revizija, servisni delovi
|
||||
-- ============================================
|
||||
|
||||
-- 1. Artikli — dodajemo PDV stopu (nabavna_cena već postoji)
|
||||
ALTER TABLE artikli ADD COLUMN pdv_stopa REAL NOT NULL DEFAULT 20.0;
|
||||
|
||||
-- 2. Stavke prodaje — PDV po stavci (za tačan obračun i buduće fiskalne račune)
|
||||
ALTER TABLE stavke_prodaje ADD COLUMN pdv_stopa REAL NOT NULL DEFAULT 20.0;
|
||||
ALTER TABLE stavke_prodaje ADD COLUMN pdv_iznos REAL NOT NULL DEFAULT 0;
|
||||
ALTER TABLE stavke_prodaje ADD COLUMN cena_bez_pdv REAL NOT NULL DEFAULT 0;
|
||||
|
||||
-- 3. Prodajni nalozi — način plaćanja i storno
|
||||
ALTER TABLE prodajni_nalozi ADD COLUMN nacin_placanja TEXT NOT NULL DEFAULT 'gotovina'
|
||||
CHECK (nacin_placanja IN ('gotovina', 'kartica', 'prenos'));
|
||||
ALTER TABLE prodajni_nalozi ADD COLUMN stornirano INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE prodajni_nalozi ADD COLUMN razlog_storniranja TEXT;
|
||||
|
||||
-- 4. Servisni nalozi — tehničar i garancija
|
||||
ALTER TABLE servisni_nalozi ADD COLUMN tehnicar_id INTEGER REFERENCES korisnici(id) ON DELETE SET NULL;
|
||||
ALTER TABLE servisni_nalozi ADD COLUMN garancija_do DATE;
|
||||
|
||||
-- 5. Klijenti — JMBG i tip (fizičko/pravno lice)
|
||||
ALTER TABLE klijenti ADD COLUMN jmbg TEXT;
|
||||
ALTER TABLE klijenti ADD COLUMN tip TEXT NOT NULL DEFAULT 'fizicko'
|
||||
CHECK (tip IN ('fizicko', 'pravno'));
|
||||
|
||||
-- 6. Magacinska revizija — trag svake promene stanja artikla
|
||||
CREATE TABLE IF NOT EXISTS magacinske_promene (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
artikal_id INTEGER NOT NULL REFERENCES artikli(id) ON DELETE RESTRICT,
|
||||
tip_promene TEXT NOT NULL CHECK (tip_promene IN (
|
||||
'ulaz_nabavka', 'izlaz_prodaja', 'izlaz_servis', 'povracaj', 'korekcija'
|
||||
)),
|
||||
referentni_id INTEGER NOT NULL,
|
||||
promena_kolicine INTEGER NOT NULL,
|
||||
stanje_pre INTEGER NOT NULL,
|
||||
stanje_posle INTEGER NOT NULL,
|
||||
korisnik_id INTEGER REFERENCES korisnici(id) ON DELETE SET NULL,
|
||||
napomena TEXT,
|
||||
datum DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 7. Servisni delovi — artikli ugrađeni u servisni nalog
|
||||
CREATE TABLE IF NOT EXISTS servisni_delovi (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
nalog_id INTEGER NOT NULL REFERENCES servisni_nalozi(id) ON DELETE CASCADE,
|
||||
artikal_id INTEGER NOT NULL REFERENCES artikli(id) ON DELETE RESTRICT,
|
||||
kolicina INTEGER NOT NULL DEFAULT 1 CHECK (kolicina > 0),
|
||||
cena_komada REAL NOT NULL,
|
||||
datum DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Dodaje nabavnu cenu u artikle ako već ne postoji.
|
||||
-- Potrebno jer je migration 002 mogla biti preskočena na bazama koje su
|
||||
-- imale tabelu artikli pre uvođenja te migracije.
|
||||
ALTER TABLE artikli ADD COLUMN nabavna_cena REAL NOT NULL DEFAULT 0;
|
||||
+18
-2
@@ -208,8 +208,22 @@ body {
|
||||
/* accordion podmeni u sidebaru */
|
||||
.nav-podmeni {
|
||||
overflow: hidden;
|
||||
max-height: 0;
|
||||
transition: max-height 0.22s ease-out;
|
||||
}
|
||||
.nav-podmeni.otvoren {
|
||||
max-height: 300px;
|
||||
transition: max-height 0.25s ease-in;
|
||||
}
|
||||
/* pri inicijalnom učitavanju sprečavamo animaciju podmenia */
|
||||
.nav-podmeni.bez-tranzicije {
|
||||
transition: none !important;
|
||||
}
|
||||
/* u skupljenom modu podmeni se nikad ne prikazuje */
|
||||
.sidebar.skupljen .nav-podmeni {
|
||||
max-height: 0 !important;
|
||||
transition: none;
|
||||
}
|
||||
/* u skupljenom modu alpine x-show kontroliše vidljivost — ne skrivamo display:none */
|
||||
.nav-podstavka {
|
||||
padding-left: 48px !important;
|
||||
}
|
||||
@@ -256,8 +270,10 @@ body {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-stavka:hover .nav-tooltip {
|
||||
/* specifičniji selektor pobjeđuje .sidebar.skupljen .nav-stavka span { opacity:0 } */
|
||||
.sidebar.skupljen .nav-stavka:hover .nav-tooltip {
|
||||
opacity: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* dno sidebara */
|
||||
|
||||
+51
-3
@@ -1,3 +1,43 @@
|
||||
// otvara/zatvara podmeni u sidebaru — funkcioniše i posle HTMX swap-a (čisti onclick, bez Alpine-a)
|
||||
function ntechTogglePodmeni(btn) {
|
||||
var sidebar = document.getElementById('sidebar');
|
||||
// ako je sidebar skupljen, raširimo ga pre nego otvorimo podmeni
|
||||
if (sidebar && sidebar.classList.contains('skupljen')) {
|
||||
sidebar.classList.remove('skupljen');
|
||||
localStorage.setItem('sidebar-skupljeno', 'false');
|
||||
}
|
||||
var podmeni = btn.nextElementSibling;
|
||||
if (!podmeni) return;
|
||||
podmeni.classList.toggle('otvoren');
|
||||
var strelicaSvg = btn.querySelector('.nav-strelica svg');
|
||||
if (strelicaSvg) {
|
||||
strelicaSvg.style.transform = podmeni.classList.contains('otvoren') ? 'rotate(180deg)' : 'rotate(0deg)';
|
||||
}
|
||||
}
|
||||
|
||||
// registruje klik listenere na podmeni dugmad — sidebar se nikad ne menja, poziva se jednom
|
||||
function ntechDodajPodmeniListenere() {
|
||||
document.querySelectorAll('#sidebar [data-podmeni-dugme]').forEach(function(btn) {
|
||||
btn.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
ntechTogglePodmeni(btn);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// sprečava CSS tranziciju na podmeniima koji su već otvoreni pri inicijalnom učitavanju
|
||||
// (bez ovoga, max-height animira od 0 do 300px pri svakom swap-u)
|
||||
function ntechInicijalizujPodmeni() {
|
||||
document.querySelectorAll('.nav-podmeni.otvoren').forEach(function(el) {
|
||||
el.classList.add('bez-tranzicije');
|
||||
requestAnimationFrame(function() {
|
||||
requestAnimationFrame(function() {
|
||||
el.classList.remove('bez-tranzicije');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('alpine:init', () => {
|
||||
|
||||
// sidebar — podmeni "Moj profil"
|
||||
@@ -82,7 +122,7 @@ document.addEventListener('alpine:init', () => {
|
||||
|
||||
// forma za prodaju
|
||||
Alpine.data('prodajaForma', () => ({
|
||||
stavke: [{artikal_id: '', kolicina: 1, cena: 0}],
|
||||
stavke: [{artikal_id: '', kolicina: 1, cena: 0, pdv_stopa: 20}],
|
||||
artikliOpcije: [],
|
||||
isMobile: false,
|
||||
init() {
|
||||
@@ -93,14 +133,17 @@ document.addEventListener('alpine:init', () => {
|
||||
})
|
||||
},
|
||||
dodajStavku() {
|
||||
this.stavke.push({artikal_id: '', kolicina: 1, cena: 0})
|
||||
this.stavke.push({artikal_id: '', kolicina: 1, cena: 0, pdv_stopa: 20})
|
||||
},
|
||||
ukloniStavku(i) {
|
||||
if (this.stavke.length > 1) this.stavke.splice(i, 1)
|
||||
},
|
||||
popuniCenu(stavka) {
|
||||
const a = this.artikliOpcije.find(x => x.id == stavka.artikal_id)
|
||||
if (a) stavka.cena = a.cena
|
||||
if (a) {
|
||||
stavka.cena = a.cena
|
||||
stavka.pdv_stopa = a.pdv_stopa !== undefined ? a.pdv_stopa : 20
|
||||
}
|
||||
},
|
||||
dostupnaKolicina(i) {
|
||||
const stavka = this.stavke[i]
|
||||
@@ -202,3 +245,8 @@ document.addEventListener('alpine:init', () => {
|
||||
}))
|
||||
|
||||
})
|
||||
|
||||
// za prvo učitavanje (defer script) — sprečava animaciju podmenia koji su inicijalno otvoreni
|
||||
ntechInicijalizujPodmeni()
|
||||
// dodaje klik listenere na podmeni dugmad (data-podmeni-dugme)
|
||||
ntechDodajPodmeniListenere()
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="sidebar-nav" aria-label="Glavna navigacija" hx-boost="true" hx-target="body" hx-swap="innerHTML">
|
||||
<nav class="sidebar-nav" aria-label="Glavna navigacija" hx-boost="true" hx-target="#glavni-sadrzaj" hx-select="#glavni-sadrzaj" hx-swap="outerHTML">
|
||||
<div class="nav-oznaka">Glavni meni</div>
|
||||
|
||||
<a href="/dashboard" class="nav-stavka {{if eq .Stranica "dashboard"}}aktivan{{end}}">
|
||||
@@ -89,21 +89,21 @@
|
||||
<div class="nav-oznaka">Nalog</div>
|
||||
|
||||
{{if index .Dozvole "tema.lokalno"}}
|
||||
<div x-data="sidebarProfil" data-otvoren="{{if or (eq .Stranica "profil") (eq .Stranica "profil-tema")}}true{{else}}false{{end}}">
|
||||
<button type="button" @click="otvoren = !otvoren"
|
||||
<div>
|
||||
<button type="button" data-podmeni-dugme
|
||||
class="nav-stavka {{if or (eq .Stranica "profil") (eq .Stranica "profil-tema")}}aktivan{{end}}"
|
||||
style="width:100%;background:none;border:none;cursor:pointer;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
||||
<span>Moj profil</span>
|
||||
<span class="nav-strelica">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||
:style="'transition:transform 0.2s;transform:' + (otvoren ? 'rotate(180deg)' : 'rotate(0deg)')">
|
||||
style="transition:transform 0.2s;transform:{{if or (eq .Stranica "profil") (eq .Stranica "profil-tema")}}rotate(180deg){{else}}rotate(0deg){{end}}">
|
||||
<polyline points="6 9 12 15 18 9"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="nav-tooltip">Moj profil</span>
|
||||
</button>
|
||||
<div class="nav-podmeni" x-show="otvoren" x-transition>
|
||||
<div class="nav-podmeni {{if or (eq .Stranica "profil") (eq .Stranica "profil-tema")}}otvoren{{end}}">
|
||||
<a href="/admin/profil" class="nav-stavka nav-podstavka {{if eq .Stranica "profil"}}aktivan{{end}}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
||||
<span>Opšte</span>
|
||||
@@ -137,32 +137,32 @@
|
||||
<div class="nav-oznaka">Sistem</div>
|
||||
|
||||
{{if index .Dozvole "podesavanja.pregled"}}
|
||||
<div x-data="sidebarPodesavanja" data-otvoren="{{if or (eq .Stranica "podesavanja") (eq .Stranica "dozvole")}}true{{else}}false{{end}}">
|
||||
<button type="button" @click="otvoren = !otvoren"
|
||||
class="nav-stavka {{if eq .Stranica "podesavanja"}}aktivan{{end}}"
|
||||
<div>
|
||||
<button type="button" data-podmeni-dugme
|
||||
class="nav-stavka {{if or (eq .Stranica "podesavanja") (eq .Stranica "podesavanja-opste") (eq .Stranica "podesavanja-izgled") (eq .Stranica "podesavanja-sistem") (eq .Stranica "dozvole")}}aktivan{{end}}"
|
||||
style="width:100%;background:none;border:none;cursor:pointer;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06-.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
|
||||
<span>Podešavanja</span>
|
||||
<span class="nav-strelica">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||
:style="'transition:transform 0.2s;transform:' + (otvoren ? 'rotate(180deg)' : 'rotate(0deg)')">
|
||||
style="transition:transform 0.2s;transform:{{if or (eq .Stranica "podesavanja") (eq .Stranica "podesavanja-opste") (eq .Stranica "podesavanja-izgled") (eq .Stranica "podesavanja-sistem") (eq .Stranica "dozvole")}}rotate(180deg){{else}}rotate(0deg){{end}}">
|
||||
<polyline points="6 9 12 15 18 9"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="nav-tooltip">Podešavanja</span>
|
||||
</button>
|
||||
<div class="nav-podmeni" x-show="otvoren" x-transition>
|
||||
<a href="/admin/podesavanja/opste" class="nav-stavka nav-podstavka">
|
||||
<div class="nav-podmeni {{if or (eq .Stranica "podesavanja") (eq .Stranica "podesavanja-opste") (eq .Stranica "podesavanja-izgled") (eq .Stranica "podesavanja-sistem") (eq .Stranica "dozvole")}}otvoren{{end}}">
|
||||
<a href="/admin/podesavanja/opste" class="nav-stavka nav-podstavka {{if eq .Stranica "podesavanja-opste"}}aktivan{{end}}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21h18"/><path d="M5 21V7a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v14"/><path d="M9 21v-5h6v5"/><path d="M9 9h.01M15 9h.01M9 13h.01M15 13h.01"/></svg>
|
||||
<span>Opšte</span>
|
||||
<span class="nav-tooltip">Opšte</span>
|
||||
</a>
|
||||
<a href="/admin/podesavanja/izgled" class="nav-stavka nav-podstavka">
|
||||
<a href="/admin/podesavanja/izgled" class="nav-stavka nav-podstavka {{if eq .Stranica "podesavanja-izgled"}}aktivan{{end}}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 21a9 9 0 1 1 0-18c4.97 0 9 3.582 9 8 0 1.06-.474 2.078-1.318 2.828S17.626 15 16.5 15H15a2 2 0 0 0-2 2 2 2 0 0 0 2 2h.5"/><circle cx="8.5" cy="10.5" r=".5"/><circle cx="12" cy="7.5" r=".5"/><circle cx="15.5" cy="10.5" r=".5"/></svg>
|
||||
<span>Izgled</span>
|
||||
<span class="nav-tooltip">Izgled</span>
|
||||
</a>
|
||||
<a href="/admin/podesavanja/sistem" class="nav-stavka nav-podstavka">
|
||||
<a href="/admin/podesavanja/sistem" class="nav-stavka nav-podstavka {{if eq .Stranica "podesavanja-sistem"}}aktivan{{end}}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="8" rx="1"/><rect x="2" y="13" width="20" height="8" rx="1"/><path d="M6 7h.01M6 17h.01"/></svg>
|
||||
<span>Sistem</span>
|
||||
<span class="nav-tooltip">Sistem</span>
|
||||
@@ -181,11 +181,33 @@
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-dno">
|
||||
<a href="/odjava" class="nav-stavka">
|
||||
<a href="/odjava" class="nav-stavka" hx-boost="false">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
|
||||
<span>Odjava</span>
|
||||
<span class="nav-tooltip">Odjava</span>
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
<script>
|
||||
(function() {
|
||||
var sidebar = document.getElementById('sidebar');
|
||||
if (!sidebar) return;
|
||||
sidebar.querySelectorAll('[data-podmeni-dugme]').forEach(function(btn) {
|
||||
btn.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
if (sidebar.classList.contains('skupljen')) {
|
||||
sidebar.classList.remove('skupljen');
|
||||
localStorage.setItem('sidebar-skupljen', 'false');
|
||||
}
|
||||
var podmeni = btn.nextElementSibling;
|
||||
if (!podmeni) return;
|
||||
podmeni.classList.toggle('otvoren');
|
||||
var svg = btn.querySelector('.nav-strelica svg');
|
||||
if (svg) {
|
||||
svg.style.transform = podmeni.classList.contains('otvoren') ? 'rotate(180deg)' : 'rotate(0deg)';
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
@@ -41,8 +41,10 @@
|
||||
(function() {
|
||||
var dugme = document.getElementById('avatar-dugme');
|
||||
var meni = document.getElementById('avatar-meni');
|
||||
if (!dugme || !meni) return;
|
||||
|
||||
// premesti meni na kraj body — izlazi iz stacking contexta topbara
|
||||
// ukloni stari meni iz body-ja (ostao od prethodne stranice) i dodaj novi
|
||||
document.querySelectorAll('body > #avatar-meni').forEach(function(el) { el.remove(); });
|
||||
document.body.appendChild(meni);
|
||||
|
||||
function pozicioniraj() {
|
||||
@@ -52,6 +54,7 @@
|
||||
meni.style.left = 'auto';
|
||||
}
|
||||
|
||||
// dugme dobija novi listener svaki put (novi element pri svakoj navigaciji)
|
||||
dugme.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
if (meni.style.display === 'none' || meni.style.display === '') {
|
||||
@@ -63,13 +66,23 @@
|
||||
}
|
||||
});
|
||||
|
||||
// document i window listeneri — samo jednom, ne smeju da se gomilaju
|
||||
if (!window._ntechAvatarDodato) {
|
||||
window._ntechAvatarDodato = true;
|
||||
document.addEventListener('click', function() {
|
||||
meni.style.display = 'none';
|
||||
var m = document.getElementById('avatar-meni');
|
||||
if (m) m.style.display = 'none';
|
||||
});
|
||||
|
||||
window.addEventListener('resize', function() {
|
||||
if (meni.style.display !== 'none') pozicioniraj();
|
||||
var m = document.getElementById('avatar-meni');
|
||||
var d = document.getElementById('avatar-dugme');
|
||||
if (m && d && m.style.display !== 'none') {
|
||||
var r = d.getBoundingClientRect();
|
||||
m.style.top = (r.bottom + 8) + 'px';
|
||||
m.style.right = (window.innerWidth - r.right) + 'px';
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
{{define "naslov"}}Izveštaji — NTech{{end}}
|
||||
|
||||
{{define "dodatni-css"}}
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.4/dist/chart.umd.min.js"></script>
|
||||
<style>
|
||||
.izv-naslov { font-size: 13px; font-weight: 500; color: var(--tekst-sporedni); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 14px; }
|
||||
.toggle-red { display: flex; align-items: center; gap: 10px; margin-bottom: 16px; }
|
||||
@@ -194,11 +193,13 @@
|
||||
(function() {
|
||||
var podaci = {{.GrafikonJSON}};
|
||||
|
||||
var isDark = document.documentElement.getAttribute('data-tema') !== 'svetla';
|
||||
function inicijalizujGrafikon() {
|
||||
var tekstBoja = getComputedStyle(document.documentElement).getPropertyValue('--tekst-sporedni').trim() || '#6b7280';
|
||||
var ivicaBoja = getComputedStyle(document.documentElement).getPropertyValue('--ivica').trim() || '#e5e7eb';
|
||||
|
||||
var ctx = document.getElementById('grafikon-prihod').getContext('2d');
|
||||
var canvas = document.getElementById('grafikon-prihod');
|
||||
if (!canvas) return;
|
||||
var ctx = canvas.getContext('2d');
|
||||
var grafikon = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
@@ -256,6 +257,16 @@
|
||||
document.getElementById('toggle-grafikon').addEventListener('change', function() {
|
||||
document.getElementById('grafikon-wrapper').style.display = this.checked ? 'block' : 'none';
|
||||
});
|
||||
} // kraj inicijalizujGrafikon
|
||||
|
||||
if (window.Chart) {
|
||||
inicijalizujGrafikon();
|
||||
} else {
|
||||
var s = document.createElement('script');
|
||||
s.src = 'https://cdn.jsdelivr.net/npm/chart.js@4.4.4/dist/chart.umd.min.js';
|
||||
s.onload = inicijalizujGrafikon;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
@@ -5,13 +5,23 @@
|
||||
{{define "dodatni-css"}}
|
||||
<style>
|
||||
.greska-animacija { animation: shake 0.4s ease; }
|
||||
.tip-radio { display:flex; gap:12px; }
|
||||
.tip-radio label {
|
||||
display:flex; align-items:center; gap:6px;
|
||||
cursor:pointer; font-size:14px; color:var(--tekst-glavni);
|
||||
padding:8px 14px; border-radius:6px; border:1px solid var(--ivica);
|
||||
transition:background 0.15s, border-color 0.15s;
|
||||
}
|
||||
.tip-radio input[type=radio] { accent-color: var(--akcentna); }
|
||||
.tip-radio label:has(input:checked) {
|
||||
background:var(--akcentna-bleda); border-color:var(--akcentna);
|
||||
}
|
||||
</style>
|
||||
{{end}}
|
||||
|
||||
{{define "sadrzaj"}}
|
||||
<div style="width:100%;">
|
||||
|
||||
<!-- nazad dugme -->
|
||||
<a href="/klijenti" 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 klijente
|
||||
@@ -29,67 +39,84 @@
|
||||
{{end}}
|
||||
|
||||
<form method="POST" action="{{if .Izmena}}/klijenti/izmeni/{{.Klijent.ID}}{{else}}/klijenti/novi{{end}}">
|
||||
<div style="display:flex;flex-direction:column;gap:14px;">
|
||||
<input type="hidden" name="csrf_token" value="{{.CsrfToken}}">
|
||||
<div style="display:flex;flex-direction:column;gap:18px;">
|
||||
|
||||
<!-- ime i prezime -->
|
||||
<!-- tip klijenta -->
|
||||
<div>
|
||||
<div style="font-size:12px;font-weight:500;color:var(--tekst-sporedni);text-transform:uppercase;letter-spacing:0.05em;margin-bottom:10px;">
|
||||
<div class="sekcija-naslov">Tip klijenta</div>
|
||||
<div class="tip-radio">
|
||||
<label>
|
||||
<input type="radio" name="tip" value="fizicko"
|
||||
{{if or (eq .Klijent.Tip "fizicko") (eq .Klijent.Tip "")}}checked{{end}}
|
||||
onchange="promenaTipa(this.value)">
|
||||
Fizičko lice
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="tip" value="pravno"
|
||||
{{if eq .Klijent.Tip "pravno"}}checked{{end}}
|
||||
onchange="promenaTipa(this.value)">
|
||||
Pravno lice
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- sekcija fizičko lice -->
|
||||
<div id="sec-fizicko" style="display:flex;flex-direction:column;gap:12px;">
|
||||
<div class="sekcija-naslov">Ime i prezime</div>
|
||||
<div class="forma-grid-2" style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
|
||||
<div>
|
||||
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:6px;">Ime</label>
|
||||
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:6px;">
|
||||
Ime <span id="ime-oznaka" style="color:#dc2626;">*</span>
|
||||
</label>
|
||||
<input type="text" name="ime" value="{{.Klijent.Ime}}"
|
||||
placeholder="npr. Marko"
|
||||
style="width:100%;">
|
||||
placeholder="npr. Marko" style="width:100%;">
|
||||
</div>
|
||||
<div>
|
||||
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:6px;">Prezime</label>
|
||||
<input type="text" name="prezime" value="{{.Klijent.Prezime}}"
|
||||
placeholder="npr. Petrović"
|
||||
style="width:100%;">
|
||||
placeholder="npr. Petrović" style="width:100%;">
|
||||
</div>
|
||||
</div>
|
||||
<div style="max-width:260px;">
|
||||
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:6px;">JMBG</label>
|
||||
<input type="text" name="jmbg" value="{{.Klijent.JMBG}}"
|
||||
placeholder="13 cifara" maxlength="13" style="width:100%;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- naziv firme i pib -->
|
||||
<div>
|
||||
<div style="font-size:12px;font-weight:500;color:var(--tekst-sporedni);text-transform:uppercase;letter-spacing:0.05em;margin-bottom:10px;">
|
||||
Firma
|
||||
</div>
|
||||
<!-- sekcija pravno lice -->
|
||||
<div id="sec-pravno" style="display:none;flex-direction:column;gap:12px;">
|
||||
<div class="sekcija-naslov">Firma</div>
|
||||
<div class="forma-grid-2" style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
|
||||
<div>
|
||||
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:6px;">Naziv firme</label>
|
||||
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:6px;">
|
||||
Naziv firme <span style="color:#dc2626;">*</span>
|
||||
</label>
|
||||
<input type="text" name="naziv_firme" value="{{.Klijent.NazivFirme}}"
|
||||
placeholder="npr. TechSolutions d.o.o."
|
||||
style="width:100%;">
|
||||
placeholder="npr. TechSolutions d.o.o." style="width:100%;">
|
||||
</div>
|
||||
<div>
|
||||
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:6px;">PIB</label>
|
||||
<input type="text" name="pib" value="{{.Klijent.PIB}}"
|
||||
placeholder="npr. 123456789"
|
||||
style="width:100%;">
|
||||
placeholder="npr. 123456789" style="width:100%;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- telefon i e-pošta -->
|
||||
<!-- kontakt -->
|
||||
<div>
|
||||
<div style="font-size:12px;font-weight:500;color:var(--tekst-sporedni);text-transform:uppercase;letter-spacing:0.05em;margin-bottom:10px;">
|
||||
Kontakt
|
||||
</div>
|
||||
<div class="sekcija-naslov">Kontakt</div>
|
||||
<div class="forma-grid-2" style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
|
||||
<div>
|
||||
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:6px;">Telefon</label>
|
||||
<input type="text" name="telefon" value="{{.Klijent.Telefon}}"
|
||||
placeholder="npr. 060 123 4567"
|
||||
style="width:100%;">
|
||||
placeholder="npr. 060 123 4567" style="width:100%;">
|
||||
</div>
|
||||
<div>
|
||||
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:6px;">E-pošta</label>
|
||||
<input type="text" name="email" value="{{.Klijent.Email}}"
|
||||
placeholder="npr. marko@example.com"
|
||||
style="width:100%;">
|
||||
placeholder="npr. marko@example.com" style="width:100%;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -102,11 +129,6 @@
|
||||
style="width:100%;resize:vertical;">{{.Klijent.Napomena}}</textarea>
|
||||
</div>
|
||||
|
||||
<!-- obaveštenje o obaveznim poljima -->
|
||||
<div style="font-size:12px;color:var(--tekst-sporedni);">
|
||||
Mora biti uneto ime i prezime <em>ili</em> naziv firme.
|
||||
</div>
|
||||
|
||||
<!-- dugmad -->
|
||||
<div style="display:flex;justify-content:flex-end;gap:10px;margin-top:6px;">
|
||||
<a href="/klijenti" class="btn-sekundarno">Odustani</a>
|
||||
@@ -119,4 +141,23 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function promenaTipa(tip) {
|
||||
var secFiz = document.getElementById('sec-fizicko');
|
||||
var secPrav = document.getElementById('sec-pravno');
|
||||
if (tip === 'pravno') {
|
||||
secFiz.style.display = 'none';
|
||||
secPrav.style.display = 'flex';
|
||||
} else {
|
||||
secFiz.style.display = 'flex';
|
||||
secPrav.style.display = 'none';
|
||||
}
|
||||
}
|
||||
// pri učitavanju prikazujemo ispravnu sekciju
|
||||
(function() {
|
||||
var checked = document.querySelector('input[name="tip"]:checked');
|
||||
if (checked) promenaTipa(checked.value);
|
||||
})();
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
@@ -43,15 +43,16 @@
|
||||
padding-bottom: 14px;
|
||||
border-bottom: 0.5px solid var(--ivica);
|
||||
">
|
||||
<span
|
||||
style="
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--tekst-glavni);
|
||||
font-family: monospace;
|
||||
">
|
||||
<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;">
|
||||
<span style="font-size:20px;font-weight:600;color:var(--tekst-glavni);font-family:monospace;">
|
||||
{{.Nalog.BrojNaloga}}
|
||||
</span>
|
||||
{{if .Nalog.Stornirano}}
|
||||
<span style="display:inline-block;padding:3px 10px;border-radius:20px;font-size:12px;font-weight:500;background:rgba(220,38,38,0.12);color:#dc2626;">
|
||||
Stornirano
|
||||
</span>
|
||||
{{end}}
|
||||
</div>
|
||||
<a
|
||||
href="/prodaja/{{.Nalog.ID}}/stampa"
|
||||
target="_blank"
|
||||
@@ -110,18 +111,26 @@
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
style="
|
||||
font-size: 12px;
|
||||
color: var(--tekst-sporedni);
|
||||
margin-bottom: 4px;
|
||||
">
|
||||
Ukupno
|
||||
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:4px;">Način plaćanja</div>
|
||||
<div style="font-size:14px;font-weight:500;color:var(--tekst-glavni);">
|
||||
{{if eq .Nalog.NacinPlacanja "gotovina"}}Gotovina
|
||||
{{else if eq .Nalog.NacinPlacanja "kartica"}}Platna kartica
|
||||
{{else if eq .Nalog.NacinPlacanja "prenos"}}Bankarski prenos
|
||||
{{else}}—{{end}}
|
||||
</div>
|
||||
<div style="font-size: 20px; font-weight: 600; color: var(--sb-akcent)">
|
||||
</div>
|
||||
<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" .Nalog.Ukupno}} din
|
||||
</div>
|
||||
</div>
|
||||
{{if .Nalog.RazlogStorniranja}}
|
||||
<div style="grid-column:1/-1;">
|
||||
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:4px;">Razlog storniranja</div>
|
||||
<div style="font-size:13px;color:#dc2626;">{{.Nalog.RazlogStorniranja}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{if .Nalog.Napomena}}
|
||||
<div style="grid-column: 1/-1">
|
||||
<div
|
||||
@@ -184,15 +193,10 @@
|
||||
">
|
||||
Cena/kom
|
||||
</th>
|
||||
<th
|
||||
style="
|
||||
padding: 10px 20px;
|
||||
text-align: right;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--tekst-sporedni);
|
||||
width: 120px;
|
||||
">
|
||||
<th style="padding:10px 20px;text-align:center;font-size:12px;font-weight:500;color:var(--tekst-sporedni);width:80px;">
|
||||
PDV %
|
||||
</th>
|
||||
<th style="padding:10px 20px;text-align:right;font-size:12px;font-weight:500;color:var(--tekst-sporedni);width:120px;">
|
||||
Ukupno
|
||||
</th>
|
||||
</tr>
|
||||
@@ -200,105 +204,68 @@
|
||||
<tbody>
|
||||
{{range .Stavke}}
|
||||
<tr style="border-bottom: 0.5px solid var(--ivica)">
|
||||
<td
|
||||
style="
|
||||
padding: 10px 20px;
|
||||
font-size: 14px;
|
||||
color: var(--tekst-glavni);
|
||||
">
|
||||
{{.ArtikalNaziv}}
|
||||
</td>
|
||||
<td
|
||||
style="
|
||||
padding: 10px 20px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: var(--tekst-glavni);
|
||||
">
|
||||
{{.Kolicina}}
|
||||
</td>
|
||||
<td
|
||||
style="
|
||||
padding: 10px 20px;
|
||||
text-align: right;
|
||||
font-size: 14px;
|
||||
color: var(--tekst-sporedni);
|
||||
">
|
||||
{{printf "%.2f" .CenaPoKomadu}} din
|
||||
</td>
|
||||
<td
|
||||
style="
|
||||
padding: 10px 20px;
|
||||
text-align: right;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--tekst-glavni);
|
||||
">
|
||||
{{printf "%.2f" .Ukupno}} din
|
||||
<td style="padding:10px 20px;font-size:14px;color:var(--tekst-glavni);">{{.ArtikalNaziv}}</td>
|
||||
<td style="padding:10px 20px;text-align:center;font-size:14px;color:var(--tekst-glavni);">{{.Kolicina}}</td>
|
||||
<td style="padding:10px 20px;text-align:right;font-size:14px;color:var(--tekst-sporedni);">{{printf "%.2f" .CenaPoKomadu}} din</td>
|
||||
<td style="padding:10px 20px;text-align:center;font-size:13px;color:var(--tekst-sporedni);">
|
||||
{{if .PdvStopa}}{{printf "%.0f" .PdvStopa}}%{{else}}—{{end}}
|
||||
</td>
|
||||
<td style="padding:10px 20px;text-align:right;font-size:14px;font-weight:500;color:var(--tekst-glavni);">{{printf "%.2f" .Ukupno}} din</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr style="border-top: 0.5px solid var(--ivica)">
|
||||
<td
|
||||
colspan="3"
|
||||
style="
|
||||
padding: 12px 20px;
|
||||
text-align: right;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--tekst-sporedni);
|
||||
">
|
||||
Ukupno:
|
||||
</td>
|
||||
<td
|
||||
style="
|
||||
padding: 12px 20px;
|
||||
text-align: right;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--sb-akcent);
|
||||
">
|
||||
{{printf "%.2f" .Nalog.Ukupno}} din
|
||||
</td>
|
||||
<td colspan="4" style="padding:12px 20px;text-align:right;font-size:13px;font-weight:500;color:var(--tekst-sporedni);">Ukupno:</td>
|
||||
<td style="padding:12px 20px;text-align:right;font-size:16px;font-weight:600;color:var(--sb-akcent);">{{printf "%.2f" .Nalog.Ukupno}} din</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{if index $.Dozvole "prodaja.obrisi"}}
|
||||
<!-- zona za brisanje -->
|
||||
<div class="kartica detalji-kartica animiraj" style="border-color: #dc262633">
|
||||
<div
|
||||
style="display: flex; align-items: flex-start; gap: 12px; flex-wrap: wrap">
|
||||
<div style="flex: 1; min-width: 200px">
|
||||
<div
|
||||
style="
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #dc2626;
|
||||
margin-bottom: 4px;
|
||||
">
|
||||
Brisanje naloga
|
||||
<!-- storno i brisanje -->
|
||||
<div class="kartica detalji-kartica animiraj" style="border-color:#dc262633;">
|
||||
<div style="display:flex;flex-direction:column;gap:16px;">
|
||||
|
||||
{{if and (index $.Dozvole "prodaja.storno") (not .Nalog.Stornirano) }}
|
||||
<div style="display:flex;align-items:flex-start;gap:12px;flex-wrap:wrap;">
|
||||
<div style="flex:1;min-width:200px;">
|
||||
<div style="font-size:14px;font-weight:500;color:#f97316;margin-bottom:4px;">Storniranje naloga</div>
|
||||
<div style="font-size:13px;color:var(--tekst-sporedni);">
|
||||
Storno vraća sve artikle na stanje u magacinu. Nalog ostaje u evidenciji sa oznakom storniran.
|
||||
</div>
|
||||
<div style="font-size: 13px; color: var(--tekst-sporedni)">
|
||||
Brisanje je trajno. Količine artikala biće vraćene na stanje u
|
||||
magacinu.
|
||||
</div>
|
||||
<form method="POST" action="/prodaja/storno/{{.Nalog.ID}}" style="display:flex;flex-direction:column;gap:8px;align-items:flex-end;">
|
||||
<input type="hidden" name="csrf_token" value="{{.CsrfToken}}">
|
||||
<input type="text" name="razlog" placeholder="Razlog storniranja (opciono)" style="width:240px;">
|
||||
<button type="submit" class="btn-primarno" style="background:#f97316;"
|
||||
data-potvrda="Stornirati nalog {{.Nalog.BrojNaloga}}?">
|
||||
Storniraj nalog
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if index $.Dozvole "prodaja.obrisi"}}
|
||||
<div style="display:flex;align-items:flex-start;gap:12px;flex-wrap:wrap;padding-top:14px;border-top:0.5px solid var(--ivica);">
|
||||
<div style="flex:1;min-width:200px;">
|
||||
<div style="font-size:14px;font-weight:500;color:#dc2626;margin-bottom:4px;">Brisanje naloga</div>
|
||||
<div style="font-size:13px;color:var(--tekst-sporedni);">
|
||||
Brisanje je trajno.{{if not .Nalog.Stornirano}} Količine artikala biće vraćene na stanje u magacinu.{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<form method="POST" action="/prodaja/obrisi/{{.Nalog.ID}}">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn-primarno"
|
||||
style="background: #dc2626"
|
||||
data-potvrda="Da li ste sigurni da želite da obrišete nalog {{.Nalog.BrojNaloga}}? Količine artikala biće vraćene na stanje.">
|
||||
<input type="hidden" name="csrf_token" value="{{.CsrfToken}}">
|
||||
<button type="submit" class="btn-primarno" style="background:#dc2626;"
|
||||
data-potvrda="Da li ste sigurni da želite da obrišete nalog {{.Nalog.BrojNaloga}}?">
|
||||
Obriši nalog
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -49,35 +49,28 @@
|
||||
style="font-size: 16px; font-weight: 500; color: var(--tekst-glavni)">Nova prodaja</span>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 14px">
|
||||
<div class="forma-grid-2" style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
|
||||
<div>
|
||||
<label
|
||||
style="
|
||||
font-size: 13px;
|
||||
color: var(--tekst-sporedni);
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
">Klijent</label>
|
||||
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:6px;">Klijent</label>
|
||||
<select name="klijent_id" style="width: 100%">
|
||||
<option value="">— bez klijenta —</option>
|
||||
{{range .Klijenti}}
|
||||
<option value="{{.ID}}">
|
||||
{{if .NazivFirme}}{{.NazivFirme}}{{else}}{{.Ime}}
|
||||
{{.Prezime}}{{end}}
|
||||
</option>
|
||||
<option value="{{.ID}}">{{.PunoIme}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
style="
|
||||
font-size: 13px;
|
||||
color: var(--tekst-sporedni);
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
">Napomena</label>
|
||||
<textarea
|
||||
name="napomena"
|
||||
rows="2"
|
||||
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:6px;">Način plaćanja</label>
|
||||
<select name="nacin_placanja" style="width:100%;">
|
||||
<option value="gotovina">Gotovina</option>
|
||||
<option value="kartica">Platna kartica</option>
|
||||
<option value="prenos">Bankarski prenos</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:6px;">Napomena</label>
|
||||
<textarea name="napomena" rows="2"
|
||||
placeholder="Interna napomena o prodaji..."
|
||||
style="width: 100%; resize: vertical"></textarea>
|
||||
</div>
|
||||
@@ -163,6 +156,7 @@
|
||||
<template x-for="(stavka, i) in stavke" :key="i">
|
||||
<tr style="border-bottom: 0.5px solid var(--ivica)">
|
||||
<td style="padding: 8px 10px">
|
||||
<input type="hidden" :name="'pdv_stopa[]'" :value="stavka.pdv_stopa">
|
||||
<select
|
||||
:name="'artikal_id[]'"
|
||||
x-model="stavka.artikal_id"
|
||||
@@ -322,6 +316,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 10px">
|
||||
<input type="hidden" :name="'pdv_stopa[]'" :value="stavka.pdv_stopa">
|
||||
<div>
|
||||
<label
|
||||
style="
|
||||
|
||||
@@ -68,12 +68,24 @@
|
||||
<div style="font-size:14px;color:var(--tekst-glavni);">{{.Nalog.DatumZavrsetka.Format "02.01.2006."}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{if .Nalog.GarancijaDo}}
|
||||
<div>
|
||||
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:4px;">Garancija do</div>
|
||||
<div style="font-size:14px;color:#16a34a;font-weight:500;">{{.Nalog.GarancijaDo.Format "02.01.2006."}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<div>
|
||||
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:4px;">Klijent</div>
|
||||
<div style="font-size:14px;font-weight:500;color:var(--tekst-glavni);">
|
||||
{{if .KlijentNaziv}}{{.KlijentNaziv}}{{else}}—{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{if .TehnicarNaziv}}
|
||||
<div>
|
||||
<div style="font-size:12px;color:var(--tekst-sporedni);margin-bottom:4px;">Tehničar</div>
|
||||
<div style="font-size:14px;font-weight:500;color:var(--tekst-glavni);">{{.TehnicarNaziv}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -149,12 +161,81 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- placeholder za buduće funkcije -->
|
||||
<div class="kartica detalji-kartica animiraj" style="border-style:dashed;">
|
||||
<div style="display:flex;align-items:center;gap:10px;color:var(--tekst-sporedni);">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
|
||||
<span style="font-size:13px;">Istorija promena statusa i vezani artikli biće dostupni u narednoj verziji.</span>
|
||||
<!-- ugrađeni delovi -->
|
||||
<div class="kartica detalji-kartica animiraj">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;padding-bottom:14px;border-bottom:0.5px solid var(--ivica);flex-wrap:wrap;gap:10px;">
|
||||
<span style="font-size:15px;font-weight:500;color:var(--tekst-glavni);">Ugrađeni delovi</span>
|
||||
</div>
|
||||
|
||||
<!-- forma za dodavanje dela -->
|
||||
{{if index .Dozvole "servis.izmeni"}}
|
||||
<form method="POST" action="/servis/{{.Nalog.ID}}/delovi" style="margin-bottom:16px;">
|
||||
<input type="hidden" name="csrf_token" value="{{.CsrfToken}}">
|
||||
<div style="display:grid;grid-template-columns:1fr 80px 120px auto;gap:10px;align-items:flex-end;">
|
||||
<div>
|
||||
<label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">Artikal</label>
|
||||
<select name="artikal_id" style="width:100%;" required>
|
||||
<option value="">— odaberi —</option>
|
||||
{{range .Artikli}}
|
||||
<option value="{{.ID}}">{{.Naziv}} ({{.Kolicina}} kom)</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">Kol.</label>
|
||||
<input type="number" name="kolicina" min="1" value="1" style="width:100%;" required>
|
||||
</div>
|
||||
<div>
|
||||
<label style="font-size:12px;color:var(--tekst-sporedni);display:block;margin-bottom:4px;">Cena/kom (din)</label>
|
||||
<input type="number" name="cena_komada" min="0" step="0.01" value="0" style="width:100%;" required>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class="btn-primarno" style="font-size:13px;padding:8px 14px;white-space:nowrap;">+ Dodaj</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{{end}}
|
||||
|
||||
<!-- lista delova -->
|
||||
{{if .ServisniDelovi}}
|
||||
<table style="width:100%;border-collapse:collapse;">
|
||||
<thead>
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<th style="padding:8px 10px;text-align:left;font-size:12px;font-weight:500;color:var(--tekst-sporedni);">Artikal</th>
|
||||
<th style="padding:8px 10px;text-align:center;font-size:12px;font-weight:500;color:var(--tekst-sporedni);width:80px;">Kol.</th>
|
||||
<th style="padding:8px 10px;text-align:right;font-size:12px;font-weight:500;color:var(--tekst-sporedni);width:130px;">Cena/kom</th>
|
||||
<th style="padding:8px 10px;text-align:right;font-size:12px;font-weight:500;color:var(--tekst-sporedni);width:130px;">Ukupno</th>
|
||||
<th style="width:50px;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .ServisniDelovi}}
|
||||
<tr style="border-bottom:0.5px solid var(--ivica);">
|
||||
<td style="padding:9px 10px;font-size:14px;color:var(--tekst-glavni);">{{.ArtikalNaziv}}</td>
|
||||
<td style="padding:9px 10px;text-align:center;font-size:14px;color:var(--tekst-glavni);">{{.Kolicina}}</td>
|
||||
<td style="padding:9px 10px;text-align:right;font-size:14px;color:var(--tekst-glavni);">{{printf "%.2f" .CenaKomada}} din</td>
|
||||
<td style="padding:9px 10px;text-align:right;font-size:14px;font-weight:500;color:var(--tekst-glavni);">{{printf "%.2f" .Ukupno}} din</td>
|
||||
<td style="padding:9px 10px;text-align:center;">
|
||||
{{if index $.Dozvole "servis.izmeni"}}
|
||||
<form method="POST" action="/servis/{{$.Nalog.ID}}/delovi/{{.ID}}/obrisi" style="display:inline;">
|
||||
<input type="hidden" name="csrf_token" value="{{$.CsrfToken}}">
|
||||
<button type="submit"
|
||||
data-potvrda="Ukloniti deo '{{.ArtikalNaziv}}'?"
|
||||
style="background:none;border:none;cursor:pointer;color:#dc2626;font-size:16px;padding:2px 6px;border-radius:4px;">
|
||||
×
|
||||
</button>
|
||||
</form>
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{else}}
|
||||
<div style="font-size:13px;color:var(--tekst-sporedni);text-align:center;padding:16px 0;">
|
||||
Nema ugrađenih delova za ovaj nalog.
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<!-- zona za brisanje -->
|
||||
|
||||
@@ -70,25 +70,42 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- klijent -->
|
||||
<!-- klijent i tehničar -->
|
||||
<div>
|
||||
<div class="sekcija-naslov">Klijent</div>
|
||||
<div class="sekcija-naslov">Klijent i tehničar</div>
|
||||
<div class="forma-grid-2" style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
|
||||
<div>
|
||||
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:6px;">Klijent</label>
|
||||
<select name="klijent_id" style="width:100%;">
|
||||
<option value="">— bez klijenta —</option>
|
||||
{{range .Klijenti}}
|
||||
<option value="{{.ID}}"
|
||||
{{if eq .ID $.Nalog.KlijentIDVrednost}}selected{{end}}>
|
||||
{{if .NazivFirme}}{{.NazivFirme}}{{else}}{{.Ime}} {{.Prezime}}{{end}}
|
||||
{{.PunoIme}}
|
||||
</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:6px;">Tehničar</label>
|
||||
<select name="tehnicar_id" style="width:100%;">
|
||||
<option value="">— nedodeljen —</option>
|
||||
{{range .Tehnicari}}
|
||||
<option value="{{.ID}}"
|
||||
{{if eq .ID $.Nalog.TehnicarIDVrednost}}selected{{end}}>
|
||||
{{.KorisnickoIme}}
|
||||
</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- status i datum završetka -->
|
||||
<div>
|
||||
<div class="sekcija-naslov">Status</div>
|
||||
<div class="forma-grid-2" style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
|
||||
<!-- status i datumi -->
|
||||
<div>
|
||||
<div class="sekcija-naslov">Status i datumi</div>
|
||||
<div class="forma-grid-4" style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:12px;">
|
||||
<div style="grid-column:span 2;">
|
||||
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:6px;">Status naloga</label>
|
||||
<select name="status" style="width:100%;">
|
||||
{{range .SviStatusi}}
|
||||
@@ -102,6 +119,12 @@
|
||||
value="{{if .Nalog.DatumZavrsetka}}{{.Nalog.DatumZavrsetka.Format "2006-01-02"}}{{end}}"
|
||||
style="width:100%;">
|
||||
</div>
|
||||
<div>
|
||||
<label style="font-size:13px;color:var(--tekst-sporedni);display:block;margin-bottom:6px;">Garancija do</label>
|
||||
<input type="date" name="garancija_do"
|
||||
value="{{if .Nalog.GarancijaDo}}{{.Nalog.GarancijaDo.Format "2006-01-02"}}{{end}}"
|
||||
style="width:100%;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -81,14 +81,10 @@
|
||||
<div class="sidebar-overlay" id="sidebar-overlay"></div>
|
||||
{{template "sidebar" .}}
|
||||
|
||||
<div class="glavni-sadrzaj">
|
||||
<div class="glavni-sadrzaj" id="glavni-sadrzaj">
|
||||
{{template "topbar" .}}
|
||||
|
||||
<main class="sadrzaj">{{block "sadrzaj" .}}{{end}}</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- flash poruka — prikazuje se kao toast u gornjem desnom uglu -->
|
||||
<main class="sadrzaj" id="sadrzaj">
|
||||
{{if .Flash}}
|
||||
<div id="flash-toast" class="flash-toast flash-{{.Flash.Tip}}" role="alert">
|
||||
<span class="flash-ikona">{{if eq .Flash.Tip "uspeh"}}✓{{else}}!{{end}}</span>
|
||||
@@ -106,6 +102,10 @@
|
||||
})();
|
||||
</script>
|
||||
{{end}}
|
||||
{{block "sadrzaj" .}}{{end}}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- alpine.js komponente (mora biti pre Alpine-a) -->
|
||||
<script src="/static/js/ntech.js" defer></script>
|
||||
@@ -115,22 +115,27 @@
|
||||
<!-- htmx za komunikaciju sa serverom -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.x.x/dist/htmx.min.js"></script>
|
||||
|
||||
<!-- sidebar logika -->
|
||||
<!-- sidebar logika — sidebar se NIKAD ne menja pri HTMX navigaciji, pa nema potrebe za swap logikom -->
|
||||
<script>
|
||||
(function() {
|
||||
if (window._ntechSidebarDodato) return;
|
||||
window._ntechSidebarDodato = true;
|
||||
|
||||
function mobilni() { return window.innerWidth <= 768; }
|
||||
|
||||
function inicijalizujSidebar() {
|
||||
var sidebar = document.getElementById("sidebar");
|
||||
var hamburger = document.getElementById("hamburger");
|
||||
var overlay = document.getElementById("sidebar-overlay");
|
||||
if (!sidebar || !hamburger || !overlay) return;
|
||||
|
||||
if (!mobilni() && localStorage.getItem("sidebar-skupljen") === "true") {
|
||||
// skupljeno stanje pri učitavanju stranice
|
||||
if (sidebar && !mobilni() && localStorage.getItem("sidebar-skupljen") === "true") {
|
||||
sidebar.classList.add("skupljen");
|
||||
}
|
||||
document.documentElement.classList.remove('sidebar-init-skupljen');
|
||||
if (typeof ntechInicijalizujPodmeni === 'function') ntechInicijalizujPodmeni();
|
||||
|
||||
// hamburger — sidebar element živi ceo vek stranice, listener se dodaje jednom
|
||||
if (hamburger && sidebar && overlay) {
|
||||
hamburger.addEventListener("click", function() {
|
||||
if (mobilni()) {
|
||||
sidebar.classList.toggle("otvoren");
|
||||
@@ -140,27 +145,50 @@
|
||||
localStorage.setItem("sidebar-skupljen", sidebar.classList.contains("skupljen"));
|
||||
}
|
||||
});
|
||||
|
||||
overlay.addEventListener("click", function() {
|
||||
sidebar.classList.remove("otvoren");
|
||||
overlay.classList.remove("aktivan");
|
||||
});
|
||||
}
|
||||
|
||||
// resize listener se dodaje samo jednom — ne sme da se gomila po swap-ovima
|
||||
if (!window._ntechResizeDodato) {
|
||||
window._ntechResizeDodato = true;
|
||||
window.addEventListener("resize", function() {
|
||||
var sidebar = document.getElementById("sidebar");
|
||||
var overlay = document.getElementById("sidebar-overlay");
|
||||
if (sidebar && overlay && !mobilni()) {
|
||||
if (!mobilni() && sidebar && overlay) {
|
||||
sidebar.classList.remove("otvoren");
|
||||
overlay.classList.remove("aktivan");
|
||||
}
|
||||
});
|
||||
|
||||
// aktivna stavka — ažurira se pri svakoj HTMX navigaciji
|
||||
function azurirajAktivne() {
|
||||
var putanja = window.location.pathname;
|
||||
document.querySelectorAll('#sidebar .nav-stavka').forEach(function(el) {
|
||||
el.classList.remove('aktivan');
|
||||
});
|
||||
document.querySelectorAll('#sidebar .nav-podmeni').forEach(function(el) {
|
||||
el.classList.remove('otvoren');
|
||||
var strelica = el.previousElementSibling && el.previousElementSibling.querySelector('.nav-strelica svg');
|
||||
if (strelica) strelica.style.transform = 'rotate(0deg)';
|
||||
});
|
||||
document.querySelectorAll('#sidebar a.nav-stavka').forEach(function(el) {
|
||||
var href = el.getAttribute('href');
|
||||
if (!href) return;
|
||||
if (putanja === href || putanja.startsWith(href + '/')) {
|
||||
el.classList.add('aktivan');
|
||||
var podmeni = el.closest('.nav-podmeni');
|
||||
if (podmeni) {
|
||||
podmeni.classList.add('otvoren');
|
||||
var roditelj = podmeni.previousElementSibling;
|
||||
if (roditelj) {
|
||||
roditelj.classList.add('aktivan');
|
||||
var str = roditelj.querySelector('.nav-strelica svg');
|
||||
if (str) str.style.transform = 'rotate(180deg)';
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
inicijalizujSidebar();
|
||||
document.addEventListener('htmx:afterSwap', azurirajAktivne);
|
||||
})();
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user