prompt 7
This commit is contained in:
parent
036aadb09a
commit
c60ff109a4
|
|
@ -0,0 +1,22 @@
|
||||||
|
Implementa modulo “users” sotto /web/templates/private/users.
|
||||||
|
|
||||||
|
Routes protette (RequireAuth):
|
||||||
|
- GET /users -> pagina con search + container tabella
|
||||||
|
- GET /users/table -> partial HTML tabella (htmx)
|
||||||
|
- GET /users/:id/modal -> partial HTML contenuto modal
|
||||||
|
|
||||||
|
Requisiti tabella:
|
||||||
|
- query params: q, sort (id|name|email whitelist), dir (asc|desc), page, pageSize
|
||||||
|
- server-driven paging/sort/search usando GORM (Count + Limit/Offset + Order)
|
||||||
|
- _table.html deve includere:
|
||||||
|
- header th cliccabili con hx-get (toggle dir)
|
||||||
|
- pager prev/next con hx-get
|
||||||
|
- bottone “Apri” che hx-get sul modal e hx-target="#userModal" hx-swap="innerHTML"
|
||||||
|
- apri modal via JS minimal: setAttribute('open','') dopo swap (o onclick)
|
||||||
|
|
||||||
|
Crea template:
|
||||||
|
- private/users/index.html
|
||||||
|
- private/users/_table.html
|
||||||
|
- private/users/_modal.html
|
||||||
|
|
||||||
|
Integra <ui-modal id="userModal"> nella index privata.
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
httpmw "trustcontact/internal/http/middleware"
|
||||||
|
"trustcontact/internal/services"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UsersController struct {
|
||||||
|
usersService *services.UsersService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUsersController(usersService *services.UsersService) *UsersController {
|
||||||
|
return &UsersController{usersService: usersService}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (uc *UsersController) Index(c *fiber.Ctx) error {
|
||||||
|
pageData, err := uc.queryPage(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
viewData := map[string]any{
|
||||||
|
"Title": "Users",
|
||||||
|
"NavSection": "private",
|
||||||
|
"PageData": pageData,
|
||||||
|
}
|
||||||
|
for k, v := range localsTemplateData(c) {
|
||||||
|
viewData[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpl, err := template.ParseFiles(
|
||||||
|
"web/templates/layout.html",
|
||||||
|
"web/templates/public/_flash.html",
|
||||||
|
"web/templates/private/users/index.html",
|
||||||
|
"web/templates/private/users/_table.html",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return executeLayout(c, tmpl, viewData)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (uc *UsersController) Table(c *fiber.Ctx) error {
|
||||||
|
pageData, err := uc.queryPage(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
viewData := map[string]any{"PageData": pageData}
|
||||||
|
tmpl, err := template.ParseFiles("web/templates/private/users/_table.html")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Type("html", "utf-8")
|
||||||
|
return tmpl.ExecuteTemplate(c.Response().BodyWriter(), "users_table", viewData)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (uc *UsersController) Modal(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||||
|
if err != nil || id == 0 {
|
||||||
|
return c.Status(fiber.StatusBadRequest).SendString("invalid user id")
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := uc.usersService.GetByID(uint(id))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if user == nil {
|
||||||
|
return c.Status(fiber.StatusNotFound).SendString("user not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
viewData := map[string]any{"User": user}
|
||||||
|
tmpl, err := template.ParseFiles("web/templates/private/users/_modal.html")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Type("html", "utf-8")
|
||||||
|
return tmpl.ExecuteTemplate(c.Response().BodyWriter(), "users_modal", viewData)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (uc *UsersController) queryPage(c *fiber.Ctx) (*services.UsersPage, error) {
|
||||||
|
page := parseIntOrDefault(c.Query("page"), 1)
|
||||||
|
pageSize := parseIntOrDefault(c.Query("pageSize"), 10)
|
||||||
|
q := strings.TrimSpace(c.Query("q"))
|
||||||
|
sort := c.Query("sort", "id")
|
||||||
|
dir := c.Query("dir", "asc")
|
||||||
|
|
||||||
|
pageData, err := uc.usersService.List(services.UsersQuery{
|
||||||
|
Q: q,
|
||||||
|
Sort: sort,
|
||||||
|
Dir: dir,
|
||||||
|
Page: page,
|
||||||
|
PageSize: pageSize,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("list users: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pageData, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseIntOrDefault(value string, fallback int) int {
|
||||||
|
parsed, err := strconv.Atoi(value)
|
||||||
|
if err != nil || parsed <= 0 {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
func executeLayout(c *fiber.Ctx, tmpl *template.Template, viewData map[string]any) error {
|
||||||
|
httpmw.SetTemplateData(c, "NavSection", viewData["NavSection"])
|
||||||
|
c.Type("html", "utf-8")
|
||||||
|
return tmpl.ExecuteTemplate(c.Response().BodyWriter(), filepath.Base("web/templates/layout.html"), viewData)
|
||||||
|
}
|
||||||
|
|
@ -18,30 +18,35 @@ func Seed(database *gorm.DB) error {
|
||||||
|
|
||||||
seedUsers := []models.User{
|
seedUsers := []models.User{
|
||||||
{
|
{
|
||||||
|
Name: "Admin User",
|
||||||
Email: "admin@example.com",
|
Email: "admin@example.com",
|
||||||
Role: models.RoleAdmin,
|
Role: models.RoleAdmin,
|
||||||
EmailVerified: true,
|
EmailVerified: true,
|
||||||
PasswordHash: passwordHash,
|
PasswordHash: passwordHash,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
Name: "Normal User",
|
||||||
Email: "user@example.com",
|
Email: "user@example.com",
|
||||||
Role: models.RoleUser,
|
Role: models.RoleUser,
|
||||||
EmailVerified: true,
|
EmailVerified: true,
|
||||||
PasswordHash: passwordHash,
|
PasswordHash: passwordHash,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
Name: "Demo One",
|
||||||
Email: "demo1@example.com",
|
Email: "demo1@example.com",
|
||||||
Role: models.RoleUser,
|
Role: models.RoleUser,
|
||||||
EmailVerified: true,
|
EmailVerified: true,
|
||||||
PasswordHash: passwordHash,
|
PasswordHash: passwordHash,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
Name: "Demo Two",
|
||||||
Email: "demo2@example.com",
|
Email: "demo2@example.com",
|
||||||
Role: models.RoleUser,
|
Role: models.RoleUser,
|
||||||
EmailVerified: true,
|
EmailVerified: true,
|
||||||
PasswordHash: passwordHash,
|
PasswordHash: passwordHash,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
Name: "Demo Three",
|
||||||
Email: "demo3@example.com",
|
Email: "demo3@example.com",
|
||||||
Role: models.RoleUser,
|
Role: models.RoleUser,
|
||||||
EmailVerified: true,
|
EmailVerified: true,
|
||||||
|
|
@ -62,6 +67,7 @@ func upsertUser(database *gorm.DB, user models.User) error {
|
||||||
result := database.Clauses(clause.OnConflict{
|
result := database.Clauses(clause.OnConflict{
|
||||||
Columns: []clause.Column{{Name: "email"}},
|
Columns: []clause.Column{{Name: "email"}},
|
||||||
DoUpdates: clause.AssignmentColumns([]string{
|
DoUpdates: clause.AssignmentColumns([]string{
|
||||||
|
"name",
|
||||||
"role",
|
"role",
|
||||||
"email_verified",
|
"email_verified",
|
||||||
"password_hash",
|
"password_hash",
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ func RegisterRoutes(app *fiber.App, store *session.Store, database *gorm.DB, cfg
|
||||||
return fmt.Errorf("init auth service: %w", err)
|
return fmt.Errorf("init auth service: %w", err)
|
||||||
}
|
}
|
||||||
authController := controllers.NewAuthController(authService)
|
authController := controllers.NewAuthController(authService)
|
||||||
|
usersController := controllers.NewUsersController(services.NewUsersService(database))
|
||||||
|
|
||||||
app.Get("/healthz", func(c *fiber.Ctx) error {
|
app.Get("/healthz", func(c *fiber.Ctx) error {
|
||||||
return c.SendStatus(fiber.StatusOK)
|
return c.SendStatus(fiber.StatusOK)
|
||||||
|
|
@ -39,10 +40,13 @@ func RegisterRoutes(app *fiber.App, store *session.Store, database *gorm.DB, cfg
|
||||||
app.Post("/forgot-password", authController.ForgotPassword)
|
app.Post("/forgot-password", authController.ForgotPassword)
|
||||||
app.Get("/reset-password", authController.ShowResetPassword)
|
app.Get("/reset-password", authController.ShowResetPassword)
|
||||||
app.Post("/reset-password", authController.ResetPassword)
|
app.Post("/reset-password", authController.ResetPassword)
|
||||||
|
app.Get("/users", httpmw.RequireAuth(), usersController.Index)
|
||||||
|
app.Get("/users/table", httpmw.RequireAuth(), usersController.Table)
|
||||||
|
app.Get("/users/:id/modal", httpmw.RequireAuth(), usersController.Modal)
|
||||||
|
|
||||||
private := app.Group("/private", httpmw.RequireAuth())
|
private := app.Group("/private", httpmw.RequireAuth())
|
||||||
private.Get("/", func(c *fiber.Ctx) error {
|
private.Get("/", func(c *fiber.Ctx) error {
|
||||||
return c.SendString("private area")
|
return c.Redirect("/users")
|
||||||
})
|
})
|
||||||
|
|
||||||
admin := app.Group("/admin", httpmw.RequireAuth(), httpmw.RequireAdmin())
|
admin := app.Group("/admin", httpmw.RequireAuth(), httpmw.RequireAdmin())
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ const (
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
ID uint `gorm:"primaryKey"`
|
ID uint `gorm:"primaryKey"`
|
||||||
|
Name string `gorm:"size:120;index"`
|
||||||
Email string `gorm:"size:320;uniqueIndex;not null"`
|
Email string `gorm:"size:320;uniqueIndex;not null"`
|
||||||
PasswordHash string `gorm:"size:255;not null"`
|
PasswordHash string `gorm:"size:255;not null"`
|
||||||
EmailVerified bool `gorm:"not null;default:false"`
|
EmailVerified bool `gorm:"not null;default:false"`
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ package repo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"trustcontact/internal/models"
|
"trustcontact/internal/models"
|
||||||
|
|
||||||
|
|
@ -12,6 +14,14 @@ type UserRepo struct {
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UserListParams struct {
|
||||||
|
Query string
|
||||||
|
Sort string
|
||||||
|
Dir string
|
||||||
|
Page int
|
||||||
|
PageSize int
|
||||||
|
}
|
||||||
|
|
||||||
func NewUserRepo(db *gorm.DB) *UserRepo {
|
func NewUserRepo(db *gorm.DB) *UserRepo {
|
||||||
return &UserRepo{db: db}
|
return &UserRepo{db: db}
|
||||||
}
|
}
|
||||||
|
|
@ -55,3 +65,65 @@ func (r *UserRepo) UpdatePasswordHash(userID uint, passwordHash string) error {
|
||||||
Where("id = ?", userID).
|
Where("id = ?", userID).
|
||||||
Update("password_hash", passwordHash).Error
|
Update("password_hash", passwordHash).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *UserRepo) List(params UserListParams) ([]models.User, int64, error) {
|
||||||
|
query := r.db.Model(&models.User{})
|
||||||
|
|
||||||
|
search := strings.TrimSpace(params.Query)
|
||||||
|
if search != "" {
|
||||||
|
like := "%" + strings.ToLower(search) + "%"
|
||||||
|
query = query.Where("LOWER(name) LIKE ? OR LOWER(email) LIKE ?", like, like)
|
||||||
|
}
|
||||||
|
|
||||||
|
var total int64
|
||||||
|
if err := query.Count(&total).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
orderBy := sanitizeSort(params.Sort)
|
||||||
|
orderDir := sanitizeDir(params.Dir)
|
||||||
|
orderClause := fmt.Sprintf("%s %s", orderBy, orderDir)
|
||||||
|
|
||||||
|
page := params.Page
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
pageSize := params.PageSize
|
||||||
|
if pageSize <= 0 {
|
||||||
|
pageSize = 10
|
||||||
|
}
|
||||||
|
if pageSize > 100 {
|
||||||
|
pageSize = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := (page - 1) * pageSize
|
||||||
|
|
||||||
|
var users []models.User
|
||||||
|
if err := query.Order(orderClause).Limit(pageSize).Offset(offset).Find(&users).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return users, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sanitizeSort(sort string) string {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(sort)) {
|
||||||
|
case "id":
|
||||||
|
return "id"
|
||||||
|
case "name":
|
||||||
|
return "name"
|
||||||
|
case "email":
|
||||||
|
return "email"
|
||||||
|
default:
|
||||||
|
return "id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sanitizeDir(dir string) string {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(dir)) {
|
||||||
|
case "desc":
|
||||||
|
return "desc"
|
||||||
|
default:
|
||||||
|
return "asc"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,132 @@
|
||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"trustcontact/internal/models"
|
||||||
|
"trustcontact/internal/repo"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UsersService struct {
|
||||||
|
users *repo.UserRepo
|
||||||
|
}
|
||||||
|
|
||||||
|
type UsersQuery struct {
|
||||||
|
Q string
|
||||||
|
Sort string
|
||||||
|
Dir string
|
||||||
|
Page int
|
||||||
|
PageSize int
|
||||||
|
}
|
||||||
|
|
||||||
|
type UsersPage struct {
|
||||||
|
Users []models.User
|
||||||
|
Total int64
|
||||||
|
Page int
|
||||||
|
PageSize int
|
||||||
|
TotalPages int
|
||||||
|
HasPrev bool
|
||||||
|
HasNext bool
|
||||||
|
PrevPage int
|
||||||
|
NextPage int
|
||||||
|
Sort string
|
||||||
|
Dir string
|
||||||
|
Q string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUsersService(database *gorm.DB) *UsersService {
|
||||||
|
return &UsersService{users: repo.NewUserRepo(database)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UsersService) List(query UsersQuery) (*UsersPage, error) {
|
||||||
|
page := query.Page
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
pageSize := query.PageSize
|
||||||
|
if pageSize <= 0 {
|
||||||
|
pageSize = 10
|
||||||
|
}
|
||||||
|
if pageSize > 100 {
|
||||||
|
pageSize = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
sort := normalizeSort(query.Sort)
|
||||||
|
dir := normalizeDir(query.Dir)
|
||||||
|
|
||||||
|
users, total, err := s.users.List(repo.UserListParams{
|
||||||
|
Query: strings.TrimSpace(query.Q),
|
||||||
|
Sort: sort,
|
||||||
|
Dir: dir,
|
||||||
|
Page: page,
|
||||||
|
PageSize: pageSize,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
totalPages := 0
|
||||||
|
if total > 0 {
|
||||||
|
totalPages = int((total + int64(pageSize) - 1) / int64(pageSize))
|
||||||
|
}
|
||||||
|
if totalPages > 0 && page > totalPages {
|
||||||
|
page = totalPages
|
||||||
|
users, total, err = s.users.List(repo.UserListParams{
|
||||||
|
Query: strings.TrimSpace(query.Q),
|
||||||
|
Sort: sort,
|
||||||
|
Dir: dir,
|
||||||
|
Page: page,
|
||||||
|
PageSize: pageSize,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hasPrev := page > 1
|
||||||
|
hasNext := totalPages > 0 && page < totalPages
|
||||||
|
|
||||||
|
return &UsersPage{
|
||||||
|
Users: users,
|
||||||
|
Total: total,
|
||||||
|
Page: page,
|
||||||
|
PageSize: pageSize,
|
||||||
|
TotalPages: totalPages,
|
||||||
|
HasPrev: hasPrev,
|
||||||
|
HasNext: hasNext,
|
||||||
|
PrevPage: max(1, page-1),
|
||||||
|
NextPage: page + 1,
|
||||||
|
Sort: sort,
|
||||||
|
Dir: dir,
|
||||||
|
Q: strings.TrimSpace(query.Q),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UsersService) GetByID(id uint) (*models.User, error) {
|
||||||
|
return s.users.FindByID(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeSort(sort string) string {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(sort)) {
|
||||||
|
case "id", "name", "email":
|
||||||
|
return strings.ToLower(strings.TrimSpace(sort))
|
||||||
|
default:
|
||||||
|
return "id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeDir(dir string) string {
|
||||||
|
if strings.ToLower(strings.TrimSpace(dir)) == "desc" {
|
||||||
|
return "desc"
|
||||||
|
}
|
||||||
|
return "asc"
|
||||||
|
}
|
||||||
|
|
||||||
|
func max(a, b int) int {
|
||||||
|
if a > b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
.muted { color: #6b7280; font-size: 0.95rem; }
|
.muted { color: #6b7280; font-size: 0.95rem; }
|
||||||
.row { display: flex; gap: 10px; flex-wrap: wrap; }
|
.row { display: flex; gap: 10px; flex-wrap: wrap; }
|
||||||
</style>
|
</style>
|
||||||
|
<script src="https://unpkg.com/htmx.org@1.9.12"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<nav>
|
<nav>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
{{define "users_modal"}}
|
||||||
|
<div style="padding:16px;">
|
||||||
|
<h3 style="margin-top:0;">Dettaglio utente #{{.User.ID}}</h3>
|
||||||
|
<p><strong>Name:</strong> {{if .User.Name}}{{.User.Name}}{{else}}-{{end}}</p>
|
||||||
|
<p><strong>Email:</strong> {{.User.Email}}</p>
|
||||||
|
<p><strong>Role:</strong> {{.User.Role}}</p>
|
||||||
|
<p><strong>Verified:</strong> {{if .User.EmailVerified}}yes{{else}}no{{end}}</p>
|
||||||
|
<p><strong>Created:</strong> {{.User.CreatedAt}}</p>
|
||||||
|
<div class="row">
|
||||||
|
<button type="button" onclick="document.getElementById('userModal').removeAttribute('open')">Chiudi</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
{{define "users_table"}}
|
||||||
|
{{ $p := .PageData }}
|
||||||
|
<table style="width:100%;border-collapse:collapse;margin-top:16px;">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="text-align:left;border-bottom:1px solid #e5e7eb;padding:8px;">
|
||||||
|
<a href="#" hx-get="/users/table?q={{$p.Q}}&sort=id&dir={{if and (eq $p.Sort "id") (eq $p.Dir "asc")}}desc{{else}}asc{{end}}&page=1&pageSize={{$p.PageSize}}" hx-target="#usersTableContainer" hx-swap="innerHTML">ID</a>
|
||||||
|
</th>
|
||||||
|
<th style="text-align:left;border-bottom:1px solid #e5e7eb;padding:8px;">
|
||||||
|
<a href="#" hx-get="/users/table?q={{$p.Q}}&sort=name&dir={{if and (eq $p.Sort "name") (eq $p.Dir "asc")}}desc{{else}}asc{{end}}&page=1&pageSize={{$p.PageSize}}" hx-target="#usersTableContainer" hx-swap="innerHTML">Name</a>
|
||||||
|
</th>
|
||||||
|
<th style="text-align:left;border-bottom:1px solid #e5e7eb;padding:8px;">
|
||||||
|
<a href="#" hx-get="/users/table?q={{$p.Q}}&sort=email&dir={{if and (eq $p.Sort "email") (eq $p.Dir "asc")}}desc{{else}}asc{{end}}&page=1&pageSize={{$p.PageSize}}" hx-target="#usersTableContainer" hx-swap="innerHTML">Email</a>
|
||||||
|
</th>
|
||||||
|
<th style="text-align:left;border-bottom:1px solid #e5e7eb;padding:8px;">Role</th>
|
||||||
|
<th style="text-align:left;border-bottom:1px solid #e5e7eb;padding:8px;">Azioni</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{range $u := $p.Users}}
|
||||||
|
<tr>
|
||||||
|
<td style="border-bottom:1px solid #f1f5f9;padding:8px;">{{$u.ID}}</td>
|
||||||
|
<td style="border-bottom:1px solid #f1f5f9;padding:8px;">{{if $u.Name}}{{$u.Name}}{{else}}-{{end}}</td>
|
||||||
|
<td style="border-bottom:1px solid #f1f5f9;padding:8px;">{{$u.Email}}</td>
|
||||||
|
<td style="border-bottom:1px solid #f1f5f9;padding:8px;">{{$u.Role}}</td>
|
||||||
|
<td style="border-bottom:1px solid #f1f5f9;padding:8px;">
|
||||||
|
<button
|
||||||
|
hx-get="/users/{{$u.ID}}/modal"
|
||||||
|
hx-target="#userModal"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-on::after-request="document.getElementById('userModal').setAttribute('open','')"
|
||||||
|
>Apri</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{else}}
|
||||||
|
<tr><td colspan="5" style="padding:12px;">Nessun utente trovato.</td></tr>
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="row" style="margin-top:12px;align-items:center;justify-content:space-between;">
|
||||||
|
<div class="muted">Totale: {{$p.Total}} utenti. Pagina {{$p.Page}}{{if gt $p.TotalPages 0}} / {{$p.TotalPages}}{{end}}</div>
|
||||||
|
<div class="row">
|
||||||
|
<button {{if not $p.HasPrev}}disabled{{end}} hx-get="/users/table?q={{$p.Q}}&sort={{$p.Sort}}&dir={{$p.Dir}}&page={{$p.PrevPage}}&pageSize={{$p.PageSize}}" hx-target="#usersTableContainer" hx-swap="innerHTML">Prev</button>
|
||||||
|
<button {{if not $p.HasNext}}disabled{{end}} hx-get="/users/table?q={{$p.Q}}&sort={{$p.Sort}}&dir={{$p.Dir}}&page={{$p.NextPage}}&pageSize={{$p.PageSize}}" hx-target="#usersTableContainer" hx-swap="innerHTML">Next</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
{{define "content"}}
|
||||||
|
<h1>Users</h1>
|
||||||
|
<p class="muted">Ricerca, ordinamento e paging server-side via HTMX.</p>
|
||||||
|
|
||||||
|
<form id="usersFilters" class="row" hx-get="/users/table" hx-target="#usersTableContainer" hx-swap="innerHTML">
|
||||||
|
<input type="text" name="q" placeholder="Cerca nome o email" value="{{.PageData.Q}}">
|
||||||
|
<input type="number" name="pageSize" min="1" max="100" value="{{.PageData.PageSize}}" style="max-width:120px;">
|
||||||
|
<input type="hidden" name="sort" value="{{.PageData.Sort}}">
|
||||||
|
<input type="hidden" name="dir" value="{{.PageData.Dir}}">
|
||||||
|
<input type="hidden" name="page" value="1">
|
||||||
|
<button type="submit">Cerca</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div id="usersTableContainer" hx-get="/users/table?q={{.PageData.Q}}&sort={{.PageData.Sort}}&dir={{.PageData.Dir}}&page={{.PageData.Page}}&pageSize={{.PageData.PageSize}}" hx-trigger="load" hx-swap="innerHTML">
|
||||||
|
{{template "users_table" .}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ui-modal id="userModal"></ui-modal>
|
||||||
|
{{end}}
|
||||||
Loading…
Reference in New Issue