diff --git a/README.md b/README.md index a6da0fe..fd60b9d 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,11 @@ ## À faire +- traduire le message Rodauth « Please login to continue » ; - limiter le spam sur `/contact/send` ; -- développer les demandes d'adhésion et leur examen par le Noyau ; -- développer l'espace personnel des membres ; - afficher les recettes et les dépenses sur la page d'accueil ; -- gérer les accès aux services selon la situation des membres. +- gérer les accès aux services selon la situation des membres ; +- traiter les demandes de démission et de suppression des comptes. ## Développement @@ -85,6 +85,38 @@ Les personnes physiques et les personnes morales peuvent créer un compte. La création du compte crée également une fiche dans `members`. L'adresse électronique doit être vérifiée avant de pouvoir se connecter. +### Espaces utilisateurs + +Après avoir vérifié son adresse électronique, chaque personne connectée +accède à son espace personnel : + +```text +http://[::1]:9292/account +``` + +Cet espace permet notamment de consulter sa situation d'adhésion et +de formuler une demande d'adhésion à l'association. + +Les membres du Noyau disposent également d'un accès à la gestion de +l'association : + +```text +http://[::1]:9292/noyau +``` + +Cet espace permet notamment de consulter les demandes d'adhésion, +de les accepter ou de les refuser, et de consulter les comptes +de l'association. + +Les demandes en attente sont accessibles à l'adresse : + +```text +http://[::1]:9292/noyau/memberships +``` + +La consultation et la gestion de l'association sont réservées +aux membres du Noyau. + ### Initialisation du Noyau Pour ajouter une personne au Noyau, utiliser le script : @@ -93,17 +125,22 @@ Pour ajouter une personne au Noyau, utiliser le script : bundle exec ruby bin/add-core-member adresse@example.net ``` -Le compte doit exister, son adresse électronique doit être vérifiée et -la fiche membre doit correspondre à une personne physique. Une adhésion -au statut `accepted` doit également déjà exister dans `memberships`. +Le compte doit exister, son adresse électronique doit être vérifiée +et la fiche membre doit correspondre à une personne physique. +Une adhésion au statut `accepted` doit également déjà exister +dans `memberships`. Le script ne crée ni le compte ni l'adhésion. Il attribue uniquement la qualité de membre du Noyau à une fiche membre qui remplit ces conditions. Il doit être exécuté séparément pour chaque personne. -La gestion des demandes d'adhésion n'est pas encore disponible dans -l'interface. Une adhésion acceptée doit donc être enregistrée dans la -base avant l'utilisation de ce script. +Les demandes d'adhésion sont formulées depuis l'espace personnel +(`/account`) et examinées par le Noyau depuis l'espace de gestion +de l'association (`/noyau/memberships`). + +Pour constituer le premier Noyau, les adhésions des personnes +désignées doivent être enregistrées comme acceptées avant +l'utilisation du script. Les statuts prévoient un Noyau composé d'au moins trois personnes physiques. diff --git a/public/css/style.css b/public/css/style.css index 9476521..393dfcf 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -621,6 +621,77 @@ summary:focus-visible { vertical-align: top; } +/* Demandes d'adhésion +------------------------------------------------------------------------ */ + +.table-wrapper { + width: 100%; + margin-block: 1.5rem; +} + +.memberships-table { + width: 100%; + table-layout: fixed; + font-size: 1rem; +} + +.membership-col-identity { + width: 35%; +} + +.membership-col-email { + width: 23%; +} + +.membership-col-date { + width: 17%; +} + +.membership-col-actions { + width: 25%; +} + +.memberships-table th, +.memberships-table td { + padding: 10px 8px; + white-space: nowrap; + vertical-align: middle; +} + +.memberships-table td strong, +.membership-kind { + display: inline; +} + +.membership-kind { + margin-left: 0.25rem; + color: var(--color-link); + font-size: 0.85rem; +} + +.membership-date { + font-size: 0.9rem; +} + +.membership-actions { + display: flex; + flex-wrap: nowrap; + align-items: center; + gap: 0.5rem; +} + +.membership-actions form { + flex: 0 0 auto; + margin: 0; +} + +.membership-actions .button { + padding: 8px 10px; + font-size: 1rem; + line-height: 1.2; + white-space: nowrap; +} + /* Pied de page ------------------------------------------------------------------------ */ @@ -679,6 +750,68 @@ footer { } } +/* Adaptations pour les écrans intermédiaires et petits +------------------------------------------------------------------------ */ + +@media screen and (max-width: 1100px) { + .memberships-table, + .memberships-table tbody { + display: block; + } + + .memberships-table thead { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + } + + .memberships-table tbody { + display: grid; + gap: 1rem; + } + + .memberships-table tr { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.5rem 1rem; + padding: 1rem; + border: 1px solid var(--color-shadow-light); + background: var(--color-surface-dark); + } + + .memberships-table td { + display: block; + min-width: 0; + padding: 0.35rem 0; + border: 0; + white-space: normal; + overflow-wrap: anywhere; + } + + .memberships-table td::before { + display: block; + margin-bottom: 0.25rem; + color: var(--color-highlight); + font-weight: 700; + content: attr(data-label); + } + + .memberships-table td[data-label="Décision"] { + grid-column: 1 / -1; + } + + .membership-kind { + font-size: 0.9rem; + } + + .membership-actions { + flex-wrap: wrap; + } +} + /* Adaptations pour les petits écrans ------------------------------------------------------------------------ */ @@ -693,6 +826,22 @@ footer { padding-bottom: 60px; } + .memberships-table tr { + grid-template-columns: minmax(0, 1fr); + } + + .memberships-table td[data-label="Décision"] { + grid-column: auto; + } + + .membership-actions form { + flex: 1 1 auto; + } + + .membership-actions .button { + width: 100%; + } + footer { padding: 24px 20px; } diff --git a/routes/noyau.rb b/routes/noyau.rb index 2fc9271..2fde317 100644 --- a/routes/noyau.rb +++ b/routes/noyau.rb @@ -1,21 +1,106 @@ class Netig + private + + # Enregistrement d'une décision d'adhésion + def record_membership_decision!(r, membership_id, status) + member_id = DB[:members] + .where(account_id: rodauth.session_value) + .get(:id) + + unless member_id + r.halt [403, {}, ["Accès interdit."]] + end + + message = case status + when "accepted" + "La demande d'adhésion a été acceptée." + when "rejected" + "La demande d'adhésion a été refusée." + end + + updated = DB[:memberships] + .where(id: membership_id, status: "pending") + .update( + status: status, + decided_at: Sequel::CURRENT_TIMESTAMP, + recorded_by: member_id + ) + + if updated == 1 + flash["notice"] = message + else + flash["error"] = "Cette demande n'est plus en attente ou n'existe pas." + end + + r.redirect "/noyau/memberships" + end + hash_branch "noyau" do |r| require_core_member!(r) - @page_title = "Noyau — NETig" - @page_description = "Espace de gestion de l'association NETig." + @page_title = "Gestion de l'association — NETig" + @page_description = "Gestion collective de l'association NETig par le Noyau." @canonical_url = "https://netig.net/noyau" + # Accueil r.is do r.get do view "noyau" end end + # Demandes d'adhésion + r.on "memberships" do + r.is do + r.get do + @page_title = "Demandes d'adhésion — NETig" + @page_description = "Examen des demandes d'adhésion à NETig." + @canonical_url = "https://netig.net/noyau/memberships" + + @memberships = DB[:memberships] + .join(:members, id: :member_id) + .join(:accounts, id: Sequel[:members][:account_id]) + .where( + Sequel[:memberships][:status] => "pending", + Sequel[:accounts][:status_id] => 2 + ) + .select_all(:memberships) + .select_append( + Sequel[:members][:kind].as(:member_kind), + Sequel[:members][:first_name].as(:member_first_name), + Sequel[:members][:last_name].as(:member_last_name), + Sequel[:members][:organization_name].as(:member_organization_name), + Sequel[:accounts][:email].as(:account_email) + ) + .order( + Sequel.asc(Sequel[:memberships][:requested_at]), + Sequel.asc(Sequel[:memberships][:id]) + ) + .all + + view "noyau/memberships" + end + end + + # Décision sur une demande + r.on Integer do |membership_id| + r.post "accept" do + record_membership_decision!(r, membership_id, "accepted") + end + + r.post "reject" do + record_membership_decision!(r, membership_id, "rejected") + end + end + end + + # Gestion des comptes r.on "members" do r.is do r.get do - @page_title = "Comptes — Noyau — NETig" + @page_title = "Comptes — NETig" + @page_description = "Consultation des comptes de l'association NETig." + @canonical_url = "https://netig.net/noyau/members" @members = DB[:members] .join(:accounts, id: :account_id) diff --git a/views/account.erb b/views/account.erb index 6605e26..ef60a4f 100644 --- a/views/account.erb +++ b/views/account.erb @@ -36,7 +36,7 @@ <% if @membership.nil? || @membership[:status] == "rejected" %>
- <%== rodauth.csrf_tag %> + <%== rodauth.csrf_tag("/account/membership") %> diff --git a/views/layout.erb b/views/layout.erb index fdf26e3..b1ba225 100644 --- a/views/layout.erb +++ b/views/layout.erb @@ -49,17 +49,20 @@ Déconnexion
+ <% elsif request.path.start_with?("/noyau/") %> + + Gestion de l'association ➜ + <% else %> - Mon espace → + Mon espace ➜ <% end %> <% elsif request.path != rodauth.login_path %> - Mon espace → + Mon espace ➜ <% end %> - diff --git a/views/noyau.erb b/views/noyau.erb index db70eea..1662232 100644 --- a/views/noyau.erb +++ b/views/noyau.erb @@ -1,11 +1,33 @@
-
-

