Kartica artikla: prikaz vezanih dobavljača sa dodavanjem/uklanjanjem; helper telefon (064/123-4567); CLAUDE.md frontend i formatiranje sekcije
This commit is contained in:
@@ -309,6 +309,8 @@ func main() {
|
||||
r.With(doz("artikal.izmeni")).Post("/magacin/izmeni/{id}", h.SacuvajIzmenuArtikla)
|
||||
r.With(doz("artikal.obrisi")).Get("/magacin/obrisi/{id}", h.ObrisiArtikal)
|
||||
r.With(doz("artikal.obrisi")).Get("/magacin/vrati/{id}", h.VratiArtikal)
|
||||
r.With(doz("artikal.izmeni")).Post("/magacin/kartica/{id}/dobavljac/dodaj", h.DodajDobavljacaArtiklu)
|
||||
r.With(doz("artikal.izmeni")).Post("/magacin/kartica/{id}/dobavljac/obrisi", h.ObrisiDobavljacaArtikla)
|
||||
r.With(doz("artikal.premesti")).Post("/magacin/premesti/{id}", h.PremestiArtikal)
|
||||
r.With(doz("artikal.izmeni")).Post("/magacin/promeni-cenu/{id}", h.PromeniCenuArtikla)
|
||||
r.With(doz("artikal.izmeni")).Get("/nivelacije", h.Nivelacije)
|
||||
|
||||
@@ -37,6 +37,8 @@ type ArtikalRepository interface {
|
||||
PostaviDobavljaceArtikla(ctx context.Context, artikalID int64, dobavljaciID []int64) error
|
||||
// PoveziDobavljaca dodaje vezu artikal–dobavljač ako ne postoji (auto pri nabavci)
|
||||
PoveziDobavljaca(ctx context.Context, artikalID, dobavljacID int64) error
|
||||
// OdveziDobavljaca uklanja vezu artikal–dobavljač
|
||||
OdveziDobavljaca(ctx context.Context, artikalID, dobavljacID int64) error
|
||||
// SveDobavljaceArtikala vraća mapu artikal_id → lista dobavljac_id (za filter u nabavci)
|
||||
SveDobavljaceArtikala(ctx context.Context) (map[int64][]int64, error)
|
||||
}
|
||||
|
||||
@@ -362,6 +362,16 @@ func (r *ArtikalRepo) PoveziDobavljaca(ctx context.Context, artikalID, dobavljac
|
||||
return nil
|
||||
}
|
||||
|
||||
// OdveziDobavljaca uklanja vezu artikal–dobavljač
|
||||
func (r *ArtikalRepo) OdveziDobavljaca(ctx context.Context, artikalID, dobavljacID int64) error {
|
||||
_, err := r.db.ExecContext(ctx,
|
||||
"DELETE FROM artikal_dobavljac WHERE artikal_id = ? AND dobavljac_id = ?", artikalID, dobavljacID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntech: ArtikalRepo.OdveziDobavljaca: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SveDobavljaceArtikala vraća mapu artikal_id → lista dobavljac_id
|
||||
func (r *ArtikalRepo) SveDobavljaceArtikala(ctx context.Context) (map[int64][]int64, error) {
|
||||
redovi, err := r.db.QueryContext(ctx,
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"log/slog"
|
||||
"math"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var bazniSabloni = []string{
|
||||
@@ -62,6 +63,8 @@ var sablonskeFunkcije = template.FuncMap{
|
||||
"dinariCeli": func(v float64) string {
|
||||
return formatirajDinare(v, 0)
|
||||
},
|
||||
// telefon formatira srpski broj telefona radi lakšeg čitanja: "0641234567" → "064 123 4567"
|
||||
"telefon": formatirajTelefon,
|
||||
// statusPre vraća true ako je `a` pre `b` u redosledu statusa
|
||||
"statusPre": func(a, b string, statusi []string) bool {
|
||||
ia, ib := -1, -1
|
||||
@@ -219,3 +222,60 @@ func formatirajDinare(v float64, decimale int) string {
|
||||
}
|
||||
return rezultat
|
||||
}
|
||||
|
||||
// formatirajTelefon formatira srpski broj telefona radi lakšeg čitanja:
|
||||
// pozivni broj odvojen kosom crtom, ostatak grupisan crticom.
|
||||
// Primeri: "0641234567" → "064/123-4567", "+381641234567" → "+381 64/123-4567".
|
||||
// Ako format nije prepoznat, vraća original.
|
||||
func formatirajTelefon(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// izdvoj cifre i zapamti da li je međunarodni (+)
|
||||
medjunarodni := strings.HasPrefix(s, "+") || strings.HasPrefix(s, "00")
|
||||
var cifre []rune
|
||||
for _, c := range s {
|
||||
if c >= '0' && c <= '9' {
|
||||
cifre = append(cifre, c)
|
||||
}
|
||||
}
|
||||
d := string(cifre)
|
||||
|
||||
// međunarodni srpski prefiks (381): "+381 64/123-4567"
|
||||
if medjunarodni {
|
||||
d = strings.TrimPrefix(d, "00")
|
||||
if strings.HasPrefix(d, "381") {
|
||||
ostatak := d[3:] // bez vodeće nule, npr. "641234567"
|
||||
if len(ostatak) < 7 || len(ostatak) > 9 {
|
||||
return s
|
||||
}
|
||||
return "+381 " + ostatak[:2] + "/" + grupisiTelefon(ostatak[2:])
|
||||
}
|
||||
return s // strani broj — ne diramo
|
||||
}
|
||||
|
||||
// lokalni format: očekujemo vodeću nulu i 8–10 cifara ukupno
|
||||
if !strings.HasPrefix(d, "0") || len(d) < 8 || len(d) > 10 {
|
||||
return s
|
||||
}
|
||||
// pozivni (3 cifre, npr. 064/011) "/" ostatak grupisan crticom
|
||||
return d[:3] + "/" + grupisiTelefon(d[3:])
|
||||
}
|
||||
|
||||
// grupisiTelefon deli niz cifara u grupe od po 3 crticom (poslednja može 4) — "1234567" → "123-4567"
|
||||
func grupisiTelefon(d string) string {
|
||||
if len(d) <= 4 {
|
||||
return d
|
||||
}
|
||||
var delovi []string
|
||||
delovi = append(delovi, d[:3])
|
||||
ostatak := d[3:]
|
||||
for len(ostatak) > 4 {
|
||||
delovi = append(delovi, ostatak[:3])
|
||||
ostatak = ostatak[3:]
|
||||
}
|
||||
delovi = append(delovi, ostatak)
|
||||
return strings.Join(delovi, "-")
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
@@ -222,6 +223,8 @@ type PodaciMagacinskeKartice struct {
|
||||
model.PodaciStranice
|
||||
Artikal model.Artikal
|
||||
Promene []model.MagacinskaPromenaSaDetaljem
|
||||
Dobavljaci []model.Dobavljac // dobavljači vezani za artikal
|
||||
DostupniDobavljaci []model.Dobavljac // dobavljači koji još nisu vezani (za dodavanje)
|
||||
}
|
||||
|
||||
// MagacinskaKartica prikazuje sve promene stanja za jedan artikal
|
||||
@@ -250,6 +253,22 @@ func (h *Handler) MagacinskaKartica(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// dobavljači: vezani za artikal i oni koji još nisu vezani (za padajući izbor)
|
||||
sviDobavljaci, _ := h.DobavljaciRepo.Lista(r.Context(), "")
|
||||
vezaniIDs, _ := h.Artikli.DobavljaciArtikla(r.Context(), id)
|
||||
vezanSet := map[int64]bool{}
|
||||
for _, did := range vezaniIDs {
|
||||
vezanSet[did] = true
|
||||
}
|
||||
var vezani, dostupni []model.Dobavljac
|
||||
for _, d := range sviDobavljaci {
|
||||
if vezanSet[d.ID] {
|
||||
vezani = append(vezani, d)
|
||||
} else {
|
||||
dostupni = append(dostupni, d)
|
||||
}
|
||||
}
|
||||
|
||||
ps := h.popuniPodaciStranice(r, podesavanja)
|
||||
ps.Stranica = "magacin"
|
||||
ps.NaslovStranice = "Kartica: " + artikal.Naziv
|
||||
@@ -258,5 +277,43 @@ func (h *Handler) MagacinskaKartica(w http.ResponseWriter, r *http.Request) {
|
||||
PodaciStranice: ps,
|
||||
Artikal: *artikal,
|
||||
Promene: promene,
|
||||
Dobavljaci: vezani,
|
||||
DostupniDobavljaci: dostupni,
|
||||
})
|
||||
}
|
||||
|
||||
// DodajDobavljacaArtiklu veže izabranog dobavljača za artikal
|
||||
func (h *Handler) DodajDobavljacaArtiklu(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||
if err != nil {
|
||||
http.Error(w, "Neispravan ID artikla", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
dobID, err := strconv.ParseInt(r.FormValue("dobavljac_id"), 10, 64)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/magacin/kartica/"+chi.URLParam(r, "id"), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
if e := h.Artikli.PoveziDobavljaca(r.Context(), id, dobID); e != nil {
|
||||
slog.Error("vezivanje dobavljača nije uspelo", "artikal_id", id, "error", e)
|
||||
}
|
||||
http.Redirect(w, r, "/magacin/kartica/"+chi.URLParam(r, "id"), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// ObrisiDobavljacaArtikla uklanja vezu dobavljača sa artiklom
|
||||
func (h *Handler) ObrisiDobavljacaArtikla(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||
if err != nil {
|
||||
http.Error(w, "Neispravan ID artikla", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
dobID, err := strconv.ParseInt(r.FormValue("dobavljac_id"), 10, 64)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/magacin/kartica/"+chi.URLParam(r, "id"), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
if e := h.Artikli.OdveziDobavljaca(r.Context(), id, dobID); e != nil {
|
||||
slog.Error("uklanjanje dobavljača nije uspelo", "artikal_id", id, "error", e)
|
||||
}
|
||||
http.Redirect(w, r, "/magacin/kartica/"+chi.URLParam(r, "id"), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
{{if .KontaktOsoba}}{{.KontaktOsoba}}{{else}}—{{end}}
|
||||
</td>
|
||||
<td style="padding:12px 16px;font-size:13px;color:var(--tekst-sporedni);">
|
||||
{{if .Telefon}}{{.Telefon}}{{else}}—{{end}}
|
||||
{{if .Telefon}}{{telefon .Telefon}}{{else}}—{{end}}
|
||||
</td>
|
||||
<td style="padding:12px 16px;font-size:13px;color:var(--tekst-sporedni);">
|
||||
{{if .Email}}{{.Email}}{{else}}—{{end}}
|
||||
@@ -109,7 +109,7 @@
|
||||
{{end}}
|
||||
{{if .Telefon}}
|
||||
<div class="pomocni-tekst">
|
||||
<span style="color:var(--tekst-glavni);font-weight:500;">Telefon:</span> {{.Telefon}}
|
||||
<span style="color:var(--tekst-glavni);font-weight:500;">Telefon:</span> {{telefon .Telefon}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{if .Email}}
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
{{end}}
|
||||
</td>
|
||||
<td class="pomocni-tekst">
|
||||
{{if .Telefon}}{{.Telefon}}{{else}}—{{end}}
|
||||
{{if .Telefon}}{{telefon .Telefon}}{{else}}—{{end}}
|
||||
</td>
|
||||
<td class="pomocni-tekst">
|
||||
{{if .Email}}{{.Email}}{{else}}—{{end}}
|
||||
@@ -141,7 +141,7 @@
|
||||
<div class="kolona" style="gap:6px;">
|
||||
{{if .Telefon}}
|
||||
<div class="pomocni-tekst">
|
||||
<span style="color:var(--tekst-glavni);font-weight:500;">Telefon:</span> {{.Telefon}}
|
||||
<span style="color:var(--tekst-glavni);font-weight:500;">Telefon:</span> {{telefon .Telefon}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{if .Email}}
|
||||
|
||||
@@ -39,6 +39,55 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- dobavljači artikla -->
|
||||
<div class="kartica animiraj">
|
||||
<div style="font-size:14px;font-weight:500;color:var(--tekst-glavni);margin-bottom:14px;padding-bottom:10px;border-bottom:0.5px solid var(--ivica);">
|
||||
Dobavljači
|
||||
<span style="font-size:12px;font-weight:400;color:var(--tekst-slabi);margin-left:8px;">{{len .Dobavljaci}}</span>
|
||||
</div>
|
||||
|
||||
{{if .Dobavljaci}}
|
||||
<div style="display:flex;flex-direction:column;gap:8px;margin-bottom:16px;">
|
||||
{{range .Dobavljaci}}
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;padding:8px 12px;border:0.5px solid var(--ivica);border-radius:8px;">
|
||||
<div>
|
||||
<div style="font-size:14px;font-weight:500;color:var(--tekst-glavni);">{{.Naziv}}</div>
|
||||
{{if or .Telefon .KontaktOsoba}}
|
||||
<div style="font-size:12px;color:var(--tekst-sporedni);margin-top:2px;">
|
||||
{{if .KontaktOsoba}}{{.KontaktOsoba}}{{end}}{{if and .KontaktOsoba .Telefon}} · {{end}}{{if .Telefon}}{{telefon .Telefon}}{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{if index $.Dozvole "artikal.izmeni"}}
|
||||
<form method="POST" action="/magacin/kartica/{{$.Artikal.ID}}/dobavljac/obrisi" style="margin:0;">
|
||||
<input type="hidden" name="dobavljac_id" value="{{.ID}}">
|
||||
<button type="submit" class="btn-obrisi-malo" data-potvrda="Ukloniti dobavljača {{.Naziv}} sa ovog artikla?">Ukloni</button>
|
||||
</form>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
<div style="font-size:13px;color:var(--tekst-sporedni);margin-bottom:16px;">Nijedan dobavljač nije vezan za ovaj artikal.</div>
|
||||
{{end}}
|
||||
|
||||
{{if index .Dozvole "artikal.izmeni"}}
|
||||
{{if .DostupniDobavljaci}}
|
||||
<form method="POST" action="/magacin/kartica/{{.Artikal.ID}}/dobavljac/dodaj" style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;">
|
||||
<select name="dobavljac_id" required style="flex:1;min-width:200px;">
|
||||
<option value="">— izaberi dobavljača —</option>
|
||||
{{range .DostupniDobavljaci}}
|
||||
<option value="{{.ID}}">{{.Naziv}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
<button type="submit" class="btn-primarno">+ Dodaj dobavljača</button>
|
||||
</form>
|
||||
{{else}}
|
||||
<div style="font-size:12px;color:var(--tekst-slabi);">Svi dobavljači su već vezani za ovaj artikal.</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<!-- tabela promena -->
|
||||
<div class="kartica animiraj">
|
||||
<div style="font-size:14px;font-weight:500;color:var(--tekst-glavni);margin-bottom:14px;padding-bottom:10px;border-bottom:0.5px solid var(--ivica);">
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
{{end}} {{if .Adresa}}
|
||||
<div class="firma-kontakt">{{.Adresa}}</div>
|
||||
{{end}} {{if .Telefon}}
|
||||
<div class="firma-kontakt">{{.Telefon}}</div>
|
||||
<div class="firma-kontakt">{{telefon .Telefon}}</div>
|
||||
{{end}} {{if .PIB}}
|
||||
<div class="firma-kontakt">PIB: {{.PIB}}</div>
|
||||
{{end}}
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
<div class="firma-info">
|
||||
{{if .Podnazlov}}{{.Podnazlov}}<br>{{end}}
|
||||
{{if .Adresa}}{{.Adresa}}<br>{{end}}
|
||||
{{if .Telefon}}Tel: {{.Telefon}}<br>{{end}}
|
||||
{{if .Telefon}}Tel: {{telefon .Telefon}}<br>{{end}}
|
||||
{{if .PIB}}PIB: {{.PIB}}{{end}}
|
||||
</div>
|
||||
</div>
|
||||
@@ -112,7 +112,7 @@
|
||||
<div class="strana-naziv">{{if .NazivFirme}}{{.NazivFirme}}{{else}}—{{end}}</div>
|
||||
<div class="strana-info">
|
||||
{{if .Adresa}}{{.Adresa}}<br>{{end}}
|
||||
{{if .Telefon}}{{.Telefon}}{{end}}
|
||||
{{if .Telefon}}{{telefon .Telefon}}{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="strana-kartica">
|
||||
@@ -121,7 +121,7 @@
|
||||
<div class="strana-naziv">{{.KlijentNaziv}}</div>
|
||||
<div class="strana-info">
|
||||
{{if .Klijent}}
|
||||
{{if .Klijent.Telefon}}Tel: {{.Klijent.Telefon}}<br>{{end}}
|
||||
{{if .Klijent.Telefon}}Tel: {{telefon .Klijent.Telefon}}<br>{{end}}
|
||||
{{if .Klijent.Email}}{{.Klijent.Email}}<br>{{end}}
|
||||
{{if .Klijent.Mesto}}{{.Klijent.Mesto}}{{end}}
|
||||
{{end}}
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
<div class="firma-info">
|
||||
{{if .Podnazlov}}{{.Podnazlov}}<br>{{end}}
|
||||
{{if .Adresa}}{{.Adresa}}<br>{{end}}
|
||||
{{if .Telefon}}Tel: {{.Telefon}}<br>{{end}}
|
||||
{{if .Telefon}}Tel: {{telefon .Telefon}}<br>{{end}}
|
||||
{{if .PIB}}PIB: {{.PIB}}{{end}}
|
||||
</div>
|
||||
</div>
|
||||
@@ -109,7 +109,7 @@
|
||||
<div class="strana-naziv">{{if .NazivFirme}}{{.NazivFirme}}{{else}}—{{end}}</div>
|
||||
<div class="strana-info">
|
||||
{{if .Adresa}}{{.Adresa}}<br>{{end}}
|
||||
{{if .Telefon}}{{.Telefon}}{{end}}
|
||||
{{if .Telefon}}{{telefon .Telefon}}{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="strana-kartica">
|
||||
@@ -118,7 +118,7 @@
|
||||
<div class="strana-naziv">{{.KlijentNaziv}}</div>
|
||||
<div class="strana-info">
|
||||
{{if .Klijent}}
|
||||
{{if .Klijent.Telefon}}Tel: {{.Klijent.Telefon}}<br>{{end}}
|
||||
{{if .Klijent.Telefon}}Tel: {{telefon .Klijent.Telefon}}<br>{{end}}
|
||||
{{if .Klijent.Email}}{{.Klijent.Email}}<br>{{end}}
|
||||
{{if .Klijent.Mesto}}{{.Klijent.Mesto}}{{end}}
|
||||
{{end}}
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
<div class="firma-info">
|
||||
{{if .Podnazlov}}{{.Podnazlov}}<br>{{end}}
|
||||
{{if .Adresa}}{{.Adresa}}<br>{{end}}
|
||||
{{if .Telefon}}Tel: {{.Telefon}}<br>{{end}}
|
||||
{{if .Telefon}}Tel: {{telefon .Telefon}}<br>{{end}}
|
||||
{{if .PIB}}PIB: {{.PIB}}{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -198,7 +198,7 @@
|
||||
{{if .Telefon}}
|
||||
<div class="kontakt">
|
||||
<div class="kontakt-naslov">Kontakt</div>
|
||||
<a href="tel:{{.Telefon}}" class="kontakt-tel">{{.Telefon}}</a>
|
||||
<a href="tel:{{.Telefon}}" class="kontakt-tel">{{telefon .Telefon}}</a>
|
||||
{{if .Adresa}}<div class="kontakt-adresa">{{.Adresa}}</div>{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
Reference in New Issue
Block a user