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.*, 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, ) 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 %s %s GROUP BY people.id ORDER BY CONCAT(people.last_name, people.first_name) COLLATE NOCASE ASC %s `, sqlQuery, sqlFieldJoins, sqlFilters, sqlPagination) sqlResult := db.Raw(sqlQuery, sqlParams...).Scan(&results.Results) if sqlResult.Error != nil { return results, sqlResult.Error } return results, nil }