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:
2026-06-21 01:34:15 +02:00
parent ac6deeeba4
commit 7eb472b9e6
13 changed files with 198 additions and 18 deletions
+2
View File
@@ -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)
+2
View File
@@ -37,6 +37,8 @@ type ArtikalRepository interface {
PostaviDobavljaceArtikla(ctx context.Context, artikalID int64, dobavljaciID []int64) error
// PoveziDobavljaca dodaje vezu artikaldobavljač ako ne postoji (auto pri nabavci)
PoveziDobavljaca(ctx context.Context, artikalID, dobavljacID int64) error
// OdveziDobavljaca uklanja vezu artikaldobavljač
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)
}
+10
View File
@@ -362,6 +362,16 @@ func (r *ArtikalRepo) PoveziDobavljaca(ctx context.Context, artikalID, dobavljac
return nil
}
// OdveziDobavljaca uklanja vezu artikaldobavljač
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,
+60
View File
@@ -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 810 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, "-")
}
+57
View File
@@ -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)
}
+2 -2
View File
@@ -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}}
+2 -2
View File
@@ -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);">
+1 -1
View File
@@ -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}}
+3 -3
View File
@@ -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}}
+1 -1
View File
@@ -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}}