Merge new search (members + contacts) into main branch #3
8 changed files with 820 additions and 448 deletions
|
|
@ -1,17 +1,18 @@
|
||||||
package controllers
|
package controllers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.readonly.ch/bouzoure/pop-camarades/helpers"
|
"git.readonly.ch/bouzoure/pop-camarades/helpers"
|
||||||
|
"git.readonly.ch/bouzoure/pop-camarades/helpers/database"
|
||||||
"git.readonly.ch/bouzoure/pop-camarades/models"
|
"git.readonly.ch/bouzoure/pop-camarades/models"
|
||||||
|
"github.com/charmbracelet/log"
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Contacts(c *fiber.Ctx) error {
|
func Contacts(c *fiber.Ctx) error {
|
||||||
|
|
@ -46,131 +47,53 @@ func Contacts(c *fiber.Ctx) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
filterSection := c.Query("se")
|
searchJSON := c.Query("s")
|
||||||
filterArchive := c.Query("a")
|
var params database.PeopleSearchParams
|
||||||
filterSearch := c.Query("s")
|
|
||||||
|
|
||||||
sqlFilterSections := allowedSections
|
if len(searchJSON) > 0 {
|
||||||
sqlFilterAppend := "IS NULL"
|
err = json.Unmarshal([]byte(searchJSON), ¶ms)
|
||||||
if filterArchive == "1" {
|
if err != nil {
|
||||||
sqlFilterAppend = "IS NOT NULL"
|
log.Warn(err)
|
||||||
sqlFilterSections = allowedSectionsArchived
|
searchJSON = ""
|
||||||
}
|
|
||||||
|
|
||||||
var filterSectionUint uint
|
|
||||||
if len(filterSection) > 0 {
|
|
||||||
filterSectionUint64, err := strconv.ParseUint(filterSection, 10, 0)
|
|
||||||
if err == nil {
|
|
||||||
for _, s := range sqlFilterSections {
|
|
||||||
if s == uint(filterSectionUint64) {
|
|
||||||
filterSectionUint = uint(filterSectionUint64)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if filterSectionUint > 0 {
|
params.PageNumber, _ = strconv.Atoi(c.Query("p"))
|
||||||
sqlFilterAppend = fmt.Sprintf(
|
params.PageSize = 50
|
||||||
"%s AND section_id = %d",
|
params.PersonType = "contacts"
|
||||||
sqlFilterAppend,
|
|
||||||
filterSectionUint,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
var sqlFilterSearch string
|
|
||||||
if len(filterSearch) > 0 {
|
|
||||||
sqlFilterSearch = fmt.Sprintf(
|
|
||||||
"%%%s%%", filterSearch,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
var count int64
|
|
||||||
if len(filterSearch) > 0 {
|
|
||||||
db.Model(&models.Person{}).Where(
|
|
||||||
fmt.Sprintf(
|
|
||||||
"is_contact = ? AND section_id IN ? AND (first_name LIKE ? OR last_name LIKE ?) AND deleted_at %s",
|
|
||||||
sqlFilterAppend,
|
|
||||||
),
|
|
||||||
true, allowedSections, sqlFilterSearch, sqlFilterSearch,
|
|
||||||
).Count(&count)
|
|
||||||
} else {
|
|
||||||
db.Model(&models.Person{}).Where(
|
|
||||||
fmt.Sprintf(
|
|
||||||
"is_contact = ? AND section_id IN ? AND deleted_at %s",
|
|
||||||
sqlFilterAppend,
|
|
||||||
),
|
|
||||||
true, allowedSections,
|
|
||||||
).Count(&count)
|
|
||||||
}
|
|
||||||
|
|
||||||
page, _ := strconv.Atoi(c.Query("p"))
|
|
||||||
pagination := helpers.Paginate(50, int(count), page)
|
|
||||||
|
|
||||||
var people []models.Person
|
|
||||||
if len(filterSearch) > 0 {
|
|
||||||
result := db.Unscoped().Offset(
|
|
||||||
pagination.Offset,
|
|
||||||
).Limit(
|
|
||||||
pagination.PageSize,
|
|
||||||
).Order(
|
|
||||||
"concat(last_name, first_name) collate nocase asc",
|
|
||||||
).Preload(
|
|
||||||
"Section",
|
|
||||||
).Find(
|
|
||||||
&people,
|
|
||||||
fmt.Sprintf(
|
|
||||||
"is_contact = ? AND section_id IN ? AND (first_name LIKE ? OR last_name LIKE ?) AND deleted_at %s",
|
|
||||||
sqlFilterAppend,
|
|
||||||
),
|
|
||||||
true, sqlFilterSections, sqlFilterSearch, sqlFilterSearch,
|
|
||||||
)
|
|
||||||
|
|
||||||
if result.Error != nil && !errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
result := db.Unscoped().Offset(
|
|
||||||
pagination.Offset,
|
|
||||||
).Limit(
|
|
||||||
pagination.PageSize,
|
|
||||||
).Order(
|
|
||||||
"concat(last_name, first_name) collate nocase asc",
|
|
||||||
).Preload(
|
|
||||||
"Section",
|
|
||||||
).Find(
|
|
||||||
&people,
|
|
||||||
fmt.Sprintf(
|
|
||||||
"is_contact = ? AND section_id IN ? AND deleted_at %s",
|
|
||||||
sqlFilterAppend,
|
|
||||||
),
|
|
||||||
true, sqlFilterSections,
|
|
||||||
)
|
|
||||||
|
|
||||||
if result.Error != nil && !errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var sections []models.Section
|
var sections []models.Section
|
||||||
db.Order(
|
db.Order("name collate nocase asc").Find(§ions, "contains_contacts = ? AND id IN ?", true, allowedSections)
|
||||||
"name collate nocase asc",
|
params.AllowedSections = allowedSections
|
||||||
).Find(
|
|
||||||
§ions,
|
// Security for active contacts
|
||||||
"contains_contacts = ? AND id IN ?",
|
if !permShow {
|
||||||
true, sqlFilterSections,
|
params.Active = false
|
||||||
)
|
}
|
||||||
|
|
||||||
|
// Security for archived contacts
|
||||||
|
if !permShowArchived {
|
||||||
|
params.Archive = false
|
||||||
|
}
|
||||||
|
|
||||||
|
results, err := database.PeopleSearch(params)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var fields []models.Field
|
||||||
|
db.Order("position asc").Find(&fields, "person_type = ?", "contact")
|
||||||
|
|
||||||
return c.Render("people", fiber.Map{
|
return c.Render("people", fiber.Map{
|
||||||
"PageTitle": "Contacts",
|
"PageTitle": "Contacts",
|
||||||
"MembersPage": false,
|
"MembersPage": false,
|
||||||
"People": people,
|
"People": results.Results,
|
||||||
"Pagination": pagination,
|
"Pagination": results.Pagination,
|
||||||
"PermShow": permShow,
|
"PermShow": permShow,
|
||||||
"PermShowArchived": permShowArchived,
|
"PermShowArchived": permShowArchived,
|
||||||
|
"SearchJSON": searchJSON,
|
||||||
"Sections": sections,
|
"Sections": sections,
|
||||||
"FilterArchive": filterArchive,
|
"Fields": fields,
|
||||||
"FilterSection": filterSectionUint,
|
|
||||||
"FilterSearch": filterSearch,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -206,76 +129,38 @@ func ContactsExport(c *fiber.Ctx) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
filterSection := c.Query("se")
|
searchJSON := c.Query("s")
|
||||||
filterArchive := c.Query("a")
|
var params database.PeopleSearchParams
|
||||||
filterSearch := c.Query("s")
|
|
||||||
|
|
||||||
sqlFilterSections := allowedSections
|
if len(searchJSON) > 0 {
|
||||||
sqlFilterAppend := "IS NULL"
|
err = json.Unmarshal([]byte(searchJSON), ¶ms)
|
||||||
if filterArchive == "1" {
|
if err != nil {
|
||||||
sqlFilterAppend = "IS NOT NULL"
|
log.Warn(err)
|
||||||
sqlFilterSections = allowedSectionsArchived
|
searchJSON = ""
|
||||||
}
|
|
||||||
|
|
||||||
var filterSectionUint uint
|
|
||||||
if len(filterSection) > 0 {
|
|
||||||
filterSectionUint64, err := strconv.ParseUint(filterSection, 10, 0)
|
|
||||||
if err == nil {
|
|
||||||
for _, s := range sqlFilterSections {
|
|
||||||
if s == uint(filterSectionUint64) {
|
|
||||||
filterSectionUint = uint(filterSectionUint64)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if filterSectionUint > 0 {
|
params.PageSize = 0
|
||||||
sqlFilterAppend = fmt.Sprintf(
|
params.PersonType = "members"
|
||||||
"%s AND section_id = %d",
|
|
||||||
sqlFilterAppend,
|
var sections []models.Section
|
||||||
filterSectionUint,
|
db.Order("name collate nocase asc").Find(§ions, "contains_members = ? AND id IN ?", true, allowedSections)
|
||||||
)
|
params.AllowedSections = allowedSections
|
||||||
|
|
||||||
|
// Security for active contacts
|
||||||
|
if !permShow {
|
||||||
|
params.Active = false
|
||||||
}
|
}
|
||||||
|
|
||||||
var sqlFilterSearch string
|
// Security for archived contacts
|
||||||
if len(filterSearch) > 0 {
|
if !permShowArchived {
|
||||||
sqlFilterSearch = fmt.Sprintf(
|
params.Archive = false
|
||||||
"%%%s%%", filterSearch,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var people []models.Person
|
results, err := database.PeopleSearch(params)
|
||||||
if len(filterSearch) > 0 {
|
if err != nil {
|
||||||
result := db.Unscoped().Order(
|
|
||||||
"last_name collate nocase asc, first_name collate nocase asc",
|
|
||||||
).Preload("Section").Find(
|
|
||||||
&people,
|
|
||||||
fmt.Sprintf(
|
|
||||||
"is_contact = ? AND section_id IN ? AND (first_name LIKE ? OR last_name LIKE ?) AND deleted_at %s",
|
|
||||||
sqlFilterAppend,
|
|
||||||
),
|
|
||||||
true, sqlFilterSections, sqlFilterSearch, sqlFilterSearch,
|
|
||||||
)
|
|
||||||
|
|
||||||
if result.Error != nil && !errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
result := db.Unscoped().Order(
|
|
||||||
"last_name collate nocase asc, first_name collate nocase asc",
|
|
||||||
).Preload("Section").Find(
|
|
||||||
&people,
|
|
||||||
fmt.Sprintf(
|
|
||||||
"is_contact = ? AND section_id IN ? AND deleted_at %s",
|
|
||||||
sqlFilterAppend,
|
|
||||||
),
|
|
||||||
true, sqlFilterSections,
|
|
||||||
)
|
|
||||||
|
|
||||||
if result.Error != nil && !errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var fields []models.Field
|
var fields []models.Field
|
||||||
db.Order("position asc").Preload(
|
db.Order("position asc").Preload(
|
||||||
|
|
@ -324,7 +209,7 @@ func ContactsExport(c *fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, person := range people {
|
for _, person := range results.Results {
|
||||||
c.Writef("\"%s\";", person.FirstName)
|
c.Writef("\"%s\";", person.FirstName)
|
||||||
c.Writef("\"%s\";", person.LastName)
|
c.Writef("\"%s\";", person.LastName)
|
||||||
c.Writef("\"%s\";", person.Email)
|
c.Writef("\"%s\";", person.Email)
|
||||||
|
|
|
||||||
|
|
@ -297,3 +297,25 @@ func FieldMoveDown(c *fiber.Ctx) error {
|
||||||
|
|
||||||
return c.Redirect("/admin/fields")
|
return c.Redirect("/admin/fields")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func FieldJSON(c *fiber.Ctx) error {
|
||||||
|
id := c.Params("id")
|
||||||
|
|
||||||
|
db, err := helpers.GetDatabase()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var field models.Field
|
||||||
|
result := db.Preload("List").Preload("List.ListItems").Find(&field, "id = ?", id)
|
||||||
|
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.RowsAffected < 1 {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, "Not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(field)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,18 @@
|
||||||
package controllers
|
package controllers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.readonly.ch/bouzoure/pop-camarades/helpers"
|
"git.readonly.ch/bouzoure/pop-camarades/helpers"
|
||||||
|
"git.readonly.ch/bouzoure/pop-camarades/helpers/database"
|
||||||
"git.readonly.ch/bouzoure/pop-camarades/models"
|
"git.readonly.ch/bouzoure/pop-camarades/models"
|
||||||
|
"github.com/charmbracelet/log"
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type PersonValidation struct {
|
type PersonValidation struct {
|
||||||
|
|
@ -28,6 +29,8 @@ type PersonValidation struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func Members(c *fiber.Ctx) error {
|
func Members(c *fiber.Ctx) error {
|
||||||
|
log := helpers.GetLogger()
|
||||||
|
|
||||||
userid, err := helpers.GetSessionUserId(c)
|
userid, err := helpers.GetSessionUserId(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -59,131 +62,53 @@ func Members(c *fiber.Ctx) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
filterSection := c.Query("se")
|
searchJSON := c.Query("s")
|
||||||
filterArchive := c.Query("a")
|
var params database.PeopleSearchParams
|
||||||
filterSearch := c.Query("s")
|
|
||||||
|
|
||||||
sqlFilterSections := allowedSections
|
if len(searchJSON) > 0 {
|
||||||
sqlFilterAppend := "IS NULL"
|
err = json.Unmarshal([]byte(searchJSON), ¶ms)
|
||||||
if filterArchive == "1" {
|
if err != nil {
|
||||||
sqlFilterAppend = "IS NOT NULL"
|
log.Warn(err)
|
||||||
sqlFilterSections = allowedSectionsArchived
|
searchJSON = ""
|
||||||
}
|
|
||||||
|
|
||||||
var filterSectionUint uint
|
|
||||||
if len(filterSection) > 0 {
|
|
||||||
filterSectionUint64, err := strconv.ParseUint(filterSection, 10, 0)
|
|
||||||
if err == nil {
|
|
||||||
for _, s := range sqlFilterSections {
|
|
||||||
if s == uint(filterSectionUint64) {
|
|
||||||
filterSectionUint = uint(filterSectionUint64)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if filterSectionUint > 0 {
|
params.PageNumber, _ = strconv.Atoi(c.Query("p"))
|
||||||
sqlFilterAppend = fmt.Sprintf(
|
params.PageSize = 50
|
||||||
"%s AND section_id = %d",
|
params.PersonType = "members"
|
||||||
sqlFilterAppend,
|
|
||||||
filterSectionUint,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
var sqlFilterSearch string
|
|
||||||
if len(filterSearch) > 0 {
|
|
||||||
sqlFilterSearch = fmt.Sprintf(
|
|
||||||
"%%%s%%", filterSearch,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
var count int64
|
|
||||||
if len(filterSearch) > 0 {
|
|
||||||
db.Model(&models.Person{}).Where(
|
|
||||||
fmt.Sprintf(
|
|
||||||
"is_member = ? AND section_id IN ? AND (first_name LIKE ? OR last_name LIKE ?) AND deleted_at %s",
|
|
||||||
sqlFilterAppend,
|
|
||||||
),
|
|
||||||
true, allowedSections, sqlFilterSearch, sqlFilterSearch,
|
|
||||||
).Count(&count)
|
|
||||||
} else {
|
|
||||||
db.Model(&models.Person{}).Where(
|
|
||||||
fmt.Sprintf(
|
|
||||||
"is_member = ? AND section_id IN ? AND deleted_at %s",
|
|
||||||
sqlFilterAppend,
|
|
||||||
),
|
|
||||||
true, allowedSections,
|
|
||||||
).Count(&count)
|
|
||||||
}
|
|
||||||
|
|
||||||
page, _ := strconv.Atoi(c.Query("p"))
|
|
||||||
pagination := helpers.Paginate(50, int(count), page)
|
|
||||||
|
|
||||||
var people []models.Person
|
|
||||||
if len(filterSearch) > 0 {
|
|
||||||
result := db.Unscoped().Offset(
|
|
||||||
pagination.Offset,
|
|
||||||
).Limit(
|
|
||||||
pagination.PageSize,
|
|
||||||
).Order(
|
|
||||||
"concat(last_name, first_name) collate nocase asc",
|
|
||||||
).Preload(
|
|
||||||
"Section",
|
|
||||||
).Find(
|
|
||||||
&people,
|
|
||||||
fmt.Sprintf(
|
|
||||||
"is_member = ? AND section_id IN ? AND (first_name LIKE ? OR last_name LIKE ?) AND deleted_at %s",
|
|
||||||
sqlFilterAppend,
|
|
||||||
),
|
|
||||||
true, sqlFilterSections, sqlFilterSearch, sqlFilterSearch,
|
|
||||||
)
|
|
||||||
|
|
||||||
if result.Error != nil && !errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
result := db.Unscoped().Offset(
|
|
||||||
pagination.Offset,
|
|
||||||
).Limit(
|
|
||||||
pagination.PageSize,
|
|
||||||
).Order(
|
|
||||||
"concat(last_name, first_name) collate nocase asc",
|
|
||||||
).Preload(
|
|
||||||
"Section",
|
|
||||||
).Find(
|
|
||||||
&people,
|
|
||||||
fmt.Sprintf(
|
|
||||||
"is_member = ? AND section_id IN ? AND deleted_at %s",
|
|
||||||
sqlFilterAppend,
|
|
||||||
),
|
|
||||||
true, sqlFilterSections,
|
|
||||||
)
|
|
||||||
|
|
||||||
if result.Error != nil && !errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var sections []models.Section
|
var sections []models.Section
|
||||||
db.Order(
|
db.Order("name collate nocase asc").Find(§ions, "contains_members = ? AND id IN ?", true, allowedSections)
|
||||||
"name collate nocase asc",
|
params.AllowedSections = allowedSections
|
||||||
).Find(
|
|
||||||
§ions,
|
// Security for active contacts
|
||||||
"contains_members = ? AND id IN ?",
|
if !permShow {
|
||||||
true, sqlFilterSections,
|
params.Active = false
|
||||||
)
|
}
|
||||||
|
|
||||||
|
// Security for archived contacts
|
||||||
|
if !permShowArchived {
|
||||||
|
params.Archive = false
|
||||||
|
}
|
||||||
|
|
||||||
|
results, err := database.PeopleSearch(params)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var fields []models.Field
|
||||||
|
db.Order("position asc").Find(&fields, "person_type = ?", "member")
|
||||||
|
|
||||||
return c.Render("people", fiber.Map{
|
return c.Render("people", fiber.Map{
|
||||||
"PageTitle": "Membres",
|
"PageTitle": "Membres",
|
||||||
"MembersPage": true,
|
"MembersPage": true,
|
||||||
"People": people,
|
"People": results.Results,
|
||||||
"Pagination": pagination,
|
"Pagination": results.Pagination,
|
||||||
"PermShow": permShow,
|
"PermShow": permShow,
|
||||||
"PermShowArchived": permShowArchived,
|
"PermShowArchived": permShowArchived,
|
||||||
|
"SearchJSON": searchJSON,
|
||||||
"Sections": sections,
|
"Sections": sections,
|
||||||
"FilterArchive": filterArchive,
|
"Fields": fields,
|
||||||
"FilterSection": filterSectionUint,
|
|
||||||
"FilterSearch": filterSearch,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -219,76 +144,38 @@ func MembersExport(c *fiber.Ctx) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
filterSection := c.Query("se")
|
searchJSON := c.Query("s")
|
||||||
filterArchive := c.Query("a")
|
var params database.PeopleSearchParams
|
||||||
filterSearch := c.Query("s")
|
|
||||||
|
|
||||||
sqlFilterSections := allowedSections
|
if len(searchJSON) > 0 {
|
||||||
sqlFilterAppend := "IS NULL"
|
err = json.Unmarshal([]byte(searchJSON), ¶ms)
|
||||||
if filterArchive == "1" {
|
if err != nil {
|
||||||
sqlFilterAppend = "IS NOT NULL"
|
log.Warn(err)
|
||||||
sqlFilterSections = allowedSectionsArchived
|
searchJSON = ""
|
||||||
}
|
|
||||||
|
|
||||||
var filterSectionUint uint
|
|
||||||
if len(filterSection) > 0 {
|
|
||||||
filterSectionUint64, err := strconv.ParseUint(filterSection, 10, 0)
|
|
||||||
if err == nil {
|
|
||||||
for _, s := range sqlFilterSections {
|
|
||||||
if s == uint(filterSectionUint64) {
|
|
||||||
filterSectionUint = uint(filterSectionUint64)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if filterSectionUint > 0 {
|
params.PageSize = 0
|
||||||
sqlFilterAppend = fmt.Sprintf(
|
params.PersonType = "members"
|
||||||
"%s AND section_id = %d",
|
|
||||||
sqlFilterAppend,
|
var sections []models.Section
|
||||||
filterSectionUint,
|
db.Order("name collate nocase asc").Find(§ions, "contains_members = ? AND id IN ?", true, allowedSections)
|
||||||
)
|
params.AllowedSections = allowedSections
|
||||||
|
|
||||||
|
// Security for active contacts
|
||||||
|
if !permShow {
|
||||||
|
params.Active = false
|
||||||
}
|
}
|
||||||
|
|
||||||
var sqlFilterSearch string
|
// Security for archived contacts
|
||||||
if len(filterSearch) > 0 {
|
if !permShowArchived {
|
||||||
sqlFilterSearch = fmt.Sprintf(
|
params.Archive = false
|
||||||
"%%%s%%", filterSearch,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var people []models.Person
|
results, err := database.PeopleSearch(params)
|
||||||
if len(filterSearch) > 0 {
|
if err != nil {
|
||||||
result := db.Unscoped().Order(
|
|
||||||
"last_name collate nocase asc, first_name collate nocase asc",
|
|
||||||
).Preload("Section").Find(
|
|
||||||
&people,
|
|
||||||
fmt.Sprintf(
|
|
||||||
"is_member = ? AND section_id IN ? AND (first_name LIKE ? OR last_name LIKE ?) AND deleted_at %s",
|
|
||||||
sqlFilterAppend,
|
|
||||||
),
|
|
||||||
true, sqlFilterSections, sqlFilterSearch, sqlFilterSearch,
|
|
||||||
)
|
|
||||||
|
|
||||||
if result.Error != nil && !errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
result := db.Unscoped().Order(
|
|
||||||
"last_name collate nocase asc, first_name collate nocase asc",
|
|
||||||
).Preload("Section").Find(
|
|
||||||
&people,
|
|
||||||
fmt.Sprintf(
|
|
||||||
"is_member = ? AND section_id IN ? AND deleted_at %s",
|
|
||||||
sqlFilterAppend,
|
|
||||||
),
|
|
||||||
true, sqlFilterSections,
|
|
||||||
)
|
|
||||||
|
|
||||||
if result.Error != nil && !errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var fields []models.Field
|
var fields []models.Field
|
||||||
db.Order("position asc").Preload(
|
db.Order("position asc").Preload(
|
||||||
|
|
@ -337,7 +224,7 @@ func MembersExport(c *fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, person := range people {
|
for _, person := range results.Results {
|
||||||
c.Writef("\"%s\";", person.FirstName)
|
c.Writef("\"%s\";", person.FirstName)
|
||||||
c.Writef("\"%s\";", person.LastName)
|
c.Writef("\"%s\";", person.LastName)
|
||||||
c.Writef("\"%s\";", person.Email)
|
c.Writef("\"%s\";", person.Email)
|
||||||
|
|
|
||||||
349
helpers/database/people.go
Normal file
349
helpers/database/people.go
Normal file
|
|
@ -0,0 +1,349 @@
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.readonly.ch/bouzoure/pop-camarades/helpers"
|
||||||
|
"git.readonly.ch/bouzoure/pop-camarades/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PeopleSearchParams struct {
|
||||||
|
// JSON, provided by user
|
||||||
|
Advanced bool `json:"advanced"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Section uint `json:"section"`
|
||||||
|
Active bool `json:"active"`
|
||||||
|
Archive bool `json:"archive"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Phone string `json:"phone"`
|
||||||
|
Address string `json:"address"`
|
||||||
|
PostalCode string `json:"postal_code"`
|
||||||
|
City string `json:"city"`
|
||||||
|
Fields map[uint][]any `json:"fields"`
|
||||||
|
// Not JSON, defined in backend
|
||||||
|
PersonType string `json:"-"`
|
||||||
|
PageSize int `json:"-"`
|
||||||
|
PageNumber int `json:"-"`
|
||||||
|
AllowedSections []uint `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PeopleSearchResults struct {
|
||||||
|
Results []models.Person
|
||||||
|
Count int64
|
||||||
|
Pagination helpers.Pagination
|
||||||
|
}
|
||||||
|
|
||||||
|
func PeopleSearch(params PeopleSearchParams) (PeopleSearchResults, error) {
|
||||||
|
var results PeopleSearchResults
|
||||||
|
|
||||||
|
db, err := helpers.GetDatabase()
|
||||||
|
if err != nil {
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SQL qeury for results
|
||||||
|
sqlQuery := `--sql
|
||||||
|
SELECT people.*,
|
||||||
|
sections.name AS Section__name
|
||||||
|
FROM people
|
||||||
|
INNER JOIN sections
|
||||||
|
ON people.section_id = sections.id`
|
||||||
|
|
||||||
|
// SQL query to count results
|
||||||
|
sqlQueryCount := `--sql
|
||||||
|
SELECT people.id
|
||||||
|
FROM people
|
||||||
|
`
|
||||||
|
|
||||||
|
// Create filters for both queries
|
||||||
|
var sqlParams []any
|
||||||
|
var sqlFilters string
|
||||||
|
|
||||||
|
// Filter: member or contact
|
||||||
|
sqlFilters = `--sql
|
||||||
|
WHERE is_member = @is_member
|
||||||
|
AND is_contact = @is_contact
|
||||||
|
`
|
||||||
|
if params.PersonType == "members" {
|
||||||
|
sqlParams = append(sqlParams, sql.Named("is_member", true))
|
||||||
|
sqlParams = append(sqlParams, sql.Named("is_contact", false))
|
||||||
|
} else if params.PersonType == "contacts" {
|
||||||
|
sqlParams = append(sqlParams, sql.Named("is_member", false))
|
||||||
|
sqlParams = append(sqlParams, sql.Named("is_contact", true))
|
||||||
|
} else {
|
||||||
|
return results, fmt.Errorf("unkown person type")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter: name -> first_name + last_name
|
||||||
|
if len(params.Name) > 0 {
|
||||||
|
nameFilter := `--sql
|
||||||
|
people.first_name LIKE @name
|
||||||
|
OR people.last_name LIKE @name
|
||||||
|
OR CONCAT(people.first_name, ' ', people.last_name) LIKE @name
|
||||||
|
OR CONCAT(people.last_name, ' ', people.first_name) LIKE @name
|
||||||
|
`
|
||||||
|
sqlParams = append(sqlParams, sql.Named(
|
||||||
|
"name",
|
||||||
|
fmt.Sprintf("%%%s%%", params.Name),
|
||||||
|
))
|
||||||
|
|
||||||
|
if strings.Contains(params.Name, " ") {
|
||||||
|
names := strings.Split(params.Name, " ")
|
||||||
|
for index, name := range names {
|
||||||
|
nameFilter = fmt.Sprintf(`--sql
|
||||||
|
%s
|
||||||
|
OR people.first_name LIKE @name_%d
|
||||||
|
OR people.last_name LIKE @name_%d
|
||||||
|
`, nameFilter, index, index)
|
||||||
|
|
||||||
|
sqlParams = append(sqlParams, sql.Named(
|
||||||
|
fmt.Sprintf("name_%d", index),
|
||||||
|
fmt.Sprintf("%%%s%%", name),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlFilters = fmt.Sprintf(`--sql
|
||||||
|
%s
|
||||||
|
AND (%s)
|
||||||
|
`, sqlFilters, nameFilter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter: section
|
||||||
|
if params.Section > 0 {
|
||||||
|
sqlFilters = fmt.Sprintf(`--sql
|
||||||
|
%s
|
||||||
|
AND people.section_id = @section
|
||||||
|
`, sqlFilters)
|
||||||
|
sqlParams = append(sqlParams, sql.Named("section", params.Section))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter: active (only apply if archived is false)
|
||||||
|
if params.Active && !params.Archive {
|
||||||
|
sqlFilters = fmt.Sprintf(`--sql
|
||||||
|
%s
|
||||||
|
AND people.deleted_at IS NULL
|
||||||
|
`, sqlFilters)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter: archived (only apply if active is false)
|
||||||
|
if params.Archive && !params.Active {
|
||||||
|
sqlFilters = fmt.Sprintf(`--sql
|
||||||
|
%s
|
||||||
|
AND people.deleted_at IS NOT NULL
|
||||||
|
`, sqlFilters)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filters: email
|
||||||
|
if len(params.Email) > 0 {
|
||||||
|
sqlFilters = fmt.Sprintf(`--sql
|
||||||
|
%s
|
||||||
|
AND people.email LIKE @email
|
||||||
|
`, sqlFilters)
|
||||||
|
sqlParams = append(sqlParams, sql.Named(
|
||||||
|
"email",
|
||||||
|
fmt.Sprintf("%%%s%%", params.Email),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filters: phone
|
||||||
|
if len(params.Phone) > 0 {
|
||||||
|
params.Phone = strings.ReplaceAll(params.Phone, " ", "")
|
||||||
|
params.Phone = strings.ReplaceAll(params.Phone, "-", "")
|
||||||
|
params.Phone = strings.ReplaceAll(params.Phone, ".", "")
|
||||||
|
params.Phone = strings.ReplaceAll(params.Phone, "/", "")
|
||||||
|
params.Phone = strings.ReplaceAll(params.Phone, "+", "")
|
||||||
|
|
||||||
|
sqlFilters = fmt.Sprintf(`--sql
|
||||||
|
%s
|
||||||
|
AND (
|
||||||
|
REPLACE(
|
||||||
|
REPLACE(
|
||||||
|
REPLACE(
|
||||||
|
REPLACE(
|
||||||
|
REPLACE(people.phone, ' ', '')
|
||||||
|
, '-', '')
|
||||||
|
, '.', '')
|
||||||
|
, '/', '')
|
||||||
|
, '+', '') LIKE @phone
|
||||||
|
OR REPLACE(
|
||||||
|
REPLACE(
|
||||||
|
REPLACE(
|
||||||
|
REPLACE(
|
||||||
|
REPLACE(people.mobile, ' ', '')
|
||||||
|
, '-', '')
|
||||||
|
, '.', '')
|
||||||
|
, '/', '')
|
||||||
|
, '+', '') LIKE @phone
|
||||||
|
)
|
||||||
|
`, sqlFilters)
|
||||||
|
sqlParams = append(sqlParams, sql.Named(
|
||||||
|
"phone",
|
||||||
|
fmt.Sprintf("%%%s%%", params.Phone),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filters: address
|
||||||
|
if len(params.Address) > 0 {
|
||||||
|
sqlFilters = fmt.Sprintf(`--sql
|
||||||
|
%s
|
||||||
|
AND (
|
||||||
|
people.address1 LIKE @address
|
||||||
|
OR people.address2 LIKE @address
|
||||||
|
)
|
||||||
|
`, sqlFilters)
|
||||||
|
sqlParams = append(sqlParams, sql.Named(
|
||||||
|
"address",
|
||||||
|
fmt.Sprintf("%%%s%%", params.Address),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filters: postal code
|
||||||
|
if len(params.PostalCode) > 0 {
|
||||||
|
sqlFilters = fmt.Sprintf(`--sql
|
||||||
|
%s
|
||||||
|
AND people.postal_code = @postal_code
|
||||||
|
`, sqlFilters)
|
||||||
|
sqlParams = append(sqlParams, sql.Named(
|
||||||
|
"postal_code",
|
||||||
|
params.PostalCode,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filters: city
|
||||||
|
if len(params.City) > 0 {
|
||||||
|
sqlFilters = fmt.Sprintf(`--sql
|
||||||
|
%s
|
||||||
|
AND people.city LIKE @city
|
||||||
|
`, sqlFilters)
|
||||||
|
sqlParams = append(sqlParams, sql.Named(
|
||||||
|
"city",
|
||||||
|
fmt.Sprintf("%%%s%%", params.City),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Security filter: only show results in allowed secitons
|
||||||
|
sqlFilters = fmt.Sprintf(`--sql
|
||||||
|
%s
|
||||||
|
AND people.section_id IN @allowed_sections
|
||||||
|
`, sqlFilters)
|
||||||
|
sqlParams = append(sqlParams, sql.Named("allowed_sections", params.AllowedSections))
|
||||||
|
|
||||||
|
// Filter: optional fields
|
||||||
|
var sqlFieldJoins string
|
||||||
|
for fieldID, values := range params.Fields {
|
||||||
|
var field models.Field
|
||||||
|
db.First(&field, "id = ?", fieldID)
|
||||||
|
|
||||||
|
// Skip if field ID not found in DB
|
||||||
|
if field.ID <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlFieldJoins = fmt.Sprintf(`--sql
|
||||||
|
%s
|
||||||
|
LEFT JOIN field_values
|
||||||
|
AS field_%d
|
||||||
|
ON people.id = field_%d.person_id
|
||||||
|
AND field_%d.field_id = @field_%d_id
|
||||||
|
`, sqlFieldJoins, field.ID, field.ID, field.ID, field.ID)
|
||||||
|
|
||||||
|
sqlParams = append(sqlParams, sql.Named(
|
||||||
|
fmt.Sprintf("field_%d_id", field.ID),
|
||||||
|
field.ID,
|
||||||
|
))
|
||||||
|
|
||||||
|
var fieldFilter string
|
||||||
|
for index, value := range values {
|
||||||
|
var filter string
|
||||||
|
if fieldFilter != "" {
|
||||||
|
filter = "OR"
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v := value.(type) {
|
||||||
|
default:
|
||||||
|
fmt.Println(v)
|
||||||
|
continue
|
||||||
|
case string:
|
||||||
|
filter = fmt.Sprintf(`--sql
|
||||||
|
%s
|
||||||
|
field_%d.value_string LIKE @field_%d_%d
|
||||||
|
OR field_%d.value_date LIKE @field_%d_%d
|
||||||
|
`, filter, field.ID, field.ID, index, field.ID, field.ID, index)
|
||||||
|
sqlParams = append(sqlParams, sql.Named(
|
||||||
|
fmt.Sprintf("field_%d_%d", field.ID, index),
|
||||||
|
fmt.Sprintf("%%%s%%", v),
|
||||||
|
))
|
||||||
|
case float64:
|
||||||
|
filter = fmt.Sprintf(`--sql
|
||||||
|
%s
|
||||||
|
field_%d.value_int = @field_%d_%d
|
||||||
|
OR field_%d.list_item_id = @field_%d_%d
|
||||||
|
`, filter, field.ID, field.ID, index, field.ID, field.ID, index)
|
||||||
|
sqlParams = append(sqlParams, sql.Named(
|
||||||
|
fmt.Sprintf("field_%d_%d", field.ID, index),
|
||||||
|
v,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldFilter = fmt.Sprintf("%s %s", fieldFilter, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fieldFilter != "" {
|
||||||
|
sqlFilters = fmt.Sprintf(`--sql
|
||||||
|
%s
|
||||||
|
AND (%s)
|
||||||
|
`, sqlFilters, fieldFilter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build and run count query
|
||||||
|
sqlQueryCount = fmt.Sprintf(`--sql
|
||||||
|
%s
|
||||||
|
%s
|
||||||
|
%s
|
||||||
|
GROUP BY people.id
|
||||||
|
`, sqlQueryCount, sqlFieldJoins, sqlFilters)
|
||||||
|
|
||||||
|
var count []uint
|
||||||
|
sqlCountResult := db.Raw(sqlQueryCount, sqlParams...).Scan(&count)
|
||||||
|
if sqlCountResult.Error != nil {
|
||||||
|
return results, sqlCountResult.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create pagination with count
|
||||||
|
results.Pagination = helpers.Paginate(
|
||||||
|
params.PageSize,
|
||||||
|
len(count),
|
||||||
|
params.PageNumber,
|
||||||
|
)
|
||||||
|
|
||||||
|
var sqlPagination string
|
||||||
|
if params.PageSize > 0 {
|
||||||
|
sqlPagination = `--sql
|
||||||
|
LIMIT @pagination_limit
|
||||||
|
OFFSET @pagination_offset
|
||||||
|
`
|
||||||
|
sqlParams = append(sqlParams, sql.Named("pagination_limit", results.Pagination.PageSize))
|
||||||
|
sqlParams = append(sqlParams, sql.Named("pagination_offset", results.Pagination.Offset))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build and run paginated result query
|
||||||
|
sqlQuery = fmt.Sprintf(`--sql
|
||||||
|
%s
|
||||||
|
%s
|
||||||
|
%s
|
||||||
|
GROUP BY people.id
|
||||||
|
ORDER BY CONCAT(people.last_name, people.first_name) COLLATE NOCASE ASC
|
||||||
|
%s
|
||||||
|
`, sqlQuery, sqlFieldJoins, sqlFilters, sqlPagination)
|
||||||
|
|
||||||
|
sqlResult := db.Raw(sqlQuery, sqlParams...).Scan(&results.Results)
|
||||||
|
if sqlResult.Error != nil {
|
||||||
|
return results, sqlResult.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
3
main.go
3
main.go
|
|
@ -167,6 +167,9 @@ func main() {
|
||||||
app.Post("/contacts/:id<int;min(0)>/restore", controllers.ContactRestore)
|
app.Post("/contacts/:id<int;min(0)>/restore", controllers.ContactRestore)
|
||||||
app.Post("/contacts/:id<int;min(0)>/purge", controllers.ContactPurge)
|
app.Post("/contacts/:id<int;min(0)>/purge", controllers.ContactPurge)
|
||||||
|
|
||||||
|
// Fields
|
||||||
|
app.Get("/fields/:id<int;min(0)>", controllers.FieldJSON)
|
||||||
|
|
||||||
// Account manage
|
// Account manage
|
||||||
app.Get("/account/manage", controllers.AccountManage)
|
app.Get("/account/manage", controllers.AccountManage)
|
||||||
app.Post("/account/manage", controllers.AccountManage)
|
app.Post("/account/manage", controllers.AccountManage)
|
||||||
|
|
|
||||||
199
static/search.js
Normal file
199
static/search.js
Normal file
|
|
@ -0,0 +1,199 @@
|
||||||
|
$(document).ready(function() {
|
||||||
|
$("#search").on("click", function() {
|
||||||
|
search();
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#advanced").on("click", function() {
|
||||||
|
if ($(this).data("state") === "true") {
|
||||||
|
$(this).find("i").removeClass("bi-arrow-up");
|
||||||
|
$(this).find("i").addClass("bi-arrow-down");
|
||||||
|
$("#advanced-section").addClass("d-none");
|
||||||
|
$(this).data("state", "false");
|
||||||
|
} else {
|
||||||
|
$(this).find("i").removeClass("bi-arrow-down");
|
||||||
|
$(this).find("i").addClass("bi-arrow-up");
|
||||||
|
$("#advanced-section").removeClass("d-none");
|
||||||
|
$(this).data("state", "true");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#reset-search").on("click", function() {
|
||||||
|
$("#search-json").val("");
|
||||||
|
$("#search-page").val("1");
|
||||||
|
$("#search-form").submit();
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#search-container").on("change", ".field-select", function() {
|
||||||
|
var lastField = $(this).attr("data-last-field");
|
||||||
|
var isInitial = $(this).attr("data-is-initial");
|
||||||
|
var initialValue = $(this).attr("data-initial-value");
|
||||||
|
|
||||||
|
var field = $(this).val();
|
||||||
|
if (field == "") {
|
||||||
|
$(this).parents(".row").remove();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((lastField === undefined || lastField.length === 0) && (isInitial === undefined || isInitial !== "yes")) {
|
||||||
|
createFieldSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
$(this).attr("data-last-field", field);
|
||||||
|
|
||||||
|
$(this).parents(".row").find(".end-col").find("input").remove();
|
||||||
|
$(this).parents(".row").find(".end-col").find("select").remove();
|
||||||
|
|
||||||
|
$(elem).parents(".row").find(".end-col").append($("<input>", {
|
||||||
|
class: "form-control",
|
||||||
|
type: "text",
|
||||||
|
disabled: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
var elem = $(this);
|
||||||
|
$.getJSON("/fields/" + field, function(data) {
|
||||||
|
$(this).parents(".row").find(".end-col").find("input").remove();
|
||||||
|
|
||||||
|
if (data.FieldType === "list") {
|
||||||
|
var select = $("<select>", {
|
||||||
|
class: "form-select",
|
||||||
|
"data-optional-field": field,
|
||||||
|
"data-search-type": "int"
|
||||||
|
});
|
||||||
|
$(elem).parents(".row").find(".end-col").append(select);
|
||||||
|
|
||||||
|
for (const [_, value] of Object.entries(data.List.ListItems)) {
|
||||||
|
$(select).append($("<option>", {
|
||||||
|
value: value.ID,
|
||||||
|
text: value.Value
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} else if (data.FieldType === "number") {
|
||||||
|
$(elem).parents(".row").find(".end-col").append($("<input>", {
|
||||||
|
class: "form-control",
|
||||||
|
type: "number",
|
||||||
|
"data-optional-field": field,
|
||||||
|
"data-search-type": "int"
|
||||||
|
}));
|
||||||
|
} else if (data.FieldType === "date") {
|
||||||
|
$(elem).parents(".row").find(".end-col").append($("<input>", {
|
||||||
|
class: "form-control",
|
||||||
|
type: "date",
|
||||||
|
"data-optional-field": field
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
$(elem).parents(".row").find(".end-col").append($("<input>", {
|
||||||
|
class: "form-control",
|
||||||
|
type: "text",
|
||||||
|
"data-optional-field": field
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (initialValue !== undefined && initialValue.length > 0) {
|
||||||
|
$(elem).parents(".row").find("[data-optional-field]").val(initialValue);
|
||||||
|
$(elem).attr("data-initial-value", null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#search-container").on("keyup", "input[type=text]", function(e) {
|
||||||
|
if (e.keyCode === 13) {
|
||||||
|
search();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var json = $("#search-json").val();
|
||||||
|
if (json.length > 0) {
|
||||||
|
var searchData = JSON.parse(json);
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(searchData)) {
|
||||||
|
if (key === "advanced") {
|
||||||
|
if (value) $("#advanced").trigger("click");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === "fields") {
|
||||||
|
for (const [field_id, field_values] of Object.entries(value)) {
|
||||||
|
for (const field_value of field_values) {
|
||||||
|
createFieldSelection(field_id, field_value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === "boolean") {
|
||||||
|
$("[data-search-field=" + key + "]").prop("checked", value);
|
||||||
|
} else {
|
||||||
|
$("[data-search-field=" + key + "]").val(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createFieldSelection();
|
||||||
|
});
|
||||||
|
|
||||||
|
function createFieldSelection(field, value) {
|
||||||
|
var elem = $("#search-fields-model").clone().appendTo("#search-fields");
|
||||||
|
$(elem).attr("id", null);
|
||||||
|
$(elem).removeClass("d-none");
|
||||||
|
$(elem).addClass("search-fields-count");
|
||||||
|
|
||||||
|
if (field !== undefined && field.length > 0) {
|
||||||
|
$(elem).find(".field-select").attr("data-initial-value", value);
|
||||||
|
$(elem).find(".field-select").attr("data-is-initial", "yes");
|
||||||
|
$(elem).find(".field-select").val(field);
|
||||||
|
$(elem).find(".field-select").trigger("change");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function search() {
|
||||||
|
var advancedSearch = ($("#advanced").data("state") === "true");
|
||||||
|
var searchData = {
|
||||||
|
advanced: advancedSearch
|
||||||
|
};
|
||||||
|
|
||||||
|
$("[data-search-field]").each(function() {
|
||||||
|
var advancedField = $(this).data("search-advanced");
|
||||||
|
if (!advancedSearch && advancedField) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var index = $(this).data("search-field");
|
||||||
|
var value = $(this).val();
|
||||||
|
|
||||||
|
if ($(this).data("search-type") === "int") {
|
||||||
|
value = parseInt(value);
|
||||||
|
if (isNaN(value)) return;
|
||||||
|
} else if ($(this).attr("type") === "checkbox") {
|
||||||
|
value = $(this).prop("checked");
|
||||||
|
}
|
||||||
|
|
||||||
|
searchData[index] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
fields = {}
|
||||||
|
$("[data-optional-field]:not(:disabled)").each(function() {
|
||||||
|
if (!advancedSearch) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var index = $(this).attr("data-optional-field");
|
||||||
|
var value = $(this).val();
|
||||||
|
|
||||||
|
if ($(this).data("search-type") === "int") {
|
||||||
|
value = parseInt(value);
|
||||||
|
if (isNaN(value)) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fields[index] !== undefined) {
|
||||||
|
fields[index].push(value);
|
||||||
|
} else {
|
||||||
|
fields[index] = [value];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
searchData["fields"] = fields;
|
||||||
|
|
||||||
|
var json = JSON.stringify(searchData);
|
||||||
|
$("#search-json").val(json);
|
||||||
|
$("#search-page").val("1");
|
||||||
|
$("#search-form").submit();
|
||||||
|
}
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
<i class="bi-plus-lg"></i>
|
<i class="bi-plus-lg"></i>
|
||||||
Ajouter
|
Ajouter
|
||||||
</a>
|
</a>
|
||||||
<a class="btn btn-outline-primary" href="/members/export?se={{ FilterSection }}&s={{ FilterSearch|urlencode }}&a={{ FilterArchive }}">
|
<a class="btn btn-outline-primary" href="/members/export?s={{ SearchJSON|urlencode }}">
|
||||||
<i class="bi-filetype-csv"></i>
|
<i class="bi-filetype-csv"></i>
|
||||||
Exporter
|
Exporter
|
||||||
</a>
|
</a>
|
||||||
|
|
@ -36,7 +36,7 @@
|
||||||
<i class="bi-plus-lg"></i>
|
<i class="bi-plus-lg"></i>
|
||||||
Ajouter
|
Ajouter
|
||||||
</a>
|
</a>
|
||||||
<a class="btn btn-outline-primary" href="/contacts/export?se={{ FilterSection }}&s={{ FilterSearch|urlencode }}&a={{ FilterArchive }}">
|
<a class="btn btn-outline-primary" href="/contacts/export?s={{ SearchJSON|urlencode }}">
|
||||||
<i class="bi-filetype-csv"></i>
|
<i class="bi-filetype-csv"></i>
|
||||||
Exporter
|
Exporter
|
||||||
</a>
|
</a>
|
||||||
|
|
@ -44,98 +44,130 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form id="filters" method="get" class="my-3">
|
<div id="search-container" class="h-100 p-4 mb-3 bg-body-tertiary border rounded-3">
|
||||||
<input type="hidden" name="p" value="{{ Pagination.CurrentPage }}">
|
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-6 col-lg-3 mb-2">
|
<div class="col-lg-6 mb-3">
|
||||||
|
|
||||||
<label
|
<label for="name" class="form-label">Nom et prénom</label>
|
||||||
for="archive"
|
<input type="text" class="form-control" id="name" data-search-field="name" data-search-advanced="false" name="name">
|
||||||
class="form-label"
|
|
||||||
>
|
</div>
|
||||||
Section
|
<div class="col-sm-6 col-lg-3 mb-3">
|
||||||
</label>
|
|
||||||
<select
|
<label for="section" class="form-label">Section</label>
|
||||||
class="form-select"
|
<select class="form-select" id="section" data-search-field="section" data-search-type="int" data-search-advanced="false" name="section">
|
||||||
id="section"
|
<option value=""></option>
|
||||||
name="se"
|
|
||||||
>
|
|
||||||
<option value="0">Choisir...</option>
|
|
||||||
{% for Section in Sections %}
|
{% for Section in Sections %}
|
||||||
<option
|
<option value="{{ Section.ID }}">
|
||||||
value="{{ Section.ID }}"
|
|
||||||
|
|
||||||
{% if Section.ID == FilterSection %}
|
|
||||||
selected
|
|
||||||
{% endif %}
|
|
||||||
>
|
|
||||||
{{ Section.Name }}
|
{{ Section.Name }}
|
||||||
</option>
|
</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6 col-lg-3 mb-2">
|
<div class="col-sm-6 col-lg-3 mb-3 pt-3">
|
||||||
|
|
||||||
<label
|
|
||||||
for="archive"
|
|
||||||
class="form-label"
|
|
||||||
>
|
|
||||||
Status
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
class="form-select"
|
|
||||||
id="archive"
|
|
||||||
name="a"
|
|
||||||
>
|
|
||||||
{% if PermShow %}
|
{% if PermShow %}
|
||||||
<option value="0">
|
<div class="form-check form-switch">
|
||||||
Actif
|
<input class="form-check-input" type="checkbox" role="switch" data-search-field="active" data-search-advanced="false" id="active" checked>
|
||||||
</option>
|
{% if MembersPage %}
|
||||||
|
<label class="form-check-label" for="active">Afficher membres actifs</label>
|
||||||
|
{% else %}
|
||||||
|
<label class="form-check-label" for="active">Afficher contacts actifs</label>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if PermShowArchived %}
|
{% if PermShowArchived %}
|
||||||
<option
|
<div class="form-check form-switch">
|
||||||
value="1"
|
<input class="form-check-input" type="checkbox" role="switch" data-search-field="archive" data-search-advanced="false" id="archive">
|
||||||
|
{% if MembersPage %}
|
||||||
{% if FilterArchive == "1" %}
|
<label class="form-check-label" for="archive">Afficher membres archivés</label>
|
||||||
selected
|
{% else %}
|
||||||
|
<label class="form-check-label" for="archive">Afficher contacts archivés</label>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
>
|
</div>
|
||||||
Archivé
|
|
||||||
</option>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</select>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-6 mb-2">
|
</div>
|
||||||
|
|
||||||
<label
|
<div class="d-none" id="advanced-section">
|
||||||
for="search"
|
<div class="row">
|
||||||
class="form-label"
|
<div class="col-lg-6 mb-3">
|
||||||
>
|
|
||||||
Recherche
|
<label for="email" class="form-label">Email</label>
|
||||||
</label>
|
<input type="text" class="form-control" id="email" data-search-field="email" data-search-advanced="true" name="email">
|
||||||
<div class="input-group">
|
|
||||||
<input
|
</div>
|
||||||
type="text"
|
<div class="col-lg-6 mb-3">
|
||||||
class="form-control"
|
|
||||||
id="search"
|
<label for="phone" class="form-label">Téléphone</label>
|
||||||
name="s"
|
<input type="text" class="form-control" id="phone" data-search-field="phone" data-search-advanced="true" name="phone">
|
||||||
value="{{ FilterSearch }}"
|
|
||||||
>
|
</div>
|
||||||
<button
|
<div class="col-lg-6 mb-3">
|
||||||
class="btn btn-outline-success"
|
|
||||||
type="submit"
|
<label for="address" class="form-label">Adresse</label>
|
||||||
>
|
<input type="text" class="form-control" id="address" data-search-field="address" data-search-advanced="true" name="address">
|
||||||
<i class="bi-search"></i>
|
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-4 col-lg-2 mb-3">
|
||||||
|
|
||||||
|
<label for="postal_code" class="form-label">Code postal</label>
|
||||||
|
<input type="text" class="form-control" id="postal_code" data-search-field="postal_code" data-search-advanced="true" name="postal_code">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-8 col-lg-4 mb-3">
|
||||||
|
|
||||||
|
<label for="city" class="form-label">Lieu</label>
|
||||||
|
<input type="text" class="form-control" id="city" data-search-field="city" data-search-advanced="true" name="city">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="search-fields-model" class="row d-none">
|
||||||
|
<div class="start-col col-lg-6 mb-3">
|
||||||
|
<label class="form-label">Champ supplémentaire</label>
|
||||||
|
<select class="form-select field-select" data-last-field="">
|
||||||
|
<option value=""></option>
|
||||||
|
{% for Field in Fields %}
|
||||||
|
<option value="{{ Field.ID }}">
|
||||||
|
{{ Field.Name }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="end-col col-lg-6 mb-3">
|
||||||
|
<label class="form-label">Valeur</label>
|
||||||
|
<input type="text" class="form-control" disabled>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="search-fields" class="mb-3"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-2">
|
||||||
|
<div class="col-6">
|
||||||
|
<button class="btn btn-outline-primary" id="advanced" data-state="false" type="button">
|
||||||
|
<i class="bi-arrow-down me-1"></i> Avancé
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-outline-primary" id="reset-search" type="button">
|
||||||
|
<i class="bi-arrow-clockwise me-1"></i> Réinitialiser
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-6 text-end">
|
||||||
|
<button class="btn btn-outline-success" id="search" type="button">
|
||||||
|
<i class="bi-search me-1"></i> Recherche
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
<form id="search-form" method="get" class="d-none">
|
||||||
</div>
|
<input type="hidden" id="search-page" name="p" value="{{ Pagination.CurrentPage }}">
|
||||||
|
<input type="hidden" id="search-json" name="s" value="{{ SearchJSON }}">
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table">
|
<table class="table">
|
||||||
|
|
@ -196,7 +228,7 @@
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a
|
<a
|
||||||
class="page-link"
|
class="page-link"
|
||||||
href="?se={{ FilterSection }}&s={{ FilterSearch|urlencode }}&a={{ FilterArchive }}&p=1"
|
href="?p=1&s={{ SearchJSON|urlencode }}"
|
||||||
>
|
>
|
||||||
<i class="bi-rewind"></i>
|
<i class="bi-rewind"></i>
|
||||||
</a>
|
</a>
|
||||||
|
|
@ -204,7 +236,7 @@
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a
|
<a
|
||||||
class="page-link"
|
class="page-link"
|
||||||
href="?se={{ FilterSection }}&s={{ FilterSearch|urlencode }}&a={{ FilterArchive }}&p={{ Pagination.CurrentPage - 1 }}"
|
href="?&p={{ Pagination.CurrentPage - 1 }}&s={{ SearchJSON|urlencode }}"
|
||||||
>
|
>
|
||||||
<i class="bi-caret-left"></i>
|
<i class="bi-caret-left"></i>
|
||||||
</a>
|
</a>
|
||||||
|
|
@ -220,7 +252,7 @@
|
||||||
class="page-link"
|
class="page-link"
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
href="?se={{ FilterSection }}&s={{ FilterSearch|urlencode }}&a={{ FilterArchive }}&p={{ i }}"
|
href="?&p={{ i }}&s={{ SearchJSON|urlencode }}"
|
||||||
>
|
>
|
||||||
{{ i }}
|
{{ i }}
|
||||||
</a>
|
</a>
|
||||||
|
|
@ -242,7 +274,7 @@
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a
|
<a
|
||||||
class="page-link"
|
class="page-link"
|
||||||
href="?se={{ FilterSection }}&s={{ FilterSearch|urlencode }}&a={{ FilterArchive }}&p={{ Pagination.CurrentPage + 1 }}"
|
href="?&p={{ Pagination.CurrentPage + 1 }}&s={{ SearchJSON|urlencode }}"
|
||||||
>
|
>
|
||||||
<i class="bi-caret-right"></i>
|
<i class="bi-caret-right"></i>
|
||||||
</a>
|
</a>
|
||||||
|
|
@ -250,7 +282,7 @@
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a
|
<a
|
||||||
class="page-link"
|
class="page-link"
|
||||||
href="?se={{ FilterSection }}&s={{ FilterSearch|urlencode }}&a={{ FilterArchive }}&p={{ Pagination.MaxPages }}"
|
href="?p={{ Pagination.MaxPages }}&s={{ SearchJSON|urlencode }}"
|
||||||
>
|
>
|
||||||
<i class="bi-fast-forward"></i>
|
<i class="bi-fast-forward"></i>
|
||||||
</a>
|
</a>
|
||||||
|
|
@ -263,11 +295,5 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block javascript %}
|
{% block javascript %}
|
||||||
<script>
|
<script src="/static/search.js"></script>
|
||||||
$(document).ready(function() {
|
|
||||||
$("#filters select").on("change", function() {
|
|
||||||
$("#filters").submit();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
@ -234,10 +234,11 @@
|
||||||
{% set count = 1 %}
|
{% set count = 1 %}
|
||||||
{% for FieldValue in FieldValues %}
|
{% for FieldValue in FieldValues %}
|
||||||
{% if FieldValue.FieldID == Field.ID %}
|
{% if FieldValue.FieldID == Field.ID %}
|
||||||
{% set count = count + 1 %}
|
{% if FieldValue.FieldID == Field.ID %}
|
||||||
{% if forloop.Last %}
|
<div class="mb-1">
|
||||||
<textarea type="text" class="form-control" disabled readonly rows="{{ count }}">{% for FieldValue in FieldValues %}{% if FieldValue.FieldID == Field.ID %}- {{ FieldValue.ListItem.Value }}
|
<input type="checkbox" class="form-check-input me-2" checked disabled>
|
||||||
{% endif %}{% endfor %}</textarea>
|
<label class="form-label">{{ FieldValue.ListItem.Value }}</label>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue