Paginacija, interaktivna pretraga i optimizacija prikaza

- Dodata server-side paginacija za magacin (127 artikala) i klijente (1040)
  — Limit/Offset u ArtikalFilter i KlijentFilter, 100 po stranici
  — PrebrojiPoFilteru za izračunavanje ukupnog broja stranica
- Interaktivna pretraga (search-as-you-type) sa HTMX:
  — hx-trigger="keyup changed delay:300ms" na polju pretrage
  — HTMX menja samo #magacin-rezultati / #klijenti-rezultati
  — Polje pretrage ostaje u fokusu tokom osvežavanja
- Popravljena pretraga klijenata po imenu i prezimenu:
  — Dodato (ime || ' ' || prezime) LIKE u sva tri upita
  — "Ivana Lazić" sada pronalazi klijenta
- CSS optimizacije za velike liste:
  — content-visibility: auto na redovima tabela i karticama
  — contain-intrinsic-size za stabilan scroll
  — animation-delay produžen do 20. reda / 10. kartice
This commit is contained in:
2026-06-20 16:19:42 +02:00
parent 064d6dfa2a
commit a8f368ca06
8 changed files with 346 additions and 37 deletions
+12
View File
@@ -10,6 +10,7 @@ import (
// ArtikalRepository definiše operacije nad artiklima // ArtikalRepository definiše operacije nad artiklima
type ArtikalRepository interface { type ArtikalRepository interface {
Lista(ctx context.Context, filter ArtikalFilter) ([]model.ArtikalSaKategorijom, error) Lista(ctx context.Context, filter ArtikalFilter) ([]model.ArtikalSaKategorijom, error)
PrebrojiPoFilteru(ctx context.Context, filter ArtikalFilter) (int, error)
DohvatiID(ctx context.Context, id int64) (*model.Artikal, error) DohvatiID(ctx context.Context, id int64) (*model.Artikal, error)
Kreiraj(ctx context.Context, a *model.Artikal) (int64, error) Kreiraj(ctx context.Context, a *model.Artikal) (int64, error)
Izmeni(ctx context.Context, a *model.Artikal) error Izmeni(ctx context.Context, a *model.Artikal) error
@@ -78,6 +79,8 @@ type ArtikalFilter struct {
Pretraga string Pretraga string
KategorijaID *int64 KategorijaID *int64
SamoKriticni bool SamoKriticni bool
Limit int
Offset int
} }
// NabavkaRepository definiše operacije nad nabavkama // NabavkaRepository definiše operacije nad nabavkama
@@ -99,9 +102,18 @@ type DobavljacRepository interface {
Obrisi(ctx context.Context, id int64) error Obrisi(ctx context.Context, id int64) error
} }
// KlijentFilter definiše parametre za filtriranje liste klijenata
type KlijentFilter struct {
Pretraga string
Limit int
Offset int
}
// KlijentRepository definiše operacije nad klijentima // KlijentRepository definiše operacije nad klijentima
type KlijentRepository interface { type KlijentRepository interface {
Lista(ctx context.Context, pretraga string) ([]model.Klijent, error) Lista(ctx context.Context, pretraga string) ([]model.Klijent, error)
ListaFilter(ctx context.Context, filter KlijentFilter) ([]model.Klijent, error)
PrebrojiPoFilteru(ctx context.Context, filter KlijentFilter) (int, error)
DohvatiID(ctx context.Context, id int64) (*model.Klijent, error) DohvatiID(ctx context.Context, id int64) (*model.Klijent, error)
Kreiraj(ctx context.Context, k *model.Klijent) (int64, error) Kreiraj(ctx context.Context, k *model.Klijent) (int64, error)
Izmeni(ctx context.Context, k *model.Klijent) error Izmeni(ctx context.Context, k *model.Klijent) error
+42
View File
@@ -50,6 +50,15 @@ func (r *ArtikalRepo) Lista(ctx context.Context, filter db.ArtikalFilter) ([]mod
upit += " ORDER BY a.naziv ASC" upit += " ORDER BY a.naziv ASC"
if filter.Limit > 0 {
upit += " LIMIT ?"
args = append(args, filter.Limit)
if filter.Offset > 0 {
upit += " OFFSET ?"
args = append(args, filter.Offset)
}
}
redovi, err := r.db.QueryContext(ctx, upit, args...) redovi, err := r.db.QueryContext(ctx, upit, args...)
if err != nil { if err != nil {
return nil, fmt.Errorf("ntech: ArtikalRepo.Lista: %w", err) return nil, fmt.Errorf("ntech: ArtikalRepo.Lista: %w", err)
@@ -258,3 +267,36 @@ func (r *ArtikalRepo) KorigujKolicinu(ctx context.Context, artikalID int64, nova
return tx.Commit() return tx.Commit()
} }
// PrebrojiPoFilteru vraća ukupan broj artikala koji zadovoljavaju filter (bez LIMIT/OFFSET)
func (r *ArtikalRepo) PrebrojiPoFilteru(ctx context.Context, filter db.ArtikalFilter) (int, error) {
upit := `
SELECT COUNT(*)
FROM artikli a
LEFT JOIN kategorije k ON a.kategorija_id = k.id
WHERE 1=1`
args := []any{}
if filter.Pretraga != "" {
upit += " AND (a.naziv LIKE ? OR a.sifra LIKE ? OR a.barkod LIKE ? OR a.lokacija LIKE ? OR k.naziv LIKE ?)"
t := "%" + filter.Pretraga + "%"
args = append(args, t, t, t, t, t)
}
if filter.KategorijaID != nil {
upit += " AND a.kategorija_id = ?"
args = append(args, *filter.KategorijaID)
}
if filter.SamoKriticni {
upit += " AND a.kolicina <= a.kolicina_min"
}
var broj int
if err := r.db.QueryRowContext(ctx, upit, args...).Scan(&broj); err != nil {
return 0, fmt.Errorf("ntech: ArtikalRepo.PrebrojiPoFilteru: %w", err)
}
return broj, nil
}
+79 -3
View File
@@ -5,6 +5,7 @@ import (
"database/sql" "database/sql"
"fmt" "fmt"
"ntech/internal/db"
"ntech/internal/model" "ntech/internal/model"
) )
@@ -28,9 +29,9 @@ func (r *KlijentRepo) Lista(ctx context.Context, pretraga string) ([]model.Klije
args := []any{} args := []any{}
if pretraga != "" { if pretraga != "" {
upit += " AND (ime LIKE ? OR prezime LIKE ? OR naziv_firme LIKE ?)" upit += " AND (ime LIKE ? OR prezime LIKE ? OR (ime || ' ' || prezime) LIKE ? OR naziv_firme LIKE ? OR telefon LIKE ? OR email LIKE ?)"
p := "%" + pretraga + "%" p := "%" + pretraga + "%"
args = append(args, p, p, p) args = append(args, p, p, p, p, p, p)
} }
upit += " ORDER BY datum_unosa DESC" upit += " ORDER BY datum_unosa DESC"
@@ -138,6 +139,81 @@ func (r *KlijentRepo) Izmeni(ctx context.Context, k *model.Klijent) error {
return nil return nil
} }
// ListaFilter vraća listu klijenata sa limitom i offsetom (paginacija)
func (r *KlijentRepo) ListaFilter(ctx context.Context, filter db.KlijentFilter) ([]model.Klijent, error) {
upit := `
SELECT id, tip, ime, prezime, jmbg, naziv_firme, pib, telefon, email, mesto, napomena, datum_unosa
FROM klijenti
WHERE 1=1`
args := []any{}
if filter.Pretraga != "" {
upit += " AND (ime LIKE ? OR prezime LIKE ? OR (ime || ' ' || prezime) LIKE ? OR naziv_firme LIKE ? OR telefon LIKE ? OR email LIKE ?)"
p := "%" + filter.Pretraga + "%"
args = append(args, p, p, p, p, p, p)
}
upit += " ORDER BY datum_unosa DESC"
if filter.Limit > 0 {
upit += " LIMIT ?"
args = append(args, filter.Limit)
if filter.Offset > 0 {
upit += " OFFSET ?"
args = append(args, filter.Offset)
}
}
redovi, err := r.db.QueryContext(ctx, upit, args...)
if err != nil {
return nil, fmt.Errorf("ntech: KlijentRepo.ListaFilter: %w", err)
}
defer redovi.Close()
var rezultat []model.Klijent
for redovi.Next() {
var k model.Klijent
var ime, prezime, jmbg, nazivFirme, pib, telefon, email, mesto, napomena sql.NullString
err := redovi.Scan(
&k.ID, &k.Tip, &ime, &prezime, &jmbg, &nazivFirme, &pib, &telefon, &email, &mesto, &napomena, &k.DatumUnosa,
)
if err != nil {
return nil, fmt.Errorf("ntech: KlijentRepo.ListaFilter: 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
k.Email = email.String
k.Mesto = mesto.String
k.Napomena = napomena.String
rezultat = append(rezultat, k)
}
return rezultat, nil
}
// PrebrojiPoFilteru vraća broj klijenata koji zadovoljavaju filter
func (r *KlijentRepo) PrebrojiPoFilteru(ctx context.Context, filter db.KlijentFilter) (int, error) {
upit := `SELECT COUNT(*) FROM klijenti WHERE 1=1`
args := []any{}
if filter.Pretraga != "" {
upit += " AND (ime LIKE ? OR prezime LIKE ? OR (ime || ' ' || prezime) LIKE ? OR naziv_firme LIKE ? OR telefon LIKE ? OR email LIKE ?)"
p := "%" + filter.Pretraga + "%"
args = append(args, p, p, p, p, p, p)
}
var broj int
if err := r.db.QueryRowContext(ctx, upit, args...).Scan(&broj); err != nil {
return 0, fmt.Errorf("ntech: KlijentRepo.PrebrojiPoFilteru: %w", err)
}
return broj, nil
}
// Obrisi briše klijenta po ID-u // Obrisi briše klijenta po ID-u
func (r *KlijentRepo) Obrisi(ctx context.Context, id int64) error { func (r *KlijentRepo) Obrisi(ctx context.Context, id int64) error {
_, err := r.db.ExecContext(ctx, "DELETE FROM klijenti WHERE id = ?", id) _, err := r.db.ExecContext(ctx, "DELETE FROM klijenti WHERE id = ?", id)
+61 -11
View File
@@ -3,8 +3,10 @@ package handler
import ( import (
"log/slog" "log/slog"
"net/http" "net/http"
"strconv"
"strings" "strings"
"ntech/internal/db"
"ntech/internal/db/sqlite" "ntech/internal/db/sqlite"
"ntech/internal/model" "ntech/internal/model"
@@ -14,10 +16,16 @@ import (
// PodaciKlijenata su podaci za stranicu sa listom klijenata // PodaciKlijenata su podaci za stranicu sa listom klijenata
type PodaciKlijenata struct { type PodaciKlijenata struct {
model.PodaciStranice model.PodaciStranice
Klijenti []model.Klijent Klijenti []model.Klijent
Pretraga string Pretraga string
Sacuvano bool Sacuvano bool
Obrisan bool Obrisan bool
StranicaBr int
UkupnoStranica int
UkupnoKlijenata int
StranicaPrev int
StranicaNext int
StranicaQueryUrl string
} }
// PodaciFormeKlijenta su podaci za formu novog/izmenjenog klijenta // PodaciFormeKlijenta su podaci za formu novog/izmenjenog klijenta
@@ -28,7 +36,7 @@ type PodaciFormeKlijenta struct {
Izmena bool Izmena bool
} }
// Klijenti renderuje listu svih klijenata sa opcionom pretragom // Klijenti renderuje listu svih klijenata sa opcionom pretragom i paginacijom
func (h *Handler) Klijenti(w http.ResponseWriter, r *http.Request) { func (h *Handler) Klijenti(w http.ResponseWriter, r *http.Request) {
podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB) podesavanja, err := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
if err != nil { if err != nil {
@@ -38,21 +46,63 @@ func (h *Handler) Klijenti(w http.ResponseWriter, r *http.Request) {
pretraga := r.URL.Query().Get("pretraga") pretraga := r.URL.Query().Get("pretraga")
klijenti, err := h.KlijentiRepo.Lista(r.Context(), pretraga) const pageSize = 100
stranicaBr := 1
if p := r.URL.Query().Get("stranica"); p != "" {
if v, err := strconv.Atoi(p); err == nil && v > 0 {
stranicaBr = v
}
}
filter := db.KlijentFilter{
Pretraga: pretraga,
Limit: pageSize,
Offset: (stranicaBr - 1) * pageSize,
}
klijenti, err := h.KlijentiRepo.ListaFilter(r.Context(), filter)
if err != nil { if err != nil {
http.Error(w, "Greška pri učitavanju klijenata", http.StatusInternalServerError) http.Error(w, "Greška pri učitavanju klijenata", http.StatusInternalServerError)
return return
} }
ukupno, err := h.KlijentiRepo.PrebrojiPoFilteru(r.Context(), filter)
if err != nil {
http.Error(w, "Greška pri učitavanju klijenata", http.StatusInternalServerError)
return
}
ukupnoStranica := (ukupno + pageSize - 1) / pageSize
queryDelići := ""
if pretraga != "" {
queryDelići += "&pretraga=" + pretraga
}
stranicaPrev := stranicaBr - 1
if stranicaPrev < 1 {
stranicaPrev = 1
}
stranicaNext := stranicaBr + 1
if stranicaNext > ukupnoStranica {
stranicaNext = ukupnoStranica
}
ps := h.popuniPodaciStranice(r, podesavanja) ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "klijenti" ps.Stranica = "klijenti"
ps.NaslovStranice = "Klijenti" ps.NaslovStranice = "Klijenti"
podaci := PodaciKlijenata{ podaci := PodaciKlijenata{
PodaciStranice: ps, PodaciStranice: ps,
Klijenti: klijenti, Klijenti: klijenti,
Pretraga: pretraga, Pretraga: pretraga,
Sacuvano: r.URL.Query().Get("sacuvano") == "1", Sacuvano: r.URL.Query().Get("sacuvano") == "1",
Obrisan: r.URL.Query().Get("obrisan") == "1", Obrisan: r.URL.Query().Get("obrisan") == "1",
StranicaBr: stranicaBr,
UkupnoStranica: ukupnoStranica,
UkupnoKlijenata: ukupno,
StranicaPrev: stranicaPrev,
StranicaNext: stranicaNext,
StranicaQueryUrl: queryDelići,
} }
h.renderujTemplate(w, "klijenti", podaci) h.renderujTemplate(w, "klijenti", podaci)
+67 -15
View File
@@ -14,13 +14,19 @@ import (
// PodaciMagacina su podaci za stranicu magacina // PodaciMagacina su podaci za stranicu magacina
type PodaciMagacina struct { type PodaciMagacina struct {
model.PodaciStranice model.PodaciStranice
Artikli []model.ArtikalSaKategorijom Artikli []model.ArtikalSaKategorijom
Kategorije []model.Kategorija Kategorije []model.Kategorija
Filter db.ArtikalFilter Filter db.ArtikalFilter
KategorijaIDStr string KategorijaIDStr string
Sacuvano bool Sacuvano bool
Obrisan bool Obrisan bool
Premesten bool Premesten bool
StranicaBr int
UkupnoStranica int
UkupnoArtikala int
StranicaPrev int
StranicaNext int
StranicaQueryUrl string // čuva filtere za linkove paginacije
} }
// Magacin renderuje listu artikala // Magacin renderuje listu artikala
@@ -45,12 +51,30 @@ func (h *Handler) Magacin(w http.ResponseWriter, r *http.Request) {
} }
} }
const pageSize = 100
stranicaBr := 1
if p := r.URL.Query().Get("stranica"); p != "" {
if v, err := strconv.Atoi(p); err == nil && v > 0 {
stranicaBr = v
}
}
filter.Limit = pageSize
filter.Offset = (stranicaBr - 1) * pageSize
artikli, err := h.Artikli.Lista(r.Context(), filter) artikli, err := h.Artikli.Lista(r.Context(), filter)
if err != nil { if err != nil {
http.Error(w, "Greška pri učitavanju artikala", http.StatusInternalServerError) http.Error(w, "Greška pri učitavanju artikala", http.StatusInternalServerError)
return return
} }
ukupno, err := h.Artikli.PrebrojiPoFilteru(r.Context(), filter)
if err != nil {
http.Error(w, "Greška pri učitavanju artikala", http.StatusInternalServerError)
return
}
ukupnoStranica := (ukupno + pageSize - 1) / pageSize
kategorije, err := h.KategorijeRepo.Lista(r.Context()) kategorije, err := h.KategorijeRepo.Lista(r.Context())
if err != nil { if err != nil {
http.Error(w, "Greška pri učitavanju kategorija", http.StatusInternalServerError) http.Error(w, "Greška pri učitavanju kategorija", http.StatusInternalServerError)
@@ -60,15 +84,43 @@ func (h *Handler) Magacin(w http.ResponseWriter, r *http.Request) {
ps := h.popuniPodaciStranice(r, podesavanja) ps := h.popuniPodaciStranice(r, podesavanja)
ps.Stranica = "magacin" ps.Stranica = "magacin"
ps.NaslovStranice = "Magacin" ps.NaslovStranice = "Magacin"
// izgradi query string za paginaciju (čuva filtere)
queryDelići := ""
if v := filter.Pretraga; v != "" {
queryDelići += "&pretraga=" + v
}
if katIDStr != "" {
queryDelići += "&kategorija=" + katIDStr
}
if filter.SamoKriticni {
queryDelići += "&kriticni=1"
}
stranicaPrev := stranicaBr - 1
if stranicaPrev < 1 {
stranicaPrev = 1
}
stranicaNext := stranicaBr + 1
if stranicaNext > ukupnoStranica {
stranicaNext = ukupnoStranica
}
podaci := PodaciMagacina{ podaci := PodaciMagacina{
PodaciStranice: ps, PodaciStranice: ps,
Artikli: artikli, Artikli: artikli,
Kategorije: kategorije, Kategorije: kategorije,
Filter: filter, Filter: filter,
KategorijaIDStr: katIDStr, KategorijaIDStr: katIDStr,
Sacuvano: r.URL.Query().Get("sacuvano") == "1", Sacuvano: r.URL.Query().Get("sacuvano") == "1",
Obrisan: r.URL.Query().Get("obrisan") == "1", Obrisan: r.URL.Query().Get("obrisan") == "1",
Premesten: r.URL.Query().Get("premesten") == "1", Premesten: r.URL.Query().Get("premesten") == "1",
StranicaBr: stranicaBr,
UkupnoStranica: ukupnoStranica,
UkupnoArtikala: ukupno,
StranicaPrev: stranicaPrev,
StranicaNext: stranicaNext,
StranicaQueryUrl: queryDelići,
} }
h.renderujTemplate(w, "magacin", podaci) h.renderujTemplate(w, "magacin", podaci)
+28
View File
@@ -1109,6 +1109,29 @@ select {
.tabela tbody tr:nth-child(8) { animation-delay: 0.32s; } .tabela tbody tr:nth-child(8) { animation-delay: 0.32s; }
.tabela tbody tr:nth-child(9) { animation-delay: 0.36s; } .tabela tbody tr:nth-child(9) { animation-delay: 0.36s; }
.tabela tbody tr:nth-child(10) { animation-delay: 0.40s; } .tabela tbody tr:nth-child(10) { animation-delay: 0.40s; }
.tabela tbody tr:nth-child(11) { animation-delay: 0.44s; }
.tabela tbody tr:nth-child(12) { animation-delay: 0.48s; }
.tabela tbody tr:nth-child(13) { animation-delay: 0.52s; }
.tabela tbody tr:nth-child(14) { animation-delay: 0.56s; }
.tabela tbody tr:nth-child(15) { animation-delay: 0.60s; }
.tabela tbody tr:nth-child(16) { animation-delay: 0.64s; }
.tabela tbody tr:nth-child(17) { animation-delay: 0.68s; }
.tabela tbody tr:nth-child(18) { animation-delay: 0.72s; }
.tabela tbody tr:nth-child(19) { animation-delay: 0.76s; }
.tabela tbody tr:nth-child(20) { animation-delay: 0.80s; }
/* content-visibility: auto browser preskače render elemenata van viewport-a.
Ovo rešava problem sa 1000+ redova gde skrolovanje postaje prazno
dok browser ne stigne da iscrta sve. */
.tabela tbody tr {
content-visibility: auto;
contain-intrinsic-size: 48px;
}
[class*="-kartice"] > .animiraj,
.klijenti-kartice > .animiraj {
content-visibility: auto;
contain-intrinsic-size: 120px;
}
/* Stagger mobilnih lista-kartica (parnjak gornjeg za tabele). Pogađa kartice /* Stagger mobilnih lista-kartica (parnjak gornjeg za tabele). Pogađa kartice
unutar bilo kog .X-kartice kontejnera; ostali tipovi kartica (detalji/forma/ unutar bilo kog .X-kartice kontejnera; ostali tipovi kartica (detalji/forma/
@@ -1118,6 +1141,11 @@ select {
[class*="-kartice"] > .animiraj:nth-child(3) { animation-delay: 0.16s; } [class*="-kartice"] > .animiraj:nth-child(3) { animation-delay: 0.16s; }
[class*="-kartice"] > .animiraj:nth-child(4) { animation-delay: 0.22s; } [class*="-kartice"] > .animiraj:nth-child(4) { animation-delay: 0.22s; }
[class*="-kartice"] > .animiraj:nth-child(5) { animation-delay: 0.28s; } [class*="-kartice"] > .animiraj:nth-child(5) { animation-delay: 0.28s; }
[class*="-kartice"] > .animiraj:nth-child(6) { animation-delay: 0.34s; }
[class*="-kartice"] > .animiraj:nth-child(7) { animation-delay: 0.40s; }
[class*="-kartice"] > .animiraj:nth-child(8) { animation-delay: 0.46s; }
[class*="-kartice"] > .animiraj:nth-child(9) { animation-delay: 0.52s; }
[class*="-kartice"] > .animiraj:nth-child(10) { animation-delay: 0.58s; }
/* Stagger naslaganih .kartica.animiraj na stranicama podešavanja/profila JEDNO mesto. /* Stagger naslaganih .kartica.animiraj na stranicama podešavanja/profila JEDNO mesto.
Descendant selektor (razmak, ne >): nth-child se računa po neposrednom roditelju kao i Descendant selektor (razmak, ne >): nth-child se računa po neposrednom roditelju kao i
+25 -3
View File
@@ -12,19 +12,24 @@
<div class="poruka-uspeh poruka-animacija">Klijent je uspešno obrisan.</div> <div class="poruka-uspeh poruka-animacija">Klijent je uspešno obrisan.</div>
{{end}} {{end}}
<!-- gornja traka: dugme + pretraga --> <!-- gornja traka: dugme + interaktivna pretraga -->
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center;"> <div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center;">
<a href="/klijenti/novi" class="btn-primarno">+ Novi klijent</a> <a href="/klijenti/novi" class="btn-primarno">+ Novi klijent</a>
<form method="GET" action="/klijenti" style="display:flex;gap:8px;flex:1;min-width:200px;"> <form method="GET" action="/klijenti" style="display:flex;gap:8px;flex:1;min-width:200px;"
hx-get="/klijenti" hx-target="#klijenti-rezultati" hx-select="#klijenti-rezultati" hx-swap="innerHTML" hx-push-url="true">
<input type="text" name="pretraga" value="{{.Pretraga}}" <input type="text" name="pretraga" value="{{.Pretraga}}"
placeholder="Pretraži po imenu ili nazivu firme..." placeholder="Pretraži po imenu ili nazivu firme..."
style="flex:1;"> style="flex:1;"
hx-trigger="keyup changed delay:300ms, search"
hx-get="/klijenti" hx-target="#klijenti-rezultati" hx-select="#klijenti-rezultati" hx-swap="innerHTML" hx-push-url="true">
<button type="submit" class="btn-primarno"> <button type="submit" class="btn-primarno">
Traži Traži
</button> </button>
</form> </form>
</div> </div>
<div id="klijenti-rezultati">
<!-- desktop tabela --> <!-- desktop tabela -->
<div class="kartica klijenti-tabela kartica-tabela animiraj"> <div class="kartica klijenti-tabela kartica-tabela animiraj">
<div class="tabela-skrol"> <div class="tabela-skrol">
@@ -140,5 +145,22 @@
{{end}} {{end}}
</div> </div>
<!-- paginacija -->
{{if gt .UkupnoStranica 1}}
<div class="paginacija" style="display:flex;align-items:center;justify-content:center;gap:12px;padding:12px 0;flex-wrap:wrap;">
<span style="font-size:13px;color:var(--tekst-sporedni);">{{.UkupnoKlijenata}} klijenata — {{.StranicaBr}}/{{.UkupnoStranica}}</span>
<div style="display:flex;gap:4px;">
{{if gt .StranicaBr 1}}
<a href="?stranica={{.StranicaPrev}}{{.StranicaQueryUrl}}" class="btn-sekundarno-malo" hx-target="#klijenti-rezultati" hx-select="#klijenti-rezultati" hx-swap="innerHTML" hx-push-url="true">← Prethodna</a>
{{end}}
{{if lt .StranicaBr .UkupnoStranica}}
<a href="?stranica={{.StranicaNext}}{{.StranicaQueryUrl}}" class="btn-primarno-malo" hx-target="#klijenti-rezultati" hx-select="#klijenti-rezultati" hx-swap="innerHTML" hx-push-url="true">Sledeća →</a>
{{end}}
</div>
</div>
{{end}}
</div><!-- kraj #klijenti-rezultati -->
</div> </div>
{{end}} {{end}}
+32 -5
View File
@@ -25,20 +25,27 @@
{{end}} {{end}}
</div> </div>
<!-- pretraga i filteri --> <!-- pretraga i filteri — interaktivna pretraga (hx-trigger) -->
<form method="GET" action="/magacin" class="kolona" style="gap:10px;"> <form method="GET" action="/magacin" class="kolona" style="gap:10px;"
hx-get="/magacin" hx-target="#magacin-rezultati" hx-select="#magacin-rezultati" hx-swap="innerHTML" hx-push-url="true">
<input type="text" name="pretraga" value="{{.Filter.Pretraga}}" <input type="text" name="pretraga" value="{{.Filter.Pretraga}}"
placeholder="Pretraži artikle..." placeholder="Pretraži artikle..."
style="width:100%;"> style="width:100%;"
hx-trigger="keyup changed delay:300ms, search"
hx-get="/magacin" hx-target="#magacin-rezultati" hx-select="#magacin-rezultati" hx-swap="innerHTML" hx-push-url="true">
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center;"> <div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center;">
<select name="kategorija" style="flex:1;min-width:140px;"> <select name="kategorija" style="flex:1;min-width:140px;"
hx-trigger="change"
hx-get="/magacin" hx-target="#magacin-rezultati" hx-select="#magacin-rezultati" hx-swap="innerHTML" hx-push-url="true">
<option value="">Sve kategorije</option> <option value="">Sve kategorije</option>
{{range .Kategorije}} {{range .Kategorije}}
<option value="{{.ID}}" {{if eq (printf "%d" .ID) $.KategorijaIDStr}}selected{{end}}>{{.Naziv}}</option> <option value="{{.ID}}" {{if eq (printf "%d" .ID) $.KategorijaIDStr}}selected{{end}}>{{.Naziv}}</option>
{{end}} {{end}}
</select> </select>
<label style="display:inline-flex;align-items:center;gap:6px;font-size:13px;color:var(--tekst-sporedni);cursor:pointer;padding:8px 12px;border:0.5px solid var(--ivica);border-radius:8px;background:var(--kartica);white-space:nowrap;"> <label style="display:inline-flex;align-items:center;gap:6px;font-size:13px;color:var(--tekst-sporedni);cursor:pointer;padding:8px 12px;border:0.5px solid var(--ivica);border-radius:8px;background:var(--kartica);white-space:nowrap;">
<input type="checkbox" name="kriticni" value="1" {{if .Filter.SamoKriticni}}checked{{end}}> <input type="checkbox" name="kriticni" value="1" {{if .Filter.SamoKriticni}}checked{{end}}
hx-trigger="change"
hx-get="/magacin" hx-target="#magacin-rezultati" hx-select="#magacin-rezultati" hx-swap="innerHTML" hx-push-url="true">
Samo kritični Samo kritični
</label> </label>
<button type="submit" class="btn-primarno"> <button type="submit" class="btn-primarno">
@@ -47,6 +54,9 @@
</div> </div>
</form> </form>
<!-- rezultati pretrage — HTMX zamenjuje samo ovo, polje za pretragu ostaje u fokusu -->
<div id="magacin-rezultati">
<!-- tabela artikala --> <!-- tabela artikala -->
<div class="kartica animiraj" style="padding:0;overflow:hidden;"> <div class="kartica animiraj" style="padding:0;overflow:hidden;">
<div style="overflow-x:auto;"> <div style="overflow-x:auto;">
@@ -162,6 +172,23 @@
{{end}} {{end}}
</div> </div>
<!-- paginacija -->
{{if gt .UkupnoStranica 1}}
<div class="paginacija" style="display:flex;align-items:center;justify-content:center;gap:12px;padding:12px 0;flex-wrap:wrap;">
<span style="font-size:13px;color:var(--tekst-sporedni);">{{.UkupnoArtikala}} artikala — {{.StranicaBr}}/{{.UkupnoStranica}}</span>
<div style="display:flex;gap:4px;">
{{if gt .StranicaBr 1}}
<a href="?stranica={{.StranicaPrev}}{{.StranicaQueryUrl}}" class="btn-sekundarno-malo" hx-target="#magacin-rezultati" hx-select="#magacin-rezultati" hx-swap="innerHTML" hx-push-url="true">← Prethodna</a>
{{end}}
{{if lt .StranicaBr .UkupnoStranica}}
<a href="?stranica={{.StranicaNext}}{{.StranicaQueryUrl}}" class="btn-primarno-malo" hx-target="#magacin-rezultati" hx-select="#magacin-rezultati" hx-swap="innerHTML" hx-push-url="true">Sledeća →</a>
{{end}}
</div>
</div>
{{end}}
</div><!-- kraj #magacin-rezultati -->
</div> </div>
{{end}} {{end}}