New search for members + contacts on list + csv export

This commit is contained in:
William Bouzourène 2025-03-26 18:41:23 +01:00
parent d8662a32d1
commit 04bd096019
5 changed files with 133 additions and 271 deletions

View file

@ -1,17 +1,18 @@
package controllers
import (
"errors"
"encoding/json"
"fmt"
"strconv"
"strings"
"time"
"git.readonly.ch/bouzoure/pop-camarades/helpers"
"git.readonly.ch/bouzoure/pop-camarades/helpers/database"
"git.readonly.ch/bouzoure/pop-camarades/models"
"github.com/charmbracelet/log"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
)
func Contacts(c *fiber.Ctx) error {
@ -46,131 +47,53 @@ func Contacts(c *fiber.Ctx) error {
return err
}
filterSection := c.Query("se")
filterArchive := c.Query("a")
filterSearch := c.Query("s")
searchJSON := c.Query("s")
var params database.PeopleSearchParams
sqlFilterSections := allowedSections
sqlFilterAppend := "IS NULL"
if filterArchive == "1" {
sqlFilterAppend = "IS NOT NULL"
sqlFilterSections = allowedSectionsArchived
}
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 len(searchJSON) > 0 {
err = json.Unmarshal([]byte(searchJSON), &params)
if err != nil {
log.Warn(err)
searchJSON = ""
}
}
if filterSectionUint > 0 {
sqlFilterAppend = fmt.Sprintf(
"%s AND section_id = %d",
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
}
}
params.PageNumber, _ = strconv.Atoi(c.Query("p"))
params.PageSize = 50
params.PersonType = "contacts"
var sections []models.Section
db.Order(
"name collate nocase asc",
).Find(
&sections,
"contains_contacts = ? AND id IN ?",
true, sqlFilterSections,
)
db.Order("name collate nocase asc").Find(&sections, "contains_contacts = ? AND id IN ?", true, 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)
if err != nil {
return err
}
var fields []models.Field
db.Order("position asc").Find(&fields, "person_type = ?", "contact")
return c.Render("people", fiber.Map{
"PageTitle": "Contacts",
"MembersPage": false,
"People": people,
"Pagination": pagination,
"People": results.Results,
"Pagination": results.Pagination,
"PermShow": permShow,
"PermShowArchived": permShowArchived,
"SearchJSON": searchJSON,
"Sections": sections,
"FilterArchive": filterArchive,
"FilterSection": filterSectionUint,
"FilterSearch": filterSearch,
"Fields": fields,
})
}
@ -206,76 +129,38 @@ func ContactsExport(c *fiber.Ctx) error {
return err
}
filterSection := c.Query("se")
filterArchive := c.Query("a")
filterSearch := c.Query("s")
searchJSON := c.Query("s")
var params database.PeopleSearchParams
sqlFilterSections := allowedSections
sqlFilterAppend := "IS NULL"
if filterArchive == "1" {
sqlFilterAppend = "IS NOT NULL"
sqlFilterSections = allowedSectionsArchived
}
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 len(searchJSON) > 0 {
err = json.Unmarshal([]byte(searchJSON), &params)
if err != nil {
log.Warn(err)
searchJSON = ""
}
}
if filterSectionUint > 0 {
sqlFilterAppend = fmt.Sprintf(
"%s AND section_id = %d",
sqlFilterAppend,
filterSectionUint,
)
params.PageSize = 0
params.PersonType = "members"
var sections []models.Section
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
if len(filterSearch) > 0 {
sqlFilterSearch = fmt.Sprintf(
"%%%s%%", filterSearch,
)
// Security for archived contacts
if !permShowArchived {
params.Archive = false
}
var people []models.Person
if len(filterSearch) > 0 {
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) {
results, err := database.PeopleSearch(params)
if err != nil {
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
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.LastName)
c.Writef("\"%s\";", person.Email)

View file

@ -2,7 +2,6 @@ package controllers
import (
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"
@ -11,9 +10,9 @@ import (
"git.readonly.ch/bouzoure/pop-camarades/helpers"
"git.readonly.ch/bouzoure/pop-camarades/helpers/database"
"git.readonly.ch/bouzoure/pop-camarades/models"
"github.com/charmbracelet/log"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
)
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)
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)
if err != nil {
return err
@ -135,76 +144,38 @@ func MembersExport(c *fiber.Ctx) error {
return err
}
filterSection := c.Query("se")
filterArchive := c.Query("a")
filterSearch := c.Query("s")
searchJSON := c.Query("s")
var params database.PeopleSearchParams
sqlFilterSections := allowedSections
sqlFilterAppend := "IS NULL"
if filterArchive == "1" {
sqlFilterAppend = "IS NOT NULL"
sqlFilterSections = allowedSectionsArchived
}
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 len(searchJSON) > 0 {
err = json.Unmarshal([]byte(searchJSON), &params)
if err != nil {
log.Warn(err)
searchJSON = ""
}
}
if filterSectionUint > 0 {
sqlFilterAppend = fmt.Sprintf(
"%s AND section_id = %d",
sqlFilterAppend,
filterSectionUint,
)
params.PageSize = 0
params.PersonType = "members"
var sections []models.Section
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
if len(filterSearch) > 0 {
sqlFilterSearch = fmt.Sprintf(
"%%%s%%", filterSearch,
)
// Security for archived contacts
if !permShowArchived {
params.Archive = false
}
var people []models.Person
if len(filterSearch) > 0 {
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) {
results, err := database.PeopleSearch(params)
if err != nil {
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
db.Order("position asc").Preload(
@ -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.LastName)
c.Writef("\"%s\";", person.Email)

View file

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

View file

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

View file

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