Merge new search (members + contacts) into main branch #3

Merged
bouzoure merged 7 commits from nextgen-search into main 2025-03-26 18:43:56 +01:00
5 changed files with 133 additions and 271 deletions
Showing only changes of commit 04bd096019 - Show all commits

View file

@ -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), &params)
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(&sections, "contains_contacts = ? AND id IN ?", true, allowedSections)
"name collate nocase asc", params.AllowedSections = allowedSections
).Find(
&sections, // 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,75 +129,37 @@ 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), &params)
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(&sections, "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( return err
"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
}
} 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
@ -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)

View file

@ -2,7 +2,6 @@ package controllers
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
@ -11,9 +10,9 @@ import (
"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/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 {
@ -82,6 +81,16 @@ func Members(c *fiber.Ctx) error {
db.Order("name collate nocase asc").Find(&sections, "contains_members = ? AND id IN ?", true, allowedSections) db.Order("name collate nocase asc").Find(&sections, "contains_members = ? AND id IN ?", true, allowedSections)
params.AllowedSections = allowedSections params.AllowedSections = allowedSections
// Security for active contacts
if !permShow {
params.Active = false
}
// Security for archived contacts
if !permShowArchived {
params.Archive = false
}
results, err := database.PeopleSearch(params) results, err := database.PeopleSearch(params)
if err != nil { if err != nil {
return err return err
@ -135,75 +144,37 @@ 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), &params)
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(&sections, "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( return err
"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
}
} 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
@ -253,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)

View file

@ -45,15 +45,7 @@ func PeopleSearch(params PeopleSearchParams) (PeopleSearchResults, error) {
// SQL qeury for results // SQL qeury for results
sqlQuery := `--sql sqlQuery := `--sql
SELECT people.id, SELECT people.*,
people.is_member,
people.is_contact,
people.first_name,
people.last_name,
people.address1,
people.postal_code,
people.city,
people.section_id,
sections.name AS Section__name sections.name AS Section__name
FROM people FROM people
INNER JOIN sections INNER JOIN sections
@ -328,6 +320,16 @@ func PeopleSearch(params PeopleSearchParams) (PeopleSearchResults, error) {
params.PageNumber, 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 // Build and run paginated result query
sqlQuery = fmt.Sprintf(`--sql sqlQuery = fmt.Sprintf(`--sql
%s %s
@ -335,12 +337,8 @@ func PeopleSearch(params PeopleSearchParams) (PeopleSearchResults, error) {
%s %s
GROUP BY people.id GROUP BY people.id
ORDER BY CONCAT(people.last_name, people.first_name) COLLATE NOCASE ASC ORDER BY CONCAT(people.last_name, people.first_name) COLLATE NOCASE ASC
LIMIT @pagination_limit %s
OFFSET @pagination_offset `, sqlQuery, sqlFieldJoins, sqlFilters, sqlPagination)
`, sqlQuery, sqlFieldJoins, sqlFilters)
sqlParams = append(sqlParams, sql.Named("pagination_limit", results.Pagination.PageSize))
sqlParams = append(sqlParams, sql.Named("pagination_offset", results.Pagination.Offset))
sqlResult := db.Raw(sqlQuery, sqlParams...).Scan(&results.Results) sqlResult := db.Raw(sqlQuery, sqlParams...).Scan(&results.Results)
if sqlResult.Error != nil { if sqlResult.Error != nil {

View file

@ -172,6 +172,10 @@ function search() {
fields = {} fields = {}
$("[data-optional-field]:not(:disabled)").each(function() { $("[data-optional-field]:not(:disabled)").each(function() {
if (!advancedSearch) {
return;
}
var index = $(this).attr("data-optional-field"); var index = $(this).attr("data-optional-field");
var value = $(this).val(); var value = $(this).val();

View file

@ -67,23 +67,27 @@
</div> </div>
<div class="col-sm-6 col-lg-3 mb-3 pt-3"> <div class="col-sm-6 col-lg-3 mb-3 pt-3">
<div class="form-check form-switch"> {% if PermShow %}
<input class="form-check-input" type="checkbox" role="switch" data-search-field="active" data-search-advanced="false" id="active" checked> <div class="form-check form-switch">
{% if MembersPage %} <input class="form-check-input" type="checkbox" role="switch" data-search-field="active" data-search-advanced="false" id="active" checked>
<label class="form-check-label" for="active">Afficher membres actifs</label> {% if MembersPage %}
{% else %} <label class="form-check-label" for="active">Afficher membres actifs</label>
<label class="form-check-label" for="active">Afficher contactss actifs</label> {% else %}
{% endif %} <label class="form-check-label" for="active">Afficher contacts actifs</label>
</div> {% endif %}
</div>
{% endif %}
<div class="form-check form-switch"> {% if PermShowArchived %}
<input class="form-check-input" type="checkbox" role="switch" data-search-field="archive" data-search-advanced="false" id="archive"> <div class="form-check form-switch">
{% if MembersPage %} <input class="form-check-input" type="checkbox" role="switch" data-search-field="archive" data-search-advanced="false" id="archive">
<label class="form-check-label" for="archive">Afficher membres archivés</label> {% if MembersPage %}
{% else %} <label class="form-check-label" for="archive">Afficher membres archivés</label>
<label class="form-check-label" for="archive">Afficher contacts archivés</label> {% else %}
{% endif %} <label class="form-check-label" for="archive">Afficher contacts archivés</label>
</div> {% endif %}
</div>
{% endif %}
</div> </div>
</div> </div>