4 * Copyright © 2003-2024 The Galette Team
6 * This file is part of Galette (https://galette.eu).
8 * Galette is free software: you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation, either version 3 of the License, or
11 * (at your option) any later version.
13 * Galette is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
18 * You should have received a copy of the GNU General Public License
19 * along with Galette. If not, see <http://www.gnu.org/licenses/>.
22 namespace Galette\Controllers\Crud
;
24 use Galette\Controllers\CrudController
;
25 use Galette\DynamicFields\Boolean
;
26 use Galette\Features\BatchList
;
27 use Slim\Psr7\Request
;
28 use Slim\Psr7\Response
;
29 use Galette\Core\GaletteMail
;
30 use Galette\Core\Gaptcha
;
31 use Galette\Entity\Adherent
;
32 use Galette\Entity\Contribution
;
33 use Galette\Entity\ContributionsTypes
;
34 use Galette\Entity\DynamicFieldsHandle
;
35 use Galette\Entity\Group
;
36 use Galette\Entity\Status
;
37 use Galette\Entity\FieldsConfig
;
38 use Galette\Entity\Social
;
39 use Galette\Filters\AdvancedMembersList
;
40 use Galette\Filters\MembersList
;
41 use Galette\Repository\Groups
;
42 use Galette\Repository\Members
;
43 use Galette\Repository\PaymentTypes
;
44 use Galette\Repository\Titles
;
48 * Galette members controller
50 * @author Johan Cwiklinski <johan@x-tnd.be>
53 class MembersController
extends CrudController
58 private bool $is_self_membership = false;
65 * @param Request $request PSR Request
66 * @param Response $response PSR Response
70 public function add(Request
$request, Response
$response): Response
72 return $this->edit($request, $response, null, 'add');
78 * @param Request $request PSR Request
79 * @param Response $response PSR Response
83 public function addChild(Request
$request, Response
$response): Response
85 return $this->edit($request, $response, null, 'addchild');
89 * Self subscription page
91 * @param Request $request PSR Request
92 * @param Response $response PSR Response
96 public function selfSubscribe(Request
$request, Response
$response): Response
98 if (!$this->preferences
->pref_bool_selfsubscribe ||
$this->login
->isLogged()) {
101 ->withHeader('Location', $this->routeparser
->urlFor('slash'));
104 if ($this->session
->member
!== null) {
105 $member = $this->session
->member
;
106 $this->session
->member
= null;
108 $member = new Adherent($this->zdb
);
109 $member->enableDep('dynamics');
112 //mark as self membership
113 $member->setSelfMembership();
115 // flagging required fields
116 $fc = $this->fields_config
;
117 $form_elements = $fc->getFormElements($this->login
, true, true);
121 $members = $m->getDropdownMembers(
124 $member->hasParent() ?
$member->parent
->id
: null
129 'filters' => $m->getFilters(),
130 'count' => $m->getCount()
134 if (count($members)) {
135 $params['members']['list'] = $members;
138 $gaptcha = new Gaptcha($this->i18n
);
139 $this->session
->gaptcha
= $gaptcha;
141 $titles = new Titles($this->zdb
);
146 'pages/member_form.html.twig',
148 'page_title' => _T("Subscription"),
149 'parent_tpl' => 'public_page.html.twig',
152 'autocomplete' => true,
153 'osocials' => new Social($this->zdb
),
156 'titles_list' => $titles->getList(),
157 'fieldsets' => $form_elements['fieldsets'],
158 'hidden_elements' => $form_elements['hiddens'],
160 'gaptcha' => $gaptcha
169 * @param Request $request PSR Request
170 * @param Response $response PSR Response
174 public function doAdd(Request
$request, Response
$response): Response
176 return $this->store($request, $response);
180 * Self subscription add action
182 * @param Request $request PSR Request
183 * @param Response $response PSR Response
187 public function doSelfSubscribe(Request
$request, Response
$response): Response
189 $this->setSelfMembership();
190 return $this->doAdd($request, $response);
197 * @param Request $request PSR Request
198 * @param Response $response PSR Response
199 * @param integer $id_adh Member ID to duplicate
203 public function duplicate(Request
$request, Response
$response, int $id_adh): Response
205 $adh = new Adherent($this->zdb
, $id_adh, ['dynamics' => true, 'parent' => true]);
206 $adh->setDuplicate();
208 //store entity in session
209 $this->session
->member
= $adh;
213 ->withHeader('Location', $this->routeparser
->urlFor('addMember'));
220 * Display member card
222 * @param Request $request PSR Request
223 * @param Response $response PSR Response
224 * @param integer $id Member ID
228 public function show(Request
$request, Response
$response, int $id): Response
230 $member = new Adherent($this->zdb
);
235 if (!$member->canShow($this->login
)) {
236 $this->flash
->addMessage(
238 _T("You do not have permission for requested URL.")
245 $this->routeparser
->urlFor('me')
249 if ($member->id
== null) {
250 //member does not exist!
251 $this->flash
->addMessage(
253 str_replace('%id', (string)$id, _T("No member #%id."))
259 $this->routeparser
->urlFor('slash')
263 // flagging fields visibility
264 $fc = $this->fields_config
;
265 $display_elements = $fc->getDisplayElements($this->login
);
270 'pages/member_show.html.twig',
272 'page_title' => _T("Member Profile"),
274 'pref_lang' => $this->i18n
->getNameFromId($member->language
),
275 'pref_card_self' => $this->preferences
->pref_card_self
,
276 'groups' => Groups
::getSimpleList(),
278 'display_elements' => $display_elements,
279 'osocials' => new Social($this->zdb
),
280 'navigate' => $this->handleNavigationLinks($member->id
)
289 * @param Request $request PSR Request
290 * @param Response $response PSR Response
294 public function showMe(Request
$request, Response
$response): Response
296 if ($this->login
->isSuperAdmin()) {
299 ->withHeader('Location', $this->routeparser
->urlFor('slash'));
301 return $this->show($request, $response, $this->login
->id
);
305 * Public pages (trombinoscope, public list)
307 * @param Request $request PSR Request
308 * @param Response $response PSR Response
309 * @param string|null $option One of 'page' or 'order'
310 * @param string|integer|null $value Value of the option
311 * @param string|null $type List type (either list or trombi)
315 public function publicList(
318 string $option = null,
319 string|
int $value = null,
322 $varname = $this->getFilterName(['prefix' => 'public', 'suffix' => $type]);
323 if (isset($this->session
->$varname)) {
324 $filters = $this->session
->$varname;
326 $filters = new MembersList();
329 if ($option !== null) {
332 $filters->current_page
= (int)$value;
335 $filters->orderby
= $value;
340 $m = new Members($filters);
341 $members = $m->getPublicList($type === 'trombi');
343 $this->session
->$varname = $filters;
345 //assign pagination variables to the template and add pagination links
346 $filters->setViewPagination($this->routeparser
, $this->view
, false);
351 ($type === 'list' ?
'pages/members_public_list' : 'pages/members_public_gallery') . '.html.twig',
353 'page_title' => ($type === 'list' ?
_T("Members list") : _T('Trombinoscope')),
354 'additionnal_html_class' => ($type === 'list' ?
'' : 'trombinoscope'),
356 'members' => $members,
357 'nb_members' => $m->getCount(),
358 'filters' => $filters,
367 * Public pages (trombinoscope, public list)
369 * @param Request $request PSR Request
370 * @param Response $response PSR Response
371 * @param string $type Type
375 public function filterPublicList(Request
$request, Response
$response, string $type): Response
377 $post = $request->getParsedBody();
379 $varname = $this->getFilterName(['prefix' => 'public', 'suffix' => $type]);
380 if (isset($this->session
->$varname)) {
381 $filters = $this->session
->$varname;
383 $filters = new MembersList();
386 //reintialize filters
387 if (isset($post['clear_filter'])) {
390 //number of rows to show
391 if (isset($post['nbshow'])) {
392 $filters->show
= (int)$post['nbshow'];
396 $this->session
->$varname = $filters;
400 ->withHeader('Location', $this->routeparser
->urlFor('publicList', ['type' => $type]));
406 * @param Request $request PSR Request
407 * @param Response $response PSR Response
408 * @param string|null $option One of 'page' or 'order'
409 * @param integer|string|null $value Value of the option
413 public function list(Request
$request, Response
$response, string $option = null, int|
string $value = null): Response
415 if (isset($this->session
->{$this->getFilterName()})) {
416 $filters = $this->session
->{$this->getFilterName()};
418 $filters = new MembersList();
421 if ($option !== null) {
424 $filters->current_page
= (int)$value;
427 $filters->orderby
= $value;
432 $members = new Members($filters);
434 $members_list = array();
435 if ($this->login
->isAdmin() ||
$this->login
->isStaff()) {
436 $members_list = $members->getMembersList(true);
438 $members_list = $members->getManagedMembersList(true);
441 $groups = new Groups($this->zdb
, $this->login
);
442 $groups_list = $groups->getList();
444 //assign pagination variables to the template and add pagination links
445 $filters->setViewPagination($this->routeparser
, $this->view
, false);
446 $filters->setViewCommonsFilters($this->preferences
, $this->view
);
448 $this->session
->{$this->getFilterName()} = $filters;
453 'pages/members_list.html.twig',
455 'page_title' => _T("Members management"),
456 'require_mass' => true,
457 'members' => $members_list,
458 'filter_groups_options' => $groups_list,
459 'nb_members' => $members->getCount(),
460 'filters' => $filters,
461 'adv_filters' => $filters instanceof AdvancedMembersList
,
462 'galette_list' => $this->lists_config
->getDisplayElements($this->login
)
471 * @param Request $request PSR Request
472 * @param Response $response PSR Response
476 public function filter(Request
$request, Response
$response): Response
478 $post = $request->getParsedBody();
479 $filters = $this->session
->{$this->getFilterName()} ??
new MembersList();
481 //reinitialize filters
482 if (isset($post['clear_filter'])) {
483 $filters = new MembersList();
484 } elseif (isset($post['clear_adv_filter'])) {
485 $this->session
->{$this->getFilterName()} = null;
486 unset($this->session
->{$this->getFilterName()});
490 ->withHeader('Location', $this->routeparser
->urlFor('advanced-search'));
491 } elseif (isset($post['adv_criteria'])) {
494 ->withHeader('Location', $this->routeparser
->urlFor('advanced-search'));
497 if (isset($post['filter_str'])) { //filter search string
498 $filters->filter_str
= stripslashes(
499 htmlspecialchars($post['filter_str'], ENT_QUOTES
)
503 if (isset($post['field_filter'])) {
504 if (is_numeric($post['field_filter'])) {
505 $filters->field_filter
= $post['field_filter'];
508 //membership to filter
509 if (isset($post['membership_filter'])) {
510 if (is_numeric($post['membership_filter'])) {
511 $filters->membership_filter
512 = $post['membership_filter'];
515 //account status to filter
516 if (isset($post['filter_account'])) {
517 if (is_numeric($post['filter_account'])) {
518 $filters->filter_account
= $post['filter_account'];
522 if (isset($post['email_filter'])) {
523 $filters->email_filter
= (int)$post['email_filter'];
527 isset($post['group_filter'])
528 && $post['group_filter'] > 0
530 $filters->group_filter
= (int)$post['group_filter'];
532 //number of rows to show
533 if (isset($post['nbshow'])) {
534 $filters->show
= (int)$post['nbshow'];
537 if (isset($post['advanced_filtering'])) {
538 if (!$filters instanceof AdvancedMembersList
) {
539 $filters = new AdvancedMembersList($filters);
543 unset($post['advanced_filtering']);
545 foreach ($post as $k => $v) {
546 if (strpos($k, 'free_', 0) === 0) {
549 foreach ($post['free_field'] as $f) {
552 && trim($post['free_text'][$i]) !== ''
554 $fs_search = htmlspecialchars($post['free_text'][$i], ENT_QUOTES
);
556 = (int)$post['free_logical_operator'][$i];
558 = (int)$post['free_query_operator'][$i];
559 $type = (int)$post['free_type'][$i];
564 'search' => $fs_search,
568 $filters->free_search
= $fs;
574 } elseif ($k == 'groups_search') {
576 $filters->groups_search_log_op
= (int)$post['groups_logical_operator'];
577 foreach ($post['groups_search'] as $g) {
578 if (trim($g) !== '') {
583 $filters->groups_search
= $gs;
589 case 'contrib_min_amount':
590 case 'contrib_max_amount':
591 if (trim($v) !== '') {
604 if (isset($post['savesearch'])) {
609 $this->routeparser
->urlFor(
616 $this->session
->{$this->getFilterName()} = $filters;
620 ->withHeader('Location', $this->routeparser
->urlFor('members'));
624 * Advanced search page
626 * @param Request $request PSR Request
627 * @param Response $response PSR Response
631 public function advancedSearch(Request
$request, Response
$response): Response
633 if (isset($this->session
->{$this->getFilterName()})) {
634 $filters = $this->session
->{$this->getFilterName()};
635 if (!$filters instanceof AdvancedMembersList
) {
636 $filters = new AdvancedMembersList($filters);
639 $filters = new AdvancedMembersList();
642 $groups = new Groups($this->zdb
, $this->login
);
643 $groups_list = $groups->getList();
645 //we want only visible fields
646 $fields = $this->members_fields
;
647 $fc = $this->fields_config
;
648 $fc->filterVisible($this->login
, $fields);
651 $member = new Adherent($this->zdb
);
654 ->enableDep('dynamics')
655 ->loadFromLoginOrMail($this->login
->login
);
656 $adh_dynamics = new DynamicFieldsHandle($this->zdb
, $this->login
, $member);
658 $contrib = new Contribution($this->zdb
, $this->login
);
659 $contrib_dynamics = new DynamicFieldsHandle($this->zdb
, $this->login
, $contrib);
662 $statuts = new Status($this->zdb
);
664 //Contributions types
665 $ct = new ContributionsTypes($this->zdb
);
668 $ptypes = new PaymentTypes(
673 $ptlist = $ptypes->getList();
675 $filters->setViewCommonsFilters($this->preferences
, $this->view
);
677 $social = new Social($this->zdb
);
678 $types = $member->getMemberRegisteredTypes();
680 foreach ($types as $type) {
681 $social_types[$type] = $social->getSystemType($type);
687 'pages/advanced_search.html.twig',
689 'page_title' => _T("Advanced search"),
690 'filter_groups_options' => $groups_list,
691 'search_fields' => $fields,
692 'adh_dynamics' => $adh_dynamics->getSearchFields(),
693 'contrib_dynamics' => $contrib_dynamics->getSearchFields(),
694 'adh_socials' => $social_types,
695 'statuts' => $statuts->getList(),
696 'contributions_types' => $ct->getList(),
697 'filters' => $filters,
698 'payments_types' => $ptlist
705 * Members list for ajax
707 * @param Request $request PSR Request
708 * @param Response $response PSR Response
709 * @param string|null $option One of 'page' or 'order'
710 * @param string|integer|null $value Value of the option
714 public function ajaxList(Request
$request, Response
$response, string $option = null, string|
int $value = null): Response
716 $post = $request->getParsedBody();
718 $filters = $this->session
->{$this->getFilterName(['prefix' => 'ajax'])} ??
new MembersList();
720 if ($option == 'page') {
721 $filters->current_page
= (int)$value;
724 //numbers of rows to display
725 if (isset($post['nbshow']) && is_numeric($post['nbshow'])) {
726 $filters->show
= (int)$post['nbshow'];
729 $members = new Members($filters);
730 if (!$this->login
->isAdmin() && !$this->login
->isStaff()) {
731 if ($this->login
->isGroupManager()) {
732 $members_list = $members->getManagedMembersList(true);
737 [$this->login
->id
, $this->login
->login
],
738 'Trying to list group members without access from #%id (%login)'
742 throw new \
Exception('Access denied.');
745 $members_list = $members->getMembersList(true);
748 //assign pagination variables to the template and add pagination links
749 $filters->setViewPagination($this->routeparser
, $this->view
, false);
751 $this->session
->{$this->getFilterName(['prefix' => 'ajax'])} = $filters;
753 $selected_members = null;
754 $unreachables_members = null;
755 if (!isset($post['from'])) {
756 $mailing = $this->session
->mailing
;
757 if (!isset($post['members'])) {
758 $selected_members = $mailing->recipients
;
759 $unreachables_members = $mailing->unreachables
;
762 $selected_members = $m->getArrayList($post['members']);
763 if (isset($post['unreachables']) && is_array($post['unreachables'])) {
764 $unreachables_members = $m->getArrayList($post['unreachables']);
768 switch ($post['from']) {
770 if (!isset($post['gid'])) {
772 'Trying to list group members with no group id provided',
775 throw new \
Exception('A group id is required.');
777 if (!isset($post['members'])) {
778 $group = new Group((int)$post['gid']);
779 $selected_members = array();
780 if (!isset($post['mode']) ||
$post['mode'] == 'members') {
781 $selected_members = $group->getMembers();
782 } elseif ($post['mode'] == 'managers') {
783 $selected_members = $group->getManagers();
786 'Trying to list group members with unknown mode',
789 throw new \
Exception('Unknown mode.');
793 $selected_members = $m->getArrayList($post['members']);
794 if (isset($post['unreachables']) && is_array($post['unreachables'])) {
795 $unreachables_members = $m->getArrayList($post['unreachables']);
800 if (!isset($post['id_adh'])) {
801 throw new \
RuntimeException(
802 'Current selected member must be excluded while attaching!'
810 'filters' => $filters,
811 'members_list' => $members_list,
812 'selected_members' => $selected_members,
813 'unreachables_members' => $unreachables_members
816 if (isset($post['multiple'])) {
817 $params['multiple'] = true;
820 if (isset($post['gid'])) {
821 $params['the_id'] = (int)$post['gid'];
824 if (isset($post['id_adh'])) {
825 $params['excluded'] = (int)$post['id_adh'];
831 'elements/ajax_members.html.twig',
838 * Batch actions handler
840 * @param Request $request PSR Request
841 * @param Response $response PSR Response
845 public function handleBatch(Request
$request, Response
$response): Response
847 $post = $request->getParsedBody();
849 if (isset($post['entries_sel'])) {
850 if (isset($this->session
->{$this->getFilterName()})) {
851 $filters = $this->session
->{$this->getFilterName()};
853 $filters = new MembersList();
856 $filters->selected
= $post['entries_sel'];
858 'cards' => 'pdf-members-cards',
859 'labels' => 'pdf-members-labels',
860 'sendmail' => 'mailing',
861 'attendance_sheet' => 'attendance_sheet_details',
862 'csv' => 'csv-memberslist',
863 'delete' => 'removeMembers',
864 'masschange' => 'masschangeMembers',
865 'masscontributions' => 'massAddContributionsChooseType'
868 foreach ($knowns as $known => $redirect_url) {
869 if (isset($post[$known])) {
870 $this->session
->{$this->getFilterName(['suffix' => $known])} = $filters;
871 $redirect_url = $this->routeparser
->urlFor($redirect_url);
872 if ($known === 'sendmail') {
873 $redirect_url .= '?mailing_new=new';
877 ->withHeader('Location', $redirect_url);
881 throw new \
RuntimeException('Does not know what to batch :(');
883 $this->flash
->addMessage(
885 _T("No member was selected, please check at least one name.")
890 ->withHeader('Location', $this->routeparser
->urlFor('members'));
900 * @param Request $request PSR Request
901 * @param Response $response PSR Response
902 * @param ?integer $id Member id/array of members id
903 * @param string $action null or 'add'
907 public function edit(
911 string $action = 'edit'
913 //instantiate member object
914 $member = new Adherent($this->zdb
);
916 if ($this->session
->member
!== null) {
917 //retrieve from session, in add or edit
918 $member = $this->session
->member
;
919 $this->session
->member
= null;
922 $member->enableAllDeps();
925 //load requested member
927 $can = $member->canEdit($this->login
);
929 $can = $member->canCreate($this->login
);
933 $this->flash
->addMessage(
935 _T("You do not have permission for requested URL.")
941 $this->routeparser
->urlFor('me')
945 //if adding a child, force parent here
946 if ($action === 'addchild') {
947 $member->setParent((int)$this->login
->id
);
950 // flagging required fields
951 $fc = $this->fields_config
;
953 // password required if we create a new member
955 $fc->setNotRequired('mdp_adh');
958 //handle requirements for parent fields
959 $parent_fields = $member->getParentFields();
960 $tpl_parent_fields = []; //for JS when detaching
961 foreach ($parent_fields as $field) {
962 if ($fc->isRequired($field)) {
963 $tpl_parent_fields[] = $field;
964 if ($member->hasParent()) {
965 $fc->setNotRequired($field);
970 // flagging required fields invisible to members
971 if ($this->login
->isAdmin() ||
$this->login
->isStaff()) {
972 $fc->setNotRequired('activite_adh');
973 $fc->setNotRequired('id_statut');
976 // template variable declaration
977 $title = _T("Member Profile");
978 if ($member->id
!= '') {
979 $title .= ' (' . _T("modification") . ')';
981 $title .= ' (' . _T("creation") . ')';
985 $statuts = new Status($this->zdb
);
987 $titles = new Titles($this->zdb
);
990 $groups = new Groups($this->zdb
, $this->login
);
991 $groups_list = $groups->getSimpleList(true);
993 $form_elements = $fc->getFormElements(
1001 if ($member->hasParent()) {
1002 $pid = ($member->parent
instanceof Adherent ?
$member->parent
->id
: $member->parent
);
1004 $members = $m->getDropdownMembers(
1010 $route_params['members'] = [
1011 'filters' => $m->getFilters(),
1012 'count' => $m->getCount()
1015 if (count($members)) {
1016 $route_params['members']['list'] = $members;
1019 if ($action === 'edit') {
1020 $route_params['navigate'] = $this->handleNavigationLinks($member->id
);
1024 $this->view
->render(
1026 'pages/member_form.html.twig',
1028 'parent_tpl' => 'page.html.twig',
1029 'autocomplete' => true,
1030 'page_title' => $title,
1031 'member' => $member,
1032 'self_adh' => false,
1033 // pseudo random int
1035 'titles_list' => $titles->getList(),
1036 'statuts' => $statuts->getList(),
1037 'groups' => $groups_list,
1038 'fieldsets' => $form_elements['fieldsets'],
1039 'hidden_elements' => $form_elements['hiddens'],
1040 'parent_fields' => $tpl_parent_fields,
1041 'addchild' => ($action === 'addchild'),
1042 'osocials' => new Social($this->zdb
)
1051 * @param Request $request PSR Request
1052 * @param Response $response PSR Response
1053 * @param integer $id Member id
1057 public function doEdit(Request
$request, Response
$response, int $id): Response
1059 return $this->store($request, $response);
1063 * Massive change page
1065 * @param Request $request PSR Request
1066 * @param Response $response PSR Response
1070 public function massChange(Request
$request, Response
$response): Response
1072 $filters = $this->session
->{$this->getFilterName(['suffix' => 'masschange'])} ??
new MembersList();
1075 'id' => $filters->selected
,
1076 'redirect_uri' => $this->routeparser
->urlFor('members')
1079 $fc = $this->fields_config
;
1080 $form_elements = $fc->getMassiveFormElements($this->members_fields
, $this->login
);
1083 $member = new Adherent($this->zdb
);
1084 $member->disableAllDeps()->enableDep('dynamics');
1087 $statuts = new Status($this->zdb
);
1089 $titles = new Titles($this->zdb
);
1092 $this->view
->render(
1094 'modals/mass_change_members.html.twig',
1096 'mode' => ($request->getHeaderLine('X-Requested-With') === 'XMLHttpRequest') ?
'ajax' : '',
1097 'page_title' => str_replace(
1099 (string)count($data['id']),
1100 _T('Mass change %count members')
1102 'form_url' => $this->routeparser
->urlFor('masschangeMembersReview'),
1103 'cancel_uri' => $this->routeparser
->urlFor('members'),
1105 'member' => $member,
1106 'fieldsets' => $form_elements['fieldsets'],
1107 'titles_list' => $titles->getList(),
1108 'statuts' => $statuts->getList(),
1109 'require_mass' => true
1116 * Massive changes validation page
1118 * @param Request $request PSR Request
1119 * @param Response $response PSR Response
1123 public function validateMassChange(Request
$request, Response
$response): Response
1125 $post = $request->getParsedBody();
1128 if (!isset($post['confirm'])) {
1129 $this->flash
->addMessage(
1131 _T("Mass changes has not been confirmed!")
1134 //we want only visibles fields
1135 $fc = $this->fields_config
;
1136 $form_elements = $fc->getMassiveFormElements($this->members_fields
, $this->login
);
1138 foreach ($form_elements['fieldsets'] as $form_element) {
1139 foreach ($form_element->elements
as $field) {
1141 isset($post['mass_' . $field->field_id
])
1142 && (isset($post[$field->field_id
]) ||
$field->type
=== FieldsConfig
::TYPE_BOOL
)
1144 $changes[$field->field_id
] = [
1145 'label' => $field->label
,
1146 'value' => $post[$field->field_id
] ??
0
1152 //handle dynamic fields
1153 $member = new Adherent($this->zdb
);
1158 $this->members_fields
,
1161 $dynamic_fields = $member->getDynamicFields()->getFields();
1162 foreach ($dynamic_fields as $field) {
1163 $mass_id = 'mass_info_field_' . $field->getId();
1164 $field_id = 'info_field_' . $field->getId() . '_1';
1166 isset($post[$mass_id])
1167 && (isset($post[$field_id]) ||
$field instanceof Boolean
)
1169 $changes[$field_id] = [
1170 'label' => $field->getName(),
1171 'value' => $post[$field_id] ??
0
1177 $filters = $this->session
->{$this->getFilterName(['suffix' => 'masschange'])};
1179 'id' => $filters->selected
,
1180 'redirect_uri' => $this->routeparser
->urlFor('members')
1184 $statuts = new Status($this->zdb
);
1186 $titles = new Titles($this->zdb
);
1189 $this->view
->render(
1191 'modals/mass_change_members.html.twig',
1193 'mode' => ($request->getHeaderLine('X-Requested-With') === 'XMLHttpRequest') ?
'ajax' : '',
1194 'page_title' => str_replace(
1196 (string)count($data['id']),
1197 _T('Review mass change %count members')
1199 'form_url' => $this->routeparser
->urlFor('massstoremembers'),
1200 'cancel_uri' => $this->routeparser
->urlFor('members'),
1202 'titles_list' => $titles->getList(),
1203 'statuts' => $statuts->getList(),
1204 'changes' => $changes
1211 * Do massive changes
1213 * @param Request $request PSR Request
1214 * @param Response $response PSR Response
1218 public function doMassChange(Request
$request, Response
$response): Response
1220 $post = $request->getParsedBody();
1221 $redirect_url = $post['redirect_uri'];
1222 $error_detected = [];
1224 $dynamic_fields = null;
1226 unset($post['redirect_uri']);
1227 if (!isset($post['confirm'])) {
1228 $error_detected[] = _T("Mass changes has not been confirmed!");
1230 unset($post['confirm']);
1234 $fc = $this->fields_config
;
1235 $form_elements = $fc->getMassiveFormElements($this->members_fields
, $this->login
);
1236 $disabled = $this->members_fields
;
1237 foreach (array_keys($post) as $key) {
1239 foreach ($form_elements['fieldsets'] as $fieldset) {
1240 if (isset($fieldset->elements
[$key])) {
1247 //try on dynamic fields
1248 if ($dynamic_fields === null) {
1249 //handle dynamic fields
1250 $member = new Adherent($this->zdb
);
1255 $this->members_fields
,
1258 $dynamic_fields = $member->getDynamicFields()->getFields();
1260 foreach ($dynamic_fields as $field) {
1261 $field_id = 'info_field_' . $field->getId() . '_1';
1262 if ($key == $field_id) {
1271 'Permission issue mass editing field ' . $key,
1276 unset($disabled[$key]);
1280 if (!count($post)) {
1281 $error_detected[] = _T("Nothing to do!");
1283 foreach ($ids as $id) {
1284 $is_manager = !$this->login
->isAdmin()
1285 && !$this->login
->isStaff()
1286 && $this->login
->isGroupManager();
1287 $member = new Adherent($this->zdb
);
1292 $member->enableDep('groups');
1294 $member->load((int)$id);
1295 $member->setDependencies(
1297 $this->members_fields
,
1300 if (!$member->canEdit($this->login
)) {
1304 $valid = $member->check($post, [], $disabled);
1305 if ($valid === true) {
1306 $done = $member->store();
1308 $error_detected[] = _T("An error occurred while storing the member.");
1313 $error_detected = array_merge($error_detected, $valid);
1319 if ($mass == 0 && !count($error_detected)) {
1320 $error_detected[] = _T('Something went wront during mass edition!');
1322 $this->flash
->addMessage(
1325 //TRANS: first parameter is the number of edited members
1327 '%1$s member has been changed successfully!',
1328 '%1$s members has been changed successfully!',
1336 if (count($error_detected) > 0) {
1337 foreach ($error_detected as $error) {
1338 $this->flash
->addMessage(
1345 if (!($request->getHeaderLine('X-Requested-With') === 'XMLHttpRequest')) {
1348 ->withHeader('Location', $redirect_url);
1350 return $this->withJson(
1353 'success' => count($error_detected) === 0
1362 * @param Request $request PSR Request
1363 * @param Response $response PSR Response
1367 public function store(Request
$request, Response
$response): Response
1369 if (!$this->preferences
->pref_bool_selfsubscribe
&& !$this->login
->isLogged()) {
1372 ->withHeader('Location', $this->routeparser
->urlFor('slash'));
1375 $post = $request->getParsedBody();
1376 $member = new Adherent($this->zdb
);
1381 $this->members_fields
,
1385 $success_detected = [];
1386 $error_detected = [];
1388 if ($this->isSelfMembership() && !isset($post[Adherent
::PK
])) {
1389 //mark as self membership
1390 $member->setSelfMembership();
1393 /** @var Gaptcha $gaptcha */
1394 $gaptcha = $this->session
->gaptcha
;
1395 if (!$gaptcha->check($post['gaptcha'])) {
1396 $error_detected[] = _T('Invalid captcha');
1401 if (isset($post['id_adh'])) {
1402 $member->load((int)$post['id_adh']);
1403 if (!$member->canEdit($this->login
)) {
1404 //redirection should have been done before. Just throw an Exception.
1405 throw new \
RuntimeException(
1408 (string)$member->id
,
1409 'No right to store member #%id'
1414 if ($member->id
!= '') {
1415 $member->load($this->login
->id
);
1419 // flagging required fields
1420 $fc = $this->fields_config
;
1422 // password required if we create a new member but not from self subscription
1423 if ($member->id
!= '' ||
$this->isSelfMembership()) {
1424 $fc->setNotRequired('mdp_adh');
1428 $member->hasParent() && !isset($post['detach_parent'])
1429 ||
isset($post['parent_id']) && !empty($post['parent_id'])
1431 $parent_fields = $member->getParentFields();
1432 foreach ($parent_fields as $field) {
1433 if ($fc->isRequired($field)) {
1434 $fc->setNotRequired($field);
1439 // flagging required fields invisible to members
1440 if ($this->login
->isAdmin() ||
$this->login
->isStaff()) {
1441 $fc->setNotRequired('activite_adh');
1442 $fc->setNotRequired('id_statut');
1445 $form_elements = $fc->getFormElements(
1448 $this->isSelfMembership()
1450 $fieldsets = $form_elements['fieldsets'];
1451 $required = array();
1452 $disabled = array();
1454 foreach ($fieldsets as $category) {
1455 foreach ($category->elements
as $field) {
1456 if ($field->required
) {
1457 $required[$field->field_id
] = true;
1459 if ($field->disabled
) {
1460 $disabled[] = $field->field_id
;
1461 } elseif (!isset($post[$field->field_id
])) {
1462 switch ($field->field_id
) {
1463 //unchecked booleans are not sent from form
1464 case 'bool_admin_adh':
1465 case 'bool_exempt_adh':
1466 case 'bool_display_info':
1467 $post[$field->field_id
] = 0;
1474 $real_requireds = array_diff(array_keys($required), array_values($disabled));
1476 // send email to member
1477 if ($this->isSelfMembership() ||
isset($post['mail_confirm']) && $post['mail_confirm'] == '1') {
1478 $member->setSendmail(); //flag to send creation email
1482 $redirect_url = $this->routeparser
->urlFor('member', ['id' => (string)$member->id
]);
1483 if (!count($real_requireds) ||
isset($post[array_shift($real_requireds)])) {
1485 $valid = $member->check($post, $required, $disabled);
1486 if ($valid !== true) {
1487 $error_detected = array_merge($error_detected, $valid);
1490 if (count($error_detected) == 0) {
1491 //all goes well, we can proceed
1494 if ($member->id
== '') {
1498 $store = $member->store();
1499 if ($store === true) {
1500 //member has been stored :)
1502 if ($this->isSelfMembership()) {
1503 $success_detected[] = _T("Your account has been created!");
1505 $this->preferences
->pref_mail_method
> GaletteMail
::METHOD_DISABLED
1506 && $member->getEmail() != ''
1508 $success_detected[] = _T("An email has been sent to you, check your inbox.");
1511 $success_detected[] = _T("New member has been successfully added.");
1514 $success_detected[] = _T("Member account has been modified.");
1517 if ($this->login
->isGroupManager()) {
1518 //add/remove user from groups
1519 $groups_adh = $post['groups_adh'] ??
[];
1520 $add_groups = Groups
::addMemberToGroups(
1525 if ($add_groups === false) {
1526 $error_detected[] = _T("An error occurred adding member to its groups.");
1529 if ($this->login
->isSuperAdmin() ||
$this->login
->isAdmin() ||
$this->login
->isStaff()) {
1530 //add/remove manager from groups
1531 $managed_groups_adh = $post['groups_managed_adh'] ??
[];
1532 $add_groups = Groups
::addMemberToGroups(
1534 $managed_groups_adh,
1537 $member->loadGroups();
1539 if ($add_groups === false) {
1540 $error_detected[] = _T("An error occurred adding member to its groups as manager.");
1544 //something went wrong :'(
1545 $error_detected[] = _T("An error occurred while storing the member.");
1549 if (count($error_detected) === 0) {
1551 if ($this->preferences
->pref_force_picture_ratio
== 1) {
1553 $cropping['ratio'] = isset($this->preferences
->pref_member_picture_ratio
) ?
$this->preferences
->pref_member_picture_ratio
: 'square_ratio';
1554 $cropping['focus'] = isset($post['crop_focus']) ?
$post['crop_focus'] : 'center';
1556 $files_res = $member->handleFiles($_FILES, $cropping);
1557 if (is_array($files_res)) {
1558 $error_detected = array_merge($error_detected, $files_res);
1561 if (isset($post['del_photo'])) {
1562 if (!$member->picture
->delete()) {
1563 $error_detected[] = _T("Delete failed");
1564 $str_adh = $member->id
. ' (' . $member->sname
. ' ' . ')';
1566 'Unable to delete picture for member ' . $str_adh,
1573 if (count($error_detected) > 0) {
1574 foreach ($error_detected as $error) {
1575 if (strpos($error, '%member_url_') !== false) {
1576 preg_match('/%member_url_(\d+)/', $error, $matches);
1577 $url = $this->routeparser
->urlFor('member', ['id' => $matches[1]]);
1578 $error = str_replace(
1579 '%member_url_' . $matches[1],
1584 $this->flash
->addMessage(
1591 if (count($success_detected) > 0) {
1592 foreach ($success_detected as $success) {
1593 $this->flash
->addMessage(
1600 if (count($error_detected) === 0) {
1601 if ($this->isSelfMembership()) {
1602 $redirect_url = $this->routeparser
->urlFor('login');
1604 isset($post['redirect_on_create'])
1605 && $post['redirect_on_create'] > Adherent
::AFTER_ADD_DEFAULT
1607 switch ($post['redirect_on_create']) {
1608 case Adherent
::AFTER_ADD_TRANS
:
1609 $redirect_url = $this->routeparser
->urlFor('addTransaction');
1611 case Adherent
::AFTER_ADD_NEW
:
1612 $redirect_url = $this->routeparser
->urlFor('addMember');
1614 case Adherent
::AFTER_ADD_SHOW
:
1615 $redirect_url = $this->routeparser
->urlFor('member', ['id' => (string)$member->id
]);
1617 case Adherent
::AFTER_ADD_LIST
:
1618 $redirect_url = $this->routeparser
->urlFor('members');
1620 case Adherent
::AFTER_ADD_HOME
:
1621 $redirect_url = $this->routeparser
->urlFor('slash');
1624 } elseif (!isset($post['id_adh']) && !$member->isDueFree()) {
1625 $redirect_url = $this->routeparser
->urlFor(
1628 ) . '?id_adh=' . $member->id
;
1630 $redirect_url = $this->routeparser
->urlFor('member', ['id' => (string)$member->id
]);
1633 //store entity in session
1634 $this->session
->member
= $member;
1636 if ($this->isSelfMembership()) {
1637 $redirect_url = $this->routeparser
->urlFor('subscribe');
1640 $redirect_url = $this->routeparser
->urlFor(
1642 ['id' => (string)$member->id
]
1645 $redirect_url = $this->routeparser
->urlFor((isset($post['addchild']) ?
'addMemberChild' : 'addMember'));
1653 ->withHeader('Location', $redirect_url);
1661 * Get redirection URI
1663 * @param array<string,mixed> $args Route arguments
1667 public function redirectUri(array $args): string
1669 return $this->routeparser
->urlFor('members');
1675 * @param array<string,mixed> $args Route arguments
1679 public function formUri(array $args): string
1681 return $this->routeparser
->urlFor(
1688 * Get confirmation removal page title
1690 * @param array<string,mixed> $args Route arguments
1694 public function confirmRemoveTitle(array $args): string
1696 if (isset($args['id_adh']) ||
isset($args['id'])) {
1697 //one member removal
1698 $id_adh = $args['id_adh'] ??
$args['id'];
1699 $adh = new Adherent($this->zdb
, (int)$id_adh);
1701 _T('Remove member %1$s'),
1705 //batch members removal
1706 $filters = $this->session
->{$this->getFilterName(['suffix' => 'delete'])};
1707 $this->session
->{$this->getFilterName(['suffix' => 'delete'])} = $filters;
1710 (string)count($filters->selected
),
1711 _T('You are about to remove %count members.')
1719 * @param array<string,mixed> $args Route arguments
1720 * @param array<string,mixed> $post POST values
1724 protected function doDelete(array $args, array $post): bool
1726 if (isset($this->session
->{$this->getFilterName(['suffix' => 'delete'])})) {
1727 $filters = $this->session
->{$this->getFilterName(['suffix' => 'delete'])};
1729 $filters = new MembersList();
1731 $members = new Members($filters);
1733 if (!is_array($post['id'])) {
1734 $ids = (array)$post['id'];
1739 return $members->removeMembers($ids);
1745 * Set self memebrship flag
1747 * @return MembersController
1749 private function setSelfMembership(): MembersController
1751 $this->is_self_membership
= true;
1756 * Is self membership?
1760 private function isSelfMembership(): bool
1762 return $this->is_self_membership
;
1766 * Handle navigation links
1768 * @param int $id_adh Current member ID
1770 * @return array<string,int>
1772 private function handleNavigationLinks(int $id_adh): array
1774 $navigate = array();
1776 if (isset($this->session
->{$this->getFilterName()})) {
1777 $filters = clone $this->session
->{$this->getFilterName()};
1779 $filters = new MembersList();
1781 //we must navigate between all members
1785 $this->login
->isAdmin()
1786 ||
$this->login
->isStaff()
1787 ||
$this->login
->isGroupManager()
1789 $m = new Members($filters);
1792 $fields = [Adherent
::PK
, 'nom_adh', 'prenom_adh'];
1793 if ($this->login
->isAdmin() ||
$this->login
->isStaff()) {
1794 $ids = $m->getMembersList(false, $fields);
1796 $ids = $m->getManagedMembersList(false, $fields);
1799 $ids = $ids->toArray();
1800 foreach ($ids as $k => $m) {
1801 if ($m['id_adh'] == $id_adh) {
1803 'cur' => $m['id_adh'],
1804 'count' => $filters->counter
,
1808 $navigate['prev'] = $ids[$k - 1]['id_adh'];
1810 if ($k < count($ids) - 1) {
1811 $navigate['next'] = $ids[$k +
1]['id_adh'];
1822 * Get filter name in session
1824 * @param array<string,mixed>|null $args Route arguments
1828 public function getFilterName(array $args = null): string
1830 $filter_name = 'filter_members';
1832 if (isset($args['prefix'])) {
1833 $filter_name = $args['prefix'] . '_' . $filter_name;
1836 if (isset($args['suffix'])) {
1837 $filter_name .= '_' . $args['suffix'];
1840 return $filter_name;