Dodavanje modula prodaje — lista, forma, detalji, provera stanja, vraćanje pri brisanju

This commit is contained in:
2026-06-02 17:53:50 +02:00
parent d3b4bb329d
commit def84e1a69
10 changed files with 1249 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
package db
// ErrNedovoljnoKolicine se vraća kada prodaja traži više nego što ima na stanju
type ErrNedovoljnoKolicine struct {
ArtikalNaziv string
}
func (e *ErrNedovoljnoKolicine) Error() string {
return "Nedovoljno količine na stanju za artikal: " + e.ArtikalNaziv
}
+10
View File
@@ -64,3 +64,13 @@ type ServisRepository interface {
Obrisi(ctx context.Context, id int64) error
SledeciBroj(ctx context.Context) (string, error)
}
// ProdajaRepository definiše operacije nad prodajnim nalozima
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)
Obrisi(ctx context.Context, id int64) error
SledeciBroj(ctx context.Context) (string, error)
}
+262
View File
@@ -0,0 +1,262 @@
package sqlite
import (
"context"
"database/sql"
"fmt"
"time"
"ntech/internal/db"
"ntech/internal/model"
)
// ProdajaRepo je SQLite implementacija ProdajaRepository interfejsa
type ProdajaRepo struct {
db *sql.DB
}
// NoviProdajaRepo kreira novi ProdajaRepo
func NoviProdajaRepo(db *sql.DB) *ProdajaRepo {
return &ProdajaRepo{db: db}
}
// SledeciBroj generiše sledeći broj naloga u formatu PR-GGGG-NNNN
func (r *ProdajaRepo) SledeciBroj(ctx context.Context) (string, error) {
godina := time.Now().Year()
uzorak := fmt.Sprintf("PR-%d-%%", godina)
var sledeci int
err := r.db.QueryRowContext(ctx, `
SELECT COALESCE(MAX(CAST(SUBSTR(broj_naloga, 9) AS INTEGER)), 0) + 1
FROM prodajni_nalozi
WHERE broj_naloga LIKE ?`, uzorak).Scan(&sledeci)
if err != nil {
return "", fmt.Errorf("ntech: ProdajaRepo.SledeciBroj: %w", err)
}
return fmt.Sprintf("PR-%d-%04d", godina, sledeci), nil
}
// Lista vraća listu prodajnih naloga sa imenom klijenta, opcionom pretragom
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,
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
WHERE 1=1`
args := []any{}
if pretraga != "" {
upit += " AND pn.broj_naloga LIKE ?"
args = append(args, "%"+pretraga+"%")
}
upit += " ORDER BY pn.datum DESC"
redovi, err := r.db.QueryContext(ctx, upit, args...)
if err != nil {
return nil, fmt.Errorf("ntech: ProdajaRepo.Lista: %w", err)
}
defer redovi.Close()
var rezultat []model.ProdajniNalogSaDetaljem
for redovi.Next() {
var n model.ProdajniNalogSaDetaljem
var klijentID sql.NullInt64
var napomena sql.NullString
err := redovi.Scan(
&n.ID, &klijentID, &n.BrojNaloga, &napomena, &n.Ukupno, &n.Datum,
&n.KlijentNaziv,
)
if err != nil {
return nil, fmt.Errorf("ntech: ProdajaRepo.Lista: scan: %w", err)
}
if klijentID.Valid {
v := klijentID.Int64
n.KlijentID = &v
}
n.Napomena = napomena.String
rezultat = append(rezultat, n)
}
return rezultat, nil
}
// 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
FROM prodajni_nalozi WHERE id = ?`, id)
var n model.ProdajniNalog
var klijentID sql.NullInt64
var napomena sql.NullString
err := red.Scan(&n.ID, &klijentID, &n.BrojNaloga, &napomena, &n.Ukupno, &n.Datum)
if err != nil {
return nil, fmt.Errorf("ntech: ProdajaRepo.DohvatiID: %w", err)
}
if klijentID.Valid {
v := klijentID.Int64
n.KlijentID = &v
}
n.Napomena = napomena.String
return &n, nil
}
// DohvatiStavke vraća stavke prodaje sa nazivima artikala 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
FROM stavke_prodaje sp
JOIN artikli a ON a.id = sp.artikal_id
WHERE sp.nalog_id = ?
ORDER BY sp.id`, nalogID)
if err != nil {
return nil, fmt.Errorf("ntech: ProdajaRepo.DohvatiStavke: %w", err)
}
defer redovi.Close()
var stavke []model.StavkaProdajeSaArtiklom
for redovi.Next() {
var s model.StavkaProdajeSaArtiklom
err := redovi.Scan(
&s.ID, &s.NalogID, &s.ArtikalID, &s.Kolicina,
&s.CenaPoKomadu, &s.Ukupno, &s.ArtikalNaziv,
)
if err != nil {
return nil, fmt.Errorf("ntech: ProdajaRepo.DohvatiStavke: scan: %w", err)
}
stavke = append(stavke, s)
}
return stavke, nil
}
// Kreiraj upisuje novi prodajni nalog u bazu u okviru jedne transakcije.
// Za svaku stavku proverava stanje u magacinu i smanjuje ga.
// 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) {
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
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,
)
if err != nil {
return 0, fmt.Errorf("ntech: ProdajaRepo.Kreiraj: insert nalog: %w", err)
}
nalogID, err := rezultat.LastInsertId()
if err != nil {
return 0, fmt.Errorf("ntech: ProdajaRepo.Kreiraj: last insert id: %w", err)
}
// insert stavki
for _, s := range stavke {
ukupnoStavke := float64(s.Kolicina) * s.CenaPoKomadu
_, err := tx.ExecContext(ctx, `
INSERT INTO stavke_prodaje (nalog_id, artikal_id, kolicina, cena_po_komadu, ukupno)
VALUES (?, ?, ?, ?, ?)`,
nalogID, s.ArtikalID, s.Kolicina, s.CenaPoKomadu, ukupnoStavke,
)
if err != nil {
return 0, fmt.Errorf("ntech: ProdajaRepo.Kreiraj: insert stavka: %w", err)
}
}
if err := tx.Commit(); err != nil {
return 0, fmt.Errorf("ntech: ProdajaRepo.Kreiraj: commit: %w", err)
}
return nalogID, 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)
if err != nil {
return fmt.Errorf("ntech: ProdajaRepo.Obrisi: begin tx: %w", err)
}
defer tx.Rollback()
// 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.Obrisi: dohvati stavke: %w", err)
}
type povrat struct{ artikalID int64; kolicina int }
var stavke []povrat
for redovi.Next() {
var p povrat
if err := redovi.Scan(&p.artikalID, &p.kolicina); err != nil {
redovi.Close()
return fmt.Errorf("ntech: ProdajaRepo.Obrisi: scan stavke: %w", err)
}
stavke = append(stavke, p)
}
redovi.Close()
for _, p := range stavke {
_, err := tx.ExecContext(ctx,
"UPDATE artikli SET kolicina = kolicina + ? WHERE id = ?",
p.kolicina, p.artikalID,
)
if err != nil {
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)
if err != nil {
return fmt.Errorf("ntech: ProdajaRepo.Obrisi: delete: %w", err)
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("ntech: ProdajaRepo.Obrisi: commit: %w", err)
}
return nil
}
+2
View File
@@ -16,6 +16,7 @@ type Handler struct {
NabavkeRepo db.NabavkaRepository
KlijentiRepo db.KlijentRepository
ServisRepo db.ServisRepository
ProdajaRepo db.ProdajaRepository
}
// Novi kreira novi Handler sa datom bazom
@@ -28,5 +29,6 @@ func Novi(baza *sql.DB) *Handler {
NabavkeRepo: sqlite.NoviNabavkaRepo(baza),
KlijentiRepo: sqlite.NoviKlijentRepo(baza),
ServisRepo: sqlite.NoviServisRepo(baza),
ProdajaRepo: sqlite.NoviProdajaRepo(baza),
}
}
+375
View File
@@ -0,0 +1,375 @@
package handler
import (
"encoding/json"
"errors"
"html/template"
"log"
"net/http"
"strconv"
"strings"
"time"
appdb "ntech/internal/db"
"ntech/internal/db/sqlite"
"ntech/internal/model"
"github.com/go-chi/chi/v5"
)
// PodaciProdaje su podaci za stranicu sa listom prodajnih naloga
type PodaciProdaje struct {
model.PodaciStranice
Nalozi []model.ProdajniNalogSaDetaljem
Sacuvano bool
Obrisan bool
Pretraga string
}
// PodaciFormeProdaje su podaci za formu unosa nove prodaje
type PodaciFormeProdaje struct {
model.PodaciStranice
Artikli []model.ArtikalSaKategorijom
ArtikliJSON template.JS
Klijenti []model.Klijent
Greska string
}
// PodaciDetaljiProdaje su podaci za pregled jedne prodaje sa stavkama
type PodaciDetaljiProdaje struct {
model.PodaciStranice
Nalog model.ProdajniNalog
Stavke []model.StavkaProdajeSaArtiklom
KlijentNaziv string
Sacuvano bool
}
// artikalUJSONSaCenom pretvara listu artikala u template.JS vrednost sa prodajnom cenom
func artikalUJSONSaCenom(artikli []model.ArtikalSaKategorijom) template.JS {
type stavka struct {
ID int64 `json:"id"`
Naziv string `json:"naziv"`
Cena float64 `json:"cena"`
}
lista := make([]stavka, 0, len(artikli))
for _, a := range artikli {
lista = append(lista, stavka{ID: a.ID, Naziv: a.Naziv, Cena: a.ProdajnaCena})
}
b, _ := json.Marshal(lista)
return template.JS(b)
}
// Prodaja renderuje listu svih prodajnih naloga
func (h *Handler) Prodaja(w http.ResponseWriter, r *http.Request) {
podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
if err != nil {
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
return
}
pretraga := strings.TrimSpace(r.URL.Query().Get("pretraga"))
nalozi, err := h.ProdajaRepo.Lista(r.Context(), pretraga)
if err != nil {
http.Error(w, "Greška pri učitavanju prodaje", http.StatusInternalServerError)
return
}
podaci := PodaciProdaje{
PodaciStranice: model.PodaciStranice{
Stranica: "prodaja",
NaslovStranice: "Prodaja",
Tema: podesavanja["tema"],
NazivFirme: podesavanja["naziv_firme"],
Podnazlov: podesavanja["podnazlov"],
LogoTip: podesavanja["logo_tip"],
LogoPutanja: podesavanja["logo_putanja"],
Korisnik: "Admin",
},
Nalozi: nalozi,
Sacuvano: r.URL.Query().Get("sacuvano") == "1",
Obrisan: r.URL.Query().Get("obrisan") == "1",
Pretraga: pretraga,
}
tmpl, err := template.ParseFiles(
"web/templates/teme/podrazumevana/base.html",
"web/templates/komponente/sidebar.html",
"web/templates/komponente/topbar.html",
"web/templates/stranice/prodaja.html",
)
if err != nil {
log.Printf("greška pri učitavanju šablona: %v", err)
http.Error(w, "Greška pri učitavanju stranice", http.StatusInternalServerError)
return
}
if err := tmpl.ExecuteTemplate(w, "base", podaci); err != nil {
log.Printf("greška pri renderovanju: %v", err)
http.Error(w, "Greška pri prikazu stranice", http.StatusInternalServerError)
}
}
// NovaProdaja prikazuje formu za unos novog prodajnog naloga
func (h *Handler) NovaProdaja(w http.ResponseWriter, r *http.Request) {
podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
if err != nil {
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
return
}
artikli, err := h.Artikli.Lista(r.Context(), appdb.ArtikalFilter{})
if err != nil {
http.Error(w, "Greška pri učitavanju artikala", http.StatusInternalServerError)
return
}
klijenti, err := h.KlijentiRepo.Lista(r.Context(), "")
if err != nil {
http.Error(w, "Greška pri učitavanju klijenata", http.StatusInternalServerError)
return
}
renderujFormuProdaje(w, PodaciFormeProdaje{
PodaciStranice: model.PodaciStranice{
Stranica: "prodaja",
NaslovStranice: "Nova prodaja",
Tema: podesavanja["tema"],
NazivFirme: podesavanja["naziv_firme"],
Podnazlov: podesavanja["podnazlov"],
LogoTip: podesavanja["logo_tip"],
LogoPutanja: podesavanja["logo_putanja"],
Korisnik: "Admin",
},
Artikli: artikli,
ArtikliJSON: artikalUJSONSaCenom(artikli),
Klijenti: klijenti,
})
}
// SacuvajProdaju prima POST formu, parsira stavke i upisuje prodajni nalog u bazu
func (h *Handler) SacuvajProdaju(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, "Greška pri čitanju forme", http.StatusBadRequest)
return
}
nalog, stavke, greska := parseFormuProdaje(r)
renderujGresku := func(poruka string) {
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
artikli, _ := h.Artikli.Lista(r.Context(), appdb.ArtikalFilter{})
klijenti, _ := h.KlijentiRepo.Lista(r.Context(), "")
renderujFormuProdaje(w, PodaciFormeProdaje{
PodaciStranice: model.PodaciStranice{
Stranica: "prodaja",
NaslovStranice: "Nova prodaja",
Tema: podesavanja["tema"],
NazivFirme: podesavanja["naziv_firme"],
Podnazlov: podesavanja["podnazlov"],
LogoTip: podesavanja["logo_tip"],
LogoPutanja: podesavanja["logo_putanja"],
Korisnik: "Admin",
},
Artikli: artikli,
ArtikliJSON: artikalUJSONSaCenom(artikli),
Klijenti: klijenti,
Greska: poruka,
})
}
if greska != "" {
renderujGresku(greska)
return
}
brojNaloga, err := h.ProdajaRepo.SledeciBroj(r.Context())
if err != nil {
log.Printf("greška pri generisanju broja naloga: %v", err)
renderujGresku("Greška pri generisanju broja naloga.")
return
}
nalog.BrojNaloga = brojNaloga
nalog.Datum = time.Now()
var ukupno float64
for _, s := range stavke {
ukupno += float64(s.Kolicina) * s.CenaPoKomadu
}
nalog.Ukupno = ukupno
id, err := h.ProdajaRepo.Kreiraj(r.Context(), &nalog, stavke)
if err != nil {
var errStanje *appdb.ErrNedovoljnoKolicine
if errors.As(err, &errStanje) {
renderujGresku(errStanje.Error())
} else {
log.Printf("greška pri čuvanju prodaje: %v", err)
renderujGresku("Greška pri čuvanju prodajnog naloga.")
}
return
}
http.Redirect(w, r, "/prodaja/"+strconv.FormatInt(id, 10)+"?sacuvano=1", http.StatusSeeOther)
}
// DetaljiProdaje prikazuje pregled jednog prodajnog naloga sa svim stavkama
func (h *Handler) DetaljiProdaje(w http.ResponseWriter, r *http.Request) {
id, err := parseID(chi.URLParam(r, "id"))
if err != nil {
http.Error(w, "Neispravan ID naloga", http.StatusBadRequest)
return
}
nalog, err := h.ProdajaRepo.DohvatiID(r.Context(), id)
if err != nil {
http.Error(w, "Nalog nije pronađen", http.StatusNotFound)
return
}
stavke, err := h.ProdajaRepo.DohvatiStavke(r.Context(), id)
if err != nil {
http.Error(w, "Greška pri učitavanju stavki", http.StatusInternalServerError)
return
}
podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
if err != nil {
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
return
}
klijentNaziv := ""
if nalog.KlijentID != nil {
klijent, err := h.KlijentiRepo.DohvatiID(r.Context(), *nalog.KlijentID)
if err == nil {
if klijent.NazivFirme != "" {
klijentNaziv = klijent.NazivFirme
} else {
klijentNaziv = strings.TrimSpace(klijent.Ime + " " + klijent.Prezime)
}
}
}
podaci := PodaciDetaljiProdaje{
PodaciStranice: model.PodaciStranice{
Stranica: "prodaja",
NaslovStranice: "Detalji prodaje",
Tema: podesavanja["tema"],
NazivFirme: podesavanja["naziv_firme"],
Podnazlov: podesavanja["podnazlov"],
LogoTip: podesavanja["logo_tip"],
LogoPutanja: podesavanja["logo_putanja"],
Korisnik: "Admin",
},
Nalog: *nalog,
Stavke: stavke,
KlijentNaziv: klijentNaziv,
Sacuvano: r.URL.Query().Get("sacuvano") == "1",
}
tmpl, err := template.ParseFiles(
"web/templates/teme/podrazumevana/base.html",
"web/templates/komponente/sidebar.html",
"web/templates/komponente/topbar.html",
"web/templates/stranice/prodaja_detalji.html",
)
if err != nil {
log.Printf("greška pri učitavanju šablona: %v", err)
http.Error(w, "Greška pri učitavanju stranice", http.StatusInternalServerError)
return
}
if err := tmpl.ExecuteTemplate(w, "base", podaci); err != nil {
log.Printf("greška pri renderovanju: %v", err)
http.Error(w, "Greška pri prikazu stranice", http.StatusInternalServerError)
}
}
// ObrisiProdaju prima POST zahtev, vraća stanje na magacin i briše nalog
func (h *Handler) ObrisiProdaju(w http.ResponseWriter, r *http.Request) {
id, err := parseID(chi.URLParam(r, "id"))
if err != nil {
http.Error(w, "Neispravan ID naloga", http.StatusBadRequest)
return
}
if err := h.ProdajaRepo.Obrisi(r.Context(), id); err != nil {
http.Error(w, "Greška pri brisanju naloga", http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/prodaja?obrisan=1", http.StatusSeeOther)
}
// parseFormuProdaje čita zaglavlje i stavke iz HTTP forme i vraća model i eventualnu grešku
func parseFormuProdaje(r *http.Request) (model.ProdajniNalog, []model.StavkaProdaje, string) {
var nalog model.ProdajniNalog
if klijentIDStr := r.FormValue("klijent_id"); klijentIDStr != "" {
id, err := strconv.ParseInt(klijentIDStr, 10, 64)
if err == nil {
nalog.KlijentID = &id
}
}
nalog.Napomena = strings.TrimSpace(r.FormValue("napomena"))
artikalIDovi := r.Form["artikal_id[]"]
kolicine := r.Form["kolicina[]"]
cene := r.Form["cena_po_komadu[]"]
if len(artikalIDovi) == 0 {
return nalog, nil, "Prodaja mora imati najmanje jednu stavku."
}
if len(artikalIDovi) != len(kolicine) || len(artikalIDovi) != len(cene) {
return nalog, nil, "Greška u podacima forme — broj stavki nije ispravan."
}
var stavke []model.StavkaProdaje
for i := range artikalIDovi {
artikalID, err := strconv.ParseInt(strings.TrimSpace(artikalIDovi[i]), 10, 64)
if err != nil || artikalID <= 0 {
return nalog, nil, "Neispravan artikal u stavci."
}
kolicina, err := strconv.Atoi(strings.TrimSpace(kolicine[i]))
if err != nil || kolicina <= 0 {
return nalog, nil, "Količina mora biti pozitivan broj."
}
cena, err := strconv.ParseFloat(strings.TrimSpace(cene[i]), 64)
if err != nil || cena < 0 {
return nalog, nil, "Cena mora biti pozitivan broj."
}
stavke = append(stavke, model.StavkaProdaje{
ArtikalID: artikalID,
Kolicina: kolicina,
CenaPoKomadu: cena,
})
}
return nalog, stavke, ""
}
// renderujFormuProdaje renderuje HTML šablon forme za unos nove prodaje
func renderujFormuProdaje(w http.ResponseWriter, podaci PodaciFormeProdaje) {
tmpl, err := template.ParseFiles(
"web/templates/teme/podrazumevana/base.html",
"web/templates/komponente/sidebar.html",
"web/templates/komponente/topbar.html",
"web/templates/stranice/prodaja_forma.html",
)
if err != nil {
log.Printf("greška pri učitavanju šablona: %v", err)
http.Error(w, "Greška pri učitavanju stranice", http.StatusInternalServerError)
return
}
if err := tmpl.ExecuteTemplate(w, "base", podaci); err != nil {
log.Printf("greška pri renderovanju: %v", err)
http.Error(w, "Greška pri prikazu stranice", http.StatusInternalServerError)
}
}
+35
View File
@@ -0,0 +1,35 @@
package model
import "time"
// ProdajniNalog predstavlja zaglavlje jedne prodaje
type ProdajniNalog struct {
ID int64
KlijentID *int64
BrojNaloga string
Napomena string
Ukupno float64
Datum time.Time
}
// StavkaProdaje predstavlja jednu liniju (artikal) unutar prodaje
type StavkaProdaje struct {
ID int64
NalogID int64
ArtikalID int64
Kolicina int
CenaPoKomadu float64
Ukupno float64
}
// ProdajniNalogSaDetaljem je nalog sa nazivom klijenta — za prikaz u listi
type ProdajniNalogSaDetaljem struct {
ProdajniNalog
KlijentNaziv string
}
// StavkaProdajeSaArtiklom je stavka prodaje sa nazivom artikla — za prikaz u detaljima
type StavkaProdajeSaArtiklom struct {
StavkaProdaje
ArtikalNaziv string
}