From 7a65aa43b1f2271f86caeb5bd99329e8dc8e3c4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?William=20Bouzour=C3=A8ne?= Date: Mon, 24 Mar 2025 20:01:33 +0100 Subject: [PATCH 1/6] Start reworking the members and contacts search --- views/people.html | 177 +++++++++++++++++++++++++--------------------- 1 file changed, 97 insertions(+), 80 deletions(-) diff --git a/views/people.html b/views/people.html index d51c34c..6d1479f 100644 --- a/views/people.html +++ b/views/people.html @@ -44,97 +44,83 @@ {% endif %} -
- +
+
-
-
- - - - -
-
- - - + + {% for Section in Sections %} + - {% endif %} + {% endfor %} + - {% if PermShowArchived %} +
+
+ + + + {% endif %} + -
-
- - -
- - -
- -
+
+ + + + +
+
+ + + + +
+
+ +
+
+ +
+
+ +
+
+ + + +
@@ -265,9 +251,40 @@ {% block javascript %} {% endblock %} \ No newline at end of file From fac4e695fcc3678f9b036cf0ef2740ad66c48b5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?William=20Bouzour=C3=A8ne?= Date: Mon, 24 Mar 2025 21:36:25 +0100 Subject: [PATCH 2/6] Javascript based search system (using json to be dynamic) --- static/search.js | 72 ++++++++++++++++++++++ views/people.html | 148 +++++++++++++++++++++------------------------- 2 files changed, 138 insertions(+), 82 deletions(-) create mode 100644 static/search.js diff --git a/static/search.js b/static/search.js new file mode 100644 index 0000000..a6ec53b --- /dev/null +++ b/static/search.js @@ -0,0 +1,72 @@ +$(document).ready(function() { + $("#search").on("click", function() { + search(); + }); + + $("#advanced").on("click", function() { + if ($(this).data("state") === "true") { + $(this).find("i").removeClass("bi-chevron-double-up"); + $(this).find("i").addClass("bi-chevron-double-down"); + $("#advanced-section").addClass("d-none"); + $(this).data("state", "false"); + } else { + $(this).find("i").removeClass("bi-chevron-double-down"); + $(this).find("i").addClass("bi-chevron-double-up"); + $("#advanced-section").removeClass("d-none"); + $(this).data("state", "true"); + } + }); + + var json = $("#search-json").val(); + if (json.length > 0) { + var searchData = JSON.parse(json); + for (const [key, value] of Object.entries(searchData)) { + if (key === "advanced") { + if (value) { + $("#advanced").trigger("click"); + } + + continue; + } + + if (key === "fields") { + // TODO: gérer les champs suppl. + continue + } + + if (typeof value === "boolean" && value) { + console.log(key, value, typeof(value)); + $("[data-search-field=" + key + "]").prop("checked", true); + } else { + $("[data-search-field=" + key + "]").val(value); + } + } + } +}); + +function search() { + var advancedSearch = ($("#advanced").data("state") === "true"); + var searchData = { + advanced: advancedSearch + }; + + $("[data-search-field]").each(function() { + var advancedField = $(this).data("search-advanced"); + if (!advancedSearch && advancedField) { + return; + } + + var index = $(this).data("search-field"); + var value = $(this).val(); + + if ($(this).attr("type") == "checkbox") { + value = $(this).prop("checked"); + } + + searchData[index] = value; + }); + + var json = JSON.stringify(searchData); + $("#search-json").val(json); + $("#search-form").submit(); +} \ No newline at end of file diff --git a/views/people.html b/views/people.html index 6d1479f..bea48f5 100644 --- a/views/people.html +++ b/views/people.html @@ -44,20 +44,24 @@ {% endif %}
-
-
+
+
+ + + + +
+
- {% if PermShow %} - - {% endif %} - - {% if PermShowArchived %} - - {% endif %} - - -
-
- - - - -
-
- - - +
+ + +
-
+
+
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+
+ +
-
+ - +
@@ -249,42 +270,5 @@ {% endblock %} {% block javascript %} - + {% endblock %} \ No newline at end of file From e6eec0dfaf878fe601dce293950c718d24c1a998 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?William=20Bouzour=C3=A8ne?= Date: Tue, 25 Mar 2025 21:55:54 +0100 Subject: [PATCH 3/6] More work on the new people search --- controllers/fields.go | 22 ++++ controllers/members.go | 49 ++++++--- helpers/database/people.go | 75 ++++++++++++++ main.go | 3 + static/search.js | 105 ++++++++++++++++--- views/people.html | 204 ++++++++++++++++++++----------------- 6 files changed, 337 insertions(+), 121 deletions(-) create mode 100644 helpers/database/people.go diff --git a/controllers/fields.go b/controllers/fields.go index 9507880..22f3d43 100644 --- a/controllers/fields.go +++ b/controllers/fields.go @@ -297,3 +297,25 @@ func FieldMoveDown(c *fiber.Ctx) error { return c.Redirect("/admin/fields") } + +func FieldJSON(c *fiber.Ctx) error { + id := c.Params("id") + + db, err := helpers.GetDatabase() + if err != nil { + return err + } + + var field models.Field + result := db.Preload("List").Preload("List.ListItems").Find(&field, "id = ?", id) + + if result.Error != nil { + return result.Error + } + + if result.RowsAffected < 1 { + return fiber.NewError(fiber.StatusNotFound, "Not found") + } + + return c.JSON(field) +} diff --git a/controllers/members.go b/controllers/members.go index 0548c70..6fa3aaa 100644 --- a/controllers/members.go +++ b/controllers/members.go @@ -1,6 +1,7 @@ package controllers import ( + "encoding/json" "errors" "fmt" "strconv" @@ -8,6 +9,7 @@ import ( "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/go-playground/validator/v10" "github.com/gofiber/fiber/v2" @@ -59,11 +61,7 @@ func Members(c *fiber.Ctx) error { return err } - filterSection := c.Query("se") - filterArchive := c.Query("a") - filterSearch := c.Query("s") - - sqlFilterSections := allowedSections + /*sqlFilterSections := allowedSections sqlFilterAppend := "IS NULL" if filterArchive == "1" { sqlFilterAppend = "IS NOT NULL" @@ -116,9 +114,6 @@ func Members(c *fiber.Ctx) error { ).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( @@ -162,28 +157,50 @@ func Members(c *fiber.Ctx) error { if result.Error != nil && !errors.Is(result.Error, gorm.ErrRecordNotFound) { return err } - } + }*/ - var sections []models.Section + /*var sections []models.Section db.Order( "name collate nocase asc", ).Find( §ions, "contains_members = ? AND id IN ?", true, sqlFilterSections, - ) + )*/ + + searchJSON := c.Query("s") + var params database.PeopleSearchParams + + if len(searchJSON) > 0 { + err = json.Unmarshal([]byte(searchJSON), ¶ms) + if err != nil { + return err + } + } + + page, _ := strconv.Atoi(c.Query("p")) + + results, err := database.PeopleSearch(params, "members", 50, page) + if err != nil { + return err + } + + var sections []models.Section + db.Order("name collate nocase asc").Find(§ions, "contains_members = ?", true) + + var fields []models.Field + db.Order("position asc").Find(&fields, "person_type = ?", "member") return c.Render("people", fiber.Map{ "PageTitle": "Membres", "MembersPage": true, - "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, }) } diff --git a/helpers/database/people.go b/helpers/database/people.go new file mode 100644 index 0000000..0080452 --- /dev/null +++ b/helpers/database/people.go @@ -0,0 +1,75 @@ +package database + +import ( + "database/sql" + "fmt" + + "git.readonly.ch/bouzoure/pop-camarades/helpers" + "git.readonly.ch/bouzoure/pop-camarades/models" +) + +type PeopleSearchParams struct { + Advanced bool `json:"advanced"` + Name string `json:"name"` + Section uint `json:"section"` + Active bool `json:"active"` + Archived bool `json:"archived"` + 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"` +} + +type PeopleSearchResults struct { + Results []models.Person + Count int64 + Pagination helpers.Pagination +} + +func PeopleSearch(params PeopleSearchParams, personType string, pageSize int, page int) (PeopleSearchResults, error) { + var results PeopleSearchResults + + db, err := helpers.GetDatabase() + if err != nil { + return results, nil + } + + var sqlQuery string + var sqlParams []any + 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 + WHERE is_member = @is_member + AND is_contact = @is_contact` + + if personType == "members" { + sqlParams = append(sqlParams, sql.Named("is_member", true)) + sqlParams = append(sqlParams, sql.Named("is_contact", false)) + } else if 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") + } + + sqlResult := db.Raw(sqlQuery, sqlParams...).Scan(&results.Results) + + if sqlResult.Error != nil { + return results, sqlResult.Error + } + + return results, nil +} diff --git a/main.go b/main.go index 9d79f3e..fe2b952 100644 --- a/main.go +++ b/main.go @@ -167,6 +167,9 @@ func main() { app.Post("/contacts/:id/restore", controllers.ContactRestore) app.Post("/contacts/:id/purge", controllers.ContactPurge) + // Fields + app.Get("/fields/:id", controllers.FieldJSON) + // Account manage app.Get("/account/manage", controllers.AccountManage) app.Post("/account/manage", controllers.AccountManage) diff --git a/static/search.js b/static/search.js index a6ec53b..5dbe1ff 100644 --- a/static/search.js +++ b/static/search.js @@ -5,13 +5,13 @@ $(document).ready(function() { $("#advanced").on("click", function() { if ($(this).data("state") === "true") { - $(this).find("i").removeClass("bi-chevron-double-up"); - $(this).find("i").addClass("bi-chevron-double-down"); + $(this).find("i").removeClass("bi-arrow-up"); + $(this).find("i").addClass("bi-arrow-down"); $("#advanced-section").addClass("d-none"); $(this).data("state", "false"); } else { - $(this).find("i").removeClass("bi-chevron-double-down"); - $(this).find("i").addClass("bi-chevron-double-up"); + $(this).find("i").removeClass("bi-arrow-down"); + $(this).find("i").addClass("bi-arrow-up"); $("#advanced-section").removeClass("d-none"); $(this).data("state", "true"); } @@ -22,10 +22,7 @@ $(document).ready(function() { var searchData = JSON.parse(json); for (const [key, value] of Object.entries(searchData)) { if (key === "advanced") { - if (value) { - $("#advanced").trigger("click"); - } - + if (value) $("#advanced").trigger("click"); continue; } @@ -34,16 +31,90 @@ $(document).ready(function() { continue } - if (typeof value === "boolean" && value) { - console.log(key, value, typeof(value)); - $("[data-search-field=" + key + "]").prop("checked", true); + if (typeof value === "boolean") { + $("[data-search-field=" + key + "]").prop("checked", value); } else { $("[data-search-field=" + key + "]").val(value); } } } + + createFieldSelection(); + $("body").on("change", ".search-fields-count .field-select", function() { + var lastfield = $(this).attr("data-last-field"); + var field = $(this).val(); + if (field == "") { + $(this).parents(".row").remove(); + return; + } + + if (lastfield.length === 0) { + createFieldSelection(); + } + $(this).attr("data-last-field", field); + + $(this).parents(".row").find(".end-col").find("input").remove(); + $(this).parents(".row").find(".end-col").find("select").remove(); + + $(elem).parents(".row").find(".end-col").append($("", { + class: "form-control", + type: "text", + disabled: true, + })); + + var elem = $(this); + $.getJSON("/fields/" + field, function(data) { + $(this).parents(".row").find(".end-col").find("input").remove(); + + if (data.FieldType === "list") { + var select = $("", { + class: "form-control", + type: "number", + "data-optional-field": field + })); + } else if (data.FieldType === "date") { + $(elem).parents(".row").find(".end-col").append($("", { + class: "form-control", + type: "date", + "data-optional-field": field + })); + } else { + $(elem).parents(".row").find(".end-col").append($("", { + class: "form-control", + type: "text", + "data-optional-field": field + })); + } + }); + }); }); +function createFieldSelection() { + const uuid = crypto.randomUUID(); + + var elem = $("#search-fields-model").clone().appendTo("#search-fields"); + $(elem).attr("id", "search-field-" + uuid); + //$(elem).find(".start-col").find("label").attr("for", "search-field-field-" + uuid); + //$(elem).find(".start-col").find("select").attr("id", "search-field-field-" + uuid); + //$(elem).find(".end-col").find("label").attr("for", "search-field-value-" + uuid); + //$(elem).find(".end-col").find("input").attr("id", "search-field-value-" + uuid); + $(elem).removeClass("d-none"); + $(elem).addClass("search-fields-count"); +} + function search() { var advancedSearch = ($("#advanced").data("state") === "true"); var searchData = { @@ -66,7 +137,17 @@ function search() { searchData[index] = value; }); + fields = {} + $("[data-optional-field]:not(:disabled)").each(function() { + var index = $(this).attr("data-optional-field"); + var value = $(this).val(); + + fields[index] = value; + }); + searchData["fields"] = fields; + var json = JSON.stringify(searchData); $("#search-json").val(json); - $("#search-form").submit(); + console.log(searchData); + //$("#search-form").submit(); } \ No newline at end of file diff --git a/views/people.html b/views/people.html index bea48f5..f6323af 100644 --- a/views/people.html +++ b/views/people.html @@ -25,7 +25,7 @@ Ajouter - + Exporter @@ -36,7 +36,7 @@ Ajouter - + Exporter @@ -44,106 +44,124 @@ {% endif %}
-
-
- - - - -
-
- - - - -
-
- - -
- - -
- -
-
- -
+
- - + +
-
+
- - + +
-
+
+ +
+ + {% if MembersPage %} + + {% else %} + + {% endif %} +
- - - -
-
- - - - -
-
- - - +
+ + {% if MembersPage %} + + {% else %} + + {% endif %} +
+ +
+
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ + +
-
-
- -
-
- -
-
- -
- - -
-
@@ -203,7 +221,7 @@
  • @@ -211,7 +229,7 @@
  • @@ -227,7 +245,7 @@ class="page-link" {% endif %} - href="?se={{ FilterSection }}&s={{ FilterSearch|urlencode }}&a={{ FilterArchive }}&p={{ i }}" + href="?&p={{ i }}&s={{ SearchJSON|urlencode }}" > {{ i }} @@ -249,7 +267,7 @@
  • @@ -257,7 +275,7 @@
  • From 8e002415d01169a5c1c968b069d75d25a95cbaa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?William=20Bouzour=C3=A8ne?= Date: Wed, 26 Mar 2025 16:50:33 +0100 Subject: [PATCH 4/6] Person page: use diabled checkboxes instead of list in textarea --- views/person.html | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/views/person.html b/views/person.html index b830ce0..f019eb2 100644 --- a/views/person.html +++ b/views/person.html @@ -234,10 +234,11 @@ {% set count = 1 %} {% for FieldValue in FieldValues %} {% if FieldValue.FieldID == Field.ID %} - {% set count = count + 1 %} - {% if forloop.Last %} - + {% if FieldValue.FieldID == Field.ID %} +
    + + +
    {% endif %} {% endif %} {% endfor %} From d8662a32d1e8b52b58bf6f24994beeba37624dd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?William=20Bouzour=C3=A8ne?= Date: Wed, 26 Mar 2025 18:13:00 +0100 Subject: [PATCH 5/6] Advanced search on members --- controllers/members.go | 125 ++------------- helpers/database/people.go | 320 ++++++++++++++++++++++++++++++++++--- static/search.js | 118 +++++++++----- views/people.html | 9 +- 4 files changed, 396 insertions(+), 176 deletions(-) diff --git a/controllers/members.go b/controllers/members.go index 6fa3aaa..f39d690 100644 --- a/controllers/members.go +++ b/controllers/members.go @@ -30,6 +30,8 @@ type PersonValidation struct { } func Members(c *fiber.Ctx) error { + log := helpers.GetLogger() + userid, err := helpers.GetSessionUserId(c) if err != nil { return err @@ -61,133 +63,30 @@ func Members(c *fiber.Ctx) error { return err } - /*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 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_member = ? 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_member = ? AND section_id IN ? AND deleted_at %s", - sqlFilterAppend, - ), - true, allowedSections, - ).Count(&count) - } - - 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_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) { - 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_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 sections []models.Section - db.Order( - "name collate nocase asc", - ).Find( - §ions, - "contains_members = ? AND id IN ?", - true, sqlFilterSections, - )*/ - searchJSON := c.Query("s") var params database.PeopleSearchParams if len(searchJSON) > 0 { err = json.Unmarshal([]byte(searchJSON), ¶ms) if err != nil { - return err + log.Warn(err) + searchJSON = "" } } - page, _ := strconv.Atoi(c.Query("p")) + params.PageNumber, _ = strconv.Atoi(c.Query("p")) + params.PageSize = 50 + params.PersonType = "members" - results, err := database.PeopleSearch(params, "members", 50, page) + var sections []models.Section + db.Order("name collate nocase asc").Find(§ions, "contains_members = ? AND id IN ?", true, allowedSections) + params.AllowedSections = allowedSections + + results, err := database.PeopleSearch(params) if err != nil { return err } - var sections []models.Section - db.Order("name collate nocase asc").Find(§ions, "contains_members = ?", true) - var fields []models.Field db.Order("position asc").Find(&fields, "person_type = ?", "member") diff --git a/helpers/database/people.go b/helpers/database/people.go index 0080452..779b97b 100644 --- a/helpers/database/people.go +++ b/helpers/database/people.go @@ -3,23 +3,30 @@ package database import ( "database/sql" "fmt" + "strings" "git.readonly.ch/bouzoure/pop-camarades/helpers" "git.readonly.ch/bouzoure/pop-camarades/models" ) type PeopleSearchParams struct { - Advanced bool `json:"advanced"` - Name string `json:"name"` - Section uint `json:"section"` - Active bool `json:"active"` - Archived bool `json:"archived"` - 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"` + // 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 { @@ -28,7 +35,7 @@ type PeopleSearchResults struct { Pagination helpers.Pagination } -func PeopleSearch(params PeopleSearchParams, personType string, pageSize int, page int) (PeopleSearchResults, error) { +func PeopleSearch(params PeopleSearchParams) (PeopleSearchResults, error) { var results PeopleSearchResults db, err := helpers.GetDatabase() @@ -36,10 +43,9 @@ func PeopleSearch(params PeopleSearchParams, personType string, pageSize int, pa return results, nil } - var sqlQuery string - var sqlParams []any - sqlQuery = `--sql - SELECT people.ID, + // SQL qeury for results + sqlQuery := `--sql + SELECT people.id, people.is_member, people.is_contact, people.first_name, @@ -51,22 +57,292 @@ func PeopleSearch(params PeopleSearchParams, personType string, pageSize int, pa sections.name AS Section__name FROM people INNER JOIN sections - ON people.section_id = sections.id - WHERE is_member = @is_member - AND is_contact = @is_contact` + ON people.section_id = sections.id` - if personType == "members" { + // 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 personType == "contacts" { + } 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") } - sqlResult := db.Raw(sqlQuery, sqlParams...).Scan(&results.Results) + // 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 } diff --git a/static/search.js b/static/search.js index 5dbe1ff..34f4ecb 100644 --- a/static/search.js +++ b/static/search.js @@ -17,40 +17,27 @@ $(document).ready(function() { } }); - var json = $("#search-json").val(); - if (json.length > 0) { - var searchData = JSON.parse(json); - for (const [key, value] of Object.entries(searchData)) { - if (key === "advanced") { - if (value) $("#advanced").trigger("click"); - continue; - } + $("#reset-search").on("click", function() { + $("#search-json").val(""); + $("#search-page").val("1"); + $("#search-form").submit(); + }); - if (key === "fields") { - // TODO: gérer les champs suppl. - continue - } + $("#search-container").on("change", ".field-select", function() { + var lastField = $(this).attr("data-last-field"); + var isInitial = $(this).attr("data-is-initial"); + var initialValue = $(this).attr("data-initial-value"); - if (typeof value === "boolean") { - $("[data-search-field=" + key + "]").prop("checked", value); - } else { - $("[data-search-field=" + key + "]").val(value); - } - } - } - - createFieldSelection(); - $("body").on("change", ".search-fields-count .field-select", function() { - var lastfield = $(this).attr("data-last-field"); var field = $(this).val(); if (field == "") { $(this).parents(".row").remove(); return; } - if (lastfield.length === 0) { + if ((lastField === undefined || lastField.length === 0) && (isInitial === undefined || isInitial !== "yes")) { createFieldSelection(); } + $(this).attr("data-last-field", field); $(this).parents(".row").find(".end-col").find("input").remove(); @@ -69,7 +56,8 @@ $(document).ready(function() { if (data.FieldType === "list") { var select = $("", { class: "form-control", type: "number", - "data-optional-field": field + "data-optional-field": field, + "data-search-type": "int" })); } else if (data.FieldType === "date") { $(elem).parents(".row").find(".end-col").append($("", { @@ -98,21 +87,62 @@ $(document).ready(function() { "data-optional-field": field })); } + + if (initialValue !== undefined && initialValue.length > 0) { + $(elem).parents(".row").find("[data-optional-field]").val(initialValue); + $(elem).attr("data-initial-value", null); + } }); }); + + $("#search-container").on("keyup", "input[type=text]", function(e) { + if (e.keyCode === 13) { + search(); + } + }); + + var json = $("#search-json").val(); + if (json.length > 0) { + var searchData = JSON.parse(json); + + for (const [key, value] of Object.entries(searchData)) { + if (key === "advanced") { + if (value) $("#advanced").trigger("click"); + continue; + } + + if (key === "fields") { + for (const [field_id, field_values] of Object.entries(value)) { + for (const field_value of field_values) { + createFieldSelection(field_id, field_value); + } + } + continue + } + + if (typeof value === "boolean") { + $("[data-search-field=" + key + "]").prop("checked", value); + } else { + $("[data-search-field=" + key + "]").val(value); + } + } + } + + createFieldSelection(); }); -function createFieldSelection() { - const uuid = crypto.randomUUID(); - +function createFieldSelection(field, value) { var elem = $("#search-fields-model").clone().appendTo("#search-fields"); - $(elem).attr("id", "search-field-" + uuid); - //$(elem).find(".start-col").find("label").attr("for", "search-field-field-" + uuid); - //$(elem).find(".start-col").find("select").attr("id", "search-field-field-" + uuid); - //$(elem).find(".end-col").find("label").attr("for", "search-field-value-" + uuid); - //$(elem).find(".end-col").find("input").attr("id", "search-field-value-" + uuid); + $(elem).attr("id", null); $(elem).removeClass("d-none"); $(elem).addClass("search-fields-count"); + + if (field !== undefined && field.length > 0) { + $(elem).find(".field-select").attr("data-initial-value", value); + $(elem).find(".field-select").attr("data-is-initial", "yes"); + $(elem).find(".field-select").val(field); + $(elem).find(".field-select").trigger("change"); + } } function search() { @@ -130,7 +160,10 @@ function search() { var index = $(this).data("search-field"); var value = $(this).val(); - if ($(this).attr("type") == "checkbox") { + if ($(this).data("search-type") === "int") { + value = parseInt(value); + if (isNaN(value)) return; + } else if ($(this).attr("type") === "checkbox") { value = $(this).prop("checked"); } @@ -142,12 +175,21 @@ function search() { var index = $(this).attr("data-optional-field"); var value = $(this).val(); - fields[index] = value; + if ($(this).data("search-type") === "int") { + value = parseInt(value); + if (isNaN(value)) return; + } + + if (fields[index] !== undefined) { + fields[index].push(value); + } else { + fields[index] = [value]; + } }); searchData["fields"] = fields; var json = JSON.stringify(searchData); $("#search-json").val(json); - console.log(searchData); - //$("#search-form").submit(); + $("#search-page").val("1"); + $("#search-form").submit(); } \ No newline at end of file diff --git a/views/people.html b/views/people.html index f6323af..1eeacd5 100644 --- a/views/people.html +++ b/views/people.html @@ -44,7 +44,7 @@ {% endif %} -
    +
    @@ -55,7 +55,7 @@
    - {% for Section in Sections %}
    - +
    From 04bd096019eefa947571dfb3caea4693c41c145f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?William=20Bouzour=C3=A8ne?= Date: Wed, 26 Mar 2025 18:41:23 +0100 Subject: [PATCH 6/6] New search for members + contacts on list + csv export --- controllers/contacts.go | 237 ++++++++++--------------------------- controllers/members.go | 99 ++++++---------- helpers/database/people.go | 28 ++--- static/search.js | 4 + views/people.html | 36 +++--- 5 files changed, 133 insertions(+), 271 deletions(-) diff --git a/controllers/contacts.go b/controllers/contacts.go index f5af97f..c49386f 100644 --- a/controllers/contacts.go +++ b/controllers/contacts.go @@ -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), ¶ms) + 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( - §ions, - "contains_contacts = ? AND id IN ?", - true, sqlFilterSections, - ) + db.Order("name collate nocase asc").Find(§ions, "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,75 +129,37 @@ 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), ¶ms) + 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(§ions, "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) { - 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 - } + results, err := database.PeopleSearch(params) + if err != nil { + return err } var fields []models.Field @@ -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) diff --git a/controllers/members.go b/controllers/members.go index f39d690..b42d472 100644 --- a/controllers/members.go +++ b/controllers/members.go @@ -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(§ions, "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,75 +144,37 @@ 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), ¶ms) + 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(§ions, "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) { - 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 - } + results, err := database.PeopleSearch(params) + if err != nil { + return err } var fields []models.Field @@ -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) diff --git a/helpers/database/people.go b/helpers/database/people.go index 779b97b..ef4d80e 100644 --- a/helpers/database/people.go +++ b/helpers/database/people.go @@ -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 { diff --git a/static/search.js b/static/search.js index 34f4ecb..825bfac 100644 --- a/static/search.js +++ b/static/search.js @@ -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(); diff --git a/views/people.html b/views/people.html index 1eeacd5..292734b 100644 --- a/views/people.html +++ b/views/people.html @@ -67,23 +67,27 @@
    -
    - - {% if MembersPage %} - - {% else %} - - {% endif %} -
    + {% if PermShow %} +
    + + {% if MembersPage %} + + {% else %} + + {% endif %} +
    + {% endif %} -
    - - {% if MembersPage %} - - {% else %} - - {% endif %} -
    + {% if PermShowArchived %} +
    + + {% if MembersPage %} + + {% else %} + + {% endif %} +
    + {% endif %}