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 }