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\Repository
;
24 use Galette\Core\Login
;
25 use Galette\Entity\Social
;
26 use Galette\Events\GaletteEvent
;
27 use Laminas\Db\ResultSet\ResultSet
;
28 use Laminas\Db\Sql\Predicate\IsNull
;
30 use Galette\DynamicFields\DynamicField
;
31 use Galette\Entity\DynamicFieldsHandle
;
33 use Laminas\Db\Sql\Predicate\Expression
;
34 use Laminas\Db\Sql\Select
;
35 use Laminas\Db\Sql\Predicate\PredicateSet
;
36 use Laminas\Db\Sql\Predicate\Operator
;
37 use Galette\Entity\Adherent
;
38 use Galette\Entity\Contribution
;
39 use Galette\Entity\Transaction
;
40 use Galette\Entity\Reminder
;
41 use Galette\Filters\MembersList
;
42 use Galette\Filters\AdvancedMembersList
;
43 use Galette\Core\Picture
;
44 use Galette\Entity\Group
;
45 use Galette\Entity\Status
;
50 * Members class for galette
52 * @author Johan Cwiklinski <johan@x-tnd.be>
56 public const TABLE
= Adherent
::TABLE
;
57 public const PK
= Adherent
::PK
;
59 public const ALL_ACCOUNTS
= 0;
60 public const ACTIVE_ACCOUNT
= 1;
61 public const INACTIVE_ACCOUNT
= 2;
63 public const SHOW_LIST
= 0;
64 public const SHOW_PUBLIC_LIST
= 1;
65 public const SHOW_ARRAY_LIST
= 2;
66 public const SHOW_STAFF
= 3;
67 public const SHOW_MANAGED
= 4;
68 public const SHOW_EXPORT
= 5;
70 public const FILTER_NAME
= 0;
71 public const FILTER_ADDRESS
= 1;
72 public const FILTER_MAIL
= 2;
73 public const FILTER_JOB
= 3;
74 public const FILTER_INFOS
= 4;
75 public const FILTER_DC_EMAIL
= 5;
76 public const FILTER_W_EMAIL
= 6;
77 public const FILTER_WO_EMAIL
= 7;
78 public const FILTER_COMPANY_NAME
= 8;
79 public const FILTER_DC_PUBINFOS
= 9;
80 public const FILTER_W_PUBINFOS
= 10;
81 public const FILTER_WO_PUBINFOS
= 11;
82 public const FILTER_ID
= 12;
83 public const FILTER_NUMBER
= 13;
85 public const MEMBERSHIP_ALL
= 0;
86 public const MEMBERSHIP_UP2DATE
= 3;
87 public const MEMBERSHIP_NEARLY
= 1;
88 public const MEMBERSHIP_LATE
= 2;
89 public const MEMBERSHIP_NEVER
= 4;
90 public const MEMBERSHIP_STAFF
= 5;
91 public const MEMBERSHIP_ADMIN
= 6;
92 public const MEMBERSHIP_NONE
= 7;
94 public const ORDERBY_NAME
= 'name';
95 public const ORDERBY_NICKNAME
= 'nickname';
96 public const ORDERBY_STATUS
= 'status';
97 public const ORDERBY_FEE_STATUS
= 'fee_status';
98 public const ORDERBY_MODIFDATE
= 'modif_date';
99 public const ORDERBY_ID
= 'id';
101 public const NON_STAFF_MEMBERS
= 30;
103 private MembersList|AdvancedMembersList
$filters;
104 private int $count = 0;
105 /** @var array<string> */
106 private array $errors = [];
108 private array $extra_order = [];
111 * Default constructor
113 * @param MembersList|AdvancedMembersList|null $filters Filtering
115 public function __construct(MembersList|AdvancedMembersList|
null $filters = null)
117 if ($filters === null) {
118 $this->filters
= new MembersList();
120 $this->filters
= $filters;
125 * Get staff members list
127 * @param bool $as_members return the results as an array of
129 * @param ?array<string> $fields field(s) name(s) to get. Should be a string or
130 * an array. If null, all fields will be
132 * @param boolean $count true if we want to count members
133 * @param boolean $limit true to LIMIT query
135 * @return Adherent[]|ResultSet
137 public function getStaffMembersList(
138 bool $as_members = false,
139 ?
array $fields = null,
143 return $this->getMembersList(
154 * Get managed members list (for groups managers)
156 * @param bool $as_members return the results as an array of
158 * @param ?array<string> $fields field(s) name(s) to get. Should be a string or
159 * an array. If null, all fields will be
161 * @param boolean $count true if we want to count members
162 * @param boolean $limit true to LIMIT query
164 * @return Adherent[]|ResultSet
166 public function getManagedMembersList(
167 bool $as_members = false,
168 ?
array $fields = null,
172 return $this->getMembersList(
185 * @param bool $as_members return the results as an array of
187 * @param ?array<string> $fields field(s) name(s) to get. Should be a string or
188 * an array. If null, all fields will be
190 * @param boolean $count true if we want to count members
191 * @param boolean $staff true if we want only staff members
192 * @param boolean $managed true if we want only managed groups
193 * @param boolean $limit true if we want records pagination
194 * @param boolean $export true if we are exporting
196 * @return Adherent[]|ResultSet
198 public function getMembersList(
199 bool $as_members = false,
200 ?
array $fields = null,
203 bool $managed = false,
209 if ($limit === true) {
210 //force count if limit is active
215 $_mode = self
::SHOW_LIST
;
216 if ($staff !== false) {
217 $_mode = self
::SHOW_STAFF
;
219 if ($managed !== false) {
220 $_mode = self
::SHOW_MANAGED
;
222 if ($export !== false) {
223 $_mode = self
::SHOW_EXPORT
;
226 $select = $this->buildSelect(
233 //add limits to retrieve only relavant rows
234 if ($limit === true) {
235 $this->filters
->setLimits($select);
238 $rows = $zdb->execute($select);
239 $this->filters
->query
= $zdb->query_string
;
247 foreach ($rows as $row) {
248 $members[] = new Adherent($zdb, $row, $deps);
254 } catch (Throwable
$e) {
256 'Cannot list members | ' . $e->getMessage(),
264 * Remove specified members
266 * @param integer|array<int> $ids Members identifiers to delete
270 public function removeMembers(int|
array $ids): bool
272 global $zdb, $hist, $emitter;
274 $processed = array();
275 $list = (is_array($ids) ?
$ids : [$ids]);
278 $zdb->connection
->beginTransaction();
280 //Retrieve some information
281 $select = $zdb->select(self
::TABLE
);
283 array(self
::PK
, 'nom_adh', 'prenom_adh', 'email_adh')
284 )->where
->in(self
::PK
, $list);
286 $results = $zdb->execute($select);
289 foreach ($results as $member) {
290 $str_adh = $member->id_adh
. ' (' . $member->nom_adh
. ' ' .
291 $member->prenom_adh
. ')';
292 $infos .= $str_adh . "\n";
294 $p = new Picture($member->id_adh
);
295 if ($p->hasPicture()) {
296 if (!$p->delete(false)) {
298 'Unable to delete picture for member ' . $str_adh,
301 throw new \
Exception(
302 'Unable to delete picture for member ' .
307 _T("Member Picture deleted"),
313 $processed[] = $member;
316 //delete contributions
317 $del_qry = $zdb->delete(Contribution
::TABLE
);
322 $zdb->execute($del_qry);
325 $select = $zdb->select(Transaction
::TABLE
);
326 $select->where
->in(self
::PK
, $list);
327 $results = $zdb->execute($select);
329 //if members has transactions;
330 //reset link with other contributions
332 if ($results->count() > 0) {
334 foreach ($results as $transaction) {
335 $transactions[] = $transaction[Transaction
::PK
];
338 $update = $zdb->update(Contribution
::TABLE
);
340 Transaction
::PK
=> new Expression('NULL')
345 $zdb->execute($update);
348 //delete transactions
349 $del_qry = $zdb->delete(Transaction
::TABLE
);
350 $del_qry->where
->in(self
::PK
, $list);
351 $zdb->execute($del_qry);
353 //delete groups membership/mamagmentship
354 Groups
::removeMembersFromGroups($list);
357 $del_qry = $zdb->delete(Reminder
::TABLE
);
362 $zdb->execute($del_qry);
364 //delete dynamic fields values
365 $del_qry = $zdb->delete(DynamicFieldsHandle
::TABLE
);
366 $del_qry->where(['field_form' => 'adh']);
367 $del_qry->where
->in('item_id', $list);
368 $zdb->execute($del_qry);
371 $del_qry = $zdb->delete(self
::TABLE
);
376 $zdb->execute($del_qry);
379 $zdb->connection
->commit();
381 foreach ($processed as $p) {
382 $emitter->dispatch(new GaletteEvent('member.remove', $p));
385 //add a history entry
387 _T("Delete members cards, transactions and dues"),
392 } catch (Throwable
$e) {
393 if ($zdb->connection
->inTransaction()) {
394 $zdb->connection
->rollBack();
396 if ($zdb->isForeignKeyException($e)) {
398 'Member still have existing dependencies in the ' .
399 'database, maybe a mailing or some content from a ' .
400 'plugin. Please remove dependencies before trying ' .
404 $this->errors
[] = _T("Cannot remove a member who still have dependencies (mailings, ...)");
407 'Unable to delete selected member(s) |' .
420 * @param boolean $as_members return the results as an array of
422 * @param ?array<string> $fields field(s) name(s) to get. Should be a string or
423 * an array. If null, all fields will be
426 * @return Adherent[]|ResultSet
428 public function getList(bool $as_members = false, ?
array $fields = null)
430 return $this->getMembersList(
442 * Get members list with public information available
444 * @param boolean $with_photos get only members which have uploaded a
445 * photo (for trombinoscope)
447 * @return array<string, Adherent[]>
449 public function getPublicList(bool $with_photos)
454 $this->extra_order
= ['priorite_statut ASC'];
455 $select = $this->buildSelect(
456 self
::SHOW_PUBLIC_LIST
,
463 array('status' => PREFIX_DB
. Status
::TABLE
),
464 'a.' . Status
::PK
. '=status.' . Status
::PK
467 $this->filters
->setLimits($select);
469 $results = $zdb->execute($select);
473 'picture' => $with_photos
476 $status = new Status($zdb);
477 $status_list = $status->getCompleteList();
481 foreach ($results as $row) {
482 $member = new Adherent($zdb, $row, $deps);
483 if ($status_list[$row->id_statut
]['extra'] < self
::NON_STAFF_MEMBERS
) {
486 $members[] = $member;
491 'members' => $members
493 } catch (Throwable
$e) {
495 'Cannot list members with public information (photos: '
496 . $with_photos . ') | ' . $e->getMessage(),
504 * Get list of members that has been selected
506 * @param int|array<int> $ids an array of members id that has been selected
507 * @param ?array<string> $orderby SQL order clause (optional)
508 * @param boolean $with_photos Should photos be loaded?
509 * @param boolean $as_members Return Adherent[] or simple ResultSet
510 * @param ?array<string> $fields Fields to use
511 * @param boolean $export True if we are exporting
512 * @param boolean $dues True if load dues as Adherent dependency
513 * @param boolean $parent True if load parent as Adherent dependency
515 * @return array <int,Adherent|ArrayObject<string, int|string>>|false
517 public function getArrayList(
519 ?
array $orderby = null,
520 bool $with_photos = false,
521 bool $as_members = true,
522 ?
array $fields = null,
523 bool $export = false,
529 if (!is_array($ids) ||
count($ids) < 1) {
530 Analog
::log('No member selected for labels.', Analog
::INFO
);
535 $damode = self
::SHOW_ARRAY_LIST
;
536 if ($export === true) {
537 $damode = self
::SHOW_EXPORT
;
539 $select = $this->buildSelect(
545 $select->where
->in('a.' . self
::PK
, $ids);
546 if (is_array($orderby) && count($orderby) > 0) {
547 foreach ($orderby as $o) {
552 $results = $zdb->execute($select);
556 'picture' => $with_photos,
561 foreach ($results as $o) {
562 if ($as_members === true) {
563 $members[] = new Adherent($zdb, $o, $deps);
569 } catch (Throwable
$e) {
571 'Cannot load members form ids array | ' . $e->getMessage(),
579 * Builds the SELECT statement
581 * @param int $mode the current mode (see self::SHOW_*)
582 * @param ?array<string> $fields fields list to retrieve
583 * @param bool $photos true if we want to get only members with photos
584 * Default to false, only relevant for SHOW_PUBLIC_LIST
585 * @param bool $count true if we want to count members, defaults to false
587 * @return Select SELECT statement
589 private function buildSelect(int $mode, ?
array $fields, bool $photos, bool $count = false): Select
594 if ($fields != null && is_array($fields) && !in_array('id_adh', $fields)) {
595 $fields[] = 'id_adh';
599 if (is_array($fields) && count($fields)) {
600 $fieldsList = $fields;
603 $select = $zdb->select(self
::TABLE
, 'a');
605 $select->columns($fieldsList);
607 $select->quantifier('DISTINCT');
610 array('so' => PREFIX_DB
. Social
::TABLE
),
611 'a.' . Adherent
::PK
. '=so.' . Adherent
::PK
,
617 array('parent' => PREFIX_DB
. self
::TABLE
),
618 'a.parent_id=parent.' . self
::PK
,
624 case self
::SHOW_STAFF
:
625 case self
::SHOW_LIST
:
626 case self
::SHOW_ARRAY_LIST
:
627 case self
::SHOW_EXPORT
:
629 array('status' => PREFIX_DB
. Status
::TABLE
),
630 'a.' . Status
::PK
. '=status.' . Status
::PK
,
631 array('priorite_statut')
634 case self
::SHOW_MANAGED
:
636 array('status' => PREFIX_DB
. Status
::TABLE
),
637 'a.' . Status
::PK
. '=status.' . Status
::PK
639 array('gr' => PREFIX_DB
. Group
::GROUPSUSERS_TABLE
),
640 'a.' . Adherent
::PK
. '=gr.' . Adherent
::PK
,
643 array('m' => PREFIX_DB
. Group
::GROUPSMANAGERS_TABLE
),
644 'gr.' . Group
::PK
. '=m.' . Group
::PK
,
646 )->where(['m.' . Adherent
::PK
=> $login->id
]);
648 case self
::SHOW_PUBLIC_LIST
:
651 array('picture' => PREFIX_DB
. Picture
::TABLE
),
652 'a.' . self
::PK
. '= picture.' . self
::PK
,
659 //check for contributions filtering
661 $this->filters
instanceof AdvancedMembersList
662 && $this->filters
->withinContributions()
665 array('ct' => PREFIX_DB
. Contribution
::TABLE
),
666 'ct.' . self
::PK
. '=a.' . self
::PK
,
672 //check if there are dynamic fields in filter
675 if ($this->filters
instanceof AdvancedMembersList
) {
677 (bool)count($this->filters
->free_search
)
678 && !isset($this->filters
->free_search
['empty'])
680 $free_searches = $this->filters
->free_search
;
681 foreach ($free_searches as $fs) {
682 if (strpos($fs['field'], 'dyn_') === 0) {
683 // simple dynamic fields
685 $dfs[] = str_replace('dyn_', '', $fs['field']);
691 //check if there are dynamic fields for contributions in filter
697 $this->filters
instanceof AdvancedMembersList
698 && $this->filters
->withinContributions()
701 count($this->filters
->contrib_dynamic
) > 0
702 && !isset($this->filters
->contrib_dynamic
['empty'])
706 //check if there are dynamic fields in the filter
707 foreach ($this->filters
->contrib_dynamic
as $k => $cd) {
708 $dyn_field = DynamicField
::loadFieldType($zdb, (int)$k);
709 if ($dyn_field instanceof \Galette\DynamicFields\Choice
) {
717 if ($hasDfc === true ||
$hasCdfc === true) {
719 array('dfc' => PREFIX_DB
. DynamicFieldsHandle
::TABLE
),
720 'dfc.item_id=ct.' . Contribution
::PK
,
726 // simple dynamic fields
727 if ($hasDf === true) {
728 foreach ($dfs as $df) {
729 $subselect = $zdb->select(DynamicFieldsHandle
::TABLE
, 'df');
732 'item_id' => 'item_id',
736 $subselect->where(['df.field_form' => 'adh']);
737 $subselect->where(['df.field_id' => $df]);
739 array('df' . $df => $subselect),
740 'a.id_adh = df' . $df . '.item_id',
747 // choice dynamic fields
748 if ($hasCdfc === true) {
749 foreach ($cdfcs as $cdf) {
750 $rcdf_field = sprintf(
752 $zdb->platform
->quoteIdentifier('cdfc' . $cdf),
753 $zdb->platform
->quoteIdentifier('id')
755 if (TYPE_DB
=== 'pgsql') {
756 $rcdf_field = $rcdf_field . '::text';
760 array('cdfc' . $cdf => DynamicField
::getFixedValuesTableName($cdf, true)),
765 $zdb->platform
->quoteIdentifier('dfc'),
766 $zdb->platform
->quoteIdentifier('field_val')
775 if ($mode == self
::SHOW_LIST ||
$mode == self
::SHOW_MANAGED
) {
776 if ($this->filters
!== false) {
777 $this->buildWhereClause($select);
779 } elseif ($mode == self
::SHOW_PUBLIC_LIST
) {
792 new Expression('true')
800 'a.bool_display_info',
802 new Expression('true')
807 new Expression('true')
816 if ($mode === self
::SHOW_STAFF
) {
817 $select->where
->lessThan(
818 'status.priorite_statut',
819 self
::NON_STAFF_MEMBERS
824 $this->proceedCount($select);
827 $this->buildOrderClause($select, $fields);
830 } catch (Throwable
$e) {
832 'Cannot build SELECT clause for members | ' . $e->getMessage(),
840 * Count members from the query
842 * @param Select $select Original select
846 private function proceedCount(Select
$select): void
851 $countSelect = clone $select;
852 $countSelect->reset($countSelect::COLUMNS
);
853 $countSelect->reset($countSelect::ORDER
);
854 $countSelect->reset($countSelect::HAVING
);
855 $joins = $countSelect->joins
;
856 $countSelect->reset($countSelect::JOINS
);
857 foreach ($joins as $join) {
864 unset($join['columns']);
866 $countSelect->columns(
868 'count' => new Expression('count(DISTINCT a.' . self
::PK
. ')')
872 $have = $select->having
;
873 if ($have->count() > 0) {
874 foreach ($have->getPredicates() as $h) {
875 $countSelect->where($h);
879 $results = $zdb->execute($countSelect);
881 $this->count
= (int)$results->current()->count
;
882 if (isset($this->filters
)) {
883 $this->filters
->setCounter($this->count
);
885 } catch (Throwable
$e) {
887 'Cannot count members | ' . $e->getMessage(),
895 * Builds the order clause
897 * @param Select $select Original select
898 * @param ?array<string> $fields Fields list to ensure ORDER clause
899 * references selected fields. Optional.
903 private function buildOrderClause(Select
$select, ?
array $fields = null): Select
905 $order = $this->extra_order
;
907 switch ($this->filters
->orderby
) {
908 case self
::ORDERBY_NICKNAME
:
909 if ($this->canOrderBy('pseudo_adh', $fields)) {
910 $order[] = 'pseudo_adh ' . $this->filters
->getDirection();
913 case self
::ORDERBY_STATUS
:
914 if ($this->canOrderBy('priorite_statut', $fields)) {
915 $order[] = 'priorite_statut ' . $this->filters
->getDirection();
918 case self
::ORDERBY_MODIFDATE
:
919 if ($this->canOrderBy('date_modif_adh', $fields)) {
920 $order[] = 'date_modif_adh ' . $this->filters
->getDirection();
923 case 'list_adh_contribstatus':
924 case self
::ORDERBY_FEE_STATUS
:
925 if ($this->canOrderBy('bool_exempt_adh', $fields)) {
926 $order[] = 'bool_exempt_adh ' . $this->filters
->getDirection();
929 if ($this->canOrderBy('date_echeance', $fields)) {
930 $order[] = 'date_echeance ' . $this->filters
->getDirection();
933 case self
::ORDERBY_ID
:
934 if ($this->canOrderBy('id_adh', $fields)) {
935 $order[] = 'id_adh ' . $this->filters
->getDirection();
938 case 'list_adh_name':
941 case self
::ORDERBY_NAME
:
945 if ($this->canOrderBy($this->filters
->orderby
, $fields)) {
946 $order[] = 'a.' . $this->filters
->orderby
. ' ' . $this->filters
->getDirection();
951 //anyways, we want to order by firstname, lastname
952 if ($this->canOrderBy('nom_adh', $fields)) {
953 $order[] = 'nom_adh ' . $this->filters
->getDirection();
955 if ($this->canOrderBy('prenom_adh', $fields)) {
956 $order[] = 'prenom_adh ' . $this->filters
->getDirection();
959 $select->order($order);
964 * Is field allowed to order? it should be present in
965 * provided fields list (those that are SELECT'ed).
967 * @param string $field_name Field name to order by
968 * @param ?array<string> $fields SELECTE'ed fields
972 private function canOrderBy(string $field_name, ?
array $fields): bool
974 if ($fields === null) {
976 } elseif (!is_array($fields)) {
978 } elseif (in_array($field_name, $fields)) {
982 'Trying to order by ' . $field_name . ' while it is not in ' .
991 * Builds where clause, for filtering on simple list mode
993 * @param Select $select Original select
997 private function buildWhereClause(Select
$select): void
1003 global $zdb, $login;
1006 if ($this->filters
->email_filter
== self
::FILTER_W_EMAIL
) {
1007 $select->where('(a.email_adh != \'\' OR a.parent_id IS NOT NULL AND parent.email_adh != \'\')');
1009 if ($this->filters
->email_filter
== self
::FILTER_WO_EMAIL
) {
1010 $select->where('(a.email_adh = \'\' OR a.email_adh IS NULL) AND (parent.email_adh = \'\' OR parent.email_adh IS NULL)');
1013 if ($this->filters
->filter_str
!= '') {
1014 $token = $zdb->platform
->quoteValue(
1015 '%' . strtolower($this->filters
->filter_str
) . '%'
1017 switch ($this->filters
->field_filter
) {
1018 case self
::FILTER_NAME
:
1019 if ($zdb->isPostgres()) {
1020 $sep = " || ' ' || ";
1031 $pre . 'LOWER(a.nom_adh)' . $sep .
1032 'LOWER(a.prenom_adh)' . $sep .
1033 'LOWER(a.pseudo_adh)' . $post . ' LIKE ' .
1036 $pre . 'LOWER(a.prenom_adh)' . $sep .
1037 'LOWER(a.nom_adh)' . $sep .
1038 'LOWER(a.pseudo_adh)' . $post . ' LIKE ' .
1043 case self
::FILTER_COMPANY_NAME
:
1045 'LOWER(a.societe_adh) LIKE ' .
1049 case self
::FILTER_ADDRESS
:
1052 'LOWER(a.adresse_adh) LIKE ' . $token
1054 'a.cp_adh LIKE ' . $token
1056 'LOWER(a.ville_adh) LIKE ' . $token
1058 'LOWER(a.pays_adh) LIKE ' . $token
1062 case self
::FILTER_MAIL
:
1065 'LOWER(a.email_adh) LIKE ' . $token
1067 'LOWER(so.url) LIKE ' . $token
1071 case self
::FILTER_JOB
:
1073 'LOWER(a.prof_adh) LIKE ' . $token
1076 case self
::FILTER_INFOS
:
1078 if ($login->isAdmin() ||
$login->isStaff()) {
1079 $more = ' OR LOWER(a.info_adh) LIKE ' . $token;
1082 '(LOWER(a.info_public_adh) LIKE ' .
1083 $token . $more . ')'
1086 case self
::FILTER_NUMBER
:
1087 $select->where
->equalTo('a.num_adh', $this->filters
->filter_str
);
1089 case self
::FILTER_ID
:
1090 $select->where
->equalTo('a.id_adh', $this->filters
->filter_str
);
1095 if ($this->filters
->membership_filter
) {
1096 switch ($this->filters
->membership_filter
) {
1097 case self
::MEMBERSHIP_NEARLY
:
1098 $now = new \
DateTime();
1099 $due_date = clone $now;
1100 $due_date->modify('+30 days');
1102 ->greaterThanOrEqualTo(
1104 $now->format('Y-m-d')
1105 )->lessThanOrEqualTo(
1107 $due_date->format('Y-m-d')
1108 )->equalTo('a.bool_exempt_adh', new Expression('false'));
1110 case self
::MEMBERSHIP_LATE
:
1114 date('Y-m-d', time())
1115 )->equalTo('a.bool_exempt_adh', new Expression('false'));
1117 case self
::MEMBERSHIP_UP2DATE
:
1119 '(' . 'a.date_echeance >= \'' . date('Y-m-d', time())
1120 . '\' OR a.bool_exempt_adh=true)'
1123 case self
::MEMBERSHIP_NEVER
:
1124 $select->where('a.date_echeance IS NULL')
1125 ->where('a.bool_exempt_adh = false');
1127 case self
::MEMBERSHIP_STAFF
:
1128 $select->where
->lessThan(
1129 'status.priorite_statut',
1130 self
::NON_STAFF_MEMBERS
1133 case self
::MEMBERSHIP_ADMIN
:
1134 $select->where
->equalTo('a.bool_admin_adh', true);
1136 case self
::MEMBERSHIP_NONE
:
1137 $select->where
->equalTo('a.id_statut', Status
::DEFAULT_STATUS
);
1142 if ($this->filters
->filter_account
) {
1143 switch ($this->filters
->filter_account
) {
1144 case self
::ACTIVE_ACCOUNT
:
1145 $select->where('a.activite_adh=true');
1147 case self
::INACTIVE_ACCOUNT
:
1148 $select->where('a.activite_adh=false');
1153 if ($this->filters
->group_filter
) {
1155 array('g' => PREFIX_DB
. Group
::GROUPSUSERS_TABLE
),
1156 'a.' . Adherent
::PK
. '=g.' . Adherent
::PK
,
1160 array('gs' => PREFIX_DB
. Group
::TABLE
),
1161 'gs.' . Group
::PK
. '=g.' . Group
::PK
,
1165 '(g.' . Group
::PK
. ' = ' . $zdb->platform
->quoteValue((string)$this->filters
->group_filter
) .
1166 ' OR gs.parent_group = NULL OR gs.parent_group = ' .
1167 $this->filters
->group_filter
. ')'
1171 if ($this->filters
instanceof AdvancedMembersList
) {
1172 $this->buildAdvancedWhereClause($select);
1174 } catch (Throwable
$e) {
1176 __METHOD__
. ' | ' . $e->getMessage(),
1184 * Builds where clause, for advanced filtering on simple list mode
1186 * @param Select $select Original select
1190 private function buildAdvancedWhereClause(Select
$select): void
1194 if (!$this->filters
instanceof AdvancedMembersList
) {
1198 // Search members who belong to any (OR) or all (AND) listed groups.
1199 // Idea is to build an array of members ID that fits groups selection
1200 // we will use in the final query.
1201 // The OR case is quite simple, AND is a bit more complex; since we must
1202 // check each member do belongs to all listed groups.
1204 count($this->filters
->groups_search
) > 0
1205 && !isset($this->filters
->groups_search
['empty'])
1209 foreach ($this->filters
->groups_search
as $gs) { // then add a row for each group
1210 $wheregroups[] = $gs['group'];
1213 $gselect = $zdb->select(Group
::GROUPSUSERS_TABLE
, 'gu');
1217 array('g' => PREFIX_DB
. Group
::TABLE
),
1218 'gu.id_group=g.' . Group
::PK
,
1223 'g.id_group' => ':group',
1224 'g.parent_group' => ':pgroup'
1228 $gselect->group(['gu.id_adh']);
1230 $stmt = $zdb->sql
->prepareStatementForSqlObject($gselect);
1234 foreach ($this->filters
->groups_search
as $gs) { // then add a row for each ig/searched group pair
1235 $gresults = $stmt->execute(
1237 'group' => $gs['group'],
1238 'pgroup' => $gs['group']
1242 switch ($this->filters
->groups_search_log_op
) {
1243 case AdvancedMembersList
::OP_AND
:
1244 foreach ($gresults as $gresult) {
1245 if (!isset($ids[$gresult['id_adh']])) {
1246 $ids[$gresult['id_adh']] = 0;
1248 $ids[$gresult['id_adh']] +
= 1;
1251 case AdvancedMembersList
::OP_OR
:
1252 foreach ($gresults as $gresult) {
1253 $mids[$gresult['id_adh']] = $gresult['id_adh'];
1260 foreach ($ids as $id_adh => $count) {
1261 if ($count == count($wheregroups)) {
1262 $mids[$id_adh] = $id_adh;
1268 //limit on found members
1269 $select->where
->in('a.id_adh', $mids);
1271 //no match in groups, end of game.
1272 $select->where('false = true');
1276 //FIXME: should be retrieved from members_fields
1278 'a.ddn_adh' => 'birth_date',
1279 'a.date_crea_adh' => 'creation_date',
1280 'a.date_modif_adh' => 'modif_date',
1281 'a.date_echeance' => 'due_date',
1282 'ct.date_enreg' => 'contrib_creation_date',
1283 'ct.date_debut_cotis' => 'contrib_begin_date',
1284 'ct.date_fin_cotis' => 'contrib_end_date'
1287 foreach ($dates as $field => $property) {
1288 $bprop = "r{$property}_begin";
1289 if ($this->filters
->$bprop) {
1290 $d = new \
DateTime($this->filters
->$bprop);
1291 $select->where
->greaterThanOrEqualTo(
1296 $eprop = "r{$property}_end";
1297 if ($this->filters
->$eprop) {
1298 $d = new \
DateTime($this->filters
->$eprop);
1299 $select->where
->lessThanOrEqualTo(
1306 if ($this->filters
->show_public_infos
) {
1307 switch ($this->filters
->show_public_infos
) {
1308 case self
::FILTER_W_PUBINFOS
:
1309 $select->where('a.bool_display_info = true');
1311 case self
::FILTER_WO_PUBINFOS
:
1312 $select->where('a.bool_display_info = false');
1314 case self
::FILTER_DC_PUBINFOS
:
1315 //nothing to do here.
1320 if ($this->filters
->status
) {
1323 $this->filters
->status
1328 $this->filters
->contrib_min_amount
1329 ||
$this->filters
->contrib_max_amount
1331 if ($this->filters
->contrib_min_amount
) {
1332 $select->where
->greaterThanOrEqualTo(
1334 $this->filters
->contrib_min_amount
1337 if ($this->filters
->contrib_max_amount
) {
1338 $select->where
->lessThanOrEqualTo(
1340 $this->filters
->contrib_max_amount
1345 if ($this->filters
->contributions_types
) {
1348 $this->filters
->contributions_types
1352 if ($this->filters
->payments_types
) {
1354 'ct.type_paiement_cotis',
1355 $this->filters
->payments_types
1360 count($this->filters
->contrib_dynamic
) > 0
1361 && !isset($this->filters
->contrib_dynamic
['empty'])
1363 foreach ($this->filters
->contrib_dynamic
as $k => $cd) {
1366 if (is_array($cd)) {
1367 //dynamic choice spotted!
1368 $prefix = 'cdfc' . $k . '.';
1369 $qry = 'dfc.field_form = \'contrib\' AND ' .
1370 'dfc.field_id = ' . $k;
1372 $select->where($qry);
1373 $select->where
->in($prefix . $field, $cd);
1375 //dynamic field spotted!
1377 $qry = 'dfc.field_form = \'contrib\' AND ' .
1378 'dfc.field_id = ' . $k . ' AND ';
1379 $field = 'field_val';
1381 $dyn_field = DynamicField
::loadFieldType($zdb, (int)$k);
1383 if ($dyn_field instanceof \Galette\DynamicFields\Boolean
) {
1385 $qry .= $field . ' = ' . (int)$cd;
1387 $select->where($qry);
1388 } elseif ($dyn_field instanceof \Galette\DynamicFields\Date
) {
1389 //dynamic dates are stored in their localized format :/
1390 //use current lang format to query for now
1391 //FIXME works with french formatted date only -_-
1392 if ($zdb->isPostgres()) {
1394 $store_fmt = __("Y-m-d") === 'Y-m-d' ?
'YYYY-MM-DD' : 'DD/MM/YYYY';
1395 $cd = "to_date('" . $cd . "', '" . $store_fmt . "')";
1396 $qry .= "to_date(" . $prefix . $field . ", '$store_fmt')";
1398 $store_fmt = __("Y-m-d") === 'Y-m-d' ?
'%Y-%m-%d' : '%d/%m/%Y';
1399 $cd = "STR_TO_DATE('" . $cd . "', '" . $store_fmt . "')";
1400 $qry .= 'STR_TO_DATE(' . $prefix . $field . ', \'' . $store_fmt . '\') ';
1402 $qry .= $qop . ' ' . $cd;
1403 $select->where($qry);
1405 $qry .= 'LOWER(' . $prefix . $field . ') ' . $qop . ' ';
1406 $select->where($qry . $zdb->platform
->quoteValue('%' . strtolower($cd) . '%'));
1413 count($this->filters
->free_search
) > 0
1414 && !isset($this->filters
->free_search
['empty'])
1416 foreach ($this->filters
->free_search
as $fs) {
1417 $fs['search'] = mb_strtolower($fs['search']);
1419 switch ($fs['qry_op']) {
1420 case AdvancedMembersList
::OP_EQUALS
:
1423 case AdvancedMembersList
::OP_CONTAINS
:
1425 $fs['search'] = '%' . $fs['search'] . '%';
1427 case AdvancedMembersList
::OP_NOT_EQUALS
:
1430 case AdvancedMembersList
::OP_NOT_CONTAINS
:
1432 $fs['search'] = '%' . $fs['search'] . '%';
1434 case AdvancedMembersList
::OP_STARTS_WITH
:
1436 $fs['search'] = $fs['search'] . '%';
1438 case AdvancedMembersList
::OP_ENDS_WITH
:
1440 $fs['search'] = '%' . $fs['search'];
1442 case AdvancedMembersList
::OP_BEFORE
:
1445 case AdvancedMembersList
::OP_AFTER
:
1450 'Unknown query operator: ' . $fs['qry_op'] .
1451 ' (will fallback to equals)',
1461 if (strpos($fs['field'], 'dyn_') === 0) {
1462 // simple dynamic field spotted!
1463 $index = str_replace('dyn_', '', $fs['field']);
1464 $dyn_field = DynamicField
::loadFieldType($zdb, (int)$index);
1465 $prefix = 'df' . $index . '.';
1466 $fs['field'] = 'val';
1469 //handle socials networks
1470 if (strpos($fs['field'], 'socials_') === 0) {
1472 $type = str_replace('socials_', '', $fs['field']);
1474 $fs['field'] = 'url';
1475 $select->where(['so.type' => $type]);
1478 if ($dyn_field && $dyn_field instanceof \Galette\DynamicFields\Boolean
) {
1479 if ($fs['search'] != 0) {
1480 $qry .= $prefix . $fs['field'] . $qop . ' ' .
1483 $qry .= $prefix . $fs['field'] . ' IS NULL';
1485 } elseif (!strncmp($fs['field'], 'bool_', strlen('bool_'))) {
1486 $qry .= $prefix . $fs['field'] . $qop . ' ' .
1489 $fs['qry_op'] === AdvancedMembersList
::OP_BEFORE
1490 ||
$fs['qry_op'] === AdvancedMembersList
::OP_AFTER
1492 if ($prefix === 'a.') {
1493 //dates are OK in the main fields. no cast, just query!
1494 $qry .= $prefix . $fs['field'] . $qop . ' ' .
1495 $zdb->platform
->quoteValue($fs['search']);
1497 //dynamic dates are stored in their localized format :/
1498 //use current lang format to query for now
1499 //FIXME works with french formatted date only -_-
1500 if ($zdb->isPostgres()) {
1501 $store_fmt = __("Y-m-d") === 'Y-m-d' ?
'YYYY-MM-DD' : 'DD/MM/YYYY';
1502 $fs['search'] = "to_date('" . $fs['search'] . "', '" . $store_fmt . "')";
1503 $qry .= "to_date('" . $prefix . $fs['field'] . "', '$store_fmt')";
1505 $store_fmt = __("Y-m-d") === 'Y-m-d' ?
'%Y-%m-%d' : '%d/%m/%Y';
1506 $fs['search'] = "STR_TO_DATE('" . $fs['search'] . "', '" . $store_fmt . "')";
1507 $qry .= 'STR_TO_DATE(' . $prefix . $fs['field'] . ', \'' . $store_fmt . '\') ';
1510 $qry .= $qop . ' ' . $fs['search'];
1513 $field = $prefix . $fs['field'];
1514 if ($zdb->isPostgres()) {
1515 $field = 'CAST(' . $field . ' AS TEXT)';
1517 $qry .= 'LOWER(' . $field . ') ' .
1518 $qop . ' ' . $zdb->platform
->quoteValue($fs['search']);
1521 if ($fs['log_op'] === AdvancedMembersList
::OP_AND
) {
1522 $select->where($qry);
1523 } elseif ($fs['log_op'] === AdvancedMembersList
::OP_OR
) {
1524 $select->where($qry, PredicateSet
::OP_OR
);
1531 * Login and password field cannot be empty.
1533 * If those are not required, or if a file has been imported
1534 * (from a CSV file for example), we fill here random values.
1538 public function emptyLogins(): bool
1543 $zdb->connection
->beginTransaction();
1544 $select = $zdb->select(Adherent
::TABLE
);
1546 array('id_adh', 'login_adh', 'mdp_adh')
1555 new IsNull('login_adh'),
1561 new IsNull('mdp_adh'),
1567 $results = $zdb->execute($select);
1570 if ($results->count() > 0) {
1571 $update = $zdb->update(Adherent
::TABLE
);
1574 'login_adh' => ':login',
1575 'mdp_adh' => ':pass'
1577 )->where
->equalTo(Adherent
::PK
, ':id');
1579 $stmt = $zdb->sql
->prepareStatementForSqlObject($update);
1581 $p = new \Galette\Core\
Password($zdb);
1583 foreach ($results as $m) {
1587 ||
!isset($m->login_adh
)
1588 ||
$m->login_adh
== 'NULL'
1590 $m->login_adh
= $p->makeRandomPassword(15);
1596 ||
!isset($m->mdp_adh
)
1597 ||
$m->mdp_adh
== 'NULL'
1599 $randomp = $p->makeRandomPassword(15);
1600 $m->mdp_adh
= password_hash(
1607 if ($dirty === true) {
1610 'login' => $m->login_adh
,
1611 'pass' => $m->mdp_adh
,
1619 $zdb->connection
->commit();
1620 $this->count
= $processed;
1622 } catch (Throwable
$e) {
1623 $zdb->connection
->rollBack();
1625 'An error occurred trying to retrieve members with ' .
1626 'empty logins/passwords (' . $e->getMessage(),
1634 * Get reminders count based on members state of dues
1636 * @return array<string, mixed>
1638 public function getRemindersCount(): array
1642 $reminders = array();
1644 // Count close to be expired reminders
1645 $select = $zdb->select(Adherent
::TABLE
, 'a');
1648 'cnt' => new Expression('count(a.' . Adherent
::PK
. ')')
1653 array('parent' => PREFIX_DB
. self
::TABLE
),
1654 'a.parent_id=parent.' . self
::PK
,
1660 ->where('a.activite_adh=true')
1661 ->where('a.bool_exempt_adh=false');
1663 $now = new \
DateTime();
1664 $due_date = clone $now;
1665 $due_date->modify('+30 days');
1668 ->greaterThanOrEqualTo('a.date_echeance', $now->format('Y-m-d'))
1669 ->lessThanOrEqualTo('a.date_echeance', $due_date->format('Y-m-d'));
1671 $select_wo_mail = clone $select;
1672 //per default, limit to members who have an email address
1674 '(a.email_adh != \'\' OR a.parent_id IS NOT NULL AND parent.email_adh != \'\')'
1676 $select_wo_mail->where(
1677 '(a.email_adh = \'\' OR a.email_adh IS NULL) AND (parent.email_adh = \'\' OR parent.email_adh IS NULL)'
1680 $results = $zdb->execute($select);
1681 $res = $results->current();
1682 $reminders['impending'] = $res->cnt
;
1684 $results_wo_mail = $zdb->execute($select_wo_mail);
1685 $res_wo_mail = $results_wo_mail->current();
1686 $reminders['nomail']['impending'] = $res_wo_mail->cnt
;
1688 // Count late reminders
1689 $select = $zdb->select(Adherent
::TABLE
, 'a');
1692 'cnt' => new Expression('count(a.' . Adherent
::PK
. ')')
1697 array('parent' => PREFIX_DB
. self
::TABLE
),
1698 'a.parent_id=parent.' . self
::PK
,
1704 ->where('a.activite_adh=true')
1705 ->where('a.bool_exempt_adh=false');
1708 ->lessThan('a.date_echeance', $now->format('Y-m-d'));
1710 $select_wo_mail = clone $select;
1711 //per default, limit to members who have an email address
1713 '(a.email_adh != \'\' OR a.parent_id IS NOT NULL AND parent.email_adh != \'\')'
1715 $select_wo_mail->where(
1716 '(a.email_adh = \'\' OR a.email_adh IS NULL) AND (parent.email_adh = \'\' OR parent.email_adh IS NULL)'
1719 $results = $zdb->execute($select);
1720 $res = $results->current();
1721 $reminders['late'] = $res->cnt
;
1723 $results_wo_mail = $zdb->execute($select_wo_mail);
1724 $res_wo_mail = $results_wo_mail->current();
1725 $reminders['nomail']['late'] = $res_wo_mail->cnt
;
1731 * Get count for current query
1735 public function getCount(): int
1737 return $this->count
;
1741 * Get registered errors
1743 * @return array<string>
1745 public function getErrors(): array
1747 return $this->errors
;
1751 * Get all existing emails
1753 * @param Db $zdb Database instance
1755 * @return array<string,int> ['email' => 'id_adh']
1757 public static function getEmails(Db
$zdb): array
1760 $select = $zdb->select(self
::TABLE
);
1765 $select->where('email_adh != \'\' AND email_adh IS NOT NULL');
1766 $rows = $zdb->execute($select);
1767 foreach ($rows as $row) {
1768 $emails[(string)$row->email_adh
] = (int)$row->{self
::PK
};
1774 * Get current filters
1776 * @return MembersList|AdvancedMembersList
1778 public function getFilters(): MembersList|AdvancedMembersList
1780 return $this->filters
;
1784 * Get members list to instanciate dropdowns
1786 * @param Db $zdb Database instance
1787 * @param Login $login Login instance
1788 * @param ?integer $current Current member
1790 * @return array<int, string>
1792 public function getDropdownMembers(Db
$zdb, Login
$login, ?
int $current = null): array
1795 $required_fields = array(
1803 if ($login->isAdmin() ||
$login->isStaff()) {
1804 $list_members = $this->getList(false, $required_fields);
1805 } elseif ($login->isGroupManager()) {
1806 $list_members = $this->getManagedMembersList(false, $required_fields);
1809 if (count($list_members) > 0) {
1810 foreach ($list_members as $member) {
1813 $members[(int)$member->$pk] = Adherent
::getNameWithCase(
1815 $member->prenom_adh
,
1823 //check if current attached member is part of the list
1824 if ($current !== null && !isset($members[$current])) {
1826 [$current => Adherent
::getSName($zdb, $current, true, true)] +