Espace du Noyau

+
+

Gestion de l'association

- - Gérer les adhérents - + Gestion collective de l'association par le Noyau.

+ +
+

Adhésions

+ +

+ Consultez et examinez les demandes d'adhésion à NETig. +

+ + + Demandes d'adhésion + +
+ +
+

Comptes

+ +

+ Consultez les comptes et les informations des membres. +

+ + + Consulter les comptes + +
diff --git a/views/noyau/memberships.erb b/views/noyau/memberships.erb new file mode 100644 index 0000000..b8c35ea --- /dev/null +++ b/views/noyau/memberships.erb @@ -0,0 +1,96 @@ +
+
+

Demandes d'adhésion

+ + <% if flash["notice"] || flash["error"] %> +
+ <% if notice = flash["notice"] %> +

<%= notice %>

+ <% end %> + + <% if error = flash["error"] %> + + <% end %> +
+ <% end %> + +

+ Consultez les demandes d'adhésion en attente d'examen + par le Noyau. +

+ + <% if @memberships.empty? %> +

Aucune demande d'adhésion en attente.

+ <% else %> +
+ + + + + + + + + + + + + + + + + + + <% @memberships.each do |membership| %> + + + + + + + + + + <% end %> + +
IdentitéEmailDemandeDécision
+ <% if membership[:member_kind] == "individual" %> + <%= "#{membership[:member_first_name]} #{membership[:member_last_name]}" %> + · Personne physique + <% else %> + <%= membership[:member_organization_name] %> + · Personne morale + <% end %> + + <%= membership[:account_email] %> + + + +
+
+ <%== rodauth.csrf_tag("/noyau/memberships/#{membership[:id]}/accept") %> + +
+ +
+ <%== rodauth.csrf_tag("/noyau/memberships/#{membership[:id]}/reject") %> + +
+
+
+
+ <% end %> +
+