pop-camarades/helpers/database/people.go

351 lines
8.3 KiB
Go

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.id,
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
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,
)
// 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
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))
sqlResult := db.Raw(sqlQuery, sqlParams...).Scan(&results.Results)
if sqlResult.Error != nil {
return results, sqlResult.Error
}
return results, nil
}