More work on the new people search

This commit is contained in:
William Bouzourène 2025-03-25 21:55:54 +01:00
parent fac4e695fc
commit e6eec0dfaf
6 changed files with 337 additions and 121 deletions

View file

@ -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)
}

View file

@ -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(
&sections,
"contains_members = ? AND id IN ?",
true, sqlFilterSections,
)
)*/
searchJSON := c.Query("s")
var params database.PeopleSearchParams
if len(searchJSON) > 0 {
err = json.Unmarshal([]byte(searchJSON), &params)
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(&sections, "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,
})
}

View file

@ -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
}

View file

@ -167,6 +167,9 @@ func main() {
app.Post("/contacts/:id<int;min(0)>/restore", controllers.ContactRestore)
app.Post("/contacts/:id<int;min(0)>/purge", controllers.ContactPurge)
// Fields
app.Get("/fields/:id<int;min(0)>", controllers.FieldJSON)
// Account manage
app.Get("/account/manage", controllers.AccountManage)
app.Post("/account/manage", controllers.AccountManage)

View file

@ -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,15 +31,89 @@ $(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($("<input>", {
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 = $("<select>", {
class: "form-select",
"data-optional-field": field
});
$(elem).parents(".row").find(".end-col").append(select);
for (const [_, value] of Object.entries(data.List.ListItems)) {
$(select).append($("<option>", {
value: value.ID,
text: value.Value
}));
}
} else if (data.FieldType === "number") {
$(elem).parents(".row").find(".end-col").append($("<input>", {
class: "form-control",
type: "number",
"data-optional-field": field
}));
} else if (data.FieldType === "date") {
$(elem).parents(".row").find(".end-col").append($("<input>", {
class: "form-control",
type: "date",
"data-optional-field": field
}));
} else {
$(elem).parents(".row").find(".end-col").append($("<input>", {
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");
@ -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();
}

View file

@ -25,7 +25,7 @@
<i class="bi-plus-lg"></i>
Ajouter
</a>
<a class="btn btn-outline-primary" href="/members/export?se={{ FilterSection }}&s={{ FilterSearch|urlencode }}&a={{ FilterArchive }}">
<a class="btn btn-outline-primary" href="/members/export?s={{ SearchJSON|urlencode }}">
<i class="bi-filetype-csv"></i>
Exporter
</a>
@ -36,7 +36,7 @@
<i class="bi-plus-lg"></i>
Ajouter
</a>
<a class="btn btn-outline-primary" href="/contacts/export?se={{ FilterSection }}&s={{ FilterSearch|urlencode }}&a={{ FilterArchive }}">
<a class="btn btn-outline-primary" href="/contacts/export?s={{ SearchJSON|urlencode }}">
<i class="bi-filetype-csv"></i>
Exporter
</a>
@ -44,22 +44,19 @@
{% endif %}
</div>
<div class="mt-3 row">
<div class="h-100 p-4 mb-3 bg-body-tertiary border rounded-3">
<div class="row">
<div class="col-lg-6 mb-3">
<label for="name" class="form-label">
Nom
</label>
<label for="name" class="form-label">Nom et prénom</label>
<input type="text" class="form-control" id="name" data-search-field="name" data-search-advanced="false" name="name">
</div>
<div class="col-sm-6 col-lg-3 mb-3">
<label for="section" class="form-label">
Section
</label>
<label for="section" class="form-label">Section</label>
<select class="form-select" id="section" data-search-field="section" data-search-advanced="false" name="section">
<option value="">Choisir...</option>
<option value=""></option>
{% for Section in Sections %}
<option value="{{ Section.ID }}">
{{ Section.Name }}
@ -68,81 +65,102 @@
</select>
</div>
<div class="col-sm-6 col-lg-3 mb-2">
<div class="col-sm-6 col-lg-3 mb-3 pt-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" data-search-field="active" data-search-advanced="false" id="active" checked>
{% if MembersPage %}
<label class="form-check-label" for="active">Afficher membres actifs</label>
{% else %}
<label class="form-check-label" for="active">Afficher contactss actifs</label>
{% endif %}
</div>
<label class="form-label">
Status
</label>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" data-search-field="archive" data-search-advanced="false" id="archive">
<label class="form-check-label" for="archive">Archivé</label>
{% if MembersPage %}
<label class="form-check-label" for="archive">Afficher membres archivés</label>
{% else %}
<label class="form-check-label" for="archive">Afficher contacts archivés</label>
{% endif %}
</div>
</div>
</div>
<div class="mb-3 d-none" id="advanced-section">
<div class="d-none" id="advanced-section">
<div class="row">
<div class="col-lg-6 mb-3">
<label for="email" class="form-label">
Email
</label>
<label for="email" class="form-label">Email</label>
<input type="text" class="form-control" id="email" data-search-field="email" data-search-advanced="true" name="email">
</div>
<div class="col-lg-6 mb-3">
<label for="phone" class="form-label">
Téléphone
</label>
<label for="phone" class="form-label">Téléphone</label>
<input type="text" class="form-control" id="phone" data-search-field="phone" data-search-advanced="true" name="phone">
</div>
<div class="col-lg-6 mb-3">
<label for="address" class="form-label">
Adresse
</label>
<label for="address" class="form-label">Adresse</label>
<input type="text" class="form-control" id="address" data-search-field="address" data-search-advanced="true" name="address">
</div>
<div class="col-sm-4 col-lg-2 mb-3">
<label for="postal_code" class="form-label">
Code postal
</label>
<label for="postal_code" class="form-label">Code postal</label>
<input type="text" class="form-control" id="postal_code" data-search-field="postal_code" data-search-advanced="true" name="postal_code">
</div>
<div class="col-sm-8 col-lg-4 mb-3">
<label for="city" class="form-label">
Lieu
</label>
<label for="city" class="form-label">Lieu</label>
<input type="text" class="form-control" id="city" data-search-field="city" data-search-advanced="true" name="city">
</div>
</div>
<div id="search-fields-model" class="row d-none">
<div class="start-col col-lg-6 mb-3">
<label class="form-label">Champ supplémentaire</label>
<select class="form-select field-select" data-last-field="">
<option value=""></option>
{% for Field in Fields %}
<option value="{{ Field.ID }}">
{{ Field.Name }}
</option>
{% endfor %}
</select>
</div>
<div class="end-col col-lg-6 mb-3">
<label class="form-label">Valeur</label>
<input type="text" class="form-control" disabled>
</div>
</div>
<div class="row my-3">
<div id="search-fields" class="mb-3"></div>
</div>
<div class="row mt-2">
<div class="col-6">
<button class="btn btn-outline-primary btn-sm" id="advanced" data-state="false" type="button">
<i class="bi-chevron-double-down me-1"></i> Avancé
<button class="btn btn-outline-primary" id="advanced" data-state="false" type="button">
<i class="bi-arrow-down me-1"></i> Avancé
</button>
</div>
<div class="col-6 text-end">
<button class="btn btn-outline-success btn-sm" id="search" type="button">
<button class="btn btn-outline-success" id="search" type="button">
<i class="bi-search me-1"></i> Recherche
</button>
</div>
</div>
<form id="search-form" method="get" class="my-3 d-none">
<form id="search-form" method="get" class="d-none">
<input type="hidden" name="p" value="{{ Pagination.CurrentPage }}">
<input type="hidden" id="search-json" name="s" value="{{ FilterSearch }}">
<input type="hidden" id="search-json" name="s" value="{{ SearchJSON }}">
</form>
</div>
<div class="table-responsive">
<table class="table">
@ -203,7 +221,7 @@
<li class="page-item">
<a
class="page-link"
href="?se={{ FilterSection }}&s={{ FilterSearch|urlencode }}&a={{ FilterArchive }}&p=1"
href="?p=1&s={{ SearchJSON|urlencode }}"
>
<i class="bi-rewind"></i>
</a>
@ -211,7 +229,7 @@
<li class="page-item">
<a
class="page-link"
href="?se={{ FilterSection }}&s={{ FilterSearch|urlencode }}&a={{ FilterArchive }}&p={{ Pagination.CurrentPage - 1 }}"
href="?&p={{ Pagination.CurrentPage - 1 }}&s={{ SearchJSON|urlencode }}"
>
<i class="bi-caret-left"></i>
</a>
@ -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 }}
</a>
@ -249,7 +267,7 @@
<li class="page-item">
<a
class="page-link"
href="?se={{ FilterSection }}&s={{ FilterSearch|urlencode }}&a={{ FilterArchive }}&p={{ Pagination.CurrentPage + 1 }}"
href="?&p={{ Pagination.CurrentPage + 1 }}&s={{ SearchJSON|urlencode }}"
>
<i class="bi-caret-right"></i>
</a>
@ -257,7 +275,7 @@
<li class="page-item">
<a
class="page-link"
href="?se={{ FilterSection }}&s={{ FilterSearch|urlencode }}&a={{ FilterArchive }}&p={{ Pagination.MaxPages }}"
href="?p={{ Pagination.MaxPages }}&s={{ SearchJSON|urlencode }}"
>
<i class="bi-fast-forward"></i>
</a>