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:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}}
|
||||||
|
|||||||
@@ -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}}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user