More work on the new people search
This commit is contained in:
parent
fac4e695fc
commit
e6eec0dfaf
6 changed files with 337 additions and 121 deletions
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
75
helpers/database/people.go
Normal file
75
helpers/database/people.go
Normal 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
|
||||
}
|
||||
3
main.go
3
main.go
|
|
@ -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)
|
||||
|
|
|
|||
105
static/search.js
105
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($("<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");
|
||||
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();
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue