pop-camarades/helpers/database/people.go

389 lines
9.4 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:"-"`
OrderColumn string `json:"-"`
OrderDirection string `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
}
if strings.EqualFold(params.OrderDirection, "DESC") {
params.OrderDirection = "DESC"
} else {
params.OrderDirection = "ASC"
}
switch strings.ToLower(params.OrderColumn) {
case "address":
params.OrderColumn = "people.address1"
case "npa":
params.OrderColumn = "people.postal_code"
case "section":
params.OrderColumn = "people.section_id"
case "created":
params.OrderColumn = "people.created_at"
case "updated":
params.OrderColumn = "people.updated_at"
default:
params.OrderColumn = "CONCAT(people.last_name, people.first_name)"
}
// SQL qeury for results
sqlQuery := `
SELECT people.*
FROM people
`
// SQL query to count results
sqlQueryCount := `
SELECT people.id
FROM people
`
// Create filters for both queries
var sqlParams []any
var sqlFilters string
// Filter: member or contact
sqlFilters = `
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 := `
LOWER(people.first_name) LIKE LOWER(@name)
OR LOWER(people.last_name) LIKE LOWER(@name)
OR LOWER(CONCAT(people.first_name, ' ', people.last_name)) LIKE LOWER(@name)
OR LOWER(CONCAT(people.last_name, ' ', people.first_name)) LIKE LOWER(@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(`
%s
OR LOWER(people.first_name) LIKE LOWER(@name_%d)
OR LOWER(people.last_name) LIKE LOWER(@name_%d)
`, nameFilter, index, index)
sqlParams = append(sqlParams, sql.Named(
fmt.Sprintf("name_%d", index),
fmt.Sprintf("%%%s%%", name),
))
}
}
sqlFilters = fmt.Sprintf(`
%s
AND (%s)
`, sqlFilters, nameFilter)
}
// Filter: section
if params.Section > 0 {
sqlFilters = fmt.Sprintf(`
%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(`
%s
AND people.deleted_at IS NULL
`, sqlFilters)
}
// Filter: archived (only apply if active is false)
if params.Archive && !params.Active {
sqlFilters = fmt.Sprintf(`
%s
AND people.deleted_at IS NOT NULL
`, sqlFilters)
}
// Filter: if both active and archived are turned off, return nothing
if !params.Archive && !params.Active {
sqlFilters = fmt.Sprintf(`
%s
AND 0=1
`, sqlFilters)
}
// Filters: email
if len(params.Email) > 0 {
sqlFilters = fmt.Sprintf(`
%s
AND LOWER(people.email) LIKE LOWER(@email)
`, sqlFilters)
sqlParams = append(sqlParams, sql.Named(
"email",
fmt.Sprintf("%%%s%%", params.Email),
))
}
// Filters: phone
if len(params.Phone) > 0 {
var phoneWithoutZeroFilter string
if string(params.Phone[0]) == "0" {
phoneWithoutZeroFilter = `
OR
TRANSLATE(people.phone, ' ,-,.,/,+', '') LIKE TRANSLATE(@phone2, ' ,-,.,/,+', '') OR
TRANSLATE(people.mobile, ' ,-,.,/,+', '') LIKE TRANSLATE(@phone2, ' ,-,.,/,+', '')
`
phone2 := params.Phone[1:]
sqlParams = append(sqlParams, sql.Named(
"phone2",
fmt.Sprintf("%%%s%%", phone2),
))
}
sqlFilters = fmt.Sprintf(`
%s
AND (
TRANSLATE(people.phone, ' ,-,.,/,+', '') LIKE TRANSLATE(@phone, ' ,-,.,/,+', '') OR
TRANSLATE(people.mobile, ' ,-,.,/,+', '') LIKE TRANSLATE(@phone, ' ,-,.,/,+', '')
%s
)
`, sqlFilters, phoneWithoutZeroFilter)
sqlParams = append(sqlParams, sql.Named(
"phone",
fmt.Sprintf("%%%s%%", params.Phone),
))
}
// Filters: address
if len(params.Address) > 0 {
sqlFilters = fmt.Sprintf(`
%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(`
%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(`
%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(`
%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(`
%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) {
case string:
if field.FieldType == "date" {
filter = fmt.Sprintf(`
%s
TO_CHAR(field_%d.value_date, 'YYYY-MM-DD') = @field_%d_%d
`, filter, field.ID, field.ID, index)
sqlParams = append(sqlParams, sql.Named(
fmt.Sprintf("field_%d_%d", field.ID, index),
v,
))
} else {
filter = fmt.Sprintf(`
%s
LOWER(field_%d.value_string) LIKE LOWER(@field_%d_%d)
`, filter, field.ID, field.ID, index)
sqlParams = append(sqlParams, sql.Named(
fmt.Sprintf("field_%d_%d", field.ID, index),
fmt.Sprintf("%%%s%%", v),
))
}
case float64:
if field.FieldType == "list" {
filter = fmt.Sprintf(`
%s
field_%d.list_item_id = @field_%d_%d
`, filter, field.ID, field.ID, index)
sqlParams = append(sqlParams, sql.Named(
fmt.Sprintf("field_%d_%d", field.ID, index),
v,
))
} else {
filter = fmt.Sprintf(`
%s
field_%d.value_int = @field_%d_%d
`, filter, 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(`
%s
AND (%s)
`, sqlFilters, fieldFilter)
}
}
// Build and run count query
sqlQueryCount = fmt.Sprintf(`
%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 = `
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(`
%s
%s
%s
GROUP BY people.id
ORDER BY %s %s
%s
`, sqlQuery, sqlFieldJoins, sqlFilters, params.OrderColumn, params.OrderDirection, sqlPagination)
sqlResult := db.Raw(sqlQuery, sqlParams...).Preload("Section").Find(&results.Results)
if sqlResult.Error != nil {
return results, sqlResult.Error
}
return results, nil
}